CHIP-8 — это интерпретируемый язык программирования из середины 70х для упрощения разработки игр под микрокомпьютеры COSMAC VIP и Telmac 1800. Код запускается на виртуальной машине.

Эмуляция

Эмуля́ция (англ. emulation) в вычислительной технике — комплекс программных, аппаратных средств или их сочетание, предназначенное для копирования (или эмулирования) функций одной вычислительной системы (гостя) на другой, отличной от первой, вычислительной системе (хосте)

Архитектура

Память

Доступно 4Кб RAM (4,096 байт). Первые 512 резервируются под нужны интерпретатора.

+---------------+= 0xFFF (4095) End of Chip-8 RAM
|               |
|               |
|               |
|               |
|               |
| 0x200 to 0xFFF|
|     Chip-8    |
| Program / Data|
|     Space     |
|               |
|               |
|               |
+- - - - - - - -+= 0x600 (1536) Start of ETI 660 Chip-8 programs
|               |
|               |
|               |
+---------------+= 0x200 (512) Start of most Chip-8 programs
| 0x000 to 0x1FF|
| Reserved for  |
|  interpreter  |
+---------------+= 0x000 (0) Start of Chip-8 RAM

Реализация

@memory = Array(UInt8).new(4096, 0)

Регистры

Chip-8 имеет 16 регистров общего назначения, регистры имеют формат UInt8. Обращение у ним обозначается как Vx, где x - номер.

Также есть два специальных регистра - общий таймер и таймер звука, которые обновляются с частотой 60Hz. И ещё псевдо регистры - PC (program counter) (указатель но текущую команду) и I (указатель на байт в памяти).

Реализация

@registers = Array(UInt8).new(16, 0) 
@audio_timer = 0_u8
@delay_timer = 0_u8
@i = 0_u16
@pc = 0x200_u16

Стек

Стек может хранить до 16 значений типа UInt16.

Используется для хранения адресов, на которые интерпретатор должен вернуться после выполнения подпрограммы.

@stack = Array(UInt16).new(16)
@stack.push @pc
@stack.pop

Ввод

Клавиатура Chip-8 состоит из 16 клавиш.

|---|---|---|---|
| 1 | 2 | 3 | C |
| 4 | 5 | 6 | D |
| 7 | 8 | 9 | E |
| A | 0 | B | F |
|---|---|---|---|

В рамках реализации эмулятор будем маппить их к

|---|---|---|---|
| 1 | 2 | 3 | 4 |
| Q | W | E | R |
| A | S | D | F |
| Z | X | C | V |
|---|---|---|---|

Дисплей

Дисплей монохромный и имеет разрешение 64x32.

Так же Chip-8 имеет встроенные шрифты представленные в виде 5 цифр UInt16.

“0” Binary Hex
** 11110000 0xF0
* * 10010000 0x90
* * 10010000 0x90
* * 10010000 0x90
** 11110000 0xF0

Интерпретатор

Это ядро эмулятора. В контексте моего эмулятора он занимается:

  • декодированием бинарного кода;
  • сопоставлением с оразцами операций;
  • выполнением соотв. операций.

Опкоды

Опкод (operation code, код операии) - это слово машинного языка, определяющая операцию, которую необходимо выполнить процессору.

Как выглядят

У Chip-8 опкод состоит из 2 байт (UInt16)

Часть опкодов

Bytes Type Call
00E0 Display disp_clear()
00EE Flow return;
1NNN Flow goto NNN;
2NNN Flow *(0xNNN)()
3XNN Cond if(Vx==NN)
4XNN Cond if(Vx!=NN)
5XY0 Cond if(Vx==Vy)
6XNN Const Vx = NN
7XNN Const Vx += NN
8XY0 Assign Vx=Vy
8XY1 BitOp Vx=Vx|Vy
8XY2 BitOp Vx=Vx&Vy
8XY3 BitOp Vx=Vx^Vy
8XY4 Math Vx += Vy
8XY5 Math Vx -= Vy
8XY6 BitOp Vx»=1
8XY7 Math Vx=Vy-Vx
8XYE BitOp Vx«=1
9XY0 Cond if(Vx!=Vy)
ANNN MEM I = NNN
BNNN Flow PC=V0+NNN
CXNN Rand Vx=rand()&NN
DXYN Disp draw(Vx,Vy,N)
EX9E KeyOp if(key()==Vx)
EXA1 KeyOp if(key()!=Vx)
FX07 Timer Vx = get_delay()
FX0A KeyOp Vx = get_key()
FX15 Timer delay_timer(Vx)
FX18 Sound sound_timer(Vx)
FX1E MEM I +=Vx
FX29 MEM I=sprite_addr[Vx]
FX55 MEM reg_dump(Vx,&I)
FX65 MEM reg_load(Vx,&I)

Работа с памятью

Запись программы в память при старте

  def load_program! : Nil
    @file.rewind
    (0...@file.size).each do |i|
      @memory[@start_p + i] = @file.read_bytes(UInt8, IO::ByteFormat::BigEndian)
    end
  end

Чтение опкода

def read_opcode
  (@memory[@pc].to_u16 << 8) | @memory[@pc + 1]
end

Паттерн

Основной паттерн определяющий класс команд: 0NNN. Но NNN может в себя включать данные (аргументы) X (0x0F00), Y (0x00F0), KK(NN) (0x00FF) и N (0x000N).

Дизассемблирование

kk = opcode & 0x00FF
nnn = opcode & 0x0FFF

x = (opcode & 0x0F00) >> 8
y = (opcode & 0x00F0) >> 4
n = opcode & 0x000F

case opcode & 0xF000
  # обработка класса кода
end

Сопоставление и выполнение операций

   case opcode & 0xF000
    when 0x0000
      case opcode & 0x000F
      when 0x0000
        @video.clear!
      when 0x000E
        @pc = @stack.pop
      end
      next_code!
    when 0x8000
      case opcode & 0x000F
      when 0x0000
        log "LD V#{x}, V#{y}"
        @registers[x] = @registers[y]
      when 0x0001
        log "OR V#{x}, V#{y}"
        @registers[x] = @registers[x] | @registers[y]
      when 0x0002
        log "AND V#{x}, V#{y}"
        @registers[x] = @registers[x] & @registers[y]
      when 0x0003
        log "XOR V#{x}, V#{y}"
        @registers[x] = @registers[x] ^ @registers[y]
      when 0x0004
        log "ADD V#{x}, V#{y}"
        @registers[0xF] = @registers[y] > UInt8::MAX - @registers[x] ? 1_u8 : 0_u8
        @registers[x] = @registers[x] &+ @registers[y]
      when 0x0005
        log "SUB V#{x}, V#{y}"
        @registers[0xF] = @registers[x] > @registers[y] ? 1_u8 : 0_u8
        @registers[x] = @registers[x] &- @registers[y]
      when 0x0006
        log "SHR V#{x} {, V#{y}}"
        @registers[0xF] = @registers[x] & 0x1
        @registers[x] = @registers[x] >> 1
      when 0x0007
        log "SUBN V#{x}, V#{y}"
        @registers[0xF] = @registers[y] > @registers[x] ? 1_u8 : 0_u8
        @registers[x] = @registers[y] &- @registers[x]
      when 0x000E
        log "SHL V#{x} {, V#{y}}"
        @registers[0xF] = @registers[x] & 0x80
        @registers[x] = @registers[x] << 1
      end
      next_code!
     end
    end

Работа с вводом со стороны интерпретатора

    when 0xE000
      case opcode & 0x00FF
      when 0x009E
        log "SKP V#{x}"
        @keyboard.pressed?(@registers[x]) ? skip_next_code! : next_code!
      when 0x00A1
        log "SKNP V#{x}"
        @keyboard.pressed?(@registers[x]) ? next_code! : skip_next_code!
      end
    when 0xF000
      case opcode & 0x00FF
      when 0x000A
        log "LD V#{x}, K"
        return unless @keyboard.any_key_pressed?

        @registers[x] = @keyboard.pressed_key.not_nil!.to_u8
      end
    end

Работа ввода

  @keyboard = Array(UInt8).new(16, 0)

  KEYMAP = {
    LibSDL::Keycode::KEY_1 => 0x1,
    LibSDL::Keycode::KEY_2 => 0x2,
    LibSDL::Keycode::KEY_3 => 0x3,
    LibSDL::Keycode::KEY_4 => 0xC,

    LibSDL::Keycode::Q => 0x4,
    LibSDL::Keycode::W => 0x5,
    LibSDL::Keycode::E => 0x6,
    LibSDL::Keycode::R => 0xD,

    LibSDL::Keycode::A => 0x7,
    LibSDL::Keycode::S => 0x8,
    LibSDL::Keycode::D => 0x9,
    LibSDL::Keycode::F => 0xE,

    LibSDL::Keycode::Z => 0xA,
    LibSDL::Keycode::X => 0x0,
    LibSDL::Keycode::C => 0xB,
    LibSDL::Keycode::V => 0xF
  }

  def poll
    case event = SDL::Event.poll
    when SDL::Event::Quit
      exit
    when SDL::Event::Keyboard
      exit if event.sym.escape?

      if KEYMAP[event.sym]?
        key = KEYMAP[event.sym]
        @keyboard[key] = event.keydown? ? 1_u8 : 0_u8
      end
    end
  end

Вывод с точки зрения интерпретатора

    case opcode & 0xF000
    when 0xD000
      log "DRW V#{x}, V#{y}, #{n}"
      @registers[0xF] = @video.draw_sprite(@memory[@i...(@i+n)], @registers[x], @registers[y])
      next_code!
    when 0xF000
      case opcode & 0x00FF
      when 0x0029
        log "LD F, V#{x}"
        @i = char_font_p(@memory[x])

Работа с выводом

  @video_memory = Array(UInt64).new(32, 0)
  @draw_flag = true

  def draw_sprite(sprite_bytes : Array(UInt8), x, y)
    @draw_flag = true
    collision = 0_u8

    sprite_bytes.each_with_index do |sprite_pixel, sprite_line_index|
      line_num = y + sprite_line_index
      (0...8).each do |xi|
        next if (sprite_pixel & (0x80 >> xi)) == 0

        offset = 63 - x - xi
        display_bit_p = 1_u64 << offset
        collision = 1_u8 if (@video_memory[line_num] & display_bit_p) > 0

        @video_memory[line_num] ^= display_bit_p
      end
    end

    collision
  end

  module Colors
    WHITE = SDL::Color[255, 255, 255, 255]
    BLACK = SDL::Color[0, 0, 0, 255]
  end

  def refresh : Nil
    return unless @draw_flag

    @draw_flag = false
    @renderer.draw_color = Colors::GRAY
    @renderer.clear

    @video_memory.each_with_index do |line, line_i|
      (0...64).each do |pix_i|
        offset = 63 - pix_i
        pix_value = (line & (1.to_u64 << offset)) >> offset

        @renderer.draw_color = pix_value == 1 ? Colors::BLACK : Colors::WHITE
        @renderer.fill_rect(pix_i * @pixel_w, line_i * @pixel_h, @pixel_w.to_i32, @pixel_h.to_i32)
      end
    end

    @renderer.present
  end

И Руби, но и не Руби?

Это Crystal!

Crystal — объектно-ориентированный язык программирования общего назначения.
Имея crystal-подобный синтаксис Crystal является компилируемым и статически типизированным языком.

Ссылки

Исходники можно найти по Ссылке