Communication bus between Raspberry Pi 5 and Xiao RP2350¶
This bus has several constraints:
- It must be managed by a master (Pi 5).
- It should keep the system wiring simple.
- It must guarantee a precise 30 Hz update rate for 5 servomotors' positions.
- The positions are integers ranging from 0 to 1023.
I2C is an excellent solution because it allows connecting both the XIAO (head) and the Arduino Nano Every (torso) on the same two wires (SDA and SCL), it is natively supported by the Raspberry Pi 5, and it frees up GPIO pins.
The Master (Pi 5) / Slave (XIAO) topology perfectly fits the choreographer role of the Pi. To meet the 30 Hz requirement efficiently, instead of parsing text strings, the Pi writes and reads continuous binary blocks (structs) directly to and from the XIAO. This is fast, robust, and easily extensible.
I2C Protocol overview¶
- Protocol: I2C
- Pi 5 Role: Master
- XIAO RP2350 Role: Slave (e.g., address
0x30) - Speed: Fast Mode (400 kHz) recommended to ensure short transaction times.
- Data Format: Raw binary (Little-Endian for 16-bits integers).
To modify the frames in the future, simply append new variables to the C++ struct on the XIAO side, and update the Python struct.pack/unpack mapping on the Pi side.
I2C Frames¶
Raspberry Pi 5 to XIAO RP2350 (Master Write)¶
Command block (PiCommand) sent by the Pi to dictate the head's behavior.
Cadence: 30 Hz Total Size: 15 bytes
| Position (Byte) | Variable | C++ Type | Range | Description |
|---|---|---|---|---|
| 0 | sequence |
uint8_t |
0-255 |
Incrementing frame counter, echoed back by the XIAO. |
| 1..2 | servo_1_target |
uint16_t |
0-1023, or 2000 |
Right Antenna target position (mirrored in firmware so 0=back, 1023=front on both antennas). |
| 3..4 | servo_2_target |
uint16_t |
0-1023, or 2000 |
Left Antenna target position. |
| 5..6 | servo_3_target |
uint16_t |
0-1023, or 2000 |
Neck 1 target position. |
| 7..8 | servo_4_target |
uint16_t |
0-1023, or 2000 |
Neck 2 (pitch) target position. |
| 9..10 | servo_5_target |
uint16_t |
0-1023, or 2000 |
Neck 3 (rotation) target position. |
| 11 | eye_mode |
uint8_t |
0-255 |
Reserved for LEDs mode; currently received and stored by the firmware but not applied (LEDs not implemented yet). |
| 12 | vibrator_level |
uint8_t |
0-255 |
Vibration intensity (0 = stopped), applied every frame. |
| 13 | command_flags |
uint8_t |
bitmask | Bit 0 (0x01): Arm servos, Bit 1 (0x02): Emergency stop. Either case also forces the vibrator to 0. |
| 14 | system_state |
uint8_t |
enum | 0: Normal, 1: Shutting Down (stored, no behavior wired yet), 10: Alive Idle (antennas move on their own, see Head firmware → "Alive idle" wander). |
Per-axis "ignore" sentinel: a target value of exactly 2000 (outside the valid 0-1023
range) tells the XIAO to leave that one axis alone for this frame — the other 4 axes are
still updated normally.
XIAO RP2350 to Raspberry Pi 5 (Master Read)¶
Telemetry block (PicoTelemetry) requested by the Pi to read the head sensors.
Cadence: 10 to 30 Hz Total Size: 28 bytes
| Position (Byte) | Variable | C++ Type | Range | Description |
|---|---|---|---|---|
| 0 | sequence_echo |
uint8_t |
0-255 |
Echo of the last received sequence from the Pi. |
| 1 | touch_left |
uint8_t |
0..1 |
Left touch sensor state (1 = touched). |
| 2 | touch_right |
uint8_t |
0..1 |
Right touch sensor state (1 = touched). |
| 3..4 | distance_cm |
uint16_t |
0-400 |
Distance sensor reading in centimeters; holds the last good reading if the sensor drops out. |
| 5..6 | imu_accel_x |
int16_t |
raw | Raw X-axis acceleration, ±4g full scale (~8192 LSB/g). |
| 7..8 | imu_accel_y |
int16_t |
raw | Raw Y-axis acceleration. |
| 9..10 | imu_accel_z |
int16_t |
raw | Raw Z-axis acceleration. |
| 11..12 | imu_gyro_x |
int16_t |
raw | Raw X-axis gyroscope, ±1000 dps full scale (~32.8 LSB/dps). |
| 13..14 | imu_gyro_y |
int16_t |
raw | Raw Y-axis gyroscope. |
| 15..16 | imu_gyro_z |
int16_t |
raw | Raw Z-axis gyroscope. |
| 17 | xiao_status |
uint8_t |
enum | Local health status (0: OK, 1: Error/failsafe active). |
| 18..19 | servo_1_pos |
uint16_t |
0-1023 |
Right Antenna, last position actually written to the servo (already mirrored). |
| 20..21 | servo_2_pos |
uint16_t |
0-1023 |
Left Antenna, last position actually written to the servo. |
| 22..23 | servo_3_pos |
uint16_t |
0-1023 |
Neck 1, last position actually written to the servo. |
| 24..25 | servo_4_pos |
uint16_t |
0-1023 |
Neck 2 (pitch), last position actually written to the servo. |
| 26..27 | servo_5_pos |
uint16_t |
0-1023 |
Neck 3 (rotation), last position actually written to the servo. |
The servo_X_pos fields report the position actually sent over the serial bus to each servo (WritePos), not just the target the motion profile is still moving toward. Since the XIAO applies speed/acceleration limiting to every move, these fields reflect the movement currently in progress rather than jumping straight to the commanded target.
Alive idle mode (system_state = 10)¶
Sending system_state = 10 (with the frame armed and not in emergency stop) switches the head's two antennas to an autonomous "alive" idle behavior instead of tracking servo_1_target/servo_2_target. This is XIAO-internal firmware behavior, not part of the wire contract — see Head firmware → "Alive idle" wander for the full description.
Safety behaviors (Failsafe)¶
- Frame tracking (
sequence): The XIAO mirrors thesequenceback so the Pi can calculate latency or detect dropped payloads. - I2C Watchdog: The Xiao firmware must implement a timer (e.g., 200 ms). If no
Writeblock is received from the Pi within this time, the I2C connection is considered lost (Pi crash/freeze). The Xiao must immediately enter a safe state: cut servo power or move them to a safe resting position, and turn off LEDs and vibration.