Вступление
В далёком уже 2015 году Алексей aka ClusterM опубликовал статью про то, как он сконструировал дампер на двух atmega64. В статье говорилось о принципах взаимодействия консоли с картриджами и как можно сдампить игру, не разбирая сам картридж по запчастям.
В свою очередь, я не буду дублировать то, что он уже рассказал. Расскажу же о нюансах, которые поджидают того, кто решил собрать свой собственный дампер.
WARNING
Я не настоящий «сварщик» и какие-то моменты людям, тесно общающимся с цифровой электроникой, могут показаться откровенно глупыми или даже вредными. Если такое встретите, прошу поправить меня в комментариях или в ЛС. Спасибо!
Приступим
Цели при сборке своего дампера я ставил следующие:
-
Использовать доступные и не дорогие компоненты (см. полупроводниковый кризис);
-
Минимальная обвязка рассыпухой в виде резисторов, конденсаторов и транзисторов;
-
Не уходить вглубь, прибегая к более сложным STM32 или даже ПЛИС. Проект скорее учебный и имеет цель немного набрать скиллов, но не пытаться объять необъятное на коротком промежутке времени;
-
Набрать минимальную базу скиллов, инструментов и компонентов, если вдруг решу собрать для себя какой-нибудь интересный девайс в будущем;
-
Ну и кроме того, у меня в шкафу почти 25 лет пылятся десяток картриджей, а от прошлых «моргалок диодами» осталось несколько контроллеров atmega8 и atmega32. ROMы для некоторых из картриджей в сети я не нашёл и решил, что это будет отличный повод для почесать руки.
И, да. Несмотря на всю простоту устройства, хочу сразу предупредить: занятие железом — это ДОРОГО и сильно бьёт по вашему кошельку. Для помигать светодиодиком хватит ISP программатора, простого контроллера, светодиода, пары батареек и мультиметра. Но если есть желание сделать что-то сложнее, то без осциллографа, логического анализатора и хорошей паялки уже не обойтись. И это необходимый минимум, т.к. по началу многие вещи неочевидны, а увидеть, что происходит на линиях, можно только соответствующими приборами. В простых случаях можно обойтись и светодиодом с мультиметром, но увидеть частоту мигания светодиода в десяток килогерц уже сложно.
О дамперах и программаторах
Принципиально дампинг можно разделить на следующие виды:
-
Шинный дампер (о котором эта статья). Суть его заключается в том, что дампер, эмулируя консоль, байт за байтом считывает данные из ПЗУ картриджей и формирует конечный файл ROM’а, понятный эмулятору. Но не все картриджи так можно сдампить. Во-первых, нам надо знать маппер (о них можно прочесть в статье Алексея по ссылке в начале поста). А во-вторых, картридж — это не какая-то флешка или жёсткий диск, а вполне законченное устройство, с которым мы должны работать по ЕГО условиям и никаких стандартов там нет.
К плюсам шинного дампера можно отнести тот факт, что нам не требуется разбирать картридж, выпаивать оттуда ПЗУ и считывать его. Достаточно вставить его как в обычную консоль, подать определённые команды мапперу и считать последовательно данные. Если, конечно, мы знаем маппер и можем подать ему соответствующие команды. -
Дампинг ПЗУ. Здесь всё просто: выпаиваем ПЗУ, вставляем в программатор и считываем содержимое. Если не предполагается конфликтов на шине, то можно и не выпаивать.
-
Реверс железной части картриджа. Тут, пожалуй, пояснять нечего. На это способны, пожалуй, отчаянные профессионалы.
Первый дампер
Напомню картинку из поста Алексея. Слот для картриджей в консоли выглядит так:
Как видно, он имеет 60 пинов, из которых 4 отводится на питание и землю, 2 звуковых используются в картриджах, которых мы в своё время не видели, 1 под сигнал прерывания, формируемый маппером и PPU /WR — низкий уровень на котором означает запись в область PPU (он для дампинга не нужен, но об этом ниже). И CIRAM /CE — им картридж включает видеопамять внутри консоли. В дампере видеопамяти нет, так что тоже выкидываем. Итого 51 пин, который требуется для дампа большинства картриджей.
Итак, у меня в наличии atmega8 и atmega32. Наперёд скажу, что для дампера хватит и atmega8, но об этом позже.
В atmega8 — 22 порта I/O, а в atmega32 — 32, и их явно не хватает, чтобы охватить все нужные линии. И именно поэтому в первой ревизии я объединил шины адресов CPU+PPU (AX) и шины данных (DX). 16 портов на адреса и 8 — на данные = 24 линии и ещё 8 остаётся. Это была моя первая ошибка. Несмотря на то, что при чтении данных из PPU нам надо на линии PPU /RD иметь низкий уровень, у нас всё равно возникнет конфликт на шине с PRG, т.к. там нет отличительных сигналов при чтении данных. Подал на шину адрес, на выход сразу получи данные. Впрочем и этот дампер может кое-какие картриджи сдампить, но таких единицы.
Эта ревизия была экстремально экономичной: контроллер, слот для картриджа, китайский переходник UART-USB на базе клона FTDI232 и шнур USB к компьютеру.
Вторая версия
Предыдущая ревизия пролежала год на полке, пока я со свежей головой не вернулся к проекту. Понятно, что ног у атмег для прямого включения явно не хватает, поэтому потребуется обвязка в виде сдвиговых регистров. Часто они упоминаются в статьях, где управляют какими-либо цифровыми индикаторами или жк-дисплеями, чтобы сэкономить порты на контроллере. Как раз то, что и требуется. Итого, вместо 30 портов на шины адресов потребуется всего 7. Нужно собрать два каскада для PRG и PPU, каждый из которых состоит из двух регистров 74HC595N. Оба каскада управляются портами в количестве 3 штук (ST, SH, /MR), а для последовательного ввода данных и для включения/выключения регистра — отдельные четыре порта для каждого из каскадов — по 2 на каждый. Итого 7 портов вместо 30.
Совет по выбору сдвиговых регистров
Здесь стоит отметить, что по невнимательности для промежуточной ревизии я использовал регистры 74HC164N. Плохая идея. Дело в том, что в регистрах без защёлки во время ввода адреса на шины адресов подаются все промежуточные значения, отчего некоторые картриджи выдавали мусор вместо данных, не успевая обработать такую быструю смену адресов. После перехода на 74HC595N всё стало работать как надо.
Итак: 7 портов для шины адресов, 2 регистра полностью под шины данных (PRG и CHR соответственно), плюс целый регистр и 1 порт от первого для вспомогательных функций.
Я намеренно привожу её в виде картинки, а не файла для печати, так как повторять её смысла нет. То есть просто для ознакомления, как оно выглядело. И, да, как я уже говорил: я не настоящий «сварщик». Компактностью похвастать не могу.
Несколько слов о вспомогательных сигналах
Помимо очевидных линий выбора адреса и отправки/получения данных, есть и вспомогательные, которые потребуются для общения с картриджем. Здесь я повторю небольшую часть из статьи Алексея:
-
M2 — clock-сигнал тактов процессора. Частота около 1.8 МГц;
-
CPU R/W — на линии должен быть низкий уровень, если мы отправляем команду мапперу (фактически как бы «пишем» по конкретному адресу CPU некоторое значение). Да, прошить в картридж новую игру, записывая так данные, не получится 🙂 ;
-
PPU /RD — на линии должен быть низкий уровень, если мы хотим прочитать данные из PPU;
-
/ROMSEL — в совокупности с M2 (логический NAND с внутренней линией A15, недоступной напрямую) определяют старший бит адреса PRG, к которому производится обращение;
-
CIRAM A10 — при помощи этой линии мы можем спросить у картриджа принцип зеркалирования.
Для чтения из PRG нам достаточно в любой момент времени перевести порты шины данных PRG в режим ввода, на шину адреса PRG + /ROMSEL подать нужный адрес, инвертировав старший бит (т.е. 15 бит адреса подаются на линии A0-A14, а инвертированный старший бит на линию /ROMSEL), после чего считать значение из регистра ввода.
Примерно то же самое и с PPU, но с тем отличием, что сперва нужно подать низкий уровень на линию PPU /RD и на линию PPU /A13 подать 13-й инвертированный бит адреса. Считываем значение с шины данных PPU и возвращаем на PPU /RD высокий уровень.
Чуть сложнее с записью в картридж. На линию CPU R/W нужно подать низкий уровень, сигнализируя мапперу, что мы планируем произвести запись в его регистры. Так вот, согласно документации, смена уровня на линии CPU R/W возможна только в момент, когда на линии M2 низкий уровень. А это значит, что на линии M2 нам надо держать постоянно высокий уровень, и в момент переключения с режима чтения в режим записи и наоборот, нам в этот момент на M2 нужно устанавливать низкий уровень. После чего в интервале между CPU R/W lo и CPU R/W hi произвести запись. Однако момент времени, когда нужно выставлять адрес и данные, я нигде не нашёл. Пришлось подсмотреть у Алексея. Да, это старая версия его дампера и у него есть новая, но там уже другой подход, не совместимый с описываемым здесь.
Однако имеется ещё линия PPU /WR, про которую я выше писал, что в большинстве случаев она не потребуется. Дело в том, что в консоли эта линия используется для картриджей, у которых CHR хранится не в ПЗУ, а в оперативной памяти картриджа. Это так называемый CHR-RAM режим. Графика (а точнее таблица знакогенератора) формируется во время выполнения игры. В качестве примера можно привести маппер UxROM. Под него написаны такие игры как: Duck Tales, Prince of Persia и т.д. Поскольку цель дампера состоит в том, чтобы считать данные из картриджа, а не записать в RAM свои данные, то этот режим я не стал использовать. Не буду исключать, что существуют мапперы, где запись именно в область PPU как-то переключает банки ПЗУ, но я о таких не слышал. Если кто-то про такие знает, дайте знать в комментариях.
В конечном итоге, была собрана вторая версия дампера. На скорую руку был написан клиент для дампера и началось самое интересное.
Сперва в ход пошли известные одноигровки, ROM’ы которых не составляет труда найти в интернете: Prince of Rosia (да-да, был такой «адаптированный» для нас китайцами Prince of Persia), Bucky O’Hare. Банки в мапперах переключаются, данные считываются и, вроде, можно праздновать победу. Но дальше захотелось сдампить многоигровки. И вот тут возникла проблема.
В китайских мапперах многоигровок надо произвести запись в его регистры, чтобы банк с игрой. Как правило принцип следующий (упрощённо): на борту картриджа стоит ПЗУ большого объёма, у которой есть линии A16, A17… и т.д. Консоль может адресовать только младшие 16 бит, а старшие биты ПЗУ управляются маппером. Например, если в картридже 4 игры, то в зависимости от того, какую игру в меню вы выбрали, маппер подаёт высокий уровень на линии старших адресов ПЗУ и консоль думает, выбирая таким образом область в ПЗУ с нужной игрой. Как правило, китайские мапперы — это надстройки над популярными. Например, MMC3. Запись в регистры маппера высокого уровня включает соответствующий (БОЛЬШОЙ!) банк для MMC3 маппера, а тот уже управляет своими маленькими, словно других и нет.
Проблема, которую я упомянул выше, заключается в том, что запись в регистры «надмаппера» не давала никакого эффекта. Зачастую это области адресов $5000-$7FFF (есть, конечно, и те, которые держат регистры в области $8000-$FFFF). К примеру, для управления маппером #49 нужно включить WRAM, записав #$80 по адресу $A001, а затем, через запись в регистр по любому адресу в интервале $6000-$7FFF включить БОЛЬШОЙ банк с выбранной игрой и передать ей управление. Так вот запись в эти адреса не меняла состояние маппера. Несколько раз переписывал код записи в PRG, менял местами фронты сигналов, но безрезультатно. И поэтому появилась…
Третья версия
В конце концов я обратился Алексею с этим вопросом. Параллельно, пока ему писал, наткнулся на тему на одном из форумов, в которой он сам задавал этот же вопрос. Так вот: статическим дампером (а предыдущие версии были именно такие: уровень M2 был постоянным и менялся только в момент переключения режима чтения записи CPU) сдампить многоигровки не получится. И вот почему.
Когда вы вставляете картридж с несколькими играми в консоль, то при её запуске на экране появляется меню с выбором игр (есть разновидность картриджей, где игры переключаются путём нажатия на RESET на консоли). Вы выбираете игру, запускаете, и какое-то время играете. Если надоело в неё играть, то нажимаете RESET и перед вами снова появляется меню выбора игр, где можно выбрать другую игру. Казалось бы ничего необычного, что тут такого?
Но работает это не так просто, как кажется. Каждая игра в самом конце содержит три вектора прерывания: NMI, RESET и IRQ. При запуске игры они размещаются в конце адресного пространства CPU при запуске игры. Когда нажимают RESET, процессор останавливается, затем, после того, как кнопку отпустили, он запускается заново. При этом он считывает адрес вектора RESET и туда передаёт управление. Здесь ключевой момент в том, что питание с картриджа в этот момент не снимается, а содержимое RAM консоли сохраняется. Однако, маппер включил банк с игрой, соответственно в адресном пространстве CPU хранится вектор RESET включённой игры, да и сам код прерывания от игры. Как же маппер узнаёт, что был нажат RESET, чтобы вернуться обратно в меню?
Те, у кого сохранились старые многоигровые картриджи с кучей микросхем на плате, могут увидеть в углу один или два счётчика серии 74XX, резистор, конденсатор и диод. Так вот, при нажатии на RESET, как писалось выше, процессор останавливается и clock на M2 останавливается. В этот момент, на счётчик 74XX попадает низкий уровень и он сбрасывается, включая обратно банк с меню. Эта самая небольшая обвязка из сигнала M2 формирует логическую единицу, как бы выравнивая тактовый сигнал в высокий уровень, и удерживает состояние счётчика в стабильном состоянии. Но при нажатии на RESET M2 пропадает и на линию сброса счётчика падает логический ноль, тем самым обнуляя его. Это простейшая схема сброса. Мапперы посложнее эту логику уже содержат в себе.
В предыдущей версии при переключении CPU R/W я устанавливал низкий уровень на M2. Но atmega не самая быстрая и выполнить несколько операций за ~1/[2 * 1800000] долю секунды не в состоянии. Поэтому на линии сброса маппера попадает лог0 и он считает, что был выполнен RESET, сбрасывая значения регистров. И вот тут, казалось бы, тупик. Я пытался из атмеги выжать таймерами, шимами и просто циклами нужную частоту, но тщетно. Простой цикл while() который дёргает порт в противоположные состояния при работе от внутреннего RC-генератора на максимальной его частоте в 8 МГц давал около 1 МГц на выходе. Но это максимум. При этом процессор целиком занят только генерацией тактового сигнала. А значит силами самого контроллера задачу эту решить невозможно. Что ж, значит нужно решать это внешними компонентами.
Компонентная база
Как я и обещал, компоненты будут бюджетные, при этом не нужно обвешивать всё конденсаторами, резисторами и транзисторами.
Плюс 4 сдвиговых регистра типа 74HC595N и 1 сдвиговый регистр с параллельными входами 74HC165N (но о нём чуть позже).
Да, генераторы дороже кварцевых резонаторов, но желание не обвешиваться дополнительной рассыпухой победило. Кроме того, стоят они 200-300р за штуку, что, как мне кажется, небольшая плата за компактизацию компонентной базы.
Идея состоит в следующем: на M2 будет подаваться тактовый сигнал от генератора 1,792 МГц, что довольно близко к оригинальной тактовой частоте, кроме того этот сигнал так же будет подаваться и на один из портов контроллера. Генератор же в 16 МГц будет тактировать сам контроллер через выходы XTAL: во-первых, внутренний RC-генератор весьма не стабильный — от касания пальцем частота уплывала на несколько килогерц от изменения температуры корпуса, а во-вторых, 16 МГц лучше 8 МГц. Кроме того, на частоте 16 МГц можно себе позволить baud rate аж в 500.000! Что весьма неплохо для девайса такого уровня.
Подключение генератора 1.792 МГц к контроллеру требуется для синхронизации во время переключения режима CPU R/W. Как я уже писал выше, делать это нужно в момент лог. 0 на линии M2. Но как это сделать? Можно делать через прерывания, задав настройку срабатывания по спадающему фронту на порту INT0, но мне этот способ показался довольно некрасивым. Да и обработчик прерывания может быть ровно один, а синхронизировать надо разные импульсы. Поэтому я сделал ожидание нужного уровня через обычный цикл while, который будет ожидать нужного уровня на нужном порту. Как показала практика, синхронизация происходит ровно в середине такта на нужном уровне.
Теперь об обратном сдвиговом регистре 74HC165N. Как показала практика, некоторые картриджи с умирающей ПЗУ, при чтении данных из PPU выдают паразитные импульсы. Импульсы иногда такие, что контроллер уходит в перезагрузку. Возможно, я что-то не учёл в цепях питания контроллера, но это непринципиально, теперь эти все пульсации достаются регистру, а он их спокойно переваривает. Кроме того, запись в PPU не планируется, поэтому можно сэкономить ещё несколько ног контроллера, подключив его на шину данных PPU.
После сборки этой версии наконец всё получилось. Китайские мапперы держат свои регистры, так как на M2 подаётся вполне легальный тактовый сигнал с нужной частотой, банки переключаются, и мне даже удалось сдампить парочку многоигровок.
Но и даже тут меня ждал сюрприз. Маппер UNROM перестал реагировать на команду переключения банков. Там, напомню, переключение банка осуществляется путём записи значения с номером нужного банка в интервал $8000-$FFFF. Теперь всё стало работать ровно наоборот: простые картриджи не реагируют на команды, а сложные мапперы работают. Пришлось подключать лог.анализатор и наблюдать за сигналами:
Как видно из диаграммы, /ROMSEL принимает нужный низкий уровень МЕЖДУ переключениями режима CPU RW, и момент записи в регистр происходит по переднему фронту /ROMSEL при низком CPU RW. В этот момент всё сошлось окончательно: достаточно сдвиговому регистру подать на вход адрес c поднятым старшим битом. Наконец заработали и простейшие мапперы и хитрые китайские.
Немного о зеркалировании PPU
Про суть зеркалирования PPU рассказывать много не буду. Об этом много где написано, даже здесь есть статьи про это. Расскажу про то, как определить зеркалирование, которое требует игра. Алгоритм делится на следующие этапы:
-
Установить лог.1 на линию PPU A10 и лог.0 на PPU A11;
-
Прочесть состояние линии CIRAM A10;
-
Установить лог.0 на линию PPU A10 и лог.1 на PPU A11;
-
Снова прочесть состояние линии CIRAM A10.
Если в результате получилось 01 — вертикальное зеркалирование, 10 — горизонтальное. Таким образом при сборке ROM’а перед дампингом можно сразу записать в заголовок правильное значение.
Бюджет
Список компонентов не слишком высокотехнологичный и не требует каких-то особенных навыков. Для такого дампера даже не обязательно разводить самому или где-то заказывать плату, можно собрать на макетной плате и он будет работать.
-
Atmega32A-PU: 600р (около 8 баксов);
-
Сдвиговые регистры: 20-50р за штуку, т.е. около 150р за всё (пара баксов);
-
60-пиновый слот: на алиэкспрессе от 150 до 200р за штуку (те же пара баксов);
-
Переходник FTDI232: на том же алиэкспрессе 100-150р;
-
Два кварцевых генератора: по 250р за штуку (около 6 баксов в сумме).
Итого (если не считать текстолита, на котором была разведена плата) получилось около 1.5 т.р., что считаю ультрабюджетно за такой девайс с текущими ценами на рынке радиокомпонентов.
Итог
Результатом я остался доволен. Впереди ждут нескучные часы по подбору китайских мапперов для тех многоигровок, что есть у меня в наличии, чтобы можно было собрать ROM. Дампер работает довольно шустро с baud rate 500к и классическая одноигровка на MMC3 дампится примерно за 20-25 секунд, что вполне прилично. При этом все компоненты в DIP-корпусах, не требуется навесной монтаж.
Теоретически, можно на шину данных PRG повесить двунаправленный сдвиговый регистр, умеющий работать в обе стороны, и тогда по количеству ног можно уложиться даже в atmega8. Думаю, что она тоже справится с этой задачей.
Ну и как водится, для тех, кому интересно поковырять, прикладываю ссылку на проект.
https://github.com/loginsinex/dendy-dumper
PS: Если нужно могу дать ссылок на то, где можно заказать компоненты. Но, как мне кажется, про алиэкспресс знают все. 🙂