Skip to content

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 the sequence back 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 Write block 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.