Пишем эмулятор Gameboy, часть 1 / Хабрахабр. Здравствуйте! Не так давно на Хабре появилась статья о создании эмулятора chip- 8, благодаря которой удалось хотя бы поверхностно понять, как пишутся эмуляторы. После реализации своего эмулятора появилось желание пойти дальше. Выбор пал на оригинальный Gameboy. ![]() В данном видео показываю запуск Windows xp в режиме эмуляции процессора intel pentium 2. Эмуляция процессора происходит в эмуляторе. Как сделать виртуальный intel pentium2? Как замедлить процессор в эмуляторе? Где скачать эмулятор процессора?С. В качестве упражнения по программированию, этого будет вполне достаточно. Если нужно сделать что-то более серьезное, стоит изучить код уже существующих эмуляторов, например, qemu. Пример реализации эмулятора ARM-процессора на Google-коде. Как сделать эмулятор своего устройства? Я хочу написать программу, эмулирующую работу процессора/устройства, но не того. Как оказалось, выбор был идеальным для ситуации, когда хочется реализовать что- то более серьезное, а опыт разработки эмуляторов практически отсутствует. С точки зрения эмуляции Gameboy относительно прост, но даже он требует изучения достаточно большого объема информации. По этой причине разработке эмулятора Gameboy будет посвящено несколько статей. Конечным итогом будет эмулятор с неплохой совместимостью, поддержкой практически всех функций оригинала, в том числе звука, который нередко отсутствует в других эмуляторах. В качестве бонуса наш эмулятор будет проходить практически все тестовые ROM’ы, но об этом позже. Данные статьи не будут содержать исчерпывающего описания реализации эмулятора. Это слишком объемно, да и весь интерес от реализации пропадает. До конкретного кода будет доходить лишь в редких случаях. Перед собой я ставил задачу дать в большей степени теоретическое описание с небольшими намеками на реализацию, что, в идеале, должно позволить вам без особых затруднений написать свой эмулятор и в тоже время чувствовать, что вы написали его самостоятельно. Где надо я буду ссылаться на собственную реализацию – при необходимости вы сможете найти нужный код без того, чтобы продираться через тонны строк кода. В данной статье мы познакомимся с Gameboy и начнем с эмуляции его процессора и памяти. Пишем эмулятор Gameboy, часть 1. Пишем эмулятор Gameboy, часть 2. Пишем эмулятор Gameboy, часть 3. Оглавление. Введение. Архитектура. Процессор. Прерывания. Память. Заключение. Введение. Gameboy – портативная консоль Nintendo, выпуск которой начался в 1. Речь пойдет именно об оригинальном черно- белом Gameboy. Стоит заметить, что в различных документах, которыми мы будем руководствоваться, используется кодовое название Gameboy – DMG(Dot Matrix Game). Далее я буду использовать именно его. Перед тем как приступить, необходимо ознакомиться с техническими характеристиками DMG: Процессор. Sharp LR3. 59. 02 работающий на частоте 4. МГц. Оперативная память. Кбайт. Видеопамять. Кбайт. Разрешение экрана. Частота вертикальной развертки. Гц. Звук. 4 канала, стерео звук. Ознакомившись с подопытным, следующим шагом является документация. Объемы необходимой информации не позволяют разместить в статье абсолютно все, поэтому необходимо заранее вооружиться документацией. Для DMG существует замечательный документ под названием Gameboy CPU Manual. Он включает в себя несколько известных документов от именитых разработчиков и содержит практически всю необходимую нам информацию. Естественно это не все, но на данном этапе этого более чем достаточно. Сразу предупреждаю, что в документах будут ошибки, даже в официальных. В течение данного цикла статей я постараюсь упомянуть все недочеты различных документов какие смог найти (вспомнить). Так же постараюсь восполнить многие пробелы. Суть в том, что для DMG не существует исчерпывающего описания. Доступные материалы дают лишь поверхностное представление о работе многих узлов консоли. Если программист не в курсе таких «подводных камней», то разработка эмулятора станет намного сложнее, чем она могла бы быть. DMG достаточно прост, когда на руках достоверная и подробная информация. И проблема в том, что многие важные детали можно почерпнуть только из исходного кода других эмуляторов, что, тем не менее, не делает нашу задачу проще. Код известных эмуляторов или излишне сложен (Gambatte), или представляет собой жуткое нагромождение, кхм, низкокачественного кода (Visual Boy Advance – смотреть без слез на его код невозможно). Поскольку статьи написаны с оглядкой на мой эмулятор, то вот сразу ссылка на исходники и бинарник Cookie. Boy. Архитектура. Начнем с архитектуры будущего эмулятора. Для эмуляции DMG нам придется реализовать множество модулей, которые практически независимы друг от друга. В подобных условиях было бы глупо идти напролом, складывая все в одну кучу (что нередко наблюдается в других эмуляторах. Более элегантным решением является реализация отдельных частей DMG как отдельных классов, эмулирующих свои части железа. Я говорю это не просто так – именно с нагромождения всех компонентов в один суперкласс я и начинал разработку эмулятора. Вскоре стало очевидно, что дальше дело пойдет намного проще, если каждый будет выполнять только то, что должен. Хотя стоит признать, что в таком подходе есть и очевидная сложность. Нужно иметь достаточно неплохое понимание внутреннего устройства DMG, чтобы правильно разграничить ответственность классов. Итак, приступим. Процессор. DMG содержит 8- битный процессор Sharp LR3. Гц (не стоит удивляться такой точности – это число понадобится нам в будущем). Можно считать его упрощенной версией процессора Zilog Z8. Intel 8. 08. 0. По сравнению с Z8. Процессор содержит восемь 8- битных регистров A, B, C, D, E, F, H, L и два 1. PC и SP. Некоторые инструкции позволяют объединять 8- битные регистры и использовать их как 1. AF, BC, DE, HL. Например, регистр BC представляет собой «склеенные» регистры B и C, где регистр C исполняет роль младшего байта, а B – старшего. Регистры A, B, C, D, E, H, L являются регистрами общего назначения. Регистр A так же является и аккумулятором. Регистр F содержит флаги процессора и напрямую недоступен. Ниже приведена схема регистра. Биты от 0 до 3 не используются. Для тех, кто не в курсе, стек это область памяти, в которую записываются значения переменных, адреса возврата и прочее. SP содержит адрес вершины стека – стек растет вниз, от старших адресов к младшим. Для него всегда существует как минимум две операции. PUSH позволяет вставить некое значение – сначала регистр SP уменьшается, а затем происходит вставка нового значения. POP позволяет извлечь значение – сначала по адресу SP значение извлекается из памяти, а затем SP увеличивается. Так же процессор содержит так называемый IME (interrupt master enable) – флаг, который разрешает обработку прерываний. Принимает, соответственно, два значения – запретить (0) и разрешить (1). С теорией все, можно приступать к реализации. Поскольку нам придется работать как с 8- битными регистрами, так и с их 1. Для этого объявим следующий тип: union Word. Register. . Поле «word» даст доступ ко всему 1. Поле «bytes» дает доступ к отдельным регистрам в паре. Единственное, регистры А и F стоит хранить отдельно. Регистр А является аккумулятором, а значит используется очень часто. Похожая ситуация с регистром F – флаги процессора приходится устанавливать довольно часто. Теперь приступим к реализации собственно процессора – за это будет отвечать класс Cookieboy: :CPU. Чтение и исполнение инструкций будет реализовано по обычной схеме – чтение опкода из памяти, а затем декодирование и исполнение посредством конструкции switch: BYTE opcode = MMC. Read(PC). switch (opcode). Реализация элементарная – как только мы наткнулись на 0x. CB, то читаем еще один байт и декодируем его вложенным switch. Данный код помещается в функцию void Step(), которая за один вызов исполняет одну инструкцию процессора и производит другие необходимые операции. Естественно для чтения и записи в память нам понадобится другой класс – Cookieboy: :Memory, объект которого можно видеть выше под именем «MMC». На данном этапе достаточно заглушки с основными методами: class Memory. Там же указано, какие флаги процессора необходимо устанавливать и сколько тактов занимает исполнение каждой инструкции. ОЧЕНЬ внимательно читайте описание флагов – неправильно реализованная установка флагов нередко приводит к неработоспособности игр, а отладка превращается в пытку. Но спешу немного успокоить – существует тестовые ROM’ы для флагов процессора, но до исполнения ROM’ов нам еще далеко. К слову о тактах. Если chip- 8 был достаточно прост, и его эмуляция не требовала учета длительности исполнения инструкций, то c DMG дело обстоит иначе. Компоненты консоли работают не абы как, а синхронизированы с помощью генератора тактовой частоты. Для нас это означает то, что нам необходимо синхронизировать работу всех компонентов нашего эмулятора с процессором. Решить эту проблему достаточно просто. Процессор является центральным звеном нашего эмулятора. Исполнив инструкцию, мы передаем другим компонентам затраченное процессором время в тактах для синхронизации всех компонентов между собой. Для этого я используют макрос SYNC. Он уже вызывает функции синхронизации остальных компонентов эмулятора. Решение проблемы синхронизации можно было бы легко вынести за пределы класса процессора, если бы не одно но. Компоненты консоли работают одновременно, никто не ждет, пока процессор закончит исполнение инструкции, как это делаем мы. Некоторые инструкции требует длительного времени на исполнение и в процессе происходит чтение и запись данных в память. Процессор, как можно догадаться, тратит определенное время на чтение/запись в память (4 такта). Это приводит к тому, что в процессе исполнения содержимое памяти может измениться, что, естественно, неплохо было бы тоже эмулировать. В этом случае требуется несколько раз использовать макрос синхронизации в процессе исполнения, чтобы в памяти находились правильные данные в момент их чтения или записи. Большинство инструкций не требует такой точной синхронизации, и позволяют производить ее после исполнения. Другие же требуют точной последовательности функций синхронизации и операций чтения/записи в память. Правильнее и красивее сделать все же по- другому. Мы точно знаем, что каждая операция записи или чтения из памяти одного байта занимает 4 такта. Достаточно добавить вспомогательные функции чтения и записи, которые сами вызывают функции синхронизации. Как только это будет сделано, большинство инструкций сразу обретет правильную длительность, ведь в действительности их время исполнения складывается как раз из операций чтения и записи. Получение опкода команды тоже сюда относится. Создаем эмулятор приставки / Хабрахабр. Вероятно, многие программисты если и не мечтали, то хотя бы задумывались о написании собственного эмулятора какого- либо процессора. Возможно, некоторые даже экспериментировали с чем- то вроде Z8. Но не многие дошли до финальной реализации эмулятора. В этой заметке я хотел бы поговорить о создании простого эмулятора игровой платформы CHIP- 8 из далеких 7. Во- первых, мы прикоснемся к истории, а во- вторых, эта платформа из за своей простоты позволит создать полностью функциональный эмулятор даже начинающим программистам. Конец. Как бы это странно не было, а начну я с конца. Вот такая программа. OPTION BINARY ; We want a binary file, not an HP4. ALIGN OFF ; And we don't want auto alignement, as some; data can be made of bytes instead of words. LD V0, 0. LD V1, 0. LOOP: LD I, LEFT ; We draw a left line by default, as the random number; is 0 or 1. Владеющие английским языком могут ознакомиться с подробной статьей в википедии, а я же попробую пересказать основные моменты своими словами. CHIP- 8 – это интерпретируемый язык программирования, созданный в середине 7. COSMAC VIP и Telmac 1. Программы, написанные и скомпилированные для CHIP- 8, выполняются на самих приставках в виртуальных машинах. Ну, по современной аналогии это что- то вроде Java байт- кода. Я же вообще советую забыть на время создания эмулятора о том, что это интерпретируемый язык, и считать, что мы эмулируем железную платформу – некий процессор со своим набором команд. Далее, когда я буду говорить “приставка”, я буду подразумевать CHIP- 8. Наша приставка имеет память, процессор, устройство видео вывода, звук и конечно устройство ввода. Рассмотрим все компоненты подробнее: Память. Приставка имеет 4. Kb основной памяти (RAM). Память начинается со смещения 2. FFFh соответственно. Почему память для программ начинается со смещения 2. Все очень просто – первые 5. CHIP- 8 в машинных кодах того процессора, на котором построена приставка. Регистры. В CHIP- 8 существует шестнадцать 8- битных регистров данных с именами V0. Регистр VF отвечает за флаг переноса (carry flag) при операциях сложения/вычитания. Также в приставке имеется 1. I. Стек. Стек используется для сохранения адреса возврата, когда завершается выполнение подпрограммы. У оригинальной версии приставки размер стека составляет 4. Поскольку мы не ограничены в ресурсах, мы будем использовать 1. Так делает большинство CHIP- 8 эмуляторов. Таймеры. В приставке присутствуют два 8- битных таймера, они оба уменьшаются с частотой 6. Гц, пока не достигнут нуля. Delay timer: Этот таймер используется для различных задержек в играх, его значение можно читать/изменять с помощью команд. Sound timer: Когда значение таймера отлично от нуля, выводится пищащий звук. Устройство ввода. Ввод осуществляется с помощью 1. В оригинальной приставке клавиши имеют коды от 0h до Fh. Если мы эмулируем на компьютере, то удобнее всего использовать правую Num. Pad часть клавиатуры, ту, где находятся цифры 0- 9 и Num. Lock. Клавиши '8', '4', '6', и '2' обычно используются для перемещения, хотя и не всегда так. Это зависит от игры. Графика и звук. В нашей приставке разрешение экрана 6. Вывод реализован с помощью спрайтов, которые всегда имеют ширину 8 пикселей и могут иметь длину от 1 до 1. Если при рисовании спрайт накладывается на другой спрайт, то в точке наложения цвет инвертируется, а регистр VF (carry flag) принимает значение 1. Иначе он принимает значение 0. Как выше уже было замечено, играется противный пищащий звук, если значение Sound timer отлично от нуля. Я думаю, звук мы реализовывать вообще не будем, не люблю эти бипы. Команды. Наш процессор (CHIP- 8 на самом деле) имеет ровно 3. Здесь таблицу команд не буду перепечатывать, она есть в википедии. Можно разобрать несколько примеров оттуда, например: 0. E0 Clears the screen. Например, если встретили команду 6. A, значит нужно в регистр V3 записать значение 5. Ah. Практика. Из рассмотренного выше видно, что эта платформа как нельзя лучше подходит для начала изучения принципов работы эмуляторов. Здесь у нас отсутствуют хитрые маскируемые и не маскированные прерывания, нет кучи периферии с портами ввода- вывода, нет сложных таймеров и так далее. Знай, читай себе команды по два байта из файла, сравнивай их с опкодами да и выполняй что требуется. Да и команд то всего ничего – 3. Есть и подводные камни, а куда без них? Ну что ж, давайте начнем. А начнем мы пожалуй с памяти. Понятно, что первым делом при запуске эмулятора мы должны проинициализировать нашу виртуальную машину. То есть очистить память, стек, регистры и видеопамять. Как я уже писал выше, смещение, по которому мы будем загружать нашу эмулируемую программу равно 2. До этого, то есть со смещения 0. FFh, должен находиться оригинальный интерпретатор. В нем, помимо всего прочего, присутствует маленький шрифт, который начинается со смещения 0. Его можно увидеть в исходных кодах моего эмулятора. Да, прошу прощения за свой французский Delphi, но программирую я на нем, не обессудьте. Для простоты я создал такую структуру: Display : Array . Здесь придется немножко вспомнить, кто такие биты, и как их извлекать из байтов и слов (word). Для простоты я создал процедуру Execute. Opcode(opcode: word), в которую передается опкод из двух байт, интерпретируется и выполняется. Чтобы понять смысл, можно сверятся с таблицей команд из википедии. Procedure Execute. Opcode(opcode : word); Begincase (op. Прыгаем на нужный адрес. PC : = op. Здесь нужно смотреть на 4 последних битаcase op. Во время написания интерпретатора можно пользоваться заглушками для каких- то команд. Теперь, когда мы реализуем основные команды процессора, останется сделать вывод на экран и реализовать устройство ввода. За вывод на экран отвечает команда DXYN. В регистре VX находится координата X, в регистре VY находится координата Y с которых мы должны начать рисовать спрайт. Адресный регистр I в это время указывает на битовый образ спрайта. Я не буду прилагать реализацию рисования графики, думаю тут не должно возникнуть сложностей, тем более всегда можно посмотреть в исходнике в конце данного поста. Так же и с клавиатурой. Заключение. Конечно все детали реализации я не смог упомянуть в данной заметке. Цель — просто натолкнуть на мысль и показать разбор опкодов. Если кому- то интересно, можно посмотреть мою реализацию эмулятора на Delphi, или найти другие реализации эмуляторов в интернете. Как модно говорить, тысячи их. Начиная от Visual Basic и заканчивая железными решениями. Заранее прошу прощения за мой код, я не приводил его в порядок — вылил как есть. Основной интересный файл там — hchip. Так же существует неплохой англоговорящий форум Emu. Talk, в котором специально выделена ветка посвященная эмуляции Chip- 8. Страница, на которой можно скачать наверное один из самых лучших эмуляторов chip. Да и вообще, по запросу в гугле «chip- 8» можно найти все что нужно. Что еще можно сделать? Можно немного модифицировать наш эмулятор для поддержки Super chip- 8 инструкций и спрайтов. Да много еще чего можно. Удачного всем дня.
0 Comments
Leave a Reply. |
AuthorWrite something about yourself. No need to be fancy, just an overview. Archives
November 2017
Categories |