Pull to refresh

Собственная платформа. Часть 0.2 Теория. Интерпретатор CHIP8

Reading time7 min
Views7.2K
Original author: Matthew Mikolay

Введение


Здравствуй, мир! Сегодня у нас перевод спецификации языка CHIP8. Это статья содержит только теоретическую часть.


*COSMAC ELF во всей красе*

COSMAC ELF


Что такое 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. Это доказывает, что программист часто не ограничен языком программирования.


Инструкции CHIP-8 хранились напрямую в памяти. Современные компьютеры позволяют хранить бинарные данные без надобности вводить их вручную в память. Спецификация COSMAC VIP предполагает, что код загружается в памяти со смещением в 512 байтов (0x200). Большинство игр и программ в 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.


Тут можно найти оригинал. Еще чуть-чуть информации тут. Еще практический туториал тут.

Tags:
Hubs:
Total votes 11: ↑10 and ↓1+9
Comments4

Articles