Меню Рубрики

Как написать программу эмулятор

Создаем эмулятор приставки

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

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

Конец

Как бы это странно не было, а начну я с конца. Вот такая программа

OPTION BINARY ; We want a binary file, not an HP48 one.
ALIGN OFF ; And we don’t want auto alignement, as some
; data can be made of bytes instead of words.

LOOP :
LD I , LEFT ; We draw a left line by default, as the random number
; is 0 or 1. If we suppose that it will be 1, we keep
; drawing the left line. If it is 0, we change register
; I to draw a right line.

RND V2 , 1 ; Load in V2 a 0. 1 random number

SE V2 , 1 ; It is 1 ? If yes, I still refers to the left line
; bitmap.

LD I , RIGHT ; If not, we change I to make it refer the right line
; bitmap.

DRW V0 , V1 , 4 ; And we draw the bitmap at V0, V1.

ADD V0 , 4 ; The next bitmap is 4 pixels right. So we update
; V0 to do so.

SE V0 , 64 ; If V0==64, we finished drawing a complete line, so we
; skip the jump to LOOP, as we have to update V1 too.

JP LOOP ; We did not draw a complete line ? So we continue !

LD V0 , 0 ; The first bitmap of each line is located 0, V1.

ADD V1 , 4 ; We update V1. The next line is located 4 pixels doan.

SE V1 , 32 ; Have we drawn all the lines ? If yes, V1==32.
JP LOOP ; No ? So we continue !

FIN : JP FIN ; Infinite loop.

RIGHT : ; 4*4 bitmap of the left line

DB $1 .
DB $ . 1 .
DB $ .. 1 .
DB $ . 1 .

LEFT : ; 4*4 bitmap of the right line
; And YES, it is like that.
DB $ .. 1 .
DB $ . 1 .
DB $1 .
DB $ . 1 .

занимающая 38 байт и в скомпилированном виде выглядящая так


должна в конечном итоге выполниться в нашем эмуляторе и вывести на экран примерно такую картинку:

С концом покончили, переходим к немного нудной, но необходимой теории.

Архитектура

Итак, что же представляет собой игровая платформа CHIP-8? Владеющие английским языком могут ознакомиться с подробной статьей в википедии, а я же попробую пересказать основные моменты своими словами.

CHIP-8 – это интерпретируемый язык программирования, созданный в середине 70-х годов для игровых приставок COSMAC VIP и Telmac 1800. Программы, написанные и скомпилированные для CHIP-8, выполняются на самих приставках в виртуальных машинах. Ну, по современной аналогии это что-то вроде Java байт-кода. Я же вообще советую забыть на время создания эмулятора о том, что это интерпретируемый язык, и считать, что мы эмулируем железную платформу – некий процессор со своим набором команд. Далее, когда я буду говорить “приставка”, я буду подразумевать CHIP-8.

Наша приставка имеет память, процессор, устройство видео вывода, звук и конечно устройство ввода. Рассмотрим все компоненты подробнее:

Приставка имеет 4Kb основной памяти (RAM). Память начинается со смещения 200h и заканчивается смещением FFFh соответственно. Почему память для программ начинается со смещения 200h? Все очень просто – первые 512 байт памяти в оригинальных приставках как раз занимает интерпретатор языка CHIP-8 в машинных кодах того процессора, на котором построена приставка.

В CHIP-8 существует шестнадцать 8-битных регистров данных с именами V0… VF. Регистр VF отвечает за флаг переноса (carry flag) при операциях сложения/вычитания. Также в приставке имеется 16-битный адресный регистр I.

Стек используется для сохранения адреса возврата, когда завершается выполнение подпрограммы. У оригинальной версии приставки размер стека составляет 48 байт, что соответствует двенадцати уровням вложения подпрограмм. Поскольку мы не ограничены в ресурсах, мы будем использовать 16 уровней вложений. Так делает большинство CHIP-8 эмуляторов.

В приставке присутствуют два 8-битных таймера, они оба уменьшаются с частотой 60 Гц, пока не достигнут нуля.
Delay timer: Этот таймер используется для различных задержек в играх, его значение можно читать/изменять с помощью команд.
Sound timer: Когда значение таймера отлично от нуля, выводится пищащий звук.

Ввод осуществляется с помощью 16 клавиш. В оригинальной приставке клавиши имеют коды от 0h до Fh.Если мы эмулируем на компьютере, то удобнее всего использовать правую NumPad часть клавиатуры, ту, где находятся цифры 0-9 и NumLock. Клавиши ‘8’, ‘4’, ‘6’, и ‘2’ обычно используются для перемещения, хотя и не всегда так. Это зависит от игры.

В нашей приставке разрешение экрана 64×32 пикселя, один цвет (монохром). Вывод реализован с помощью спрайтов, которые всегда имеют ширину 8 пикселей и могут иметь длину от 1 до 15 пикселей. Если при рисовании спрайт накладывается на другой спрайт, то в точке наложения цвет инвертируется, а регистр VF (carry flag) принимает значение 1. Иначе он принимает значение 0.

Как выше уже было замечено, играется противный пищащий звук, если значение Sound timer отлично от нуля. Я думаю, звук мы реализовывать вообще не будем, не люблю эти бипы.

Наш процессор (CHIP-8 на самом деле) имеет ровно 35 команд, каждая команда всегда имеет длину два байта. Здесь таблицу команд не буду перепечатывать, она есть в википедии. Можно разобрать несколько примеров оттуда, например:
00E0 Clears the screen. – когда встретим в коде 00E0, просто очистим экран.
6XNN Sets VX to NN. – установить регистр VX в значение NN. Например, если встретили команду 635A, значит нужно в регистр V3 записать значение 5Ah.

Практика

Из рассмотренного выше видно, что эта платформа как нельзя лучше подходит для начала изучения принципов работы эмуляторов. Здесь у нас отсутствуют хитрые маскируемые и не маскированные прерывания, нет кучи периферии с портами ввода-вывода, нет сложных таймеров и так далее. Знай, читай себе команды по два байта из файла, сравнивай их с опкодами да и выполняй что требуется. Да и команд то всего ничего – 35 штук. Есть и подводные камни, а куда без них? Ну что ж, давайте начнем. А начнем мы пожалуй с памяти.

Понятно, что первым делом при запуске эмулятора мы должны проинициализировать нашу виртуальную машину. То есть очистить память, стек, регистры и видеопамять. Как я уже писал выше, смещение, по которому мы будем загружать нашу эмулируемую программу равно 200h. До этого, то есть со смещения 000h до 1FFh, должен находиться оригинальный интерпретатор. В нем, помимо всего прочего, присутствует маленький шрифт, который начинается со смещения 000h и до 050h и занимает 80 байт. Его можно увидеть в исходных кодах моего эмулятора. Да, прошу прощения за свой французский Delphi, но программирую я на нем, не обессудьте. Для простоты я создал такую структуру:

Display : Array [ 0..64 * 32-1 ] of Byte ; //video memory
Memory : Array [ 0..4095 ] of Byte ; //RAM memory
Stack : Array [ 0..15 ] of Word ; //stack
Registers : Array [ 0..15 ] of Byte ; //registers
rI : Word = $ 200 ; // I register
SP : Byte = 0 ; // stack counter
PC : Word = $ 200 ; // mem offset counter
delay_timer : Byte = 255 ; // delay timer;
sound_timer : Byte = 255 ; // sound timer;

Итак, в начале мы заполняем нулями все массивы, затем копируем шрифт (Font: array [1..80] of byte) в массив Memory начиная с нуля и инициализируем все значения:

FillChar ( Memory , 4096 , 0 ) ; // очищаем основную память
Move ( Font , Memory , 80 ) ; // копируем в нее шрифт по смещению 000h
FillChar ( Stack , 16 , 0 ) ; // очищаем стек
FillChar ( Registers , 16 , 0 ) ; // сбрасываем регистры в ноль

rI := $ 200 ; // адресный регистр I на начало программы
PC := $ 200 ; // смещение массива

SP := 0 ; // счетчик стека
delay_timer := 0 ; // таймеры в нули
sound_timer := 0 ;

Теперь все подготовлено, можно прочитать в память эмулируемую программу по смещению 200h и браться за интерпретацию кодов. Здесь придется немножко вспомнить, кто такие биты, и как их извлекать из байтов и слов (word). Для простоты я создал процедуру ExecuteOpcode(opcode: word), в которую передается опкод из двух байт, интерпретируется и выполняется. Чтобы понять смысл, можно сверятся с таблицей команд из википедии.

Procedure ExecuteOpcode ( opcode : word ) ;
Begin
case ( op_code and $F000 ) shr 12 of // выделяем из опкода первые 4 бита
$00 : Begin // опкод начался с нуля
Case op_code and $00FF of
// Это у нас опкод 00E0 — очистка экрана
$E0 : Begin
//Делаем дела, то есть тупо очищаем экран
exit;
End ;
// А это — 00EE — выход из процедуры
$EE : Begin
// Восстанавливаем из стека адрес, прыгаем на него
exit;
End ;
End ;
// А сюда попадем, если опкод начался с нуля, но не закончился ни E0, ни EE
// Поэтому либо трапаемся, либо выводим сообщение Invalid Opcode
exit;
End ; //конец проверка на нулевой опкод
$01 : Begin // первые четыре бита опкода равно 1 (опкод начался с единицы)
// Это JMP, jump. Прыгаем на нужный адрес
PC := op_code and $0FFF;
exit;
End ;
$02 : Begin // первые четыре бита опкода равны 2 (опкод начался с двойки)
// Вызываем подпрограмму.
// увеличиваем указатель стека
// заносим в стек текущий адрес
// и пыгаем на подпрограмму
End ;
//
// Так продолжается до опкода, который начинается с 7.
//

$08 : Begin // опкод начался с 8. Здесь нужно смотреть на 4 последних бита
case op_code and $000F of // последние 4 бита опкода
// mov vx, vy
$00 : Begin
// Занесем в регистр VX значение VY
exit;
End ;
// or vx, vy
$01 : Begin
// VX = VX or VY
exit;
End ;
//
// так продолжается до 0E
//

End ; // конец проверки последних 4 бит опкода
// сюда попадаем, если Invalid Opcode
exit;
End ; // конец проверки, если опкод начался на 8

И так далее, думаю идея должна быть более-менее понятна. Во время написания интерпретатора можно пользоваться заглушками для каких-то команд. Теперь, когда мы реализуем основные команды процессора, останется сделать вывод на экран и реализовать устройство ввода. За вывод на экран отвечает команда DXYN. В регистре VX находится координата X, в регистре VY находится координата Y с которых мы должны начать рисовать спрайт. Адресный регистр I в это время указывает на битовый образ спрайта. Я не буду прилагать реализацию рисования графики, думаю тут не должно возникнуть сложностей, тем более всегда можно посмотреть в исходнике в конце данного поста. Так же и с клавиатурой.

Заключение

Конечно все детали реализации я не смог упомянуть в данной заметке. Цель — просто натолкнуть на мысль и показать разбор опкодов. Если кому-то интересно, можно посмотреть мою реализацию эмулятора на Delphi, или найти другие реализации эмуляторов в интернете. Как модно говорить, тысячи их. Начиная от Visual Basic и заканчивая железными решениями.
Заранее прошу прощения за мой код, я не приводил его в порядок — вылил как есть. Основной интересный файл там — hchip.pas, в нем реализована вся эмуляция.

Так же существует неплохой англоговорящий форум EmuTalk, в котором специально выделена ветка посвященная эмуляции Chip-8.

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

Да и вообще, по запросу в гугле «chip-8» можно найти все что нужно.

Что еще можно сделать? Можно немного модифицировать наш эмулятор для поддержки Super chip-8 инструкций и спрайтов. Да много еще чего можно.

Источник статьи: http://habr.com/ru/post/100907/

Пишем эмулятор Gameboy, часть 1

Не так давно на Хабре появилась статья о создании эмулятора chip-8, благодаря которой удалось хотя бы поверхностно понять, как пишутся эмуляторы. После реализации своего эмулятора появилось желание пойти дальше. Выбор пал на оригинальный Gameboy. Как оказалось, выбор был идеальным для ситуации, когда хочется реализовать что-то более серьезное, а опыт разработки эмуляторов практически отсутствует.

С точки зрения эмуляции Gameboy относительно прост, но даже он требует изучения достаточно большого объема информации. По этой причине разработке эмулятора Gameboy будет посвящено несколько статей. Конечным итогом будет эмулятор с неплохой совместимостью, поддержкой практически всех функций оригинала, в том числе звука, который нередко отсутствует в других эмуляторах. В качестве бонуса наш эмулятор будет проходить практически все тестовые ROM’ы, но об этом позже.

Данные статьи не будут содержать исчерпывающего описания реализации эмулятора. Это слишком объемно, да и весь интерес от реализации пропадает. До конкретного кода будет доходить лишь в редких случаях. Перед собой я ставил задачу дать в большей степени теоретическое описание с небольшими намеками на реализацию, что, в идеале, должно позволить вам без особых затруднений написать свой эмулятор и в тоже время чувствовать, что вы написали его самостоятельно. Где надо я буду ссылаться на собственную реализацию – при необходимости вы сможете найти нужный код без того, чтобы продираться через тонны строк кода.

В данной статье мы познакомимся с Gameboy и начнем с эмуляции его процессора и памяти.

Оглавление

Введение


Gameboy – портативная консоль Nintendo, выпуск которой начался в 1989 году. Речь пойдет именно об оригинальном черно-белом Gameboy. Стоит заметить, что в различных документах, которыми мы будем руководствоваться, используется кодовое название Gameboy – DMG(Dot Matrix Game). Далее я буду использовать именно его.

Перед тем как приступить, необходимо ознакомиться с техническими характеристиками DMG:

Процессор 8-битный Sharp LR35902 работающий на частоте 4.19 МГц
Оперативная память 8 Кбайт
Видеопамять 8 Кбайт
Разрешение экрана 160×144
Частота вертикальной развертки 59.73 Гц
Звук 4 канала, стерео звук

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

Для DMG существует замечательный документ под названием Gameboy CPU Manual. Он включает в себя несколько известных документов от именитых разработчиков и содержит практически всю необходимую нам информацию. Естественно это не все, но на данном этапе этого более чем достаточно.

Сразу предупреждаю, что в документах будут ошибки, даже в официальных. В течение данного цикла статей я постараюсь упомянуть все недочеты различных документов какие смог найти (вспомнить). Так же постараюсь восполнить многие пробелы. Суть в том, что для DMG не существует исчерпывающего описания. Доступные материалы дают лишь поверхностное представление о работе многих узлов консоли. Если программист не в курсе таких «подводных камней», то разработка эмулятора станет намного сложнее, чем она могла бы быть. DMG достаточно прост, когда на руках достоверная и подробная информация. И проблема в том, что многие важные детали можно почерпнуть только из исходного кода других эмуляторов, что, тем не менее, не делает нашу задачу проще. Код известных эмуляторов или излишне сложен (Gambatte), или представляет собой жуткое нагромождение, кхм, низкокачественного кода (Visual Boy Advance – смотреть без слез на его код невозможно).

Поскольку статьи написаны с оглядкой на мой эмулятор, то вот сразу ссылка на исходники и бинарник CookieBoy.

Архитектура

Начнем с архитектуры будущего эмулятора. Для эмуляции DMG нам придется реализовать множество модулей, которые практически независимы друг от друга. В подобных условиях было бы глупо идти напролом, складывая все в одну кучу (что нередко наблюдается в других эмуляторах. Привет VBA). Более элегантным решением является реализация отдельных частей DMG как отдельных классов, эмулирующих свои части железа.

Я говорю это не просто так – именно с нагромождения всех компонентов в один суперкласс я и начинал разработку эмулятора. Вскоре стало очевидно, что дальше дело пойдет намного проще, если каждый будет выполнять только то, что должен. Хотя стоит признать, что в таком подходе есть и очевидная сложность. Нужно иметь достаточно неплохое понимание внутреннего устройства DMG, чтобы правильно разграничить ответственность классов.

Процессор

DMG содержит 8-битный процессор Sharp LR35902, работающий на частоте 4194304 Гц (не стоит удивляться такой точности – это число понадобится нам в будущем). Можно считать его упрощенной версией процессора Zilog Z80, который, в свою очередь, основан на Intel 8080. По сравнению с Z80 отсутствуют некоторые регистры и наборы инструкций.

Процессор содержит восемь 8-битных регистров A, B, C, D, E, F, H, L и два 16-битных регистра специального назначения — PC и SP. Некоторые инструкции позволяют объединять 8-битные регистры и использовать их как 16-битные регистры, а именно AF, BC, DE, HL. Например, регистр BC представляет собой «склеенные» регистры B и C, где регистр C исполняет роль младшего байта, а B – старшего.
Регистры A, B, C, D, E, H, L являются регистрами общего назначения. Регистр A так же является и аккумулятором. Регистр F содержит флаги процессора и напрямую недоступен. Ниже приведена схема регистра. Биты от 0 до 3 не используются.

Бит 7 6 5 4 3 2 1 0
Флаг Z N H C 0 0 0 0

Назначение флагов:

  • Zero Flag (Z) – флаг установлен (бит равен 1), если результат последней математической операции равен нулю или два операнда оказались равными при сравнении.
  • Substract Flag (N) – флаг установлен, если последней операцией было вычитание.
  • Half Carry Flag (H) – флаг установлен, если в результате последней математической операции произошел перенос из младшего полу-байта.
  • Carry Flag (С) – флаг установлен, если в результате последней математической операции произошел перенос.

Регистр PC (program counter), как несложно догадаться, является счетчиком инструкций и содержит адрес следующей инструкции.

Регистр SP (stack pointer), соответственно, является указателем на вершину стека. Для тех, кто не в курсе, стек это область памяти, в которую записываются значения переменных, адреса возврата и прочее. SP содержит адрес вершины стека – стек растет вниз, от старших адресов к младшим. Для него всегда существует как минимум две операции. PUSH позволяет вставить некое значение – сначала регистр SP уменьшается, а затем происходит вставка нового значения. POP позволяет извлечь значение – сначала по адресу SP значение извлекается из памяти, а затем SP увеличивается.

Так же процессор содержит так называемый IME (interrupt master enable) – флаг, который разрешает обработку прерываний. Принимает, соответственно, два значения – запретить (0) и разрешить (1).

С теорией все, можно приступать к реализации. Поскольку нам придется работать как с 8-битными регистрами, так и с их 16-битными парами, то целесообразно реализовать механизм, который позволяет иметь одновременный доступ и к тем, и к тем без необходимости использовать битовые операции. Для этого объявим следующий тип:

Регистры процессора будем хранить как пары, а к отдельным частям будем иметь доступ благодаря объединению WordRegister. Поле «word» даст доступ ко всему 16-битному регистру. Поле «bytes» дает доступ к отдельным регистрам в паре. Единственное, регистры А и F стоит хранить отдельно. Регистр А является аккумулятором, а значит используется очень часто. Похожая ситуация с регистром F – флаги процессора приходится устанавливать довольно часто.

Теперь приступим к реализации собственно процессора – за это будет отвечать класс Cookieboy::CPU. Чтение и исполнение инструкций будет реализовано по обычной схеме – чтение опкода из памяти, а затем декодирование и исполнение посредством конструкции switch:

Все опкоды имеют длину 1 байт, но некоторые инструкции используют так называемый префикс – первым байтом идет префикс набора инструкций (для нас единственный префикс это 0xCB), вторым байтом идет собственно опкод из этого набора. Реализация элементарная – как только мы наткнулись на 0xCB, то читаем еще один байт и декодируем его вложенным switch.

Данный код помещается в функцию void Step(), которая за один вызов исполняет одну инструкцию процессора и производит другие необходимые операции.

Естественно для чтения и записи в память нам понадобится другой класс – Cookieboy::Memory, объект которого можно видеть выше под именем «MMC». На данном этапе достаточно заглушки с основными методами:

Процессор DMG имеет достаточно большое число инструкций, список которых можно найти в Gameboy CPU Manual. Там же указано, какие флаги процессора необходимо устанавливать и сколько тактов занимает исполнение каждой инструкции. ОЧЕНЬ внимательно читайте описание флагов – неправильно реализованная установка флагов нередко приводит к неработоспособности игр, а отладка превращается в пытку. Но спешу немного успокоить – существует тестовые ROM’ы для флагов процессора, но до исполнения ROM’ов нам еще далеко.

К слову о тактах. Если chip-8 был достаточно прост, и его эмуляция не требовала учета длительности исполнения инструкций, то c DMG дело обстоит иначе. Компоненты консоли работают не абы как, а синхронизированы с помощью генератора тактовой частоты. Для нас это означает то, что нам необходимо синхронизировать работу всех компонентов нашего эмулятора с процессором.

Решить эту проблему достаточно просто. Процессор является центральным звеном нашего эмулятора. Исполнив инструкцию, мы передаем другим компонентам затраченное процессором время в тактах для синхронизации всех компонентов между собой. Для этого я используют макрос SYNC_WITH_CPU(clockDelta), в который и передается затраченное процессором время на исполнение инструкции. Он уже вызывает функции синхронизации остальных компонентов эмулятора. Решение проблемы синхронизации можно было бы легко вынести за пределы класса процессора, если бы не одно но.

Компоненты консоли работают одновременно, никто не ждет, пока процессор закончит исполнение инструкции, как это делаем мы. Некоторые инструкции требует длительного времени на исполнение и в процессе происходит чтение и запись данных в память. Процессор, как можно догадаться, тратит определенное время на чтение/запись в память (4 такта). Это приводит к тому, что в процессе исполнения содержимое памяти может измениться, что, естественно, неплохо было бы тоже эмулировать.

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

Правильнее и красивее сделать все же по-другому. Мы точно знаем, что каждая операция записи или чтения из памяти одного байта занимает 4 такта. Достаточно добавить вспомогательные функции чтения и записи, которые сами вызывают функции синхронизации. Как только это будет сделано, большинство инструкций сразу обретет правильную длительность, ведь в действительности их время исполнения складывается как раз из операций чтения и записи. Получение опкода команды тоже сюда относится. Именно так я поступил в своем эмуляторе, что практически полностью освободило меня от ручной синхронизации и подсчета тактов. Лишь некоторые инструкции потребовали моего вмешательства.

А теперь немного отвлечемся, чтобы прояснить ситуацию с тактами. В различной документации наблюдается путаница. Некоторые документы пишут такие числа, что, например, NOP имеет длительность 4 такта, другие – 1 такт (так, например, написано в официальной документации Nintendo). Для понимания причины стоит немного отвлечься на теорию.

Любая инструкция процессора имеет определенную длительность, которую назовем машинный цикл. За один машинный цикл процессор может произвести одно действие от и до, как например чтение опкода, его декодирование и исполнение команды; чтение или запись значения в памяти. В свою очередь, машинный цикл состоит из машинных тактов, поскольку за один машинный цикл процессор может совершить несколько операций. И вот мы приходим к нашему процессору. Если мы говорим, что NOP длится 4 такта, то мы говорим о машинных тактах. Если мы говорим об 1 такте для NOP, то мы говорим о машинных циклах. Процессор DMG именно так и работает – его машинный цикл длится 4 машинных такта и многие инструкции имеют длительность ровно 4 такта или 1 машинный цикл – процессор DMG способен прочитать опкод из памяти, декодировать его и исполнить инструкцию всего за 4 машинных такта.

Здесь и далее я буду использовать более привычные машинные такты. Именно они соответствуют одному периоду тактового генератора, а значит, являются минимальной и неделимой единицей времени для нашего эмулятора. Таким образом, операция NOP будет длиться 4 такта.

На данном этапе уже можно полностью реализовать эмуляцию всех команд процессора. Отдельно стоит упомянуть некоторые из них:

  • HALT имеет достаточно интересное поведение, которое описано в CPU Manual (2.7.3. Low-Power Mode). Решение в лоб приведет к тому, что тест инструкции HALT не будет пройден. Здесь нужно быть внимательным как в реализации самой инструкции, так и в реализации обработчика прерываний (об этом далее). Реализация инструкции такова, что она не приостанавливает исполнение и приводит к упомянутому багу только в том случае, если IME равен нулю и в данный момент нет прерываний, которые необходимо обработать (об этом так же далее) – именно последний момент опущен в большинстве документов. В противном случае бага нет, и исполнение приостанавливается. Естественно, тактовый генератор и все остальные компоненты продолжают свою работу, а значит надо продолжать вызывать функции синхронизации, давая в качестве аргумента 4 такта (нет смысла считать в этом режиме по одному такту). Как будто процессор исполняет NOP.
  • В POP AF стоит учесть тот факт, что в регистре F есть неиспользуемые биты. Для этого необходимо обнулить младшие 4 бита регистра F после того, как его содержимое будет извлечено из стека.
  • Инструкции RLCA, RLA, RRCA, RRA всегда обнуляют флаг Z в регистре F.

Помимо этих недочетов есть и другие. CPU Manual содержит неполное описание длительности инструкций. Как можно догадаться, инструкции условных переходов должны иметь разную длительность в зависимости от того, произошел переход или нет. Можно было бы воспользоваться тестовыми ROM’ми, но они неправильно работают сами по себе из-за этих инструкций, поэтому выводят неизвестную ошибку даже не начав тестирование. Вот таблица этих инструкций с указанием их длительности:

Опкоды Переход не произошел Переход произошел
0xC2, 0xCA, 0xD2, 0xDA 12 16
0x20, 0x28, 0x30, 0x38 8 12
0xC4, 0xCC, 0xD4, 0xDC 12 24
0xC0, 0xC8, 0xD0, 0xD8 8 20

Так же для инструкций RST n (опкоды 0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xEF, 0xF7, 0xFF) указана неправильная длительность. Правильное значение равно 16 тактам.

И так, на данный момент наш «процессор» способен читать инструкции из памяти, исполнять их и синхронизировать другие компоненты с собой (как бы синхронизирует, пока все это функции-пустышки). После этого нам необходимо проверить, не произошло ли прерывание после всей проделанной работы.

Прерывания

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

В ходе синхронизации мы вызываем методы синхронизации других компонентов эмулятора, которые могут запросить прерывание. В DMG это осуществляется следующим образом. Существует два регистра (где они находятся будет рассмотрено далее) – IF (interrupt flags) и IE (interrupt enable). Их биты имеют определенное назначение, которое идентично в обоих регистрах:

Бит Прерывание
4 Joypad
3 Serial I/O transfer complete
2 Timer overflow
1 LCDC
0 V-Blank

Биты регистра IF показывают, какие прерывания были запрошены. Если бит установлен, то прерывание запрошено.

Биты регистра IE разрешают обработку прерываний. Если бит установлен в единицу и соответствующее прерывание было запрошено, то оно будет обработано. Если нет, то прерывание обработано не будет.

Как видно, идентичное назначение битов очень кстати и позволяет с помощью логической операции И узнать, какие прерывания следует обработать.

Одна важная деталь состоит в том, что прерывание выводит процессор из состояния останова, произошедшее в результате исполнения HALT или STOP. И здесь очень важен алгоритм, по которому проверяются регистры прерываний. Алгоритм таков:

  1. Проверяем, есть ли вообще прерывания, которые стоит обработать. Делается это с помощью операции логического И между регистрами IE и IF. Дополнительно стоит произвести операцию логического И с результатом и числом 0x1F для удаления возможного мусора, поскольку старшие три бита не используются в обоих регистрах.
  2. Если таких прерываний нет, то выходим из функции. Если же они есть, то именно сейчас мы должны вывести процессор из состояния останова.
  3. Теперь мы приступаем к обработке прерываний. Для этого мы проверяем, не запрещает ли флаг IME их обработку. Если нет, то:
    1. обнуляем IME;
    2. загружаем регистр PC в стек;
    3. вызываем обработчик прерывания путем установки регистра PC равному адресу обработчика в памяти;
    4. обнуляем бит регистра IF соответственно обработанному прерыванию.

Прерывания обрабатываются по одному за раз и в строго определенном порядке. Вся информация о приоритетах и адресах обработчиков указана в CPU Manual.

Важная деталь. Опять же, кому-то уже могла прийти в голову мысль – обработка прерывания очень похожа на вызов процедуры, а значит должна занимать какое-то время. Это действительно так и занимает она 20 тактов. По какой-то причине этот момент опущен в документах, описывающих DMG.

Теперь приступаем к реализации. Прерываниями у нас будет заниматься класс Cookieboy::Interrupts. В него мы помещаем регистры IE и IF и объявляем функции для доступа к этим регистрам (они понадобятся нам позже), а так же функцию, которая позволяет запросить определенное прерывание (нам же не хочется каждый раз манипулировать битами, чтобы запросить какое-то прерывание). Так же нам понадобится функция, которая будет проверять, какие прерывания стоит обработать. Помещаем вызов этой функции в конце функции Step процессора и дополнительно синхронизируем компоненты.

Немного про запрос прерываний. Он осуществляется установкой в регистре IF соответствующих битов. Перед установкой проверка регистра IE не требуется. Даже если биты в нем запрещают конкретное прерывание, мы все равно устанавливается биты в IF регистре для этого прерывания.

Если вы смотрели исходный код моей реализации Cookieboy::Interrupts, то могли заметить, что я возвращаю значение регистров IE и IF после того, как установлю в единицу все неиспользуемые в них биты (операция ИЛИ со значением 0xE0). Делаю я это не просто так. Многие регистры в I/O ports (об этом чуть ниже) используют не все биты, другие ограничивают доступ на чтение к некоторым битам или ко всему регистру сразу. Это так же необходимо учитывать – для этого неиспользуемые и запрещенные для чтения биты стоит установить в 1 перед возвращением.

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

Память

Заранее определимся с одним термином – банк памяти. Под этим понимается область памяти строго определенного размера. Существует два вида банков – ROM банки, имеющие длину 0x4000 байт, и RAM банки, имеющие длину 0x2000 (стоит сразу привыкнуть к 16-ричной системе счисления, так будет проще и мне, и вам). Зачем это нужно? Процессор DMG способен работать с 16-битным адресами, а значит адресное пространство ограничено 0x10000 байтам. Из них только 0x8000 байт отведено на образ игры. В большинстве случаев этого недостаточно и в ход вступают банки памяти.

Обращаясь по адресам 0x4000-0x7FFF, без банков памяти мы бы попали именно по этому адресу в образе игры. С помощью банков памяти мы можем установить так, чтобы образ был поделен на банки, а по адресу 0x4000-0x7FFF был отображен выбранный банк. Таким образом, в один момент в этой области находится второй банк, в другой – десятый. Как мы захотим, в общем. Таким образом, мы приходим к виртуальным и физическим адресам. 0x4000-0x7FFF – это виртуальные адреса, которые не обязаны совпадать с физическими. Физический адрес – это настоящий адрес, по которому происходит доступ к ячейкам памяти.

Все это необходимо для того, чтобы наш DMG мог работать с образами игры, которые намного превышают не только 0x8000 байт, но и все адресное пространство. На словах это все может показаться слишком сложным, но в ходе реализации будет понятно, что это предельно элементарные вещи, которые проще и быстрее реализовать, чем объяснить.

Все тоже самое относится и к оперативной памяти. Банки позволяют расширить ее объем, поместив микросхемы в картридж. Кроме того, так можно реализовать полноценную систему сохранений, используя встроенный в картридж аккумулятор для питания оперативной памяти.

Задача перевода виртуального адреса в физический лежит на MBC-контроллере, который находится внутри картриджа. Все операции чтения и записи в области ROM’а проходят через него. Так же сюда перенаправляются операции, связанные с внешней оперативной памятью.

Естественно мы не может изменить содержимое ROM. Операции записи используются как управляющие команды для MBC. В CPU Manual можно прочитать, какие адреса, за какие функции отвечают. Таким образом, записав по определенному адресу число 9, мы говорим, что хотим выбрать банк 9. После этого мы можем читать его содержимое, обращаясь по адресам 0x4000-0x7FFF.

На рисунке ниже представлена простейшая схема работы MBC. Здесь область 0x0000-0x3FFF всегда перенаправляется в банк 0, как в некоторых реальных контроллерах, а вот область 0x4000-0x7FFF перенаправляется в текущий банк.

Рассмотрим схему адресного пространства DMG:

Секция памяти Начальный адрес Конечный адрес
ROM bank 0 0x0000 0x3FFF
Switchable ROM bank 0x4000 0x7FFF
Video RAM 0x8000 0x9FFF
Switchable RAM bank 0xA000 0xBFFF
Internal RAM 1 0xC000 0xDFFF
Echo of Internal RAM 1 0xE000 0xFDFF
OAM 0xFE00 0xFE9F
Не используется 0xFEA0 0xFEFF
I/O ports 0xFF00 0xFF4B
Не используется 0xFF4C 0xFF7F
Internal RAM 2 0xFF80 0xFFFE
Interrupt enable register 0xFFFF 0xFFFF

Подробнее о каждой секции:

  • ROM bank 0. Switchable ROM bank. Данные области мы уже рассмотрели.
  • Video RAM. Подробнее будет рассмотрена при реализации графики.
  • Internal RAM 1. Оперативная память внутри DMG.
  • Echo of Internal RAM 1. Здесь дублируется содержимое Internal RAM 1.
  • OAM. Здесь хранится описание спрайтов.
  • I/O ports. Здесь мы получаем доступ к регистрам других компонентов DMG.
  • Internal RAM 2. Оперативная память внутри DMG.
  • Interrupt enable register. В этом регистре хранятся флаги, которые разрешают обработку определенных прерываний. Это тот самый регистр IE, о котором мы уже говорили.

Поскольку архитектура эмулятора предполагает, что каждый компонент DMG будет иметь свой класс, то класс Cookieboy::Memory, эмулирующий память, будет содержать лишь следующие области памяти – ROM банки, internal RAM 1, Echo of internal RAM 1, Switchable RAM bank, internal RAM 2. При обращении ко всем другим областям будут вызываться методы доступа соответствующих классов.

Начнем с операций чтения и записи в памяти. Все предельно просто – смотрим на адрес и перенаправляем операции в соответствующие области памяти. Я сделал следующим образом. Как можно заметить, многие области памяти хорошо выравнены, что позволяет реализовать все с помощью switch и логических операций. Вот как это выглядит:

И никаких громоздких условных конструкций. Пока можно оставить лишь заготовку, поскольку некоторые области памяти будут находиться в других классах (например, видеопамять), которые мы еще не реализовали. Можно лишь реализовать то, что действительно есть в Cookieboy::Memory. Здесь стоит обратить внимание на банки ROM’а и Switchable RAM bank.

Если картридж, с которого был снят ROM, содержал MBC-контроллер, то в этих областях памяти нам надо реализовать логику этих контроллеров. Для этого можно поступить очень просто – доступ к этим областям перенаправляется в классы, которые реализуют соответствующие MBC-контроллеры, а они уже сами пусть решают, куда, как и что. Рассмотрим два примера – MBC 2 и MMM01. Первый – как пример, который позволит вам реализовать остальное. MMM01 – довольно странный MBC. По нему практически нет документации, а его реализация довольно сильно отличается от других MBC. Не повредит восполнить этот пробел в деле эмуляции DMG.

Для начала обзаведемся базовым классом MBC. Выглядеть он будет следующим образом:

Как видно, сначала идут функции записи и чтения – именно они будут вызываться из нашего Cookieboy::Memory. Далее идут функции сохранения и загрузки RAM. Тут мы сразу готовим почву для будущей эмуляции памяти в картридже, которая запитана от аккумулятора для сохранения ее содержимого после выключения консоли. Их реализацию я опущу – это всего лишь сохранение и чтение массива RAMBanks из файла, не более. Затем предельно очевидный конструктор и несколько полей:

  • ROM. Здесь у нас находится весь образ игры.
  • RAMBanks. Здесь находится оперативная память картриджа.
  • RAMOffset и ROMOffset. Это смещения, которые указывают на текущий банк памяти.
  • ROMSize и RAMSize, думаю, не требуют пояснений. Значения хранятся в банках памяти, а не в байтах.

С базовым классом покончено, теперь приступим к реализации класса, который эмулирует MBC2. Сразу посмотрим на код, а затем уже разберемся, как работает этот контроллер:

С функцией чтения все просто. ROMOffset используется как смещение для обращения к текущему банку ROM. С оперативной памятью есть одна деталь. MBC2 имеет 512 4-битовых блоков RAM. Мы, естественно, выделяем все 512 байт, просто операции записи и чтения обрезают значения до 4 младших бит.

Теперь функция записи. Именно здесь эмулируется логика MBC. MBC2 поддерживает только смену банков ROM. Меняются они с помощью записи номера банка длиной 4 бита в область адресов 0x2000-0x3FFF. Нулевой банк выбрать нельзя, т.к. он и так находится в 0x0000-0x3FFF. Здесь же стоит проверять выход за границы ROM. Некоторые игры по неопределенной причине пытаются выбрать банк, которого не существует. Это, естественно, приводит к ошибке. С проверкой игра работает, как ни в чем не бывало. Одной из таких игр является WordZap. Может это последствия неточной эмуляции (на идеальную эмуляцию DMG я, естественно, не претендую), но в любом случае проверка не повредит.

Да, 0xFF возвращается не случайно – на DMG данное значение возвращается в случае, когда содержимое не определено.

Наконец, рассмотрим MMM01. Я не уверен в правильности своего кода, поскольку описание этого контроллера было найдено на форуме, а само оно написано неизвестно кем. Код:

Как видите, кода много. Не буду объяснять каждую строчку – после предыдущего примера я надеюсь, что вам не составит труда понять, что и зачем делается. Скажу лишь, что MMM01 вроде бы используется всего в 2 играх, поэтому не случайно он есть далеко не во всех эмуляторах.

Возвращаясь к эмуляции памяти, стоит немного прояснить область памяти под названием I/O ports. Т.к. DMG состоит из различных компонентов, то неплохо бы иметь возможность как-то влиять на их работу и даже контролировать. Для этого в области памяти I/O ports мы имеем доступ к регистрам всех остальных компонентов DMG: контроллер экрана, звук, таймеры, управление и т.д. Естественно, все эти регистры в нашем эмуляторе будут находиться в соответствующих классах, а значит Cookieboy::Memory будет лишь перенаправлять все операции в них. Список и назначение всех регистров можно найти в CPU Manual. Так же я буду рассматривать их при необходимости. Кстати, один из них мы уже рассмотрели – IF. Этот регистр доступен именно в этой области памяти, поэтому необходимо перенаправить операции чтения и записи в класс Cookieboy::Interrupts. Мы уже можем это сделать, т.к. обеспокоились об этом заранее при рассмотрении прерываний.

Настало время еще одной важной функции – загрузки ROM из файла. Перед реализацией загрузки образа в память самое время упомянуть, что же происходит при включении DMG.

Сначала идет исполнение Bootstrap ROM’а, который хранится внутри DMG. Его содержимое можно найти в исходном коде класса Cookieboy::Memory. Он не делает ничего особенного, кроме проверки содержимого картриджа и отображения лого Nintendo. Он имеет длину 256 байт, исполнение начинается с 0 – т.е. после включения регистр PC процессора равен нулю. Его исполнение заканчивается командой, которая осуществляется запись по адресу 0xFF50. По этому адресу находится скрытый регистр, который указывает, откуда в данный момент поступают команды для процессора – из Bootstrap ROM’а или картриджа. Как ни странно, описания этого регистра нет практически нигде. Более того, нет даже упоминаний о нем.

Интересный факт. Bootstrap ROM был получен не так давно, а извлекли его посредством фотографии кристалла процессора. Автор сфотографировал часть процессора, в которой находился этот ROM, и на глаз по одному биту считал все содержимое.

Замечу, что при включении оперативная память DMG и картриджа содержит случайные числа. Это незначительная деталь, поэтому обычно эмуляторы заполняют эти области нулями. Как поступать – решать вам. Скорее всего, лучше или хуже от этого игры работать не станут. Учтите, речь только об оперативной памяти. Заполнение случайными значениями других областей приведет к некорректной работе эмулятора.

Конечно, не хотелось бы каждый раз запускать этот образ. Для этого можно сделать следующее. Регистр PC должен быть равен 0x100 – именно по этому адресу находится первая команда в образах игр. Далее, все регистры процессора и область памяти I/O ports необходимо проинициализировать значениями, которые оставляет после себя Bootstrap ROM – эти значения можно найти в CPU Manual. Не все игры настолько хорошо написаны, чтобы устанавливать все необходимые значения самостоятельно, некоторые могут полагаться на значения, которые установлены после выполнения Bootstrap ROM. Для этого все компоненты содержат функцию EmulateBIOS, посредством которой устанавливаются все необходимые значения.

И так, приступим к загрузке обараза. Весь файл образа читается в массив, а из заголовочной части образа читаются метаданные образа. Самое важное это узнать тип картриджа (тип MBC-контроллера) и размер внешней оперативной памяти внутри картриджа. Адреса указаны в CPU Manual. Так же стоит реализовать те проверки, которые делает Bootstrap ROM. С помощью них можно легко узнать, действительно ли файл является образом для DMG. Первая проверка – лого Nintendo. Каждый ROM содержит логотип Nintendo, который и отображается при выполнении Bootstrap ROM. Он должен иметь строго определенное значение. Какое – указано в CPU Manual. Так же можно проверить контрольную сумму заголовка образа. Для этого можно воспользоваться следующим кодом:

Если проверки прошли, то мы выделяем место под оперативную память картриджа и создаем объект соответствующего MBC чипа.

Касательно оперативной памяти картриджа, то неплохо всегда иметь под рукой как минимум один банк памяти, даже если образ «говорит», что она не используется. Некоторые игры выдают себя за картриджи без MBС, но, тем не менее, могут иметь простенький чип лишь для оперативной памяти.

Источник статьи: http://habr.com/ru/post/154901/


0 0 голоса
Article Rating
Подписаться
Уведомить о
guest

0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии