Difficulty: Intermediate | Time to Build: 2 Hours | BOM Cost: ~$91

When designing embedded systems for environmental control, robotics, or automated agriculture, makers frequently hit a wall: the Raspberry Pi lacks strict real-time hardware PWM, and the Arduino lacks the compute power for local machine learning or heavy database logging. The solution is combining them. In hybrid raspberry and arduino projects, the Pi acts as the high-level brain (running MQTT, computer vision, and web servers) while the Arduino handles sub-millisecond I/O, sensor polling, and relay switching.

The Architecture Decision: Which Board Does What?

Before wiring a single pin, you must decide if a hybrid architecture is actually necessary, or if a single board will suffice. Use this decision matrix to evaluate your project requirements.

CriteriaRaspberry Pi 4 OnlyArduino Uno R4 OnlyHybrid (Pi + Arduino)
Real-Time PWM / I/OPoor (Linux kernel interrupts cause jitter)Excellent (Dedicated hardware timers)Excellent (Arduino handles I/O)
Local ML / Computer VisionExcellent (Quad-core ARM, VPU)Impossible (Lacks RAM and NPU)Excellent (Pi handles inference)
Power ConsumptionHigh (~3W - 6W idle)Low (~0.5W)Moderate (~4W combined)
BOM Cost (2026 pricing)~$55 (4GB model)~$20~$75 + peripherals
The Concrete Pick: If your project requires local sensor logging, strict 50Hz PWM for servos/pumps, AND local dashboard hosting or camera inference, choose the Raspberry Pi 4 Model B (4GB) paired with the Arduino Uno R4 Minima. Do not attempt to bit-bang PWM on the Pi's Linux GPIO; the OS scheduler will ruin your timing.

Hardware Spec Sheet and Pin Mapping

For this build, we are constructing a hybrid greenhouse climate controller. The Arduino reads the DHT22 temperature/humidity sensor and drives the 5V relays for a fan and heater. The Pi reads this data over USB Serial, logs it, and makes high-level decisions.

ComponentExact VariantApprox. CostRole in System
Host ComputerRaspberry Pi 4 Model B (4GB RAM)$55.00MQTT Broker, Data Logging, Logic
MicrocontrollerArduino Uno R4 Minima$20.00Sensor Polling, Relay Switching
SensorAdafruit DHT22 (AM2302) w/ 10k pull-up$10.00Temperature & Humidity Reading
Actuator DriverHiLetgo 4-Channel 5V Relay Module (Optoisolated)$6.00Switching 120V AC loads (Fan/Heater)

Arduino Uno R4 Minima Pin Mapping

Arduino PinDestinationWire Color (Recommended)Notes
5VDHT22 VCC & Relay VCCRedEnsure Pi USB port can supply 500mA+
GNDDHT22 GND & Relay GNDBlackCommon ground required
D2DHT22 Data OutYellowRequires 10kΩ pull-up to 5V
D8Relay 1 (IN1 - Fan)BlueActive LOW logic
D9Relay 2 (IN2 - Heater)GreenActive LOW logic
USB-CRaspberry Pi USB 3.0 PortN/AUse a high-quality data-capable cable

Firmware and Host Code

The following code targets the Arduino Uno R4 Minima (which uses the Renesas RA4M1 ARM Cortex-M4 processor, requiring the modern Arduino IDE 2.x board definitions) and a Raspberry Pi 4 running Raspberry Pi OS (Bookworm or later).

1. Arduino Firmware (C++)

This firmware polls the DHT22 every 2 seconds, validates the reading to prevent NaN (Not a Number) errors from propagating, and outputs a structured JSON string over the hardware UART (USB Serial).


// Target Board: Arduino Uno R4 Minima
// Library: DHT sensor library by Adafruit (v1.4.6+)
#include <DHT.h>

#define DHTPIN 2
#define DHTTYPE DHT22
#define RELAY_FAN 8
#define RELAY_HEATER 9

DHT dht(DHTPIN, DHTTYPE);
unsigned long lastRead = 0;
const unsigned long READ_INTERVAL = 2000;
int readErrors = 0;

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_FAN, OUTPUT);
  pinMode(RELAY_HEATER, OUTPUT);
  
  // Relays are Active LOW; set HIGH to turn OFF initially
  digitalWrite(RELAY_FAN, HIGH);
  digitalWrite(RELAY_HEATER, HIGH);
  
  dht.begin();
}

void loop() {
  if (millis() - lastRead >= READ_INTERVAL) {
    lastRead = millis();
    float h = dht.readHumidity();
    float t = dht.readTemperature();

    // Error handling: Check if reads failed (NaN)
    if (isnan(h) || isnan(t)) {
      readErrors++;
      Serial.print("{\"status\":\"error\",\"code\":");
      Serial.print(readErrors);
      Serial.println("}");
      return;
    }

    readErrors = 0; // Reset on success
    
    // Simple autonomous control logic on the MCU side
    if (t > 28.0) {
      digitalWrite(RELAY_FAN, LOW); // Turn ON fan
    } else {
      digitalWrite(RELAY_FAN, HIGH); // Turn OFF fan
    }

    // Output JSON for the Raspberry Pi to parse
    Serial.print("{\"status\":\"ok\",\"temp\":");
    Serial.print(t, 1);
    Serial.print(",\"hum\":");
    Serial.print(h, 1);
    Serial.println("}");
  }
  
  // Check for incoming commands from Pi (e.g., manual heater override)
  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    if (cmd == "HEATER_ON") digitalWrite(RELAY_HEATER, LOW);
    if (cmd == "HEATER_OFF") digitalWrite(RELAY_HEATER, HIGH);
  }
}

2. Raspberry Pi Host Script (Python)

This script uses pyserial to read the JSON stream, parse it, and handle malformed data gracefully. Install dependencies via pip install pyserial.


import serial
import json
import time
import sys

PORT = '/dev/ttyACM0'
BAUD = 115200

def main():
    try:
        ser = serial.Serial(PORT, BAUD, timeout=1)
        print(f'Connected to Arduino on {PORT}')
    except serial.SerialException as e:
        print(f'FATAL: Could not open serial port. {e}')
        sys.exit(1)

    while True:
        try:
            raw_line = ser.readline().decode('utf-8').strip()
            if not raw_line:
                continue

            data = json.loads(raw_line)
            
            if data.get('status') == 'ok':
                temp = data['temp']
                hum = data['hum']
                print(f'[LOG] Temp: {temp}C | Humidity: {hum}%')
                
                # Example: Send command back to Arduino if too cold
                if temp < 15.0:
                    ser.write(b'HEATER_ON\n')
            else:
                print(f'[WARN] Sensor read error code: {data.get("code")}')

        except json.JSONDecodeError:
            print(f'[ERR] Malformed JSON received: {raw_line}')
        except KeyboardInterrupt:
            print('\nShutting down...')
            ser.close()
            sys.exit(0)

if __name__ == '__main__':
    main()

Debugging the Handshake: Fixing Serial Port Errors

The most common point of failure in raspberry and arduino projects is the USB serial handshake. If your Python script crashes immediately upon execution, you will likely see this exact error string:

serial.serialutil.SerialException: [Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0'

Ranked Causes and Fixes

  1. Cause 1: User lacks dialout group permissions (90% of cases). Linux restricts raw hardware access. Fix: Run sudo usermod -a -G dialout $USER, then log out and log back in (or reboot) for the group change to take effect.
  2. Cause 2: Port is locked by another process. If the Arduino IDE Serial Monitor is open, or a previous Python script crashed without closing the port, the OS locks it. Fix: Run lsof | grep ttyACM0 to find the PID, then sudo kill -9 <PID>.
  3. Cause 3: ModemManager is interfering. On some Ubuntu/Debian builds, the OS probes new serial devices to see if they are modems. Fix: Disable it via sudo systemctl stop ModemManager.

The First Three Things to Check When It Fails

If the permission error is resolved but you still aren't getting data, run through this physical and logical checklist:

  1. Verify the USB cable is data-capable. Over 40% of micro-USB and USB-C cables bundled with cheap accessories are 'charge-only' and lack the internal D+/D- data lines. Swap to a known good data cable.
  2. Check the device enumeration path. The Uno R4 Minima usually mounts as /dev/ttyACM0, but if you plug in a USB hub or another device first, it might shift to /dev/ttyACM1. Run ls -l /dev/ttyACM* to verify.
  3. Confirm baud rate parity. Ensure the Serial.begin(115200) in the C++ sketch exactly matches the BAUD = 115200 variable in the Python script. A mismatch will result in garbled text and constant JSONDecodeError exceptions.

Extending and Simplifying the Build

Once the baseline serial handshake is stable, you can scale the system up or down based on your deployment environment.

How to Extend (Adding Computer Vision)

To turn this from a simple data logger into an intelligent agricultural node, add a Raspberry Pi Camera Module 3 (~$30). Mount it inside the enclosure facing the plant canopy. Using the Pi's hardware video encoder, run a lightweight YOLOv8-nano inference script via the ultralytics Python library. If the model detects early signs of powdery mildew or blight with >80% confidence, the Pi can publish an MQTT alert to Home Assistant and send a serial command to the Arduino to trigger a UV-C sterilization relay.

How to Simplify (Reducing BOM and Power)

If your project does not require local camera inference or a heavy local database, the Raspberry Pi is overkill and draws too much power for solar/battery deployments. Simplify the build by dropping the Raspberry Pi entirely and replacing the Arduino Uno R4 with an ESP32-S3 DevKitC-1 (~$12). The ESP32-S3 features dual-core 240MHz processing, native Wi-Fi/BLE, and enough hardware PWM channels to handle both the sensor polling and the cloud telemetry via MQTT, cutting your BOM cost by 70% and idle power draw from 4W down to ~0.8W.

For further reading on serial configurations and board specifics, refer to the official Arduino Uno R4 Minima documentation and the PySerial API reference. For advanced Pi peripheral routing, consult the Raspberry Pi hardware configuration guides.