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.pyanalysis/netbridge-emulator/test_netbridge_emu.pyanalysis/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.elfreports 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.txtcontains0.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 -lmaps executable flash at virtual address0x10000000, file offset0x1000, size0x12b40.arm-none-eabi-nm -nshows__flash_binary_startat0x10000000,__logical_binary_startat0x10000100, and__flash_binary_endat0x10013270.
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/ttyAMA2at0x25e9468,/dev/ttyAMA5at0x25e9478, OpenOCD command format at0x25e9998./games/pokemon_pro/spike3/spike_menu/game:/dev/ttyAMA2at0x3845a8,/dev/ttyAMA5at0x3845b8, OpenOCD command format at0x384ae8.
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:
- Enable watchdog with 1000 ms timeout.
- Initialize ADC/version sensing.
- Initialize nodebus.
- Initialize host UART.
- Initialize status LED, backbox light, power-fail handling, LCD, LCD backlight, I2S, power distribution, WS2812, and DMD.
- Send reset flags to host with command
0x22. - Send version to host with command
0x20. - 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 RP2040uart0. - Baud:
2000000. - Format: 8 data bits, 1 stop bit, no parity per decompiled arguments.
- RX/TX FIFOs: host RX
0x1000bytes, host TX0x400bytes.
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, or0x1bis sent as0x1bfollowed byvalue | 0x80. - RX reverses escaping by masking the byte after
0x1bwith0x7f.
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 RP2040uart1. - Decompiled baud literal is
0x70800, decimal460800.
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
pinstable. - 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
bridgeStatusbit. - Which of
/dev/ttyAMA2and/dev/ttyAMA5is 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, and0x41. - 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
0x0awith Magikarp/topper-oriented commands0xf2,0xfc,0xfd,0xfe, and0xff.
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:
- Set
SPIKE3_NETBRIDGE_EMU=1for the Docker/QEMU scaffold. - The container starts
netbridge_emu.py --unix-socket /workspace/emulation/work/netbridge.sock. - QEMU attaches that socket as a second PL011 serial port, which appears as guest
/dev/ttyAMA1. - Guest setup symlinks
/dev/ttyAMA5to/dev/ttyAMA1, because current preload logs show the target opening/dev/ttyAMA5. launch-target.shexportsSPIKE3_REAL_NETBRIDGE_SERIAL=1, causing the preload shim to stop faking/dev/ttyAMA5.- 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.
- 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 || statusfor command0x41.
Pico Firmware Recreation
analysis/netbridge-pico-firmware/ contains a Pico SDK recreation of the same Pi-facing protocol layer.
Files:
CMakeLists.txtinclude/netbridge_protocol.hsrc/netbridge_protocol.csrc/netbridge_model.csrc/pico_main.ctests/host_protocol_test.cREADME.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
0x41as 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
bridgeStatusbit semantics.