Files
pokemon_pinball_wiki/netbridge-re.md
2026-07-10 22:39:27 -05:00

14 KiB

Netbridge Reverse Engineering and Emulator

Scope

Target artifacts:

  • /games/pokemon_pro/spike3/netbridge/netbridge.elf
  • /games/pokemon_pro/spike3/netbridge/spike3-swd.cfg
  • /games/pokemon_pro/spike3/netbridge/rp2040.cfg
  • /games/pokemon_pro/spike3/netbridge/version.txt

Generated artifacts:

  • analysis/netbridge-emulator/netbridge_emu.py
  • analysis/netbridge-emulator/test_netbridge_emu.py
  • analysis/netbridge-emulator/README.md

Static caveat: this page is based on static rootfs evidence and Ghidra decompilation of the RP2040 firmware. It has not been validated against live cabinet hardware or a logic-analyzer capture.

Firmware Identity

Confirmed facts:

  • file games/pokemon_pro/spike3/netbridge/netbridge.elf reports a 32-bit little-endian ARM EABI5 executable, statically linked, with debug info, not stripped.
  • SHA-256 of /games/pokemon_pro/spike3/netbridge/netbridge.elf: 05b7f7c329da79fb24078df69c8401c7a47be5c8a855b73fb937e052bce28d85.
  • /games/pokemon_pro/spike3/netbridge/version.txt contains 0.98.4.
  • Ghidra import path used for this pass: /pokemon_spike3/netbridge.elf.
  • Ghidra auto-detected language ARM:LE:32:v8.
  • arm-none-eabi-readelf -l maps executable flash at virtual address 0x10000000, file offset 0x1000, size 0x12b40.
  • arm-none-eabi-nm -n shows __flash_binary_start at 0x10000000, __logical_binary_start at 0x10000100, and __flash_binary_end at 0x10013270.

Interpretation: the image is RP2040/Pico-SDK style firmware loaded at XIP flash base 0x10000000. The host-side game programs or verifies it through OpenOCD before talking to it over UART.

Host Programming Path

The game and SPIKE menu binaries both contain these strings:

/dev/ttyAMA2
/dev/ttyAMA5
netbridge image okay, CRC32=0x%08x
netbridge image CRC mismatch, write new image
openocd -d0 -f %s -f %s -c "program %s preverify verify reset exit"
failed to init bridge thread
/games/spike3/netbridge/spike3-swd.cfg
/games/spike3/netbridge/rp2040.cfg
/games/spike3/netbridge/netbridge.elf

String offsets:

  • /games/pokemon_pro/game: /dev/ttyAMA2 at 0x25e9468, /dev/ttyAMA5 at 0x25e9478, OpenOCD command format at 0x25e9998.
  • /games/pokemon_pro/spike3/spike_menu/game: /dev/ttyAMA2 at 0x3845a8, /dev/ttyAMA5 at 0x3845b8, OpenOCD command format at 0x384ae8.

spike3-swd.cfg configures OpenOCD bcm2835gpio SWD:

  • SWCLK: Raspberry Pi GPIO 11.
  • SWDIO: Raspberry Pi GPIO 10.
  • Adapter speed: 1000 kHz.

rp2040.cfg is the normal RP2040 dual-core OpenOCD target config with flash bank at 0x10000000.

Inference: startup tries to verify the RP2040 flash image, rewrites it through Pi GPIO bit-banged SWD if the CRC does not match, resets the RP2040, then opens a Pi UART for bridge messages.

Main Firmware Loop

Ghidra decompilation of main shows this initialization order:

  1. Enable watchdog with 1000 ms timeout.
  2. Initialize ADC/version sensing.
  3. Initialize nodebus.
  4. Initialize host UART.
  5. Initialize status LED, backbox light, power-fail handling, LCD, LCD backlight, I2S, power distribution, WS2812, and DMD.
  6. Send reset flags to host with command 0x22.
  7. Send version to host with command 0x20.
  8. Loop forever updating watchdog, status, backbox light, WS2812, LCD, power distribution, ADC, host command handling, and LCD again.

The decompiled HOST_SendVersion sends four bytes:

00 62 04 <adc_version>

This matches version 0.98.4; the fourth byte is the ADC-board/version-ladder result or 0xff if unavailable.

Pi-Facing Host UART Protocol

HOSTUART_Init configures:

  • RP2040 GPIO 16 and GPIO 17 to function 2.
  • UART base 0x40034000, consistent with RP2040 uart0.
  • Baud: 2000000.
  • Format: 8 data bits, 1 stop bit, no parity per decompiled arguments.
  • RX/TX FIFOs: host RX 0x1000 bytes, host TX 0x400 bytes.

HOSTUART_SendMessage and HOSTUART_GetMessage implement byte-stuffed frames:

0x02                       start
command                    escaped if 0x02, 0x03, or 0x1b
sequence                   escaped if needed
payload bytes              escaped if needed
checksum                   escaped if needed
0x03                       end

Escaping:

  • Literal 0x02, 0x03, or 0x1b is sent as 0x1b followed by value | 0x80.
  • RX reverses escaping by masking the byte after 0x1b with 0x7f.

Checksum:

  • The checksum byte makes sum(command, sequence, payload..., checksum) & 0xff == 0.
  • Equivalently, checksum = -sum(command, sequence, payload) & 0xff.

Host Command Map

Ghidra decompilation of HOST_Update gives this first-pass command map:

Command Meaning Response
0x10..0x1f Power-distribution passthrough on SPIKE 3; legacy voltage/frequency/power commands on non-SPIKE 3 path Usually forwarded or returns one-byte/few-byte synthetic hardware state
0x20 Netbridge version 00 62 04 <adc_version>
0x21 Backbox light set/get Optional one-byte brightness response
0x22 Reset flags One byte
0x23 Bridge status, then clear 4-byte little-endian status
0x24 VBB millivolts 4-byte little-endian value
0x25 Nodebus poll/discovery One byte node address, or 0
0x27 LCD reset/status/write/backlight subcommands Status response for query paths
0x28 DMD status/test/write Status response for query path
0x29 WS2812 count/debug setup No direct response in the decoded path
0x2a WS2812 direct data path No direct response in the decoded path
0x2b WS2812 pattern path No direct response in the decoded path
0x2c I2S enable No direct response
0x2f WS2812 node LED path No direct response in the decoded path
0x30 Netbridge firmware CRC 4-byte CRC32
0x40 Extended bridge command namespace Subcommand-dependent response
0x41 Direct nodebus request/response path Node response bytes plus bridge status

Extended command 0x40 subcommands currently identified:

Subcommand Meaning
0x00 Nodebus poll/discovery
0x03 Version bytes 00 62 04
0x04 Enter/exit node break mode
0x05 Bridge status, then clear
0x06 Set node response-time timeout in milliseconds
0x09 Backbox light helper
0x0a Backbox light get helper
0x0c Deliberate infinite loop / hang path
0x0d Four-slot seeded hash/mixing helper

The 0x30 CRC is computed in firmware as:

CRC32(0, &__logical_binary_start, 0x12a40)

For this ELF, __logical_binary_start maps to file offset 0x1100. A Python binascii.crc32 over games/pokemon_pro/spike3/netbridge/netbridge.elf[0x1100:0x1100+0x12a40] gives 0x357f5327.

Nodebus Layer

NODE_Init configures a mixed PIO/hardware-UART nodebus:

  • PIO block base 0x50300000, consistent with RP2040 PIO1.
  • PIO state machine 0 uses uart_tx_txen_program.
  • GPIO 20 and GPIO 19 are assigned PIO function 7 around the transmit/TX-enable path.
  • GPIO 21 is assigned UART function 2 around the receive path.
  • Hardware UART base 0x40038000, consistent with RP2040 uart1.
  • Decompiled baud literal is 0x70800, decimal 460800.

NODEUART_WriteAddress(address) resets node RX state and sends address | 0x180 into the PIO TX FIFO. NODEUART_Write(byte) subtracts the byte from nodeTxCs and queues it. NODEUART_SendEnd(count) sets the expected receive count and response timing window.

NODEBUS_Poll performs a binary-search-style poll over node addresses up to 0x3f, using NODEUART_WritePoll(address). It returns the discovered address or 0.

Direct host command 0x41 has special handling in HOSTUART_GetMessage: as the host frame is parsed, the payload is echoed to the physical nodebus. The payload shape inferred from the decompile and the earlier game-side node wrapper is:

byte 0: node address, high bit accepted by higher layers
byte 1: node argument count
byte 2..N: node command and arguments
last: expected response length

The bridge then reads node response bytes and sends host command 0x41 back with the node response followed by bridge status. This is the lower layer used by the previously documented Magikarp/topper commands such as 0xfc.

Other Hardware Functions

Confirmed from Ghidra decompilation:

  • ADC/version sensing uses GPIO 26 and GPIO 29 as ADC inputs.
  • LCD helper initializes GPIO 3, 6, and 7.
  • LCD backlight uses GPIO 14 PWM plus GPIO 15 enable/control.
  • Backbox light uses GPIO 23 PWM.
  • Status output uses GPIO 25.
  • Power-fail output/input uses GPIO 24.
  • Power-distribution UART uses PIO1 state machines 2 and 3, with GPIO 27 TX and GPIO 28 RX.
  • WS2812 output uses PIO0 and three configured output pin slots from the firmware's pins table.
  • DMD work is launched on RP2040 core 1.

Open validation:

  • Exact board connector routing for each RP2040 GPIO.
  • Nodebus electrical level, half-duplex direction details, and break timing on real hardware.
  • Exact meaning of every bridgeStatus bit.
  • Which of /dev/ttyAMA2 and /dev/ttyAMA5 is the Pi-facing host UART on the real image boot path.

Emulator

analysis/netbridge-emulator/netbridge_emu.py implements a first host-side recreation in Python.

It currently provides:

  • STX/ETX/ESC host frame codec.
  • Host checksum encode/decode.
  • Commands 0x20, 0x22, 0x23, 0x24, 0x25, 0x27, 0x28, 0x30, 0x40, and 0x41.
  • Inert ACK/no-response handling for early LCD/DMD/WS2812/I2S paths.
  • Firmware CRC response 0x357f5327.
  • PTY, serial-device, and Unix-socket serving modes for QEMU or host-side serial plumbing.
  • Existing-node stub at address 0x0a with Magikarp/topper-oriented commands 0xf2, 0xfc, 0xfd, 0xfe, and 0xff.

Run:

python3 analysis/netbridge-emulator/netbridge_emu.py --pty

Run as a QEMU socket backend:

python3 analysis/netbridge-emulator/netbridge_emu.py --unix-socket emulation/work/netbridge.sock

Test:

python3 -m unittest discover -s analysis/netbridge-emulator

Verification performed:

python3 -m unittest discover -s analysis/netbridge-emulator
....
Ran 4 tests in 0.000s
OK

python3 -m py_compile analysis/netbridge-emulator/netbridge_emu.py analysis/netbridge-emulator/test_netbridge_emu.py

Emulation Strategy

For QEMU:

  1. Set SPIKE3_NETBRIDGE_EMU=1 for the Docker/QEMU scaffold.
  2. The container starts netbridge_emu.py --unix-socket /workspace/emulation/work/netbridge.sock.
  3. QEMU attaches that socket as a second PL011 serial port, which appears as guest /dev/ttyAMA1.
  4. Guest setup symlinks /dev/ttyAMA5 to /dev/ttyAMA1, because current preload logs show the target opening /dev/ttyAMA5.
  5. launch-target.sh exports SPIKE3_REAL_NETBRIDGE_SERIAL=1, causing the preload shim to stop faking /dev/ttyAMA5.
  6. If the game still tries to run OpenOCD before the UART bridge thread is useful, either allow the CRC/status path to bypass programming or stub OpenOCD success until the UART bridge thread opens.
  7. Route dashboard switch events into the emulator's node model, not directly into the game.

Docker command:

SPIKE3_NETBRIDGE_EMU=1 SPIKE3_AUTOSTART=game docker compose -f emulation/compose.yaml up --build

Current validation boundary: the Python emulator tests, shell syntax checks, and QEMU wiring edits were verified locally, but the full Docker boot path with SPIKE3_NETBRIDGE_EMU=1 has not yet been run to confirm the game's bridge thread accepts the emulated serial device.

For a microcontroller recreation:

  • RP2040 is the best exact physical target because the original uses RP2040 PIO for node TX/TX-enable, WS2812, and power-distribution UART.
  • ESP32 can still emulate the Pi-facing protocol if it only replaces the bridge at the host UART layer, or if its UART/RMT/peripheral timing can satisfy the nodebus physical layer.
  • The minimum viable firmware only needs host UART framing, command responses, and a nodebus model that returns payload || checksum || status for command 0x41.

Pico Firmware Recreation

analysis/netbridge-pico-firmware/ contains a Pico SDK recreation of the same Pi-facing protocol layer.

Files:

  • CMakeLists.txt
  • include/netbridge_protocol.h
  • src/netbridge_protocol.c
  • src/netbridge_model.c
  • src/pico_main.c
  • tests/host_protocol_test.c
  • README.md

Implemented behavior:

  • UART0 on RP2040 GPIO 16/17 at 2,000,000 baud.
  • Same STX/ETX/ESC host framing and checksum as the original netbridge.
  • Same core command stubs as the Python emulator.
  • Firmware CRC response 0x357f5327.
  • Stub nodebus device at address 0x0a.
  • Pico GPIO 25 heartbeat LED.

Portable host-core test:

cmake -S analysis/netbridge-pico-firmware -B /tmp/spike3-netbridge-host
cmake --build /tmp/spike3-netbridge-host --target netbridge_host_test
/tmp/spike3-netbridge-host/netbridge_host_test

Pico UF2 build, once the Pico SDK is available:

export PICO_SDK_PATH=/path/to/pico-sdk
cmake -S analysis/netbridge-pico-firmware -B /tmp/spike3-netbridge-pico
cmake --build /tmp/spike3-netbridge-pico --target spike3_netbridge_pico

Verification performed for this pass:

cmake -S analysis/netbridge-pico-firmware -B /tmp/spike3-netbridge-host
cmake --build /tmp/spike3-netbridge-host --target netbridge_host_test
/tmp/spike3-netbridge-host/netbridge_host_test
netbridge host protocol tests passed

The UF2 was not built in this pass because PICO_SDK_PATH is not configured in the workspace environment.

Confidence

High confidence:

  • RP2040 firmware identity, version, OpenOCD programming path, and SWD GPIO mapping.
  • Pi-facing UART frame format and checksum.
  • Host command meanings for version, status, CRC, node poll, and direct node request.
  • Host UART baud and RP2040 GPIO 16/17 assignment.
  • Nodebus use of PIO plus UART1 and command 0x41 as the node request path.

Moderate confidence:

  • The Python emulator or Pico firmware recreation is sufficient to clear early bridge-thread and netbridge CRC paths once connected to the correct guest UART.
  • Default zero/success responses for LCD/DMD/WS2812/I2S are enough for early game boot.

Low confidence until hardware-tested:

  • Physical nodebus timings, connector pins, and voltage levels.
  • Full power-distribution behavior.
  • Complete bridgeStatus bit semantics.