Снимаем образы с картриджей для Dendy-Famicom-NES

в 10:27, , рубрики: avr, dendy, diy или сделай сам, dumper, dumping, Famicom, Nes, дамп, дампер, программирование микроконтроллеров, реверс-инжиниринг, Электроника для начинающих

Ни для кого не секрет, что сейчас можно легко скачать эмулятор почти любой игровой консоли 80х-90х и поиграть в классические игры на компьютере, телефоне и многих других платформах. В сети легко можно найти и ROM'ы этих самых игр. Зачастую люди качают их и даже не задумываются, каким же образом кто-то однажды прочитал их из картриджа. В этой статье я и постараюсь рассказать, как же это делалось в случае с NES/Famicom, которая у нас была больше известна как «Денди», и покажу, как можно сделать это самостоятельно.

image

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

Видео:

Статья:

Итак, как же работает картридж у Famicom? Многие сразу же скажут, что это просто ROM-память с параллельным доступом, и ничего сложного в её чтении быть не должно, но это не совсем так. Во-первых, в картридже сразу два типа памяти: с кодом игры, и с изображениями из игры. Каждая из них включается прямо в шину данных консоли. Первая — параллельно с оперативной памятью и процессором (CPU), а вторая параллельно с видеопамятью и видеочипом (PPU). Таким образом картридж является чем-то вроде оперативной памяти, куда уже загружена игра.

Рассмотрим же распиновку слота картриджа, и как он работает.

image
Вид на консоль сверху. Слева — передняя часть.

→ CPU A0-A14 — контакты, через которые задаётся адрес для чтения CPU памяти
↔ CPU D0-D7 — контакты, через которые мы передаём данные CPU памяти
→ PPU A0-A13 — контакты, через которые задаётся адрес для чтения PPU памяти
↔ PPU D0-D7 — контакты, через которые мы передаём данные PPU памяти
→ M2 — местный clock-сигнал, принимает высокий уровень, когда идёт обращение к CPU памяти
→ /ROMSEL — логический NAND между M2 и CPU A15, который недоступен напрямую
→ CPU R/W — определяет, тип операции: высокий уровень — чтение, низкий — запись
← /IRQ — позволяет картриджу генерировать прерывание, внутри консоли подтянут к +5В
→ PPU /RD — принимает низкий уровень, когда консоль читает PPU память
→ PPU /WR — принимает низкий уровень, когда консоль пишет в PPU память
→ PPU /A13 — просто напросто инвертированный сигнал от PPU A13
← CIRAM A10 — позволяет картриджу определять принцип зеркалирования видеопамяти в консоли
← CIRAM /CE — при низком уровне включает видеопамять внутри консоли
→ Звук (вход) — тут в картридж идёт звук с аудиочипа
← Звук (вход) — тут из картриджа идёт звук в том виде, в каком мы его уже слышим
* Земля и питание — без комментариев, напряжение 5 вольт

Теперь подробнее, немного технической информации.

CPU память консоли лежит в диапазоне между 0 и $FFFF (16 бит адресации). К картриджу обычно относятся адреса $8000-$FFFF. Обратите внимание, что при этом у нас нет контакта CPU A15, который должен отвечать на старший разряд адреса. Вместо него есть /ROMSEL, который принимает низкий уровень только в случае, когда M2 и теоретический CPU A15 одновременно принимают высокий уровень. Т.е. когда консоль читает или пишет в адреса $8000-$FFFF. Поэтому обычно его можно напрямую подключить к /CE ноге ROM-памяти. Чтение или запись выбираются через CPU R/W. Зачем нужна запись в картридж? Да много зачем, но об этом ниже.

PPU память имеет адреса от 0 до $3FFF (14 бит адресации), к картриджу при этом обычно относится 0-$1FFF. Именно в этом диапазоне хранятся изображения, и это может быть как ROM, так и RAM, но картридж сам определяет, какие адреса относятся к нему, а какие к внутренней части консоли, именно для этого используется CIRAM /CE. Обычно (почти всегда) его замыкают напрямую на PPU /A13, т.е. память консоли активируется, когда A13 равно единице — в диапазоне от $2000 до $3FFF. Обратите внимание, что внутри Famicom и NES памяти ниже $2000 и нет вовсе, она обязана быть в картридже. У PPU используются отдельные контакты для чтения и записи: PPU /RD и PPU /WR. Отдельно стоит сказать про CIRAM A10 — этот контакт определяет, как зеркалируется память в диапазоне между $2000 и $2FFF внутри консоли. Обычно это важно определить в зависимости от того, как в игре происходит движение — вертикально или горизонтально. В старых играх это было жёстко задано перемычкой на плате, в более новых обычно может меняться программно во время игры.

Да, в оригинальном Фамикоме были ещё аудиовход и аудиовыход, что позволяло картриджу быть дополнительным источником звука. Использовалось это редко, но позволяло сделать музыку в играх гораздо приятнее за счёт дополнительных синтезаторов звука. В NES этих контактов уже не было. В современных китайских «Денди» и прочих клонах их тоже не припаивают. Само собой, звуковой чип из картриджа никак не сдампить.

У NES принцип работы не отличается, хотя там у картриджей уже 72 контакта: несколько идут напрямую в гнездо снизу консоли (ни разу не использовалось ни в одной игре), плюс четыре идут на чип для защиты от пиратства.

Перейдём к практике.

Итак, вроде ничего особо сложного нет. Надо просто как-то прочитать все данные по всем адресам и сохранить их в NES-файл. Для этого я решил взять два микроконтроллера ATMEGA64. Да, это очень избыточно, но мне просто нужно огромное количество ног – у картриджа их всё-таки 60. Хотя CPU и PPU память не нужно читать одновременно, и их можно было бы подключить к одним и тем же ногам, но для первого эксперимента я решил их изолировать. Тем более так гораздо проще разводить плату, двустороннюю делать мне совсем не хотелось.

image

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

После сборки и печати корпуса устройство получилось таким:

image

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

Всё ли так просто? Увы, на самом деле нет. Жизненный срок у NES и Famicom был достаточно долгим, и разработчики игр очень быстро (уже в 85м году) столкнулись с тем, что при таком подходе в картридж можно впихнуть очень мало информации. И вовсе не из-за его малого объема, а из-за того, что адресное пространство для кода ограничивалось этими самыми $8000-$FFFF, а это всего-то 32 килобайта. В такой размер вписывались только самые простейшие игры типа «Battle City», «Ice Climber», «Duck Hunt», «Tetris», «Lode Runner». Проще говоря, всё то, что мы привыкли видеть на сборниках типа «9999999 in 1» с повторяющимися играми.

Так в картриджи начали ставить мапперы.

image

Это такие микросхемы, которые отвечают за переключение банков памяти, в результате чего появилась возможность существенно расширить адресное пространство. Представьте, что по какому-то адресу хранится код первого уровня игры. Вы его проходите, маппер переключает банк памяти, и в результате абсолютно по тому же самому адресу считывается уже код не первого, а второго уровня. Аналогично и с видеопамятью.

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

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

К слову, в картриджах чего только не было. Помимо ROM-памяти и мапперов туда ставили и дополнительную оперативную память (иногда с батарейкой для возможности сохраняться в игре), всякие счётчики времени, описанные выше синтезаторы звука и многое другое вплоть до модема. Увы, у нас в стране в девяностые лицензионных картриджей было днём с огнём не сыскать, а пираты не сильно заморачивались, и игры с такими наворотами тут не продавались.

Я решил реализовать хотя бы чтение игр на самых популярных мапперах. Клиентскую часть к дамперу я пишу на C#, поэтому просто описал интерфейс IMapper и класс, который соответствует каждому мапперу:

image

У каждого реализованы методы для дампинга данных. Вот как выглядит метод чтения программной памяти игры на MMC3 маппере:

        public void DumpPrg(FamicomDumperConnection dumper, List<byte> data, int size)
        {
            dumper.WritePrg(0xA001, 0);
            byte banks = (byte)(size / 0x2000);
            for (byte bank = 0; bank < banks-2; bank += 2)
            {
                Console.Write("Reading PRG banks #{0} and #{1}... ", bank, bank+1);
                dumper.WritePrg(0x8000, 6);
                dumper.WritePrg(0x8001, bank);
                dumper.WritePrg(0x8000, 7);
                dumper.WritePrg(0x8001, (byte)(bank | 1));
                data.AddRange(dumper.ReadPrg(0x8000, 0x4000));
                Console.WriteLine("OK");
            }
            Console.Write("Reading PRG banks #{0} and #{1}... ", banks-2, banks-1);
            data.AddRange(dumper.ReadPrg(0xC000, 0x4000));
            Console.WriteLine("OK");
        }

Если кому интересно, описание этого маппера можно почитать здесь: wiki.nesdev.com/w/index.php/MMC3

Я решил попробовать побыть на месте первопроходцев и сдампить картридж вот с таким необычным меню:

Снимаем образы с картриджей для Dendy-Famicom-NES - 7

Для этого я прочитал сначала картридж так, как если бы там не было маппера, запустил его на эмуляторе и начал дизассемблировать. Вскоре я нашёл нужную мне инструкию:

Снимаем образы с картриджей для Dendy-Famicom-NES - 8

После этого я прочитал картридж снова, предварительно выполнив запись по адресу $B600, и получил уже вполне работоспособный ROM. Само собой, игры в нём не запускаются, ведь для этого нужно снова переключать банки памяти. И даже если я прослежу, что же происходит в момент выбора игры в меню, и прочитаю весь картридж, эмулятор скорее всего никак не сможет всё это запустить.

Ещё мне в руки попал лицензионный картридж одной из самых культовых игр тех времён — «The Legend of Zelda». Он без проблем работает и с дампером, и с Фамикомом через простой пассивный переходник. Делать дамп этой игры смысла нет, она заинтересовала меня другим. В этом картридже стоит дополнительная RAM память и батарейка, что позволяет сохраняться в игре. Лежит эта память в диапазоне $6000-$7FFF. Я попробовал её прочитать и скормить эмулятору. Он без проблем её понял. После этого я ради эксперимента решил увеличить в ней число сердечек и записать назад в картридж. Сработало.

Снимаем образы с картриджей для Dendy-Famicom-NES - 9

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

Многие наверное спросят, зачем я вообще за это взялся, когда почти любой ROM можно найти в сети. Да банально из любопытства и самообразования. Было интересно посмотреть, что происходит внутри этих картриджей, и как всё это работает. К тому же им можно как читать, так и записывать картриджи. Но об этом в следующий раз.

Ссылки на исходники:
github.com/ClusterM/famicom-dumper — сам дампер (исходники на C, разводка платы, 3D модельки корпуса)
github.com/ClusterM/famicom-dumper-client — клиент на C#

Автор: ClusterM

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js