Всем привет! Меня зовут Сергей. И в данной статье задену тему очередной эмуляции 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

Думаю это достаточно важный пункт, и я не заметил чтоб о нём где-нибудь вели разговор. Дело в том, что для процессора 6502 (и, видимо, производных от него) в память $FFFE записывается вектор указывающий начало программы. И при сбросе программы происходит чтение с адреса $FFFE, чтобы установить стартовый адрес программы.

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

Я наткнутся на статью, где вроде как человек указывает свой собственный 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

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

Комментарии (3)


  1. MasterMentor
    22.11.2024 14:33

    Норм статья!

    Начало я бы предложил такое:

    Зачем? Зачем плодить очередной эмулятор того, что уже сделано достаточно хорошо. ...пототму что на Паскале эмулятора Денди ещё нет!

    PS Паскаль - норм язычок. Если бы вместо annoying забивающих текст begin и end его IDE показывали { }, то адптов Паскаля точно бы поприбавилось. Хотя бы на одного человека (гарантирую).

    Такие режимы в IDE бывают?


    1. Seenkao Автор
      22.11.2024 14:33

      В Lazarus вроде можно настроить. Но меня лично вполне устраивают begin-end, в большинстве случаев IDE сама всё доделывает когда начинаешь писать begin.

      на Паскале эмулятора Денди ещё нет!

      уже давно есть. ))) Правда первые брали (в основном) за основу то что было написано на ассемблере. То ли obj, то ли dll. Ну и достаточно недавно человек перевёл Mesen на Паскаль, CanyNes.

      Но я не хочу переводить, я хочу сделать сам. )))