CartPcb

Functional simulation of NES/Famicom/Dendy cartridge PCBs: a board is a JSON document of components and wiring — not C++ code

Functional simulation of NES/Famicom/Dendy cartridge PCBs (printed circuit boards).

A cartridge is simulated as a physical board: a set of components (memory chips, mapper chips, discrete logic) and the wires (nets) that connect them to each other and to the cartridge edge connector. The description of a board is data — a JSON document — not C++ code.

CartPcb is the successor of the retired Mappers component (issue #509). It replaced Mappers completely and solved every problem listed in the old Mappers/Readme.md.


1. Motivation

The retired Mappers component had accumulated the following problems, all of which CartPcb solves:

#Problem (from the old Mappers/Readme.md)How CartPcb solves it
1Working with memory dumps (PRG/CHR) is unclear; the CHR region is called "CHR-ROM", PRG is not supported at all.CartPcb works with explicit image dumps: CartImage carries PRG and CHR images, sized and named per the PCB definition. CHR is just the CHR image; nothing is called "CHR-ROM" anymore.
2Emulation of .nes mappers is chaotic: translating an iNES mapper number into board components, with no way to express the many hardware variations of the same mapper.CartPcb is PCB-centric and data-driven. A board is a JSON document listing components and wiring. The same mapper chip (e.g. MMC1) wired differently = a different board JSON, not a fork of C++ code.
3No ROM chip support; everything is a raw byte array.CartPcb uses RomChip (JEDEC-style ROM, Common/BaseBoardLib) and the reworked SRAM (Common/BaseBoardLib), with /CE / /OE / /WE / address / data / dz semantics.
4MMC1 emulation needs debugging; the divider is likely wrong.MMC1 was extracted from Mappers into Chips as a standalone chip class with its own unit tests. CartPcb only wires chips into boards; it does not implement chips.

In addition, the issue #508 specifies these architectural motivations:

2. Scope

In scope

Out of scope (by design)

3. Terminology

4. Design principles

  1. Data over code. A board family is described once, in JSON. Adding a new PCB revision must not require C++ changes.
  2. Components are chips; boards are wiring. CartPcb implements the net model and board assembly. Memory chips (RomChip, SRAM) live in BaseBoardLib; everything else (MMC1, ...) is a chip class injected from Chips.
  3. Explicit images. PRG and CHR are named, sized regions of CartImage. The images are loaded into RomChip instances, and the board wiring attaches those ROM chips to the buses — never "the CHR-ROM byte array".
  4. Identification by content, not by header. PRG/CHR CRC32s select the board through nescartdb.
  5. User-extensible. Small JSON files in a user directory augment/override built-in boards (JSONES path).
  6. One simulation contract. The edge-connector signal interface is the same as today's AbstractCartridge, so motherboards (NESBoard, FamicomBoard) keep working unchanged.

5. PCB description format (CartPcb JSON)

A board is described by a JSON document with two parts:

5.1 Example: 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": []
    }
  }
}

Notes:

5.2 Example: MMC1-based board (SGROM, mapper 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]" }
      ]
    }
  }
}

This is the exact hardware structure the old MMC1_Based implemented in C++ — now it is data: a chip instance (MMC1 from Chips) wired to the CPU/PPU buses, with the resulting address lines feeding the ROM chips.

5.3 Example: UNROM with its glue logic (74LS161 + 74LS32)

Issue #525: the board description is as close to the real PCB as possible — the bank register is a 74LS161 chip and the PRG address multiplexer is a 74LS32 quad OR gate (exactly the wiring of the old 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" }
      ]
    }
  }
}

Notes:

5.4 The circuit section (CartPcb extension)

Because nescartdb only describes the component inventory, CartPcb adds a circuit section. Its exact expression language is finalized in the implementation issue; the concepts are:

Memory images are loaded into the ROM chips; the bus attachments describe how each ROM chip's pins (JEDEC: A0..An, /CE, /OE, D0..D7) connect to the bus and to chip outputs.

The netlist model is deliberately simple (nets carry TriState; posedge/negedge timing lives inside chip classes). This keeps board descriptions declarative while chip timing stays in the chip simulators.

6. Runtime architecture

6.1 Modules

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

Dependencies: Common/BaseLogicLib (TriState), Common/JsonLib (JSON parsing), Common/BaseBoardLib (RomChip — JEDEC-style ROM, SRAM — static RAM, LS161/LS32 glue logic), Nescartdb/ (data), and Chips/ for mapper-chip classes.

6.2 Key classes

6.3 Chip interface for injected chips

Mapper chips (e.g. MMC1) are plain chip classes, following the pattern of today's Mappers::MMC1:

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

The board JSON's nets list defines which pins exist and what drives them; the C++ chip class must expose a matching pin map (name ↔ input/output index). The pin-name contract between JSON and chip classes is part of the implementation issue.

6.4 Loading & lookup flow

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

If no nescartdb match is found and no board type is forced, the iNES fallback (§8) translates the dump from its header — the iNES mapper number and the PRG/CHR sizes — so homebrew and other "wild" dumps run through the same board path (issue #514).

6.5 Custom PCB JSONs (JSONES subset)

Any single-cartridge board JSON (§5) can be provided by the user:

7. Simulation model

7.1 Edge-connector contract

Defines the cartridge port — the signal contract of the former Mappers::AbstractCartridge, which now lives in CartPcb (CartPcb::Cartridge):

A CartPcbCartridge owns a Pcb and forwards the edge-connector signals into it. Motherboards (Breaknes/BreaksCore/NESBoard.cpp, FamicomBoard.cpp) use CartPcb::Cartridge*.

7.2 Net sources

Predefined sources available to board JSONs:

SourceMeaning
cpu_addr[n], cpu_addr[a:b]CPU address bus bits
ppu_addr[n], ppu_addr[a:b]PPU address bus bits
cpu_data[n]CPU data bus bits
M2, nROMSEL, RnW, nRD, nWR, nPA13edge-connector control signals
<chip>.<PIN>any chip output pin (e.g. mmc1.PRG_A14, mmc1.VRAM_A10)
gnd, vddconstants

Derived expressions (| for concatenation, &/|/! boolean operators) form address lines and chip-selects.

7.3 Memory chip semantics

RomChip (JEDEC-style) and SRAM follow the classic memory-chip protocol (/CE — chip enable, /OE — output enable, /WE — write enable, A0..An, D0..D7):

7.4 Mirroring → Scroll

There is no "Mirroring" definition for a declarative PCB description (issue #525). The term "Mirroring" belongs to the iNES header; on the board there is only a solder jumper and the scrolling arrangement it selects (nesdev "arrangement"):

PCB jumper (Scroll)WiringiNES header (Flags6 bit 0)
H ScrollVRAM_A10 = PA101 ("vertical mirroring")
V ScrollVRAM_A10 = PA110 ("horizontal mirroring")

The two terms are mutually exclusive: H Scroll (PCB) == vertical mirroring (iNES).

The PPU's internal 2 KiB VRAM and its VRAM_A10 line are simply called VRAM.

7.5 Relationship to existing components

ComponentRole
CartPcb::CartridgeThe cartridge-port contract (inputs/outputs/debug); the former Mappers::AbstractCartridge. CartPcbCartridge implements it.
Mappers componentRetired (issue #509). The whole folder was removed; this document replaces the old Mappers/Readme.md; build files (CMakeLists.txt, VS projects) updated.
Chips/MMC1The MMC1 chip class (moved from Mappers), with unit tests.
Chips, BaseBoardLibProvide chip classes (MMC1, RomChip, SRAM, LS161, LS32) consumed by CartPcb.
Common/JsonLibJSON parsing for board JSONs and the nescartdb JSON.
Nescartdb/The converted database (identification data).

8. Identification (NesCartDb)

9. Board coverage plan

Boards are added incrementally, each as: board JSON(s) + unit test + (while the old implementation existed) parity test:

  1. NROM (NES-NROM-128/256, HVC-NROM-*, IREM-NROM-*, ...) — no mapper chip; PRG + CHR + a scroll jumper. Replaces the old NROM.
  2. UxROM (NES-UNROM, NES-UOROM, ...) — discrete logic PRG bank switch (A14), 8 KiB CHR-RAM. Described with the real glue logic: a 74LS161 bank register and a 74LS32 address multiplexer (issue #525). Replaces the old UNROM.
  3. AxROM (NES-AOROM, NES-ANROM, ...) — 1-screen mirroring via the 74LS161 bank register (Q3 → VRAM_A10). Replaces the old AOROM.
  4. MMC1 family (SGROM, SLROM, ...) — MMC1 chip from Chips wired per §5.2. Replaces the old MMC1_Based.
  5. Fallback boards (issue #514): CNROM (latch-based CHR bank switch) and SHROM (MMC1 + CHR-RAM) exist so the iNES fallback can translate mapper 3 and mapper-1-CHR-RAM dumps.
  6. Next candidates: MMC3 family, discrete logic boards (BxROM, ...).

10. Migration checklist (Mappers → CartPcb)

Completed in issue #509:

  1. The cartridge-port contract (ex-Mappers::AbstractCartridge) moved into CartPcb (CartPcb::Cartridge).
  2. CartImage replaced raw uint8_t* nesImage; PRG and CHR are explicit, named, sized dumps loaded into RomChip instances.
  3. RomChip (JEDEC-style, in BaseBoardLib) and the reworked SRAM replaced raw byte arrays.
  4. NesCartDb (CRC32 only) + PcbLoader + PcbFactory replaced the iNES-mapper switch in the old CartridgeFactory.
  5. MMC1 moved to Chips (with unit tests); the legacy unmasked PRG read was fixed.
  6. NROM/UNROM/AOROM/SGROM/SLROM were re-expressed as board JSONs and passed A/B parity tests against the old implementations.
  7. The entire Mappers component was removed (including AbstractCartridge and CartridgeFactory); build files updated.
  8. All consumers locate the Nescartdb JSON and custom board dirs per Nescartdb/Readme.md.

11. Debug & testing

12. Non-goals / future work

Source: CartPcb/Readme.md · this page is part of the docs site.