When tackling complex arduino raspberry pi projects, makers quickly hit a wall: the Raspberry Pi lacks deterministic real-time I/O and robust 5V analog capabilities, while the Arduino lacks the compute power for local databases, computer vision, or heavy MQTT routing. The professional solution is a hybrid architecture. In this build, we link a Raspberry Pi 5 and an Arduino Uno R4 Minima via a hardware UART serial connection to create a smart greenhouse controller. The Arduino handles precision sensor polling and 12V pump actuation, while the Pi logs data to SQLite and hosts a local dashboard.

Target Boards: Raspberry Pi 5 (8GB variant, Bookworm OS) and Arduino Uno R4 Minima.
Difficulty: Intermediate | Time: 2 Hours

1. Architecture and Component Selection

The most common point of failure in hybrid builds is logic-level mismatch. The Pi 5 operates strictly at 3.3V logic, while the Uno R4 Minima operates at 5V. Feeding 5V directly into the Pi's GPIO pins will permanently destroy the SoC. We use a BSS138 bi-directional logic level converter to safely bridge the UART lines.

Component Specific Variant / SKU Role in System Logic Level Est. Cost (2026)
Raspberry Pi 5 8GB RAM (SC11128) Host, Data Logging, Dashboard 3.3V $80.00
Arduino Uno R4 Minima (ABX00080) Sensor I/O, Actuator Control 5.0V $27.50
Logic Converter BSS138 4-Channel I2C/UART Voltage Translation (3.3V <-> 5V) Dual $3.00
Env Sensor Adafruit BME280 (PID 2652) Temp/Humidity/Pressure Readings 3.3V / 5V $19.95
Actuator Driver IRLZ44N Logic-Level MOSFET Switching 12V Water Pump 5V Gate $2.50

2. Pin Mapping and Wiring the Logic Level Converter

Wiring UART across different voltage domains requires strict attention to the BSS138 breakout board. The board has a low-voltage (LV) side and a high-voltage (HV) side. The LV side references the Pi's 3.3V pin, and the HV side references the Uno's 5V pin.

⚠️ Warning: Never use a simple resistor voltage divider for the Pi TX to Uno RX line. A divider can step voltage down, but it cannot step the Pi's 3.3V up to the 5V required to reliably trigger the Uno R4's RX HIGH threshold (Vih). Use the BSS138.
Signal Raspberry Pi 5 Pin BSS138 Channel Arduino Uno R4 Pin
Pi TX -> Uno RX GPIO 14 (TXD, Pin 8) LV1 -> HV1 Pin 0 (RX / Serial1)
Pi RX <- Uno TX GPIO 15 (RXD, Pin 10) LV2 <- HV2 Pin 1 (TX / Serial1)
Ground GND (Pin 6) GND (Both sides) GND
Power (LV) 3.3V (Pin 1) LV Pin N/A
Power (HV) N/A HV Pin 5V Pin

Reference: For detailed GPIO mappings, consult the Raspberry Pi UART Configuration Guide and the Arduino Uno R4 Minima Cheat Sheet.

3. Firmware and Host Code

Below is the complete, compilable code for both microcontrollers. The Arduino formats sensor data into a lightweight JSON string and pushes it over Serial1 (the hardware UART on pins 0/1). The Pi reads this stream, parses the JSON, and triggers the pump logic.

Arduino Uno R4 Minima Firmware (C++)

Requires: Adafruit BME280 Library & Adafruit Unified Sensor Library via Library Manager.

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

#define PUMP_PIN 8
#define SEALEVELPRESSURE_HPA (1013.25)

Adafruit_BME280 bme;
unsigned long lastSend = 0;

void setup() {
  Serial.begin(115200); // USB Debug
  Serial1.begin(115200); // Hardware UART to Pi
  pinMode(PUMP_PIN, OUTPUT);
  digitalWrite(PUMP_PIN, LOW);

  if (!bme.begin(0x76)) {
    Serial.println("BME280 init failed. Check wiring.");
    while (1) delay(10);
  }
}

void loop() {
  if (millis() - lastSend >= 2000) {
    lastSend = millis();
    
    float temp = bme.readTemperature();
    float hum = bme.readHumidity();
    
    // Error handling for sensor read faults
    if (isnan(temp) || isnan(hum)) {
      Serial1.println("{\"error\":\"sensor_fault\"}");
      return;
    }

    // Local actuator logic (fail-safe if Pi disconnects)
    if (hum < 40.0) {
      digitalWrite(PUMP_PIN, HIGH);
    } else {
      digitalWrite(PUMP_PIN, LOW);
    }

    // Build JSON payload manually to avoid heavy library overhead
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "{\"t\":%.2f,\"h\":%.2f}\n", temp, hum);
    Serial1.print(buffer);
  }
}

Raspberry Pi 5 Host Script (Python)

Requires: sudo apt install python3-serial

import serial
import json
import time

# /dev/serial0 is the stable symlink for the primary UART on Pi 5
PORT = '/dev/serial0'
BAUD = 115200

def main():
    try:
        ser = serial.Serial(PORT, BAUD, timeout=1)
        print(f"Listening on {PORT}...")
    except serial.SerialException as e:
        print(f"Failed to open port: {e}")
        return

    while True:
        try:
            # Read until newline, decode, and strip whitespace
            raw_line = ser.readline().decode('utf-8').strip()
            if not raw_line:
                continue
                
            data = json.loads(raw_line)
            
            if 'error' in data:
                print(f"[ALERT] Arduino reported: {data['error']}")
                continue

            temp_c = data['t']
            humidity = data['h']
            print(f"Temp: {temp_c}C | Humidity: {humidity}%")
            
            # Insert SQLite logging or MQTT publish here
            
        except json.JSONDecodeError:
            # Handles UART buffer fragmentation or noise
            print(f"[WARN] Malformed JSON received: {raw_line}")
        except Exception as e:
            print(f"[ERR] Unexpected host error: {e}")
            time.sleep(1)

if __name__ == '__main__':
    main()

4. Debugging UART Communication Failures

When your arduino raspberry pi projects fail to communicate over hardware serial, it is almost always due to OS-level port locking or physical layer mismatches. If your Python script fails immediately, you will likely see this exact error string:

serial.serialutil.SerialException: [Errno 13] Permission denied: '/dev/serial0'
OR
serial.serialutil.SerialException: [Errno 16] Device or resource busy: '/dev/ttyAMA0'

The First Three Things to Check When It Fails

  1. Disable the Serial Console: By default, Pi OS routes the Linux boot console to the UART, locking the port and injecting garbage text into your Arduino's RX pin. Run sudo raspi-config, navigate to Interface Options -> Serial Port, select No to "login shell to be accessible over serial", and Yes to "serial port hardware to be enabled". Reboot.
  2. Verify TX/RX Cross-Wiring: UART requires the transmitter of one device to connect to the receiver of the other. If you see nothing in the Python terminal, swap the LV1/HV1 and LV2/HV2 wires on the BSS138. (TX always goes to RX).
  3. Measure the Logic Levels: Use a multimeter to probe the HV side of the BSS138 while the Arduino is transmitting. You should see the voltage toggling between 0V and ~5V. If it stays at 3.3V, your level shifter is unpowered or wired backward.

Ranked Causes for Data Corruption (Garbage Characters)

If the port opens but you receive symbols like ÿÿÿ or the Python script throws a ValueError: Extra data: line 1 column 15, rank your troubleshooting in this order:

  • Baud Rate Mismatch (80% of cases): Ensure both Serial1.begin(115200) and the Python serial.Serial(..., 115200) match exactly. The Uno R4's internal oscillator is highly accurate, so software serial drift is not the culprit here.
  • Missing Common Ground (15% of cases): The Pi, the BSS138, and the Uno must share a common ground wire. Without it, the 3.3V and 5V references float, causing bit errors.
  • Buffer Fragmentation (5% of cases): UART transmits byte-by-byte. If the Pi reads the buffer before the Arduino finishes sending the \n character, json.loads() will crash. The ser.readline() method in the Python script above prevents this by blocking until the newline is received.

5. Extending and Simplifying the Build

Depending on your deployment environment, you may need to alter this architecture.

How to Simplify (The Prototyping Route)

If you are strictly prototyping on a desk and do not want to wire a BSS138 level shifter, ditch the GPIO UART entirely. Connect the Uno R4 Minima to the Pi 5 using a standard USB-A to USB-C cable.

Change the Python port to /dev/ttyACM0 and change the Arduino code to use Serial instead of Serial1. The USB connection handles the 5V-to-3.3V logic translation internally via the Uno's onboard USB-UART bridge chip. This sacrifices the "bare-metal" embedded feel and uses a Pi USB port, but it eliminates 90% of wiring bugs during initial code development.

How to Extend (The Production Route)

To turn this into a fully integrated IoT node:

  • Add MQTT: Install Mosquitto on the Pi 5 (sudo apt install mosquitto). Modify the Python script to use the paho-mqtt library to publish the parsed JSON payload to a topic like greenhouse/zone1/climate. This allows Home Assistant to ingest the data natively without scraping a local SQLite database.
  • Bi-Directional Control: Currently, the Arduino acts autonomously based on humidity thresholds. To allow the Pi to override the pump (e.g., via a web dashboard), implement a simple command listener in the Arduino loop() using Serial1.available() and Serial1.read() to parse single-byte commands (e.g., '1' for ON, '0' for OFF) sent from the Pi.

By respecting the voltage domains and utilizing hardware UART buffers, this hybrid architecture provides the best of both worlds: the deterministic sensor control of the Arduino and the heavy-lifting data pipeline of the Raspberry Pi.