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

от автора

Введение

Здравствуй, мир! Сегодня у нас перевод спецификации языка 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 хранились напрямую в памяти. Современные компьютеры позволяют хранить бинарные данные без надобности вводить их вручную в память. Спецификация 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.

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

ссылка на оригинал статьи https://habrahabr.ru/post/316788/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *