349 lines
14 KiB
Markdown
349 lines
14 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
/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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```c
|
|
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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```sh
|
|
python3 analysis/netbridge-emulator/netbridge_emu.py --pty
|
|
```
|
|
|
|
Run as a QEMU socket backend:
|
|
|
|
```sh
|
|
python3 analysis/netbridge-emulator/netbridge_emu.py --unix-socket emulation/work/netbridge.sock
|
|
```
|
|
|
|
Test:
|
|
|
|
```sh
|
|
python3 -m unittest discover -s analysis/netbridge-emulator
|
|
```
|
|
|
|
Verification performed:
|
|
|
|
```text
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```text
|
|
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.
|