Очередной эмулятор Nes. Процессор

от автора

Всем привет! Меня зовут Сергей. И в данной статье задену тему очередной эмуляции Nes/Dendy/Famicon. Зачем? Зачем плодить очередной эмулятор того, что уже сделано достаточно хорошо. Можете считать это моей прихотью, а так же пробой своих сил (хотя на самом деле для пробы своих сил лучше, наверно, что-то попроще эмулировать).

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

Кому может понадобится данная информация

Для людей,которые хотят вникнуть в то как работают процессоры или другая схемотехника «внутри», такие вещи полезны. Потому что работа различной аппаратуры основанной на микросхемах может быть совершенно разной. И на примере Nes можно увидеть, что процессор должен работать с прямой логикой и передачей данных, а на деле происходит очень сложное взаимодействие CPU, PPU, APU, картриджей и другой аппаратуры.

Возможно кто-то возьмёт себе на заметку подобные решения и реализует их в других направлениях. В жизни разное бывает.

Человек сюда зашедший, будь готов впитать в себя знания 30+ лет знаний других людей. Терпения тебе в деле твоём и силы воли для преодоления препятствий! Да прибудет с тобой сила!

Правда от меня здесь только эмулятор, и пока только процессора 6502. )))

Отличия симулятора от эмулятора.

Когда-то давно, я думал что эмулятор это то, что точно воспроизводит эмулируемый объект, а симулятор — это что-то близкое к эмулятору, но он не старается эмулировать точно… Со временем я узнал, что я ошибался.

Если вспомните, то есть игры — симуляторы. Где управляешь техникой, и вот в таких играх старались воспроизвести как можно более точно действия человека изображая реальный танк, машину, самолёт или ещё что-нибудь. Так же у военных есть симуляторы полётов и разнообразной техники. Они как раз и предназначены для более правдоподобного погружения человека в окружение. Вы заходите в симулятор самолёта, а это настоящая кабина пилота, где всё с точностью воспроизведено как в настоящей кабине пилота.

В общем надо знать, что симулятор — это то, что старается как можно более точно воспроизвести реальный объект.

Эмулятор — реализует конечный результат, не зависимо от того каким образом это делается (понятно дело, что тоже стараются сделать более реальным, но до симуляторов им далеко).

Ну и для процессоров (а может и не только для них) ещё есть транслятор, который переводит коды эмулируемого устройства, в коды устройства на котором происходит воспроизведение программы.

Подготовка

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

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

  • nesdev.org — очень много информации и не только по Nes.

  • emuverse.ru — русскоязычная информация по компьютерам приставкам и прочему. Нас будет интересовать раздел MOS 6502 (к сожаленью весь раздел может содержать ошибки).

  • migera.ru — так же русскоязычный сайт, раздел по Dendy. Там же есть версия в pdf-формате. Данный сайт так же содержит некоторые ошибки.

  • книга: «Игровые приставки. DENDY[NES], GAME BOY, SEGE MEGA DRIVE, SONY PLAYSTATION. — М.; ДМК Пресс, 2002.» — советую к прочтению, содержит достаточно структурированную техническую информацию.

Так же можете ознакомится с программированием для приставок Nes. Это будет полезно, если вы ещё не сталкивались с ассемблером и не понимаете как работает данная приставка. Дополнительно можно ознакомится с определённой информацией здесь (русскоязычная информация) и здесь (тот же автор, но задета тема программирования).

Так же на ютубе есть канал где человек делает эмулятор на C++ (англоязычный). Вы можете поискать его и в VK, но не обещаю что сможете найти все его видео. Так же он выложил исходный код.

На ютубе есть канал Алексея «Кластера», он больше по железу, но возможно вы найдёте полезную информацию по разработкам Nes/Famicon/Dendy. И есть ещё плейлист у Уютного подвальчика (так и вбивайте «Уютный подвальчик») где задета данная тема и в очень интересной форме. Там, кстати, так же есть полезные вещи, ну и просто отдохнуть можно пока смотришь. )))

Скрытый текст

Блин, зачем я это рекомендовал, в очередной раз завис на их видео.

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

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

Так же, под рукой держите эмулятор который может не только запускать игры, но и производить отладку кода. Я для этого использую Fceux. Вероятнее всего, для создания эмулятора он вам пригодится (а может и нет).

Ну и вишенка на торте, делать я буду на Pascal и используя ZenGL (честно говоря хотел изначально писать на ассемблере, но решил что это будет немного зашкварно, ведь я ещё не написал ни одного эмулятора). Это будет как раз дополнительная проверка возможностей ZenGL и приложение будет кроссплатформенным (ну я же не люблю простых вещей).

Вся работа по созданию эмулятора произведена под свободной лицензией Zlib. Исходный код будет выложен (а возможно уже выложен, ссылку прикреплю).

Чего не будет в этой статье?

В статье не будет ни каких схем. Не будет азов. В некоторых местах не будет объяснений: «почему так, а не вот так?«. В общем не будет достаточно важной информации, которую выложили до меня и у меня просто не получится всё обозреть в данной статье. Статья, в основном, идёт для подготовленного человека, который знает неплохо программирование и достаточно хорошо изучил работу приставок Nes/Dendy и, возможно, решил сделать свой эмулятор. Где-то я глубоко могу капнуть, потому что это может оказаться не таким просты для понимания даже не новичкам, а где-то я просто пропущу информацию, считая что вы её уже знаете.

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

На что я надеюсь

Вы уже хорошо изучили структурную схему приставки. Ознакомились с работой процессора (хотя бы в общих чертах), видеоадаптера и многим другим. Я специально для вас предоставил информацию выше, чтоб вы могли с ней ознакомится. Если вы решились на данные действия, то всё будет достаточно не просто. Важно понять как работает сама приставка! (честно говорю, я и сам не до конца понимаю, но постараюсь избежать критических ошибок).

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

Процессор 6502

Тут, с одной стороны ни чего сложного. Выписываем все коды и делаем их обработку в бесконечном цикле (почти бесконечном, сами мы должны будем остановить виртуальный процессор своими методами).

Как же так? Спросите вы, ведь я же писал выше что всё будет достаточно не просто. И да и нет. Если вы достаточно подготовлены, то сложности будет мало. Будет в основном реализация. Но с процессором 6502 в самом деле есть сложность. Это множество мелких недочётов данного процессора (возможно в новых версиях было исправлено), которые надо будет желательно реализовать в эмуляторе, если вы хотите иметь полнофункциональный эмулятор. Например: недокументированные команды.

Допустим, «сделали мы процессор» и запустили его. Он у нас будет работать? Нет не будет. Для того чтоб процессор работал процессору нужно читать (и писать) откуда‑то информацию. А значит нужна память. Сделать её достаточно просто, объявляем массив байт от $0000 до $FFFF и вот у нас уже есть пространство куда можно записывать данные и считывать их.

Сделали память? Так вот, это только на первый момент, в дальнейшем, есть большая вероятность, что придётся её переделать, всё решиться в процессе, а пока нам нужно банальное обращение к памяти.

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

Прерывания

Прерывания по приоритету идут: Reset, NMI, IRQ/BRK.

Думаю это достаточно важный пункт, и обходить стороной его не стоит. Информации о прерываниях так же достаточно не мало. Самое важное это понять как будут работать прерывания для эмулятора. И как вообще работают они?

Reset. С данным прерыванием не должно быть проблем. Оно по сути ни чего не делает, только сбрасывает значение счётчика на начальные.

Нас, больше, будут интересовать остальные прерывания. Причина проста, если происходит прерывание BRK, то оно может не «сработать». Если в первые 4 такта (не точно) произойдут прерывания NMI или IRQ, то вместо срабатывания BRK сработают именно эти прерывания. А это означает, что в эмуляторе мы должны проработать данную ситуацию.

Если NMI и IRQ приходят одновременно, то сработает NMI, но IRQ встанет в очередь на обработку. Я не совсем понял как это должно работать, придётся разбираться в процессе.

Так же, все прерывания, кроме Reset, будут обрабатываться в том же цикле, где обрабатываются все инструкции.

Реализация инструкций

Это одно из самых долгих и муторных занятий. В данном случае надо изучить все инструкции, узнать как они работают и постараться реализовать их в программе. Изначально можно реализовать только документированные команды, но в любом случае заглушки на остальные операции надо выставлять. Допустим есть инструкция NOP, которая ни чего не делает, а только тратит место и такты процессора (и этих Nop-инструкций в Dendy очень не мало на самом деле). Можно, для начала, в качестве заглушки использовать её. А когда у вас будет рабочий процессор, вы можете уже добавлять не документированные инструкции. Хотя вы можете их сразу добавить, но ни как не обрабатывать их. Лично я бы не рекомендовал так делать, но это на ваше усмотрение.

Итак, у нас «реализована» память и инструкции, вроде должно хватить на создание процессора. Там ещё тактовый генератор надо прикрутить… но это позже. Сейчас нам надо сделать чтоб что-то работало (вы там не убили несколько дней на реализацию инструкций?).

Реализация процессора

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

Я не буду предоставлять полностью код здесь, это будет занимать очень много места и боюсь вы долго прокручивать статью будете. Здесь же покажу только предварительную реализацию.

Тут длинный кусочек кода (в коде могут быть ошибки).
{  *  Copyright (c) 2024 SSW  *  *  This software is provided 'as-is', without any express or  *  implied warranty. In no event will the authors be held  *  liable for any damages arising from the use of this software.  *  *  Permission is granted to anyone to use this software for any purpose,  *  including commercial applications, and to alter it and redistribute  *  it freely, subject to the following restrictions:  *  *  1. The origin of this software must not be misrepresented;  *     you must not claim that you wrote the original software.  *     If you use this software in a product, an acknowledgment  *     in the product documentation would be appreciated but  *     is not required.  *  *  2. Altered source versions must be plainly marked as such,  *     and must not be misrepresented as being the original software.  *  *  3. This notice may not be removed or altered from any  *     source distribution. }  const   // флаги для регистра "P"   f_N   = $80;                    // нечётность   f_V   = $40;                    // переполнение   f_nop = $20;                    // резерв   f_B   = $10;                    // прерывание (BRK)   f_D   = $08;                    // десятичный режим (не работает на Dendy)   f_I   = $04;                    // прерывание   f_Z   = $02;                    // нуль   f_C   = $01;                    // перенос   clear_C    = $FF - f_C;   clear_Z    = $FF - f_Z;   clear_I    = $FF - f_I;   clear_V    = $FF - f_V;   clear_D    = $FF - f_D;   clear_N    = $FF - f_N;   clear_B    = $FF - f_B;   clear_nop  = $FF - f_nop;   clear_VNZC = $FF - f_V - f_N - f_Z - f_C;   clear_NZ   = $FF - f_N - f_Z;   clear_NZC  = $FF - f_N - f_Z - f_C;   clear_VNZ  = $FF - f_V - f_N - f_Z;    // мнемоники, здесь у меня длинный список всех операций, в   // развёрнутом виде. Изначально приходится делать так, по той   // причине, что мы не можем видеть какие операции будут выполняться   // одинаково. И только в процессе реализации инструкций сможем   // точно определиться какие инструкции можно объеденить.   m_ADC_IMM = $69;   m_ADC_ZP  = $65;   m_ADC_ZPX = $75;   m_ADC_ABS = $6D;   m_ADC_ABX = $7D;   m_ADC_ABY = $79;   m_ADC_NDX = $61;   m_ADC_NDY = $71;     m_AND_IMM = $29;     m_AND_ZP  = $25;     m_AND_ZPX = $35;   ...   // инстукции   instructionLen: array[0..255] of byte  = (2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 2, 3, 2, 3, 3, 3,                                             3, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 2, 3, 2, 3, 3, 3,                                             1, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 2, 3, 2, 3, 3, 3,                                             1, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 2, 3, 2, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 1, 3, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 1, 3, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 2, 3, 2, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 2, 3, 3, 3, 3,                                             2, 2, 2, 2, 2, 2, 2, 2, 1, 3, 2, 3, 2, 3, 3, 3);   instructionTime: array[0..255] of byte = (7, 6, 0, 8, 3, 3, 5, 5, 3, 2, 2, 2, 4, 4, 6, 6,                                             2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,                                             6, 6, 0, 8, 3, 3, 5, 5, 4, 2, 2, 2, 4, 4, 6, 6,                                             2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,                                             6, 6, 0, 8, 3, 3, 5, 5, 3, 2, 2, 2, 3, 4, 6, 6,                                             2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,                                             6, 6, 0, 8, 3, 3, 5, 5, 4, 2, 2, 2, 5, 4, 6, 6,                                             2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,                                             2, 6, 2, 6, 3, 3, 3, 3, 2, 2, 2, 2, 4, 4, 4, 4,                                             2, 6, 0, 6, 4, 3, 4, 4, 2, 5, 2, 5, 5, 5, 5, 5,                                             2, 6, 2, 6, 3, 3, 3, 3, 2, 2, 2, 2, 4, 4, 4, 4,                                             2, 5, 0, 5, 4, 4, 4, 4, 2, 4, 2, 4, 4, 4, 4, 4,                                             2, 6, 2, 8, 3, 3, 5, 5, 2, 2, 2, 2, 4, 4, 6, 6,                                             2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7,                                             2, 6, 2, 8, 3, 3, 5, 5, 2, 2, 2, 2, 4, 4, 6, 6,                                             2, 5, 0, 8, 4, 4, 6, 6, 2, 4, 2, 7, 4, 4, 7, 7);   // Z и N - флаги   ZNTables: array [0..255] of Byte       = (f_Z, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,       // это решение я увидел в "nes9x"                                             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,                                             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,                                             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,                                             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,                                             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,                                             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,                                             0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N,                                             f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N, f_N);   type   // структура процессора. Она будет состоять не только из регистров,   // но и так же из дополнительных данных.   TNesCPU = record     regPC: LongWord;             // на самом деле 16-ти бытный должен быть, но с 32-х битными данными в новых процессорах работать проще.                                  // Потому обязательно делать проверку на выход за пределы 16-ти байтного значения.     regA, regX, regY: LongWord;     regP, regS: LongWord;        // это флаги, так будет проще эмулировать.                                  // для regS будем использовать смещённое значение? Если брать байт от этого значения, то получим нужное число.     start: Boolean;              // этот флаг только програмно можно выключить! Реализация отключения питания.     cicles: Integer;             // это "остаточные" такты. Если cicles <> 0 то надо пропускать обработку.      reg01, reg02, reg03: LongWord;   // дополнительные внутренние регистры и флаги (созданы для того, чтоб не создавать лишних переменных).     reg04, reg05: LongWord;     _flag, addCicle: Boolean;     // регистр P = (N, V, nop, B, D, I, Z, C)     // где N - флаг знака, V - флаг переполнения, B - программное     //   прерывание (BRK), D - десятичный режим (в Dendy не работает),     //   I - прерывания, Z - нуль, C - перенос.   end;  var   // вся память, $0000-$07FF - RAM, $0800-$FFFF - ROM   NesMemory: array[0..$FFFF] of Byte;   // и сам процессор   NesCPU: TNesCPU;  // очень часто используемый код, для установки флагов "Z" и "N". // но впоследствии заменено на код // (regP := regP or ZNTables[byte(regA)];) procedure SetFlagsNegZero(reg: byte); begin   with NesCPU do   begin     if reg = 0 then       regP := (regP or f_Z) and clear_N     else       if reg >= $80 then         regP := (regP or f_N) and clear_Z       else         regP := regP and clear_Z and clear_N;   end; end;  // это всё относится к ADC, похоже к любой версии. procedure inst_ADC; begin   with NesCPU do   begin     reg03 := regA + reg02 + regP and f_C;     regP := regP and clear_VNZC;     _flag := (not (regA xor reg02) and (regA xor reg03) and $80) <> 0;     if _flag then       regP := regP or f_V;     if reg04 > $FF then     begin       regP := regP or f_C;       regA := reg03 and $FF;     end;     regP := regP or ZNTables[byte(regA)];   end; end;  // то всё относится к ASL, кроме аккумулятора procedure inst_ASL(mem: LongWord); begin   with NesCPU do   begin     reg03 := NesMemory[mem];     regP := regP and clear_NZC;     if reg03 >= $80 then       regP := regP or f_C;     reg03 := (reg03 shl 1) and $FF;     SetFlagsNegZero(byte(reg03));     NesMemory[mem] := reg03;   end; end;  // восстановление "PC" из стека. procedure PopPC; begin   with NesCPU do   begin     inc(regS);     if regS > $1FF then       regS := $1FF;     inc(regS);     regPC := NesMemory[regS];     if regs > $1FF then       regS := $1FF;     regPC := regPC or (NesMemory[regS] shl 8);   end; end;  // на данный момент, у меня это не процедура, а точка входа в // программу. Но для правильной эмуляции надо делать именно // процедуру обработки циклов процессора. procedure TimeCPU; begin   with NesCPU do   begin     // это типо основной цикл. Но на самом деле не так.     // Основной цикл - это тактовый генератор, который посылает     // сигналы синхроимпульсов.     if start then     begin       if cicles = 0 then       begin         // надо прочитать данные и работать с ними.         reg01 := NesMemory[regPC];         cicles := Instruction[reg01].time;   // ииии.... дополнительное время...         // надо отметить, находятся ли данные значения на одной         // странице. Многие инструкции требуют дополнительный байт,         // если команда переходит с одной страницы на другую.         // Не реализовано! (но возможно будет реализовано, когда         // закончу статью)         inc(regPC);       // это не правильно, но пока не будем заморачиваться.         case reg01 of           0..7, 9, 11, 12, 16..23, 26, 28, 33..39, 41, 43, 48..55, 58, 60, 65..71, 73, 75, 80..87, 90, 92, 97..103, 105, 107, 112..119, 122, 124, 128..135, 137, 139,           144..151, 160..167, 169, 171, 176..183, 192..199, 201, 203, 208..215, 218, 220, 224..231, 233, 235, 240..247, 250, 252:             begin               // все двухбайтовые значения               case reg01 of                 $02, $12, $22, $32, $42, $52, $62, $72, $92, $B2, $D2, $F2: begin                   // это "убийство" программы, больше работать программа не должна, пока не придёт полный сброс?                 end;                 $04, $0C, $14, $1A, $1C, $34, $3A, $3C, $44, $54, $5A, $5C, $64, $74, $7A, $7C, $80, $82, $89, $C2, $D4, $DA, $DC, $E2, $F4, $FA, $FC: begin                   // это чёртова туча nop-ов...                 end;                 else begin                   if (regPC mod 256) = 0 then                     addCicle := true                   else                     addCicle := False;                   // считываем второе значение из памяти                   reg02 := NesMemory[regPC];                   inc(regPC);                   case reg01 of                     m_ADC_IMM: begin                       // сразу складываем значения регистра и данных идущих следом.                       inst_ADC;                     end;                     m_ADC_ZP: begin                       // второй раз читаем из памяти, из нулевой страницы                       reg02 := NesMemory[reg02];                       inst_ADC;                     end;                     m_ADC_ZPX: begin                       // второй раз читаем со смещением из нулевой страницы                       reg02 := NesMemory[reg02 + regX];                       inst_ADC;                     end;                     m_ADC_NDX: begin                       reg02 := (reg02 + regX) and $FF;                       // читаем два значения для вычисления адреса                       reg03 := NesMemory[reg02 + 1];                       reg02 := NesMemory[reg02];                       // и читаем с адреса                       reg02 := NesMemory[(reg03 shl 8) or reg02];                       inst_ADC;                     end;                     m_ADC_NDY: begin                       reg03 := NesMemory[reg02 + 1];                       reg02 := NesMemory[reg02];                       reg02 := NesMemory[(reg03 shl 8) or reg02 + regY];                       inst_ADC;                       // команда на один цикл больше, если был переход между страницами                       if addCicle then                         inc(cicles);                     end;                     m_AND_IMM: begin                       regA := regA and reg02;                       regP := regP and clear_NZ;                       SetFlagsNegZero(byte(regA));                     end;                     m_AND_ZP: begin                       reg02 := NesMemory[reg02];                       regA := regA and reg02;                       regP := regP and clear_NZ;                       SetFlagsNegZero(byte(regA));                     end;                     m_AND_ZPX: begin                       reg02 := NesMemory[reg02 + regX];                       regA := regA and reg02;                       regP := regP and clear_NZ;                       SetFlagsNegZero(byte(regA));                     end;                     m_AND_NDX: begin                       reg02 := (reg02 + regX) and $FF;                       reg03 := NesMemory[reg02 + 1];                       reg02 := NesMemory[reg02];                       reg02 := NesMemory[(reg03 shl 8) or reg02];                       regA := regA and reg02;                       regP := regP and clear_NZ;                       SetFlagsNegZero(byte(regA));                     end;                     m_AND_NDY: begin                       reg03 := NesMemory[reg02 + 1];                       reg02 := NesMemory[reg02];                       reg02 := NesMemory[(reg03 shl 8) or reg02 + regY];                       regA := regA and reg02;                       regP := regP and clear_NZ;                       SetFlagsNegZero(byte(regA));                     end;                     m_ASL_ZP: begin                       inst_ASL(reg02);                     end;                     m_ASL_ZPX: begin                       inst_ASL(reg02 + regX);                     end;                      ...                     ...                     ...                      m_SBC_IMM_EB: begin                      end;                     m_AHX_NDY: begin                      end;                   end;                 end;               end;             end;           13..15, 25, 27, 29..32, 44..47, 57, 59, 61..63, 76..79, 89, 91, 93..95, 108..111, 121, 123, 125..127, 140..143, 153, 155..159, 172..175, 185, 187..191, 204..207,      // 74           217, 219, 221..223, 236..239, 249, 251, 253..255:             begin               // все трёхбайтовые значения               if ((regPC mod 256) = 0) or (((regPC + 1) mod 256) = 0) then                 addCicle := true               else                 addCicle := False;               // считываем второе значение из памяти               reg02 := NesMemory[regPC];               inc(regPC);               // считываем третье значение из памяти               reg03 := NesMemory[regPC];               inc(regPC);               case reg01 of                 m_ADC_ABS: begin                   reg05 := NesMemory[reg03 shl 8 or reg02];                   reg04 := regA + reg05 + Byte(regP and f_C);                   _flag := (not (regA xor reg05) and (regA xor reg04) and $80) <> 0;                   if _flag then                     regP := regP or f_V                   else                     regP := regP and clear_V;                   regA := Byte(reg04);                   if reg04 > 255 then                     regP := regP or f_C                   else                     regP := regP and clear_C;                 end;                 m_ADC_ABS: begin                   // читаем из заданной памяти                   reg02 := NesMemory[(reg03 shl 8) or reg02];                   // и после этого складываем                   inst_ADC;                 end;                 m_ADC_ABX: begin                   // тут учитываем смещение за счёт регистра                   reg02 := NesMemory[(reg03 shl 8) or reg02 + regX];                   inst_ADC;                   if addCicle then                     inc(cicles);                 end;                 m_ADC_ABY: begin                   // тут учитываем смещение за счёт регистра                   reg02 := NesMemory[(reg03 shl 8) or reg02 + regY];                   inst_ADC;                   if addCicle then                     inc(cicles);                 end;                 m_AND_ABS: begin                   reg02 := NesMemory[(reg03 shl 8) or reg02];                   regA := regA and reg02;                   regP := regP and clear_NZ;                   SetFlagsNegZero(byte(regA));                 end;                 m_AND_ABX: begin                   reg02 := NesMemory[(reg03 shl 8) or reg02 + regX];                   regA := regA and reg02;                   regP := regP and clear_NZ;                   SetFlagsNegZero(byte(regA));                   // команда на один цикл больше, если был переход между страницами                   if addCicle then                     inc(cicles);                 end;                 m_AND_ABY: begin                   reg02 := NesMemory[(reg03 shl 8) or reg02 + regY];                   regA := regA and reg02;                   regP := regP and clear_NZ;                   SetFlagsNegZero(byte(regA));                   if addCicle then                     inc(cicles);                 end;                 m_ASL_ABS: begin                   inst_ASL((reg03 shl 8) or reg02);                 end;                 m_ASL_ABX: begin                   inst_ASL((reg03 shl 8) or reg02 + regX);                 end;                  ...                 ...                 ...                  m_TAS: begin                  end;                 m_LAS: begin                  end;               end;             end;           else begin                                   // 28?             // все однобайтовые значения             case reg01 of               m_ASL_ACC: begin                 if (regA and $80) > 0 then                   regP := regP or f_C;                 regA := regA shl 1;                 SetFlagsNegZero(regA);               end;               m_CLC: begin                 regP := regP and clear_C;               end;               m_CLD: begin                 regP := regP and clear_D;               end;                ...               ...               ...                m_TXS: begin                 regS := regX;               end;               m_TYA: begin                 regA := regY;                 regP := regP and clear_NZ;                 SetFlagsNegZero(regA);               end;             end;           end;         end;       end;       cicles := cicles - 1;     end;   end;   end;

Конечный код будет изменён, потому смотрите исходники проекта.

Видео, где пробегаюсь по некоторым моментам.

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

При реализации инструкций, внимательно изучите как они работают. Даже если вы программировали для Dendy/Nes на ассемблере, это может не дать вам ни какой информации о реализации способов информации. Я запускал игру в эмуляторе и смотрел как работает та или иная адресация, кроме непосредственной и абсолютной, где более-менее всё и так понятно. Так же я не подсматривал инструкции, которые производят выполнение сразу (в основном однобайтовые).

Например: обращайте внимание на выставляемые флаги. Вероятнее всего проще будет обнулить необходимые флаги, а уже после выполнения инструкции выставлять их. И, обнуление флагов тоже надо смотреть где выполнять, где-то надо их выполнять перед выполнением инструкции, хотя по большей части после выполнения (например инструкции LSR и ASL — в них надо обнулять флаги до выполнения инструкций).

На что ещё стоит обратить внимание:

1) Для более точной эмуляции, все команды должны выполняться на каждом последнем такте. Физическое устройство заканчивает работу инструкции именно на последнем такте данной инструкции.

в данной статье идёт работа инструкции от первого поступающего такта.

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

Допустим инструкция AND:

  • при непосредственной адресации инструкция должна выполняться два такта, мы можем разделить на первый такт — считывание инструкции, и второй такт — считывание данных и выполнение инструкции.

  • при абсолютной она выполняется 4 такта, что означает, что первый такт — считывание инструкции, второй такт — считывание первого значения, третий такт — считывание второго значения и четвёртый такт — выполнение инструкции.

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

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

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

Контакты для связи со мной

Почта: M12Mirrel@yandex.ru

Ютуб канал

Я на gamedev.ru

телега: @SeenkaoSerg

Всем успехов! )))


ссылка на оригинал статьи https://habr.com/ru/articles/859300/


Комментарии

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

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