Why Combine a Raspberry Pi and Arduino?
When architecting raspberry pi arduino projects, the most common mistake beginners make is trying to force a single board to do everything. The Raspberry Pi is a powerhouse for high-level tasks: running databases, hosting web dashboards, and handling MQTT networking. However, its 3.3V logic levels, lack of native analog-to-digital conversion (ADC), and OS-level interrupt latency make it fragile for direct hardware interfacing. The Arduino, conversely, offers 5V tolerance, real-time deterministic execution, and robust PWM/ADC capabilities, but lacks native networking and a full OS.
By bridging the two, you segregate the workload. The Pi acts as the brain and network gateway, while the Arduino acts as the rugged peripheral nervous system. In this guide, we will build a hybrid environmental monitoring and relay control station. The Raspberry Pi 5 (8GB) will poll an Arduino Uno R4 WiFi over a USB-Serial link to read a BME280 sensor and trigger a 5V relay, completely avoiding the voltage-level translation headaches of direct GPIO wiring.
Protocol Selection for Pi-Arduino Communication
Before wiring anything, you must choose the right transport layer. Pushing every table to the end of a guide is a bad habit, so here is the data-dense comparison up front to help you decide which protocol fits your specific build.
| Protocol | Wiring Complexity | Logic Level Safe? | Latency | Best Use Case |
|---|---|---|---|---|
| USB-Serial (ttyACM0) | Low (1 USB cable) | Yes (Isolated via USB PHY) | ~2-5ms | Standard sensor polling & relay control (Used in this guide) |
| I2C (Direct GPIO) | Medium (4 wires + level shifters) | No (Requires 3.3V to 5V bidirectional shifter) | <1ms | High-speed, short-distance chip-to-chip data |
| SPI (Direct GPIO) | High (6+ wires + level shifters) | No (Requires logic shifting) | <1ms | High-bandwidth displays or ADCs |
| MQTT over WiFi | None (Wireless) | N/A | 15-50ms (Network dependent) | Physically separated nodes, IoT dashboards |
Hardware Bill of Materials & Pin Mapping
This build uses current-generation hardware. The Arduino Uno R4 WiFi is chosen over the older Uno R3 because its RA4M1 ARM Cortex-M4 processor handles JSON string formatting significantly faster, and its native USB-C port simplifies cabling.
Parts List
- Host: Raspberry Pi 5 (8GB variant) - ~$80.00
- Node: Arduino Uno R4 WiFi (ABX00087) - ~$27.50
- Sensor: BME280 Breakout Board (I2C, 3.3V/5V tolerant) - ~$4.50
- Actuator: 5V Single-Channel Relay Module (Optocoupler isolated) - ~$2.00
- Cabling: USB-C to USB-A cable, female-to-female jumper wires
Arduino Pin Mapping
| Component | Component Pin | Arduino Uno R4 Pin | Notes |
|---|---|---|---|
| BME280 | VIN / VCC | 5V | Module has onboard 3.3V LDO |
| BME280 | GND | GND | Common ground |
| BME280 | SCL | A5 (SCL) | Hardware I2C bus |
| BME280 | SDA | A4 (SDA) | Hardware I2C bus |
| Relay Module | VCC | 5V | Requires adequate USB current supply |
| Relay Module | GND | GND | Common ground |
| Relay Module | IN (Signal) | D8 | Active LOW trigger |
Step-by-Step Build Procedure
- Wire the Sensor: Connect the BME280 to the Arduino's A4 (SDA) and A5 (SCL) pins. Ensure the breakout board's I2C pull-up resistors are enabled (most Adafruit/SparkFun boards have them populated by default).
- Wire the Relay: Connect the relay module's VCC and GND to the Arduino's 5V and GND. Connect the IN pin to Digital Pin 8. Note: The Pi 5's USB ports can supply up to 1.6A total; a single 5V relay coil draws ~70mA, well within safe limits.
- Physical Connection: Plug the Arduino Uno R4 into the Raspberry Pi 5 using the USB-C cable. Do not plug the Arduino into a wall outlet simultaneously to avoid ground loops.
- Verify Enumeration: Boot the Pi, open a terminal, and run
ls /dev/ttyACM*. You should see/dev/ttyACM0. If you see it, the hardware link is live. - Install Dependencies: On the Pi, install the Python serial library:
sudo apt update && sudo apt install python3-serial.
Firmware and Host Code
The architecture relies on a simple newline-delimited JSON protocol. The Arduino reads the sensor and outputs JSON. It also listens for incoming serial strings to toggle the relay.
Arduino Firmware (Targets Uno R4 WiFi)
Upload this via the Arduino IDE (ensure the Adafruit BME280 Library is installed via Library Manager). Pin definitions and error handling for the I2C bus are explicitly included.
#include <Wire.h>
#include <Adafruit_BME280.h>
#define RELAY_PIN 8
#define I2C_SDA A4
#define I2C_SCL A5
#define BAUD_RATE 115200
Adafruit_BME280 bme;
bool sensorFound = false;
void setup() {
Serial.begin(BAUD_RATE);
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // Active LOW, so HIGH = OFF
Wire.begin(I2C_SDA, I2C_SCL);
// Error handling: Verify sensor presence before looping
if (!bme.begin(0x76, &Wire)) {
Serial.println("{\"error\": \"BME280 not found at 0x76\"}");
sensorFound = false;
} else {
sensorFound = true;
}
}
void loop() {
// 1. Handle incoming relay commands from Pi
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd == "RELAY_ON") {
digitalWrite(RELAY_PIN, LOW);
Serial.println("{\"status\": \"relay_on\"}");
} else if (cmd == "RELAY_OFF") {
digitalWrite(RELAY_PIN, HIGH);
Serial.println("{\"status\": \"relay_off\"}");
}
}
// 2. Output sensor data every 2 seconds
static unsigned long lastRead = 0;
if (millis() - lastRead >= 2000) {
lastRead = millis();
if (sensorFound) {
float t = bme.readTemperature();
float h = bme.readHumidity();
Serial.print("{\"temp_c\": ");
Serial.print(t, 2);
Serial.print(", \"hum_pct\": ");
Serial.print(h, 2);
Serial.println("}");
}
}
}
Raspberry Pi Python Host Script
Save this as pi_hub.py on the Pi. It handles serial reading, JSON parsing, and sends a test command.
import serial
import json
import time
import sys
PORT = '/dev/ttyACM0'
BAUD = 115200
def main():
try:
ser = serial.Serial(PORT, BAUD, timeout=1)
time.sleep(2) # Wait for Arduino auto-reset
print(f"Connected to {PORT}")
# Send a test command to toggle relay
ser.write(b'RELAY_ON\n')
while True:
if ser.in_waiting > 0:
line = ser.readline().decode('utf-8', errors='ignore').strip()
if not line:
continue
try:
data = json.loads(line)
if 'temp_c' in data:
print(f"[SENSOR] Temp: {data['temp_c']}C | Hum: {data['hum_pct']}%")
elif 'status' in data:
print(f"[CMD ACK] {data['status']}")
elif 'error' in data:
print(f"[NODE ERROR] {data['error']}")
except json.JSONDecodeError:
pass # Ignore non-JSON boot messages
except serial.SerialException as e:
print(f"Serial Error: {e}")
sys.exit(1)
except KeyboardInterrupt:
print("\nShutting down...")
ser.write(b'RELAY_OFF\n')
ser.close()
if __name__ == '__main__':
main()
Debugging: First Three Checks and Common Errors
When bridging two distinct ecosystems, failures usually happen at the OS boundary. If your Python script crashes or the Arduino seems unresponsive, execute these first three things to check:
- Verify USB Enumeration: Run
dmesg | grep ttyin the Pi terminal. If the Arduino isn't listed as a new CDC ACM device, check your USB cable (many cheap cables are power-only and lack data lines). - Check Group Permissions: Run
ls -l /dev/ttyACM0. The output should showcrw-rw---- 1 root dialout. If your user isn't in thedialoutgroup, the OS will block access. - Confirm Baud Rate Parity: Ensure the
Serial.begin(115200)in the C++ code exactly matches theBAUD = 115200in the Python script. A mismatch results in garbage characters.
serial.serialutil.SerialException: [Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0'
Ranked Causes & Fixes:
- Cause 1 (Most Likely): Your Pi user lacks permissions. Fix: Run
sudo usermod -a -G dialout $USER, then reboot the Pi or log out and back in. - Cause 2: The Arduino IDE's Serial Monitor is currently open on the Pi (or via VNC), locking the port. Fix: Close the Serial Monitor in the IDE before running the Python script.
- Cause 3: ModemManager is hijacking the serial port. Fix: Run
sudo systemctl stop ModemManagerand disable it viasystemctl disable ModemManager.
For deeper Raspberry Pi OS configuration and USB power management settings, consult the official documentation, as Pi 5 handles USB power states differently than the Pi 4.
Scaling: How to Extend or Simplify the Build
One of the primary advantages of this architecture is modularity. Depending on your end goal, you can easily adjust the complexity.
How to Simplify
If you are just learning and want to strip away the networking and relay logic to focus purely on data logging:
1. Remove the relay wiring and the Serial.available() block from the Arduino code.
2. Modify the Python script to open a local CSV file (with open('data.csv', 'a') as f:) and write the parsed temp_c and hum_pct values with a datetime timestamp.
3. Run the script as a background service using systemd.
How to Extend
To turn this into a true smart-home node:
1. Install Mosquitto on the Pi: sudo apt install mosquitto mosquitto-clients.
2. Add the paho-mqtt library to your Python environment (pip install paho-mqtt).
3. Inside the Python while loop, publish the parsed JSON dictionary to an MQTT topic like homeassistant/sensor/bme280/state.
4. This allows Home Assistant (running on the same Pi 5) to auto-discover the sensor via MQTT Discovery, giving you a beautiful UI and automated climate control triggers without writing a single line of frontend code.






