CartPcb

Функциональная симуляция картриджных PCB NES/Famicom/Dendy: плата — это JSON-документ из компонентов и соединений, а не код на C++

Функциональная симуляция картриджных PCB (печатных плат) NES/Famicom/Dendy.

Картридж симулируется как физическая плата: набор компонентов (чипы памяти, чипы-мапперы, дискретная логика) и проводов (сетей, nets), соединяющих их друг с другом и с краевым разъёмом картриджа (edge connector). Описание платы — это данные, JSON-документ, а не код на C++.

CartPcb — преемник выведенного из эксплуатации компонента Mappers (issue #509). Он полностью заменил Mappers и решил все проблемы, перечисленные в старом Mappers/Readme.md.


1. Мотивация

У выведенного из эксплуатации компонента Mappers накопились следующие проблемы, и все их CartPcb решает:

#Проблема (из старого Mappers/Readme.md)Как это решает CartPcb
1Работа с дампами памяти (PRG/CHR) непонятна: область CHR называется «CHR-ROM», а PRG вообще не поддерживается.CartPcb работает с явными дампами образов: CartImage несёт образы PRG и CHR с размерами и именами из определения PCB. CHR — это просто образ CHR; «CHR-ROM» больше ничем не называется.
2Эмуляция .nes-мапперов хаотична: перевод номера iNES-маппера в компоненты платы без возможности выразить множество аппаратных вариаций одного и того же маппера.CartPcb ориентирован на PCB и управляется данными. Плата — это JSON-документ со списком компонентов и разводки. Один и тот же чип-маппер (например, MMC1), разведённый иначе, — это другой board JSON, а не форк кода на C++.
3Нет поддержки чипов ROM — всё это сырой байтовый массив.CartPcb использует RomChip (ROM в стиле JEDEC, Common/BaseBoardLib) и переработанный SRAM (Common/BaseBoardLib) с семантикой /CE / /OE / /WE / адрес / данные / dz.
4Эмуляцию MMC1 нужно отлаживать; делитель, вероятно, неверен.MMC1 был выделен из Mappers в Chips как самостоятельный класс чипа со своими юнит-тестами. CartPcb только разводит чипы на платах; он не реализует чипы.

Кроме того, issue #508 задаёт следующие архитектурные мотивы:

2. Охват

Что входит в охват

Вне охвата (осознанно)

3. Терминология

4. Принципы проектирования

  1. Данные вместо кода. Семейство плат описывается один раз, в JSON. Добавление новой ревизии PCB не должно требовать изменений в C++.
  2. Компоненты — это чипы; платы — это разводка. CartPcb реализует модель сетей и сборку плат. Чипы памяти (RomChip, SRAM) живут в BaseBoardLib; всё остальное (MMC1, ...) — классы чипов, инжектируемые из Chips.
  3. Явные образы. PRG и CHR — именованные области CartImage с заданным размером. Образы загружаются в экземпляры RomChip, а разводка платы подключает эти чипы ROM к шинам — и никогда «байтовый массив CHR-ROM».
  4. Идентификация по содержимому, а не по заголовку. CRC32 образов PRG/CHR выбирают плату через nescartdb.
  5. Расширяемость пользователем. Небольшие JSON-файлы в пользовательском каталоге дополняют или переопределяют встроенные платы (путь JSONES).
  6. Один контракт симуляции. Сигнальный интерфейс edge-коннектора такой же, как у нынешнего AbstractCartridge, поэтому материнские платы (NESBoard, FamicomBoard) продолжают работать без изменений.

5. Формат описания PCB (CartPcb JSON)

Плата описывается JSON-документом из двух частей:

5.1 Пример: NROM-256

{
  "schemaVersion": 1,
  "board": {
    "type": "NES-NROM-256",
    "pcb": "NES-NROM-256-02",
    "mapper": 0,
    "system": ["NES-NTSC", "NES-PAL"],
    "components": {
      "prg": { "kind": "rom", "bus": "cpu", "size": 32768 },
      "chr": { "kind": "rom", "bus": "ppu", "size": 8192 }
    },
    "circuit": {
      "mirroring": { "mode": "scroll" },
      "cpu": {
        "prg": { "chip": "prg", "n_cs": "nROMSEL", "addr": "cpu_addr[13:0]" }
      },
      "ppu": {
        "chr": { "chip": "chr", "n_cs": "!nPA13", "addr": "ppu_addr[12:0]" }
      },
      "nets": []
    }
  }
}

Примечания:

5.2 Пример: плата на MMC1 (SGROM, маппер 1)

{
  "schemaVersion": 1,
  "board": {
    "type": "HVC-SGROM",
    "pcb": "HVC-SGROM-03",
    "mapper": 1,
    "system": ["Famicom"],
    "components": {
      "prg": { "kind": "rom", "bus": "cpu", "size": 262144 },
      "chr": { "kind": "rom", "bus": "ppu", "size": 8192 },
      "mmc1": { "kind": "chip", "chip": "MMC1" }
    },
    "circuit": {
      "mirroring": { "mode": "mapper", "net": "mmc1.VRAM_A10" },
      "cpu": {
        "prg": {
          "chip": "prg",
          "n_cs": "mmc1.PRG_nCE",
          "addr": "mmc1.PRG_A17..PRG_A14 | cpu_addr[13:0]"
        }
      },
      "ppu": {
        "chr": {
          "chip": "chr",
          "n_cs": "!nPA13",
          "addr": "mmc1.CHR_A16..CHR_A12 | ppu_addr[11:0]"
        }
      },
      "nets": [
        { "name": "mmc1.M2",       "from": "M2" },
        { "name": "mmc1.nROMSEL",  "from": "nROMSEL" },
        { "name": "mmc1.CPU_RnW",  "from": "RnW" },
        { "name": "mmc1.CPU_A13",  "from": "cpu_addr[13]" },
        { "name": "mmc1.CPU_A14",  "from": "cpu_addr[14]" },
        { "name": "mmc1.CPU_D0",   "from": "cpu_data[0]" },
        { "name": "mmc1.CPU_D7",   "from": "cpu_data[7]" },
        { "name": "mmc1.PPU_A10",  "from": "ppu_addr[10]" },
        { "name": "mmc1.PPU_A11",  "from": "ppu_addr[11]" },
        { "name": "mmc1.PPU_A12",  "from": "ppu_addr[12]" }
      ]
    }
  }
}

Это ровно та аппаратная структура, которую старый MMC1_Based реализовывал на C++ — теперь это данные: экземпляр чипа (MMC1 из Chips), разведённый на шины CPU/PPU, а результирующие линии адреса питают чипы ROM.

5.3 Пример: UNROM со своей клейкой логикой (74LS161 + 74LS32)

Issue #525: описание платы максимально приближено к реальной PCB — регистр банка — это чип 74LS161, а мультиплексор адреса PRG — 74LS32, счетверённый элемент ИЛИ (ровно та разводка, что была в старом Mappers::UNROM):

{
  "schemaVersion": 1,
  "board": {
    "type": "UNROM",
    "components": {
      "prg": { "kind": "rom", "bus": "cpu" },
      "chr": { "kind": "ram", "bus": "ppu", "size": 8192 },
      "ls161": { "kind": "chip", "chip": "LS161" },
      "ls32": { "kind": "chip", "chip": "LS32" }
    },
    "circuit": {
      "mirroring": { "mode": "scroll" },
      "cpu": {
        "prg": {
          "chip": "prg",
          "n_cs": "nROMSEL",
          "addr": "ls32.Y3 | ls32.Y0 | ls32.Y1 | cpu_addr[13:0]"
        }
      },
      "ppu": {
        "chr": { "chip": "chr", "n_cs": "!nPA13", "n_oe": "nRD", "n_we": "nWR", "addr": "ppu_addr[12:0]" }
      },
      "nets": [
        { "name": "ls161.CLK",  "from": "nROMSEL" },
        { "name": "ls161.nRST", "from": "vdd" },
        { "name": "ls161.nLD",  "from": "RnW" },
        { "name": "ls161.EN_T", "from": "gnd" },
        { "name": "ls161.EN_P", "from": "gnd" },
        { "name": "ls161.P0",   "from": "cpu_data[0]" },
        { "name": "ls161.P1",   "from": "cpu_data[1]" },
        { "name": "ls161.P2",   "from": "cpu_data[2]" },
        { "name": "ls161.P3",   "from": "gnd" },
        { "name": "ls32.A0",    "from": "ls161.Q1" },
        { "name": "ls32.B0",    "from": "cpu_addr[14]" },
        { "name": "ls32.A1",    "from": "ls161.Q0" },
        { "name": "ls32.B1",    "from": "cpu_addr[14]" },
        { "name": "ls32.A2",    "from": "gnd" },
        { "name": "ls32.B2",    "from": "gnd" },
        { "name": "ls32.A3",    "from": "cpu_addr[14]" },
        { "name": "ls32.B3",    "from": "ls161.Q2" }
      ]
    }
  }
}

Примечания:

5.4 Секция circuit (расширение CartPcb)

Так как nescartdb описывает только состав компонентов, CartPcb добавляет секцию circuit. Её точный язык выражений финализируется в issue по реализации; концепции таковы:

Образы памяти загружаются в чипы ROM; подключения к шинам описывают, как выводы каждого чипа ROM (JEDEC: A0..An, /CE, /OE, D0..D7) соединяются с шиной и выходами чипов.

Модель списка соединений намеренно проста (сети несут TriState; тайминги фронтов posedge/negedge живут внутри классов чипов). Это сохраняет описания плат декларативными, а тайминги чипов — в симуляторах чипов.

6. Архитектура выполнения

6.1 Модули

CartPcb/                     (new top-level component, C++/native)
  CartPcb.h                  public header
  Pcb.h / Pcb.cpp            the simulated board
  PcbFactory.h/.cpp          JSON -> Pcb
  PcbLoader.h/.cpp           locate & parse PCB JSON (built-in Nescartdb + user dir)
  NesCartDb.h/.cpp           CRC32 -> board type lookup (nescartdb index)
  InesTranslator.h/.cpp      iNES-header fallback for "wild" dumps (issue #514)
  CartImage.h                PRG/CHR dumps + battery RAM
  Readme.md                  this specification

Зависимости: Common/BaseLogicLib (TriState), Common/JsonLib (разбор JSON), Common/BaseBoardLib (RomChip — ROM в стиле JEDEC, SRAM — статическая RAM, клейкая логика LS161/LS32), Nescartdb/ (данные) и Chips/ — для классов чипов-мапперов.

6.2 Ключевые классы

6.3 Интерфейс чипа для инжектируемых чипов

Чипы-мапперы (например, MMC1) — это обычные классы чипов, следующие образцу сегодняшнего Mappers::MMC1:

class MMC1 {
    void sim(BaseLogic::TriState inputs[], BaseLogic::TriState outputs[]);
};

Список nets в board JSON определяет, какие выводы существуют и что ими управляет; класс чипа на C++ должен предоставлять согласованную карту выводов (имя ↔ индекс входа/выхода). Контракт имён выводов между JSON и классами чипов — часть issue по реализации.

6.4 Поток загрузки и поиска

CartImage (PRG/CHR dumps loaded into ROM chips)
   |
   v
NesCartDb.FindBoard(crc32(PRG), crc32(CHR))  -->  board type (e.g. "HVC-SGROM")
   |
   v
PcbLoader.Load(board type)  -->  board JSON  (built-in Nescartdb, or user override)
   |
   v
PcbFactory.Create(board JSON, CartImage)  -->  Pcb
   |
   v
CartPcbCartridge (CartPcb cartridge port)  -->  Board::InsertCartridge

Если совпадения в nescartdb нет и тип платы не принуждён, iNES-fallback (§8) переводит дамп из его заголовка — номер iNES-маппера и размеры PRG/CHR, — чтобы homebrew и прочие «дикие» дампы проходили тем же путём плат (issue #514).

6.5 Пользовательские PCB JSON (подмножество JSONES)

Пользователь может предоставить любой board JSON одного картриджа (§5):

7. Модель симуляции

7.1 Контракт edge-коннектора

Определяет картриджный порт — сигнальный контракт бывшего Mappers::AbstractCartridge, который теперь живёт в CartPcb (CartPcb::Cartridge):

CartPcbCartridge владеет Pcb и пробрасывает в него сигналы edge-коннектора. Материнские платы (Breaknes/BreaksCore/NESBoard.cpp, FamicomBoard.cpp) используют CartPcb::Cartridge*.

7.2 Источники сетей

Предопределённые источники, доступные board JSON:

ИсточникЗначение
cpu_addr[n], cpu_addr[a:b]биты шины адреса CPU
ppu_addr[n], ppu_addr[a:b]биты шины адреса PPU
cpu_data[n]биты шины данных CPU
M2, nROMSEL, RnW, nRD, nWR, nPA13управляющие сигналы edge-коннектора
<chip>.<PIN>любой выходной вывод чипа (например, mmc1.PRG_A14, mmc1.VRAM_A10)
gnd, vddконстанты

Производные выражения (| — конкатенация, булевы операторы &/|/!) формируют линии адреса и chip-select'ы.

7.3 Семантика чипов памяти

RomChip (в стиле JEDEC) и SRAM следуют классическому протоколу микросхем памяти (/CE — chip enable, /OE — output enable, /WE — write enable, A0..An, D0..D7):

7.4 Mirroring → Scroll

Для декларативного описания PCB не существует определения «Mirroring» (issue #525). Термин «Mirroring» принадлежит заголовку iNES; на плате есть только паяная перемычка и выбираемая ею scroll-схема (nesdev «arrangement»):

Перемычка PCB (Scroll)РазводкаЗаголовок iNES (Flags6, бит 0)
H ScrollVRAM_A10 = PA101 («vertical mirroring»)
V ScrollVRAM_A10 = PA110 («horizontal mirroring»)

Эти два термина взаимоисключающие: H Scroll (PCB) == vertical mirroring (iNES).

Внутренние 2 КиБ видеопамяти PPU вместе с её линией VRAM_A10 называются просто VRAM.

7.5 Связь с существующими компонентами

КомпонентРоль
CartPcb::CartridgeКонтракт картриджного порта (входы/выходы/отладка); бывший Mappers::AbstractCartridge. Реализуется классом CartPcbCartridge.
компонент MappersВыведен из эксплуатации (issue #509). Вся папка удалена; этот документ заменяет старый Mappers/Readme.md; файлы сборки (CMakeLists.txt, VS-проекты) обновлены.
Chips/MMC1Класс чипа MMC1 (перенесён из Mappers), с юнит-тестами.
Chips, BaseBoardLibПредоставляют классы чипов (MMC1, RomChip, SRAM, LS161, LS32), которые потребляет CartPcb.
Common/JsonLibРазбор JSON для board JSON и JSON nescartdb.
Nescartdb/Сконвертированная база (данные идентификации).

8. Идентификация (NesCartDb)

9. План покрытия плат

Платы добавляются постепенно, каждая как: board JSON + юнит-тест + (пока существовала старая реализация) parity-тест:

  1. NROM (NES-NROM-128/256, HVC-NROM-*, IREM-NROM-*, ...) — без чипа-маппера: PRG + CHR + перемычка scroll. Заменяет старый NROM.
  2. UxROM (NES-UNROM, NES-UOROM, ...) — дискретное переключение банков PRG (A14), 8 КиБ CHR-RAM. Описывается реальной клейкой логикой: регистр банка 74LS161 и адресный мультиплексор 74LS32 (issue #525). Заменяет старый UNROM.
  3. AxROM (NES-AOROM, NES-ANROM, ...) — одноэкранный mirroring через регистр банка 74LS161 (Q3 → VRAM_A10). Заменяет старый AOROM.
  4. Семейство MMC1 (SGROM, SLROM, ...) — чип MMC1 из Chips, разведённый по §5.2. Заменяет старый MMC1_Based.
  5. Запасные платы (issue #514): CNROM (переключение банков CHR на latch) и SHROM (MMC1 + CHR-RAM) существуют, чтобы iNES-fallback мог переводить дампы маппера 3 и маппер-1 с CHR-RAM.
  6. Следующие кандидаты: семейство MMC3, платы дискретной логики (BxROM, ...).

10. Чек-лист миграции (Mappers → CartPcb)

Выполнено в issue #509:

  1. Контракт картриджного порта (бывший Mappers::AbstractCartridge) переехал в CartPcb (CartPcb::Cartridge).
  2. CartImage заменил сырой uint8_t* nesImage; PRG и CHR — явные именованные дампы заданного размера, загружаемые в экземпляры RomChip.
  3. RomChip (в стиле JEDEC, в BaseBoardLib) и переработанный SRAM заменили сырые байтовые массивы.
  4. NesCartDb (только CRC32) + PcbLoader + PcbFactory заменили switch по iNES-мапперам в старом CartridgeFactory.
  5. MMC1 переехал в Chips (с юнит-тестами); исправлено устаревшее чтение PRG без маски.
  6. NROM/UNROM/AOROM/SGROM/SLROM перевыражены как board JSON и прошли A/B parity-тесты против старых реализаций.
  7. Весь компонент Mappers удалён (включая AbstractCartridge и CartridgeFactory); файлы сборки обновлены.
  8. Все потребители ищут JSON nescartdb и каталоги пользовательских плат согласно Nescartdb/Readme.md.

11. Отладка и тестирование

12. Не-цели / дальнейшая работа

Источник: CartPcb/Readme.md · страница входит в сайт документации.