[Перевод] Собственная платформа. Часть 0.2 Теория. Интерпретатор CHIP8
Здравствуй, мир! Сегодня у нас перевод спецификации языка CHIP8. Это статья содержит только теоретическую часть.
Что такое CHIP8?
CHIP8 это интерпретируемый язык программирования, который был разработан Джозефом Вейзбекером (прим. перевод Joseph Weisbecker) в семидесятых для использования в RCA COSMAC VIP. В дальнейшем был использован в COSMAC ELF, Telmac 1800, ETI 660, DREAM 6800. Тридцать одна (35?) инструкция давали возможности для вывода простого звука, монохромной графики в разрешении 64 на 32 пикселя, а также позволяло использовать 16 пользовательских кнопок. Сегодня CHIP-8 часто используется для обучения базовым навыком эмуляции (не интерпретации). Интерпретаторы CHIP-8 часто по ошибке называют эмуляторами. Это связанно с фактом большой схожести CHIP-8 с компьютером.
Из-за его простоты большое количество игр и программ были написаны на CHIP-8. Это доказывает, что программист часто не ограничен языком программирования.
Инструкции CHIP-8 хранились напрямую в памяти. Современные компьютеры позволяют хранить бинарные данные без надобности вводить их вручную в память. Спецификация COSMAC VIP предполагает, что код загружается в памяти со смещение в 512 байтов (0×200). Большинство игр и программ в CHIP-8 во время работы с памятью предполагают именно такое смещение.
Надо отметить, что программы в памяти CHIP-8 хранятся в Big-Endian, предполагая хранение MSB First (Most Significant Byte First — Самый «значимый» байт храниться первым). Инструкции исполняются по два байта последовательно если не было иных инструкций.
Так как инструкции CHIP-8 содержат указатели на данные или инструкции в памяти изменение кода требовало бы изменения адреса в инструкциях. К счастью псевдо-ассемблер решает эту проблему. Большое количество документации к CHIP-8 не содержат описания некоторый инструкций (8XY3, 8XY6, 8XY7 и 8XYE), но будут описаны здесь.
Архитектура
GPR General Purpose Registers (РОН — Регистры общего назначения)
Все арифметические операции используют регистры. В CHIP-8 описаны 16 регистров. Все регистры без знаковые, 8-и битные и могут использоваться в инструкциях принимающие регистры общего назначения в качестве аргумента, но стоит помнить, что некоторые инструкции могут модифицировать последний регистр (V[0xF]) (Регистр переполнения).
Псевдокод:
u8 V[16] <= 0;
Не совершайте моих ошибок: Последний регистр это полноценный 8-и битный регистр, несмотря на использование как спец регистр в некоторых инструкциях. Хотя спецификация рекомендует не использовать последний регистр в операциях.
I (Регистр)
Регистр I это 16-битный регистр. Несмотря на это в нем используется только первые 12 бит.
Псевдокод:
u16 I <= 0;
Упущение в спецификации: Что будет если произойдет переполнение?
PC (Регистр)
Регистр PC это аналогичный регистру I, только указывает на инструкции.
Упущение в спецификации: Что будет если произойдет переполнение?
Стек
В CHIP-8 Описан стек глубиной 12 ячеек. Прямого доступа к стеку нету (PUSH/POP/etc), но есть инструкции вызова и возврата, которые используют стек.
NOTE: Тут Указанно 16 ячеек. А тут — 12.
Инструкции
Все инструкции это шестнадцатеричная запись.
Обозначения смотрите в таблице:
- NNN: Адрес
- NN: 8-и битная константа.
- N: 4-х битная константа.
- X and Y: Регистры
- PC: Program Counter (Счетчик команд?)
- I: 16-и битный указатель (регистр?)
Регистры и арифметика
6XNN — Загрузить в регистр константу.
Самая простая инструкция для регистров это 6XNN
(шестнадцатеричная запись). Где X это регистр, а NN это константа загружаемая в регистр. Например 6ABB — Загрузить в регистр под номером 10 (V[0xA], регистров 16 от нуля до пятнадцати) значение BB
(187).
Псевдокод:
V[X] <= 0xNN
7XNN — Добавить константу к регистру (ADDI)
Добавляет константу NN к регистру под номером X и сохраняет в регистре под номером X. Не меняет регистр переполнения.
V[X] <= V[X] + NN
8XY0 — Сохранить регистр в другой регистр (MOV)
Еще одна инструкция работающая с регистрами. Имеет запись 8XY0
. Где X это номер регистра куда будет скопирован регистр под номером Y.
Псевдокод:
V[X] <= V[Y]
8XY4 — Сложить два регистра (ADD)
Добавляет значение регистра под номером Y к регистру X и сохраняет значение в регистр X. Если переполнение произошло регистр переполнения будет установлено в значение 1. Если переполнения не произошло регистр переполнения будет сброшен в значение 0.
V[X] <= (V[X] + V[Y]) & 0xFF;
V[F] <= (V[X] + V[Y] >= 256);
Не совершайте моих ошибок: Регистр переполнения будет модифицирован в любом случае.
Упущение в спецификации: Что будет если регистр X будет регистром переполнения?
8XY5 — Вычесть из регистра (SUB)
Вычитает из регистра под номером X значение регистра Y и если произошло заимствование (прим. перевод Borrow) установить регистр переполнения в 1. И установить регистр переполнения в 0 если заимствование не произошло.
8XY7 — «Обратное» вычитание (SUB)
Установить регистр под номером X в результат вычитания значения регистра X из регистра Y. И если произошло заимствование установить регистр переполнения в 1. И установить регистр переполнения в 0 если заимствование не произошло.
8XY2, 8XY1 и 8XY3 — Логические операции (AND, OR, XOR)
Установить регистр X в результат операции
8XY2 — Логической «И»,
8XY1 — Логической «ИЛИ»,
8XY3 — Исключающее «ИЛИ»
двух операндов: регистра X и регистра Y. Не модифицирует регистр переполнения.
Не совершайте моих ошибок: Эти операции НЕ МОДИФИЦИРУЮТ регистр переполнения.
NOTE: Здесь нет опечатки. 8XY2 — AND. 8XY1 OR. 8XY3 XOR.
8XY6 — Сдвиг Вправо (Shift Right)
Сохранить в регистр X результат сдвига регистра Y вправо.
Установить регистр переполнения в значение младшего бита регистра Y.
Не совершайте моих ошибок: Результат сдвига регистра Y сохраняется в регистр X, а не в регистр Y. Хотя многие интерпретаторы это правило игнорируют.
8XYE — Сдвиг Влево (Shift Right)
Сохранить старший бит регистра Y в регистр переполнения.
Сохранить результат сдвига регистра Y в регистр X.
CXNN — Рандом Случайное число
Установить значение регистра X в результат логической «И» константы NN и рандомного случайного числа.
Управления исполнением (Прим. перевод «flow control»)
1NNN — Прыжок в NNN
Ставит PC в значение NNN.
Следующая инструкция будет исполнена из адреса NNN
BNNN — Прыжок в NNN+V0
Ставит PC в значение NNN+V0.
Следующая инструкция будет исполнена из адреса NNN+V0
2NNN — Вызов функции (Call Subroutine)
Вызывает функции по адресу 2NNN. В стек записывается значение PC + 2.
00EE — Возврат из функции (Return from Subroutine)
Регистр PC будет установлен в значение последнего элемента стека.
3XNN — Пропустить инструкцию, если константа и регистр равны.
Пропускает инструкцию (PC+4) если константа NN и регистр X равны. Иначе не пропускать (PC+2).
5XY0 — Пропустить инструкцию, если оба регистра равны.
Пропускает инструкцию (PC+4) если регистр Y и регистр X равны. Иначе не пропускать (PC+2).
4XNN — Пропустить инструкцию, если константа и регистр не равны.
Пропускает инструкцию (PC+4) если константа NN и регистр X НЕ равны. Иначе не пропускать (PC+2).
9XY0 — Пропустить инструкцию, если регистры не равны.
Пропускает инструкцию (PC+4) если регистр Y и регистр X НЕ равны. Иначе не пропускать (PC+2).
Таймеры
В CHIP-8 есть два таймера. Один таймер отсчитывает задержку (прим. перевод Delay Timer), другой «Звуковой таймер» (прим. перевод Sound Timer) воспроизводит звук пока значение таймера больше нуля. Оба таймера уменьшают собственные значения с частотой 60Hz (60 раз в секунду). Из таймера задержек можно читать, а из «Звукового таймера» нельзя.
FX15 — Установить значение таймера задержек в значения регистра X.
FX07 — Установить значение регистра X в значение таймера задержек.
Здесь все понятно :)
FX18 — Установить значение звукового таймера в значения регистра X.
NOTE: Стоит помнить, что в COSMAC VIP указанно, что значение 1 не даст никакого эффекта.
Ввод (Keypad)
Для ввода используется 16 кнопок 0–9 и A-F. Упущение в спецификации: Что будет если в кнопки под этим номером не будет. Например:17.
FX0A — Ожидание нажатия.
Приостанавливает исполнение до нажатия клавиши указанной в регистре X.
Не совершайте моих ошибок: Не любых нажатий, а только нажатий именно этой клавиши.
EX9E — Пропустить следующую инструкцию если кнопка соответствующая значению регистра X нажата.
EXA1 — Пропустить следующую инструкцию если кнопка соответствующая значению регистра X не нажата.
Здесь думаю все понятно.
Регистр I
ANNN — Записать в регистр I значение NNN.
FX1E — Добавить значение регистра X к регистру I.
Регистр переполнения будет установлено в 1 если произошло переполнение, иначе в 0.
Графика и спрайты
NOTE: Графика будет подробно описана в практической части.
DXYN — Нарисовать спрайт.
Рисуем спрайт размером N байт (Не нулевое значение) в позиции на экране: (Vx, Vy). Спрайт находиться в памяти по адресу I. Спрайт рисуется логически исключающим ИЛИ (XOR). Если мы перерисовали пиксель (1,1 → 0) регистр переполнения будет установлен в 1, иначе в 0.
Упущение в спецификации:
- Что будет если N == 0.
- Что будет если VX >= 64.
- Что будет если VY >= 32.
- Где 0,0 или 64,32 и т.д.
- Какого цвета пиксели.
NOTE: Чуть больше информации тут.
00E0 — Очистить экран.
FX29 — Установить значение I в адрес спрайта для числа указанного в регистре X.
Спрайт храниться в первых 512 байтах.
Эта таблица в C:
unsigned char chip8_fontset[80] =
{
0xF0, 0x90, 0x90, 0x90, 0xF0, // 0
0x20, 0x60, 0x20, 0x20, 0x70, // 1
0xF0, 0x10, 0xF0, 0x80, 0xF0, // 2
0xF0, 0x10, 0xF0, 0x10, 0xF0, // 3
0x90, 0x90, 0xF0, 0x10, 0x10, // 4
0xF0, 0x80, 0xF0, 0x10, 0xF0, // 5
0xF0, 0x80, 0xF0, 0x90, 0xF0, // 6
0xF0, 0x10, 0x20, 0x40, 0x40, // 7
0xF0, 0x90, 0xF0, 0x90, 0xF0, // 8
0xF0, 0x90, 0xF0, 0x10, 0xF0, // 9
0xF0, 0x90, 0xF0, 0x90, 0x90, // A
0xE0, 0x90, 0xE0, 0x90, 0xE0, // B
0xF0, 0x80, 0x80, 0x80, 0xF0, // C
0xE0, 0x90, 0x90, 0x90, 0xE0, // D
0xF0, 0x80, 0xF0, 0x80, 0xF0, // E
0xF0, 0x80, 0xF0, 0x80, 0x80 // F
};
Взято отсюда.
FX33 — Сохранить значения регистра в двоично десятичном формате в I, I+1 и I+2.
Смотрите: Двоично десятичный код (прим. перевод BCD — Binary-Coded Decimal)
Регистры и память
FX55 — Сохранить значения регистров V0 до VX включительно в память начиная адреса I.
После выполнения регистр I будет равен I + X + 1. Хотя некоторые интерпретаторы игнорируют это правило.
FX65 — Загрузить регистры V0 до VX включительно в значения сохраненный в памяти начиная с адреса I.
После выполнения регистр I будет равен I + X + 1. Хотя некоторые интерпретаторы игнорируют это правило.
Все остальное
На какой частоте работает интерпретатор?
Про это ничего не могу найти в оригинале. Из интернета были получены самые разные частоты: 1000Hz, 840Hz, 540Hz, 500Hz, даже 60Hz.
Что будет если прыжок (Jump) будет не выровнен?
Никакой информации об этом я не нашел, но думаю что инструкция будет загружаться и исполняться.
Что будет если прочитать или записать из первых 512 байт?
Снова ничего не найдено. Думаю надо отдавать 0, а при записи игнорировать.
Конец
На этом конец. При опечатках писать в личные сообщения. Буду рад любым замечаниям. Практическая часть находиться в процессе создания. Практическая часть будет на C (не C++) и SDL2.
Тут можно найти оригинал. Еще чуть-чуть информации тут. Еще практический туториал тут.