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 является компилируемым и статически типизированным языком.
Ссылки
Исходники можно найти по Ссылке