The most reliable way to interface a Raspberry Pi and an Arduino for mixed-signal telemetry is over a hardware I2C bus using a bidirectional logic level shifter. While USB serial is easier to plug in, I2C frees up your USB ports, operates without the overhead of serial-to-USB bridge chips, and allows you to daisy-chain multiple microcontrollers on the same two wires.
This guide targets the Raspberry Pi 4 Model B (4GB) acting as the I2C Master, and the Arduino Nano Every (ATmega4809, 5V logic) acting as the I2C Slave. We will cover the exact hardware needed to prevent frying your Pi's 3.3V GPIO pins, the code to make them talk, and how to fix the inevitable bus errors.
The Architecture Decision: Why Combine a Pi and Arduino?
Before wiring anything, use this decision matrix to verify you actually need both boards. Modern microcontrollers are powerful, and over-engineering a project with a Linux SBC when a single MCU will do is a common bench mistake.
| If your project requires... | Then choose... |
|---|---|
| Local low-latency motor control + WiFi telemetry | ESP32 alone (Skip the Pi) |
| Computer vision, local MQTT broker, or InfluxDB logging | Raspberry Pi 4/5 alone (Use USB ADCs) |
| Heavy Linux processing + noisy 5V analog sensors or hardware interrupts | Pi + Arduino (Default Pick) |
The Verdict: Use the Pi Arduino combo when the Pi handles the 'heavy lifting' (OpenCV, network routing, database writes) and the Arduino handles 'dirty' hardware tasks (reading 5V industrial sensors, managing hardware debouncing, or driving high-current relays) where Linux kernel latency jitter would cause missed steps or bad ADC reads.
Parts List & Hardware Selection
The Raspberry Pi 4 operates at 3.3V logic. The Arduino Nano Every operates at 5V logic. Connecting them directly on an I2C bus will backfeed 5V into the Pi's BCM2711 SoC, permanently damaging the GPIO ring. You must use a level shifter.
| Component | Exact Variant | Approx. Cost | Why this specific part? |
|---|---|---|---|
| SBC (Master) | Raspberry Pi 4 Model B (4GB) | $55.00 | Hardware I2C on GPIO 2/3; sufficient RAM for Python logging. |
| MCU (Slave) | Arduino Nano Every (with headers) | $11.50 | ATmega4809 has better I2C silicon than the legacy ATmega328P. |
| Level Shifter | Adafruit 4-channel I2C-safe Bi-directional (ID: 757) | $4.95 | Uses NXP PCA9306. Crucially, it includes onboard pull-up resistors, unlike cheap MOSFET clones that cause bus capacitance issues. |
| Wiring | 22 AWG solid core jumper wires | $5.00 | Stranded wire loosens in breadboards over time, causing intermittent I2C drops. |
Pin Mapping & Wiring the I2C Bus
I2C requires a common ground and shared pull-up resistors on both the SDA (data) and SCL (clock) lines. Because we are using the Adafruit 757 module, the pull-ups are handled internally. Wire the boards exactly as mapped below.
| Raspberry Pi 4 (3.3V Side) | Wire Color | Adafruit Level Shifter | Wire Color | Arduino Nano Every (5V Side) |
|---|---|---|---|---|
| Pin 1 (3.3V Power) | Red | LV (Low Voltage) | - | - |
| Pin 4 (5V Power) | Orange | HV (High Voltage) | - | 5V Pin |
| Pin 6 (Ground) | Black | GND (Both sides) | Black | GND Pin |
| Pin 3 (GPIO 2 / SDA1) | Blue | LV1 | Green | HV1 -> A4 (SDA) |
| Pin 5 (GPIO 3 / SCL1) | Yellow | LV2 | Purple | HV2 -> A5 (SCL) |
Note: Ensure the 'LV' side of the shifter faces the Pi, and the 'HV' side faces the Arduino. Reversing this will not immediately fry components due to the PCA9306's architecture, but it will disable the translation.
The Code: Pi Master and Arduino Slave
This implementation uses a strict command-response protocol. The Pi sends a 1-byte command; the Arduino responds with a 4-byte payload. This prevents the Pi from reading mid-byte while the Arduino is updating its sensor variables.
Arduino Nano Every (Slave Code)
Target Board: Arduino Nano Every (ATmega4809). Upload via Arduino IDE 2.x.
#include <Wire.h>
#define SLAVE_ADDR 0x08
#define CMD_READ_SENSOR 0x10
// Volatile buffer to prevent I2C interrupt collisions
volatile uint8_t txBuffer[4] = {0, 0, 0, 0};
volatile bool dataReady = false;
void setup() {
Wire.begin(SLAVE_ADDR);
Wire.onReceive(receiveEvent);
Wire.onRequest(requestEvent);
// Simulate sensor initialization
pinMode(A0, INPUT);
}
void loop() {
// Read sensor in main loop to avoid blocking I2C interrupts
// The Pi's BCM2711 hardware I2C does NOT support clock stretching well.
// If you delay() inside requestEvent, the Pi will throw an I/O error.
int sensorVal = analogRead(A0);
txBuffer[0] = (sensorVal >> 8) & 0xFF; // MSB
txBuffer[1] = sensorVal & 0xFF; // LSB
txBuffer[2] = 0xAA; // Status byte
txBuffer[3] = (txBuffer[0] ^ txBuffer[1] ^ 0xAA); // Simple XOR Checksum
dataReady = true;
delay(50); // Polling rate limit
}
void receiveEvent(int bytes) {
if (bytes > 0) {
uint8_t cmd = Wire.read();
// Consume any extra bytes to clear the buffer
while(Wire.available()) Wire.read();
}
}
void requestEvent() {
if (dataReady) {
Wire.write((uint8_t*)txBuffer, 4);
dataReady = false;
} else {
uint8_t empty[4] = {0xFF, 0xFF, 0x00, 0x00};
Wire.write(empty, 4);
}
}
Raspberry Pi 4 (Master Python Code)
Requires the smbus2 library. Install via terminal: pip3 install smbus2. Ensure I2C is enabled in sudo raspi-config.
import time
import sys
from smbus2 import SMBus
SLAVE_ADDR = 0x08
CMD_READ_SENSOR = 0x10
I2C_BUS = 1
def read_arduino_sensor(bus):
try:
# Send command byte
bus.write_byte(SLAVE_ADDR, CMD_READ_SENSOR)
time.sleep(0.01) # Brief pause for Arduino to prep buffer
# Read 4 bytes
data = bus.read_i2c_block_data(SLAVE_ADDR, CMD_READ_SENSOR, 4)
# Verify Checksum
checksum = data[0] ^ data[1] ^ data[2]
if checksum != data[3]:
print(f'Checksum mismatch: Calc {checksum}, Recv {data[3]}')
return None
# Reconstruct 10-bit ADC value
sensor_val = (data[0] << 8) | data[1]
return sensor_val
except OSError as e:
# Catch the specific I2C bus failure
print(f'I2C Bus Error: {e}', file=sys.stderr)
return None
if __name__ == '____main__':
with SMBus(I2C_BUS) as bus:
print('Starting Pi-Arduino I2C Polling...')
while True:
val = read_arduino_sensor(bus)
if val is not None:
# Convert to voltage (5V reference / 1024 steps)
voltage = val * (5.0 / 1023.0)
print(f'Sensor ADC: {val} | Voltage: {voltage:.2f}V')
time.sleep(1.0)
Debugging: Fixing the 'Remote I/O error'
When working with Pi Arduino I2C setups, you will eventually encounter this exact traceback in your Python console:
OSError: [Errno 121] Remote I/O error
This error means the Pi sent a clock pulse and an address, but received no Acknowledge (ACK) bit from the slave. Here is the ranked decision path to fix it, ordered from most to least likely.
- Check the I2C Address Map: Run
i2cdetect -y 1in the Pi terminal. If you don't see08in the grid, the Pi physically cannot see the Arduino.- Fix: Verify your wiring against the pin table above. Ensure the Nano Every is powered (the ON LED should be lit).
- Check Clock Stretching Violations: The Broadcom BCM2711 chip in the Pi 4 has a known hardware bug where it does not support I2C clock stretching. If your Arduino code uses
delay(),Serial.print(), or heavy math inside therequestEvent()interrupt, the Arduino holds the SCL line low too long, and the Pi's I2C controller times out and throws Errno 121.- Fix: Keep the
requestEvent()function strictly toWire.write(). Do all sensor reading and math in the mainloop(), as demonstrated in the code above.
- Fix: Keep the
- Verify Level Shifter Orientation and Power: The PCA9306 chip requires power on both the LV and HV sides to pass signals.
- Fix: Use a multimeter to verify Pin 1 on the Pi is outputting 3.2V-3.3V, and the Arduino 5V pin is outputting 4.8V-5.0V. If the Pi's 3.3V rail is sagging, the level shifter will lock up.
i2cdetect shows the address, but Python still throws Errno 121, you are likely suffering from I2C bus capacitance. Add a 1ms delay between the write_byte and read_i2c_block_data commands in Python to give the Arduino time to load its transmit buffer.
Extending and Simplifying the Build
Once your baseline Pi Arduino handshake is stable, you will likely need to scale the system. Here is how to adapt the architecture based on your evolving constraints.
How to Extend (Scaling Up)
- Add More Slaves: The I2C bus supports up to 127 devices. You can add an ESP32 at
0x09and an Arduino Uno at0x0Aon the exact same SDA/SCL wires. Just ensure every 5V board routes through the level shifter's HV channels. - Increase Bus Speed: The default I2C speed on the Pi is 100kHz. You can increase this to 400kHz (Fast Mode) by editing the Pi's
/boot/config.txtfile and addingdtparam=i2c_baudrate=400000. This is highly recommended if you are polling more than 3 sensors per second.
How to Simplify (Dumbing it Down)
- Switch to USB Serial: If I2C clock-stretching bugs are ruining your week, abandon the GPIO header entirely. Plug the Nano Every into the Pi's USB port. Use the Arduino
Seriallibrary and Python'spyserialpackage. You lose the multi-drop capability, but you gain hardware flow control and immunity to 3.3V/5V logic mismatching. - Use Firmata: If you don't want to write C++ for the Arduino, flash the StandardFirmata sketch onto the Nano Every via the Arduino IDE. On the Pi, use the
pyFirmataPython library. This allows the Pi to directly command the Arduino's pins over USB without writing custom slave logic.
By respecting the 3.3V logic limits of the Pi and keeping the Arduino's I2C interrupts lean, this Pi Arduino architecture will run for months without a dropped packet.






