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

image



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

Видео:



(Ссылкой: www.youtube.com/watch?v=gPSpk2gbAD4)

Статья:


Итак, как же работает картридж у 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

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



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



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

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



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

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

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