Задача: issue #527 — проверить особенности работы игры, описанные в чате nesdev Discord, и проверить работоспособность игры в Breaknes.
Итог:
- Игра не загружалась в Breaknes: тип платы
NAMCOT-3305отсутствовал в семейном индексеNescartdb/boards/index.json,PcbLoaderне мог разрешить его в определение платы. Добавлено отображениеNAMCOT-3305 → nrom.json— картридж загружается. - После фикса игра загружается и проходит свою загрузочную последовательность (сторожевая
строка
"YAMAMO", флаг первой загрузки$26), но не выводит титульный экран: обработчик NMI игры повторно входит в себя по 5 раз за кадр, переполняя стек. Причина — ошибка тайминга записи$2000в PPUSim (см. §4): бит$2000[7](разрешение NMI) кратковременно сбрасывается во время записи, из-за чего линия NMI дёргается и 6502 повторно принимает NMI в середине обработчика. - Механизм глитча из nesdev Discord (порча RAM, обходящая проверку первой загрузки) подтверждён и воспроизводится принудительной записью сторожевой строки в RAM до запуска.
1. Идентификация картриджа
Дамп идентичен записи nescartdb (MD5 b63a55584796c106bcfd3e2c08ea1e3c; PRG CRC32
AF6E8571, CHR 6DC7E8EA, суммарный CRC EB764567):
| Параметр | Значение |
|---|---|
| Игра | The Tower of Druaga (Namco, Famicom, 1985-08-06) |
| Плата | NAMCOT-3305 (PCB 3305), класс NAMCOT-3305 |
| iNES Mapper | 0 |
| PRG | 32 КБ (2×16K) |
| CHR | 8 КБ |
| Зеркалирование | вертикальное (заголовок iNES бит 0 = 1; профиль nescartdb "Mirroring: Vertical"; паяные площадки платы h=1 = H Scroll, VRAM_A10 = PA10) |
| CIC | отсутствует |
NAMCOT-3305 электрически эквивалентен NROM-256: на плате только два масочных ПЗУ
(PRG Fujitsu MB83256, CHR Sharp LH2367), без дополнительной логики. Та же плата у
Pac-Land. Зеркалирование согласуется по трём источникам; в Breaknes перемычка скролла
следует за заголовком iNES (ApplyScrollFromHeader), что совпадает с реальной платой.
2. Механизм "первой загрузки" (сторожевая строка)
Загрузочный код по вектору $8000 (подтверждено дизассемблированием и эмуляцией):
$801E-$8031 очистка $0500-$07FF (косвенная запись STA ($00),Y; $0000-$04FF не трогается)
$8033-$8040 проверка: RAM $0002-$0007 == ROM $8044-$8049 ("YAMAMO"), 6 байт
$803B BNE $804A -> не совпало: первая загрузка
$8042 BEQ $8072 -> совпало: "тёплый" сброс (продолжение состояния из RAM)
$8044-$8049 данные: "YAMAMO"
$804A-$8052 [первая загрузка] очистка нулевой страницы $0000-$00FF
$8054-$805A [первая загрузка] JSR $91A2, JSR $938D, JSR $91B4 (инициализация)
$8061-$806C [первая загрузка] копирование "YAMAMO" из $8044 в $0002-$0007
$806E-$8070 [первая загрузка] $26 = 1 (флаг первой загрузки)
$8072+ общий путь: JSR $A120, инициализация скролла/рекордов и т.д.
Если в RAM при старте уже лежит "YAMAMO" в $0002-$0007, игра считает это продолжением
(сброс без выключения питания) и восстанавливает состояние из RAM — ровно то, что описал
ykst в nesdev Discord.
В Breaknes: при питании SRAM обнуляется (BaseBoard::SRAM, "all memory cells read as 0") —
детерминированный выбор эмулятора; у реальных SRAM состояние при включении не определено.
Для этой игры нулевая RAM даёт корректную первую загрузку. При сбросе (Board::Reset)
SRAM не очищается, поэтому повторный сброс находит сторожевую строку → "тёплый сброс",
что соответствует реальному поведению.
3. Глитч (RAM corruption, nesdev Discord)
Быстрое выключение/включение питания частично портит RAM. Если сторожевая строка сохранилась, а остальное состояние испорчено, игра идёт по ветке "тёплого сброса" с повреждёнными данными: неверный рекорд, сигнал (возможен вход в отладочный режим), обилие экипировки на 1-м этаже, вплоть до мгновенной концовки.
Воспроизведение в Breaknes: записать в RAM $0002-$0007 строку "YAMAMO" и испортить остальную
RAM (DebugHub memMap / отладчик), затем запустить — игра уходит в ветку $8072 без
инициализации первой загрузки (флаг $26 остаётся мусорным, нулевая страница не очищается).
Проверено headless-сборкой (режим --glitch тестового харнесса, см. Temp/boottest.cpp).
4. Проблема с выводом титульного экрана (NMI storm)
После загрузки игра входит в холостой цикл $80E1 (ожидание $0C) с включённым NMI.
Обработчик NMI ($828E) в самом начале выполняет OAM DMA (STA $4014 = $05, 513 циклов),
затем пишет $2000 = $B4 ($82A3) и только потом, в $82FD, читает $2002 (сброс флага
VBlank). В Breaknes это приводит к следующему:
- Пока флаг VBlank установлен, линия NMI низкая. Запись
$2000в обработчике вызывает кратковременный (4 такта) сброс бита$2000[7](разрешение NMI /wire.VBL) — линия NMI дёргается вверх-вниз, и детектор фронта 6502 получает новый фронт. - 6502 повторно принимает NMI в середине обработчика (I=1 не блокирует NMI): ещё одна
OAM DMA (513 циклов) и ещё 7 байт стека. Обработчик так и не доходит до
$82FD, флаг не сбрасывается — шторм повторяется на V=241, 245, 250, 255, 260 (5 вхождений за кадр). - Стек переполняется, игра зависает с чёрным экраном (титул не рисуется).
Доказательства (headless-сборка, плата Famicom/HVC):
- Трассировка Nintendulator: 5 вхождений в NMI за кадр (V=241, 245, 250, 255, 260),
стек монотонно убывает (S: F8→F1→EA→E3→DC→...), обработчик прерывается на
$82A8. $2002показывает vb=1 почти постоянно; при чтении$2002вне обработчика флаг чист.- Патч ROM "убрать
STA $2000из обработчика NMI" — шторм исчезает (3 вхождения за 4 кадра вместо 10), игра работает. - Патч ROM "читать
$2002сразу после DMA в обработчике" — шторм также исчезает. - Для сравнения: SMB (обработчик NMI которого рано снимает линию NMI) работает корректно; DK (не использует NMI) — корректно.
Вывод и исправление: корневая причина — формирование /DBE (входа n_DBE PPU) на плате.
/CS PPU (выход LS139, стробируемый M2) активен не только в фазе PHI2, но и в 3 тактах
PHI1 перед ней (M2 опережает PHI2). Регистры PPU сэмплируют шину данных по уровню, пока
активен строб записи — а в этих 3 тактах PHI1 на шине ещё лежит значение предыдущего чтения
(например, старший байт операнда STA). Поэтому запись $2000 = $B4 сразу после чтения
операнда кратковременно сбрасывает бит CTRL0[7] (NMI enable) в 0. Из-за задержки в один
такт (PPU обновляет выход NMI после того, как ядро сэмплирует вход) детектор фронта 6502
видит ложное снятие/установку линии NMI и повторно принимает прерывание.
При этом для чтений /CS обязан оставаться активным всю фазу PHI1: ядро 6502
сэмплирует шину данных в начале PHI2, поэтому PPU должен выставить данные чтения заранее
(в PHI1). Гейтирование всего /CS фазой PHI2 ломает чтения.
Исправление (только на плате, сигналы интерфейса PPU не меняются): /CS остаётся
активным весь цикл для чтений, но для записей стробируется фазой PHI2:
// NESBoard.cpp / FamicomBoard.cpp
ppu_inputs[PPUSim::InputPad::n_DBE] = OR(PPU_nCE, AND(NOT(CPU_RnW), NOT(apu->GetPHI2())));
- чтение (
RnW = 1):/DBEактивен весь цикл → PPU выставляет данные заранее → чтения работают как раньше; - запись (
RnW = 0):/DBEактивен только в PHI2 (данные на шине валидны) → PPU не сэмплирует устаревшие данные PHI1 → глитчCTRL0[7]исчезает → шторм NMI прекращается.
После исправления игра загружается, проходит загрузочную последовательность и выводит
титульный экран (проверено headless-сборкой: 1 NMI за кадр, "YAMAMO" в $0002-$0007,
$26 = 1, OAM DMA работает). SMB и Donkey Kong работают как раньше.
5. Изменения в репозитории
Nescartdb/boards/index.json:NAMCOT-3305 → nrom.json(плата электрически NROM-256).Breaknes/BreaksCore/NESBoard.cpp,Breaknes/BreaksCore/FamicomBoard.cpp: исправление формирования/DBEPPU (гейтирование записей фазой PHI2) — устранён NMI-шторм, из-за которого игра не выводила титульный экран.UnitTest/CartPcbTest.cpp: регрессионный тестTestNamcot3305Board(идентификация по CRC, разрешение семейства, end-to-end загрузка реального дампа, сторожевая строка, зеркалирование).Nescartdb/Readme.md: упоминание NAMCOT-3305 в описании семейного индекса.
Временный headless-харнесс для проверки: Temp/boottest.cpp (не коммитится, папка Temp в
.gitignore).