ND-100 Multi-Device Controller Card Design (RP2350B)¶
Overview¶
This document describes the hardware design for an ND-100 controller card based on the Raspberry Pi RP2350B microcontroller. The card is intended to emulate multiple peripheral devices for the ND-100/ND-110/ND-120 systems, plugging into a standard slot on the shared backplane via the C connector.
Target device emulations:
| Device | Interrupt Level | Notes |
|---|---|---|
| Floppy disk | 10 / 11 | PIO + DMA |
| SMD disk | 10 / 11 | PIO + DMA |
| Terminal | 10 / 12 | PIO only |
| HDLC | 13 | PIO + DMA, fastest path |
The card must support all four ND-100 bus cycle types from the bus signal reference:
- IOX/IOXT (programmed I/O) - read and write to device registers
- IDENT PLxx - interrupt identification via daisy-chain
- Interrupt generation - asserting BINT 10-13
- DMA transfers - memory read and write as bus master
See ND-100-BUS-C-CONNECTOR.md for the complete bus signal reference and cycle protocols.
Drawing the schematic? Jump straight to SCHEMATIC-CAPTURE.md -- it has every IC pin, every connector pin, every reference designator, every net name, and verified LCSC part numbers in one single-page reference. This document (CONTROLLER-DESIGN.md) explains the why; SCHEMATIC-CAPTURE.md is the practical bill-of-pins.
Selected Hardware Module: Olimex RP2350-PICO2-BB48R¶
The controller card uses the Olimex RP2350-PICO2-BB48R as the MCU subsystem. The Olimex board plugs into our PCB via headers; everything else on the card is surface mount.
Module Specifications¶
| Feature | Value |
|---|---|
| MCU | RP2350B (48 GPIO variant) |
| Flash | 16 MB QSPI |
| PSRAM | 8 MB QSPI (BB48R variant only) |
| SD Card | Micro SD card slot ON BOARD |
| GPIO exposed | All 48 GPIO (GP0-GP47) |
| USB | USB-C data and power |
| Buttons | BOOT and RESET on board |
| Status LED | On board, GPIO25 (User_Led) |
| UEXT connector | pUEXT 1.0 mm pitch (debug/expansion) |
| Qwiic/Stemma | I2C connector |
| Dimensions | 69 x 18 mm |
| Pin spacing | 15.24 mm (0.6") -- compatible with standard 0.1" headers |
| Open source hardware | Yes -- KiCad files, schematic, manual provided |
| Power | On-board DCDC 3.3V @ 2A continuous (3A peak) |
| Order codes | RP2350-PICO2-BB48 (no PSRAM/SD), RP2350-PICO2-BB48R (with PSRAM + SD) |
Why BB48R is the Right Choice¶
- SD card on board -- no external SD slot needed on our PCB
- BOOT and RESET buttons on board -- no external buttons needed
- USB-C connector -- modern, robust, handles firmware updates and virtual serial
- Status LED on board (GPIO25) -- free heartbeat indicator
- Open source hardware with full KiCad files and schematic
- PSRAM included -- 8 MB for floppy/SMD/HDLC buffer caching
- 3.3V regulator at 2A -- ample power for level shifters and our circuitry
PCB Integration: Headers + Surface Mount¶
The Olimex BB48R is a through-hole module with 0.6" (15.24 mm) row spacing. It plugs into our PCB via female pin headers. Everything else on the card is surface mount for compactness and reliability.
| Section | Mount Type |
|---|---|
| Olimex BB48R module | Through-hole female headers (the module itself is socketed) |
| Level shifters (74LVC245, 74LVC14, 74LVC07, 74LVC125) | SMD (SOIC-14/16/20 or TSSOP) |
| Latches (74LVC574, 74LVT245) | SMD (SOIC-20/TSSOP) |
| Pull resistors and bypass caps | SMD (0603 or 0402) |
| LEDs | SMD (0805) |
| C connector (DIN 41612) | Through-hole (PCB-mount male right-angle) |
| Status LEDs | SMD (0805) |
Olimex BB48R Connector Footprint¶
The BB48R has two pin rows (EXT1 and EXT2), each 27 pins long, spaced 15.24 mm apart. On our PCB:
| PCB Footprint | Detail |
|---|---|
| 2x female pin sockets, 27-pin each | 0.1" pitch, 0.6" row spacing |
| Or one 54-pin DIP socket | Some assemblers prefer single socket |
| Module orientation | USB-C connector facing away from C connector |
The female sockets allow the Olimex board to be removed and replaced if needed. The Olimex board itself is industrial grade and should not need replacement under normal use.
Card-Edge Layout (Suggested)¶
+---------------------------------------+
| |
| [ Olimex BB48R module socket ] | <- 0.6" header rows
| |
| |
| SMD: latches, transceivers, decoder |
| SMD: 74LVC125 daisy bypass |
| SMD: status LEDs |
| |
| <Power supply: 5V from bus, 3.3V |
| from BB48R or local LDO> |
| |
+============== C connector ============+ <- DIN 41612 male, right-angle
(96 pins, 5V ND-100 bus)
The DIN 41612 C connector is at the bottom edge for plugging into the ND-100 backplane. The Olimex module is mounted on the top portion of the card, well away from the bus connector for signal integrity.
Pin Reservation Analysis (Verified from Manual)¶
Analysis of the official Olimex BB48R user manual (Sept 2025) confirms these pin reservations:
| GPIO | Function | Source |
|---|---|---|
| GPIO8 | PSRAM CS (QMI_CS1n) |
8 MB QSPI PSRAM |
| GPIO9 | SD card CS (SPI1_CSn) |
Micro SD slot |
| GPIO10 | SD card CLK (SPI1_SCK / SD_CLK) |
Micro SD slot |
| GPIO11 | SD card CMD/MOSI (SPI1_TX / SD_CMD) |
Micro SD slot |
| GPIO24 | SD card DATA0/MISO (SPI1_RX / SD_DAT0) |
Micro SD slot |
| GPIO25 | User LED (User_Led) |
Status LED on board |
| Total reserved | 6 GPIO |
USB, RESET, BOOTSEL, and the QSPI flash all use dedicated pins or QSPI lines outside the GPIO pool, so they consume zero user GPIO.
Available GPIO Blocks¶
| Block | Pins | Notes |
|---|---|---|
| GPIO0-7 | 8 pins | Available; shared with pUEXT/Qwiic connectors but those are optional |
| GPIO8-11 | 0 | Reserved for PSRAM and SD card |
| GPIO12-23 | 12 pins | Free, contiguous |
| GPIO24-25 | 0 | Reserved for SD DAT0 and User LED |
| GPIO26-31 | 6 pins | Free, contiguous (LOW bank end) |
| GPIO32-47 | 16 pins | Free, contiguous (HIGH bank, all of it) |
| Total available | 42 GPIO |
Critical Finding: 24-bit Contiguous BD Bus is NOT Possible¶
The reserved pins (GPIO8-11 for PSRAM/SD, GPIO24 for SD DAT0) fragment the LOW bank:
GPIO0-7 : free (8 pins)
GPIO8-11 : RESERVED (4 pins) ← gap
GPIO12-23: free (12 pins)
GPIO24-25: RESERVED (2 pins) ← gap
GPIO26-31: free (6 pins)
GPIO32-47: free (16 pins) ← in HIGH bank
There is NO 24-pin contiguous block in a single bank. Possible candidates all fail:
| Block | Result |
|---|---|
| GPIO0-23 | ❌ blocked by GPIO8-11 |
| GPIO12-35 | ❌ spans LOW (12-31) and HIGH (32-35) banks -- not single-cycle |
| GPIO16-39 | ❌ spans both banks |
| GPIO24-47 | ❌ blocked by GPIO24-25 |
Conclusion: Design 1/4 (Direct GPIO with single-cycle 24-bit BD access) is NOT VIABLE on the BB48R. The architecture must use a multiplexed BD interface.
Confirmed Architecture: Design 2 (8-bit Latched)¶
Design 2 only needs 8 contiguous GPIO for the shared 8-bit MCU<->latch bus, which fits perfectly in GPIO12-19.
Bonus Features (Free)¶
These board features are usable without additional GPIO cost:
| Feature | Pin | What we get |
|---|---|---|
| Micro SD card slot | GPIO9-11, 24 | Uses SPI1 hardware peripheral. Just call SDK functions. |
| 8 MB PSRAM | GPIO8 | Uses QSPI. Can be used as XIP memory or via SDK PSRAM API. |
| User LED | GPIO25 | Heartbeat / status indicator |
| BOOT button | QSPI_CS | Press during reset = USB mass storage mode |
| RESET button | RUN | Hardware reset |
| USB-C | USB_DP/USB_DM (dedicated) | Firmware update + virtual COM port |
| UART0 debug | GPIO0/GPIO1 | Optional serial debug (if not using USB CDC) |
Power Supply¶
| Rail | Source | Notes |
|---|---|---|
| VBUS | USB-C | 5V from USB |
| VDD_SYS | USB or external | 5V (input/output) |
| +3.3V | On-board DCDC | 2A @ 3.3V (3A peak) -- enough for level shifters |
| 3V3_EN | Input | Pull to GND to disable 3.3V regulator |
The BB48R provides 3.3V at up to 2A, ample for our level shifters, latches, and LEDs. The +5V for level shifter VCCB side comes from the ND-100 bus connector itself (bus pin 2/31).
Complete Pin Allocation¶
This is the definitive pin map for the controller card.
CRITICAL DESIGN RULE: The PIO must read both the 8-bit DBUS (from latches) AND the 4 trigger control signals in a single PIO IN instruction. This requires both groups to be contiguous in the same GPIO bank. We allocate GPIO12-23 as one contiguous block: GPIO12-19 = DBUS, GPIO20-23 = trigger signals.
A single
IN PINS, 12reads all 12 pins in one PIO cycle (~7 ns at 150 MHz). The result is a 12-bit value: lower 8 bits = DBUS data, upper 4 bits = trigger signals (BAPR/BIOXE/BDAP/BDRY).Crossing the LOW/HIGH bank boundary (e.g., putting DBUS on GPIO28-35 which spans GPIO31->GPIO32) would force two separate operations and break the timing.
Trigger Signal Strategy¶
The 4 most critical input control signals are the trigger signals -- they tell the PIO when something is happening on the bus:
| Signal | When Active | What Happens |
|---|---|---|
| /BAPR | CPU asserts address strobe | Address phase started, level/address on BD lines |
| /BIOXE | CPU asserts IOX execute | IOX cycle data phase, data on BD lines |
| /BDAP | Bus master asserts data present | Memory cycle data phase, data on BD lines |
| /BDRY | Memory or device asserts data ready | Data response, data on BD lines |
The PIO uses an external OR gate to combine these 4 signals into a single trigger that the PIO wait instruction can monitor. When ANY trigger is active, the PIO reads the full 12-bit window.
Other input control signals are not time-critical -- the C code reads them directly via gpio_get() when needed:
| Signal | Why C reads directly |
|---|---|
| /BINACK | Read during IOX response handshake |
| /BMEM | Read during cycle classification |
| /BMCL | Read during reset handling |
| /BINPUT | Read during cycle classification |
| /INGRANT | Read during DMA grant handshake |
| /INIDENT | Read during IDENT cycle |
EXT1 connector (GPIO0-23) - LOW bank, time-critical PIO group¶
| GPIO | Pin Use | Direction | Buffer | Notes |
|---|---|---|---|---|
| GPIO0 | INT_BB48R (from Pi Pico W) | Input | -- | Pi Pico W signals "I have data" (when populated) |
| GPIO1 | INT_PICO (to Pi Pico W) | Output | -- | BB48R signals "wake/command" (when populated) |
| GPIO2 | /BINT 12 drive | Output | 74LVC07 | Open-drain to bus |
| GPIO3 | /BINT 13 drive | Output | 74LVC07 | Open-drain (HDLC) |
| GPIO4 | SPI0_RX (MISO from Pi Pico W) | Input | -- | Hardware SPI0 |
| GPIO5 | SPI0_CSn (CS to Pi Pico W) | Output | -- | Hardware SPI0 |
| GPIO6 | SPI0_SCK (clock to Pi Pico W) | Output | -- | Hardware SPI0 |
| GPIO7 | SPI0_TX (MOSI to Pi Pico W) | Output | -- | Hardware SPI0 |
| GPIO8 | PSRAM CS | -- | -- | Board reserved |
| GPIO9 | SD card CS | -- | -- | Board reserved |
| GPIO10 | SD card CLK | -- | -- | Board reserved |
| GPIO11 | SD card CMD | -- | -- | Board reserved |
| GPIO12 | DBUS 0 | Bidir | -- | PIO read group: bit 0 |
| GPIO13 | DBUS 1 | Bidir | -- | PIO read group: bit 1 |
| GPIO14 | DBUS 2 | Bidir | -- | PIO read group: bit 2 |
| GPIO15 | DBUS 3 | Bidir | -- | PIO read group: bit 3 |
| GPIO16 | DBUS 4 | Bidir | -- | PIO read group: bit 4 |
| GPIO17 | DBUS 5 | Bidir | -- | PIO read group: bit 5 |
| GPIO18 | DBUS 6 | Bidir | -- | PIO read group: bit 6 |
| GPIO19 | DBUS 7 | Bidir | -- | PIO read group: bit 7 |
| GPIO20 | /BAPR_IN (sniff) | Input | 74LVC14 | PIO read group: bit 8 -- trigger |
| GPIO21 | /BIOXE_IN (sniff) | Input | 74LVC14 | PIO read group: bit 9 -- trigger |
| GPIO22 | /BDAP_IN (sniff) | Input | 74LVC14 | PIO read group: bit 10 -- trigger |
| GPIO23 | /BDRY_IN (sniff) | Input | 74LVC14 | PIO read group: bit 11 -- trigger |
GPIO12-23 form one contiguous block of 12 pins that the PIO reads in a single
IN PINS, 12instruction.
EXT2 connector (GPIO24-47) - LOW bank end + HIGH bank¶
| GPIO | Pin Use | Direction | Buffer | Notes |
|---|---|---|---|---|
| GPIO24 | SD card DAT0 | -- | -- | Board reserved |
| GPIO25 | User LED | Output | -- | On-board LED -- heartbeat |
| GPIO26 | /OE_IN_0 | Output | -- | Read-enable input latch 0 (BD 0-7) |
| GPIO27 | /OE_IN_1 | Output | -- | Read-enable input latch 1 (BD 8-15) |
| GPIO28 | /OE_IN_2 | Output | -- | Read-enable input latch 2 (BD 16-23) |
| GPIO29 | LE_OUT_0 | Output | -- | Latch-enable output latch 0 (BD 0-7) |
| GPIO30 | LE_OUT_1 | Output | -- | Latch-enable output latch 1 (BD 8-15) |
| GPIO31 | LE_OUT_2 | Output | -- | Latch-enable output latch 2 (BD 16-23) |
| GPIO32 | /BD_OE_BUS | Output | -- | Master OE for output 74LVT245 transceivers |
| GPIO33 | /BMEM_IN (sniff) | Input | 74LVC14 | Read directly via gpio_get() |
| GPIO34 | /BINACK_IN (sniff) | Input | 74LVC14 | Read directly |
| GPIO35 | /BMCL_IN (sniff) | Input | 74LVC14 | Read directly |
| GPIO36 | /BINPUT_IN (sniff) | Input | 74LVC14 | Read directly |
| GPIO37 | /INGRANT_IN | Input | 74LVC14 | Read directly |
| GPIO38 | /INIDENT_IN | Input | 74LVC14 | Read directly |
| GPIO39 | /BINT 10 drive | Output | 74LVC07 | Open-drain interrupt level 10 (freed by removing TRIGGER_OR) |
| GPIO40 | /BINT 11 drive | Output | 74LVC07 | Open-drain interrupt level 11 |
| GPIO41 | /BAPR_OUT | Output | 74LVC07 | Drive BAPR during DMA cycles |
| GPIO42 | /BDRY_OUT | Output | 74LVC07 | Drive BDRY when responding |
| GPIO43 | /BINPUT_OUT | Output | 74LVC07 | Drive BINPUT during IOX read response or DMA write |
| GPIO44 | /BDAP_OUT | Output | 74LVC07 | Drive BDAP during DMA cycles |
| GPIO45 | /BREQ_OUT | Output | 74LVC07 | Drive BREQ to request DMA |
| GPIO46 | /OE_DAISY_GRANT | Output | -- | Controls 74LVC125 grant pass-through |
| GPIO47 | /OE_DAISY_IDENT | Output | -- | Controls 74LVC125 ident pass-through |
Pin allocation now fits: Removing the TRIGGER_OR pin (no longer needed because PIO does the trigger detection internally) freed GPIO33, which let us shift the layout and accommodate /BINT 10 (GPIO39) and /BINT 11 (GPIO40). All pins fit without needing a 74HC595 shift register.
Pin Count Summary¶
| Category | Pins | GPIOs |
|---|---|---|
| PIO read group (8 DBUS + 4 trigger) | 12 | GPIO12-23 (contiguous!) |
| Latch control outputs (3 OE + 3 LE) | 6 | GPIO26-31 |
| /BD_OE_BUS master enable | 1 | GPIO32 |
| Other input sniffs (BMEM, BINACK, BMCL, BINPUT, INGRANT, INIDENT) | 6 | GPIO33-38 |
| /BINT 10 + /BINT 11 drive | 2 | GPIO39-40 |
| Bus output drives (BAPR, BDRY, BINPUT, BDAP, BREQ) | 5 | GPIO41-45 |
| Daisy chain control | 2 | GPIO46-47 |
| Pi Pico W SPI + INTs (when populated) | 6 | GPIO0-1, 4-7 |
| /BINT 12 + /BINT 13 | 2 | GPIO2-3 |
| Subtotal used | 42 | |
| Board reserved (PSRAM, SD, LED) | 6 | GPIO8-11, 24-25 |
| Total | 48 |
Note: The pin layout puts the 8 DBUS bits (GPIO12-19) adjacent to the 4 trigger control signals (GPIO20-23), creating a single contiguous 12-bit block for atomic PIO reads. No external NOR gate or trigger combining logic needed -- PIO does the trigger detection internally via mask compare.
PIO Capture: Three State Machines (Signal + Address + Data)¶
The capture logic is split into three PIO state machines with clear separation of responsibilities. Address and data are read by separate SMs so each can have its own DMA channel and target buffer:
| State Machine | Purpose | Width | Triggered By |
|---|---|---|---|
| SM_SIGNAL | Watch trigger control signals, dispatch | 4 bits | Continuous polling |
| SM_ADDR | Read 24-bit address from 3x latches | 24 bits | IRQ from SM_SIGNAL when /BAPR active |
| SM_DATA | Read 16-bit data from 2x latches | 16 bits | IRQ from SM_SIGNAL when /BIOXE/BDAP/BDRY active |
The three state machines communicate via PIO IRQs: - SM_SIGNAL detects which trigger fired - Raises IRQ4 for address phase (BAPR active) - Raises IRQ5 for data phase (BIOXE/BDAP/BDRY active) - SM_ADDR waits for IRQ4 - SM_DATA waits for IRQ5
This way, address and data events go to separate FIFOs, and the CPU can use separate DMA channels to move them into separate circular buffers in PSRAM.
Architecture¶
+----------------+
/BAPR_IN (GPIO20) --+ | SM_SIGNAL |
/BIOXE_IN (GPIO21) --+------> | sample-on- |
/BDAP_IN (GPIO22) --+ | change, decide |
/BDRY_IN (GPIO23) --+ | which IRQ |
+---+--------+---+
| |
IRQ4 | | IRQ5
(BAPR active)| |(BIOXE/BDAP/BDRY active)
v v
+---------+--+ +--+---------+
GPIO12-19 (DBUS) <----+ | SM_ADDR | | SM_DATA | -+--> Data FIFO (16-bit)
+-| read 3 | | read 2 | | via DMA channel 2
/OE_IN_0 (GPIO26) <---+ | latches | | latches | | --> data buffer
/OE_IN_1 (GPIO27) <---+ | -> 24 bits | | -> 16 bits | |
/OE_IN_2 (GPIO28) <---+ +------+-----+ +-----+------+ +--> Address FIFO (24-bit)
| | via DMA channel 1
v v --> address buffer
Address FIFO Data FIFO
How SM_SIGNAL Decides Which IRQ¶
SM_SIGNAL examines the trigger pattern when a change is detected: - /BAPR active (bit 0 LOW) → raise IRQ4 (address phase) - /BIOXE, /BDAP, or /BDRY active (bits 1-3 LOW) → raise IRQ5 (data phase) - All inactive → no IRQ (just bus state release, optional push)
Note: /BAPR is always asserted first in any cycle (it's the address strobe). The data phase triggers (BIOXE/BDAP/BDRY) come later. So the typical sequence is: 1. /BAPR LOW → IRQ4 → SM_ADDR reads 24-bit address 2. /BIOXE (or /BDAP, /BDRY) LOW → IRQ5 → SM_DATA reads 16-bit data 3. Cycle releases
SM_SIGNAL: Trigger Detection with Dispatch¶
.program sm_signal
; Reads 4 trigger pins (GPIO20-23) and dispatches to ADDR or DATA SM.
; Pin base = GPIO20.
;
; Y register holds previous trigger state.
; X register holds current trigger state.
.wrap_target
mov isr, null ; clear ISR
in pins, 4 ; sample 4 trigger pins (1 cycle)
mov x, isr ; X = current state (1 cycle)
jmp x != y new_state ; compare with Y (1 cycle)
jmp .wrap_target ; same, keep sampling
new_state:
mov y, x ; remember new state (1 cycle)
push ; push trigger state to signal FIFO (1 cycle)
; Decide which IRQ to raise based on which trigger is active.
; Trigger pins are active LOW. We need to check bit 0 (BAPR).
; Use OUT to shift bit 0 into the lowest bit of OSR/scratch.
mov osr, x ; copy X to OSR
out null, 1 ; discard, but check via JMP-on-OSR? No.
; Simpler: use a separate jmp test
; Actually, since /BAPR is always first, we can use a different approach:
; Just raise IRQ4 always, let CPU dispatch
;
; OR raise both IRQ4 and IRQ5, both target SMs check the trigger bits
irq set 4 ; address phase signal (cleared by SM_ADDR)
irq set 5 ; data phase signal (cleared by SM_DATA)
.wrap
Note: PIO mov+jmp branching on bit values is awkward. The simpler approach is to raise both IRQs when any trigger changes, and let SM_ADDR and SM_DATA each verify their condition. This is more PIO-friendly.
Alternative cleaner design: SM_SIGNAL only watches /BAPR and the OR-of-other-3-via-PIO-comparison. But this gets complex.
Simplest correct design: SM_SIGNAL raises BOTH IRQs on any trigger change. SM_ADDR reads the latches whenever IRQ4 fires (which is always after BAPR captures). SM_DATA reads when IRQ5 fires (which is when /BIOXE/BDAP/BDRY captures). The CPU uses the signal FIFO to know which read is valid (e.g., ignore SM_ADDR output if BAPR was not active, or use separate DMA channels and let CPU correlate).
OR: have SM_SIGNAL only raise IRQ4 when bit 0 (BAPR) goes LOW, and only raise IRQ5 when bits 1-3 (data triggers) go LOW. This requires two separate checks in PIO, which is doable.
.program sm_signal_v2
; More precise dispatch using two separate checks.
.wrap_target
mov isr, null
in pins, 4
mov x, isr
jmp x != y new_state
jmp .wrap_target
new_state:
mov y, x ; remember new state
push ; push trigger state
; Check if /BAPR (bit 0) is now LOW
mov osr, x ; OSR = X
out null, 0 ; (no shift, prepare)
out x, 1 ; X = bit 0 of OSR (= /BAPR state)
jmp !x bapr_active ; if X == 0 (BAPR LOW = active), raise ADDR IRQ
jmp check_data
bapr_active:
irq set 4 ; signal ADDR SM
check_data:
; X has been clobbered. Reload from Y (which still has the new state).
mov x, y
mov osr, x
out null, 1 ; shift past bit 0
out x, 3 ; X = bits 1-3 (BIOXE/BDAP/BDRY)
jmp x != 0b111 data_active
jmp .wrap_target
data_active:
irq set 5 ; signal DATA SM
.wrap
This is more complex but more precise. Each iteration: - 4 cycles for the basic sample-compare - +6 cycles when triggered (push + check BAPR + check data) - Total ~10 cycles (~70 ns) per trigger event
SM_ADDR: 24-bit Address Reader¶
Purpose: Wait for IRQ4 from SM_SIGNAL. Read all 3 bytes from the input latches and push the 24-bit address.
.program sm_addr
; Reads 24-bit address from 3x 74LVC574 latches.
; Pin base for IN = GPIO12 (DBUS 0).
; Sideset = 3 pins on GPIO26-28 (/OE_IN_0/1/2).
;
; Sideset values (active LOW chip selects):
; 0b111 = all latches deasserted
; 0b110 = OE_IN_0 LOW = latch 0 (BD 0-7)
; 0b101 = OE_IN_1 LOW = latch 1 (BD 8-15)
; 0b011 = OE_IN_2 LOW = latch 2 (BD 16-23)
.side_set 3
.wrap_target
irq wait 4 side 0b111 ; wait for IRQ4, deassert all (cleared on wait)
; Read latch 0 (BD 0-7)
nop side 0b110 ; assert /OE_IN_0 (settling time)
in pins, 8 side 0b110 ; read 8 bits into ISR
; Read latch 1 (BD 8-15)
nop side 0b101 ; switch to /OE_IN_1
in pins, 8 side 0b101 ; read next 8 bits
; Read latch 2 (BD 16-23)
nop side 0b011 ; switch to /OE_IN_2
in pins, 8 side 0b011 ; read final 8 bits
push side 0b111 ; push 24-bit address, deassert all
.wrap
Time per address read: 8 cycles (~56 ns) including IRQ wait + 3 latch reads + push.
SM_DATA: 16-bit Data Reader¶
Purpose: Wait for IRQ5 from SM_SIGNAL. Read 2 bytes from the input latches (BD 0-15 only -- data is 16-bit on the ND-100 bus). Push the 16-bit data.
.program sm_data
; Reads 16-bit data from 2x 74LVC574 latches (bytes 0 and 1).
; Pin base for IN = GPIO12 (DBUS 0).
; Sideset = 3 pins on GPIO26-28.
.side_set 3
.wrap_target
irq wait 5 side 0b111 ; wait for IRQ5, deassert all
; Read latch 0 (BD 0-7)
nop side 0b110
in pins, 8 side 0b110
; Read latch 1 (BD 8-15)
nop side 0b101
in pins, 8 side 0b101
push side 0b111 ; push 16-bit data, deassert all
.wrap
Time per data read: 6 cycles (~42 ns) -- one less latch read than SM_ADDR.
Note: ND-100 data is 16-bit, but we have 3 latches. SM_DATA only reads latches 0 and 1 (BD 0-15). Latch 2 (BD 16-23) is only read by SM_ADDR for the 24-bit memory address.
DMA Channels¶
| FIFO | DMA Channel | Target Buffer | Width | Purpose |
|---|---|---|---|---|
| Signal FIFO | DMA0 | signal_events[] | 4 bits | Trigger states |
| Address FIFO | DMA1 | address_events[] | 24 bits | Latched addresses |
| Data FIFO | DMA2 | data_events[] | 16 bits | Latched data words |
Each DMA channel runs continuously, draining its FIFO into a circular buffer in PSRAM. The CPU iterates through the buffers and correlates events by sequence/timestamp.
CPU Side -- Three Buffers¶
// Circular buffers (in PSRAM, sized for ~1ms of bus activity)
volatile uint32_t signal_events[1024];
volatile uint32_t address_events[1024];
volatile uint32_t data_events[1024];
void process_bus_events(void) {
// Drain all 3 buffers in order
while (signal_buffer_has_data()) {
uint32_t signals = signal_pop();
bool bapr_act = !(signals & 0x1);
bool bioxe_act = !(signals & 0x2);
bool bdap_act = !(signals & 0x4);
bool bdry_act = !(signals & 0x8);
if (bapr_act) {
// Address phase -- read corresponding entry from address buffer
uint32_t addr = address_pop() & 0x00FFFFFF;
handle_address_phase(addr);
}
if (bioxe_act || bdap_act || bdry_act) {
// Data phase -- read corresponding entry from data buffer
uint16_t data = data_pop() & 0xFFFF;
if (bioxe_act) handle_iox_data(data);
else if (bdap_act) handle_memory_data(data);
else if (bdry_act) handle_dma_response(data);
}
}
}
The signal FIFO is the synchronization source -- it tells the CPU what kind of event is in the address and data FIFOs. The CPU uses the signal events as the dispatcher.
Why Three SMs Instead of Two?¶
| Benefit | Why |
|---|---|
| Address and data on separate DMA channels | Each goes to its own buffer in PSRAM |
| 24-bit vs 16-bit assembly happens in PIO | CPU receives clean values, no byte assembly |
| Separate timing | Address read (8 cycles) vs data read (6 cycles) optimized for each |
| Parallelism | SM_ADDR and SM_DATA can run in parallel if needed |
| Cleaner C code | Separate handlers for address vs data, no mode flag |
| Easier debugging | Each FIFO/buffer is one purpose -- easy to inspect |
Latch Timing¶
The 74LVC574 propagation delay (output enable to data valid) is 3-7 ns. The PIO instruction time is ~7 ns at 150 MHz. The nop side X instruction between asserting the chip select and reading provides one cycle of settling time -- enough margin in most cases.
If the data is unstable (high-speed bus, long traces), add more nop instructions to extend settling time.
Bandwidth Analysis¶
| Bus state | Signal/Address/Data events | Combined bandwidth |
|---|---|---|
| Idle | 0 | 0 |
| Single IOX cycle | ~3 sig + 1 addr + 1 data | ~16 bytes |
| DMA cycle | ~5 sig + 1 addr + 1 data | ~24 bytes |
| Burst DMA continuous | ~1M signal evts/s, ~500K addr/s, ~500K data/s | ~12 MB/s combined |
Even worst case is well within PSRAM DMA capability.
Decoding in C code:
void process_bus_event(uint16_t event) {
uint8_t dbus = event & 0xFF; // bits 0-7: DBUS data
bool bapr_active = !(event & 0x100); // bit 8: /BAPR (inverted because active LOW)
bool bioxe_active = !(event & 0x200); // bit 9: /BIOXE
bool bdap_active = !(event & 0x400); // bit 10: /BDAP
bool bdry_active = !(event & 0x800); // bit 11: /BDRY
if (bapr_active) {
// Address phase -- dbus contains the address byte
// Continue reading other bytes of address from latches via OE_IN_1/2
handle_address_phase(dbus);
} else if (bioxe_active) {
// IOX data phase
handle_iox_data(dbus);
} else if (bdap_active) {
// Memory data phase
handle_memory_data(dbus);
} else if (bdry_active) {
// DMA read response
handle_dma_data(dbus);
}
}
Note on the 8-bit DBUS: When PIO captures, it reads only 8 bits of the 24-bit BD bus -- the byte currently selected by /OE_IN_0/1/2. The PIO program should sequence through reading all 3 bytes of the latches before processing.
"Spare" Pin Uses (Not Truly Free)¶
The 6 GPIO0-1, GPIO2-3, GPIO4-7 marked as "spare" are technically available but have BB48R hardware functions assigned:
| GPIO | BB48R Function | Notes |
|---|---|---|
| GPIO0 | UART0_TX (pUEXT pin 4) | Free if UART0 not used |
| GPIO1 | UART0_RX (pUEXT pin 5) | Free if UART0 not used |
| GPIO2 | I2C1_SDA (pUEXT/Qwiic) | 2.2K pull-up to 3.3V always present |
| GPIO3 | I2C1_SCL (pUEXT/Qwiic) | 2.2K pull-up to 3.3V always present |
| GPIO4 | SPI0_RX/MISO (pUEXT pin 8) | Hardware SPI0 peripheral |
| GPIO5 | SPI0_CSn (pUEXT pin 9) | Hardware SPI0 peripheral |
| GPIO6 | SPI0_SCK (pUEXT pin 10) | Hardware SPI0 peripheral |
| GPIO7 | SPI0_TX/MOSI (pUEXT pin 11) | Hardware SPI0 peripheral |
These pins are exposed via the pUEXT and Qwiic connectors on the BB48R module itself. The connectors do not consume pins -- they just expose them for breadboard use.
How these pins are used in the locked-in design:
| Pins | Locked-in use | Notes |
|---|---|---|
| GPIO0, 1 | Pi Zero handshake INT pair (INT_BB48R, INT_FROM_ZERO) |
Optional Pi Zero only -- if no Pi Zero is fitted these are spare |
| GPIO2, 3 | /BINT 12, /BINT 13 drive (74LVC07 open-drain) | Always populated. The 2.2 kΩ I2C pull-ups on these pins help the open-drain bus drive |
| GPIO4, 5, 6, 7 | SPI0 to Pi Zero (MISO, CS, SCK, MOSI -- straight wiring) | Optional Pi Zero only. If no Pi Zero, these are spare |
When Pi Zero is not populated, GPIO0-1 and GPIO4-7 (6 pins) are spare. The pUEXT/Qwiic connectors on the module remain physically accessible -- you can plug a sensor or debug board straight onto the BB48R while it sits in the controller card socket.
SD Card Software (Built-in)¶
The BB48R uses SPI1 hardware peripheral for the SD card:
// SD card uses SPI1 with these pins (set by Olimex):
// GPIO9 = CS (SPI1_CSn)
// GPIO10 = CLK (SPI1_SCK)
// GPIO11 = CMD/MOSI (SPI1_TX)
// GPIO24 = DAT0/MISO (SPI1_RX)
// Use the pico-extras SD card library:
// https://github.com/raspberrypi/pico-extras/tree/master/src/rp2_common/pico_sd_card
The default mode is SPI mode. The schematic notes "Option for 1-bit MMC Data was added too" -- so SDIO mode could be enabled if higher throughput is needed in the future.
Why RP2350B (chip-level)¶
The RP2350B is selected over the smaller RP2350A because of its 48 GPIO pins (vs 30), enabling a full 24-bit parallel bus interface within a single GPIO bank without compromises.
Key features used¶
| Feature | Why |
|---|---|
| 48 GPIO in two banks (32 + 16) | Full 24-bit bus + control + SD card + debug |
| 12 PIO state machines | Deterministic bus timing for IOX/IDENT/DMA cycles |
| DMA controllers | High-throughput transfers between PIO FIFOs and RAM |
| Dual ARM Cortex-M33 cores | One core for bus protocol, one for device emulation |
| 520 KB internal SRAM | Multi-device buffers, FIFOs, ring buffers |
| 16 MB QSPI flash (BB48R) | Firmware + boot ROM images + small disk images |
| 8 MB PSRAM (BB48R) | Floppy image cache, SMD sector cache, HDLC FIFOs |
GPIO Bank Architecture¶
The RP2350B has two GPIO banks with separate control registers:
| Bank | Pins | Purpose |
|---|---|---|
| LOW bank | GPIO0-31 | Time-critical: bus signals, control |
| HIGH bank | GPIO32-47 | Slow peripherals: SD card, debug, UART |
Critical rule: The 24-bit bus must be in a single bank to allow single-cycle parallel read/write. Splitting across banks would require two separate writes with timing skew -- unacceptable for bus protocol timing.
External Flash and PSRAM Pin Reservations¶
Both RP2040 and RP2350B require dedicated pins for external QSPI flash (XIP execute-in-place). These pins are not usable as GPIO.
| Resource | RP2040 (raw) | RP2350B (raw) | Olimex BB48R module |
|---|---|---|---|
| Total GPIO pool | 30 (GPIO0-29) | 48 (GPIO0-47) | 48 (GPIO0-GPIO47) |
| QSPI flash pins | 6 (mandatory, eats GPIO) | ~6 (separate from GPIO) | Internal to module, not in GPIO pool |
| QSPI PSRAM (optional) | Not supported | 6-11 pins if used | Internal QSPI, only GPIO8 used as CS |
| Module-reserved | -- | -- | GPIO8 (PSRAM), 9-11 (SD), 24 (SD), 25 (LED) |
| USB pins | GP24/GP25 | varies | Internal, not in GPIO pool |
| RESET pin | RUN (dedicated) | RUN (dedicated) | RUN, button on board |
| BOOTSEL | -- | QSPI_CS | QSPI_CS, button on board |
| Practical usable GPIO | ~24 | ~31-36 | 42 (after SD/PSRAM/LED reservations) |
Olimex BB48R: All special pins (QSPI flash, USB, RUN, BOOTSEL) are either internal to the module or use dedicated pins. The only GPIO costs are 4 pins for SD card (GPIO9-11, 24), 1 pin for PSRAM CS (GPIO8), and 1 pin for User LED (GPIO25) -- total 6 pins. The remaining 42 GPIO are free for the controller card design.
Single-bank atomic GPIO access (used by the PIO read path)¶
The 8-bit DBUS lives on GPIO12-19 -- a single contiguous byte inside the LOW bank. The PIO state machine reads and drives it via IN PINS, 8 / OUT PINS, 8 instructions, so the C code never touches the DBUS directly. For housekeeping operations (initial direction setup, sanity checks) the MCU can still hit the GPIO mask register directly:
#define DBUS_MASK (0xFFu << 12) // GPIO12-19, the 8-bit shared MCU<->latch bus
#define LATCH_OE_MASK (0x07u << 26) // GPIO26-28: /OE_IN_0/1/2 (read input latches)
#define LATCH_LE_MASK (0x07u << 29) // GPIO29-31: LE_OUT_0/1/2 (write output latches)
// Set DBUS as outputs (when driving the latches)
gpio_hw->oe_set = DBUS_MASK;
// Set DBUS as inputs (when reading from the latches)
gpio_hw->oe_clr = DBUS_MASK;
// Drive a byte on DBUS (PIO normally does this, not C)
gpio_hw->out = (gpio_hw->out & ~DBUS_MASK) | ((value & 0xFFu) << 12);
// Read the current DBUS state
uint32_t byte = (gpio_hw->in & DBUS_MASK) >> 12;
Never use SDK functions like
gpio_put()in the hot path -- too slow and non-deterministic for bus-level timing. Always go through the PIO state machine for bus cycles, and use the rawgpio_hw->out/gpio_hw->inregisters for any C-level housekeeping.Note: There is no single 24-bit GPIO read/write in this architecture. The full 24-bit BD value is moved as 3 sequential 8-bit operations through the latches (see "BD 0-23 Bus Interface Architecture"). This is the trade-off for not consuming 24 contiguous LOW-bank GPIO -- which would be impossible anyway because GPIO8-11 are reserved by the BB48R.
BD 0-23 Bus Interface Architecture (8-bit Latched, PIO-driven)¶
This is the locked-in architecture for the controller card. Alternative architectures (Direct GPIO, SPI Shift Register, PIO-as-Latch) were evaluated but are not viable on the Olimex BB48R due to GPIO fragmentation. See Appendix A for the rejected alternatives.
A shared 8-bit data bus connects to 6 octal latches: 3 for capturing input from the bus, 3 for driving output to the bus. Chip selects choose which latch the MCU is currently accessing. The MCU reads or writes the 24-bit BD value as 3 sequential 8-bit operations controlled by a PIO state machine.
Block Diagram¶
flowchart LR
subgraph BUS["ND-100 Bus"]
BD["/BD 0-23 (24 lines)"]
end
subgraph IN["Input Latches (BAPR clocked)"]
L1["74LVC573 #1<br/>BD 0-7"]
L2["74LVC573 #2<br/>BD 8-15"]
L3["74LVC573 #3<br/>BD 16-23"]
end
subgraph OUT["Output Drivers"]
D1["74LVC245 #1<br/>BD 0-7"]
D2["74LVC245 #2<br/>BD 8-15"]
D3["74LVC245 #3<br/>BD 16-23"]
end
subgraph MCU["RP2350B"]
D8["Shared 8-bit bus<br/>GPIO 12-19"]
CS["3x /OE_IN<br/>3x LATCH_OUT"]
OE["/BD_OE_BUS"]
end
BD --> L1
BD --> L2
BD --> L3
BD <-- D1
BD <-- D2
BD <-- D3
L1 --> D8
L2 --> D8
L3 --> D8
D8 --> D1
D8 --> D2
D8 --> D3
CS --> L1
CS --> L2
CS --> L3
CS --> D1
CS --> D2
CS --> D3
OE --> D1
OE --> D2
OE --> D3
BAPR["/BAPR (clock)"] --> L1
BAPR --> L2
BAPR --> L3
style BUS fill:#FFF3E0,stroke:#E65100,color:#E65100
style IN fill:#E0F7FA,stroke:#00838F,color:#00838F
style OUT fill:#E0F7FA,stroke:#00838F,color:#00838F
style MCU fill:#E3F2FD,stroke:#0D47A1,color:#0D47A1
Pin Allocation¶
DBUS sits on GPIO12-19 (not GPIO0-7) so the on-module SPI0/I2C/UART pins (GPIO0-7) stay free for the optional Pi Zero header. GPIO8-11 are blocked by the BB48R itself (PSRAM CS, SD CS/CLK/CMD), and GPIO24-25 by the on-module SD_DAT0 / User_LED.
| GPIO | Signal | Direction | Function |
|---|---|---|---|
| 12-19 | DBUS 0-7 | Bidirectional | Shared 8-bit MCU↔latch bus (single-bank, single-cycle) |
| 26 | /OE_IN_0 | Output | Read input latch 0 (BD 0-7) |
| 27 | /OE_IN_1 | Output | Read input latch 1 (BD 8-15) |
| 28 | /OE_IN_2 | Output | Read input latch 2 (BD 16-23) |
| 29 | LE_OUT_0 | Output | Latch output 0 (BD 0-7) |
| 30 | LE_OUT_1 | Output | Latch output 1 (BD 8-15) |
| 31 | LE_OUT_2 | Output | Latch output 2 (BD 16-23) |
| 32 | /BD_OE_BUS | Output | Enable our card to drive the bus (gates all 3 output drivers) |
BD pins used: 15 (8 data + 6 latch control + 1 master OE)
All 15 BD pins are LOW-bank-friendly except
/BD_OE_BUSon GPIO32. The 8 DBUS lines (GPIO12-19) and the 6 latch-control lines (GPIO26-31) are all in the LOW bank, so the PIO state machine can drive them with a singleout pins/in pinsinstruction./BD_OE_BUSlives in the HIGH bank because it does not need to switch in lockstep with the data — it changes once at the start of a drive cycle and again at the end.
Component List¶
| IC | Qty | Function | Approx Cost |
|---|---|---|---|
| 74LVC573 | 3 | Octal D-latch, captures BD on /BAPR | $1.50 |
| 74LVC245 | 3 | Octal bus driver, output to bus | $1.50 |
| 74LVT138 | 1 | 3-to-8 decoder (optional, for CS generation) | $0.40 |
| Bypass caps (0.1uF) | 6 | One per IC | $0.20 |
| Subtotal BD path | $3.60 |
PIO State Machine Operation¶
Read 24 bits from bus (after /BAPR latched data):
PIO SM:
Set /OE_IN_0 = 0 (1 cycle)
Read DBUS 0-7 (1 cycle)
Set /OE_IN_0 = 1 (1 cycle)
Set /OE_IN_1 = 0 (1 cycle)
Read DBUS 0-7 (1 cycle)
Set /OE_IN_1 = 1 (1 cycle)
Set /OE_IN_2 = 0 (1 cycle)
Read DBUS 0-7 (1 cycle)
Set /OE_IN_2 = 1 (1 cycle)
Total: ~9 PIO cycles @ 150 MHz = ~60 ns
Write 24 bits to bus:
PIO SM:
Drive DBUS = byte0 (1 cycle)
Pulse LE_OUT_0 (1 cycle)
Drive DBUS = byte1 (1 cycle)
Pulse LE_OUT_1 (1 cycle)
Drive DBUS = byte2 (1 cycle)
Pulse LE_OUT_2 (1 cycle)
Set /BD_OE_BUS = 0 (1 cycle, enable bus drive)
Total: ~7 PIO cycles @ 150 MHz = ~50 ns + level shifter delay
Timing Analysis¶
| Operation | Time | Notes |
|---|---|---|
| 24-bit read (3 chunks) | ~60 ns | PIO @ 150 MHz |
| 24-bit write (3 chunks) | ~50 ns | PIO @ 150 MHz |
| Latch propagation | 3-5 ns | 74LVC573 |
| Level shifter delay | 3-6 ns | 74LVC245 |
| Total round-trip read | ~70-80 ns | Including latch + read chunks |
| Total round-trip write | ~60-70 ns | Including write chunks + latch |
Bus Cycle Performance¶
| Cycle | Time | Margin (8 us) |
|---|---|---|
| IOX response | ~200-400 ns | 20x |
| IDENT decision | ~70-90 ns | within 100 ns window ✓ |
| DMA word cycle | ~400-700 ns | 11x |
| DMA throughput | ~1.5-2 MB/s | Sufficient for all devices including SMD |
Pros¶
- Moderate pin count -- 15 pins for BD interface (vs 26 for Design 1)
- Frees ~10 LOW bank pins for other time-critical signals
- Good throughput -- meets all device requirements
- Deterministic via PIO state machine
- Hardware latch decouples MCU from BAPR timing
Cons¶
- More external components -- 6 latch/driver chips + optional decoder
- PIO program complexity higher than Design 1
- Three sequential operations per 24-bit access (vs single op in Design 1)
- IDENT decision time is tighter (~70-90 ns vs 100 ns window)
Common Non-BD Signal Layout¶
The non-BD signals use the same layout in all three designs:
| Signal | RP2350 GPIO | Direction | Buffer IC |
|---|---|---|---|
| /BAPR | 1 pin | In | 74LVC14 (Schmitt) |
| /BIOXE | 1 pin | In | 74LVC14 |
| /BINACK | 1 pin | In | 74LVC14 |
| /BMEM | 1 pin | In | 74LVC14 |
| /BMCL | 1 pin | In | 74LVC14 |
| /BDRY | 1 pin | Bidir | 74LVC07 (open-drain) + 74LVC14 (read-back) |
| /BINPUT | 1 pin | Bidir | 74LVC07 + 74LVC14 |
| /BDAP | 1 pin | Bidir | 74LVC07 + 74LVC14 |
| /BREQ | 1 pin | Out | 74LVC07 (open-drain) |
| /INGRANT | 1 pin | In | (via daisy-chain chip) |
| /OUTGRANT | 1 pin | Out | (via daisy-chain chip) |
| /INIDENT | 1 pin | In | (via daisy-chain chip) |
| /OUTIDENT | 1 pin | Out | (via daisy-chain chip) |
| /BINT 10 | 1 pin | Out | 74LVC07 |
| /BINT 11 | 1 pin | Out | 74LVC07 |
| /BINT 12 | 1 pin | Out | 74LVC07 |
Common non-BD pins: 16
For the bidirectional signals (/BDRY, /BINPUT, /BDAP), one approach uses two GPIO pins (1 to drive open-drain output, 1 to read state). The simpler approach uses 1 GPIO in open-drain mode (RP2350 GPIO supports this) plus a separate input buffer reading the same line.
IDENT/GRANT Daisy-Chain Pass-Through Chip (All Designs)¶
The 100 ns IDENT decision window and the need for fast daisy-chain propagation make a hardware default pass-through essential. A single 74LVC125 quad 3-state buffer handles both daisy chains:
Operation¶
Default (idle): PIO drives /OE_PASS = LOW
Buffer enabled
/INIDENT --> [LVC125 ch.1] --> /OUTIDENT (3-5 ns delay)
/INGRANT --> [LVC125 ch.2] --> /OUTGRANT (3-5 ns delay)
Capture (we want this interrupt or DMA cycle): PIO drives /OE_PASS = HIGH
Buffer high-Z
/OUTIDENT and /OUTGRANT float HIGH (next slot sees idle)
Our card drives BD lines + /BDRY for IDENT response
Or starts DMA cycle for GRANT capture
IC Selection¶
| IC | Function | Notes |
|---|---|---|
| 74LVC125 | Quad 3-state buffer with independent enables | Recommended -- 3-5 ns delay, simple |
| 74LVC126 | Same with active-high enables | Alternative |
| ATF22V10 PAL | Programmable | Overkill but allows custom logic |
| ATF1502 CPLD | Programmable | If complex pass-through logic needed |
The 74LVC125 is the simplest and fastest option. Two channels handle each daisy-chain pair (in + out), leaving 0 spare. If you need spare buffer channels for other use, a 74LVC126 offers 4 channels.
Pin Cost¶
The pass-through chip needs: - 1 GPIO for /OE_IDENT_PASS (controls IDENT chain) - 1 GPIO for /OE_GRANT_PASS (controls GRANT chain) - Or combine both onto 1 GPIO if they're always controlled together
Pin cost: 1-2 GPIO (already counted in non-BD signal table above for INGRANT/OUTGRANT and INIDENT/OUTIDENT)
Design 2 Detailed Implementation -- Chip Selection, Read/Write, Memory Emulation¶
This section details exactly how Design 2 (8-bit Latched BD Interface) works at the chip level, including read/write sequences, bus drive control, timing verification for IOX and DMA, and an extension for emulating RAM on the bus.
Chip Selection¶
Design 2 uses three logical groups:
Input Latch Group (Capture from Bus)¶
Function: Capture the 24-bit BD bus state instantly when /BAPR asserts, so the MCU can read it later at its leisure.
Recommended chip: 74LVC574 -- octal D-type flip-flop with 3-state outputs.
| Feature | Value |
|---|---|
| Type | Octal positive-edge-triggered D flip-flop |
| Inputs | 5V tolerant (input pins accept 5V at 3.3V VCC) |
| Outputs | 3-state, controlled by /OE |
| Clock | Captures D on rising edge of CLK |
| Propagation delay | 3-7 ns |
| Supply | 1.65V - 3.6V |
Why edge-triggered (574) instead of transparent latch (573): - 574 captures on a clean edge (rising edge of CLK) - 573 is transparent while LE is HIGH -- data tracks input - We want to freeze the bus state at the moment /BAPR asserts, not track it
Connection:
Bus side: /BD 0-7 -> 74LVC574 #1 D inputs (8 lines)
/BD 8-15 -> 74LVC574 #2 D inputs
/BD 16-23 -> 74LVC574 #3 D inputs
Clock: /BAPR -> 74LVC14 inverter -> CLK pin (rising edge = falling edge of /BAPR)
(all three latches share same CLK)
MCU side: /OE_IN_0 controlled by PIO -> 74LVC574 #1 /OE
/OE_IN_1 controlled by PIO -> 74LVC574 #2 /OE
/OE_IN_2 controlled by PIO -> 74LVC574 #3 /OE
Q outputs of all three latches connect to shared 8-bit MCU data bus (only one /OE active at a time)
Total: 3x 74LVC574 + 1x 74LVC14 (the inverter is shared with other signal conditioning).
Output Drive Group (Drive to Bus)¶
Function: Hold a 24-bit value stable, then drive it onto the bus when commanded.
Two-stage approach (recommended):
- Output latches: 3x 74LVC574 -- hold the 24-bit value the MCU wants to send
- Output transceivers: 3x 74LVT245 -- level shift 3.3V to 5V, drive the bus with sufficient strength
| Stage | Chip | Function |
|---|---|---|
| Latch | 74LVC574 | Holds data, clocked by PIO when MCU writes |
| Driver | 74LVT245 | 3.3V to 5V level shift, /OE controlled by PIO |
Connection:
MCU side: Shared 8-bit bus -> 74LVC574 #4 D inputs
-> 74LVC574 #5 D inputs
-> 74LVC574 #6 D inputs
Latch CLK: PIO drives LE_OUT_0 -> 74LVC574 #4 CLK
PIO drives LE_OUT_1 -> 74LVC574 #5 CLK
PIO drives LE_OUT_2 -> 74LVC574 #6 CLK
Latch -> Driver:
74LVC574 #4 Q outputs -> 74LVT245 #1 A inputs
74LVC574 #5 Q outputs -> 74LVT245 #2 A inputs
74LVC574 #6 Q outputs -> 74LVT245 #3 A inputs
Driver -> Bus:
74LVT245 #1 B outputs -> /BD 0-7
74LVT245 #2 B outputs -> /BD 8-15
74LVT245 #3 B outputs -> /BD 16-23
All three 74LVT245 DIR pins tied HIGH (always A->B = transmit)
All three 74LVT245 /OE pins tied to /BD_OE_BUS (PIO control)
Total: 3x 74LVC574 (output latches) + 3x 74LVT245 (output drivers) = 6 chips.
Alternative: Registered Transceivers (74LVC646)¶
A more compact option uses 74LVC646 -- octal bus transceiver with built-in registers in BOTH directions and 3-state outputs.
| Feature | 74LVC646 |
|---|---|
| Type | Bus transceiver with internal D registers |
| Direction | Bidirectional (DIR pin) |
| Latches | One in each direction (8-bit each) |
| 3-state | Independent /OE for each direction |
Three 74LVC646 chips can replace the 3x 74LVC574 input + 3x 74LVC574 output + 3x 74LVT245 = 6 chips become 3 chips. This saves PCB area and reduces routing complexity.
Trade-off: 74LVC646 is less common than 74LVC574 and 74LVT245. Stock and price may favor the discrete approach.
Recommendation: Start with discrete 74LVC574 + 74LVT245 for the prototype (easy to source, well documented). Migrate to 74LVC646 for the production board to reduce chip count.
Total Chip Count for Design 2 (Discrete)¶
| Chip | Quantity | Function |
|---|---|---|
| 74LVC574 | 3 | Input latches (BD bus -> MCU) |
| 74LVC574 | 3 | Output latches (MCU -> drive stage) |
| 74LVT245 | 3 | Output drivers (3.3V -> 5V, drive bus) |
| 74LVC14 | 1 | Schmitt-trigger inverters (/BAPR clock + signal conditioning) |
| 74LVC07 | 1 | Open-drain wired-OR drivers (BREQ, BINT, etc.) |
| 74LVC125 | 1 | Daisy-chain pass-through buffer |
| Total | 12 |
(Plus pull resistors and bypass capacitors)
Read Sequence (24-bit BD Bus -> MCU)¶
The read happens in two phases: hardware capture (instant on /BAPR), then MCU reads via 3 chunks.
Phase 1: Hardware Capture (0 ns MCU time)¶
1. /BAPR asserts on bus (CPU drives address)
2. 74LVC14 inverts /BAPR -> CLK rising edge
3. All three 74LVC574 input latches capture BD 0-23 simultaneously
4. Address is now frozen in latches
5. Bus state can change without affecting captured data
Phase 2: MCU Reads via Shared 8-bit Bus¶
PIO state machine reads (typical timing at 150 MHz PIO clock):
Cycle 1: Set /OE_IN_0 = LOW (~7 ns)
Cycle 2: Read GPIO 12-19 -> byte0 (~7 ns)
Cycle 3: Set /OE_IN_0 = HIGH (~7 ns)
Cycle 4: Set /OE_IN_1 = LOW (~7 ns)
Cycle 5: Read GPIO 12-19 -> byte1 (~7 ns)
Cycle 6: Set /OE_IN_1 = HIGH (~7 ns)
Cycle 7: Set /OE_IN_2 = LOW (~7 ns)
Cycle 8: Read GPIO 12-19 -> byte2 (~7 ns)
Cycle 9: Set /OE_IN_2 = HIGH (~7 ns)
MCU value = (byte2 << 16) | (byte1 << 8) | byte0
Total: ~63 ns (9 cycles @ 150 MHz)
Plus latch /OE propagation delay: ~3-5 ns per chunk
Total realistic: ~80-100 ns
The shared bus prevents bus contention because only one input latch has /OE active at a time.
Write Sequence (MCU -> 24-bit BD Bus)¶
Phase 1: MCU Loads Output Latches¶
PIO state machine writes:
Cycle 1: Drive GPIO 12-19 = byte0 (~7 ns)
Cycle 2: Pulse LE_OUT_0 (HIGH-LOW) (~14 ns, 2 cycles)
-> 74LVC574 #4 captures byte0
Cycle 3: Drive GPIO 12-19 = byte1 (~7 ns)
Cycle 4: Pulse LE_OUT_1 (~14 ns)
Cycle 5: Drive GPIO 12-19 = byte2 (~7 ns)
Cycle 6: Pulse LE_OUT_2 (~14 ns)
Total: ~63 ns to load all three output latches
All 24 bits now sit on the 74LVT245 input pins, ready to drive
Phase 2: Enable Bus Drivers¶
Cycle 7: Set /BD_OE_BUS = LOW (~7 ns)
Cycle 8: Wait for level shifter (3-5 ns 74LVT245 propagation)
Bus now shows the 24-bit value
Driven by 3x 74LVT245 (each with mA-class drive strength)
Total time from start of write to valid bus output: ~80 ns
Phase 3: Release the Bus¶
When data is no longer needed (after /BDRY handshake):
Cycle N: Set /BD_OE_BUS = HIGH (~7 ns)
74LVT245 outputs go high-Z within ~5 ns
Bus is released, other devices may drive
Bus Drive Enable (/BD_OE_BUS) Control¶
The /BD_OE_BUS signal is the master enable for our card to drive the BD bus. Critical safety rules:
| Situation | /BD_OE_BUS State |
|---|---|
| Power-up (before MCU boot) | HIGH (pull-up resistor) -- bus drivers OFF |
| MCU initializing | HIGH (initial GPIO state) -- bus drivers OFF |
| CPU IOX read cycle, we are not target | HIGH -- not driving |
| CPU IOX read cycle, we are the target | LOW during data phase only |
| CPU IOX write cycle | HIGH -- CPU drives, we just listen |
| DMA we initiated, address phase | LOW (we drive address) |
| DMA we initiated, data phase write | LOW (we drive data) |
| DMA we initiated, data phase read | HIGH (memory drives data) |
| Power-down or fault | HIGH (pull-up brings it back) |
The PIO state machine carefully sequences /BD_OE_BUS based on the current bus cycle phase.
Timing Verification: IOX¶
IOX Read (CPU reads from us)¶
Time Event
-------- ------------------------------------------------
0 ns CPU asserts /BAPR (address valid on bus)
2 ns Hardware: 74LVC14 inverts /BAPR
5 ns Hardware: 74LVC574 latches capture 24-bit address
5 ns RP2350 PIO sees /BAPR, starts reading address
85 ns PIO has read all 24 bits via 3 chunks
85 ns PIO decodes: is this our register?
100 ns YES -- prepare data response
100 ns CPU asserts /BIOXE
105 ns PIO sees /BIOXE
110 ns Interface decides: this is read (target reg is input)
110 ns PIO asserts /BINPUT
130 ns CPU sees /BINPUT, releases WDA buffer
140 ns CPU asserts /BINACK
145 ns PIO sees /BINACK, starts loading output latches
220 ns PIO has loaded 3 output latches (24 bits)
220 ns PIO asserts /BD_OE_BUS = LOW (drive bus)
225 ns Output transceivers driving bus
230 ns PIO asserts /BDRY
245 ns CPU strobes data into DBR
250 ns CPU releases /BIOXE and /BINACK
255 ns PIO releases /BD_OE_BUS, /BDRY, /BINPUT, BD lines
255 ns Bus free
TOTAL: ~255 ns from /BAPR to bus release
BUS LIMIT: 8000 ns (8 us)
MARGIN: 31x -- comfortable
IOX Write (CPU writes to us)¶
Time Event
-------- ------------------------------------------------
0 ns CPU asserts /BAPR + address on BD
5 ns Hardware latches address
85 ns PIO has read address
90 ns PIO decodes: is this our register?
100 ns CPU asserts /BIOXE + data on BD
105 ns Hardware: but wait, our latches still hold the ADDRESS
We need to either re-latch on /BIOXE, or use a SEPARATE data latch
** This requires either: **
Option 1: Re-clock the input latches on /BIOXE OR /BAPR (combine via gate)
Option 2: Add separate data input latches clocked by /BIOXE
Option 3: Use 74LVC646 registered transceivers with two captures
Important design consideration: For IOX write, the same input latches that captured the address must be re-clocked to capture the data, OR we need a second set of latches. See "IOX Write Latch Strategy" below.
IOX Write Latch Strategy¶
There are three ways to handle data capture during IOX write:
Strategy A: Re-clock the same latches on /BIOXE
Combine /BAPR and /BIOXE via a logic gate (e.g., AND of inverted signals = OR of asserted signals) to clock the same latches:
CLK_LATCH = NOT(/BAPR) OR NOT(/BIOXE)
= /BAPR LOW OR /BIOXE LOW
74LVC02 NOR gate:
/BAPR ----+
|--NOR--> CLK_LATCH (inverted output)
/BIOXE ---+
Wait, this needs inverted output. Use 74LVC32 (OR):
NOT(/BAPR) -+
|--OR--> CLK_LATCH
NOT(/BIOXE)-+
Or use 74LVC02 NOR:
/BAPR -+ ____
|--NOR--> CLK_LATCH = /BAPR + /BIOXE (HIGH when either is LOW)
/BIOXE-+
When either /BAPR or /BIOXE goes LOW, CLK_LATCH goes HIGH
Rising edge of CLK_LATCH triggers 74LVC574 capture
The PIO must read the latches after the address phase but before the data phase (to grab the address), then again after the data phase (to grab the data).
Strategy B: Separate data input latches
Add 2x more 74LVC574 (16-bit data is sufficient for IOX, but we can do 24 bits to match the bus):
| Latch Set | Trigger | Captures |
|---|---|---|
| Address latches (existing 3x 74LVC574) | /BAPR | 24-bit address |
| Data input latches (new 2x 74LVC574) | /BIOXE | 16-bit data (BD 0-15) |
PIO reads address from address latches, then later reads data from data latches via different chip selects.
Cost: +2 chips, +2 GPIO for separate /OE pins.
Strategy C: Use 74LVC646 with two-step capture
The 74LVC646 has independent latch enables. Trigger the AB latch on /BAPR (address) then trigger again on /BIOXE (data).
But the 74LVC646 has only ONE latch per direction, so you'd lose the address when capturing data. Unless we use the second direction (BA) for data capture. This is messy.
Recommendation: Strategy B (separate data latches). Slightly more chips but cleaner logic and easier debugging.
Updated Chip Count for Design 2 (with Strategy B for IOX write)¶
| Chip | Quantity | Function |
|---|---|---|
| 74LVC574 | 3 | Address input latches (BD 0-23, /BAPR clock) |
| 74LVC574 | 2 | Data input latches (BD 0-15, /BIOXE clock) |
| 74LVC574 | 3 | Output latches (MCU -> drive stage) |
| 74LVT245 | 3 | Output drivers (3.3V -> 5V, drive bus) |
| 74LVC14 | 1 | Schmitt inverters (/BAPR, /BIOXE clock conditioning) |
| 74LVC07 | 1 | Open-drain wired-OR drivers |
| 74LVC125 | 1 | Daisy-chain pass-through buffer |
| Total | 14 |
Timing Verification: DMA¶
For DMA, our card is the bus master. We drive everything.
DMA Output (Memory Read) Cycle¶
Time Event
-------- ------------------------------------------------
0 ns PIO asserts /BREQ (we want the bus)
(CPU/BCU completes current cycle)
200 ns BCU asserts /BMEM and /OUTGRANT
205 ns PIO sees /INGRANT
215 ns We have grant, prepare to drive bus
215 ns PIO loads memory address into output latches (~80 ns)
295 ns PIO asserts /BD_OE_BUS = LOW
300 ns Address valid on bus
305 ns PIO asserts /BAPR
310 ns /BINPUT remains HIGH (= read direction)
360 ns Address phase done, PIO releases /BD_OE_BUS (or holds for BDAP)
360 ns PIO asserts /BDAP ("BD free for memory data")
(Wait for memory to respond)
500 ns Memory drives data on BD lines
510 ns Memory asserts /BDRY
515 ns Hardware: 74LVC574 input latches capture data on /BAPR... wait
NO -- /BAPR is no longer being clocked
We need ANOTHER capture trigger for the read data
Use: /BDRY clocks the input latches for DMA read data
Important: For DMA read, we need to capture the data when memory asserts /BDRY. This is similar to the IOX write problem -- we need a second capture trigger.
Solution: Combine /BAPR + /BIOXE + /BDRY into the latch clock (any of them can trigger capture). Or use Strategy B with a third trigger source.
Updated Strategy B for DMA Read¶
| Latch Set | Trigger | Captures |
|---|---|---|
| Address latches (3x 74LVC574) | /BAPR | 24-bit address (CPU IOX or our DMA) |
| Data input latches (2x 74LVC574) | /BIOXE OR /BDRY (gated) | 16-bit data |
Use a 74LVC02 NOR gate to combine /BIOXE and /BDRY:
CLK_DATA_LATCH = NOT(/BIOXE) OR NOT(/BDRY)
= HIGH when either is LOW
= capture when CPU writes IOX data OR memory responds with DMA data
This works because: - IOX write: CPU asserts /BIOXE -> data latch captures - IOX read: we don't need data latch (we're driving) - DMA read: memory asserts /BDRY -> data latch captures - DMA write: we don't need data latch (we're driving)
So one set of data latches serves both IOX write and DMA read.
DMA Cycle Total Time¶
Time Event
-------- ------------------------------------------------
0 ns Assert /BREQ
200 ns Receive /INGRANT
215 ns Prepare and load address (~80 ns)
305 ns Assert /BAPR + /BMEM + drive bus
360 ns Address phase complete, assert /BDAP
500 ns Memory drives data
515 ns Hardware captures data
600 ns PIO reads data from latches (~80 ns)
610 ns PIO releases /BD_OE_BUS (if still asserted)
620 ns /BDRY trailing edge, bus free
TOTAL: ~620 ns per DMA word
THROUGHPUT: ~1.6 MB/s (one word every 620 ns)
SUFFICIENT FOR: Floppy, terminal, HDLC, SMD (marginal)
For DMA write, the timing is similar but we drive the data instead of capturing it.
Memory Emulation Extension¶
To make the controller card emulate memory (so the CPU sees it as part of the memory system), the card must respond to memory read/write cycles addressed to its memory range.
What Changes¶
- Sniff /BMEM to detect memory cycles vs IOX cycles
- Capture address on /BAPR (already done)
- Decode: is the address in our memory range?
- For memory READ: drive data from our internal RAM/PSRAM to the bus
- For memory WRITE: capture data from the bus into our internal RAM/PSRAM
- Assert /BDRY when ready
Memory Cycle Detection¶
The PIO state machine watches /BMEM and /BAPR: - /BMEM HIGH + /BAPR HIGH = idle - /BAPR LOW with /BMEM HIGH = IOX cycle (existing) - /BAPR LOW with /BMEM LOW = memory cycle (new)
Memory READ Response¶
Time Event
-------- ------------------------------------------------
0 ns CPU asserts /BAPR + /BMEM, address on BD
5 ns Address latches capture
85 ns PIO has read address
90 ns Decode: is this our memory range? YES
90 ns Look up data in internal SRAM/PSRAM
(PSRAM access ~100-200 ns, SRAM ~10 ns)
100 ns CPU asserts /BDAP ("BD free for our data")
200 ns PIO has fetched data from PSRAM
200 ns Load data into output latches (~80 ns for 16-bit)
280 ns Assert /BD_OE_BUS = LOW
285 ns Drive 16-bit data on BD 0-15
290 ns Assert /BDRY
305 ns CPU reads data
310 ns CPU releases /BMEM
315 ns Release everything
TOTAL: ~315 ns per memory read
WITHIN 8 us: yes, 25x margin
Memory WRITE Capture¶
Time Event
-------- ------------------------------------------------
0 ns CPU asserts /BAPR + /BMEM + /BINPUT, address on BD
5 ns Address latches capture
85 ns PIO has read address
90 ns Decode: is this our memory range? YES
100 ns CPU drives data on BD + asserts /BDAP
105 ns Data latches capture (clocked by /BDAP via gate)
185 ns PIO has read data
200 ns PIO writes data to internal SRAM/PSRAM
250 ns Assert /BDRY ("data accepted")
270 ns CPU releases /BMEM
275 ns Release everything
TOTAL: ~275 ns per memory write
Hardware Additions for Memory Emulation¶
The existing Design 2 hardware supports memory emulation with one critical addition: the data input latches must be clocked by /BDAP (not /BIOXE) for memory cycles, OR we add a third trigger:
| Signal | Source | When to Capture |
|---|---|---|
| /BIOXE | CPU IOX cycle | Capture data for IOX write |
| /BDRY | Memory response in DMA | Capture data for DMA read |
| /BDAP | CPU memory write | Capture data for memory write to us |
Combine all three with a 3-input NOR gate (74LVC10) or two 2-input NOR gates:
CLK_DATA_LATCH = NOT(/BIOXE) OR NOT(/BDRY) OR NOT(/BDAP)
= capture on any of the three triggers
Cost: +1 chip (74LVC02 NOR or 74LVC10 3-input NOR)
Memory Emulation Storage¶
For the memory emulation, internal RAM choices:
| Storage | Size | Speed | Use Case |
|---|---|---|---|
| RP2350 internal SRAM | 520 KB | ~10 ns | Fastest, register-style memory |
| PSRAM (BB48R) | 8 MB | ~100-200 ns | Bulk memory, large emulated region |
For a small ROM emulation (boot loader, ~1 KB), use SRAM for fastest response. For emulating a large memory bank, use PSRAM with the SRAM as a sector cache.
Memory Emulation Timing Risk¶
Memory cycles on the ND-100 are fast in real hardware (~200-500 ns). Our emulated memory takes ~275-315 ns. This is within spec but tight if the rest of the system expects faster memory.
If memory emulation is needed for performance-critical purposes (e.g., emulating an extension memory bank used by SINTRAN), test carefully on real hardware. For diagnostic or boot purposes, the timing is fine.
Memory Emulation Limitations¶
- PSRAM access latency (~100-200 ns) eats into the bus cycle budget
- No cache coherency with real ND-100 memory -- our emulated memory is a separate region
- Address space conflicts must be avoided (CPU must not access the same physical address from both real memory and our card)
Summary: Is Design 2 Quick Enough?¶
| Cycle Type | Time | Bus Limit | Margin |
|---|---|---|---|
| IOX read | ~255 ns | 8000 ns | 31x |
| IOX write | ~200 ns | 8000 ns | 40x |
| IDENT response | ~150 ns | 100 ns window for decision | ✓ if hardware pass-through |
| DMA word read | ~620 ns | 8000 ns | 13x |
| DMA word write | ~600 ns | 8000 ns | 13x |
| Memory emulation read | ~315 ns | 8000 ns | 25x |
| Memory emulation write | ~275 ns | 8000 ns | 29x |
All operations fit comfortably within the 8 us bus cycle limit. The IDENT 100 ns decision window requires hardware default pass-through (74LVC125), which Design 2 already includes.
Final Component List for Design 2 (Full Memory Emulation)¶
| Chip | Qty | Function | Approx Cost |
|---|---|---|---|
| 74LVC574 | 3 | Address input latches (24-bit, /BAPR clocked) | $1.50 |
| 74LVC574 | 2 | Data input latches (16-bit, /BIOXE+/BDRY+/BDAP clocked) | $1.00 |
| 74LVC574 | 3 | Output latches (24-bit, PIO clocked) | $1.50 |
| 74LVT245 | 3 | Output drivers (3.3V -> 5V, /BD_OE_BUS controlled) | $2.40 |
| 74LVC14 | 1 | Schmitt inverters for clock conditioning | $0.30 |
| 74LVC07 | 1 | Open-drain drivers (/BREQ, /BINT, /BDRY out, /BINPUT out, /BDAP out) | $0.30 |
| 74LVC02 or 74LVC10 | 1 | NOR gate for data latch clock combining | $0.30 |
| 74LVC125 | 1 | Daisy-chain pass-through buffer | $0.30 |
| Total | 15 | ~$7.60 |
Plus pull resistors (~16 x 10K, ~$0.50 in arrays), bypass caps (~15 x 0.1uF, ~$0.50), connectors, PCB.
BD interface chip count: 15 (modest) Total BD interface cost: ~$8 (plus passives)
Firmware Architecture Reference¶
This section describes the firmware architecture in detail: PIO state machines, central registers, DMA, CPU-side handlers, and the bus drive API. This is the definitive firmware design for the controller card.
PIO Capture Speed Analysis¶
The PIO can capture bus state into its FIFO within ~40 ns, well inside the 50 ns BAPR address hold window. This means external hardware latches are still useful (Design 2 architecture), but PIO-based capture is also viable as a software pattern. The actual hardware uses Design 2 (8-bit latched), but the PIO-based timing analysis below shows the firmware can react to bus events in microsecond timeframes.
Why PIO Can Replace Hardware Latches (Theoretical)¶
PIO Latency Analysis¶
The RP2350 PIO runs at the system clock (up to 150 MHz on RP2350). One PIO cycle = ~6.67 ns.
A PIO program to capture BD on /BAPR LOW:
.wrap_target
wait 0 pin BAPR_PIN ; wait until /BAPR goes LOW (1 cycle when triggered)
in pins, 24 ; read 24 BD GPIOs into ISR (1 cycle)
push ; push to RX FIFO (1 cycle, or auto-push)
wait 1 pin BAPR_PIN ; wait for /BAPR HIGH (release for next cycle)
.wrap
Timing breakdown:
| Step | Time | Notes |
|---|---|---|
| Bus /BAPR asserted | 0 ns | CPU drives /BAPR LOW on bus |
| Level shifter delay (74LVC245) | 3-6 ns | 5V to 3.3V translation |
| GPIO synchronizer | ~13 ns | 2 PIO cycles for input synchronization |
wait 0 pin triggers |
~7 ns | 1 PIO cycle to detect |
in pins, 24 |
~7 ns | 1 PIO cycle to read 24 bits |
push to FIFO |
~7 ns | 1 PIO cycle |
| Total time to FIFO | ~37-40 ns | Within 50 ns BAPR window ✓ |
The critical path: bus edge -> level shifter -> GPIO sync -> PIO read -> FIFO completes well within the 50 ns address hold time. The 24 BD lines are captured BEFORE the CPU releases the address.
Validation: This works because the RP2350 input synchronizers and PIO together react in ~3-4 cycles. At 150 MHz that's ~20-27 ns. Well below 50 ns.
Why FIFO Acts as Latch¶
Once the data is in the PIO RX FIFO, it stays there until the CPU reads it. The bus can change, the CPU can be busy doing other things -- the captured 24-bit value is safely held in the FIFO. This is functionally identical to a hardware latch.
The PIO RX FIFO is 4 words deep (or 8 deep when joined). Plenty for buffering multiple bus events.
PIO Trigger Signal Mapping¶
In Design 4, the PIO acts as the latch -- but trigger signals still need to reach the PIO state machines so they know when to read the BD lines. There are no external data latches; instead, PIO state machines wait on the trigger signals and read BD GPIOs directly.
Each PIO state machine watches one trigger signal and reads 24 BD bits when the trigger asserts:
| PIO SM | Trigger Signal | Trigger Direction | Action | FIFO Tag |
|---|---|---|---|---|
| PIO0.SM0 -- ADDR_CAPTURE | /BAPR | Falling edge (asserted LOW) | Read 24 BD bits, push to RX FIFO | "ADDR" event |
| PIO0.SM1 -- IOX_DATA | /BIOXE | Falling edge | Read 24 BD bits, push to RX FIFO | "IOX_DATA" event |
| PIO0.SM2 -- MEM_DATA | /BDAP | Falling edge | Read 16 BD bits, push to RX FIFO | "MEM_DATA" event |
| PIO0.SM3 -- DMA_READ | /BDRY (incoming) | Falling edge during our DMA | Read 16 BD bits, push to RX FIFO | "DMA_DATA" event |
Important: All 4 PIO state machines share the same input pin set (BD 0-23). Multiple PIO SMs can read the same GPIO pins simultaneously without conflict because they're all reading, not driving. Each SM has its own wait condition and its own FIFO.
Why Separate State Machines per Trigger¶
A PIO wait instruction can only watch one pin (or a Y register condition) at a time. We have three different events that can trigger a data read:
- /BIOXE asserted: CPU IOX cycle, capture data on the bus
- /BDAP asserted: CPU memory write or our DMA write phase, capture data
- /BDRY asserted (incoming): Memory responding to our DMA read, capture data
Each event requires a different PIO state machine because each wait is single-pin. The RP2350B has 12 PIO state machines (4 per PIO block x 3 blocks), so dedicating 4 to bus capture is fine.
PIO Program Examples¶
SM0 -- Address Capture:
.program addr_capture
.wrap_target
wait 0 pin BAPR_PIN ; wait for /BAPR LOW
in pins, 24 ; read 24 BD bits
push ; push to FIFO (CPU drains)
wait 1 pin BAPR_PIN ; wait for /BAPR HIGH (release)
.wrap
SM1 -- IOX Data Capture:
.program iox_data_capture
.wrap_target
wait 0 pin BIOXE_PIN ; wait for /BIOXE LOW
in pins, 24 ; read 24 BD bits (data driven by CPU on IOX write)
push ; push to FIFO
wait 1 pin BIOXE_PIN ; wait for /BIOXE HIGH
.wrap
SM2 -- Memory Data Capture:
.program mem_data_capture
.wrap_target
wait 0 pin BDAP_PIN ; wait for /BDAP LOW
in pins, 16 ; read lower 16 BD bits (data is 16-bit on memory cycles)
push ; push to FIFO
wait 1 pin BDAP_PIN ; wait for /BDAP HIGH
.wrap
SM3 -- DMA Read Data Capture:
.program dma_read_capture
.wrap_target
wait 0 pin BDRY_PIN ; wait for /BDRY LOW (memory responding)
in pins, 16 ; read lower 16 BD bits
push ; push to FIFO
wait 1 pin BDRY_PIN ; wait for /BDRY HIGH
.wrap
Note on SM3: This SM should only be ARMED when our card has initiated a DMA read cycle. Otherwise it would trigger on every /BDRY assertion (including ones from CPU IOX cycles). The CPU enables/disables this SM as needed.
Trigger Signals as GPIO Inputs¶
The trigger signals are normal RP2350 GPIO inputs (after level shifting through 74LVC14). They are not connected to any external latch -- the PIO directly senses them and reacts.
| Signal | RP2350 GPIO | Buffer | Used by PIO SM |
|---|---|---|---|
| /BAPR | GP26 | 74LVC14 | PIO0.SM0 (ADDR_CAPTURE) |
| /BIOXE | GP27 | 74LVC14 | PIO0.SM1 (IOX_DATA) |
| /BDAP | GP31 | 74LVC14 | PIO0.SM2 (MEM_DATA) |
| /BDRY | GP30 | 74LVC14 | PIO0.SM3 (DMA_READ) |
These pins are also readable by software via normal GPIO read for state checks (e.g., the bus protocol handler can check /BIOXE state directly).
Event Tagging¶
Since multiple SMs push to separate FIFOs, the CPU drains each FIFO and knows which kind of event it is by which FIFO it came from. No tag bits needed.
If using a single FIFO (joined), the SM would need to push a tag along with the data:
in pins, 24
set y, 1 ; tag = 1 for "address"
in y, 8 ; shift tag into ISR
push ; push 32-bit value: [tag][data]
But with 4 separate FIFOs, this is unnecessary.
Unified Single-State-Machine Architecture (Recommended)¶
A more elegant approach uses one state machine that handles BOTH address and data capture, and uses bit 24 (or bit 31) to tag whether each FIFO entry is an address or data word. The CPU consumer (or DMA) reads from a single FIFO and dispatches based on the tag bit.
This works because address capture (/BAPR) and data capture (/BIOXE, /BDAP, /BDRY) are sequential within a bus cycle:
- /BAPR asserts first (address phase)
- /BIOXE / /BDAP / /BDRY asserts later (data phase)
A single PIO state machine can wait for /BAPR, capture address, then wait for the data trigger, capture data, push both -- all sequentially.
Tag Bit Convention¶
Use bit 24 (or bit 31 for 32-bit alignment) to distinguish:
| Tag bit | Meaning | Lower bits |
|---|---|---|
0 |
Address | 24-bit BD address value |
1 |
Data | 24-bit BD data value (or 16-bit padded) |
CPU side dispatches:
uint32_t fifo_word = pio_sm_get(pio, sm);
if (fifo_word & (1u << 24)) {
handle_data(fifo_word & 0x00FFFFFF);
} else {
handle_address(fifo_word & 0x00FFFFFF);
}
Single-SM PIO Program (Unified Capture)¶
.program bus_capture
.wrap_target
; --- Address phase ---
wait 0 pin BAPR_PIN ; wait for /BAPR LOW
in pins, 24 ; read 24 BD bits (address)
set y, 0 ; tag = 0 (address)
in y, 8 ; shift tag into ISR -> bit 24-31
push ; push to FIFO: [00000000][address]
wait 1 pin BAPR_PIN ; wait for /BAPR HIGH
; --- Data phase ---
; The next strobe could be /BIOXE, /BDAP, or /BDRY
; We use IRQ-driven dispatch via a helper state machine
irq wait 4 ; wait for IRQ4 (raised by helper SM)
in pins, 24 ; read 24 BD bits (data)
set y, 1 ; tag = 1 (data)
in y, 8 ; shift tag into ISR
push ; push to FIFO: [00000001][data]
.wrap
Helper State Machine for Multi-Trigger Detection¶
Since PIO wait can only watch one pin, we need a helper SM that watches all three data trigger signals (/BIOXE, /BDAP, /BDRY) and raises an IRQ to wake the main SM:
.program data_trigger
.wrap_target
mov osr, ~null ; load 0xFFFFFFFF
out pins, 1 ; configure pin direction (one-time setup)
; Watch /BIOXE
jmp pin_low_check
pin_low_check:
in pins, 3 ; read /BIOXE, /BDAP, /BDRY (3 pins, contiguous)
mov x, isr ; save to X
jmp x-- not_zero ; if all 3 pins HIGH (X = 7), no trigger
irq set 4 ; one of the pins is LOW, signal main SM
not_zero:
.wrap
Or more simply, run 3 helper SMs in parallel, each watching one signal:
.program iox_trigger
.wrap_target
wait 0 pin BIOXE_PIN
irq set 4 ; signal main SM
wait 1 pin BIOXE_PIN
.wrap
.program bdap_trigger
.wrap_target
wait 0 pin BDAP_PIN
irq set 4 ; same IRQ
wait 1 pin BDAP_PIN
.wrap
.program bdry_trigger
.wrap_target
wait 0 pin BDRY_PIN
irq set 4 ; same IRQ
wait 1 pin BDRY_PIN
.wrap
All three trigger SMs raise the same IRQ4. The main capture SM waits for IRQ4 and reads data when any trigger fires.
Updated Architecture (Unified Capture with IRQ Dispatch)¶
| PIO SM | Role | Watches | Raises |
|---|---|---|---|
| PIO0.SM0 -- BUS_CAPTURE | Main capture (address + data, push with tag bit) | /BAPR direct, IRQ4 for data | Pushes to RX FIFO |
| PIO0.SM1 -- IOX_TRIGGER | Helper -- detect /BIOXE assertion | /BIOXE | IRQ4 |
| PIO0.SM2 -- BDAP_TRIGGER | Helper -- detect /BDAP assertion | /BDAP | IRQ4 |
| PIO0.SM3 -- BDRY_TRIGGER | Helper -- detect /BDRY assertion (DMA only) | /BDRY | IRQ4 |
Only one FIFO needs to be drained by the CPU, simplifying the consumer logic. The tag bit indicates whether the entry is an address or data event.
Why Tag the Data on the PIO Side¶
Tagging on the PIO side (rather than tracking state in CPU code) has key advantages:
- Atomic event ordering: The FIFO order is the bus cycle order. No race conditions between separate FIFOs.
- Simpler CPU code: Single dispatch loop, no FIFO arbitration.
- DMA-friendly: The CPU can configure a hardware DMA to drain the single FIFO into a circular buffer in RAM. The DMA sees a continuous stream of tagged events.
- Lower latency: No CPU intervention needed to read multiple FIFOs in sequence.
DMA-Driven FIFO Drain¶
Instead of CPU polling, configure a hardware DMA channel to continuously transfer from the PIO RX FIFO to a circular buffer in SRAM/PSRAM:
PIO0.SM0 RX FIFO --[DMA]--> Circular buffer in RAM
^
|
CPU drains buffer
and dispatches based
on tag bit
The DMA fires automatically whenever the FIFO has data, without any CPU intervention. The CPU processes events from the buffer at its own pace, decoupled from the bus timing.
Detailed Tag Bit Format¶
Using bit 24 leaves bits 25-31 for additional metadata:
| Bits | Content | Notes |
|---|---|---|
| 0-23 | BD bus value | 24-bit address or data |
| 24 | Tag: 0 = address, 1 = data | Set by PIO SM |
| 25 | Trigger source bit 0 | (optional) which signal triggered |
| 26 | Trigger source bit 1 | (optional) |
| 27-31 | Reserved | Future use |
If we want to know which trigger caused the data event (BIOXE vs BDAP vs BDRY), encode 2 bits in positions 25-26:
| Bit 26 | Bit 25 | Trigger |
|---|---|---|
| 0 | 0 | /BAPR (address) |
| 0 | 1 | /BIOXE (IOX data) |
| 1 | 0 | /BDAP (memory data) |
| 1 | 1 | /BDRY (DMA data) |
Each helper SM would need to set a different value before raising the IRQ. This costs more PIO instructions but fully identifies the bus cycle type from the FIFO entry alone.
Output Latch + Output Enable in One Chip¶
For driving the bus, the ideal chip combines: 1. Latch (D flip-flop or transparent latch) -- holds 8 bits stable 2. 3-state output with output enable -- chip can be tristated until commanded
Several chip families provide this in a single package:
Recommended Output Latch+OE Chips¶
| Chip | Type | Trigger | OE | Speed | Notes |
|---|---|---|---|---|---|
| 74LVC574 | Octal D flip-flop | Edge (rising CLK) | /OE pin | 3-7 ns | Most common, recommended |
| 74LVC573 | Octal transparent latch | Level (LE high = transparent) | /OE pin | 3-7 ns | Latches on LE falling edge |
| 74LVT574 | Octal D flip-flop, low voltage | Edge | /OE pin | 2-5 ns | Faster, higher drive |
| 74LVT373 | Octal transparent latch, LV | Level | /OE pin | 2-5 ns | Faster than 573 |
| 74AHC574 | Octal D flip-flop, advanced HC | Edge | /OE pin | 4-8 ns | 5V tolerant |
| 74LVC16374 | 16-bit D flip-flop | Edge | 2x /OE pins | 3-7 ns | One chip for 16 bits |
| 74LVC16374A | 16-bit version (faster) | Edge | 2x /OE pins | 2-5 ns | Saves PCB space |
How They Work (74LVC574 Example)¶
The 74LVC574 has:
| Pin | Function |
|---|---|
| D0-D7 | 8 data inputs (from MCU) |
| Q0-Q7 | 8 data outputs (to bus) |
| CLK | Clock input -- captures D on rising edge |
| /OE | Output enable -- HIGH = high-Z, LOW = drive Q outputs |
Sequence to drive 24 bits to the bus:
Start: All /OE pins HIGH (outputs high-Z, bus released)
1. PIO drives shared 8-bit bus = byte 0
2. PIO pulses CLK on chip 1 (rising edge captures byte 0)
3. PIO drives shared 8-bit bus = byte 1
4. PIO pulses CLK on chip 2 (captures byte 1)
5. PIO drives shared 8-bit bus = byte 2
6. PIO pulses CLK on chip 3 (captures byte 2)
Now all three chips hold the desired values, but outputs are still high-Z.
7. PIO drives /OE LOW on all three chips simultaneously
(single GPIO connected to all three /OE pins)
8. All 24 bits appear on the bus instantly (~3-7 ns propagation)
Bus is now driven with the prepared 24-bit value.
When done:
9. PIO drives /OE HIGH on all three chips
Outputs go high-Z within ~3-7 ns
Bus is released
Key feature: The data is loaded into the latches before the bus is driven. The bus only sees the value when /OE is asserted, not during the loading phase. This avoids glitches and bus contention.
74LVC574 Direct to Bus (No Separate Transceiver)¶
The 74LVC574 is a 3.3V CMOS chip. To drive a 5V bus directly, we need:
| Approach | Chips | Notes |
|---|---|---|
| 74LVC574 + 74LVT245 transceiver | 6 chips | Latch + level shifter, 2-stage |
| 74LVC574 alone (if 3.3V output is enough) | 3 chips | Works ONLY if bus accepts 3.3V signals as logic HIGH |
| 74AHC574 alone | 3 chips | 5V supply with 3.3V tolerant inputs from MCU side |
| 74LVC16374 | 2 chips | 16-bit version, 3 chips for 24 bits but bigger pads |
Critical detail: The ND-100 bus uses 5V TTL. A 3.3V signal may be interpreted as HIGH by the receiving 5V logic if the threshold is below 2.0V. Standard TTL VIH is 2.0V, so a 3.3V output is well above this and should work. However, this is borderline -- some 5V parts have higher VIH or are sensitive to noise.
Recommendation: For maximum reliability, use the 2-stage approach: - 74LVC574 as latch (3.3V CMOS, accepts MCU outputs) - 74LVT245 as output transceiver (3.3V to 5V, drives the bus with proper levels)
This guarantees correct 5V signaling and provides higher drive strength.
Alternative single-chip: 74AHC574 runs from 5V and accepts 3.3V inputs. It provides 5V CMOS output levels directly to the bus. Saves chips but verify the 5V variant supports 3.3V input thresholds.
Updated Output Stage for Design 2¶
| Option | Chips | Complexity | 5V output reliability |
|---|---|---|---|
| 74LVC574 + 74LVT245 (2-stage) | 6 chips | Medium | Best -- proper 5V levels |
| 74AHC574 (single stage, 5V supply) | 3 chips | Simple | Good -- verify VIH input compatibility |
| 74LVC574 alone (3.3V output) | 3 chips | Simple | Marginal -- works but borderline TTL HIGH |
| 74LVC16374 + level shifter | 2+2 chips | Medium | Best -- 16-bit per chip |
Recommendation for Design 2: 74LVC574 + 74LVT245 for guaranteed 5V signaling and higher drive strength. Total 6 chips for the output path (already counted in Design 2 chip list).
Why This is Important¶
The user's question highlights a key design pattern: load the latches first, then enable the output. This is called "deferred drive" and provides several benefits:
- No glitches: Bus sees only the final value, not intermediate bytes during loading
- Atomic update: All 24 bits appear simultaneously when /OE asserts
- Easy timing: PIO can take its time loading; the bus only sees the result
- Clean release: /OE deassert tristates the output cleanly
This pattern is essential for any bus interface with multi-byte loading.
Central Bus Drive API -- Single Source of Truth¶
Critical safety section. All BD bus driving and bus control signal manipulation MUST go through this centralized API. Direct GPIO writes or PIO commands for these signals are forbidden in handler code.
This rule prevents bus contention which can damage the CPU and our 74LVT245 drivers. By having a single code path for bus driving, we can audit and verify the rule in one place.
The Single Rule¶
We can drive BD 0-23 ONLY in these five states. In any other state, BD MUST be high-Z (/BD_OE_BUS HIGH).
| # | State | Start condition | End condition | What we drive |
|---|---|---|---|---|
| 1 | BD_DRIVE_IOX_READ |
CPU asserts /BINACK (after we asserted /BINPUT in response to /BIOXE) |
CPU releases /BIOXE |
Read register data |
| 2 | BD_DRIVE_IDENT |
/INIDENT active AND our active interrupt mask matches level on BD 0-5 |
CPU releases /INIDENT |
Ident code |
| 3 | BD_DRIVE_MEM_READ_RESPONSE |
CPU asserts /BDAP during memory read cycle to our memory range |
CPU releases /BMEM |
Memory read data |
| 4 | BD_DRIVE_DMA_ADDRESS |
We captured /INGRANT after asserting /BREQ |
~50 ns after we assert /BAPR |
Memory address |
| 5 | BD_DRIVE_DMA_WRITE_DATA |
DMA write cycle, address phase complete, /BINPUT LOW |
Memory asserts /BDRY |
Write data |
We NEVER drive BD when:
- /BAPR is asserted by the CPU (CPU is driving the address)
- /BIOXE is active during a write to us (CPU drives the write data)
- /BDAP is active during a CPU memory read (memory drives the data)
- IDENT cycle but our level doesn't match
- DMA read data phase (memory drives, not us)
- We have not been explicitly granted by one of the 5 conditions
- Power-up, reset, or any unknown state (default = high-Z)
Central Module Layout¶
controller_bus.h // Public API for handlers
controller_bus.c // Implementation, state machine, PIO commands
bus_handlers.c // IOX, IDENT, memory, DMA handlers (USE the API)
device_emulators.c // Floppy, SMD, terminal, HDLC (USE the API)
(handlers NEVER touch GPIO or PIO directly for bus signals)
Public API (controller_bus.h)¶
#ifndef CONTROLLER_BUS_H
#define CONTROLLER_BUS_H
#include <stdint.h>
#include <stdbool.h>
// === BD Bus Drive State (single source of truth) ===
typedef enum {
BD_DRIVE_NONE = 0, // Default - high-Z, NOT driving
BD_DRIVE_IOX_READ, // State 1
BD_DRIVE_IDENT, // State 2
BD_DRIVE_MEM_READ_RESPONSE, // State 3
BD_DRIVE_DMA_ADDRESS, // State 4
BD_DRIVE_DMA_WRITE_DATA, // State 5
} bd_drive_state_t;
// === Central Registers (DMA-updated, read-only for handlers) ===
extern volatile uint32_t bus_address_latest; // Last captured address from /BAPR
extern volatile uint32_t bus_data_latest; // Last captured data from /BIOXE/BDAP/BDRY
extern volatile uint32_t bus_event_seq; // Increments on each new event
// === BD Bus Drive Control (the ONLY way to drive BD) ===
// Acquire the BD bus and drive a 24-bit value.
// Returns true if acquired successfully, false if already held.
// Asserts /BD_OE_BUS LOW after the data is valid on the GPIOs.
bool bd_drive_begin(bd_drive_state_t reason, uint32_t value);
// Update the BD value while holding the bus.
// Must be called between bd_drive_begin and bd_drive_end.
void bd_drive_update(uint32_t value);
// Release the BD bus.
// De-asserts /BD_OE_BUS HIGH (high-Z) and clears state.
void bd_drive_end(void);
// Query current state (for debugging/asserts)
bd_drive_state_t bd_drive_get_state(void);
// === Control Signal API (centralized open-drain assertions) ===
// Bus Data Ready (we drive when responding to IOX/IDENT/MEM-read or DMA-write completion)
void bus_assert_bdry(void);
void bus_release_bdry(void);
// Bus Input (we assert during IOX read response or DMA write)
void bus_assert_binput(void);
void bus_release_binput(void);
// Bus Data Present (we assert during DMA cycles)
void bus_assert_bdap(void);
void bus_release_bdap(void);
// Bus Memory cycle (we assert during DMA cycles when accessing memory)
void bus_assert_bmem(void);
void bus_release_bmem(void);
// Bus Address Present (we pulse during DMA cycles)
void bus_pulse_bapr(void); // Asserts ~50ns then releases
// Bus Request (we assert to request DMA grant)
void bus_assert_breq(void);
void bus_release_breq(void);
// === Daisy-Chain Pass-Through Control ===
void daisy_ident_passthrough_enable(void); // Allow INIDENT -> OUTIDENT
void daisy_ident_passthrough_block(void); // Block forwarding (we're capturing)
void daisy_grant_passthrough_enable(void);
void daisy_grant_passthrough_block(void);
// === Interrupt Output Control ===
void int_assert(int level); // 10, 11, 12, or 13
void int_release(int level);
uint8_t int_get_active_mask(void); // Returns bits set for active levels
// === Bus Phase Tracking ===
typedef enum {
PHASE_IDLE = 0,
PHASE_IOX_READ_RESPOND,
PHASE_IOX_WRITE_CAPTURE,
PHASE_IDENT_RESPOND,
PHASE_MEM_READ_RESPOND,
PHASE_MEM_WRITE_CAPTURE,
PHASE_DMA_REQUEST,
PHASE_DMA_ADDR,
PHASE_DMA_READ_WAIT,
PHASE_DMA_WRITE_DRIVE,
} bus_phase_t;
extern volatile bus_phase_t bus_phase;
#endif // CONTROLLER_BUS_H
Implementation Sketch (controller_bus.c)¶
#include "controller_bus.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include <assert.h>
// State variables
volatile bd_drive_state_t bd_drive_state = BD_DRIVE_NONE;
volatile bus_phase_t bus_phase = PHASE_IDLE;
volatile uint32_t bus_address_latest = 0;
volatile uint32_t bus_data_latest = 0;
volatile uint32_t bus_event_seq = 0;
// Internal interrupt mask
static uint8_t int_active_mask = 0;
// === BD Drive Implementation ===
bool bd_drive_begin(bd_drive_state_t reason, uint32_t value) {
// Enforce: only one driver at a time
if (bd_drive_state != BD_DRIVE_NONE) {
// Already driving -- this is a bug, log it
log_error("bd_drive_begin called while state = %d", bd_drive_state);
return false;
}
// Enforce: reason must be valid
assert(reason != BD_DRIVE_NONE);
// Set state
bd_drive_state = reason;
// Drive value via PIO output SM
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE_CMD(value));
// /BD_OE_BUS asserted LOW by PIO state machine on the same command
return true;
}
void bd_drive_update(uint32_t value) {
assert(bd_drive_state != BD_DRIVE_NONE);
pio_sm_put(pio1, SM_BD_DRIVE, BD_UPDATE_CMD(value));
}
void bd_drive_end(void) {
if (bd_drive_state == BD_DRIVE_NONE) {
// Not driving -- nothing to do
return;
}
// Release BD bus
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE_CMD);
// Clear state
bd_drive_state = BD_DRIVE_NONE;
}
bd_drive_state_t bd_drive_get_state(void) {
return bd_drive_state;
}
// === Control Signal Implementation ===
void bus_assert_bdry(void) { gpio_put(BDRY_DRIVE_PIN, 0); } // Open-drain LOW
void bus_release_bdry(void) { gpio_put(BDRY_DRIVE_PIN, 1); } // Released HIGH
void bus_assert_binput(void) { gpio_put(BINPUT_DRIVE_PIN, 0); }
void bus_release_binput(void) { gpio_put(BINPUT_DRIVE_PIN, 1); }
void bus_assert_bdap(void) { gpio_put(BDAP_DRIVE_PIN, 0); }
void bus_release_bdap(void) { gpio_put(BDAP_DRIVE_PIN, 1); }
void bus_assert_bmem(void) { gpio_put(BMEM_DRIVE_PIN, 0); }
void bus_release_bmem(void) { gpio_put(BMEM_DRIVE_PIN, 1); }
void bus_assert_breq(void) { gpio_put(BREQ_DRIVE_PIN, 0); }
void bus_release_breq(void) { gpio_put(BREQ_DRIVE_PIN, 1); }
void bus_pulse_bapr(void) {
// Atomic pulse via PIO output SM (50 ns hold guaranteed)
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_PULSE_BAPR);
}
// === Daisy-Chain Implementation ===
void daisy_ident_passthrough_enable(void) { gpio_put(OE_DAISY_IDENT_PIN, 0); }
void daisy_ident_passthrough_block(void) { gpio_put(OE_DAISY_IDENT_PIN, 1); }
void daisy_grant_passthrough_enable(void) { gpio_put(OE_DAISY_GRANT_PIN, 0); }
void daisy_grant_passthrough_block(void) { gpio_put(OE_DAISY_GRANT_PIN, 1); }
// === Interrupt Output Implementation ===
void int_assert(int level) {
assert(level >= 10 && level <= 13);
int_active_mask |= (1 << (level - 10));
update_int_outputs();
}
void int_release(int level) {
assert(level >= 10 && level <= 13);
int_active_mask &= ~(1 << (level - 10));
update_int_outputs();
}
uint8_t int_get_active_mask(void) {
return int_active_mask;
}
static void update_int_outputs(void) {
// Drive 4 GPIOs (BINT 10/11/12/13) via 74LVC07 open-drain
// Set LOW = assert, HIGH = release
gpio_put(BINT10_DRIVE_PIN, !(int_active_mask & 0x01));
gpio_put(BINT11_DRIVE_PIN, !(int_active_mask & 0x02));
gpio_put(BINT12_DRIVE_PIN, !(int_active_mask & 0x04));
gpio_put(BINT13_DRIVE_PIN, !(int_active_mask & 0x08));
}
Handler Code MUST Use the API¶
Every bus cycle handler must use controller_bus.h and never touch GPIOs/PIO directly for bus signals.
WRONG (bypasses the central API):
void bad_iox_read_handler(uint32_t addr, uint32_t data) {
// DON'T DO THIS -- bypasses safety checks
gpio_put(BD_OE_BUS_PIN, 0);
pio_sm_put(pio1, SM_BD_DRIVE, data);
gpio_put(BDRY_DRIVE_PIN, 0);
// ...
}
RIGHT (uses the API):
void handle_iox_read(uint32_t addr) {
int reg = decode_register(addr);
if (reg < 0) return; // Not for us
uint32_t data = read_register(reg);
// Wait for /BIOXE
while (gpio_get(BIOXE_PIN)) tight_loop_contents();
// Assert BINPUT (we are responding to a read)
bus_assert_binput();
// Wait for BINACK
while (gpio_get(BINACK_PIN)) tight_loop_contents();
// Now we can drive BD (state 1: BD_DRIVE_IOX_READ)
if (!bd_drive_begin(BD_DRIVE_IOX_READ, data)) {
log_error("bd_drive busy during IOX read");
return;
}
// Assert BDRY
bus_assert_bdry();
// Wait for CPU to release /BIOXE
while (!gpio_get(BIOXE_PIN)) tight_loop_contents();
// Release everything via API
bd_drive_end();
bus_release_bdry();
bus_release_binput();
}
Why This Matters¶
| Risk | Without API | With API |
|---|---|---|
| Bus contention damaging CPU | High -- handlers can fight | Eliminated -- single drive state machine |
| Forgetting to release BD | Easy -- state lost | Caught by bd_drive_get_state() audit |
| Mixed up direction | Easy -- wrong PIO command | Single function with explicit reason |
| Multi-device contention | High -- floppy and SMD can race | Mediated through shared state |
| Power-up state | Undefined | API initializes to BD_DRIVE_NONE |
| Debugging | Hard -- spread across files | Easy -- single state variable |
Audit Checklist¶
Before merging any handler code:
- [ ] Does the handler call bd_drive_begin() with correct reason for the cycle type?
- [ ] Does the handler call bd_drive_end() exactly once for every bd_drive_begin()?
- [ ] Does the handler use bus_assert_* / bus_release_* for control signals (no direct GPIO)?
- [ ] Does the handler check bd_drive_state == BD_DRIVE_NONE before claiming the bus?
- [ ] Are all error paths (return early) accompanied by cleanup?
Add a runtime watchdog: if bd_drive_state != BD_DRIVE_NONE for more than 5 us, force release and log an error.
void bus_drive_watchdog(void) {
static uint64_t drive_start_us = 0;
if (bd_drive_state != BD_DRIVE_NONE) {
if (drive_start_us == 0) {
drive_start_us = time_us_64();
} else if (time_us_64() - drive_start_us > 5) {
log_error("BD drive watchdog: state %d held > 5us, forcing release", bd_drive_state);
bd_drive_end();
drive_start_us = 0;
}
} else {
drive_start_us = 0;
}
}
This runs from a 1 us timer interrupt or in the main loop.
State Machine Architecture (Complete)¶
The full design uses PIO state machines for low-level bus operations and CPU-side state machines (running on Cortex-M33 cores) for high-level bus cycle protocol logic.
PIO State Machines (Hardware Layer)¶
PIO0 -- Input/Capture (already designed above):
| SM | Role | Input |
|---|---|---|
| PIO0.SM0 | BUS_CAPTURE -- main capture | /BAPR + IRQ4 |
| PIO0.SM1 | IOX_TRIGGER helper | /BIOXE -> IRQ4 |
| PIO0.SM2 | BDAP_TRIGGER helper | /BDAP -> IRQ4 |
| PIO0.SM3 | BDRY_TRIGGER helper | /BDRY -> IRQ4 |
Output: single tagged-event RX FIFO (drained by DMA into circular buffer in PSRAM).
PIO1 -- Output/Drive:
| SM | Role | Output |
|---|---|---|
| PIO1.SM0 | BD_DRIVE -- drives 24 BD lines | BD 0-23 + /BD_OE |
| PIO1.SM1 | CTRL_DRIVE -- drives bus control signals | /BAPR, /BDRY, /BDAP, /BINPUT, /BMEM, /BREQ |
| PIO1.SM2 | INT_DRIVE -- drives interrupt lines | /BINT 10, /BINT 11, /BINT 12 |
| PIO1.SM3 | DAISY_CTRL -- controls daisy-chain bypass | /OE_DAISY_PASS |
Each output SM has its own TX FIFO. The CPU pushes commands to the appropriate FIFO.
BD_DRIVE State Machine (PIO1.SM0)¶
This SM is always running, waiting for commands from its TX FIFO. Each command tells it to drive specific data or release the bus.
Command word format (32 bits):
| Bits | Field | Meaning |
|---|---|---|
| 0-23 | DATA | 24-bit value to drive on BD |
| 24 | DRIVE | 1 = drive bus, 0 = release (high-Z) |
| 25-31 | Reserved | Future use |
Behavior: 1. Pull 32-bit command from TX FIFO (blocks if empty) 2. If DRIVE bit set: a. Set BD pindirs to OUTPUT (24 bits) b. Write DATA to BD pins c. Assert /BD_OE LOW (via 74LVT245 OE) 3. If DRIVE bit clear: a. De-assert /BD_OE HIGH (74LVT245 outputs go high-Z) b. Set BD pindirs to INPUT 4. Loop back to wait for next command
The CPU pushes "drive 0x123456" then later pushes "release" (drive bit = 0). The bus is held in the driven state between commands.
Pseudocode (PIO assembly conceptual):
loop:
pull ; wait for command
out x, 1 ; extract DRIVE bit
jmp !x release
out pins, 24 ; write DATA to BD
set sideset, 0 ; /BD_OE LOW
jmp loop
release:
set sideset, 1 ; /BD_OE HIGH
set pindirs, 0 ; BD as input
jmp loop
CTRL_DRIVE State Machine (PIO1.SM1)¶
Drives bus control signals individually via bitmask commands.
Command word format (32 bits):
| Bits | Field | Meaning |
|---|---|---|
| 0-7 | SET_MASK | Signals to assert (1 = drive LOW) |
| 8-15 | CLR_MASK | Signals to release (1 = let go HIGH) |
| 16-23 | Reserved |
Signal bit assignments (within SET_MASK and CLR_MASK):
| Bit | Signal |
|---|---|
| 0 | /BAPR |
| 1 | /BMEM |
| 2 | /BDAP |
| 3 | /BDRY |
| 4 | /BINPUT |
| 5 | /BREQ |
| 6 | /BINACK (if we ever drive it -- normally CPU output) |
| 7 | Reserved |
Behavior: 1. Pull command word 2. For each bit in SET_MASK: drive corresponding signal LOW (assert) 3. For each bit in CLR_MASK: release corresponding signal (let pull-up bring HIGH) 4. Loop
This allows atomic multi-signal updates: e.g., assert /BAPR + /BMEM + /BINPUT in one command.
INT_DRIVE State Machine (PIO1.SM2)¶
Drives interrupt outputs based on level mask.
Command word format:
| Bits | Field | Meaning |
|---|---|---|
| 0 | /BINT 10 enable | 1 = assert (LOW), 0 = release |
| 1 | /BINT 11 enable | |
| 2 | /BINT 12 enable | |
| 3 | /BINT 13 enable | (if used in future) |
| 4-31 | Reserved |
Behavior: Pull command, write 4 bits to INT pins, loop.
This allows the CPU to atomically assert/release multiple interrupt levels.
DAISY_CTRL State Machine (PIO1.SM3)¶
Controls the daisy-chain pass-through buffer (74LVC125 /OE).
Command word format:
| Bits | Meaning |
|---|---|
| 0 | /OE_IDENT_PASS (1 = block pass-through, 0 = pass through) |
| 1 | /OE_GRANT_PASS (1 = block, 0 = pass through) |
When the controller wants to capture an INIDENT or INGRANT (because it has an interrupt or DMA request active), it pushes a command to block the pass-through. After the cycle, it pushes a command to re-enable pass-through.
CPU-Side Bus Cycle Handlers¶
The CPU runs state machines that consume events from the RX FIFO (filled by PIO0) and dispatch commands to the PIO1 output state machines.
These are software state machines, typically implemented as a main loop on Core 0 (bus protocol handler), with Core 1 running device emulation logic (floppy, SMD, terminal, HDLC).
Bus Cycle Identification¶
When an ADDR event arrives in the FIFO, the bus protocol handler reads the current state of /BMEM and /BINPUT (via direct GPIO read) to determine the cycle type:
| /BMEM | /BINPUT (at ADDR time) | Cycle Type |
|---|---|---|
| HIGH (inactive) | -- | IOX cycle (CPU programmed I/O) |
| LOW (active) | HIGH (read) | Memory READ cycle (CPU reading memory) |
| LOW (active) | LOW (write) | Memory WRITE cycle (CPU writing memory) |
For IDENT cycles, the handler also checks /INIDENT state (which is the trigger).
Handler 1: IOX Read (CPU reads from us)¶
Purpose: CPU executes IOXT instruction reading from one of our device registers.
State diagram:
stateDiagram-v2
[*] --> Idle
Idle --> AddrReceived: ADDR event<br/>(/BMEM HIGH = IOX)
AddrReceived --> NotOurAddr: address not ours
NotOurAddr --> Idle
AddrReceived --> WaitBIOXE: address matches
WaitBIOXE --> AssertBINPUT: /BIOXE detected
AssertBINPUT --> WaitBINACK: send drive /BINPUT
WaitBINACK --> DriveBD: /BINACK detected
DriveBD --> AssertBDRY: send drive BD=data
AssertBDRY --> WaitRelease: send drive /BDRY
WaitRelease --> ReleaseAll: /BIOXE released
ReleaseAll --> Idle: send release BD, /BINPUT, /BDRY
Pseudocode (CPU-side):
void handle_iox_read(uint32_t addr) {
if (!is_our_register(addr)) return;
// Wait for /BIOXE assertion (event from FIFO or GPIO poll)
wait_for_signal(BIOXE_PIN, LOW);
// Assert /BINPUT (signal "this is a read")
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BINPUT));
// Wait for /BINACK from CPU
wait_for_signal(BINACK_PIN, LOW);
// Drive data on BD
uint32_t data = read_emulated_register(addr);
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(data));
// Assert /BDRY
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDRY));
// Wait for CPU to release /BIOXE
wait_for_signal(BIOXE_PIN, HIGH);
// Release everything
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE);
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BINPUT | BDRY));
}
Handler 2: IOX Write (CPU writes to us)¶
Purpose: CPU executes IOX instruction writing to one of our device registers.
State diagram:
stateDiagram-v2
[*] --> Idle
Idle --> AddrReceived: ADDR event
AddrReceived --> WaitData: address matches
WaitData --> ProcessWrite: DATA event<br/>(captured on /BIOXE)
ProcessWrite --> AssertBDRY: store data in emulated register
AssertBDRY --> WaitRelease: send drive /BDRY
WaitRelease --> ReleaseAll: /BIOXE released
ReleaseAll --> Idle: send release /BDRY
Pseudocode:
void handle_iox_write(uint32_t addr) {
if (!is_our_register(addr)) return;
// Wait for DATA event (PIO captures BD on /BIOXE)
uint32_t data = wait_data_event();
// Store the write
write_emulated_register(addr, data);
// Assert /BDRY ("data accepted")
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDRY));
// Wait for CPU to release /BIOXE
wait_for_signal(BIOXE_PIN, HIGH);
// Release /BDRY
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BDRY));
}
Handler 3: IDENT (interrupt identification)¶
Purpose: CPU executes IDENT PLxx instruction. Card responds with ident code if it has interrupt active on the specified level.
State diagram:
stateDiagram-v2
[*] --> Idle
Idle --> AddrReceived: ADDR event<br/>(IDENT cycle = /INIDENT triggered)
AddrReceived --> CheckInterrupts: extract level from address
CheckInterrupts --> NotForUs: no active interrupt on this level
NotForUs --> Idle
CheckInterrupts --> CaptureChain: have active interrupt
CaptureChain --> DriveIdent: block daisy-chain pass-through
DriveIdent --> AssertBDRY: send drive BD=ident_code
AssertBDRY --> ResetInterrupt: send drive /BDRY
ResetInterrupt --> WaitRelease: clear interrupt enable bit
WaitRelease --> Cleanup: cycle complete
Cleanup --> Idle: re-enable pass-through, release BD/BDRY
Pseudocode:
void handle_ident(uint32_t addr) {
int level = extract_int_level(addr);
int device = find_device_with_interrupt(level);
if (device < 0) {
// Pass-through happens automatically via 74LVC125 hardware default
return;
}
// Block hardware pass-through
pio_sm_put(pio1, SM_DAISY_CTRL, DAISY_BLOCK_IDENT);
// Drive ident code on BD
uint32_t ident_code = devices[device].ident_code;
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(ident_code));
// Assert /BDRY
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDRY));
// Reset device interrupt enable bit (per ND-100 spec)
devices[device].int_enable = 0;
// Wait for cycle release
wait_for_signal(BAPR_PIN, HIGH);
// Cleanup: release everything
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE);
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BDRY));
pio_sm_put(pio1, SM_DAISY_CTRL, DAISY_PASS_IDENT);
}
Handler 4: Memory Read from CPU (CPU reads our emulated memory)¶
Purpose: CPU does a memory read at an address in our emulated memory range.
Pseudocode:
void handle_mem_read_from_cpu(uint32_t addr) {
if (!is_our_memory_range(addr)) return;
// Look up data in emulated memory (SRAM or PSRAM)
uint16_t data = emulated_memory[addr];
// Wait for CPU to assert /BDAP (signaling "BD free for memory data")
wait_for_signal(BDAP_PIN, LOW);
// Drive data on BD 0-15
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(data));
// Assert /BDRY (data valid)
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDRY));
// Wait for cycle release
wait_for_signal(BMEM_PIN, HIGH);
// Release
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE);
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BDRY));
}
Handler 5: Memory Write from CPU (CPU writes to our emulated memory)¶
Purpose: CPU does a memory write at an address in our emulated memory range.
Pseudocode:
void handle_mem_write_from_cpu(uint32_t addr) {
if (!is_our_memory_range(addr)) return;
// Wait for DATA event (captured by PIO on /BDAP)
uint16_t data = wait_data_event();
// Store in emulated memory
emulated_memory[addr] = data;
// Assert /BDRY ("data accepted")
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDRY));
// Wait for cycle release
wait_for_signal(BMEM_PIN, HIGH);
// Release /BDRY
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BDRY));
}
Handler 6: DMA Read from Real Memory (we initiate, read from CPU memory or another card)¶
Purpose: Our card needs to read data from real memory at a specific address.
Pseudocode:
uint16_t dma_read(uint32_t mem_addr) {
// Step 1: Request the bus
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BREQ));
// Step 2: Wait for /INGRANT (with our /BREQ active at /BMEM leading edge)
wait_for_signal(INGRANT_PIN, LOW);
pio_sm_put(pio1, SM_DAISY_CTRL, DAISY_BLOCK_GRANT);
// Step 3: Drive memory address on BD
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(mem_addr));
// Step 4: Assert /BAPR (address strobe)
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BAPR));
delay_ns(60);
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BAPR));
// Step 5: /BINPUT remains HIGH (= read direction)
// Step 6: Release BD lines (memory will drive)
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE);
// Step 7: Assert /BDAP ("BD free for memory data")
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDAP));
// Step 8: Wait for memory's /BDRY response (DMA_DATA event from PIO)
uint16_t data = wait_dma_read_event();
// Step 9: Release everything
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BDAP | BREQ));
pio_sm_put(pio1, SM_DAISY_CTRL, DAISY_PASS_GRANT);
return data;
}
Handler 7: DMA Write to Real Memory (we initiate, write to CPU memory or another card)¶
Purpose: Our card writes data to real memory at a specific address.
Pseudocode:
void dma_write(uint32_t mem_addr, uint16_t data) {
// Step 1: Request the bus
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BREQ));
// Step 2: Wait for /INGRANT
wait_for_signal(INGRANT_PIN, LOW);
pio_sm_put(pio1, SM_DAISY_CTRL, DAISY_BLOCK_GRANT);
// Step 3: Drive memory address + assert /BAPR + /BMEM + /BINPUT (write)
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(mem_addr));
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BAPR | BMEM | BINPUT));
delay_ns(60);
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BAPR));
// Step 4: Drive data on BD
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(data));
// Step 5: Assert /BDAP (data valid)
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDAP));
// Step 6: Wait for memory's /BDRY response
wait_for_signal(BDRY_PIN, LOW);
// Step 7: Release everything
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE);
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BAPR | BMEM | BINPUT | BDAP | BREQ));
pio_sm_put(pio1, SM_DAISY_CTRL, DAISY_PASS_GRANT);
}
Top-Level Bus Protocol Handler (Main Loop)¶
The bus protocol handler runs on Core 0 in a tight loop, dispatching events from the RX FIFO:
void bus_protocol_main_loop(void) {
while (1) {
// Read next event from FIFO (blocks until event arrives)
uint32_t event = read_event_fifo();
bool is_data = (event >> 24) & 1;
uint32_t value = event & 0x00FFFFFF;
if (!is_data) {
// Address event -- dispatch based on cycle type
uint32_t addr = value;
// Read current state of cycle-type signals
bool bmem = !gpio_get(BMEM_PIN); // active LOW
bool binput = !gpio_get(BINPUT_PIN);
bool inident = !gpio_get(INIDENT_PIN);
if (inident) {
handle_ident(addr);
} else if (bmem && !binput) {
handle_mem_read_from_cpu(addr);
} else if (bmem && binput) {
handle_mem_write_from_cpu(addr);
} else {
// IOX cycle -- determine read or write later
// (interface decides direction based on register type)
handle_iox_cycle(addr);
}
} else {
// Stale data event without preceding address?
// Or DMA read response from our own DMA cycle?
// Handled by DMA initiator code via wait_dma_read_event()
}
}
}
Core 1 runs the device emulation state machines (floppy, SMD, terminal, HDLC) which interact with the bus handler via shared memory and inter-core FIFOs.
Architecture Summary Diagram¶
flowchart TB
subgraph BUS["ND-100 Bus"]
direction LR
BD["/BD 0-23"]
CTL["/BAPR /BIOXE /BMEM /BDAP /BDRY /BINPUT"]
INT["/BINT 10 11 12"]
DMA["/BREQ /INGRANT /OUTGRANT /INIDENT /OUTIDENT"]
end
subgraph LS["Level Shifters (3.3V <-> 5V)"]
TS["3x 74LVC245 (BD)<br/>74LVC14 (in)<br/>74LVC07 (out)<br/>74LVC125 (daisy)"]
end
subgraph PIO0["PIO0 - Capture"]
SM00["SM0: BUS_CAPTURE<br/>(addr + data)"]
SM01["SM1: IOX_TRIGGER"]
SM02["SM2: BDAP_TRIGGER"]
SM03["SM3: BDRY_TRIGGER"]
end
subgraph PIO1["PIO1 - Drive"]
SM10["SM0: BD_DRIVE"]
SM11["SM1: CTRL_DRIVE"]
SM12["SM2: INT_DRIVE"]
SM13["SM3: DAISY_CTRL"]
end
subgraph CPU["RP2350 Cores"]
C0["Core 0:<br/>Bus Protocol Handler<br/>(IOX/IDENT/MEM/DMA)"]
C1["Core 1:<br/>Device Emulation<br/>(floppy/SMD/term/HDLC)"]
BUFFER["Circular event buffer<br/>(SRAM/PSRAM)"]
DEVS["Emulated device state<br/>+ memory regions"]
end
BUS <--> LS
LS <--> PIO0
LS <--> PIO1
SM00 -- "DMA" --> BUFFER
SM01 -- "IRQ4" --> SM00
SM02 -- "IRQ4" --> SM00
SM03 -- "IRQ4" --> SM00
BUFFER --> C0
C0 -- "TX FIFO" --> SM10
C0 -- "TX FIFO" --> SM11
C0 -- "TX FIFO" --> SM12
C0 -- "TX FIFO" --> SM13
C0 <--> C1
C1 <--> DEVS
style BUS fill:#FFF3E0,stroke:#E65100,color:#E65100
style LS fill:#E0F7FA,stroke:#00838F,color:#00838F
style PIO0 fill:#E8F5E9,stroke:#2E7D32,color:#2E7D32
style PIO1 fill:#F3E5F5,stroke:#7B1FA2,color:#7B1FA2
style CPU fill:#E3F2FD,stroke:#0D47A1,color:#0D47A1
Why This Architecture Works¶
-
Hardware (PIO) handles deterministic timing: All time-critical bus capture and drive happens in PIO state machines with cycle-accurate timing.
-
Software (CPU) handles protocol logic: The CPU has plenty of time (microseconds) to make decisions and dispatch commands. No tight loops on individual GPIO pins.
-
FIFOs decouple hardware from software: PIO captures bus events into a FIFO. CPU drains it at its own pace. No race conditions.
-
DMA decouples FIFO from CPU: Hardware DMA continuously moves FIFO data into a circular buffer in PSRAM. CPU never directly polls the FIFO.
-
Single-FIFO design simplifies dispatch: Tagged events in one FIFO maintain bus cycle ordering and avoid multi-FIFO arbitration complexity.
-
Two cores divide work cleanly: Core 0 handles bus protocol, Core 1 handles device emulation. Inter-core communication via shared memory.
-
All state machines are independent: Each PIO SM has its own program and FIFO. Adding new functionality (e.g., new bus cycle type) means adding a new handler, not modifying existing ones.
LED Indicator Design¶
LEDs provide at-a-glance visual feedback without needing to attach to USB serial. The goal is maximum visibility with minimum GPIO consumption.
LED Categories¶
LEDs are divided into three classes based on how they are driven:
| Class | Driver | GPIO Cost |
|---|---|---|
| Power indicators | Direct from power rails | 0 |
| Hardware-tapped indicators | Tap from existing bus signals | 0 |
| Software-controlled indicators | Via 74HC595 shift register on SD SPI bus | 1 |
This scheme adds 15 LEDs while consuming only 1 GPIO pin.
Class 1: Power Indicators (0 GPIO)¶
Wired directly to the power rails through current-limiting resistors.
| LED | Color | Indicates | Wiring |
|---|---|---|---|
| PWR_5V | Green | +5V rail present | +5V -> 1K -> LED -> GND |
| PWR_3V3 | Green | +3.3V rail present | +3.3V -> 470R -> LED -> GND |
Purpose: Verify power supply integrity. If PWR_5V is dim or off, the bus connector or LDO has failed.
Class 2: Hardware-Tapped Indicators (0 GPIO)¶
Tapped from existing bus signals after the level shifters. The tap points add no electrical load and are visible only on signal assertion.
| LED | Color | Tap Point | Indicates |
|---|---|---|---|
| BINT10 | Amber | 74LVC07 output for /BINT 10 | We are asserting interrupt level 10 |
| BINT11 | Amber | 74LVC07 output for /BINT 11 | Interrupt level 11 active |
| BINT12 | Amber | 74LVC07 output for /BINT 12 | Interrupt level 12 active |
| BINT13 | Amber | 74LVC07 output for /BINT 13 | Interrupt level 13 active (HDLC) |
| BREQ | Yellow | 74LVC07 output for /BREQ | We are requesting the bus (DMA pending) |
| BD_DRIVE | Blue | 74LVT245 /OE_BUS | We are driving the BD bus (IOX response or DMA) |
| BAPR_RX | White | /BAPR after level shifter | Bus address strobe (ANY card driving BAPR) -- visible flicker = bus activity |
| BMCL | Red | /BMCL after level shifter | Bus master clear active (system reset) |
Wiring example for active-LOW signal:
/BINT10 (after 74LVC07, active LOW = LED on)
|
+-- 1K -- LED -- +3.3V
When /BINT10 is HIGH (idle), no current flows, LED off. When /BINT10 is LOW (asserted), current flows from +3.3V through LED through resistor to the open-drain output, LED on.
Critical: Use 3.3V-side signals (after level shifter), not 5V bus signals directly. Keeps current low and avoids loading the bus.
Class 3: Software-Controlled Indicators (1 GPIO via 74HC595)¶
A single 74HC595 8-bit shift register drives 8 software LEDs. The 74HC595 shares the SD card SPI bus (SCK and MOSI) with a dedicated chip select pin.
RP2350 SPI bus (shared with SD card):
SCK ──┬──> SD card
└──> 74HC595 SCK
MOSI ──┬──> SD card MOSI
└──> 74HC595 SER
/SD_CS ──> SD card /CS
/LED_CS ──> 74HC595 /SS (latch enable)
74HC595 outputs Q0-Q7 ──> 8x LEDs
| LED | Color | Q-pin | Indicates |
|---|---|---|---|
| HEARTBEAT | Green | Q0 | 1 Hz blink -- firmware alive |
| FW_ERROR | Red | Q1 | Firmware fault / assertion failure |
| FLOPPY_ACT | Blue | Q2 | Floppy emulator active (read/write) |
| SMD_ACT | Blue | Q3 | SMD disk emulator active |
| TERM_ACT | Blue | Q4 | Terminal emulator active (data received/sent) |
| HDLC_ACT | Blue | Q5 | HDLC emulator active |
| SD_ACT | Yellow | Q6 | SD card read/write in progress |
| USB_CONN | Cyan | Q7 | USB host connected |
Update: The CPU shifts an 8-bit byte to the 74HC595 via SPI (~1 us at 25 MHz). LEDs update once per main loop iteration, more than fast enough for visual feedback.
Total LED Count¶
| Class | Count | GPIO Cost |
|---|---|---|
| Power | 2 | 0 |
| Hardware-tapped | 8 | 0 |
| Software-controlled (74HC595) | 8 | 1 |
| Total | 18 LEDs | 1 GPIO + 1 shift register chip |
LED Layout on PCB¶
Suggest grouping LEDs visually on the PCB edge or top-side for easy viewing:
+-------------------------------------------+
| |
| PWR HEARTBEAT FW_ERR FLOPPY SMD | <- Top row: status
| * * * * * |
| |
| TERM HDLC SD_ACT USB_CONN | <- Top row continued: devices
| * * * * |
| |
| BINT10 BINT11 BINT12 BINT13 | <- Mid row: interrupts
| * * * * |
| |
| BREQ BD_DRIVE BAPR_RX BMCL | <- Mid row: bus activity
| * * * * |
| |
| PWR_5V PWR_3V3 | <- Bottom row: power
| * * |
| |
+-------------------------------------------+
Cost and Component Addition¶
| Item | Quantity | Cost |
|---|---|---|
| 0805 LEDs (assorted colors) | 18 | ~$1.50 |
| 1K current-limiting resistors (0805) | 18 | ~$0.20 |
| 74HC595 shift register (SOIC-16) | 1 | ~$0.30 |
| Total LED system | ~$2.00 |
Software API¶
Simple LED update function on the CPU side:
typedef enum {
LED_HEARTBEAT = 0,
LED_FW_ERROR = 1,
LED_FLOPPY_ACT = 2,
LED_SMD_ACT = 3,
LED_TERM_ACT = 4,
LED_HDLC_ACT = 5,
LED_SD_ACT = 6,
LED_USB_CONN = 7,
} led_id_t;
static volatile uint8_t led_state = 0;
void led_set(led_id_t led, bool on) {
if (on) led_state |= (1u << led);
else led_state &= ~(1u << led);
led_update_hw(); // Shift to 74HC595
}
void led_update_hw(void) {
gpio_put(LED_CS_PIN, 0); // Latch low
spi_write_blocking(SPI0, &led_state, 1);
gpio_put(LED_CS_PIN, 1); // Latch high to update outputs
}
The led_update_hw() is called whenever a state changes, or on a 10-100 Hz timer for periodic updates (heartbeat blink).
Heartbeat LED Pattern¶
The HEARTBEAT LED uses a distinctive pattern to show MCU health:
| Pattern | Meaning |
|---|---|
| Off | MCU not running |
| 1 Hz blink | Normal operation |
| 4 Hz blink | High bus activity |
| Solid on | Stuck (no main loop iteration) |
| 0.25 Hz blink | Initialization in progress |
Debugging Workflow¶
With LEDs available:
- Power up: PWR_5V, PWR_3V3 should be solid green
- MCU boot: HEARTBEAT starts at 0.25 Hz, then 1 Hz when ready
- USB connect: USB_CONN turns on
- CPU activity on bus: BAPR_RX flickers
- IOX to our card: Brief BD_DRIVE flash
- Floppy access: FLOPPY_ACT blinks while servicing
- Interrupt assertion: BINT* LED stays on until IDENT serviced
- DMA cycle: BREQ flashes briefly, then BD_DRIVE during data
- Error: FW_ERROR turns red, USB serial has details
USB serial gives the detail (logs, debug prints, register dumps), while LEDs give the at-a-glance state.
Software Architecture: Central Registers with DMA¶
The recommended firmware architecture is:
- PIO state machines: Pure capture only -- no decision logic. Simply read bus state on triggers and push to FIFO.
- DMA: Always running, continuously transfers FIFO entries to central registers in SRAM.
- Central registers: Two volatile globals updated by DMA:
bus_address_latestandbus_data_latest. - C code: Polls (or is woken by IRQ) when central registers update. Makes all decisions. Controls outputs via PIO output SMs and direct GPIOs.
This architecture is simple, robust, and gives the C code full control over the bus protocol without trying to encode complex logic in PIO.
Central Register Layout¶
// Updated by DMA from PIO RX FIFO
volatile uint32_t bus_address_latest; // Latest captured address (from /BAPR)
volatile uint32_t bus_data_latest; // Latest captured data (from /BIOXE/BDAP/BDRY)
volatile uint32_t bus_event_seq; // Increments on each new event (for change detection)
// Driven by software, controls output stage
volatile bool boutident_enable; // Controls 74LVC125 daisy-chain pass-through
volatile bool boutgrant_enable; // Controls 74LVC125 grant pass-through
volatile uint32_t bd_output_value; // Latest value driven on BD bus (when active)
volatile bool bd_output_enable; // Controls /BD_OE_BUS
DMA Configuration¶
A single DMA channel continuously drains the BUS_CAPTURE PIO RX FIFO into a circular buffer or directly into the central registers.
// DMA channel setup (RP2350 SDK style)
dma_channel_config c = dma_channel_get_default_config(dma_chan);
channel_config_set_read_increment(&c, false); // Always read from FIFO
channel_config_set_write_increment(&c, false); // Always write to same address
channel_config_set_dreq(&c, pio_get_dreq(pio0, sm_capture, false));
channel_config_set_transfer_data_size(&c, DMA_SIZE_32);
dma_channel_configure(
dma_chan, &c,
&bus_address_latest, // dest = central register
&pio0->rxf[sm_capture], // src = PIO RX FIFO
UINT32_MAX, // transfer count = infinite
true // start immediately
);
The DMA continuously transfers each FIFO push into bus_address_latest. Whenever PIO captures, the central register updates within ~30 ns.
For separating address vs data updates, use two DMA channels with two FIFOs (or use the tag bit in the captured word and let software dispatch).
IDENT Handling with Central Registers¶
Now the IDENT flow becomes very clean:
void main_loop_ident_handler(void) {
static uint32_t last_seq = 0;
while (1) {
// Check if a new bus event occurred
if (bus_event_seq == last_seq) continue;
last_seq = bus_event_seq;
// Read latest bus address (always up to date via DMA)
uint32_t addr = bus_address_latest;
// Decode bus cycle type from current GPIO state
bool bmem_active = !gpio_get(BMEM_PIN);
bool inident_active = !gpio_get(INIDENT_PIN);
if (!bmem_active && !inident_active) {
// Normal IOX cycle (handle elsewhere)
handle_iox_cycle(addr);
} else if (bmem_active) {
// Memory cycle (handle elsewhere)
handle_memory_cycle(addr);
} else if (inident_active || is_ident_cycle(addr)) {
// IDENT cycle
handle_ident_central(addr);
}
}
}
void handle_ident_central(uint32_t addr) {
int level = extract_int_level(addr);
int device = find_device_with_interrupt(level);
if (device < 0) {
// === Case A/B: Not for us, forward ===
// Enable 74LVC125 buffer to forward BINIDENT -> BOUTIDENT
gpio_put(OE_DAISY_PASS_PIN, 0); // /OE LOW = buffer enabled = forward
boutident_enable = true;
// The 74LVC125 buffer now mirrors BINIDENT to BOUTIDENT
// Next card will see INIDENT and process it
// We don't need to do anything else
return;
}
// === Match: We respond ===
// Disable forwarding (block BINIDENT from reaching BOUTIDENT)
gpio_put(OE_DAISY_PASS_PIN, 1); // /OE HIGH = buffer disabled = blocked
boutident_enable = false;
// Drive ident code on BD bus
uint32_t ident_code = devices[device].ident_code;
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE_CMD(ident_code));
bd_output_value = ident_code;
bd_output_enable = true;
// Assert BDRY
gpio_put(BDRY_DRIVE_PIN, 0); // 74LVC07 input LOW = BDRY pulled LOW on bus
// Wait for CPU to release BINIDENT (poll the input pin)
while (gpio_get(INIDENT_PIN) == 0) {
// BINIDENT still LOW (active)
tight_loop_contents();
}
// BINIDENT released -- release BD bus and BDRY
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE_CMD);
bd_output_enable = false;
gpio_put(BDRY_DRIVE_PIN, 1); // 74LVC07 input HIGH = BDRY released
// Restore default forwarding state
gpio_put(OE_DAISY_PASS_PIN, 0); // Re-enable pass-through for next cycle
boutident_enable = true;
// Mark device as serviced
devices[device].int_active = false;
update_active_levels_mask();
}
Key Architecture Points¶
- PIO does ONE thing: capture bus state on triggers and push to FIFO
- DMA does ONE thing: continuously copy FIFO to central registers
- C code does decisions: reads central registers, decides, drives outputs
- Direct GPIO for fast control: Some signals (like /OE_DAISY_PASS, /BDRY drive) are controlled directly by GPIO writes from C code, not via PIO commands. This is faster than queuing PIO commands.
Timing Analysis (Central Register Architecture)¶
| Step | Time | Cumulative |
|---|---|---|
| /BAPR asserted on bus | 0 ns | 0 ns |
| Level shifter (74LVC14) | 5 ns | 5 ns |
| PIO sync + capture | 21 ns | 26 ns |
| DMA transfer FIFO -> central register | 30 ns | 56 ns |
| C code polls and detects update | 50-200 ns | 106-256 ns |
| C decodes cycle type and level | 50 ns | 156-306 ns |
| Forwarding case (no match): | ||
| C writes /OE_DAISY_PASS = LOW | 20 ns | 176-326 ns |
| 74LVC125 buffer enables, INIDENT propagates | 5 ns | 181-331 ns |
| Forwarding total | ~200-330 ns | -- |
| Capture case (match): | ||
| C writes BD output value via PIO | 50 ns | 206-356 ns |
| C asserts /BDRY drive | 20 ns | 226-376 ns |
| Bus sees response | 5 ns | 231-381 ns |
| Capture total | ~230-380 ns | -- |
Both forwarding and capture complete in under 400 ns, well within the 8 us bus cycle limit.
Why This Architecture is Better¶
| Aspect | PIO-managed (previous design) | Central register + C (this design) |
|---|---|---|
| PIO complexity | High (multiple SMs, sideset, IRQs) | Low (capture only) |
| C code complexity | Medium (commands to PIO) | Low (direct decisions) |
| Decision flexibility | Limited (PIO can only do simple ops) | Unlimited (full C code logic) |
| Multi-device support | Hard (PIO can't easily look up tables) | Easy (C code does lookups) |
| Debugging | Harder (PIO state hidden) | Easier (visible C variables) |
| Modification | Requires PIO reflash | Just edit C code |
| Total response time | ~50-100 ns (PIO direct) | ~200-400 ns (C dispatch) |
| Within 8 us budget? | Yes (huge margin) | Yes (huge margin) |
The C-driven architecture is much cleaner and the timing margin is more than sufficient.
Direct GPIO vs PIO Output for Control Signals¶
For very fast control signals, direct GPIO writes from C code are sometimes faster than queuing PIO commands:
| Signal | Method | Latency |
|---|---|---|
| /OE_DAISY_PASS (forward enable) | Direct GPIO | ~5 ns |
| /BDRY drive (open-drain) | Direct GPIO | ~5 ns |
| /BREQ drive | Direct GPIO | ~5 ns |
| /BINT 10/11/12 | Direct GPIO | ~5 ns |
| BD bus 24-bit drive | PIO output SM | ~50 ns |
| Coordinated multi-signal (BAPR + BMEM + BINPUT for DMA) | PIO output SM | ~50 ns |
Direct GPIO is fine for single-signal updates. PIO output SMs are needed when you need atomic multi-signal updates or to drive 24 bits at once.
IDENT Cycle Reality Check: It's Just a Bus Cycle¶
Re-reading the ND-100 manual carefully, the 100 ns "be ready" window is a design target, not a hard deadline. The IDENT cycle is a normal bus cycle bounded by the 8 us total cycle limit, and the bus is fully asynchronous -- it waits for signals to settle.
What actually matters:
| Constraint | Value | Hard? |
|---|---|---|
| Total bus cycle (BAPR to BDRY release) | 8000 ns | Hard -- BCU timeout |
| BAPR address hold | 50 ns | Hard -- need hardware latch (PIO is fast enough) |
| BAPR -> "interface ready to forward or capture" | ~100 ns | Soft -- target, not enforced |
| INIDENT propagation through chain | varies | Soft -- limited only by total cycle time |
The 100 ns figure is the manufacturer's expected response time. Bus cycles can take longer if needed -- the BCU only fails on the 8 us timeout.
Practical implication: We can do IDENT decision and forwarding in software within a few hundred ns, and the system will work. With a typical 5-10 cards in the chain, even 500 ns per card sums to 2.5-5 us -- well under the 8 us budget.
IDENT State Machine Design (Block-First Approach)¶
The cleanest design uses block-first pass-through control: by default forward, but immediately block when /BAPR asserts so we have time to decide. Software then either: - Releases the block (no match, signal continues to OUTIDENT) - Keeps the block and drives the response (match)
This eliminates the race condition where INIDENT might arrive before software has decided.
Sequence Diagram (BLOCK-FIRST IDENT)¶
sequenceDiagram
participant CPU as ND-100 CPU
participant BUS as NORD-100 Bus
participant PIO as PIO State Machines
participant SW as Pi Pico Software
participant DEV as Emulated Device
Note over CPU,BUS: IDENT PLxx instruction starts
CPU->>BUS: BD 0-5 = level (e.g. 12)
CPU->>BUS: /BAPR asserted
BUS->>PIO: PIO sees /BAPR LOW
Note right of PIO: ~14 ns
PIO->>PIO: Block pass-through<br/>(/OE_DAISY_PASS = HIGH)
PIO->>PIO: Read BD 0-5 (level)
PIO->>SW: Push event to FIFO
Note over SW: ~200-500 ns dispatch
SW->>SW: Check active interrupts<br/>vs requested level
alt Level matches our active interrupt
SW->>PIO: Send "drive ident code" command
SW->>PIO: Send "assert BDRY" command
Note right of PIO: ~50 ns
PIO->>BUS: Drive BD 0-23 = ident_code
PIO->>BUS: /BDRY asserted
BUS->>CPU: CPU strobes BD into A reg
CPU->>BUS: /INIDENT released
BUS->>PIO: PIO sees /INIDENT HIGH
PIO->>BUS: Release BD lines (high-Z)
PIO->>BUS: /BDRY released
SW->>DEV: Mark interrupt as serviced
else No match - forward
SW->>PIO: Send "release block" command
Note right of PIO: ~30 ns
PIO->>PIO: /OE_DAISY_PASS = LOW
Note over BUS: INIDENT now propagates<br/>via 74LVC125 to OUTIDENT
Note over BUS: Next card processes INIDENT
Note over CPU: Eventually some card responds<br/>or CPU times out
end
PIO State Machine Architecture for IDENT¶
We use two PIO state machines dedicated to IDENT handling, plus the existing BUS_CAPTURE for general address capture:
SM A: IDENT_BLOCKER -- detects /BAPR and blocks pass-through immediately
.program ident_blocker
.side_set 1 ; sideset pin = /OE_DAISY_PASS
.wrap_target
; Idle state: pass-through ENABLED (sideset 0 = LOW = OE active)
wait 0 pin BAPR_PIN side 1 ; on BAPR LOW: BLOCK immediately (sideset 1 = HIGH = OE disabled)
in pins, 6 side 1 ; read BD 0-5 (level)
push side 1 ; push level event to FIFO
pull side 1 ; wait for software command (still blocking)
out x, 1 side 1 ; bit 0: 0 = release, 1 = keep blocking
jmp x-- keep side 1
nop side 0 ; release block (forward enabled)
jmp wait_release side 0
keep:
; Stay blocked while we're driving the response
wait_release:
wait 1 pin BAPR_PIN side 0 ; wait for BAPR release, restore default
.wrap
Time from BAPR active to block engaged: ~14-21 ns. The pass-through is blocked before INIDENT even arrives at our card.
SM B: IDENT_RESPONDER -- when armed, drives ident code response on INIDENT
.program ident_responder
.side_set 1 ; sideset pin = /BDRY drive
.wrap_target
; Wait for software to arm us with an ident code
pull side 1 ; pull ident code (24 bits), BDRY released
; OSR now contains the 24-bit ident code
; Set BD pin directions to OUTPUT (24 bits)
mov x, osr side 1 ; save ident code in X
mov osr, ~null side 1 ; load 0xFFFFFFFF
out pindirs, 24 side 1 ; pin dirs = output for BD
; Restore ident code and wait for INIDENT
mov osr, x side 1
wait 0 pin INIDENT_PIN side 1
; INIDENT active -- drive ident code, assert BDRY
out pins, 24 side 0 ; drive BD with ident code, BDRY LOW
; Wait for INIDENT release
wait 1 pin INIDENT_PIN side 0
; Release BD lines and BDRY
mov osr, null side 1 ; BDRY released
out pindirs, 24 side 1 ; pin dirs back to input
.wrap
Time from INIDENT to BD driven: ~14-21 ns.
Software Coordination¶
The software (Core 0 main loop) coordinates the two SMs:
void handle_ident_event(uint32_t level_event) {
// Event came from IDENT_BLOCKER SM (already blocking pass-through)
int level = level_event & 0x3F;
int device = find_device_with_interrupt(level);
if (device < 0) {
// No match -- release the block, INIDENT will forward to next card
pio_sm_put(pio_ident, SM_IDENT_BLOCKER, 0); // bit 0 = 0 = release
return;
}
// Match -- arm the responder with our ident code
uint32_t ident_code = devices[device].ident_code;
pio_sm_put(pio_ident, SM_IDENT_RESPONDER, ident_code);
// Tell blocker to KEEP blocking (we're capturing)
pio_sm_put(pio_ident, SM_IDENT_BLOCKER, 1); // bit 0 = 1 = keep blocking
// Mark device as serviced
devices[device].int_active = false;
// The IDENT_RESPONDER SM handles the rest automatically:
// - Waits for INIDENT
// - Drives BD + BDRY
// - Releases on INIDENT release
}
Total Timing Budget¶
| Step | Time | Cumulative |
|---|---|---|
| /BAPR asserted on bus | 0 ns | 0 ns |
| Level shifter (74LVC14) | 5 ns | 5 ns |
| PIO sync + IDENT_BLOCKER detects, blocks, captures | 21 ns | 26 ns |
| FIFO -> DMA -> circular buffer -> CPU | 100 ns | 126 ns |
| CPU dispatch handler | 100 ns | 226 ns |
| CPU decode level, check active interrupts | 50 ns | 276 ns |
| CPU push command to PIO (release or arm responder) | 20 ns | 296 ns |
| Decision complete | ~300 ns | |
| (CPU asserts INIDENT some time later, e.g., 500 ns after BAPR) | -- | -- |
| IDENT_RESPONDER detects INIDENT | 21 ns | -- |
| IDENT_RESPONDER drives BD + BDRY | 14 ns | -- |
| Total response time after INIDENT: | ~35 ns | -- |
Total IDENT cycle time (BAPR to BDRY assertion): typically 600-800 ns including the CPU's BAPR-to-INIDENT delay. Well under the 8 us limit (10x margin).
What If Software is Slow?¶
If software takes longer than expected (say 1-2 us), the only consequence is that INIDENT propagation is delayed by that amount. The CPU is patient and waits. The total cycle stays under 8 us with margin.
There is no hard deadline for our software response, only the soft 8 us total cycle limit. We have ample headroom.
Forwarding Speed Analysis (Cases A and B)¶
Case A: No active interrupts
Software flow:
1. ADDR event arrives
2. Check active_levels_mask == 0 -> true
3. Send "release block" command to PIO
4. Done
Time: ~200-300 ns
Case B: Active interrupts but level mismatch
Software flow:
1. ADDR event arrives
2. Check (active_levels_mask >> level) & 1 -> false
3. Send "release block" command to PIO
4. Done
Time: ~200-300 ns
In both cases, forwarding takes ~200-300 ns. With a 5-card chain, total propagation is ~1.5 us. Total IDENT cycle: ~2 us. Well under 8 us.
Hardware Required¶
| Chip | Purpose | Quantity |
|---|---|---|
| 74LVC14 | Schmitt-trigger inverter for BAPR conditioning | 1 (shared) |
| 74LVC125 | Daisy-chain pass-through buffer (already in design) | 1 |
| 74LVC07 | Open-drain BDRY drive (already in design) | 1 (shared) |
| No CPLD, no comparator, no extra chips |
The IDENT handling fits entirely within the existing hardware design. Just two PIO state machines and some software.
Summary¶
By recognizing that IDENT is a normal 8 us bus cycle (not a hard 100 ns deadline), we can implement IDENT entirely in PIO + software:
- No CPLD needed
- No extra hardware comparator needed
- 2 PIO state machines (IDENT_BLOCKER + IDENT_RESPONDER) handle the timing
- Block-first approach eliminates race conditions
- Software has 200-500 ns to decide, well within the cycle budget
- Total IDENT cycle: ~600-800 ns typical, ~2 us with chain propagation
- 8 us limit: Comfortable 4-13x margin
This design works for V1 (last card) and V2 (middle of chain) without modification. The block-first PIO approach handles both cases identically.
IDENT Timing Solutions Without CPLD¶
The 100 ns IDENT decision window is the tightest constraint. The CPLD approach (Option B in critical analysis) is reliable but adds a programmable chip. Two alternative approaches avoid the CPLD entirely.
Solution A: "Last Card on Bus" (V1, Simplest)¶
Core idea: If the controller card is physically the last card in the daisy chain, there is no card downstream that needs INIDENT to pass through. The 100 ns window becomes irrelevant because there is nothing to forward to.
Constraints: - Card MUST occupy the lowest-priority slot (last in the chain) - No I/O cards may be installed in slots after ours - /OUTIDENT and /OUTGRANT can be left unconnected (or driven HIGH for safety)
Behavior:
Since we're the last card: - /INIDENT arrives at our card from the previous slot (or directly from CPU if we're slot 1 and only) - We have all the time we need to decide whether to respond - If we have an active interrupt on the queried level, we respond with our ident code - If we don't, we simply do nothing -- there's no next card to forward to - The CPU sees no /BDRY response and the IDENT instruction returns "no device" (or whatever the CPU does for an unmatched IDENT)
Software flow:
void handle_ident_last_card(uint32_t addr) {
int level = extract_int_level(addr);
// No timing pressure -- we have microseconds
int device = find_device_with_interrupt(level);
if (device < 0) {
// Not for us -- do nothing, no forwarding needed (we're last)
return;
}
// Drive ident code response
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(devices[device].ident_code));
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDRY));
// Reset device interrupt
devices[device].int_active = false;
// Wait for cycle release
wait_for_signal(BAPR_PIN, HIGH);
// Cleanup
pio_sm_put(pio1, SM_BD_DRIVE, BD_RELEASE);
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_CLR(BDRY));
}
Bonus: The same simplification applies to INGRANT/OUTGRANT. As the last card, we don't need fast pass-through for DMA grants either. Software handles everything.
Hardware impact:
| Component | Action |
|---|---|
| 74LVC125 daisy-chain bypass | Still install for future, but functionally optional |
| /OUTIDENT pin | Can be left unconnected or wired to nothing |
| /OUTGRANT pin | Same |
| CPLD | Not needed |
V1 design rule: The card MUST be physically installed as the last card in the bus chain. Document this in the user manual. PCB silkscreen warning: "Install in highest-numbered slot only".
Pros: - Eliminates the 100 ns timing problem entirely - No CPLD, no fast comparator - Simpler firmware (no preemption needed) - Cheaper BOM
Cons: - Card cannot be in middle of chain - Other cards must be in lower-numbered (higher-priority) slots - For a single-card system, this is no constraint at all
Recommendation: Use this for V1. It's the simplest viable design.
Solution B: SPI-Loaded Comparator (V2, No CPLD)¶
If V2 needs to support middle-of-chain operation, a discrete-logic comparator can do it without a CPLD.
Core idea: Pre-load an "active levels mask" into a hardware register via SPI. When /BAPR asserts, hardware compares the BD level against the mask and blocks the pass-through if there's a match.
Components:
| Chip | Function |
|---|---|
| 74HC595 | 8-bit shift register -- holds the "active levels" bitmask, loaded via SPI from MCU |
| 74LVC151 (or 74HC151) | 8-input multiplexer -- selects one bit from the mask based on BD level |
| 74LVC02 (or 74HC02) | NOR gate -- combines /BAPR + /BMEM_HIGH + match output to drive block signal |
| 74LVC125 | Daisy-chain bypass buffer (already in design) |
Wiring:
CPU SPI bus (shared with SD card and LEDs):
SCK --> 74HC595 SCK (clock in active levels mask)
MOSI --> 74HC595 SER (mask data)
/CS_LEVELS --> 74HC595 /SS
74HC595 outputs Q0-Q3 = active levels (Q0=level 10, Q1=11, Q2=12, Q3=13)
Q0-Q3 (and Q4-Q7 unused) --> 74LVC151 inputs D0-D7
Bus side (level bits from BD lines):
/BD 0 --> level shifter --> 74LVC151 select A (S0)
/BD 1 --> level shifter --> 74LVC151 select B (S1)
/BD 2 --> level shifter --> 74LVC151 select C (S2)
74LVC151 output Y = "we have an interrupt active on this level"
Combinational logic (74LVC02 NOR or similar):
block_signal = Y AND (/BAPR low) AND (/BMEM high) AND (/INIDENT incoming)
block_signal --> /OE_IDENT_PASS on 74LVC125
Encoding note: The BD level bits encode 10, 11, 12, 13. The lower 2 bits give 10, 11, 00, 01 -- not contiguous. Software must load the 74HC595 mask with the same encoding so the multiplexer selects correctly. The MCU is responsible for writing the right pattern.
Or simpler: use only BD 0-1 (lower 2 bits of level) and a 4-input mux instead of 8-input. The MCU pre-encodes the mask appropriately.
Timing:
| Stage | Time |
|---|---|
| Level shifter (74LVC) | 3-5 ns |
| 74LVC151 mux propagation | 3-7 ns |
| 74LVC02 gate | 3-5 ns |
| 74LVC125 buffer disable | 3-5 ns |
| Total | ~12-22 ns |
Well within the 100 ns window with massive margin.
Pros: - No CPLD - Discrete logic chips, all common 74xx parts - Predictable, fully combinational - Programmable via SPI (can update mask dynamically)
Cons: - 3-4 extra chips - BD 0-2 must be tapped before the level shifters (or after, if 5V tolerant) - More PCB routing
Cost: ~$1.50 in chips + passives.
Solution C: PIO Fast Path (V2 alternative, no extra chips)¶
Use one PIO state machine per interrupt level (4 SMs total) to watch /BAPR, read the level, compare, and block the chain in <60 ns.
Cost: 4 PIO state machines (we have 12 total, plenty available).
Pros: - No external hardware - Fully software-defined - Can be updated firmware-side
Cons: - Uses 4 PIO state machines just for IDENT - Per-SM compare against fixed Y register -- limited flexibility - Borderline timing (60 ns of 100 ns budget)
Recommendation: Solution A (last card) for V1, Solution B (SPI comparator) for V2 if middle-of-chain needed.
Decision Matrix¶
| Solution | V1 viable? | V2 viable? | Cost | Complexity | Timing margin |
|---|---|---|---|---|---|
| A: Last card simplification | ✓ | ✗ (only last) | $0 | Lowest | Unlimited |
| B: SPI comparator | ✓ | ✓ | ~$1.50 | Medium | 5-8x |
| C: PIO fast path | ✓ | ✓ | $0 | Medium | 1.5-2x |
| D: CPLD (original) | ✓ | ✓ | ~$3-5 | Medium | 10-20x |
For V1: Solution A. Simplest, cheapest, fastest to build.
For V2: Solution B (discrete logic) or Solution C (PIO) if middle-of-chain is needed. Avoid CPLD unless future flexibility is required.
V1 Final Recommendation¶
Use Solution A: Last Card on Bus for the first version. Update the design accordingly:
- PCB silkscreen: "Install in last/highest slot of bus chain"
- /OUTIDENT and /OUTGRANT outputs: Wire them but they have no functional effect (no card downstream)
- 74LVC125 daisy-chain buffer: Still install (V2 ready), default to pass-through enabled via pull-down
- Firmware: Implement IDENT handler without time pressure
- No CPLD, no comparator -- save the chip count and complexity for V1
This makes the design fully buildable with V1 firmware while leaving the hardware ready for V2 enhancements.
Critical Design Analysis -- Priorities and Timing¶
This section analyzes the design against the explicit priorities for the controller card and identifies critical issues that need resolution before PCB.
Priority 1: IOX Read/Write + Interrupt Signaling + IDENT Response¶
Sub-goal 1a: IOX Read/Write -- VIABLE ✓
The CPU-side handlers (Handler 1 IOX Read, Handler 2 IOX Write) are correct. Total time: - IOX Read: ~255 ns from BAPR to bus release - IOX Write: ~200 ns - Bus limit: 8000 ns - Margin: 30x+
The path is: PIO captures address (~40 ns) -> CPU dispatches handler (~200-500 ns of software) -> PIO drives response. Well within budget.
Sub-goal 1b: Interrupt assertion -- VIABLE ✓
Driving /BINT 10/11/12 LOW via INT_DRIVE PIO state machine is straightforward. The CPU asserts the interrupt line whenever an emulated device needs attention. This is asynchronous and not time-critical.
Sub-goal 1c: IDENT response -- ⚠ CRITICAL TIMING ISSUE
The 100 ns IDENT decision window is the tightest constraint in the entire design. Let me critically analyze if our software-based response can meet it.
The problem:
When the CPU executes IDENT PLxx, the bus protocol is: 1. CPU drives interrupt level on BD lines 2. CPU asserts /BAPR 3. CPU asserts /INIDENT (after a brief delay) 4. Within 100 ns, every card in the daisy chain must either: - Capture /INIDENT (block pass-through to OUTIDENT) and prepare to respond - Pass through /INIDENT to OUTIDENT for the next card
Path analysis (current design):
0 ns /BAPR asserted on bus
~5 ns Level shifter propagation
~12 ns PIO sync delay
~20 ns PIO0.SM0 ADDR_CAPTURE reads BD into FIFO
~25 ns Event written to FIFO
~25 ns DMA sees FIFO has data, copies to PSRAM circular buffer
~50 ns Core 0 polling sees new event in buffer
~100 ns Core 0 dispatches IDENT handler
~150 ns Core 0 reads interrupt active mask, compares with level
~200 ns Core 0 decides: capture or pass
~250 ns Core 0 sends DAISY_BLOCK command to PIO1
~270 ns PIO1 receives command, asserts /OE_DAISY_PASS HIGH
~280 ns 74LVC125 buffer goes high-Z, OUTIDENT released
This is ~280 ns total -- nearly 3x the 100 ns budget.
The hardware default pass-through (74LVC125 with pull-down on /OE) means /INIDENT is ALREADY passing to OUTIDENT before our CPU has even decided. By the time our CPU reacts, the next card has already received INIDENT and may have responded.
This is a critical flaw in the current design.
Solution Options:
Option A: Pre-armed Hardware Block¶
When the CPU detects that one of our emulated devices has an interrupt active, it pre-blocks the daisy-chain pass-through immediately. The 74LVC125 /OE goes HIGH, breaking the chain. Then when IDENT arrives, we capture it (we're already blocking).
Drawback: While our interrupt is active, we block all downstream cards from getting their interrupts identified. This violates the daisy-chain priority scheme -- our card effectively becomes the highest priority for ALL levels we have active, blocking other devices.
Mitigation: Only block for the level we have active. Use 4 separate /OE control bits, one per level (10, 11, 12, 13). But then we need 4 daisy-chain bypass channels (4 x 74LVC125 = 1 chip with 4 channels).
Actually no -- the daisy chain is one signal /INIDENT, not per-level. The level is on the BD lines during the IDENT cycle. We can't do per-level blocking with separate buffers because there's only one INIDENT signal.
Better mitigation: Hardware comparator (next option).
Option B: Hardware Comparator (RECOMMENDED)¶
Add a small comparator chip that: 1. Reads the interrupt level from BD 0-3 (or 0-5) 2. Compares against a CPU-controlled "active levels" register 3. If match AND /BAPR asserted AND /INIDENT asserted -> block pass-through 4. If no match -> allow pass-through
This is combinational logic -- no software involved. Response time: ~5-10 ns.
Required hardware:
| Chip | Function |
|---|---|
| 74LVC373 | Latch the BD lines on /BAPR (4-bit level) |
| 74LVC85 (or 74LVC688) | 4-bit magnitude comparator OR equality comparator |
| 74HC595 (or GPIO directly) | CPU-controlled "active levels" register (4 bits) |
| 74LVC02 NOR gate | Combine signals: /BAPR_LATCHED AND match AND /INIDENT |
OR a simpler approach: use a small CPLD (ATF1502 or Lattice MachXO2) to implement all this logic in one chip. The CPLD can be programmed with the comparator + latch + glue logic.
CPLD approach is cleanest: 1 chip, ~5 ns response, fully programmable.
Option C: PIO Fast Path (Marginal)¶
Dedicate a PIO state machine to do nothing but watch /BAPR + /INIDENT and immediately compare BD level against a pre-loaded mask. PIO can do this in ~50-70 ns.
Pseudocode:
.program ident_fast
.wrap_target
wait 0 pin BAPR_PIN ; ~14 ns sync
in pins, 4 ; read BD 0-3 (level), 7 ns
mov x, isr ; ~7 ns
jmp x!=y skip ; compare with Y (preloaded mask), 7 ns
set pins, 1 ; assert OE_BLOCK, 7 ns -- TOTAL ~42 ns
skip:
.wrap
Total: ~42 ns -- fits within 100 ns window with margin.
But: PIO can only compare against a single Y register value. We can't easily implement a "match any of these levels" check. If we have interrupts on multiple levels, this gets complex.
Workaround: Use one PIO SM per level (4 SMs for levels 10/11/12/13). Each SM watches for its specific level and blocks the chain if matched. This works but uses 4 PIO state machines just for IDENT.
RECOMMENDED SOLUTION¶
Option B (Hardware Comparator via small CPLD) is the cleanest and most reliable solution. The CPLD: - Has guaranteed timing (~5 ns) - Doesn't consume PIO state machines - Doesn't require CPU intervention - Automatically handles multi-level interrupt cases - Programmable for future enhancements
CPLD configuration: - Inputs: BD 0-5 (interrupt level), /BAPR, /INIDENT, /BMEM (to distinguish IDENT from MEM cycle), 4-bit "active levels" register from MCU - Outputs: /OE_DAISY_PASS (drives 74LVC125), IDENT_HIT_FLAG (notifies MCU) - Logic: when /BAPR asserts AND /BMEM is HIGH (= IDENT cycle, not memory) AND BD level matches one of active levels -> block pass-through and raise IDENT_HIT_FLAG
The MCU updates the "active levels" register whenever it asserts/clears an interrupt. The CPLD handles the rest.
Cost: 1 CPLD chip (~$3-5), some passives. Saves PIO state machines, eliminates timing risk.
Priority 2: Controller-Initiated DMA Read/Write -- VIABLE ✓¶
The DMA handlers (Handler 6 dma_read, Handler 7 dma_write) are designed correctly. Critical analysis:
Timing: - BREQ assertion -> INGRANT: ~200 ns (depends on BCU and other DMA traffic) - Drive address + BAPR: ~80 ns - Wait for memory response (BDRY): ~200-500 ns (memory dependent) - Capture data (read) or write completion: ~80 ns - Release: ~30 ns - Total per word: ~600-900 ns - Throughput: ~1.1-1.7 MB/s
Sufficient for: - Floppy (~50 KB/s) ✓ massive margin - HDLC (~64-512 Kbit/s) ✓ ample margin - Slow HDD (~500 KB/s) ✓ adequate margin - Fast SMD (~3 MB/s sustained) ⚠ marginal -- may need optimization
Optimizations for SMD: - Burst mode: queue multiple DMA cycles back-to-back - Pipeline: start next BREQ while processing current word - DMA controller in RP2350 can pipeline FIFO transfers
Verdict: Priority 2 is achievable. SMD might need software pipelining but the hardware path is correct.
Priority 3 (Low): Memory Emulation -- DEFERRED ✗¶
The current design includes memory emulation (Handler 4, Handler 5) but this is low priority. Critical analysis suggests this should be deferred or removed:
Issues:
-
Address space conflict risk: Our emulated memory must not overlap real memory. The user must ensure SINTRAN configuration excludes our region. Wrong configuration = system crash.
-
PSRAM latency: ~100-200 ns to fetch from PSRAM eats into the bus cycle budget. Internal SRAM is fast but limited to 520 KB.
-
Coherency: Our memory is separate from real memory. No cache coherency.
-
Test difficulty: Hard to validate without affecting real memory regions.
-
Use case unclear: What memory would we emulate? Boot ROM? Extension memory bank? The user has not specified.
Recommendation: Remove memory emulation from initial design. Keep the bus signals available (we already capture /BMEM and /BDAP) so it can be added in firmware later if needed. The hardware doesn't change. Just don't write the handler.
This simplifies the firmware and reduces risk for Priority 1 and 2 work.
Priority 4 (CRITICAL): Bus Phase Tracking and Multi-Device Coordination¶
The user explicitly requires that the controller can emulate multiple devices simultaneously (e.g., a floppy AND a terminal AND HDLC on one card). The bus interface is shared -- only one device can drive the bus at a time.
The risk: Two device emulators try to drive the bus at the same time. Result: bus contention, possibly damaging the CPU 74F241 outputs or our 74LVT245 drivers.
Mitigation: Explicit phase tracking with mutual exclusion.
Bus Phase State Machine¶
Core 0 maintains a single global variable bus_phase that tracks the current phase of the bus interface. All device emulators must check this variable and queue their requests if the bus is busy.
Phase enumeration:
typedef enum {
PHASE_IDLE = 0, // No bus activity, ready for next event
// CPU-initiated cycles (we are passive responder)
PHASE_IOX_ADDR_RECEIVED, // CPU asserted BAPR for IOX, decoded address
PHASE_IOX_READ_RESPOND, // We are driving BD + BINPUT for IOX read
PHASE_IOX_WRITE_CAPTURE, // Waiting for IOX write data (BIOXE)
PHASE_IDENT_RESPOND, // Driving ident code on BD + BDRY
PHASE_MEM_READ_RESPOND, // Driving memory data (memory emulation)
PHASE_MEM_WRITE_CAPTURE, // Capturing memory write data
// Controller-initiated cycles (we are bus master)
PHASE_DMA_REQUEST, // BREQ asserted, waiting for INGRANT
PHASE_DMA_ADDR, // Driving address on BD
PHASE_DMA_READ_WAIT, // Waiting for memory response
PHASE_DMA_WRITE_DRIVE, // Driving data to memory
} bus_phase_t;
volatile bus_phase_t bus_phase = PHASE_IDLE;
Device Emulator Coordination¶
Each emulated device runs its own state machine on Core 1. When a device wants to do a bus operation (e.g., DMA transfer), it submits a request to a bus operation queue:
typedef struct {
uint8_t op_type; // OP_DMA_READ, OP_DMA_WRITE, OP_INT_ASSERT, etc.
uint8_t device_id; // Which emulated device
uint32_t address; // Memory address (for DMA)
uint16_t data; // Data (for DMA write)
uint16_t* result; // Where to store result (for DMA read)
sem_t* completion_sem; // Signal when done
} bus_op_request_t;
queue_t bus_op_queue; // FIFO queue between cores
Core 0 processes requests one at a time:
void core0_main_loop(void) {
while (1) {
// Priority 1: Handle any pending CPU-initiated cycles
if (event_fifo_has_data()) {
handle_cpu_cycle();
continue;
}
// Priority 2: Handle controller-initiated requests from Core 1
if (bus_phase == PHASE_IDLE && queue_has_request(&bus_op_queue)) {
bus_op_request_t req;
queue_pop(&bus_op_queue, &req);
handle_controller_op(&req);
}
}
}
Mutual Exclusion Rules¶
- Only Core 0 manipulates
bus_phase-- single writer, no locks needed - Only Core 0 sends commands to PIO1 output state machines -- no contention
- Core 1 NEVER touches the bus directly -- only submits requests via queue
- CPU-initiated cycles (priority 1) preempt controller requests (priority 2) -- if a CPU IOX cycle starts while we have a queued DMA, the IOX cycle runs first
CPU vs Controller Conflict Resolution¶
Scenario: We are about to start a DMA cycle (PHASE_DMA_REQUEST) when the CPU starts an IOX cycle to one of our registers.
Resolution: - The BCU handles bus arbitration. Even if we asserted BREQ, the CPU might get the bus first if it had priority. - We will receive an ADDR event (CPU's IOX) before INGRANT is granted. - Our handler must process the IOX cycle, complete it, then resume waiting for INGRANT. - The bus phase should track this:
void handle_event() {
bus_phase_t saved_phase = bus_phase;
if (saved_phase == PHASE_DMA_REQUEST) {
// We were waiting for INGRANT, but CPU is doing IOX to us
// Process IOX first, then resume DMA wait
bus_phase = PHASE_IOX_ADDR_RECEIVED;
handle_iox_cycle();
bus_phase = PHASE_DMA_REQUEST;
} else {
bus_phase = PHASE_IOX_ADDR_RECEIVED;
handle_iox_cycle();
bus_phase = PHASE_IDLE;
}
}
Important: The BREQ assertion remains active across the IOX cycle. After the IOX completes, we continue waiting for INGRANT.
Multi-Device Example: Floppy DMA + Terminal IOX¶
Scenario: Emulated floppy is doing a 256-word DMA transfer, emulated terminal needs to respond to a status read IOX from the CPU.
Time Event bus_phase
-------- ---------------------------------- ----------------
0 ns Floppy queues DMA request PHASE_IDLE
10 ns Core 0 picks up request PHASE_DMA_REQUEST
20 ns PIO asserts /BREQ PHASE_DMA_REQUEST
(waiting for INGRANT)
100 ns CPU asserts /BAPR for IOX read PHASE_DMA_REQUEST
140 ns Core 0 receives ADDR event PHASE_DMA_REQUEST
Decodes: terminal status register
145 ns Save state, switch to IOX handling PHASE_IOX_READ_RESPOND
150 ns /BIOXE asserted by CPU PHASE_IOX_READ_RESPOND
160 ns Send drive /BINPUT PHASE_IOX_READ_RESPOND
200 ns /BINACK asserted by CPU PHASE_IOX_READ_RESPOND
210 ns Send drive BD = terminal status PHASE_IOX_READ_RESPOND
220 ns Send drive /BDRY PHASE_IOX_READ_RESPOND
250 ns CPU strobes data, releases /BIOXE PHASE_IOX_READ_RESPOND
260 ns Release BD, /BINPUT, /BDRY PHASE_DMA_REQUEST
(resume DMA wait)
300 ns /INGRANT arrives PHASE_DMA_ADDR
310 ns Drive memory address PHASE_DMA_ADDR
... (continue DMA cycle)
The /BREQ remains asserted throughout the IOX interruption -- the BCU sees it and continues the grant process.
Critical Rule: PIO State Machine Locking¶
Only one PIO output state machine can be driving the BD bus at a time. The bus phase variable enforces this by serializing all bus drive operations through Core 0's main loop.
If a device emulator on Core 1 wants to drive the bus directly (bypassing the queue), this must be forbidden. Code review and runtime assertions should catch any attempt.
Multi-Device Interrupt Coordination¶
Each emulated device has its own interrupt_active flag and interrupt_level. When a device asserts an interrupt:
void emulated_device_assert_interrupt(int device_id, int level) {
devices[device_id].int_active = true;
devices[device_id].int_level = level;
// Update active levels mask
int mask = 0;
for (int i = 0; i < NUM_DEVICES; i++) {
if (devices[i].int_active) {
mask |= (1 << devices[i].int_level);
}
}
update_active_levels_register(mask); // Send to CPLD or PIO
// Drive the BINT line via INT_DRIVE PIO SM
pio_sm_put(pio1, SM_INT_DRIVE, mask);
}
When the CPU does IDENT and we respond, the device's interrupt is reset:
void handle_ident(int level) {
// Find which device has interrupt on this level (round-robin if multiple)
int device = find_device_with_interrupt(level);
if (device < 0) return;
// Drive ident code
pio_sm_put(pio1, SM_BD_DRIVE, BD_DRIVE(devices[device].ident_code));
pio_sm_put(pio1, SM_CTRL_DRIVE, CTRL_SET(BDRY));
// Reset device interrupt
devices[device].int_active = false;
update_active_levels_mask();
}
This handles the case where multiple emulated devices share the same interrupt level (e.g., two terminals on level 12). Round-robin or fixed-priority arbitration determines which one responds first.
Critical Issues Summary¶
| Issue | Severity | Solution |
|---|---|---|
| IDENT 100 ns timing | CRITICAL | Add CPLD comparator (Option B) for hardware-level ident decision |
| Bus phase race conditions | High | Single bus_phase variable on Core 0, mutual exclusion |
| Multi-device contention | High | Bus operation queue, single drive path through Core 0 |
| Memory emulation complexity | Medium | Defer to phase 2, remove from initial design |
| DMA SMD throughput | Medium | Software pipelining, may be acceptable as-is |
| Bus contention damage risk | Critical | All output drivers default disabled (pull resistors), single PIO drive path |
Updated Architecture Recommendation¶
For initial board (Phase 1): 1. Add a CPLD (ATF1502 or similar small, programmable logic) for IDENT fast-path comparator. This is the only way to reliably meet the 100 ns IDENT window. 2. Implement Priority 1 (IOX + interrupts + IDENT) with CPLD-assisted IDENT 3. Implement Priority 2 (controller DMA) with software pipelining for SMD throughput 4. Defer memory emulation -- hardware supports it, firmware doesn't implement initially 5. Implement bus phase tracking with operation queue for multi-device coordination
For Phase 2 (after validation): - Add memory emulation if needed - Optimize DMA pipelining for higher throughput - Add additional device emulations
Physical Bus Interface IC Options¶
The ND-100 bus is 5V TTL. The RP2350B is 3.3V with non-5V-tolerant inputs. Level shifters are mandatory for all bus signals.
IC Catalog -- Available Options¶
Bidirectional Level Shifters¶
| IC | Speed | Direction | Best For | Notes |
|---|---|---|---|---|
| TXS0108E | 2.5-10 ns (slower on direction change) | Auto-sensing | Open-drain, low-speed buses | Auto-direction adds variable delay -- problematic on multiplexed buses |
| TXB0108 | 1-2 ns | Auto-sensing | Push-pull, high-speed | Fastest, but less robust on long/noisy traces |
| 74LVC245 | 3-6 ns | Manual DIR pin | Push-pull bus, time-critical, 3.3V to 5V | Best for 3.3V MCU to 5V bus -- 1.65-5.5V supply, 5V tolerant inputs at 3.3V, deterministic, proven |
| 74LC245A | 3-6 ns | Manual DIR pin | Octal bidirectional transceiver | Non-inverting, 3-state, similar to LVC245. Verify 5V tolerance for your variant before using on bus side |
| 74LVT245 | 3-5 ns | Manual DIR pin | Push-pull, mixed 3.3V/5V | Slightly faster than LVC, higher drive strength, used in ZuluSCSI |
| SN74LVC8T245 (KS245) | 3-5 ns | Manual DIR pin | High-speed octal | Advanced version of LVC245 |
| SN74LVCH16T245 | 3-5 ns | Manual DIR pin | 16-bit version | Saves PCB space if many signals |
| 74AS648 | 4-6 ns | Manual DIR pin | 5V TTL only | Advanced Schottky -- 5V system, not for 3.3V interfacing |
| 74LS240 | 10-14 ns | Inverting output | 5V TTL only | Original ND-100 era part -- too slow for 3.3V level shifting |
Latches and Buffers¶
| IC | Function | Notes |
|---|---|---|
| 74LVC573 | Octal transparent D-latch with 3-state | Good for input latch (clocked by /BAPR) |
| 74LVC574 | Octal D flip-flop with 3-state | Edge-triggered version of 573 |
| 74LVT245 | Octal bus transceiver | Used in ZuluSCSI -- proven for SCSI bus interfacing |
| 74LVC244 | Octal buffer with 3-state | For non-multiplexed signals only |
| 74LVC125 | Quad buffer with 3-state | Independent enables per channel |
| 74LVTH125 | Low-voltage quad buffer 3-state | Lower power version |
| 74LVC14 | Hex Schmitt-trigger inverter | Clean noisy input signals (good for /BAPR, /BIOXE) |
Open-Drain Drivers (for wired-OR signals)¶
| IC | Function | Notes |
|---|---|---|
| 74LVC07 | Hex non-inverting open-drain buffer | 5V tolerant, pulls LOW only -- ideal for /BINT, /BREQ |
| 74LVC1G07 | Single-gate version | When only 1-2 open-drain outputs needed |
| 74LS641 | Octal open-collector transceiver | Used in RaSCSI -- proven for legacy bus driving |
| SN75138 | Quad bus transceiver | Heavier drive for backplane |
Multiplexers (for muxing options)¶
| IC | Function | Notes |
|---|---|---|
| 74CB3T3257 | 4-bit FET mux/demux with voltage translation | Good for multiplexing 24-bit BD onto 8-bit MCU bus |
| 74LC257A | Quad 2-to-1 mux with 3-state | Lighter alternative |
| 74LVT138 | 3-to-8 line decoder | For chip select generation |
Port Expanders (for slow signals)¶
| IC | Interface | Notes |
|---|---|---|
| MCP23S08 | SPI, 8-bit | For non-time-critical control signals |
| 74HC595 | SPI shift register | For interrupt latch outputs |
| 74HC165 | SPI shift register | For interrupt status inputs |
Primary Recommendation: 74LVC245¶
For interfacing the RP2350B (3.3V) with the ND-100 5V bus, use the 74LVC245 as the default choice for bidirectional level shifting.
Key features: - Octal bus transceiver with 3-state outputs - 1.65V - 5.5V supply range - 5V-tolerant inputs at 3.3V -- safe to receive 5V bus signals while powered from 3.3V - Deterministic direction control via DIR pin (no auto-sensing surprises) - 3-6 ns propagation delay -- comfortable for the 50 ns BAPR window - Low-voltage CMOS, very fast - Widely available, proven in similar bus interface designs
The 74LVT245 is an acceptable alternative with slightly higher drive strength (better for heavy backplane loading) and used in ZuluSCSI.
The 74LC245A is similar but verify the specific variant supports 5V on the bus side before using.
The TXS0108E is not recommended for the BD bus due to auto-direction sensing variability that can cause issues on a multiplexed address/data bus.
Propagation Delay Comparison¶
The 50 ns BAPR address hold window is the tight constraint, but with hardware latches the level shifter delay only matters for: 1. The latch capture path (must complete within 50 ns of /BAPR leading edge) 2. The IDENT pass-through chain (~100 ns budget total across all daisy-chained cards)
/BAPR window (50 ns):
74LS240: 10-14 ns -> 36-40 ns left for latch setup OK
74LVC245: 3-6 ns -> 44-47 ns left Good
74LVT245: 3-5 ns -> 45-47 ns left Good
74AS648: 4-6 ns -> 44-46 ns left (5V only) 5V system
TXB0108: 1-2 ns -> 48-49 ns left Best
TXS0108E: 2.5-10 ns -> 40-47.5 ns left Marginal on dir change
IDENT pass-through (100 ns budget, multiple cards in chain):
74LVC245: 3-6 ns per card -> 16+ cards in chain Good
74LVT245: 3-5 ns per card -> 20+ cards Best
TXS0108E: up to 10 ns per card -> 10 cards max Risky
74LS240: 10-14 ns per card -> 7-10 cards max Tight
Recommendations Per Signal Type¶
BD 0-23 (input path -- bus to RP2350)¶
Recommended: 74LVC573 or 74LVC574 (octal latches), 3 chips for 24 bits.
- Clocked directly by /BAPR (inverted via 74LVC14 if needed for clean edge)
- Captures address/data instantly when /BAPR asserts
- 3-state outputs allow MCU to read via 8-bit shared bus with chip selects
- Output enable (/OE) controlled by RP2350 PIO state machine
/BD 0-7 ──> [74LVC573 #1] ──> 8-bit shared bus ──> RP2350 GPIO 12-19
/BD 8-15 ──> [74LVC573 #2] ──> ↑
/BD 16-23 ─> [74LVC573 #3] ──> ↑
↑
/BAPR ────────────┘ (clock all three latches)
/OE_0 ──┐
/OE_1 ──┼─> RP2350 PIO
/OE_2 ──┘
BD 0-23 (output path -- RP2350 to bus)¶
Recommended: 74LVT245 (3 chips for 24 bits), DIR fixed for output.
- Higher drive strength for backplane fan-out
- Proven in ZuluSCSI for similar use cases
- 3-5 ns propagation delay
- Output enable controlled by PIO
RP2350 GPIO 12-19 ──> [74LVT245 #1] ──> /BD 0-7
──> [74LVT245 #2] ──> /BD 8-15
──> [74LVT245 #3] ──> /BD 16-23
PIO STROBE_0/1/2 controls when each chip drives the bus
/OE_BUS controls when our card is allowed to drive at all
Wired-OR signals (/BREQ, /BINT 10/11/12/13, /BINPUT, /BDAP)¶
Recommended: 74LVC07 (hex open-drain buffer)
- Bus pull-ups (on CPU card) handle the HIGH state
- Our card pulls LOW to assert
- 5V tolerant input -- safe with /BAPR sniff
- Use 74LVC1G07 single-gate version if only 1-2 outputs needed
Input-only signals (/BIOXE, /BINACK, /BMEM, /INGRANT, /INIDENT, /BMCL)¶
Recommended: 74LVC14 (hex Schmitt-trigger inverter) or 74LVC125 (quad buffer 3-state)
- Schmitt trigger cleans up noisy edges from the backplane
- 5V tolerant inputs
- 3-6 ns delay
Daisy-chain outputs (/OUTGRANT, /OUTIDENT)¶
Recommended: 74LVT245 (single channel used) or 74LVC125 (quad buffer)
- Push-pull point-to-point (NOT wired-OR)
- 3-5 ns delay critical for IDENT chain accumulation
- Hardware default pass-through preferred (see below)
IDENT/GRANT Daisy-Chain Hardware Pass-Through¶
When the controller does NOT capture INGRANT or INIDENT, it must pass through with minimal delay. Software-driven pass-through via PIO would add ~10-20 ns per slot, which accumulates across the chain.
Recommended hardware bypass approach:
/INIDENT ──┬──> [74LVC125] ──> /OUTIDENT (default: pass through, ~3-5 ns)
│
└──> RP2350 PIO (sniff)
│
└─> If capture: assert /OE_BD to drive ident code
assert /BDRY
PIO blocks the pass-through buffer
This means INIDENT flows through to OUTIDENT automatically with single-buffer delay (~3-5 ns). The PIO state machine intercepts only when capture is needed by: 1. Disabling the pass-through buffer 2. Driving the ident code on BD via the output transceivers 3. Asserting /BDRY
Same approach for INGRANT/OUTGRANT.
Final IC Selection (Architecture B - PIO 8-bit chunked)¶
| Function | IC | Quantity | Notes |
|---|---|---|---|
| BD 0-23 input latches | 74LVC573 | 3 | Clocked by /BAPR, captures address within hardware timing |
| BD 0-23 output drivers | 74LVC245 (or 74LVT245) | 3 | DIR fixed to output, OE controlled by PIO. Primary choice 74LVC245 for 3.3V/5V interfacing |
| Bus phase signals (input) | 74LVC14 | 1 | Schmitt-trigger inverter for /BAPR, /BIOXE, /BINACK, /BMEM, /BMCL |
| Wired-OR outputs | 74LVC07 | 1 | Open-drain for /BREQ, /BINT 10/11/12, /BINPUT, /BDAP |
| Daisy-chain bypass | 74LVC125 | 1 | Hardware default pass-through for INGRANT->OUTGRANT, INIDENT->OUTIDENT |
| Chip select decoder | 74LVT138 | 1 | Optional -- generates /OE_0/1/2 from 2-bit MCU address |
Total external chips for bus interface: ~10 ICs (small SOIC-14/16 packages)
Note: The 74LVC245 is the primary choice for 3.3V to 5V level shifting in this design. Substitute 74LVT245 only if drive strength is needed for heavy backplane loading. Avoid TXS0108E on the BD bus path due to auto-direction sensing variability.
PIO Architecture¶
The bus protocol timing is too tight for software-only handling. PIO state machines handle the time-critical signal monitoring and response.
Suggested PIO state machine allocation¶
| SM | Role | Watches |
|---|---|---|
| SM0 | Address phase capture | /BAPR falling edge -> latch BD 0-23 |
| SM1 | IOX cycle handler | /BIOXE active + address match -> respond |
| SM2 | IDENT cycle handler | /BAPR + level on BD -> check own interrupts |
| SM3 | DMA cycle generator | Drives BD/BAPR/BMEM/BDAP/BDRY for DMA cycles |
| SM4 | DMA grant capture | /INGRANT + own /BREQ active -> capture |
| SM5 | Interrupt assertion | Drives /BINT 10/11/12/13 with proper timing |
| SM6-11 | Reserved | Future / per-device emulation logic |
Data flow¶
Bus -> Level shifters -> RP2350 GPIO -> PIO -> FIFO -> DMA -> RAM -> CPU task
|
v
CPU task -> RAM -> DMA -> PIO -> RP2350 GPIO -> Level shifters -> Bus
This keeps bus handling deterministic while device emulation runs in CPU tasks.
SD Card Support¶
The Olimex BB48R already has a microSD slot wired to the RP2350B on its bottom side (BB48R variant only -- the BB48 plain variant does not populate the slot). Our controller card does not need to add an SD card slot at all -- we just use the on-board one.
Wiring (already provided by the BB48R module)¶
| Pin | Signal | RP2350 GPIO | BB48R label |
|---|---|---|---|
| CLK | SD_CLK | GPIO10 | SPI1_SCK |
| CMD | SD_CMD | GPIO11 | SPI1_TX |
| DAT0 | SD_DAT0 | GPIO24 | SPI1_RX |
| CD/DAT3 | SD_CS | GPIO9 | SPI1_CSn |
Default mode is SPI, using the RP2350 hardware SPI1 peripheral. The Olimex schematic has 10 kΩ pull-ups on SPI1_CSn, SD_CMD, SD_CLK, and SD_DAT0 already populated. 1-bit MMC mode is also wired but you would have to drop the SPI peripheral and bit-bang/PIO it -- not worth the effort.
Driver¶
Use the pico-extras SD card library which is already SPI1-aware on this pin layout:
https://github.com/raspberrypi/pico-extras/tree/master/src/rp2_common/pico_sd_card
https://github.com/raspberrypi/pico-extras/blob/master/src/common/pico_sd_card/include/pico/sd_card.h
Performance with SPI1 + DMA at 25 MHz: ~1-3 MB/s real throughput. This is enough for floppy emulation (250 KB/s sustained), terminal disk images (negligible), and HDLC frame buffering (low-rate). It is marginal for SMD at sustained full throughput but adequate for typical access patterns.
If we ever need higher throughput¶
The BB48R only routes 4 SD pins (CLK/CMD/DAT0/CS), not the full 4-bit SDIO bus. We cannot switch to 4-bit SDIO on this module without adding external pin routing -- the DAT1/DAT2/DAT3 signals are not exposed. Options if SPI throughput becomes a bottleneck:
- Move the disk image to PSRAM -- 8 MB on-board, much faster than SPI SD. Useful for floppy images that fit entirely in PSRAM.
- Stream from the Pi Zero over SPI0 -- the Pi Zero has full SDIO and a microSD slot of its own. Bulk data can come over SPI0 from the Pi Zero file system at much higher rates.
- Move to a different RP2350 module that exposes all 4 SDIO data lines.
Electrical notes¶
- SD card is 3.3 V only -- direct connection to RP2350, no level shifter needed (already wired on the BB48R)
- Pull-ups already populated on the BB48R schematic (R19, R20, R21, R22, R23 = 10 kΩ to 3.3 V)
- SD card signals are isolated from the bus interface (different pins, different bank halves)
- No additional components needed on the controller card for SD support
Interrupt Handling¶
Interrupt assertion (controller -> CPU)¶
The controller must drive /BINT 10, /BINT 11, /BINT 12, and /BINT 13 based on which emulated device needs attention. These are wired-OR lines with pull-ups on the bus -- the controller drives them LOW via 74LVC07 open-drain buffers and releases (lets the pull-up float HIGH) to deassert.
Pin Allocation (locked in)¶
Each /BINT level gets its own dedicated GPIO. There is no shift-register or latch option in the V1 design -- direct drive is simplest, fastest, and the GPIO budget allows it. The chosen GPIOs straddle both banks:
| Signal | GPIO | Bank | Buffer |
|---|---|---|---|
| /BINT 10 | GPIO39 | HIGH | 74LVC07 (1 of 6) |
| /BINT 11 | GPIO40 | HIGH | 74LVC07 (1 of 6) |
| /BINT 12 | GPIO2 | LOW | 74LVC07 (1 of 6) |
| /BINT 13 | GPIO3 | LOW | 74LVC07 (1 of 6) |
Cost: 4 GPIO pins, 4 channels of one shared 74LVC07 (the same chip drives /BAPR_OUT, /BDRY_OUT, /BINPUT_OUT, /BDAP_OUT, /BREQ_OUT for the bidirectional control signals -- one 74LVC07 has 6 channels, so we use one full chip plus partial of a second).
GPIO2/GPIO3 are also the I2C1_SDA/SCL pins on the BB48R with 2.2 kΩ pull-ups to 3.3 V always present on the module. That is fine for our open-drain interrupt drive -- the pull-up just helps the input side of the 74LVC07.
IDENT PLxx response¶
When /INIDENT arrives via the daisy-chain, the controller must:
- Check if any of its emulated devices has an interrupt active on the level currently presented on BD 0-5
- If yes: capture INIDENT (do NOT pass through), place ident code on BD 0-23, assert /BDRY
- If no: pass INIDENT through to /OUTIDENT with minimal delay
The "minimal delay" requirement makes a hardware default-pass-through essential. Software-driven pass-through via PIO would add ~10-20 ns delay per slot, which accumulates across the chain.
Implemented approach: A 74LVC125 quad 3-state buffer sits between /INIDENT and /OUTIDENT (and a second channel between /INGRANT and /OUTGRANT). Default state has the buffer enabled, propagating /INIDENT → /OUTIDENT in ~3-5 ns -- well inside the 100 ns IDENT window. When the controller wants to capture the IDENT, it asserts /OE_DAISY_IDENT (GPIO47) which puts the buffer in high-Z, breaking the chain. /OUTIDENT then floats HIGH so the next slot sees the chain idle, and our card drives BD 0-23 + /BDRY for the IDENT response.
See the IDENT/GRANT Daisy-Chain Pass-Through Chip section earlier in this document for the full implementation details.
DMA Support¶
For SMD disk and HDLC emulation, the controller acts as a DMA master, performing memory read/write cycles directly to ND-100 memory.
DMA cycle generation¶
The controller's PIO state machine (SM3) drives the bus for DMA:
- Assert /BREQ (open-drain via 74LVC07)
- Wait for /INGRANT to arrive (with own BREQ active at /BMEM leading edge)
- Capture INGRANT (do not pass to OUTGRANT)
- Drive memory address on BD 0-23 + /BAPR
- For read: leave /BINPUT inactive, assert /BDAP, wait for /BDRY from memory, latch data
- For write: assert /BINPUT, drive write data on BD 0-23, assert /BDAP, wait for /BDRY
- Release /BREQ on /BDRY trailing edge
Throughput requirements¶
| Device | Word transfer rate | Notes |
|---|---|---|
| Floppy | ~50 KB/s | Slow, no problem |
| SMD disk | ~1-3 MB/s | Needs SDIO SD card option |
| HDLC | ~64 Kbit/s typical | Real-time constraint |
The 8 us bus cycle limit and toggled CPU/DMA priority means DMA gets approximately every other cycle, yielding ~125K word transfers/second peak per card.
Top-Level Schematic Block Diagram¶
This is the visual overview of the complete controller card showing all major subsystems and how they connect.
flowchart TB
subgraph BUS["ND-100 Bus (DIN 41612 C connector, 5V TTL)"]
BD["BD 0-23<br/>(24-bit multiplexed)"]
CTRL["Bus control<br/>BAPR BIOXE BMEM<br/>BDAP BDRY BINPUT<br/>BINACK BMCL"]
DMA_S["DMA chain<br/>BREQ INGRANT<br/>OUTGRANT"]
IDENT_S["IDENT chain<br/>INIDENT<br/>OUTIDENT"]
INT["Interrupts<br/>BINT 10/11/12/13"]
BUS_PWR["Bus +5V<br/>(pins 2/31)"]
BUS_GND["GND<br/>(pins 1/11/24/32)"]
end
subgraph LATCHES["BD Bus Interface (SMD)"]
IN_LATCH["3x 74LVC574<br/>Input latches<br/>clocked by /BAPR"]
OUT_LATCH["3x 74LVC574<br/>Output latches<br/>+ 3x 74LVT245<br/>Output drivers"]
DAISY["74LVC125<br/>IDENT/GRANT<br/>pass-through"]
end
subgraph BUFFERS["Signal Buffers (SMD)"]
IN_BUF["74LVC14<br/>Schmitt input<br/>buffers (sniffs)"]
OUT_BUF["74LVC07<br/>Open-drain<br/>output drivers"]
end
subgraph BB48R["Olimex RP2350-PICO2-BB48R (socketed)"]
RP["RP2350B<br/>Dual M33<br/>+ 12 PIO SMs<br/>+ DMA"]
SRAM["520 KB SRAM"]
FLASH["16 MB QSPI Flash"]
PSRAM["8 MB PSRAM<br/>(GPIO8 CS)"]
SD["MicroSD card<br/>(GPIO9-11, 24)"]
USBC["USB-C<br/>(programming +<br/>virtual COM)"]
BTN["BOOT + RESET<br/>buttons"]
end
subgraph PIZERO["Raspberry Pi Zero 2 W (header, optional)"]
PIZ_CPU["Quad Cortex-A53<br/>1 GHz + 512 MB RAM"]
PIZ_WIFI["WiFi + BT<br/>BCM43436"]
PIZ_SD["microSD<br/>(Linux + apps)"]
PIZ_USB["USB OTG +<br/>USB PWR IN"]
end
subgraph POWER["Power Distribution"]
F1["F1: 2A polyfuse"]
F2["F2: 2A polyfuse"]
D1["D1: SS14 Schottky<br/>(BB48R OR-ing)"]
D2["D2: LTC4412<br/>ideal diode<br/>(Pi Zero OR-ing)"]
BULK["Bulk caps<br/>1000uF + 470uF<br/>+ 10uF + 0.1uF"]
TVS["TVS5.0A clamp"]
end
BUS_PWR -- "+5V" --> F1
F1 -- "+5V_LOCAL" --> D1
F1 --> F2
D1 -- "VDD_SYS<br/>(when no USB)" --> BB48R
USBC -- "5V<br/>(when connected)" --> BB48R
F2 --> D2
D2 -- "+5V to Pi Zero<br/>header pin 2/4" --> PIZERO
BULK --> PIZERO
TVS --> PIZERO
BB48R -- "3.3V output<br/>(2A from DCDC)" --> BUFFERS
BB48R -- "3.3V" --> LATCHES
BD <--> IN_LATCH
BD <--> OUT_LATCH
CTRL --> IN_BUF
CTRL <-- OUT_BUF
INT <-- OUT_BUF
DMA_S <-- OUT_BUF
DMA_S <--> DAISY
IDENT_S <--> DAISY
IN_LATCH -- "8-bit shared<br/>DBUS GPIO12-19" --> RP
OUT_LATCH -- "8-bit shared<br/>DBUS GPIO12-19" --> RP
RP -- "/OE_IN_0/1/2<br/>LE_OUT_0/1/2" --> IN_LATCH
RP -- "/BD_OE_BUS" --> OUT_LATCH
IN_BUF -- "GPIO20-23 (PIO)<br/>+ GPIO33-38 (sniff)" --> RP
OUT_BUF <-- "GPIO2-3 (BINT12/13)<br/>+ GPIO39-45 (BINT10/11+drives)" --- RP
DAISY <-- "GPIO46-47" --- RP
RP <-- "SPI0 + INT<br/>GPIO0-1, 4-7" --> PIZERO
BUS_GND -.-> POWER
BUS_GND -.-> BB48R
BUS_GND -.-> PIZERO
BUS_GND -.-> LATCHES
BUS_GND -.-> BUFFERS
style BUS fill:#FFF3E0,stroke:#E65100,color:#E65100
style BB48R fill:#E3F2FD,stroke:#0D47A1,color:#0D47A1
style PIZERO fill:#F3E5F5,stroke:#7B1FA2,color:#7B1FA2
style LATCHES fill:#E0F7FA,stroke:#00838F,color:#00838F
style BUFFERS fill:#E0F7FA,stroke:#00838F,color:#00838F
style POWER fill:#E8F5E9,stroke:#2E7D32,color:#2E7D32
Block Description¶
| Block | Components | Purpose |
|---|---|---|
| ND-100 Bus | DIN 41612 C connector (96-pin, male right-angle on PCB) | Physical connection to backplane |
| BD Bus Interface (SMD) | 3x 74LVC574 input latches, 3x 74LVC574 output latches, 3x 74LVT245 drivers, 74LVC125 daisy-chain bypass | 24-bit BD bus level shifting and latching (Design 2) |
| Signal Buffers (SMD) | 74LVC14 Schmitt inverters (input), 74LVC07 open-drain (output) | 5V to 3.3V level shifting for control signals |
| Olimex BB48R (socketed) | RP2350B + 16MB flash + 8MB PSRAM + microSD slot + USB-C + buttons | Main MCU subsystem -- bus protocol, device emulation, firmware |
| Pi Zero 2 W (header, optional) | Quad Cortex-A53 + WiFi + Linux + microSD | Wireless companion -- nd100x emulator host, terminal server, HDLC bridge |
| Power Distribution (SMD) | F1/F2 polyfuses, D1 SS14, D2 LTC4412 ideal diode, bulk caps, TVS clamp | Source OR-ing, fault protection, decoupling |
Physical Layout (Card Top View)¶
+================================================+
| |
| +========================+ +-----------+ |
| | Olimex BB48R | | F1 D1 F2 | |
| | (socketed, removable) | | D2 caps | |
| | | | TVS | |
| | USB-C, BOOT/RESET | | (Power | |
| | facing edge | | zone) | |
| +========================+ +-----------+ |
| |
| +-----------+ +-----------+ +-----------+ |
| | 3x 74LVC574| | 3x 74LVC574| | 3x 74LVT245| |
| | input | | output | | drivers | |
| | latches | | latches | | | |
| +-----------+ +-----------+ +-----------+ |
| |
| 74LVC14 74LVC07 74LVC125 |
| buffers open-dr daisy bypass |
| |
| Status LEDs (multiple, around perimeter) |
| |
| +============================+ |
| | Pi Zero 40-pin header | |
| | (2x20, M2.5 standoffs) | |
| | Pi Zero stacks here | |
| +============================+ |
| |
+============== C connector (DIN41612) ==========+
bus pins facing the backplane edge
Subsystem Power and Ground¶
| Subsystem | Power Source | Approx Current |
|---|---|---|
| BB48R (RP2350B + flash + PSRAM) | Bus 5V via D1, OR USB-C | ~150 mA @ 3.3V (internal) |
| BB48R 3.3V output → level shifters | BB48R DCDC | ~50-200 mA |
| Level shifters and latches | BB48R 3.3V | (see above) |
| Pi Zero 2 W (when populated) | Bus 5V via F2 + D2 | 250-700 mA, peaks ~1A |
| Status LEDs | BB48R 3.3V or 5V via resistors | ~10-20 mA total |
| Total bus 5V draw (Pi Zero populated, WiFi active) | -- | ~1.0-1.3 A |
| Total bus 5V draw (no Pi Zero) | -- | ~200 mA |
The bus +5V (multiple pins, ~2-3A per pin) handles this easily.
Signal Flow Summary¶
ND-100 CPU on bus
↓
BD/CTL/INT signals (5V active LOW)
↓
Level shifters (74LVC family)
↓
Latches capture on /BAPR
↓
PIO state machines (8-bit reads)
↓
DMA → central registers in PSRAM
↓
Core 0 bus protocol handler
↓
Core 1 device emulation (floppy, SMD, terminal, HDLC)
↓
(optional) SPI to Pi Zero for network/full emulator
Power Distribution Design¶
The controller card needs to power three things:
- Olimex BB48R (RP2350B + flash + PSRAM + SD)
- Raspberry Pi Zero (when populated -- ~250-500 mA)
- Level shifters and discrete logic (small consumption, ~50-200 mA at 3.3V)
Power comes from the ND-100 bus +5V rail (bus pins A2/A31, B2/B31, C2/C31). The bus has GND on pins A1/A11/A24/A32, B1/B11/B24/B32, C1/C11/C24/C32.
Power Architecture¶
ND-100 Bus
+5V (pin 2/31) ──┬── F1 (1A polyfuse) ── +5V_LOCAL ──┬── D1 (Schottky) ── BB48R VDD_SYS (EXT2 pin 2)
│ │
│ ├── F2 (1A polyfuse) ── Pi Zero +5V (header pin 2/4)
│ │
│ └── (used by anything else needing 5V)
GND (pin 1/...) ─┴────────────────────────────── GND plane
BB48R USB-C ─────── Internal USB power ─── BB48R VDD_SYS (internally OR'd with our D1)
BB48R EXT1 pin 1 (3.3V output, 2A) ──── +3.3V_LOCAL ──── all level shifters, latches, glue logic
(74LVC245, 74LVC14, 74LVC07,
74LVC125, 74LVC574, 74LVT245)
Power Source OR-ing (Critical)¶
The Olimex manual warns: ⚠ "If you want to use VDD_SYS as input to feed power from external 5V to this line, make sure board is not connected to USB!"
This means we CANNOT directly tie bus 5V to VDD_SYS while USB-C may be plugged in (during programming, debug, or virtual COM port use). Two power sources fighting causes unpredictable damage.
Solution: Schottky diode OR-ing. A Schottky diode between bus +5V and VDD_SYS prevents backflow:
+5V_LOCAL ──>|── BB48R VDD_SYS
(D1 = 1N5817 or SS14, anode on bus side, cathode on VDD_SYS)
How it works:
| State | USB Connected | Bus Powered | D1 State | BB48R Power Source |
|---|---|---|---|---|
| Bus only | No | Yes (5V) | Forward (Vd ~0.3V) | Bus → D1 → VDD_SYS @ 4.7V |
| USB only | Yes (5V) | No | Reverse (blocked) | USB → BB48R internal → VDD_SYS @ 5V |
| Both connected | Yes (5V) | Yes (5V) | Reverse (USB wins) | USB → BB48R internal → VDD_SYS @ 5V (D1 blocks 4.7V from bus) |
| Card removed | No | No | -- | BB48R off |
Why USB wins when both connected: USB provides 5V directly. Bus 5V passes through D1 with ~0.3V drop = 4.7V on the anode side (post-diode). USB side is 5V > 4.7V, so D1 is reverse-biased and blocks. The BB48R uses USB power.
Diode selection: - 1N5817 (axial through-hole) -- 1A, 20V, ~0.32V Vf at 1A, ~$0.20 - SS14 (SMD SMA package) -- 1A, 40V, ~0.4V Vf at 1A, ~$0.10 - MBR0520LT1G (SMD SOD-123) -- 0.5A, 20V, ~0.385V Vf, ~$0.10 - SS24 (SMD SMA) -- 2A version of SS14 if higher current needed
Recommended: SS14 in SMA package -- standard, cheap, fits the SMD theme of the rest of the design.
Pi Zero Power (Including WiFi TX Bursts)¶
The Pi Zero has two micro USB ports: - PWR IN (left, labeled "PWR") -- power input only, no data - USB OTG (middle, labeled "USB") -- USB On-The-Go for data + power
Both ports can supply 5V to the Pi Zero. The Pi Zero accepts power from any of these sources: 1. Micro USB PWR IN (for standalone use with a USB power supply) 2. Micro USB OTG (when connected to a host PC for development) 3. GPIO header pins 2 or 4 (+5V) -- our backplane power path
CONFIRMED FROM RASPBERRY PI FORUMS: The micro USB power inputs and GPIO header pins 2/4 (5V) are directly in parallel on the Pi Zero PCB -- no protection diodes, no polyfuse, no isolation. This is true for all Pi Zero variants including Pi Zero 2 W.
Source: Raspberry Pi Forums - Sanity check: Powering a Zero 2 W through GPIO
Quote (Burngate, experienced forum contributor): "The USB 5v and gnd pins are in parallel with the ones on the header, so as long as the same supply is used for both Pis, no damage will occur. (Different supplies with slightly different voltages could mean large currents flowing between them, which could damage the supplies and the Pis)"
Implications:
- Pi Zero has NO power input protection -- no Schottky diode, no polyfuse on the +5V input
- The micro USB PWR IN, USB OTG, and GPIO header pins 2/4 all share the same +5V net
- If two different power sources are connected (e.g., bus 5V via our PCB + USB cable to PC), current flows between them
- This can damage the bus PSU, the PC's USB port, and the Pi Zero itself
Therefore: We MUST add an isolation diode (D2) on our PCB to prevent back-feeding between bus power and any Pi Zero USB power input.
Power Scenario Matrix¶
| Pi Zero PWR IN | Pi Zero USB OTG | GPIO Header +5V (from our PCB) | Result |
|---|---|---|---|
| Connected to PSU | -- | -- | Pi Zero powered from PWR IN |
| -- | Connected to PC | -- | Pi Zero powered from OTG |
| -- | -- | Bus 5V via our PCB | Pi Zero powered from header (our normal mode) |
| -- | Connected to PC | Bus 5V | Both sources connected -- generally works (Pi Zero internal OR-ing), but may back-feed from PC USB to bus if no protection |
| Connected to PSU | Connected to PC | Bus 5V | All three sources -- must ensure no conflicts |
The risk: If we feed bus 5V to the Pi Zero header pin 2 while a USB host (PC) is also connected to the Pi Zero OTG port, the Pi Zero internal +5V rail is at ~5V from both sources. Current can flow back through the GPIO header into our +5V_LOCAL rail and from there back to the bus. Depending on the bus PSU's tolerance for source current, this can cause issues.
Solution: Schottky Diode on Pi Zero +5V Feed¶
Add a Schottky diode D2 between +5V_LOCAL and the Pi Zero header +5V pins:
+5V_LOCAL ── F2 (2A polyfuse) ── D2 (Schottky, e.g., SS24) ── Pi Zero header pin 2 + pin 4
D2 behavior:
| State | Pi Zero Power Source | D2 |
|---|---|---|
| Bus only | Bus → F2 → D2 → Pi Zero @ 4.7V (after Vf drop) | Forward |
| Pi Zero PWR IN connected | PWR IN → Pi Zero internal 5V (5V) | Reverse-blocked at 4.7V vs 5V |
| Pi Zero OTG connected | USB host → Pi Zero internal 5V (5V) | Reverse-blocked |
| Bus + PWR IN both | Pi Zero from PWR IN (5V wins), bus blocked by D2 | Reverse |
| All three connected | Highest of (PWR IN, OTG) wins, bus blocked | Reverse |
D2 prevents back-feeding from the Pi Zero's USB power into our +5V_LOCAL rail.
D2 selection: SS24 (2A, 40V, SMA) -- needs to handle the same ~1A peaks as the polyfuse. SS14 (1A) might be marginal during WiFi TX bursts. Use SS24 for the Pi Zero feed.
Voltage Drop Concern¶
Adding D2 introduces another ~0.4V drop. With both F2 polyfuse and D2 diode in series: - Bus +5V (~4.95V from typical PSU) - After F1 polyfuse: ~4.90V - After F2 polyfuse: ~4.85V - After D2 Schottky (SS24, ~0.35V Vf at 1A): ~4.50V at Pi Zero header
The Pi Zero specifies +5V minimum 4.75V. 4.50V is below spec.
Options to fix:
-
Use ideal diode IC instead of Schottky -- e.g., LTC4412 or MAX40200 -- voltage drop ~10-50 mV
- LTC4412: ideal diode controller + P-MOSFET, very low drop (~30 mV at 1A), ~$2-3
- MAX40200: 1A ideal diode in single chip, ~$1
-
Use a P-MOSFET load switch with reverse current protection -- ~$1-2
-
Skip the diode -- accept that the Pi Zero may back-feed power if USB is connected. The +5V_LOCAL rail is shared with the BB48R VDD_SYS through D1. If the Pi Zero feeds 5V back into +5V_LOCAL, D1 prevents backflow to BB48R VDD_SYS (already protected), and the bus PSU can absorb the small back-fed current.
-
Tell the user: "Don't connect the Pi Zero's USB ports while the card is in the bus". Simple but error-prone.
Recommendation: Use an ideal diode IC (LTC4412 or MAX40200) for D2. The ~$1-2 cost is worth it to: - Maintain proper 5V at the Pi Zero (4.85V instead of 4.50V) - Prevent back-feeding - Allow safe simultaneous use of bus power and Pi Zero USB (e.g., for headless monitoring while developing)
Updated Power Path¶
Bus +5V ── F1 (2A polyfuse) ── +5V_LOCAL ──┬── D1 (SS14) ───────── BB48R VDD_SYS
│
└── F2 (2A polyfuse) ── D2 (LTC4412 ideal diode) ── Pi Zero header pin 2/4
│
└── 1000 uF + 470 uF + 10 uF + 0.1 uF caps
Pi Zero PWR IN ─────┐
├── (internal Pi Zero +5V net)
Pi Zero USB OTG ─────┘
The ideal diode D2 prevents back-feeding from Pi Zero USB inputs to our PCB. The Pi Zero's onboard ESD/bulk caps handle the local decoupling.
The Pi Zero 2 W can draw significant current spikes during WiFi transmission. The power design must handle these without: - Polyfuse tripping - Voltage droop causing brownout/reset - IR drop on PCB traces
Pi Zero 2 W Power Profile¶
| State | Current |
|---|---|
| Idle (no WiFi traffic) | ~80-150 mA |
| Linux running, light load | ~250 mA |
| Boot inrush | ~500 mA (~1 second) |
| WiFi RX active | ~350-450 mA |
| WiFi TX active (sustained) | ~500-700 mA |
| WiFi TX bursts (peak) | ~700-1000 mA (microsecond spikes) |
| Heavy CPU + WiFi TX worst case | up to ~1.2 A peak |
The BCM43436 WiFi chip alone can draw 300-500 mA during TX bursts on top of the Cortex-A53 base load.
Updated Power Path (WiFi-Capable)¶
+5V_LOCAL ── F2 (2A polyfuse) ── wide trace ── Pi Zero header pin 2 + pin 4
│
├── 1000 uF aluminum polymer (low ESR)
├── 470 uF tantalum (handles boot inrush)
├── 10 uF ceramic
└── 0.1 uF ceramic (high freq)
Changes from original:
| Item | Original | Updated | Why |
|---|---|---|---|
| Polyfuse F2 | 1A | 2A | 2A polyfuse trips at ~4A and holds 2A continuously, gives margin for ~1A WiFi TX peaks |
| Bulk capacitance | 470 uF tantalum only | 1000 uF aluminum polymer + 470 uF tantalum + 10 uF + 0.1 uF | Multi-stage decoupling handles low ESR, mid-freq, and high-freq TX bursts |
| Trace width | Standard | Min 1.5 mm, recommended 2-3 mm | Carries ~1A peaks without IR drop |
| Power path length | -- | Keep < 30 mm from polyfuse to Pi Zero pin | Minimize trace resistance |
| Use both 5V pins | One pin | Pin 2 AND pin 4 (parallel) | Two parallel pins reduce contact resistance and trace impedance |
Pi Zero GPIO Header Power Pins¶
The Raspberry Pi Zero (and Pi Zero W / Zero 2 W) 40-pin GPIO header exposes power pins:
| Pin | Function | Direction | Notes |
|---|---|---|---|
| Pin 1 | 3.3V | OUTPUT from Pi Zero's onboard 3.3V regulator (~150 mA available) | Do not connect to our +3.3V_LOCAL -- we use BB48R's 3.3V |
| Pin 2 | 5V | Input or output -- same net as USB power | Connect from our +5V via D2 ideal diode |
| Pin 4 | 5V | Same as pin 2 (parallel) | Connect from our +5V via D2 |
| Pin 6 | GND | -- | Connect to GND plane |
| Pin 9 | GND | -- | Connect to GND plane |
| Pin 14 | GND | -- | Connect to GND plane |
| Pin 17 | 3.3V | OUTPUT (same net as pin 1) | Do not connect |
| Pin 20 | GND | -- | Connect to GND plane |
| Pin 25 | GND | -- | Connect to GND plane |
| Pin 30 | GND | -- | Connect to GND plane |
| Pin 34 | GND | -- | Connect to GND plane |
| Pin 39 | GND | -- | Connect to GND plane |
Critical: Pins 1 and 17 are 3.3V outputs from the Pi Zero's onboard regulator. Do NOT connect these to our +3.3V_LOCAL rail. Doing so would back-feed power between two regulators (BB48R DCDC and Pi Zero LDO), causing instability.
Our +3.3V_LOCAL comes from the BB48R's 3.3V output (2A capacity). The Pi Zero generates its own 3.3V internally. Two separate 3.3V rails -- they share GND but no other connection.
Use BOTH 5V pins (pin 2 AND pin 4): They are the same +5V net on the Pi Zero, but using both header pins in parallel reduces contact resistance and provides redundancy. The 0.1" header contacts are rated ~1A each, so two parallel = 2A capacity.
GND pins: All 8 GND pins (6, 9, 14, 20, 25, 30, 34, 39) should connect to the GND plane on our PCB. Multiple parallel ground connections reduce ground bounce and EMI.
Capacitor Selection for WiFi TX Bursts¶
Multi-stage decoupling is essential for WiFi modules:
| Cap | Value | Type | Purpose |
|---|---|---|---|
| Bulk | 1000 uF | Aluminum polymer (low ESR ~10 mOhm) | Handles ms-scale TX current bursts |
| Bulk | 470 uF | Tantalum (ESR ~50 mOhm) | Handles boot inrush + slow load steps |
| Mid | 10 uF | Ceramic X5R/X7R 1206 | Handles us-scale spikes |
| HF | 0.1 uF | Ceramic X7R 0603 | Handles ns-scale switching noise |
Place all caps as close as possible to the Pi Zero +5V pin (within ~10 mm).
Alternative all-ceramic option (no electrolytics, smaller, longer life): - 4x 100 uF ceramic 1210 (X5R) in parallel = 400 uF total - Plus 10 uF + 0.1 uF - Slightly lower bulk capacity but much better ESR and longevity
Trace Width Calculation¶
For ~1A peak current at 5V on 1 oz copper: - 0.5 mm trace = ~1.5 A capacity, ~150 mOhm/m, drops ~50 mV at 1A over 30mm -- acceptable - 1 mm trace = ~3 A capacity, ~75 mOhm/m, drops ~25 mV at 1A over 30mm -- good - 2 mm trace = ~5 A capacity, ~37 mOhm/m, drops ~12 mV at 1A over 30mm -- excellent
Recommendation: 2 mm trace width for the +5V path from polyfuse to Pi Zero. Use a wide pour (3+ mm) if PCB area allows.
Use multiple vias if changing layers (each via has ~10 mOhm resistance and ~1A capacity).
Polyfuse Behavior¶
Polyfuses don't trip instantly -- they're thermal devices. A 2A polyfuse: - Holds 2A indefinitely - Trips at ~4A continuous - Allows brief peaks above the trip current (microseconds to milliseconds) - Once tripped, stays tripped until cooled (~30 sec to minutes)
For WiFi TX bursts (~1A peaks lasting microseconds), a 2A polyfuse won't trip even though the peak briefly exceeds the hold current.
Sustained heavy traffic (continuous ~700 mA) is well below the 2A hold current.
Catastrophic short (>4A) trips the polyfuse, protecting the bus.
Recommended polyfuse: 2A, 6V, 0805 SMD (e.g., MF-MSMF200/6 or MF-NSMF200)
Bus +5V Capacity¶
The ND-100 bus has +5V on pins A2, A31, B2, B31, C2, C31 (6 pins total). DIN 41612 contacts are rated 2-3 A per pin, so the bus can supply 12-18 A combined. A Pi Zero drawing 1A is trivial -- the bus handles it easily.
The backplane PSU (PC ATX PSU) typically provides 5-20 A on the +5V rail, more than enough for multiple Pi Zeros across multiple controller cards.
Voltage Droop Analysis¶
| Component | Voltage Drop at 1A peak |
|---|---|
| Bus connector resistance | ~10 mV (multiple parallel pins) |
| Polyfuse F1 (in series) | ~50 mV (typical R when cold) |
| Polyfuse F2 (in series) | ~100 mV (smaller, ~50-100 mOhm) |
| 30 mm of 2 mm trace | ~12 mV |
| Total drop | ~170 mV |
5V bus → ~4.83V at Pi Zero pin during 1A peak. The Pi Zero accepts +5V ±10% (4.5V minimum), so 4.83V is well within spec.
Bulk caps absorb the actual transient -- the trace and polyfuse drops are quasi-static, not instantaneous. The 1000 uF cap supplies the burst, then recharges from the +5V rail at lower current.
Brownout Protection¶
If the bus +5V briefly droops (e.g., another card on the same backplane has a power glitch), the bulk caps on our card supply the Pi Zero for several milliseconds. The Pi Zero won't reset unless +5V drops below ~4V for > 50 ms.
For extra safety, add a TVS diode (e.g., SMBJ5.0A) on the +5V_LOCAL rail to clamp any voltage spikes.
Updated Power Components (WiFi-Capable)¶
| Item | Quantity | Cost |
|---|---|---|
| D1: SS14 Schottky diode | 1 | $0.10 |
| F1: 2A polyfuse (bus 5V protection) | 1 | $0.30 |
| F2: 2A polyfuse (Pi Zero protection) | 1 | $0.30 |
| C_PiZero_bulk_low_ESR: 1000 uF aluminum polymer | 1 | $0.80 |
| C_PiZero_bulk_tantalum: 470 uF tantalum | 1 | $0.40 |
| C_PiZero_mid: 10 uF ceramic 1206 | 1 | $0.05 |
| C_PiZero_hf: 0.1 uF ceramic 0603 | 1 | $0.01 |
| TVS5.0A: TVS diode (5V clamp) | 1 | $0.20 |
| 0.1 uF decoupling caps (other chips) | ~15 | $0.30 |
| 10 uF bulk caps (other) | ~5 | $0.30 |
| 47 uF bus input cap | 1 | $0.20 |
| Total power components | ~$3.00 |
About $1.30 more than the original budget but handles WiFi TX properly.
3.3V for Level Shifters and Logic¶
We do NOT need a separate 3.3V LDO. The BB48R generates 3.3V at up to 2A from its internal DCDC and exposes it on EXT1 pin 1. Our level shifters and discrete logic chips (74LVC family) draw together ~50-200 mA at 3.3V -- well within the BB48R's 2A budget.
3.3V wiring:
BB48R EXT1 pin 1 (3.3V output) ──── +3.3V_LOCAL plane on PCB
├── all 74LVC245 VCC
├── all 74LVC574 VCC
├── 74LVC14 VCC
├── 74LVC07 VCC
├── 74LVC125 VCC
├── 74LVT245 VCC
└── decoupling caps (0.1uF per chip + 10uF bulk)
3.3V budget estimate:
| Consumer | Typical | Max |
|---|---|---|
| 6x 74LVC chips (latches, transceivers) | ~30 mA | ~60 mA |
| 3x 74LVT245 output drivers | ~30 mA | ~75 mA |
| Pull-up resistor networks | ~5 mA | ~10 mA |
| Status LEDs (a few mA each) | ~10 mA | ~20 mA |
| BB48R itself (internal) | ~80 mA | ~150 mA |
| Total | ~155 mA | ~315 mA |
Well within the BB48R's 2A 3.3V output capability.
Ground Plane¶
A continuous GND plane under all chips provides low-impedance return paths. Connect to bus GND on pins A1, A11, A24, A32, B1, B11, B24, B32, C1, C11, C24, C32 (multiple parallel connections reduce inductance).
Pi Zero GND header pins 6, 9, 14, 20, 25, 30, 34, 39 all tie to the same GND plane.
Decoupling Capacitors¶
| Location | Cap |
|---|---|
| Each 74LVC/74LVT chip VCC pin | 0.1 uF (0603 SMD) |
| Bulk decoupling near chips | 10 uF (0805 SMD) |
| Pi Zero +5V near header | 470 uF tantalum (handles boot inrush) |
| BB48R 3.3V output | 10 uF + 0.1 uF |
| Bus +5V input | 47 uF + 0.1 uF |
Power Components Summary¶
| Item | Quantity | Cost |
|---|---|---|
| D1: Schottky diode (SS14 SMA) | 1 | $0.10 |
| F1: 1A polyfuse (bus 5V protection) | 1 | $0.20 |
| F2: 1A polyfuse (Pi Zero protection) | 1 | $0.20 |
| 0.1 uF decoupling caps (one per chip) | ~15 | $0.30 |
| 10 uF bulk caps | ~5 | $0.30 |
| 47 uF bus input cap | 1 | $0.20 |
| 470 uF tantalum (Pi Zero) | 1 | $0.40 |
| Total power components | ~$1.70 |
Power Distribution Verification¶
| Scenario | BB48R Power | Pi Zero Power | Logic Power | Status |
|---|---|---|---|---|
| Bus only, no Pi Zero | Bus → D1 → VDD_SYS (4.7V) | -- | BB48R 3.3V | ✓ |
| Bus only, Pi Zero populated | Bus → D1 → VDD_SYS (4.7V) | Bus → F2 → +5V (5V) | BB48R 3.3V | ✓ |
| USB only (no bus -- bench test) | USB → VBUS → VDD_SYS (5V) | None (bus 5V missing) | BB48R 3.3V | ⚠ Pi Zero off (expected) |
| Both USB and bus, no Pi Zero | USB wins (D1 reverse) → 5V | -- | BB48R 3.3V | ✓ |
| Both USB and bus, Pi Zero populated | USB wins → BB48R 5V | Bus → F2 → 5V (independent) | BB48R 3.3V | ✓ |
The Pi Zero is powered from the bus only -- it does NOT have a USB-C connection and depends entirely on bus power.
Reset Signal¶
| Signal | Source | Notes |
|---|---|---|
| /BMCL | Bus pin B20 | Reset input -- use to trigger BB48R reset |
The /BMCL signal is the bus master clear. The controller must reset all device emulation state when /BMCL is asserted. The BB48R reads /BMCL via GPIO35.
The Pi Zero is not directly affected by /BMCL -- it's a Linux system that doesn't reset on bus events. The BB48R can optionally signal the Pi Zero via INT_FROM_BB48R or via a dedicated reset line if needed.
Power-Up Safe State and Default Pull Resistors¶
When the controller card is powered up, the RP2350 takes ~50-200 ms to boot, configure GPIOs, and start the firmware. During this window:
- All RP2350 GPIOs are inputs (high-Z) by default
- The card must not drive the bus (would corrupt CPU operation)
- The IDENT and GRANT daisy chains must keep working (otherwise lower-priority cards lose interrupts and DMA grants)
- All wired-OR outputs must stay released (not pulled LOW)
This is achieved by carefully placing pull-up and pull-down resistors on the buffer chip enable pins and MCU GPIO lines. The pull resistors ensure the correct default state at power-up before the MCU takes control.
Pull Resistor Strategy by Signal Type¶
Daisy-chain pass-through (74LVC125 /OE pins)¶
The pass-through must be enabled by default so the IDENT and GRANT chains work the moment power is applied -- before the MCU boots. The 74LVC125 has active-LOW enable.
| Pin | Pull | Default State |
|---|---|---|
| /OE_IDENT_PASS (74LVC125 ch.1+2 /OE) | Pull-DOWN to GND (10K) | Enabled at power-up: INIDENT passes to OUTIDENT |
| /OE_GRANT_PASS (74LVC125 ch.3+4 /OE) | Pull-DOWN to GND (10K) | Enabled at power-up: INGRANT passes to OUTGRANT |
After boot, the MCU drives these pins HIGH only when it wants to capture (block pass-through) for an active IDENT response or DMA cycle. The pull-down keeps them LOW when MCU GPIO floats during reset.
BD bus output drivers (74LVC245 /OE in output mode)¶
The card must not drive the bus at power-up. The 74LVC245 has active-LOW output enable.
| Pin | Pull | Default State |
|---|---|---|
| /BD_OE_BUS (74LVC245 /OE for output) | Pull-UP to 3.3V (10K) | Disabled at power-up: BD lines high-Z (safe) |
After boot and address decode, the MCU drives this LOW only during the data phase when responding to an IOX cycle directed at us, or during DMA when we're the bus master.
Output latch strobes (Design 2: LE_OUT_0/1/2)¶
The output latches must not latch garbage data at power-up. 74LVC573 latch enable is active-HIGH.
| Pin | Pull | Default State |
|---|---|---|
| LE_OUT_0/1/2 (latch enable) | Pull-DOWN to GND (10K) | LOW at power-up: latches hold no/old data |
Input latch read enables (Design 2: /OE_IN_0/1/2)¶
The input latches' /OE controls when they drive the shared 8-bit DBUS. Must default to not driving to avoid contention.
| Pin | Pull | Default State |
|---|---|---|
| /OE_IN_0/1/2 | Pull-UP to 3.3V (10K) | HIGH at power-up: latches are high-Z |
Wired-OR open-drain outputs (74LVC07 inputs from MCU)¶
For signals like /BREQ, /BINT 10/11/12, /BDRY drive, /BINPUT drive, /BDAP drive, the 74LVC07 input must be HIGH to keep its open-drain output OFF (bus line released).
| MCU GPIO | Pull | Default State |
|---|---|---|
| /BREQ drive | Pull-UP to 3.3V (10K) | HIGH = 74LVC07 OFF = bus line released |
| /BINT 10 drive | Pull-UP to 3.3V (10K) | Same -- no spurious interrupt |
| /BINT 11 drive | Pull-UP to 3.3V (10K) | Same |
| /BINT 12 drive | Pull-UP to 3.3V (10K) | Same |
| /BDRY drive | Pull-UP to 3.3V (10K) | Same -- not asserting BDRY |
| /BINPUT drive | Pull-UP to 3.3V (10K) | Same |
| /BDAP drive | Pull-UP to 3.3V (10K) | Same |
SPI shift register strobes (Design 3)¶
| Pin | Pull | Default State |
|---|---|---|
| /PL_LOAD (74LVC165) | Pull-UP to 3.3V (10K) | HIGH = no parallel load |
| LATCH_OUT (74LVC595) | Pull-DOWN to GND (10K) | LOW = no output update |
| SPI_SCK | Pull-DOWN to GND (10K) | LOW = clock idle |
| /BD_OE_BUS | Pull-UP to 3.3V (10K) | Same as Design 1/2 |
SD card SPI¶
| Pin | Pull | Default State |
|---|---|---|
| /SD_CS | Pull-UP to 3.3V (10K) | HIGH = SD card not selected |
Master Bus Drive Enable (Optional Safety Feature)¶
A single BUS_DRIVE_OK signal can gate ALL bus output drivers. This adds belt-and-suspenders safety:
Pull-down to GND (default LOW = drivers disabled)
RP2350 GPIO drives HIGH after firmware completes initialization
/BMCL (active LOW) AND'd in to force LOW during bus reset
BUS_DRIVE_OK = (RP2350_READY) AND (not /BMCL_active)
This signal feeds the /OE pins of: - 74LVC245 BD output drivers - 74LVC07 wired-OR drivers (via series gate or AND logic)
When BUS_DRIVE_OK is LOW, all card outputs are isolated from the bus.
Implementation note: The simplest version is just a pull-down on a single GPIO that enables all output stages. The MCU drives it HIGH only after firmware boot is complete and address decoding is configured. /BMCL can be wire-OR'd via diode to force the line LOW during bus reset.
Reset Behavior¶
When /BMCL is asserted on the bus:
- The RP2350 should detect /BMCL via a GPIO interrupt
- Firmware should:
- Disable all bus drivers (drive BUS_DRIVE_OK LOW or rely on pull-up on /BD_OE_BUS)
- Release all wired-OR outputs (set GPIO floating, pull-up handles HIGH state)
- Reset all device emulation state machines
- Re-enable bus drivers when /BMCL releases
The pull resistors automatically achieve safe state during the brief window when GPIOs may be reconfigured. The pull resistor design means even a hardware fault that crashes the MCU still leaves the card in a safe state -- the bus pull-ups and our pull-resistor defaults bring everything back to released.
Pull Resistor Summary Table¶
| Signal Group | Pull Type | Value | Quantity | Total |
|---|---|---|---|---|
| Daisy-chain enables | Pull-down | 10K | 2 | 2 |
| BD output enable | Pull-up | 10K | 1 | 1 |
| Output latch strobes (D2) | Pull-down | 10K | 3 | 3 |
| Input latch /OE (D2) | Pull-up | 10K | 3 | 3 |
| Wired-OR drivers | Pull-up | 10K | 7 | 7 |
| SPI strobes (D3 only) | Mixed | 10K | 4 | 4 |
| SD card /CS | Pull-up | 10K | 1 | 1 |
| BUS_DRIVE_OK (master) | Pull-down | 10K | 1 | 1 |
Total pull resistors per design: - Design 1 (Direct GPIO): ~12 resistors - Design 2 (8-bit Latched): ~18 resistors - Design 3 (SPI Shift): ~16 resistors
PCB tip: Use 4-resistor SMD networks (e.g., 0603 4x10K array) to save board space. A single chip can hold 4 pull-up resistors.
Companion Computer: Raspberry Pi Zero Connector¶
For network connectivity, full ND-100 emulation, or any other heavy lifting, the controller PCB includes a Raspberry Pi Zero header (40-pin GPIO connector). A Pi Zero (or Pi Zero 2 W) plugs in via standoffs and a 2x20 ribbon cable or direct stacking.
Why Raspberry Pi Zero?¶
The Pi Zero is dramatically more capable than a Pi Pico W:
| Aspect | Pi Pico W | Pi Zero 2 W |
|---|---|---|
| CPU | RP2040 dual M0+ @ 133 MHz | Quad Cortex-A53 @ 1 GHz |
| RAM | 264 KB | 512 MB |
| Storage | 2 MB flash | microSD (any size) |
| OS | bare metal / RTOS | Full Linux (Raspberry Pi OS) |
| WiFi | 802.11n via CYW43 | 802.11n via BCM43436 |
| Bluetooth | yes | yes (4.2 BLE) |
| USB | USB-C device | micro USB (host + device) |
| Network stack | lwIP | Full Linux TCP/IP (telnet, SSH, samba, NFS, etc.) |
| Cost | ~$6-8 | ~$15 |
| Power draw | ~80 mA | ~250 mA average, ~500 mA peak |
| Form factor | 21x51 mm | 65x30 mm |
The Pi Zero can:
- Run a full ND-100 emulator (
nd100x) directly, communicating with our card via SPI to drive the bus signals -- effectively making the controller card a "bus extension cable" for the emulator - Telnet/SSH server for terminal sessions -- modern terminal access without physical serial cables
- HDLC over IP bridge -- connect ND-100 HDLC frames to remote systems over WiFi/Ethernet
- NFS/Samba file server for disk images on its microSD card
- HTTP server for web-based control and monitoring
- MQTT publisher for telemetry
- Programming/firmware update host for the BB48R
Connector Choice: 40-pin Pi Zero Header¶
The Pi Zero exposes its GPIO via a standard 40-pin 0.1" header (2x20). Our PCB has the matching connector to accept the Pi Zero either:
- Directly stacked on top of the controller card (with standoffs)
- Connected via ribbon cable (40-pin IDC) for flexible placement
The 40-pin header provides: - Power (5V from VBUS or 3.3V from regulator) - GND (multiple) - SPI0 (GPIO7-11 on Pi Zero -- CS0, MISO, MOSI, SCLK) - SPI1 (GPIO16-21 on Pi Zero -- second SPI) - I2C (GPIO2-3) - UART (GPIO14-15) - Plenty of GPIO for INT/RST signals
Power Considerations¶
The Pi Zero draws ~250 mA average, ~500 mA peak. This is significant: - Cannot power from BB48R 3.3V regulator (limited to 2A but we need 3.3V for level shifters too) - Power Pi Zero from the +5V bus rail via the 40-pin header pin 2 or 4 - The +5V rail comes from the ND-100 backplane (which is fed by a PC PSU on our backplane design) -- has plenty of current
5V power path:
Backplane PSU --> Bus +5V --> Controller card --> 40-pin header pin 2/4 --> Pi Zero
A polyfuse (1A) on the +5V to Pi Zero protects against shorts.
SPI Connection (BB48R ↔ Pi Zero)¶
Pi Zero is the SPI master, BB48R is the SPI slave.
Reasoning: The Raspberry Pi Zero hardware SPI controllers are master-only. To use SPI between Pi Zero and BB48R, the Pi Zero must be the master. The BB48R RP2350 has plenty of PIO state machines and can easily implement SPI slave mode in PIO.
The Pi Zero uses its hardware SPI0 peripheral as master, giving it efficient DMA-driven SPI from Linux user space (/dev/spidev0.0).
Pin mapping (verified against official Raspberry Pi Zero pinout):
Master/Slave is Software-Configurable¶
SPI wiring is STRAIGHT (not crossover) regardless of which chip is master. MOSI/MISO refer to function ("Master Out, Slave In"), not direction -- so the same physical wires work whether the BB48R or the Pi Zero is the master. Only firmware configuration changes when swapping master/slave roles.
Initial choice: Pi Zero is the master. Reasoning: - Pi Zero hardware SPI is mature and master-mode is the standard Linux usage (
/dev/spidev0.0) - Pi Zero runs Linux user-space apps directly with the SPI peripheral - BB48R RP2350 PIO is well-suited for SPI slave (lots of PIO examples)Future option: BB48R can be the master instead. If we later want the BB48R to drive transactions (e.g., the BB48R initiates all communication), we just: - Reconfigure BB48R's GPIO4-7 as hardware SPI0 master - Reconfigure Pi Zero to use bit-banging or PIO-style SPI slave in software (or use one of the SPI slave projects on GitHub) - No PCB changes needed -- the wiring is identical
Pin Direction Table (with both modes)¶
| Signal | Pi Zero Master Mode | BB48R Master Mode |
|---|---|---|
| MOSI | Pi Zero GPIO10 = output, BB48R GPIO7 = input | BB48R GPIO7 = output, Pi Zero GPIO10 = input |
| MISO | Pi Zero GPIO9 = input, BB48R GPIO4 = output | BB48R GPIO4 = input, Pi Zero GPIO9 = output |
| SCLK | Pi Zero GPIO11 = output, BB48R GPIO6 = input | BB48R GPIO6 = output, Pi Zero GPIO11 = input |
| CE/CS | Pi Zero GPIO8 = output, BB48R GPIO5 = input | BB48R GPIO5 = output, Pi Zero GPIO8 = input |
Important: SPI peripherals/PIO state machines automatically handle pin direction based on master/slave configuration. The PCB wiring is identical for both modes -- it's a software decision.
BB48R as SPI slave via PIO: The BB48R RP2350's hardware SPI0 peripheral is master-only in standard usage. To act as an SPI slave, the BB48R uses a PIO state machine programmed for slave-side SPI. PIO doesn't care about the "SPI0_TX" / "SPI0_RX" pin labels -- we configure GPIO7 as the data-in pin and GPIO4 as the data-out pin. The Raspberry Pi Pico SDK includes example PIO SPI slave code.
Full Pin Mapping Table¶
| Signal | Pi Zero Pin | Pi Zero GPIO | BB48R GPIO | Direction |
|---|---|---|---|---|
| SPI_MOSI | Pi Zero pin 19 | GPIO10 (SPI0_MOSI) | GPIO7 | Master → Slave |
| SPI_MISO | Pi Zero pin 21 | GPIO9 (SPI0_MISO) | GPIO4 | Slave → Master |
| SPI_SCLK | Pi Zero pin 23 | GPIO11 (SPI0_SCLK) | GPIO6 | Master → Slave |
| SPI_CE0 | Pi Zero pin 24 | GPIO8 (SPI0_CE0) | GPIO5 | Master → Slave |
| (SPI_CE1 unused) | Pi Zero pin 26 | GPIO7 (SPI0_CE1) | -- | Reserved for future second slave |
| INT_FROM_BB48R | Pi Zero pin 11 | GPIO17 (input on Pi Zero) | GPIO0 (output on BB48R) | BB48R → Pi Zero "I have data" |
| INT_FROM_ZERO | Pi Zero pin 13 | GPIO27 (output on Pi Zero) | GPIO1 (input on BB48R) | Pi Zero → BB48R "I have data" (optional) |
| RST_BB48R (optional) | Pi Zero pin 15 | GPIO22 (output on Pi Zero) | RUN pin or reset circuit | Pi Zero can hardware-reset BB48R |
| 5V | Pi Zero pin 2 or 4 | -- | -- | Power to Pi Zero from +5V_LOCAL rail |
| GND | Pi Zero pins 6, 9, 14, 20, 25, 30, 34, 39 | -- | -- | Common ground |
Verified against official Raspberry Pi Zero W / Zero 2 W pinout: - MOSI: GPIO10 (header pin 19) - MISO: GPIO9 (header pin 21) - SCLK: GPIO11 (header pin 23) - CE0: GPIO8 (header pin 24) - CE1: GPIO7 (header pin 26)
Bidirectional INT Signals¶
Same logic as before: - INT_BB48R: BB48R signals Pi Zero "I have data ready, please initiate a SPI read" - INT_PICO_ZERO: Pi Zero signals BB48R "I have data ready" (less common -- Pi Zero is master and can poll)
In practice with Pi Zero as master: - Pi Zero polls or interrupts on INT_BB48R for incoming data from BB48R - Pi Zero just initiates SPI writes whenever it has data to send
Pin Conflict Resolution (Same as Pi Pico W)¶
| BB48R GPIO | Function | When Pi Zero populated |
|---|---|---|
| GPIO0 | INT_BB48R (output to Pi Zero) | Pi Zero polls or interrupts on this |
| GPIO1 | INT_PICO_ZERO (input from Pi Zero) | Optional |
| GPIO2 | /BINT 12 drive | Unchanged |
| GPIO3 | /BINT 13 drive | Unchanged |
| GPIO4 | SPI0 MISO | Slave data out |
| GPIO5 | SPI0 CSn | Slave CS in |
| GPIO6 | SPI0 SCK | Slave clock in |
| GPIO7 | SPI0 MOSI | Slave data in |
Pi Zero Header on PCB¶
| Item | Specification |
|---|---|
| Connector | 2x20 pin 0.1" header (male, on our PCB) |
| Mating | Pi Zero plugs into the header via female header on Pi Zero side |
| Mounting | 4x M2.5 standoffs at the corners (Pi Zero standard) |
| Pi Zero variant | Pi Zero (no wireless), Pi Zero W (WiFi), Pi Zero 2 W (quad-core + WiFi) -- all same footprint |
| Orientation | Pi Zero stacked on top of controller card, USB and HDMI ports facing outward |
| Power | +5V from bus (not BB48R 3.3V) |
| Polyfuse | 1A polyfuse on the 5V to Pi Zero |
Pi Zero Footprint Layout¶
+-----------------------------------------+
| Olimex BB48R (socketed) |
| |
| ND-100 bus interface (SMD latches, |
| level shifters, transceivers) |
| |
| +============================+ |
| | Pi Zero 40-pin header | |
| | (2x20 male) | |
| | Pi Zero stacks on top | |
| | via standoffs | |
| +============================+ |
| o M2.5 mounting holes (4x) |
| |
+============== C connector (DIN41612) ===+
The Pi Zero stacks on top via 4 M2.5 standoffs, leaving its USB, HDMI, and microSD ports accessible.
When Pi Zero is Not Populated¶
- The 40-pin header is still on the PCB (cheap, ~$0.50)
- Just don't connect a Pi Zero
- The 6 BB48R GPIOs (GPIO0-1, 4-7) are free for other uses
- No power draw from the +5V rail
Pi Zero Software¶
The Pi Zero runs Raspberry Pi OS Lite (headless Debian) with:
As Network Bridge¶
- Telnet server (xinetd) for terminal sessions
- SSH server for secure terminal access
- WiFi or Ethernet (Pi Zero W has WiFi, all variants have USB Ethernet via OTG)
- Custom Python/C app that translates network traffic to/from BB48R via SPI
As Full ND-100 Emulator¶
nd100xemulator (already developed in this project) runs on the Pi Zero- The emulator's bus interface uses SPI to drive the BB48R, which in turn drives the physical bus
- The Pi Zero IS the ND-100 CPU, the BB48R is just a physical bus translator
- Perfect for testing: real ND-100 software runs on the Pi Zero, real ND-100 cards see real bus signals
As HDLC Bridge¶
- HDLC over IP translation
- Custom daemon that takes HDLC frames from BB48R via SPI and forwards over TCP
- Replaces physical HDLC modems with WiFi connections
Cost¶
| Item | Cost |
|---|---|
| 2x20 pin header (male) | ~$0.50 |
| 4x M2.5 mounting holes (PCB feature) | $0 |
| 1A polyfuse on +5V | $0.20 |
| Decoupling caps near header | $0.10 |
| Total controller card additions | ~$0.80 |
The Pi Zero itself is purchased separately: - Pi Zero (no wireless) ~$5 - Pi Zero W ~$15 - Pi Zero 2 W (recommended) ~$15 -- quad-core Cortex-A53, plenty for nd100x emulator
Programming the Pi Zero¶
The Pi Zero is programmed exactly like any Raspberry Pi: 1. Flash Raspberry Pi OS Lite to a microSD card 2. Configure WiFi via wpa_supplicant.conf 3. Insert microSD into Pi Zero 4. Power on (via the 5V from our PCB) 5. SSH into the Pi Zero over WiFi 6. Install your software
No JTAG or special programming needed -- just SD card and Linux.
Why This Is Better Than Pi Pico W¶
| Capability | Pi Pico W | Pi Zero 2 W |
|---|---|---|
| Run nd100x emulator | ❌ Too small | ✓ Easy |
| Full Linux | ❌ | ✓ |
| WiFi + telnet/SSH | ✓ (limited) | ✓ (full) |
| Disk image storage | 2 MB flash | microSD (any size) |
| Code reuse with main MCU | ✓ Same family | ❌ Different arch (but easier dev) |
| Cost | ~$6-8 | ~$15 |
For our use case (full ND-100 emulator + network bridge), the Pi Zero 2 W is the right choice. The extra $7 buys an enormous capability boost.
ND-100 CPU Emulation Mode (Same PCB, Software-Configured)¶
To enable safe development and testing without risking real ND-100 hardware, the same controller PCB can be configured to emulate the ND-100 CPU instead of (or in addition to) emulating I/O devices. This is critical for:
- Validating bus protocol implementations against a known CPU emulator
- Testing controller cards without a real ND-100
- Reproducing timing edge cases on demand
- Stress-testing multi-card setups
Goal: One PCB Design, Two Modes¶
Producing 10+ identical PCBs from JLCPCB (with PCB + SMD assembly) means we cannot afford two separate designs. The same physical card must be configurable as either:
| Mode | Function |
|---|---|
| Device mode (default) | Emulates I/O devices (floppy, SMD, terminal, HDLC). Responds to CPU IOX/IDENT/DMA cycles. |
| CPU mode | Acts as the bus master. Drives BAPR/BIOXE/BMEM/BINACK and responds to BREQ/BINT from device cards. |
Hardware Requirements for CPU Mode¶
In CPU mode the card must additionally:
- Drive /BAPR (it already can -- we have BAPR_OUT for DMA)
- Drive /BIOXE (new -- we don't drive this in device mode)
- Drive /BINACK (new -- this is normally a CPU output; we sniff it in device mode)
- Drive /BMEM (new -- BCU output during memory cycles)
- Generate /OUTGRANT in response to /BREQ (we are the BCU)
- Generate /OUTIDENT during IDENT PLxx instructions
- Watch /BINT 10-15 as inputs (the device cards drive these)
The key insight: most of these signals already have GPIO pins assigned (we sniff them in device mode). In CPU mode we need to drive them instead of just reading.
Pin Strategy: Sniff + Drive Pairs Already Exist¶
Looking at the pin allocation, we already have separate sniff (input) and drive (output) GPIOs for the bidirectional signals BAPR, BDRY, BINPUT, BDAP. For CPU mode we need to add drive pins for BIOXE, BINACK, BMEM (currently only sniffed).
Updated Pin Allocation (CPU + Device Mode Compatible)¶
The 6 spare GPIOs (GPIO0-1, GPIO4-7) can be repurposed for the additional drive signals:
| GPIO | Original | CPU Mode Use |
|---|---|---|
| GPIO0 | spare | /BIOXE_OUT drive (CPU mode) |
| GPIO1 | spare | /BINACK_OUT drive (CPU mode) |
| GPIO4 | spare | /BMEM_OUT drive (CPU mode) |
| GPIO5 | spare | MODE_SELECT input (read once at boot) |
| GPIO6 | spare | Future or status LED |
| GPIO7 | spare | Future or status LED |
Mode Selection: Hardware + Software¶
Hardware: MODE_SELECT Jumper¶
A single GPIO pin (GPIO5) reads a physical jumper or DIP switch at power-up to determine the operating mode:
GPIO5 ----+----[ pull-up to 3.3V (10K) ]
|
[JUMPER]----GND
Jumper installed (GPIO5 = LOW) = CPU mode
Jumper removed (GPIO5 = HIGH) = Device mode
The MCU reads GPIO5 once at boot and configures itself accordingly. No further GPIO consumption -- it's a static input.
Software: Mode Variable¶
typedef enum {
MODE_DEVICE = 0, // Default - emulates I/O devices
MODE_CPU = 1, // ND-100 CPU emulation (BCU + bus master)
} controller_mode_t;
controller_mode_t controller_mode;
void mode_init(void) {
gpio_init(MODE_SELECT_PIN);
gpio_set_dir(MODE_SELECT_PIN, GPIO_IN);
gpio_pull_up(MODE_SELECT_PIN);
sleep_ms(1); // settling
controller_mode = gpio_get(MODE_SELECT_PIN) ? MODE_DEVICE : MODE_CPU;
if (controller_mode == MODE_CPU) {
// Initialize CPU emulation: BCU, bus master, refresh oscillator
cpu_emulation_init();
} else {
// Initialize device emulation
device_emulation_init();
}
}
Bus Output Driver Differences¶
| Signal | Device mode | CPU mode |
|---|---|---|
| /BAPR | Drive only during DMA we initiate | Drive for every CPU cycle |
| /BIOXE | Sniff (CPU drives) | Drive (we are CPU) |
| /BINACK | Sniff (CPU drives) | Drive in response to BINPUT |
| /BMEM | Sniff (BCU drives) | Drive during memory cycles |
| /BDRY | Drive when responding | Sniff (devices drive) |
| /BINPUT | Drive during IOX read response or DMA write | Sniff (devices drive) |
| /BDAP | Drive during DMA we initiate | Drive during memory cycles |
| /BREQ | Drive to request DMA | Sniff (devices drive) |
| /OUTGRANT | Pass-through | Generate (we are BCU) |
| /OUTIDENT | Pass-through | Generate during IDENT instructions |
| /BINT 10-15 | Drive (we have interrupts) | Sniff (devices drive) |
Hardware: All Drivers Already Present¶
The good news: the existing 74LVC07 open-drain drivers handle both modes. The same physical pins drive the bus -- just under different software logic. The new pins (GPIO0/1/4 for BIOXE/BINACK/BMEM) need additional 74LVC07 channels.
We need to add 3 more 74LVC07 channels (or use a second 74LVC07 chip).
| Mode | Active Drive Pins |
|---|---|
| Device | BAPR_OUT, BDRY_OUT, BINPUT_OUT, BDAP_OUT, BREQ_OUT, BINT 10/11/12/13 = 9 outputs |
| CPU | BAPR_OUT, BIOXE_OUT, BINACK_OUT, BMEM_OUT, BDAP_OUT, OUTGRANT, OUTIDENT = 7 outputs |
The total set of drive signals across both modes is ~12, plus interrupts. One 74LVC07 handles 6 channels. We need 2x 74LVC07 to cover all signals.
Cost of Dual-Mode Support¶
| Item | Cost |
|---|---|
| Additional 74LVC07 chip | $0.30 |
| MODE_SELECT jumper / pin header | $0.10 |
| Extra PCB routing for 3 new drive signals | minimal |
| Firmware: dual-mode support | software effort, no hardware cost |
| Total | ~$0.50 |
This is a tiny additional cost for huge testing flexibility.
CPU Mode Limitations¶
The Pi Pico-based "CPU emulation" is not a cycle-accurate ND-100 emulator. It can:
- ✓ Drive bus signals correctly to test device cards
- ✓ Respond to IOX/IDENT/DMA cycles
- ✓ Run a simple test program (loop reading device registers, etc.)
- ✓ Simulate refresh oscillator
- ✓ Handle interrupts and IDENT cycles
- ✗ Run real SINTRAN OS (no MMU emulation)
- ✗ Execute actual ND-100 instructions
- ✗ Be cycle-accurate timing-wise
The CPU mode is a bus protocol generator, not a full ND-100 emulator. Sufficient for testing controller cards, validating bus protocols, and integration tests.
Recommended: Build BOTH Versions¶
When ordering 10+ PCBs from JLCPCB, build them all identically. Set the MODE_SELECT jumper to: - Most boards = Device mode (controllers with floppy/SMD/terminal/HDLC emulation) - One or two boards = CPU mode (test bench drivers)
You can swap the jumper to repurpose any board.
Backplane Design¶
The Backplane is a separate PCB that holds multiple controller cards plus a CPU card (real ND-100 CPU or our CPU-mode controller). It provides bus routing, power distribution, and physical card mounting.
No version numbering: This is the Backplane, not "Backplane V1" or similar. There is one backplane design.
Reference Design¶
The retrobrewcomputers ECB Backplane-4 is a good reference for layout principles: - https://www.retrobrewcomputers.org/doku.php?id=boards:ecb:backplane-4:start
Key takeaways: - Multiple slot positions on a single PCB - Star-grounded power distribution - Clear silkscreen labeling per slot - Power LED + power switch - Standard PC PSU connector for power
Backplane Specifications¶
| Feature | Value |
|---|---|
| Slot count | 4-8 slots (configurable, recommend 4 for first version) |
| Slot connector | DIN 41612 Type C, 96-pin (3 rows × 32 pins, rows A/B/C), female receptacle, vertical through-hole, 2.54 mm (0.1") pitch |
| Connector mounting | Vertical THT -- the connector body sits perpendicular to the backplane PCB so cards plug in from above |
| Slot spacing | 0.6" (15.24 mm) standard ND-100 backplane spacing |
| Card form factor | Standard ND-100 card height + custom width |
| Power input | Molex 15-24-4745 (Farnell 1391827) -- standard PC PSU 4-pin connector |
| Power rails | +5V, +12V (and GND) from PC PSU; -12V optional from PC PSU |
| Power switch | SPST toggle, breaks +5V (and +12V) input |
| Power LED | Green LED indicating bus power is on |
Connector clarification: The receptacle is the 3 rows × 32 pins = 96-pin DIN 41612 Type C female socket, vertical through-hole, 2.54 mm pitch. The mating part on the controller card is the corresponding male connector. JLCPCB does not stock DIN 41612 in their assembly library, so these are sourced separately (Farnell, Mouser, RS) and hand-soldered during board bring-up.
Power Connector¶
Molex 15-24-4745 (Farnell order code 1391827) is the standard PC PSU 4-pin Molex connector used for floppy/HDD power. This makes it trivial to power the backplane from any standard PC PSU.
| Pin | Wire Color | Function |
|---|---|---|
| 1 | Yellow | +12V |
| 2 | Black | GND |
| 3 | Black | GND |
| 4 | Red | +5V |
This connector mates with the standard PC floppy/HDD power cable, eliminating the need for a custom PSU.
Backplane Block Diagram¶
flowchart TB
subgraph PSU["External PC PSU"]
PWR["+5V / +12V / GND<br/>via Molex 15-24-4745"]
end
subgraph BP["Backplane PCB"]
SW["SPST Power Switch<br/>(breaks +5V)"]
LED["Power LED<br/>(green)"]
FUSE["Fuse / polyfuse<br/>(optional protection)"]
subgraph SLOTS["Card Slots (4-8x DIN 41612)"]
S1["Slot 1: CPU card<br/>(real ND-100 or<br/>our CPU mode card)"]
S2["Slot 2: Device card<br/>(floppy/SMD/term/HDLC)"]
S3["Slot 3: Device card"]
S4["Slot 4: Device card<br/>(last in chain)"]
end
BUS["96-pin C bus signals<br/>(routed to all slots)"]
end
PWR --> SW
SW --> FUSE
FUSE --> BUS
SW --> LED
BUS --> S1
BUS --> S2
BUS --> S3
BUS --> S4
style PSU fill:#FFF3E0,stroke:#E65100,color:#E65100
style BP fill:#E3F2FD,stroke:#0D47A1,color:#0D47A1
style SLOTS fill:#E8F5E9,stroke:#2E7D32,color:#2E7D32
Signal Routing Categories¶
The 96 pins on the C connector fall into four routing categories on the backplane. Mixing them up will silently break the bus, so this is the most important section in the backplane design.
| Category | How it is routed | Signals |
|---|---|---|
| A. Shared bus (parallel) | Single net per signal, bussed across all slot positions | BD 0-23, BAPR, BMEM, BIOXE, BINPUT, BINACK, BDAP, BDRY, BMCL, BREF, BREQ, BPERR, BINT 10-13, +5 V, +12 V, GND |
| B. Daisy chain (slot-to-slot) | OUTPUT of slot N → INPUT of slot N+1. No shared net. Last slot's output is unconnected. | INGRANT/OUTGRANT, INIDENT/OUTIDENT, INCONTR/OUTCONTR |
| C. Slot-unique hard-wired | Per-slot fixed wiring to GND or +3.3 V/+5 V. Different on every slot. | PA 0, PA 1, PA 2, PA 3 (Position Address) |
| D. CPU-crate-only (optional) | Bussed across slots only if you have a control panel; otherwise pulled to safe levels | LOAD, RESTART, RUN, CONTINUE, STOP |
Each category gets its own subsection below.
Category A -- Shared Bus Signals¶
The bulk of the C connector is shared bus -- one net per signal, every slot connector pin tied to it. These are the data lines, address strobes, IO control, interrupts, refresh, and power rails. Just bus them across.
| Group | Signals | Notes |
|---|---|---|
| Data/address lines | BD 0-23 | All 24 lines, tri-state, bussed parallel |
| Bus cycle control | BAPR, BMEM, BIOXE, BINPUT, BINACK, BDAP, BDRY, BMCL | All wired-OR or active drive |
| Refresh + DMA | BREF, BREQ | BREF is CPU-out shared, BREQ is wired-OR DMA request |
| Interrupts | BINT 10, BINT 11, BINT 12, BINT 13 | Wired-OR, shared across all slots |
| Memory error | BPERR | Memory cards drive, CPU listens |
| Power rails | +5 V, +12 V (and -12 V if PSU has it), GND | Distributed every slot |
Bus pull-ups for the wired-OR signals (BAPR, BREQ, BINT 10-13, BDRY, BINPUT, BMCL) live on the backplane in this design (rather than relying on the CPU card). A 4.7 kΩ resistor to +5 V on each wired-OR signal makes the bus well-behaved even before the CPU card is plugged in. Place these in a single resistor array near the CPU slot.
Category B -- Daisy-Chain Signals¶
There are three daisy-chain pairs on the C connector. Each one originates at the CPU slot and walks through every slot in physical order. Each pair is a sequence of point-to-point links, NOT a shared net.
| Pair | Input pin (A row) | Output pin (C row) | Purpose |
|---|---|---|---|
| INGRANT / OUTGRANT | A23 | C23 | DMA bus-grant priority chain |
| INIDENT / OUTIDENT | A22 | C22 | Interrupt-IDENT priority chain |
| INCONTR / OUTCONTR | A21 | C21 | Bus-control priority chain (reserved for future, route it anyway so future cards work) |
Routing rule -- this is what the user explicitly called out:
Slot 1 (CPU) Slot 2 Slot 3 Slot 4 (last)
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
(chain head) ─── │ OUTGRANT │ ──> │ INGRANT │ │ INGRANT │ │ INGRANT │
from CPU's │ │ │ OUTGRANT │ ──> │ OUTGRANT │ ──> │ OUTGRANT │ ──> n/c (chain end)
internal logic └────────────┘ └────────────┘ └────────────┘ └────────────┘
Same pattern, independently, for INIDENT/OUTIDENT and INCONTR/OUTCONTR.
Concrete net naming (for the schematic):
GRANT_S1_OUT = CPU.OUTGRANT ───> Slot2.INGRANT
GRANT_S2_OUT = Slot2.OUTGRANT ──> Slot3.INGRANT
GRANT_S3_OUT = Slot3.OUTGRANT ──> Slot4.INGRANT
GRANT_S4_OUT = Slot4.OUTGRANT ──> (no connect)
IDENT_S1_OUT = CPU.OUTIDENT ───> Slot2.INIDENT
IDENT_S2_OUT = Slot2.OUTIDENT ──> Slot3.INIDENT
IDENT_S3_OUT = Slot3.OUTIDENT ──> Slot4.INIDENT
IDENT_S4_OUT = Slot4.OUTIDENT ──> (no connect)
CONTR_S1_OUT = CPU.OUTCONTR ───> Slot2.INCONTR
CONTR_S2_OUT = Slot2.OUTCONTR ──> Slot3.INCONTR
CONTR_S3_OUT = Slot3.OUTCONTR ──> Slot4.INCONTR
CONTR_S4_OUT = Slot4.OUTCONTR ──> (no connect)
For an N-slot backplane, this is 3 × N nets (or 3 × (N-1) point-to-point links plus 3 dangling chain ends). Do not be tempted to short these together "just in case" -- the OUTGRANT driver on the original 3202D is 74F00 push-pull and will fight any other driver. Wiring two OUTGRANTs to the same net will damage chips.
The CPU slot is always the chain head by convention. By construction, slot N has higher priority than slot N+1 for all three chains. Plan slot ordering accordingly: put performance-critical or latency-sensitive controllers (HDLC, SMD) close to the CPU slot.
Category C -- Slot-Unique Hard-Wired Pins (PA 0-3)¶
The Position Address pins are inputs to every controller card and tell the card which physical slot it lives in. They are not connected to a shared bus net; instead, the backplane PCB has per-slot fixed wiring to GND or +3.3 V/+5 V (level matches the card's input buffer).
| Signal | Pin | Encodes |
|---|---|---|
| PA 0 | C13 | bit 0 |
| PA 1 | A13 | bit 1 |
| PA 2 | C14 | bit 2 |
| PA 3 | A14 | bit 3 |
A 4-bit code allows up to 16 distinct slot addresses, plenty for any homebrew backplane.
Encoding scheme (active LOW = 0, pull-up = 1):
| Slot | PA3 | PA2 | PA1 | PA0 | Hex |
|---|---|---|---|---|---|
| 1 (CPU) | 1 | 1 | 1 | 1 | F |
| 2 | 1 | 1 | 1 | 0 | E |
| 3 | 1 | 1 | 0 | 1 | D |
| 4 | 1 | 1 | 0 | 0 | C |
| 5 | 1 | 0 | 1 | 1 | B |
| ... | ... | ... | ... | ... | ... |
| 16 | 0 | 0 | 0 | 0 | 0 |
Note: the exact encoding (Gray code, binary, hex) is not pinned down by the bus spec -- I/O cards typically use these bits as a tag and the OS configures the device-number-to-slot mapping. The simplest scheme is straight binary, with slot 0 = CPU.
On the PCB, each slot connector's PA0-3 pins go to per-slot 0-ohm jumper pads that select GND or +5 V. To change the slot address, depopulate the wrong jumper and populate the right one. No DIP switch, no rework -- just one resistor pad per bit.
Category D -- CPU-Crate-Only Signals (LOAD/RESTART/RUN/CONTINUE/STOP)¶
These are control-panel signals on Row B pins 12-16. They are wired-OR shared bus signals, but only meaningful if you have a physical control panel or a diagnostic device.
| Signal | Pin | Notes |
|---|---|---|
| LOAD | B12 | Activate load microprogram (STOP mode only) |
| RESTART | B13 | Start at address 20₈ (STOP mode only) |
| RUN | B14 | CPU is currently running (CPU output) |
| CONTINUE | B15 | Resume from STOP |
| STOP | B16 | Force STOP mode |
Backplane handling:
- If you have no control panel: leave these as a 2x5 pin header at one edge of the backplane. Pull LOAD/RESTART/CONTINUE/STOP to +5 V via 10 kΩ (inactive). Bring RUN out as an output. The header is for plugging in a panel later.
- If you have a control panel: route the panel ribbon cable to that header.
- For our CPU-mode controller card: the BB48R can drive these signals via firmware (it lives in the CPU slot). The header is still a useful debug interface.
Slot Layout (visual)¶
+─── DIN 41612 socket per slot (vertical THT, 96-pin) ───+
│ │
│ Slot 1 Slot 2 Slot 3 Slot 4 │
│ (CPU) (Floppy) (HDLC) (last/SMD) │
│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ │ │ ───> │ │ ───> │ │ ───> │ │ ───> n/c │ GRANT chain
│ │ │ ───> │ │ ───> │ │ ───> │ │ ───> n/c │ IDENT chain
│ │ │ ───> │ │ ───> │ │ ───> │ │ ───> n/c │ CONTR chain
│ │ │ │ │ │ │ │ │ │
│ └───┘ └───┘ └───┘ └───┘ │
│ ==== ==== ==== ==== │
│ |||| Shared bus signals (BD 0-23, BAPR, ...) |||| │
│ ==== ==== ==== ==== │
│ PA=F PA=E PA=D PA=C │ (slot-unique)
│ │
│ +5V/+12V/GND distribution (every slot) │
│ │
+─── Control Panel header (LOAD/RESTART/RUN/...) ──────+
+─── Molex 15-24-4745 PSU connector ───────────────────+
Slot ordering matters: higher priority cards go closer to the CPU slot (left in the diagram). Once the slot order is fixed in the PCB it is hard to change because the IDENT/GRANT chains are physically wired between specific connector positions.
Slot Position Coding (PA 0-3)¶
Per the bus spec, each slot has Position Address (PA) 0-3 signals that are hard-wired on the backplane. The card reads these to know its physical slot number.
| Slot | PA0 | PA1 | PA2 | PA3 | Binary | Decimal |
|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 0 | 0000 | 0 |
| 2 | 1 | 0 | 0 | 0 | 0001 | 1 |
| 3 | 0 | 1 | 0 | 0 | 0010 | 2 |
| 4 | 1 | 1 | 0 | 0 | 0011 | 3 |
| ... | ... | ... | ... | ... | ... | ... |
The PA pins are simple GND or pull-up jumpers per slot, encoded according to the slot position.
Backplane Power Distribution¶
| Rail | Source | Distribution |
|---|---|---|
| +5V | Molex pin 4 (red) | All bus pins 2/31 (every slot) |
| +12V | Molex pin 1 (yellow) | Bus pin 28 (memory rail) |
| GND | Molex pins 2/3 (black) | All bus GND pins 1/11/24/32 (every slot) |
| -12V | Optional Molex (depends on PSU) | Bus pin 27 (analog supply) |
| +15V | LDO from +12V (or external) | Bus pin 25 (analog +) |
| -15V | LDO from -12V (or external) | Bus pin 27 (analog -) |
| 5V Standby | Optional, external | Bus pin 30 |
For simplicity, +15V/-15V are optional -- only needed if analog process I/O cards are used. Most digital controller cards don't need them.
Power Switch and LED¶
| Component | Notes |
|---|---|
| Power switch | SPST toggle, rated 5A @ 12V minimum, panel mount |
| Power LED | Green 0805 SMD, 1K resistor to +5V (LED on when bus powered) |
| Optional: per-rail LEDs | Separate LEDs for +5V and +12V rails to verify both present |
| Optional: polyfuse | 3A polyfuse on +5V rail to protect against short circuits |
Backplane Components List¶
| Item | Quantity | Part |
|---|---|---|
| DIN 41612 Type C female receptacle (96-pin) | 4-8 | Standard part, sourced from Farnell/Mouser |
| Molex 15-24-4745 PC PSU connector | 1 | Farnell 1391827 |
| SPST toggle switch | 1 | Panel mount, 5A rating |
| Power LED (green, 0805 SMD) | 1 | Standard |
| Current limiting resistor (1K) | 1 | 0805 SMD |
| Polyfuse (3A) | 1 (optional) | 0805 SMD or through-hole |
| Decoupling caps (10uF + 0.1uF per slot) | 2 per slot | 0805/0603 SMD |
| Mounting holes | 4 | M3 standoff |
PCB Notes¶
- Power distribution: Star-grounded, with the Molex connector as the central point
- Signal routing: Bus signals run as parallel traces across the slot array
- Decoupling: 10uF + 0.1uF caps per slot, close to the connectors
- Trace impedance: Not critical at the bus speeds, but keep traces short
- Layer count: 2-layer is sufficient for the backplane (no high-speed signals; everything is at the ND-100 bus rate of ~8 MHz). Top = signals, bottom = ground pour + power distribution.
- Mechanical: Mounting holes for case, optional rack mount
- Dimensions: depends on slot count -- 4 slots = ~150 mm x 100 mm
Test Setup with Backplane¶
+-----------------+ +-----------------+
| Backplane PCB | | PC PSU |
| 4 x slots |<--Molex-| (5V + 12V out) |
| Power switch | +-----------------+
| Power LED |
+-----------------+
| | | |
| | | +- Slot 4: Controller (Device mode, last)
| | +- Slot 3: Controller (Device mode)
| +- Slot 2: Controller (Device mode)
+- Slot 1: Controller (CPU mode, MODE_SELECT jumper IN)
OR real ND-100 CPU card if available
This setup lets you: - Run integration tests without a real ND-100 - Validate bus protocol on real hardware - Develop and debug controller firmware safely - Stress test with multiple devices
Complete Bill of Materials (BOM)¶
This is the consolidated BOM for one controller card with the recommended configuration: Olimex BB48R socketed, BD bus interface (Design 2), Pi Zero header (optional populate), and all support circuitry.
Modules (Pre-built Boards)¶
| Item | Part | Qty | Cost (USD) | Notes |
|---|---|---|---|---|
| MCU module | Olimex RP2350-PICO2-BB48R | 1 | ~$15 | Socketed via female pin headers |
| Wireless companion (optional) | Raspberry Pi Zero 2 W | 0-1 | ~$15 | Plugs into 40-pin header |
Bus Interface ICs (SMD)¶
| Item | Part | Qty | Cost (USD) | Function |
|---|---|---|---|---|
| Input latches | 74LVC574 (SOIC-20 or TSSOP-20) | 3 | $1.50 | Capture BD 0-23 on /BAPR |
| Output latches | 74LVC574 | 3 | $1.50 | Hold MCU output before driving |
| Output drivers | 74LVT245 | 3 | $2.40 | 3.3V to 5V level shift, drive bus |
| Schmitt input buffer | 74LVC14 (SOIC-14) | 1 | $0.30 | Clean control signal inputs |
| Open-drain output | 74LVC07 (SOIC-14) | 1 | $0.30 | Drive wired-OR signals (BREQ, BINT, BDRY, BINPUT, BDAP) |
| Daisy-chain bypass | 74LVC125 (SOIC-14) | 1 | $0.30 | INIDENT/INGRANT pass-through |
| Subtotal Bus Interface | 12 chips | $6.30 |
Power Distribution (SMD)¶
| Item | Part | Qty | Cost (USD) | Function |
|---|---|---|---|---|
| Schottky diode | SS14 (SMA) | 1 | $0.10 | D1: BB48R VDD_SYS source OR-ing |
| Ideal diode IC | LTC4412 or MAX40200 | 1 | $1.50 | D2: Pi Zero +5V source OR-ing (low Vdrop) |
| Polyfuse | MF-MSMF200 (2A, 0805) | 2 | $0.60 | F1 (bus 5V), F2 (Pi Zero 5V) |
| TVS diode | SMBJ5.0A | 1 | $0.20 | TVS clamp on +5V_LOCAL |
| Bulk cap (Pi Zero) | 1000 uF aluminum polymer (low ESR) | 1 | $0.80 | WiFi TX burst handling |
| Bulk cap (Pi Zero) | 470 uF tantalum | 1 | $0.40 | Boot inrush |
| Mid cap (Pi Zero) | 10 uF ceramic 1206 X5R | 1 | $0.05 | Mid-frequency decoupling |
| HF cap (Pi Zero) | 0.1 uF ceramic 0603 X7R | 1 | $0.01 | Switching noise |
| BB48R input cap | 47 uF ceramic 1210 | 1 | $0.30 | Bus 5V input |
| BB48R output cap | 10 uF + 0.1 uF ceramic | 2 | $0.06 | 3.3V output decoupling |
| Per-chip decoupling | 0.1 uF ceramic 0603 | ~15 | $0.30 | One per IC |
| Bulk decoupling | 10 uF ceramic 0805 | ~5 | $0.30 | Distributed bulk caps |
| Subtotal Power | ~$4.62 |
Pull Resistors¶
| Item | Part | Qty | Cost (USD) | Function |
|---|---|---|---|---|
| Pull-up resistor arrays | 10K x 4 SMD network (0603 x4) | 4 | $0.40 | Pull-ups for BD output OE, /BD_OE_BUS, etc. |
| Pull-down resistor arrays | 10K x 4 SMD network | 2 | $0.20 | Pull-downs for daisy-chain enables |
| Discrete pull resistors | 10K 0603 SMD | ~10 | $0.10 | Misc. pull-ups/downs |
| Subtotal Resistors | ~$0.70 |
Connectors¶
| Item | Part | Qty | Cost (USD) | Function |
|---|---|---|---|---|
| C connector | DIN 41612 Type C, 96-pin male right-angle | 1 | $5.00 | Bus connection (THT) |
| BB48R sockets | 2x 27-pin female header (0.1" pitch, 0.6" row spacing) | 2 | $0.80 | BB48R module socketing |
| Pi Zero header | 2x20 pin male header (0.1" pitch) | 1 | $0.50 | Pi Zero connection |
| Standoffs (Pi Zero) | M2.5 nylon standoff + screw | 4 | $0.50 | Pi Zero mechanical mount |
| Subtotal Connectors | ~$6.80 |
LEDs and Misc¶
| Item | Part | Qty | Cost (USD) | Function |
|---|---|---|---|---|
| Status LEDs | 0805 SMD (assorted colors) | ~10-15 | $1.00 | Power, activity, error indicators |
| LED current limiters | 1K 0603 SMD | ~10-15 | $0.20 | LED resistors |
| Subtotal LEDs | ~$1.20 |
PCB¶
| Item | Part | Qty | Cost (USD) | Notes |
|---|---|---|---|---|
| PCB (2-layer, ~100 x 100 mm, HASL, JLCPCB) | -- | 1 | $2-5 | 2-layer is fine -- highest signal rate on our PCB is 8 MHz bus. Quantity 10 drops unit cost further. |
Total Cost per Controller Card¶
| Configuration | Cost |
|---|---|
| Base card (Olimex BB48R + bus interface + power, no Pi Zero) | ~$33-43 |
| Full card (with Pi Zero 2 W populated) | ~$48-58 |
| Quantity 10 (PCB unit cost drops) | ~$30-50 per card |
Optional Components (Not in Base BOM)¶
| Item | When to Add | Cost |
|---|---|---|
| 74HC595 shift register | If you want to free GPIOs by multiplexing slow signals via SD SPI bus | $0.30 |
| ATF1502 CPLD or 74LVC02 NOR | V2 hardware IDENT comparator (not needed for V1 "last card" simplification) | $0.30-3.00 |
| Additional SMD pull resistors | For custom features | <$0.50 |
| Through-hole headers | For debug probes (SWD, UART) | $0.50 |
Backplane BOM (Separate Card)¶
The backplane is a separate PCB. Its BOM is:
| Item | Part | Qty | Cost (USD) | Notes |
|---|---|---|---|---|
| PCB (2-layer, ~150 x 100 mm) | -- | 1 | $5-15 | Larger area for slot count |
| DIN 41612 Type C female receptacles | -- | 4-8 | $5 each | One per slot |
| Power connector | Molex 15-24-4745 (Farnell 1391827) | 1 | $1.00 | PC PSU 4-pin Molex |
| Power switch | SPST toggle, 5A | 1 | $2.00 | Panel mount |
| Power LED | Green 0805 + 1K resistor | 1 | $0.20 | Bus power indicator |
| Polyfuse | 3A polyfuse (optional) | 1 | $0.30 | Short circuit protection |
| Decoupling caps | 10uF + 0.1uF per slot | 8-16 | $0.50 | Per-slot decoupling |
| Backplane total | ~$30-50 | (4-slot configuration) |
Total Project Cost (10 Controllers + 1 Backplane)¶
| Item | Quantity | Unit Cost | Subtotal |
|---|---|---|---|
| Controller cards (full config) | 10 | $50 | $500 |
| Pi Zero 2 W modules | 10 (or fewer if mixed) | $15 | $150 |
| Backplane | 1 | $40 | $40 |
| PC PSU (used) | 1 | $20 | $20 |
| Cables and standoffs | 1 set | $20 | $20 |
| Total | ~$730 |
This buys you a fully working ND-100 emulator + multi-device controller test setup.
JLCPCB Parts and KiCad Integration¶
For PCB manufacturing and assembly via JLCPCB, we need to use parts from JLCPCB's parts library. JLCPCB sources from LCSC (their parts catalog at lcsc.com) and offers two categories:
| Type | Cost | Notes |
|---|---|---|
| Basic parts | No setup fee | ~700 most-common parts (resistors, caps, transistors, common ICs). Always available. |
| Extended parts | $3 setup fee per design + part cost | Larger catalog (~50K parts). One-time fee per BOM, applies even if you use 1 of them. |
Workflow¶
- Browse parts at https://jlcpcb.com/parts/componentSearch -- search by name, MPN, or LCSC number
- Note LCSC part numbers (format:
Cnnnnnn) for each component - Convert LCSC parts to KiCad libraries using
easyeda2kicadorJLC2KiCadLib - Use the KiCad-JLCPCB-Tools plugin to manage BOM and Component Placement List (CPL)
- Generate BOM and CPL files for JLCPCB assembly upload
- Submit gerbers + BOM + CPL to JLCPCB for PCB + assembly
Recommended KiCad Tools¶
| Tool | Purpose | Source |
|---|---|---|
| easyeda2kicad | Convert LCSC parts to KiCad symbols/footprints/3D models | PyPI -- pip install easyeda2kicad |
| JLC2KiCadLib | Same purpose, alternative tool | PyPI |
| Pre-built JLCPCB KiCad Library | Ready-to-use library of basic parts | GitHub: CDFER/JLCPCB-Kicad-Library |
| KiCad-JLCPCB-Tools | KiCad plugin for BOM/CPL generation and JLCPCB workflow | KiCad PCM (Plugin and Content Manager) |
Using easyeda2kicad¶
# Install
pip install easyeda2kicad
# Download a single component (symbol + footprint + 3D)
easyeda2kicad --full --lcsc_id=C6097 # 74LVC574 example
easyeda2kicad --full --lcsc_id=C2480 # SS14 Schottky example
# Or just symbol/footprint/3D individually
easyeda2kicad --symbol --footprint --3d --lcsc_id=C6097
This generates:
- easyeda2kicad.kicad_sym -- KiCad 7+ symbol library
- easyeda2kicad.pretty/ -- footprint library folder
- easyeda2kicad.3dshapes/ -- 3D models (.STEP and .WRL)
⚠ Always verify the converted symbols/footprints against datasheets before committing to PCB. Tool conversions are not always perfect, especially for unusual packages.
LCSC Part Numbers for Our BOM¶
Note: LCSC stock and prices change. Always verify each part on jlcpcb.com/parts/componentSearch before finalizing the BOM. The numbers below are common variants and starting points -- substitutes may be needed.
Bus Interface ICs¶
| Part | Recommended LCSC | Package | Type | Notes |
|---|---|---|---|---|
| 74LVC574 (input/output latch) | C6097 (Nexperia) | SOIC-20 | Extended | ~$1.20 each. Multiple manufacturers available. |
| 74LVT245 (output driver) | C82393 (Nexperia 74LVT245A) | SOIC-20 | Extended | Alternative: 74LVC8T245 (C32431) |
| 74LVC14 (Schmitt input) | C5181 (Nexperia 74LVC14A) | SOIC-14 | Extended | |
| 74LVC07 (open-drain output) | search "74LVC07" | SOIC-14 | Extended | Sometimes harder to find -- alt: 74HC07 (C7501) |
| 74LVC125 (3-state buffer) | C6087 (Nexperia 74LVC125A) | SOIC-14 | Extended |
Power Distribution¶
| Part | LCSC | Package | Type | Notes |
|---|---|---|---|---|
| SS14 (Schottky D1) | C2480 | SMA (DO-214AC) | Basic | Very common, no extended fee |
| LTC4412 (ideal diode D2) | search "LTC4412" | SOT-23-6 or MSOP-8 | Extended | Linear Tech / ADI part |
| MAX40200 (alternative D2) | search "MAX40200" | SOT-23-6 | Extended | Maxim/ADI part |
| MF-MSMF200 (2A polyfuse F1, F2) | search "polyfuse 2A" | 1812 SMD | Extended | Bourns or Littelfuse |
| SMBJ5.0A (TVS clamp) | C8466 | SMB | Extended | |
| 1000 uF aluminum polymer | search by value | various | Extended | Panasonic SP-Cap or similar |
| 470 uF tantalum | search by value | Case D or E | Extended | -- |
| 47 uF / 10 uF / 0.1 uF ceramic caps | various | 0603/0805/1206 | Basic | Standard JLCPCB basic library |
Resistors¶
| Part | LCSC | Package | Type |
|---|---|---|---|
| 10K resistor 0603 | C25804 | 0603 | Basic |
| 1K resistor 0603 | C21190 | 0603 | Basic |
| 10K x 4 resistor network | search "10K array 4x" | 0804 / 0612 | Extended |
Most resistors are JLCPCB basic parts -- no extra fee.
LEDs¶
| Part | LCSC | Package | Type |
|---|---|---|---|
| Green LED 0805 | C84256 | 0805 | Basic |
| Red LED 0805 | C84257 | 0805 | Basic |
| Yellow LED 0805 | C72038 | 0805 | Basic |
| Blue LED 0805 | C72041 | 0805 | Basic |
LEDs are commonly basic parts.
Connectors (Often NOT in JLCPCB Library)¶
JLCPCB's parts library is mainly SMD components. Through-hole connectors like the DIN 41612 and 0.1" headers are usually NOT in the JLCPCB library. You need to: - Hand-solder these after JLCPCB returns the PCB - Or source separately and assemble yourself - Or specify "PCBA with through-hole assembly" (extra cost) if JLCPCB offers it for your design
| Part | Source | Notes |
|---|---|---|
| DIN 41612 Type C 96-pin male right-angle | Mouser, Farnell, Digi-Key | Search "DIN 41612 male right angle 96 pin C". Try Harting or ept brand. |
| 2x 27-pin female header (BB48R sockets) | Pollin, Farnell, Mouser, Adafruit | Standard 0.1" 0.6" row spacing |
| 2x20 pin male header (Pi Zero) | Farnell, Mouser, Digi-Key | Standard 40-pin 2x20 0.1" header |
JLCPCB hand-assembly note: For 10-card runs, hand-soldering 3-4 through-hole connectors per card is feasible (~5 minutes per card). For larger runs, consider JLCPCB's PCBA service which now supports some through-hole parts.
KiCad-JLCPCB-Tools Plugin¶
This KiCad plugin streamlines the JLCPCB assembly workflow:
| Feature | Benefit |
|---|---|
| Per-component LCSC field | Add LCSC numbers directly in KiCad symbol properties |
| BOM generation | Generates JLCPCB-compatible BOM CSV |
| CPL generation | Generates Component Placement List with rotation correction |
| Part stock checking | Real-time stock status from JLCPCB |
| Rotation database | Built-in database of rotation corrections for common parts (JLCPCB uses different orientation conventions than KiCad) |
Install: KiCad → Tools → Plugin and Content Manager → search "JLCPCB"
Usage: Right-click any component → JLCPCB Tools → Set LCSC Part. The plugin saves the LCSC number in the symbol's LCSC custom field. When generating production files, it creates bom.csv and cpl.csv automatically.
Per-Design Workflow¶
- Start KiCad project, add the standard libraries
- Run easyeda2kicad for each LCSC part you'll use:
for lcsc in C6097 C82393 C5181 C2480 C25804 C84256; do easyeda2kicad --full --lcsc_id=$lcsc done - Add the generated libraries to KiCad project (Preferences → Manage Symbol/Footprint Libraries)
- Place components in schematic, using the converted symbols
- Set LCSC numbers in each symbol's properties (or use KiCad-JLCPCB-Tools plugin to automate)
- Run DRC to verify the PCB
- Generate gerbers + drill files (KiCad → File → Plot)
- Generate BOM and CPL via KiCad-JLCPCB-Tools plugin
- Upload to JLCPCB at jlcpcb.com → Order Now → upload zip + BOM + CPL
- Verify the JLCPCB part placement preview before confirming order
Cost Considerations for Extended Parts¶
JLCPCB charges a $3 setup fee per BOM that contains extended parts. Most of our specialty parts (74LVC family, ideal diode IC, polyfuse) are extended parts. So:
- One-time $3 extra per design submission (not per part)
- This is per order, so 10 boards in one order share the $3 fee
- The design becomes economical at quantity 5+
Typical assembly cost for 10 controller cards (estimated): - PCB cost (2-layer, ~100 x 100 mm, HASL): ~$5-10 for 10 (JLCPCB 2-layer is dramatically cheaper than 4-layer) - SMD assembly (10 boards, ~30 components each): ~$50-80 - Components (extended): ~$5 setup + ~$50 = ~$55 - Components (basic): ~$10 - Total: ~$140-185 for 10 fully-assembled cards (without modules)
Plus: 10x Olimex BB48R (~$150) + optional Pi Zero 2 W (~$150) = ~$440-485 total.
Verification Before Ordering¶
Before submitting to JLCPCB, verify: - [ ] Every component in the BOM has an LCSC number - [ ] Stock check shows >0 for all extended parts at the order time - [ ] CPL placements are correct (use the JLCPCB preview) - [ ] Pin 1 markers and rotation match the parts (rotation corrections applied via plugin) - [ ] Through-hole connectors are NOT in the BOM (assemble manually after delivery) - [ ] Polarized parts (diodes, electrolytic caps, LEDs) have correct orientation - [ ] No DRC errors in KiCad
Open Design Questions¶
Most of the V1 critical questions have been resolved (see "Resolved decisions" below). What remains is mostly validation work that has to wait until we have a real PCB on a real bus.
Resolved decisions (V1 locked in)¶
| Question | Decision | Where it lives in this doc |
|---|---|---|
| MCU module | Olimex RP2350-PICO2-BB48R | "Selected Hardware Module" section |
| BD bus interface architecture | Design 2: 8-bit shared MCU↔latch bus, PIO-driven | "BD 0-23 Bus Interface Architecture" |
| Trigger detection | PIO sample-on-change with mask compare (no external NOR) | "PIO Architecture" |
| /BINT 10-13 drive | 4 direct GPIO pins via 74LVC07, no shift register | Pin Allocation Summary (GPIO2, 3, 39, 40) |
| IDENT/GRANT pass-through | 74LVC125 quad 3-state buffer, hardware default pass + PIO override | "IDENT/GRANT Daisy-Chain Pass-Through Chip" |
| Level shifters | 74LVC574 (input latches), 74LVT245 (output drivers), 74LVC14 (input sniff buffers), 74LVC07 (open-drain outputs) | "Design 2 Detailed Implementation" |
| SD card | Use the on-board BB48R microSD slot via SPI1 (already wired GPIO9-11/24) | "SD Card Support" |
| Companion computer | Optional Raspberry Pi Zero on a 40-pin header, Pi Zero is SPI master | "Companion Computer: Raspberry Pi Zero Connector" |
| Power | Dual-source (USB-C OR backplane 5 V) Schottky-OR'd, separate ideal-diode for Pi Zero | "Power Distribution Design" |
| CPU emulation mode | Same PCB, jumper-selected at boot | "ND-100 CPU Emulation Mode" |
| Backplane | Separate PCB, PC PSU power, 4 slots V1 | "Backplane Design" |
Still open -- needs hardware validation¶
These cannot be answered from datasheets alone. They need a real PCB on a real ND-100 backplane:
- Daisy-chain pass-through delay -- measure actual
/INIDENT→/OUTIDENTpropagation through the 74LVC125. Datasheet says 3-5 ns; we need to confirm we stay well under the 100 ns IDENT window even with two cards in series. - Bus loading -- how many of these cards can hang off a real ND-100 backplane before signal integrity degrades? Need to measure rise/fall times on /BAPR with 1, 2, 4, 8 cards installed.
- Pull-up resistors -- are the CPU card's existing bus pull-ups sufficient, or do we need additional bus termination on the backplane? This depends on bus length and card count.
- DMA throughput at full SMD rate -- we calculate ~1.5-2 MB/s on the BD path, which should be plenty. Need to confirm with a real SMD-class transfer (256 KB sustained).
- Pi Zero peak current during WiFi TX -- 500 mA peak is the spec, but the 1A polyfuse and bulk caps need to be verified on a scope under real WiFi load.
- Schottky OR-ing forward drop -- SS14 has Vf ≈ 0.45 V at 1 A. With both USB and bus 5V present, the higher rail wins. Confirm both rails stay above 4.75 V at the Olimex VBUS pin under full Pi Zero + level-shifter load.
- PIO trigger latency end-to-end -- from /BAPR falling edge on the bus to PIO IRQ in the MCU. Calculated ~70-90 ns; needs real measurement.
Nice to have (not blocking)¶
- Card detect signal -- the BB48R microSD slot has a card-detect line; expose it to the application as a status flag.
- Per-device status LEDs -- one LED per emulated device on the front edge of the card. Driven via 74HC595 to keep GPIO cost at 3 pins. Already designed in the LED Indicator section.
- Console UART -- USB CDC is the primary debug channel; UART0 on GPIO0/1 is taken by the Pi Zero handshake. Live with USB CDC.
- JTAG/SWD header -- BB48R already exposes SWDIO/SWCLK on the SWD1 connector at the top of the module. No additional header needed on the controller card.
Pin Allocation Summary¶
This is the locked-in pin map for the Olimex RP2350-PICO2-BB48R. Six GPIO are reserved by the module itself (GPIO8 PSRAM, GPIO9-11 SD, GPIO24 SD, GPIO25 LED). The remaining 42 GPIO are allocated below. Detailed reasoning is in the "Pin Layout for Latched Architecture" and "BD 0-23 Bus Interface Architecture" sections earlier in this document — this section is the single-page reference.
BB48R MODULE-RESERVED (do not touch):
GPIO8 -> PSRAM CS (QMI_CS1n) ............... 8 MB QSPI PSRAM
GPIO9 -> SD card CS (SPI1_CSn) ............. on-board microSD
GPIO10 -> SD card CLK (SPI1_SCK / SD_CLK) ... on-board microSD
GPIO11 -> SD card CMD (SPI1_TX / SD_CMD) .... on-board microSD
GPIO24 -> SD card DAT0 (SPI1_RX / SD_DAT0) .. on-board microSD
GPIO25 -> User LED (User_Led) ............... on-board green LED
LOW BANK (GPIO0-31) -- Time-critical bus interface:
Pi Zero companion (optional, populated if Pi Zero is fitted):
GPIO0 -> INT_BB48R (output to Pi Zero, "I have data")
GPIO1 -> INT_FROM_ZERO (input from Pi Zero, optional)
Bus interrupts (always populated):
GPIO2 -> /BINT 12 drive (74LVC07 open-drain)
GPIO3 -> /BINT 13 drive (74LVC07 open-drain)
SPI0 to Pi Zero (optional, also pUEXT-compatible):
GPIO4 -> SPI0_RX / MISO (Pi Zero SPI master mode)
GPIO5 -> SPI0_CSn / CS (Pi Zero SPI master mode)
GPIO6 -> SPI0_SCK (Pi Zero SPI master mode)
GPIO7 -> SPI0_TX / MOSI (Pi Zero SPI master mode)
PIO read group -- single contiguous 12-bit block (atomic IN PINS, 12):
GPIO12-19 -> DBUS 0-7 (shared 8-bit MCU<->latch bus)
GPIO20 -> /BAPR_IN (sniff) (PIO trigger bit 8)
GPIO21 -> /BIOXE_IN (sniff) (PIO trigger bit 9)
GPIO22 -> /BDAP_IN (sniff) (PIO trigger bit 10)
GPIO23 -> /BDRY_IN (sniff) (PIO trigger bit 11)
Latch control (LOW bank end -- close to PIO read group):
GPIO26 -> /OE_IN_0 (read-enable input latch 0, BD 0-7)
GPIO27 -> /OE_IN_1 (read-enable input latch 1, BD 8-15)
GPIO28 -> /OE_IN_2 (read-enable input latch 2, BD 16-23)
GPIO29 -> LE_OUT_0 (latch-enable output latch 0, BD 0-7)
GPIO30 -> LE_OUT_1 (latch-enable output latch 1, BD 8-15)
GPIO31 -> LE_OUT_2 (latch-enable output latch 2, BD 16-23)
HIGH BANK (GPIO32-47) -- Slower signals, no PIO timing constraint:
Bus drive enable:
GPIO32 -> /BD_OE_BUS (master OE for 3x 74LVT245 output drivers)
Other input sniffs (read directly via gpio_get(), no PIO):
GPIO33 -> /BMEM_IN (74LVC14 buffered)
GPIO34 -> /BINACK_IN (74LVC14 buffered)
GPIO35 -> /BMCL_IN (74LVC14 buffered, master clear)
GPIO36 -> /BINPUT_IN (74LVC14 buffered)
GPIO37 -> /INGRANT_IN (daisy chain in)
GPIO38 -> /INIDENT_IN (daisy chain in)
Bus interrupts (high pair):
GPIO39 -> /BINT 10 drive (74LVC07 open-drain)
GPIO40 -> /BINT 11 drive (74LVC07 open-drain)
Bus output drives (74LVC07 open-drain, except where noted):
GPIO41 -> /BAPR_OUT (drive BAPR during DMA cycles)
GPIO42 -> /BDRY_OUT (drive BDRY when responding)
GPIO43 -> /BINPUT_OUT (drive BINPUT during IOX read / DMA write)
GPIO44 -> /BDAP_OUT (drive BDAP during DMA cycles)
GPIO45 -> /BREQ_OUT (drive BREQ to request DMA)
Daisy-chain pass-through control (74LVC125 OE):
GPIO46 -> /OE_DAISY_GRANT (controls grant pass-through)
GPIO47 -> /OE_DAISY_IDENT (controls ident pass-through)
Pin Budget Check¶
| Group | Pins | GPIOs |
|---|---|---|
| Module reserved | 6 | 8, 9, 10, 11, 24, 25 |
| Pi Zero INT pair | 2 | 0, 1 |
| Bus interrupts (4 levels) | 4 | 2, 3, 39, 40 |
| Pi Zero SPI0 | 4 | 4, 5, 6, 7 |
| PIO read group (8 DBUS + 4 trigger) | 12 | 12-23 (contiguous) |
| Latch control (3 OE + 3 LE) | 6 | 26-31 |
| /BD_OE_BUS master enable | 1 | 32 |
| Other input sniffs | 6 | 33-38 |
| Bus output drives | 5 | 41-45 |
| Daisy chain control | 2 | 46-47 |
| Total used | 48 | all 48 GPIO |
Every GPIO is allocated. No spares. If a future requirement needs another pin, the candidates to free up are: GPIO0/1 (Pi Zero INT — collapse to a single bidirectional handshake), or one of the "other input sniffs" (e.g. fold /BINACK and /BMEM into a single PIO read group at the cost of one more PIO state machine). Anything beyond that requires moving to a different RP2350 module or adding an I/O expander.
Bank-by-bank Justification¶
| Why this pin lives where it lives | |
|---|---|
| GPIO12-23 contiguous | The 8 DBUS lines + 4 trigger sniffs must be one contiguous block in the LOW bank so a single PIO IN PINS, 12 instruction can sample them atomically with mask-compare. This is the entire reason DBUS is on GPIO12-19 instead of GPIO0-7 -- moving it gets it adjacent to the trigger group. |
| Latch control GPIO26-31 | LE/OE pulses must be deterministic (PIO sideset or PIO out pins). LOW bank is required for PIO sideset to share a state machine with the DBUS read instruction. GPIO26-31 is the next contiguous block after the read group with no module reservations in the way. |
| /BD_OE_BUS on GPIO32 | Master output enable does not pulse per byte -- it goes LOW once at the start of a drive cycle and HIGH once at the end. HIGH bank (GPIO32) is fine; no PIO timing constraint. |
| Bus output drives in HIGH bank | /BAPR_OUT, /BDRY_OUT, /BINPUT_OUT, /BDAP_OUT, /BREQ_OUT all change at human-perceptible bus-cycle granularity (hundreds of ns minimum). HIGH bank GPIO via standard SDK calls is fast enough -- no PIO needed. |
| Daisy chain GPIO46-47 | /OE_DAISY_GRANT and /OE_DAISY_IDENT switch state at most a few times per IDENT/DMA cycle. HIGH bank is fine. |
References¶
- ND-100-BUS-C-CONNECTOR.md - Complete bus signal reference
- ../../../Reference-Manuals/ND-06.016.01_NORD-100_Input_Output_System.md - NORD-100 I/O System reference manual
- Olimex-rp2350/RP2350-PICO2-BB48-user-manual.pdf - Olimex BB48R user manual
- Olimex-rp2350/RP-008373-DS-2-rp2350-datasheet.pdf - RP2350 datasheet
- 74LVC574, 74LVT245, 74LVC07, 74LVC14, 74LVC125 datasheets
Next Steps¶
Architecture is locked in. Remaining work:
- KiCad schematic capture -- translate this document into a real schematic. Use the LCSC parts from the JLCPCB section. Apply Schottky OR-ing for power, 1A polyfuse on Pi Zero 5 V, decoupling caps per IC.
- PCB layout -- 2-layer board, HASL, ~100 × 100 mm, JLCPCB. Top layer = signals + components. Bottom layer = ground pour with star ground at the regulator + a few crossover traces + power distribution. Place latches/drivers near the DIN 41612 connector to keep BD stub lengths short (<50 mm). Place the BB48R EXT1 header close to the input latches so DBUS 0-7 routing stays under 30 mm. Pi Zero header on the opposite edge from the DIN connector.
- First firmware skeleton -- pico-sdk project with the three PIO state machines (SM_SIGNAL, SM_ADDR, SM_DATA), the Central Bus Drive API, and a stub IOX/IDENT responder. No device emulation yet.
- Bench bring-up -- power up alone (no backplane), verify 3.3 V rail, USB CDC console, GPIO sanity. Then plug into a single-slot test backplane with no real CPU and prove the level shifters and latches work end-to-end.
- Backplane design + manufacture -- 4-slot backplane PCB, PC PSU power, debug access for the logic analyzer. Ordered separately from JLCPCB.
- First real bus cycle -- plug into a real ND-100 CPU on the backplane, prove we can respond to a single IOX read with a known constant. This is the first "alive" milestone.
- Terminal device emulation -- simplest device class. Implements IOX read/write only, no DMA, no IDENT initially.
- IDENT/interrupt support -- add /BINT drive + IDENT response for the terminal device.
- Floppy device emulation -- next-simplest. Adds DMA but small transfer sizes.
- DMA throughput tuning -- benchmark word/sec on the BD path, tune PIO program if needed.
- HDLC and SMD device emulation -- the high-bandwidth devices. Validate at full SMD throughput.
- CPU mode firmware -- prove the same PCB can act as the bus master (ND-100 CPU emulator side).