Difficulty: Intermediate | Time: 45 Minutes | Cost: ~$95

Why Pair an Arduino with Raspberry Pi?

The most direct answer to why you should connect an arduino with raspberry pi is to offload real-time sensor polling and hardware interrupts from the Pi's non-deterministic Linux environment to the Arduino's bare-metal microcontroller. While the Raspberry Pi is excellent for heavy computation, networking, and running databases, its Linux kernel introduces jitter that makes precise timing and high-frequency I2C/SPI sensor reading unreliable. By using the Arduino as an I2C slave coprocessor, you get the best of both worlds: precise hardware-level data acquisition and high-level data processing.

In this guide, we will build an environmental monitoring node. The Arduino will poll a BME280 sensor and buffer the data, while the Raspberry Pi will periodically request that data over the I2C bus to log it to a local SQLite database or push it via MQTT.

Hardware Warning: The Raspberry Pi operates at 3.3V logic, while the standard Arduino Uno operates at 5V. Connecting them directly will destroy the Pi's GPIO pins. You must use a bi-directional logic level converter.

Required Parts List

  • Host: Raspberry Pi 4 Model B (4GB) or Pi 5 (~$55 - $80)
  • Microcontroller: Arduino Uno R3 (or R4 Minima) (~$25)
  • Level Shifter: SparkFun Logic Level Converter - Bi-Directional (BSS138) (~$3)
  • Sensor: Adafruit BME280 I2C Temperature/Humidity/Pressure Sensor (~$15)
  • Wiring: 22 AWG solid core jumper wires and a half-size breadboard

Hardware Wiring and Pin Mapping

Proper I2C wiring requires attention to pull-up resistors and voltage domains. The BSS138 level shifter handles the voltage translation between the Pi's 3.3V I2C bus and the Arduino's 5V bus. The NXP I2C-bus specification mandates specific capacitance and pull-up limits, which the BSS138 module handles internally via its onboard 10kΩ resistors.

Pin Mapping: Pi to Level Shifter to Arduino
Raspberry Pi Pin Level Shifter (LV Side) Level Shifter (HV Side) Arduino Uno R3 Pin
Pin 1 (3.3V Power) LV --- ---
Pin 2 (5V Power) --- HV 5V Pin
Pin 6 (Ground) GND (LV) GND (HV) GND
Pin 3 (GPIO 2 / SDA1) LV1 HV1 A4 (SDA)
Pin 5 (GPIO 3 / SCL1) LV2 HV2 A5 (SCL)

Wiring Steps

  1. Insert the BSS138 level shifter into the center of the breadboard, straddling the power rails.
  2. Connect the Pi's 3.3V pin to the LV rail and the Arduino's 5V pin to the HV rail. Tie all grounds together on a common bus.
  3. Route Pi Pin 3 (SDA) to LV1, and LV1's corresponding HV1 to Arduino A4.
  4. Route Pi Pin 5 (SCL) to LV2, and HV2 to Arduino A5.
  5. Wire the BME280 sensor directly to the Arduino's 5V, GND, A4 (SDA), and A5 (SCL) pins. The Arduino will act as the I2C master for the sensor, and an I2C slave to the Pi.

Firmware and Host Code

This code targets the Arduino Uno R3 and the Raspberry Pi 4/5 running Raspberry Pi OS (Bookworm or later). Ensure I2C is enabled on the Pi via sudo raspi-config (Interface Options > I2C > Enable). For more on Pi configuration, refer to the official Raspberry Pi documentation.

Arduino Slave Firmware (C++)

The Arduino uses the Wire library to act as a slave device at address 0x08. It reads the BME280 and formats the temperature as a 2-byte integer to send to the Pi.

#include <Wire.h>
#include <Adafruit_BME280.h>

#define I2C_SLAVE_ADDRESS 0x08
#define BME_ADDRESS 0x76

Adafruit_BME280 bme;
volatile float latestTemp = 0.0;

void setup() {
  Serial.begin(115200);
  
  // Initialize BME280
  if (!bme.begin(BME_ADDRESS)) {
    Serial.println(F("BME280 init failed. Check wiring!"));
    while (1); // Halt on sensor failure
  }
  
  // Initialize I2C Slave
  Wire.begin(I2C_SLAVE_ADDRESS);
  Wire.onRequest(requestEvent);
}

void loop() {
  // Poll sensor every 500ms
  latestTemp = bme.readTemperature();
  delay(500);
}

// Callback triggered when Pi requests data
void requestEvent() {
  // Convert float to centigrade integer to save I2C bytes (e.g., 24.5C -> 2450)
  int16_t tempPayload = (int16_t)(latestTemp * 100);
  
  // Send 2 bytes (Little Endian)
  Wire.write((uint8_t*)&tempPayload, 2);
}

Raspberry Pi Host Script (Python)

The Pi uses the smbus2 library. Install it via pip install smbus2. This script includes explicit error handling for I2C bus faults.

import smbus2
import time
import struct

I2C_BUS = 1
ARDUINO_ADDR = 0x08

# Initialize I2C bus
bus = smbus2.SMBus(I2C_BUS)

def read_arduino_temp():
    try:
        # Request 2 bytes from Arduino
        data = bus.read_i2c_block_data(ARDUINO_ADDR, 0x00, 2)
        
        # Unpack little-endian signed 16-bit integer
        temp_raw = struct.unpack('<h', bytes(data))[0]
        temp_c = temp_raw / 100.0
        
        print(f"Temperature: {temp_c:.2f} C")
        return temp_c
        
    except FileNotFoundError as e:
        print(f"Fatal: I2C Bus not found. Is I2C enabled in raspi-config? ({e})")
        return None
    except OSError as e:
        print(f"I2C Communication Error: {e}")
        return None

if __name__ == '__main__':
    while True:
        read_arduino_temp()
        time.sleep(2)

Debugging I2C Communication Failures

I2C is notoriously sensitive to wiring capacitance and missing pull-ups. If your script fails, here are the first three things to check:

  1. Verify Bus Visibility: Run i2cdetect -y 1 in the Pi terminal. You should see 08 in the grid. If the grid is empty, your SDA/SCL lines are swapped, or the level shifter lacks power on the HV/LV rails.
  2. Check Level Shifter Power: Use a multimeter to verify that the LV pin reads exactly 3.3V and the HV pin reads exactly 5.0V. If HV reads 0V, the Arduino isn't powering the high-side pull-ups, and I2C will fail.
  3. Inspect Grounding: The Pi, Arduino, and Level Shifter must share a common ground. A missing ground wire causes the logic signals to float, resulting in intermittent packet corruption.

Common Error Strings and Ranked Causes

Error: OSError: [Errno 121] Remote I/O error
Meaning: The Pi sent a clock pulse, but the Arduino did not acknowledge (ACK) or pulled the SDA line low unexpectedly.
  • Cause 1 (Most Likely): The Arduino is locked in a delay() or blocking sensor read and missed the I2C interrupt window. (Fix: Keep the Arduino loop() non-blocking or use hardware I2C buffers).
  • Cause 2: I2C bus capacitance is too high due to excessively long jumper wires (>30cm). (Fix: Shorten wires or reduce I2C clock speed to 10kHz).
  • Cause 3: The Arduino browned out or reset due to a power spike from the sensor. (Fix: Add a 100µF decoupling capacitor across the Arduino's 5V and GND pins).
Error: FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'
Meaning: The Linux kernel module for I2C is not loaded.
Fix: Run sudo raspi-config, navigate to Interface Options, enable I2C, and reboot the Pi.

Extending and Simplifying the Build

How to Extend: If you need to stream high-frequency data (like raw accelerometer vibration data at 1kHz), I2C's 400kbps ceiling will bottleneck you. Extend this build by switching the communication protocol to SPI. The Pi can act as the SPI master, and the Arduino as the SPI slave, pushing throughput to several megabits per second. You will need to add a dedicated Chip Select (CS) and MISO/MOSI wiring, but the level-shifting logic remains identical.

How to Simplify: If your project doesn't strictly require a full Linux desktop environment, database hosting, or HDMI output, drop the Raspberry Pi entirely. Replace it with a Raspberry Pi Pico W ($6). The Pico W runs MicroPython, has native 3.3V logic (eliminating the need for the BSS138 level shifter), and includes WiFi for MQTT publishing, cutting your BOM cost and wiring complexity in half.

Frequently Asked Questions

Can I connect an Arduino with Raspberry Pi using USB instead of I2C?

Yes, you can connect them via a standard USB-A to USB-B cable and use Serial (UART over USB). This is much easier to wire and eliminates the need for a logic level converter. However, USB introduces a software serial stack and FTDI/CH340 chip latency (often 10ms to 50ms per packet), making it unsuitable for strict real-time hardware triggering. I2C or SPI is preferred for deterministic, low-latency sensor offloading.

Does the Arduino with Raspberry Pi setup work on the Raspberry Pi 5?

Yes, the code and wiring are fully compatible with the Raspberry Pi 5. However, the Pi 5 uses the RP1 southbridge chip, which changes the underlying GPIO architecture. The I2C bus is still mapped to physical pins 3 and 5, and /dev/i2c-1 remains the correct bus address in software. Ensure your Pi 5 is running the latest Bookworm OS, as older Buster/Bullseye kernels do not support the RP1 I2C controllers.

Why is my Arduino with Raspberry Pi I2C connection dropping packets under load?

Linux is not a real-time operating system (RTOS). If your Pi is running heavy background tasks (like compiling code, updating packages, or writing heavily to an SD card), the kernel may suspend the Python I2C polling thread, causing the I2C clock to stretch or timeout. To fix this, you can isolate a CPU core on the Pi using isolcpus in cmdline.txt and pin your Python script to that core, or simply implement a retry-logic wrapper in your Python try/except block to handle dropped packets gracefully.