Combining an Arduino and Raspberry Pi in a single project is the definitive solution when you need hard real-time sensor polling alongside heavy local compute, database logging, or complex networking. The Arduino handles microsecond-level ADC reads and interrupt-driven sensor polling without OS jitter, while the Raspberry Pi runs the MQTT broker, local time-series database, and web dashboard. The bridge between them? A hardware UART serial link.
In this guide, we are building a hybrid environmental data logger. We will use the Arduino Uno R4 WiFi as the edge node and the Raspberry Pi 5 (4GB) as the local server. By the end, you will have a fully wired, logic-shifted UART bridge with complete, error-handled firmware for both boards.
The Core Decision: Microcontroller vs. Microprocessor
Before wiring anything, you need a decision framework to justify using both boards instead of just one. Here is the decision tree to determine your architecture, terminating in the concrete pick for this build.
| System Requirement | Use Microcontroller (Arduino/ESP32) | Use Microprocessor (Raspberry Pi) |
|---|---|---|
| Sensor Polling Jitter | Required (< 10μs jitter) | Unacceptable (Linux OS introduces ms-level jitter) |
| Power Consumption | Required (< 50mA sleep) | Unacceptable (Pi idles at ~2W minimum) |
| Local Database / UI | Impossible (No MMU, limited RAM) | Required (Runs InfluxDB, Grafana, Node-RED) |
| Complex Crypto / TLS | Struggles (Software TLS eats CPU) | Native (Hardware crypto accelerators) |
Parts List & Spec Sheet
Here are the exact components required for this build. Prices reflect typical 2026 retail pricing from authorized distributors like Adafruit, SparkFun, and Mouser.
| Component | Exact Model / Variant | Role | Est. Price |
|---|---|---|---|
| Microprocessor | Raspberry Pi 5 (4GB RAM) | Local MQTT Broker & Data Logger | $60.00 |
| Microcontroller | Arduino Uno R4 WiFi (ABX00087) | Edge Sensor Polling Node | $27.50 |
| Sensor | Adafruit BME280 I2C (Product 2652) | Temp/Humidity/Pressure Data | $19.95 |
| Logic Level Shifter | Adafruit 4-Channel I2C-safe Bi-directional (757) | 5V to 3.3V UART Translation | $3.95 |
| Wiring | 28 AWG Silicone Stranded Wire | Low-resistance breadboard/jumpers | $8.00 |
Wiring the UART Bridge: 5V to 3.3V Logic Level Shifting
The Raspberry Pi 5 uses the PL011 UART mapped to GPIO 14 (TXD) and GPIO 15 (RXD) by default. The Arduino Uno R4 WiFi exposes its hardware UART on Digital Pin 0 (RX) and Digital Pin 1 (TX), accessible in code via Serial1.
Pin Mapping Table
| Raspberry Pi 5 GPIO | Level Shifter (Low Side) | Level Shifter (High Side) | Arduino Uno R4 WiFi |
|---|---|---|---|
| Pin 6 (GND) | GND | GND | GND |
| Pin 1 (3.3V) | LV | - | - |
| Pin 2 (5V) | - | HV | - |
| GPIO 14 (TXD) | LV1 | HV1 | Pin 0 (RX) |
| GPIO 15 (RXD) | LV2 | HV2 | Pin 1 (TX) |
Wiring Steps
- Establish Common Ground: Connect a ground wire from the Pi's Pin 6 to the level shifter's GND, and from the shifter's GND to the Arduino's GND. Without a common ground, the logic shifter cannot reference the voltage thresholds, resulting in garbage data.
- Power the Shifter: Wire Pi Pin 1 (3.3V) to the shifter's
LVpin. Wire Pi Pin 2 (5V) to the shifter'sHVpin. Do not use the Arduino's 5V pin to power the HV side; keep the power domains isolated to the Pi's supply to avoid ground loops. - Cross the Data Lines: Wire Pi GPIO 14 (TX) to LV1. Wire HV1 to Arduino Pin 0 (RX). Wire Pi GPIO 15 (RX) to LV2. Wire HV2 to Arduino Pin 1 (TX). Notice the TX-to-RX crossover.
Arduino Firmware: Sensor Polling with Error Handling
This firmware targets the Arduino Uno R4 WiFi. It reads the BME280 sensor over I2C and transmits a CSV-formatted string over Serial1 (the hardware UART on pins 0/1) at 115200 baud. We include explicit error handling for I2C timeouts, which is a common failure mode when wires vibrate loose on a workbench.
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// --- PIN & CONFIG DEFINITIONS ---
#define SENSOR_SDA A4
#define SENSOR_SCL A5
#define SEALEVELPRESSURE_HPA (1013.25)
#define UART_BAUD 115200
#define POLL_INTERVAL_MS 2000
Adafruit_BME280 bme;
unsigned long lastPoll = 0;
void setup() {
// USB Serial for local debug, Hardware Serial1 for Pi UART
Serial.begin(115200);
Serial1.begin(UART_BAUD);
Wire.begin(SENSOR_SDA, SENSOR_SCL);
// Error handling for sensor initialization
if (!bme.begin(0x77, &Wire)) {
Serial.println(F("FATAL: BME280 not found on I2C. Check wiring."));
Serial1.println(F("ERR:I2C_TIMEOUT"));
while (1) delay(100); // Halt execution
}
Serial.println(F("BME280 initialized. UART bridge active."));
}
void loop() {
if (millis() - lastPoll >= POLL_INTERVAL_MS) {
lastPoll = millis();
float temp = bme.readTemperature();
float hum = bme.readHumidity();
float pres = bme.readPressure() / 100.0F;
// Sanity check for NaN values (sensor read glitch)
if (isnan(temp) || isnan(hum) || isnan(pres)) {
Serial1.println(F("ERR:SENSOR_NAN"));
return;
}
// Format: TEMP,HUM,PRES
char buffer[64];
snprintf(buffer, sizeof(buffer), "DATA:%.2f,%.2f,%.2f", temp, hum, pres);
Serial1.println(buffer);
Serial.println(buffer); // Echo to USB for bench debugging
}
}
Raspberry Pi Python Script: Serial Reading & MQTT
This script targets the Raspberry Pi 5 running Raspberry Pi OS (Bookworm) with Python 3.11+. It reads from /dev/ttyAMA0 (the PL011 UART), parses the CSV, and publishes it to a local MQTT broker.
Prerequisite: Disable the serial console in raspi-config (Interface Options -> Serial Port -> Login shell: No, Hardware: Yes) and install dependencies via pip install pyserial paho-mqtt.
import serial
import paho.mqtt.client as mqtt
import time
import sys
# --- CONFIGURATION ---
UART_PORT = '/dev/ttyAMA0'
BAUD_RATE = 115200
MQTT_BROKER = 'localhost'
MQTT_PORT = 1883
MQTT_TOPIC = 'sensors/greenhouse/bme280'
def on_connect(client, userdata, flags, rc):
if rc == 0:
print(f"Connected to MQTT Broker at {MQTT_BROKER}")
else:
print(f"MQTT Connection failed with code {rc}")
# Initialize MQTT
mqtt_client = mqtt.Client()
mqtt_client.on_connect = on_connect
try:
mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60)
mqtt_client.loop_start()
except Exception as e:
print(f"MQTT Init Error: {e}. Is Mosquitto running?")
sys.exit(1)
# Initialize UART
try:
ser = serial.Serial(UART_PORT, BAUD_RATE, timeout=1)
print(f"Listening on {UART_PORT} at {BAUD_RATE} baud...")
except serial.SerialException as e:
print(f"FATAL: Cannot open {UART_PORT}. {e}")
sys.exit(1)
try:
while True:
line = ser.readline().decode('utf-8', errors='replace').strip()
if not line:
continue
if line.startswith('DATA:'):
payload = line.replace('DATA:', '')
mqtt_client.publish(MQTT_TOPIC, payload)
print(f"Published: {payload}")
elif line.startswith('ERR:'):
print(f"Arduino reported error: {line}")
except KeyboardInterrupt:
print("\nShutting down gracefully...")
finally:
ser.close()
mqtt_client.loop_stop()
mqtt_client.disconnect()
Debugging the Bridge: Serial Exceptions and Garbage Data
When bridging an Arduino and Raspberry Pi via UART, you will inevitably hit synchronization or OS-level blocking issues. If your Python script crashes or outputs garbage, follow this diagnostic path.
The Exact Error: SerialException Readiness Failure
If your Python script crashes with this exact string:
serial.serialutil.SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)
Ranked Causes & Fixes:
- Linux Serial Console Hijack (Most Likely): The Raspberry Pi OS defaults to mapping the Linux boot console to
/dev/ttyAMA0. The OS and your Python script are fighting for the port. Fix: Runsudo raspi-config, navigate to Interface Options > Serial Port, disable the login shell, but enable the serial hardware port. Reboot. - Missing Common Ground: The logic level shifter is floating. Fix: Verify continuity between Pi Pin 6 and Arduino GND with a multimeter. It must read < 1 ohm.
- Baud Rate Drift: The Pi's UART clock and the Arduino's crystal are slightly out of phase at high speeds. Fix: Drop both sides to
57600baud. 115200 is usually fine, but long wires act as antennas and introduce noise at higher frequencies.
First Three Things to Check When It Fails
If you are getting garbage characters (e.g., ÿÿÿDATA:) instead of clean CSV data, check these three physical layer issues immediately:
- TX/RX Crossover: Did you wire TX to TX? TX must always go to RX. Pi GPIO 14 (TX) must go to Arduino Pin 0 (RX).
- Logic Shifter Power: Use a multimeter to probe the
HVandLVpins on the shifter. IfLVreads 5V instead of 3.3V, you wired the Pi's 5V pin to the low-voltage side, and you are overvolting the Pi's GPIO. - USB Serial Conflict (Arduino side): Ensure your Arduino code uses
Serial1.println()for the UART pins. If you useSerial.println()on the Uno R4, it routes to the USB-C port, not pins 0 and 1.
Extending and Simplifying the Build
This architecture is highly modular. Depending on your final deployment environment, you should adjust the complexity.
How to Simplify (Drop the Pi)
If you realize you do not need a local Grafana dashboard or InfluxDB instance, drop the Raspberry Pi entirely. Replace the Arduino Uno R4 WiFi with an ESP32-S3-DevKitC-1. The ESP32 handles the sensor polling and pushes data directly to a cloud MQTT broker (like AWS IoT or HiveMQ) over Wi-Fi. This reduces your BOM cost by $60, drops idle power consumption from 2.5W to ~80mA, and eliminates the logic level shifter since the ESP32 is natively 3.3V.
How to Extend (Add Time-Series Storage)
If this is a permanent greenhouse installation, extend the Raspberry Pi's role by installing InfluxDB v2.7 and Telegraf. Configure Telegraf to subscribe to the sensors/greenhouse/# MQTT topic. This gives you a local, high-retention time-series database that survives internet outages, ensuring you never lose environmental data when your ISP drops the connection.
For permanent edge-logging requiring local compute, the Raspberry Pi 5 + Arduino Uno R4 WiFi UART bridge remains the most robust, jitter-free architecture available to makers. Wire the level shifter correctly, separate your hardware serial from your USB debug serial, and your data pipeline will run for years without intervention.






