Architecture — overview¶
RPBridge is a three-layer system with a narrow wire protocol between the layers. Each layer is responsible for exactly one thing and can be tested in isolation.
┌──────────────────────────────────────────────────────────────┐
│ Linux host │
│ │
│ Userspace tools │
│ ┌──────────────┐ ┌───────────┐ ┌─────────┐ ┌──────────┐ │
│ │ i2c-tools │ │ libgpiod │ │ can-utils│ │ iio-utils│ │
│ └──────┬───────┘ └─────┬─────┘ └────┬────┘ └────┬─────┘ │
│ │ │ │ │ │
│ /dev/i2c-N /dev/gpiochipN can0 iio:deviceN │
│ │ │ │ │ │
│ ┌──────┴───────┐ ┌─────┴─────┐ ┌────┴────┐ ┌────┴─────┐ │
│ │ i2c-core │ │ gpiolib │ │ gs_usb │ │ iio │ │
│ └──────┬───────┘ └─────┬─────┘ └────┬────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └────┬───────────┴──── auxiliary_bus ────────┘ │
│ │ │
│ ┌───────────┴───────────────────────────────────────────────┐│
│ │ rpbridge.ko (Rust, kernel) ││
│ │ USB-if matcher · RPBP demux · CRC-32C · cap parser ││
│ └───────────────────┬───────────────────────────────────────┘│
└─────────────────────┬│──────────────────────────────────────┘
USB 2.0 Full Speed
┌─────────────────────┼────────────────────────────────────────┐
│ ▼ │
│ RP2354B (150 MHz dual M33, 2 MB flash) │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Core 0 │ │
│ │ TinyUSB · RPBP framing · command dispatch │ │
│ │ capability CBOR emitter · pin broker │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Core 1 │ │
│ │ I/O scheduler · DMA IRQ · PIO service · streams │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ HW peripherals: 2×UART · 2×I²C · 2×SPI · 24×PWM · 8×ADC │
│ PIO0: can2040 (3 SMs) — CAN 2.0B via software controller │
│ PIO1: extra UARTs, RS485-DE precision timing │
│ PIO2: 1-Wire, WS2812, spare applets │
│ │
│ Transceivers: SN65HVD230 (CAN) · SN65HVD72 (RS485) │
│ MAX3232 (RS232) · 4× CDC-ACM (host side) │
└──────────────────────────────────────────────────────────────┘
Layer boundaries¶
| Layer | Lives in | Language | Talks to |
|---|---|---|---|
| Userspace tools | Distro packages | any | Kernel subsystems |
| Userspace Rust | userspace/ |
Rust | USB via nusb |
| Kernel driver | driver/ |
C | USB core + subsystems |
| RPBP wire format | userspace/rpbridge-protocol/, firmware/src/rpbp/, driver/src/rpbp.rs |
Rust + C | bytes over bulk EP |
| Firmware | firmware/ |
C | RP2354B hardware |
| Hardware | hardware/ |
KiCad | physics |
Why two USB interface families¶
A single vendor interface would have been enough theoretically, but reusing mainline kernel drivers where they exist saves us real work:
- CDC-ACM × 4 — three views of UART1 through different transceiver
banks (RS232 / RS485 / TTL) plus UART2 via PIO. The firmware
switches the UART1 bank based on which CDC currently holds
DTR=1. Handled bycdc_acm(Linux) /usbser.sys(Windows); every terminal andtio-class tool works untouched. The debug log is NOT on a CDC —stdio(printf) goes to UART1 hardware on the 5-pin debug header (GP4/GP5). - gs_usb vendor — a single CAN interface speaking the gs_usb /
candleLight protocol. Handled by
drivers/net/can/usb/gs_usb.koon Linux; handled bycandle_api,python-canand SavvyCAN on Windows. - RPBP vendor — everything else: I²C, SPI, GPIO, PWM, ADC, 1-Wire, UART phy selection, control plane, OTA. Our own C driver owns this interface.
Data-path examples¶
I²C read of a BME280:
- Userspace calls
read(/dev/i2c-7, buf, 6). i2c-devforwards to thei2c_adapterregistered by the RPBridge driver'srpbridge-i2cauxiliary child.- Child composes a
CMD_REQUEST{ I2C_XFER, bus=0, addr=0x76, len=6 }RPBP frame, CRC-seals it, hands it to the core driver. - Core driver submits a bulk-OUT URB, waits on a completion keyed by
seq. - Device firmware reads the frame, runs a
i2c_read_timeout_us()via the HW I²C controller (or its PIO equivalent), formats aCMD_RESPONSE, pushes it back on bulk-IN. - Core driver validates CRC, matches
seq, wakes the waiting i2c child, which returns the buffer to i2c-dev.
Round-trip budget: 400 µs on a warm link with short payloads.
GPIO IRQ delivery is the reverse, asynchronous path: firmware's
GPIO ISR pushes an EVENT frame on the events channel (1); driver's
URB completion callback routes it to the right rpbridge-gpio
aux child, which calls generic_handle_irq_safe() for the line's IRQ
domain. End-to-end p99 budget: 1 ms.