Ang3L | Дата: Суббота, 2011-01-22, 18:06:29 | Сообщение # 1 |
Бывалый
Сообщений: 88
Репутация: 2
Замечания: 0%
Не на форуме
| Вероятно, многие программисты если и не мечтали, то хотя бы задумывались о написании собственного эмулятора какого-либо процессора. Возможно, некоторые даже экспериментировали с чем-то вроде Z80. Но не многие дошли до финальной реализации эмулятора.
В этой заметке я хотел бы поговорить о создании простого эмулятора игровой платформы CHIP-8 из далеких 70-х. Во-первых, мы прикоснемся к истории, а во-вторых, эта платформа из за своей простоты позволит создать полностью функциональный эмулятор даже начинающим программистам.
Конец
Как бы это странно не было, а начну я с конца. Вот такая программа
Code 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. 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. 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' обычно используются для перемещения, хотя и не всегда так. Это зависит от игры.
Графика и звук
В нашей приставке разрешение экрана 64x32 пикселя, один цвет (монохром). Вывод реализован с помощью спрайтов, которые всегда имеют ширину 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, но программирую я на нем, не обессудьте. Для простоты я создал такую структуру:
Code 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 начиная с нуля и инициализируем все значения:
Code 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), в которую передается опкод из двух байт, интерпретируется и выполняется. Чтобы понять смысл, можно сверятся с таблицей команд из википедии: http://en.wikipedia.org/wiki/CHIP-8
Code 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 - http://rghost.ru/2262193, или найти другие реализации эмуляторов в интернете. Как модно говорить, тысячи их. Начиная от Visual Basic и заканчивая железными решениями. Заранее прошу прощения за мой код, я не приводил его в порядок — вылил как есть. Основной интересный файл там — hchip.pas, в нем реализована вся эмуляция.
Так же существует неплохой англоговорящий форум EmuTalk, в котором специально выделена ветка посвященная эмуляции Chip-8 - http://www.emutalk.net/showthread.php?t=19894.
Страница, на которой можно скачать наверное один из самых лучших эмуляторов chip8 и игры под него - http://www.pong-story.com/chip8/
Да и вообще, по запросу в гугле «chip-8» можно найти все что нужно.
Что еще можно сделать? Можно немного модифицировать наш эмулятор для поддержки Super chip-8 инструкций и спрайтов. Да много еще чего можно.
Автор: http://tronix286.habrahabr.ru/
Сообщение отредактировал(а) Ang3L - Суббота, 2011-01-22, 18:07:23 |
|
| |