Как стать автором
Обновить

Принципы эмуляции на основе CHIP-8

Время на прочтение9 мин
Количество просмотров4.7K
Считается, что прежде чем начинать эмулировать сложные системы, нужно начать с чего-то простого, например с Chip-8. В этой статье я попытаюсь рассмотреть все аспекты того, как можно написать свою реализацию этого языка в виртуальной машине. Пойдет совсем любой язык программирования, но из-за простоты я выберу Delphi.

Подождите, не кидайтесь сразу в редактор, сначала потребуется ручка и блокнот, куда мы запишем всю важную для себя информацию. Начинается все с ее поиска. Прежде всего, это конечно Google. После получаса хождения по ссылкам мы можем увидеть, как много реализаций уже написано под любую систему, выбирай не хочу, но мы не будем подсматривать в чужие исходники, а напишем свое.


Историческая справка



CHIP-8 это языковой интерпретатор, которые использовался в конце 70-ых, начале 80-ых на некоторых маленьких коммерческих компьютерах, таких как RCA's TELMAC 1800 и COSMAC VIP, и дешевых «создай-сам» компьютеров того времени как ETI 660 и DREAM 6800…

CHIP-8 позволял легко программировать игры. TELMAC 1800 и COSMAC VIP базировались на RCA CDP-1802 процессорах. Оба шли с аудио кассетой содержащей более 12 игр, датированных 1977 годом. У интерпретатора было менее 40 команд, включавших математические, управление потоком данных, графикой и звуком.

Интерпретатор должен был быть очень маленьким, из-за лимитов памяти в этих компьютерах: COSMAC VIP имел 2Kb (но мог быть расширен до 32Kb), а TELMAC имел 4Kb. CHIP-8 был длиной всего в 512 байт.
Простота языка позволяла создать Pong, Brix, Invaders и Tank в которые мы играли в самом начале видео игр. Хороший программист мог поместить эти игры в менее чем 256 байт.

Вот короткая история о CHIP-8 одного из пользователей DREAM-6800:
"...the DREAM и ETI 660 появились в Австралийских Электронных Журналах как проекты к сборке. Объединяло эти компьютеры и их невероятно низкая цена (около $100), использование шестнадцатеричной клавиатуры, способность воспроизводить очень ограниченную графику 64 x 32 пикселей (ETI 660 могла получить 64 x 48 или 64 x 64 при ее модификации) передаваемую на телевизор, около одного килобайта оперативной памяти, и способность запускать псевдо язык высокого уровня, называемый CHIP-8 (разработанный RCA для демонстрации графики COSMAC, как мне кажется).

Как то однажды мой старший брат собрал DREAM 6800. Что это был за компьютер! Вместе со статьями по сборке DREAM & ETI 660 были горы листингов игр для CHIP-8. Некоторые игры были менее 200 байт или около того, так что ввод их не занял бы вечность. И это были классные игры. Они не были медленными. Так CHIP-8 был очень хорошо разработан для классических TV игр.
"
Пол Хайтер (Автор CHIP-8 интерпретатора для Amiga)

Позднее CHIP-8 использовался в начале 90-ых на калькуляторах HP48, потому что не существовало способа писать на нем игры быстрее. Почти все оригинальные игры от CHIP-8 работали и с CHIP48 интерпретатором, но было написано и множество новых.
Потом была выпущена новая версия языка — SUPER-CHIP. Он имел все возможности стандартного, но мог уже оперировать разрешением 128х64.

Архитектура



Все программы в CHIP-8 начинаются по адресу 200h, исключая ETI-660, у которого начало в 600h. Так сделано, потому что по адресу 000h-1FFh располагается сам интерпретатор языка.

Вся память полностью адресуема и доступна. Так как инструкции занимаю 16-бит, они обычно имеют четные адреса, а в случае если какие-то 8 бит вставлены внутри кода, их адреса становятся нечетными.

Исходя из 12 бит, используемых на адрес памяти, можно посчитать, что максимальный объем памяти без ухищрений мог составлять 4096 байт (000h-FFFh). Однако адреса F00h-FFFh занимает видео память, а EA0h-EFFh используется для хранения стека и внутренних переменных CHIP-8.

Интерпретатор использует 16 регистров общего назначения объемом в 8 бит. Они имеются как V0..VF. Причем VF используется в качестве флага для арифметических операций, в случае переноса, и детектора коллизий при рисовании спрайтов.

Так же есть 1 адресный регистр (I) размером 16 бит. Так как памяти всего 4 килобайта, интерпретатор использовал лишь его младшие 12 бит. Однако старшие 4 бита могли быть использованы для функции загрузки шрифта, так как шрифт располагался по адресу 8110.

Помимо регистров существовало 2 таймера. Один – таймер задержки, и один – звуковой таймер. Оба имели длину в 8 бит и уменьшали свое содержимое 60 раз в секунду, если не были в тот момент нулевыми. То есть имели частоту в 60 герц. Если звуковой таймер имел значение отличное от нуля, он воспроизводил звук.
Размерность стека осталась неизвестной, однако приято делать ее в 16 уровней(2х16 байт).

Графика отрисовывается спрайтами 8 на 1..15 пикселей, которые кодируются байтами. Начало координат в верхнем левом углу и начинается в точке 0. Все координаты положительные и считаются методом остатка от деления на 64 или 32 соответственно. Вывод на экран производится в режиме XOR. Если один или более пикселей очищаются (меняют свой цвет с 1 на 0) регистр VF устанавливается в 01h и в 00h в противном случае. Chip-8 имеет 4х5 пикселей шрифт, содержащий символы 0-9 и A-F.

Для наглядности рассмотрим пример спрайта и его кодирование. Возьмем спрайт 8х5.



Получим следующий набор байт: С0 A0 С0 A0 С0
Что бы стало совсем понятно возьмем еще 1 спрайт 8х8.



Спрайт займет 8 байт и будет иметь следующую структуру: С0 60 30 18 0С 06 03
Клавиатура для CHIP-8 16-ая и имеет такой внешний вид:



Об удобстве и целесообразности такой клавиатуры можно только спорить и рассуждать. Но отходить от оригинала мы не будем, потому что все разрабатывалось именно под такую клавиатуру.

Система команд



Следует сразу сказать что в качестве NNN мы будем обозначать адрес, KK – 8 битная константа, X и Y – 4 битные константы.
Теперь рассмотрим список команд.

0NNN Syscall nnn Вызов инструкции машинной инструкции процессора 1802 с кодом NNN.
00CN* scdown n Скролирование экрана вниз на N строк.
00FB* Scright Скролирование экрана на 4 пикселя вправо
00FС* Scleft Скролирование экрана на 4 пикселя влево
00FD* Выход из эмулятора
00FE* Low Установить графический режим CHIP-8 (64x32)
00FF* High Установить графический режим SUPER CHIP (128x64)
00E0 Cls Очистить экран
00EE Rts Вернуться из подпрограммы
1NNN jmp nnn Перевести выполнение программы на адрес NNN
2NNN jsr nnn Вызов функции по адресу NNN. Предыдущий адрес помещается в стек.
3XKK skeq vx,kk Пропустить следующую инструкцию (2 байта) если VX=KK
4XKK skne vx,kk Пропустить следующую инструкцию (2 байта) если VX<>KK
5XY0 skeq vx,vy Пропустить следующую инструкцию если VX=VY
6XKK mov vx,kk Записать KK в регистр VX
7XKK add vx,kk Записать VX+KK в регистр VX (По информации с Wikipedia и других источников не воздействует на флаг)
8XY0 mov vx,vy В VX записать значение регистра VY
8XY1 or vx,vy VX = VX OR VY.
8XY2 and vx,vy VX = VX AND VY
8XY3 xor vx,vy VX = VX XOR VY (недокументированна в оригинальных документах)
8XY4 add vx,vy VX = VX + VY. В VF = перенос (carry).
8XY5 sub vx,vy VX = VX – VY. (*) VF = NOT borrow.
8X06 shr vx VX = VX SHR 1 (VX=VX/2), VF = перенос
8XY7 sub vy,vx VX = VY — VX, VF = not borrow (*) (недокументирована в оригинальных документах)
8XYE shl vx VX = VX SHL 1 (VX=VX*2), VF = перенос
9XY0 skne vx,vy Пропустить следующую инструкцию если VX <> VY
ANNN mov I,nnn I = NNN
BNNN Jmi NNN Перевести выполнение программы на NNN + V0
CXKK Rand vx,kk VX = (случайное число 0..255) AND KK
DXYN Draw vx,vy,n Отрисовать спрайт высотой N (при N=0 считать N=16) и шириной 8 по координатам (VX,VY) начинающийся в памяти по адресу содержащемуся в регистре I. VF = collision.
EX9E Skpr vx Пропустить следующую инструкцию, если клавиша с номером содежащимся в VX нажата.
EXA1 Skup vx Пропустить следующую инструкцию, если клавиша с номером содежащимся в VX не нажата.
FX07 Gdelay vx VX = Delay timer
FX0A Key vx Ждем нажатия кнопки и складываем ее в VX.
FX15 Sdelay vx Delay timer = VX
FX18 Ssound vx Sound timer = VX
FX1E Add I,vx I = I + VX
FX29 Font vx Поместить в I адрес спрайта шрифта 4 x 5 шестнадцатеричного символа содержащегося в VX
FX33 Bcd vx Поместить BCD представление VX в память по адресам I..I+2. Например если в VX содержится 4Fh то в памяти будет записано 00h 07h 09h то есть десятичное представление 4Fh
FX55 Save vx Сохраняет регистры V0...VX в памяти, начинающейся по адресу I
FX65 Load vx Загружает регистры V0...VX из памяти, начинающейся по адресу I
FX75* Ssave vx Сохраняет V0...VX (X<8) в HP48 flags
FX85* Sload vx Загружает V0...VX (X<8) из HP48 flags

* — значит команда актуальна только для SUPER CHIP интерпретатора.

(*): Когда происходит VX — VY, VF устанавливается в отрицание заема. Это значит что если VX больше или равно VY, VF будет установлено в 01, так как заем = 0. If VX меньше чем VY, VF устанавливается в 00, так как заем = 1.

Подготовка базовых спрайтов



Пользуясь листочком бумаги и ручкой, набросаем спрайты шрифта. Получится примерно вот такой набор:



Пользуясь им, запишем байты значения каждого спрайта.

Принципы эмуляции



Считается, что вся эмуляция должна происходить в цикле, примерно такого типа.

Repeat
//do emulation
Until quit_pr;


Здесь quit_pr – считается признаком остановки эмуляции, и изменяться может не только снаружи, но и изнутри эмуляции.

Одним из самых важных моментов, которые следует соблюдать, является соблюдение частот работы оборудования и оптимизация. Если неверно описать эмуляцию даже такого простого языка как CHIP-8 можно получить очень низкую производительность и крайне высокие затраты процессорного времени, правильная же эмуляция даст достаточную производительность при минимальных затратах.

Используя Delphi, я решил разработать эмуляцию системы в качестве класса и дал ему название «TCpu1802», на основе модели процессора, использовавшегося внутри данной системы. Рассмотрим основную для него процедуру run.

procedure tcpu1802.run;
begin
delaytimer.Enabled:=true;
soundtimer.Enabled:=true;
drawtimer.Enabled:=true;
work:=true;
repeat
  useopcode;
  if cpumulty<>0 then
    if (round(cycle) mod (2))=0 then sleep(cpumulty);
  application.ProcessMessages;
  until work=false;
  delaytimer.Enabled:=false;
soundtimer.Enabled:=false;
drawtimer.Enabled:=false;
end;
 


Сначала, при запуске, мы активируем два стандартных для CHIP-8 таймера и один, введенный самостоятельно, для перерисовки экрана. Cycle – переменная содержащая текущий цикл команд, вызов каждой команды увеличивает cycle на единицу. Каждое второе действие мы будем пытаться приостанавливать работу интерпретатора на cpumulty миллисекунд. Это необходимо для того, что бы не происходило превышение скорости.

Таймеры Soundtimer и Drawtimer имеют интервал 17 миллисекунд, то есть работают с частотой 58,82 герца, что максимально приближено к оригиналу.

Задержку Drawtimer делаем равной 10 миллисекунд. То есть в среднем отрисовка будет выполняться каждую 3-4 команды интерпретатора. Выделение на таймер сделано для того что бы не засорять процесс выполнения интерпретатора и позволяет штатными средствами выполнить распараллеливание процессов.

Посмотрим на список переменных, который нам понадобится для выполнения самой эмуляции:

keycode:byte; // код нажатой клавиши
 memory:array[0..8191] of byte; // 8 килобайт оперативной памяти, выделено из-за расположения шрифтов
 stack:array[0..255] of word; // стек
 stacksize:byte; // текущий развер стека
 videoarray:array[0..2047] of boolean; //видео массив
 mask:array[0..2047] of boolean; // маска видео массива
 Vreg:array[0..15] of byte; // - регистры V0..VF 
 Ireg:word; // 2 byte адресный регистр
 CodeSender:word; //расположение следующего opcode в памяти
sound:boolean; // воспроизводится ли сейчас звук?


Согласно архитектуре видеопамять располагается в том же объеме оперативной памяти, что и все остальное, однако, постоянные чтение и запись туда, рассматривая байты и анализируя их, заняла бы много процессорных ресурсов и является нецелесообразной, однако, допуская что какая то программа может что то читать непосредственно оттуда, в случае если происходит чтение из той области памяти, мы можем просто вызывать дополнительную процедуру, названную mirrorvideomem, позволяющую записать туда текущее содержимое видеопамяти. Такое решение эффективно, хоть и вызывает несколько большее использование оперативной памяти. Аналогично происходит и процедура mirrorstack.

procedure tcpu1802.mirrorvideomem;
var
 i,j:integer;
 tmp:byte;
begin
 if (Ireg>=$F00) and (Ireg<=$FFF) then
 begin
   for i:=0 to 255 do
   begin
    tmp:=0;
    for j:=0 to 7 do
    begin
     if videoarray[i*j] then tmp:=tmp+1;
     tmp:=tmp shl 1;
    end;
    memory[$F00+i]:=tmp;
   end;
 end;
end;
 
 
procedure tcpu1802.mirrorstack;
var
 n,i:integer;
begin
 if (Ireg>=$EA0) and (Ireg<=$EFF) then
 begin
   if stacksize>12 then n:=11 else n:=stacksize-1;
  for i:=0 to n do
   begin
    memory[$EFF-i*2]:=stack[i] div 256;
   memory[$EFF-i*2+1]:=stack[i] mod 256;
   end;
  end;
end;


Прежде чем писать процессор можно заметить, что каждая команда условно разбивается на 4 раздела по 4 бита. Для работы с ними введен тип данных opcode (type opcode = array[0..3] of byte;).

Не буду останавливаться на преобразовании и чтении очередных данных из памяти, однако, обращу внимание на структуру процедуры run, выполняющей эмуляцию. Она в целях оптимизации использует не «if then else», а «case», и более того ряд команд позволяет использовать их с помощью ассемблера, что еще больше ускоряет работу эмуляции. Вот пример такой команды:

if op[3]=1 then
   begin
tmp:=Vreg[op[1]];
     tmp2:=Vreg[op[2]];
     asm
      mov ah,[tmp]
      or ah,[tmp2];
      mov [tmp],ah;
     end;
     Vreg[op[1]]:=tmp;
     used:=true;
     codesender:=codesender+2;
   end;


Теперь, рассмотрев все моменты эмуляции, можно написать собственную эмуляцию CHIP-8. Надеюсь, моя статья вам понравилась и была полезна. Спасибо за чтение.

Используемые статьи:
A CHIP-8 / SCHIP emulator
By David WINTER (HPMANIAC)

Wikipedia

Исходник модуля:
исходник

Именно за нее я получил приглашение от пользователя nsinreal, за что ему огромное спасибо.

Просьба не считать «копипастом» статьи, появившейся за час до опубликования этой, вследствие того что не мог отправить. Прошу считать что мы не видели статьи друг-друга, в следствие разного содержания модулей и разного подхода к эмуляции, заранее спасибо.
Теги:
Хабы:
+16
Комментарии11

Публикации

Изменить настройки темы

Истории

Ближайшие события