Communication bus between Raspberry Pi 5 and Arduino Nano Every¶
This bus has several constraints:
- It must be managed by a master (Pi 5).
- It must share the same two wires as the head controller, without adding new GPIO.
- It carries a full RGB ring payload (7 LEDs) in a single transaction.
- It must return the battery measurement of the whole robot.
I2C is the natural choice here: the Nano Every (torso) sits on the same SDA / SCL pair as the XIAO RP2350 (head), at a different address, so a single 2-wire run serves both boards.
As on the head bus, the Pi writes and reads continuous binary blocks (structs) rather than text strings. On the Nano Every the ring payload alone is 21 bytes, so a text protocol would be both slower and harder to keep in sync.
I2C Protocol overview¶
- Protocol: I2C
- Pi 5 Role: Master
- Arduino Nano Every Role: Slave, address
0x40(hardwareWire, pinsA4/A5) - Speed: imposed by the master; the slave has no clock setting of its own.
- Data Format: Raw binary (Little-Endian for 16-bit integers).
Two I2C buses coexist on the Nano Every and must not be confused:
| Bus | Pins | Role of the Nano | Device |
|---|---|---|---|
Hardware Wire |
A4 / A5 |
Slave | Raspberry Pi 5 link (0x40) |
| Bit-banged software I2C | SOFT_I2C_SDA / SOFT_I2C_SCL |
Master | INA226 power monitor (0x40) |
Both devices answer at 0x40, which is harmless because they are on physically separate wires. The bit-banged master is implemented in power_monitor.cpp precisely so that the hardware peripheral stays fully dedicated to the Pi link — see torso firmware for the module breakdown.
The Nano Every is a 5 V board. The bus pull-ups must be referenced to the Pi 5's 3.3 V rail (or a level translator used), so that nothing ever drives 5 V into the Pi's GPIO.
To extend the frames later, append new fields at the end of the CommandFrame / TelemetryFrame structs, update WRITE_FRAME_SIZE / READ_FRAME_SIZE, and update the Python struct.pack / unpack mapping on the Pi side. Note that the write length check is strict (see Frame behavior), so both sides must be updated together.
I2C Frames¶
Raspberry Pi 5 to Arduino Nano Every (Master Write)¶
Command block (CommandFrame) sent by the Pi to drive the torso ring and the fan, plus a shutdown-request flag. The Pi's halt confirmation is not part of this frame — see the note on byte 26 below.
Cadence: free, defined by the Pi (the firmware is purely event-driven) — Total Size: 27 bytes
| Position (Byte) | Variable | C++ Type | Range | Description |
|---|---|---|---|---|
| 0 | sequence |
uint8_t |
0-255 |
Incrementing frame counter, echoed back by the Nano. |
| 1 | fan_speed |
uint8_t |
0-255 |
Fan PWM duty on FAN_PWM (analogWrite). 0 = stopped, 255 = full speed. Applied every frame. |
| 2 | led_brightness |
uint8_t |
0-255 |
Global brightness of the ring (NeoPixel setBrightness), applied on top of the per-LED colors. |
| 3..23 | led_rgb[7][3] |
uint8_t |
0-255 |
Colors of the 7 ring LEDs, 3 bytes each in R, G, B order (see below). |
| 24 | button_led_brightness |
uint8_t |
0-255 |
Reserved for the head button LED; currently received and forwarded to the firmware but not applied (output not wired yet). |
| 25 | shutdown_request |
uint8_t |
0 / non-zero |
Shutdown request from the Pi, driven by the power.shutdown IPC topic. Any non-zero value is read as true. Currently stored only. |
| 26 | (reserved) | uint8_t |
Previously halt_ack. The Pi's halt confirmation is not carried over this bus — the Nano reads it directly on a dedicated GPIO pin, held high by the Pi while running; pin low means the Pi has halted. See Lifecycle & Modes § 3 for the full sequence. |
LED ring byte layout¶
| LED index | R | G | B |
|---|---|---|---|
| 0 | 3 | 4 | 5 |
| 1 | 6 | 7 | 8 |
| 2 | 9 | 10 | 11 |
| 3 | 12 | 13 | 14 |
| 4 | 15 | 16 | 17 |
| 5 | 18 | 19 | 20 |
| 6 | 21 | 22 | 23 |
The frame always carries the components in R, G, B order. The GRB reordering required by the WS2812 strip is done by the library on the Nano side, so the Pi never has to care about it.
Arduino Nano Every to Raspberry Pi 5 (Master Read)¶
Telemetry block (TelemetryFrame) requested by the Pi to read the power rail and the torso sensors.
Cadence: free, defined by the Pi — Total Size: 12 bytes
| Position (Byte) | Variable | C++ Type | Unit | Range | Description |
|---|---|---|---|---|---|
| 0 | sequence_echo |
uint8_t |
0-255 |
Echo of the last sequence actually accepted from the Pi. 0 until the first valid frame. |
|
| 1..2 | pack_voltage_cV |
uint16_t |
centivolts (10 mV) | 0-4096 |
Battery pack voltage measured by the INA226. 1250 = 12.50 V. |
| 3..4 | bus_current_mA |
int16_t |
milliamps | -8191..8191 |
Bus current measured by the INA226. Sign depends on the shunt orientation. |
| 5 | power_switch_state |
uint8_t |
0..1 |
Physical power switch state. Currently hardcoded to 0 (not wired yet). |
|
| 6 | touch_left |
uint8_t |
0..1 |
Left torso touch sensor (1 = touched), raw digitalRead, no debounce. |
|
| 7 | touch_right |
uint8_t |
0..1 |
Right torso touch sensor (1 = touched), raw digitalRead, no debounce. |
|
| 8 | shutdown_notice |
uint8_t |
0..1 |
Nano asks the Pi to start shutting down. Currently always 0. |
|
| 9 | halt_request |
uint8_t |
0..1 |
Nano asks the Pi to halt now. Currently always 0. |
|
| 10 | shutdown_cause |
uint8_t |
enum | Reason of the shutdown request. 0: None. Currently always 0. |
|
| 11 | nano_status |
uint8_t |
enum | Local health status. 0: OK. Currently always 0. |
The telemetry block is rebuilt on every loop iteration, so a read always returns the freshest available values. It is also built once in setup(), before the I2C slave starts answering, so the Pi can never read an empty or half-initialized block.
Power measurement (INA226)¶
The two power fields come from an INA226 read over the software I2C bus, configured at boot as follows:
- Shunt: 0.010 Ω, PGA
/8(±81.92 mV), i.e. ±8.192 A full scale. CONFIG=0x4127: 1 sample average, 1.1 ms bus and shunt conversion time, continuous shunt + bus mode.CALIBRATION=2048, giving a Current LSB of exactly 0.25 mA/bit.- Bus voltage LSB is fixed at 1.25 mV; the firmware converts it to 10 mV steps with rounding.
If the INA226 does not acknowledge at boot, or if a register read fails, pack_voltage_cV and bus_current_mA fall back to their default values instead of holding the last good reading. Since nano_status is not wired yet, the Pi cannot currently distinguish a real 0 V / 0 mA measurement from a missing sensor — treat a sudden drop to zero as suspicious rather than as a flat battery.
Frame behavior¶
Strict write length. The receive interrupt only accepts a write of exactly 27 bytes. Any other length is fully flushed and discarded, so a truncated frame is never partially applied. A frame is therefore applied entirely or not at all.
Single-slot buffer. Only the latest received frame is kept. If two writes arrive before the main loop polls, the older one is silently dropped. sequence_echo lets the Pi detect it: if the echo skips a value, a frame was overwritten before being applied.
Atomic telemetry. The transmit buffer is filled with interrupts disabled, so a master read never observes a half-updated block; it gets either the previous frame or the new one.
Boot defaults. Actuators are initialized before the I2C slave starts, so the torso has a defined state even if the Pi never talks: fan at ~50% duty and ring lit in a low gray. The first command frame overrides both.
Applied on every frame. fan_speed, led_brightness and led_rgb are re-applied on each accepted frame; there is no "ignore" sentinel as on the head bus. The Pi must therefore send the complete desired state every time.
Safety behaviors (Failsafe)¶
- Frame tracking (
sequence): the Nano mirrors thesequenceback so the Pi can measure latency and detect dropped payloads. - I2C Watchdog: not implemented yet. If the Pi stops writing (crash or freeze), the Nano keeps the last commanded fan speed and ring colors indefinitely. A timer (e.g. 200 ms) must be added, after which the firmware falls back to a safe state — fan at a guaranteed cooling duty and a visible "link lost" ring pattern — and reports it through
nano_status.