Why Connect an Arduino to a Raspberry Pi? (And the 3.3V vs 5V Trap)

Pairing an Arduino with a Raspberry Pi gives you the best of both embedded worlds: the Pi handles high-level OS tasks like MQTT brokering, database logging, and computer vision, while the Arduino manages deterministic, real-time hardware polling like motor PWM and analog sensor reading. The most robust way to bridge them is via UART (Universal Asynchronous Receiver-Transmitter) serial communication over the GPIO pins.

However, the most common mistake makers make when wiring an Arduino to a Raspberry Pi is ignoring logic levels. The Raspberry Pi's Broadcom SoC operates at 3.3V logic. The standard Arduino Uno operates at 5V logic. If you wire the Arduino's 5V TX pin directly to the Pi's 3.3V RX pin, you will backfeed voltage into the Pi's GPIO ring. Over time, this degrades the BCM2711 silicon and will permanently brick your Raspberry Pi. You must use a bi-directional logic level shifter.

⚠️ CRITICAL HARDWARE WARNING: Never connect 5V Arduino GPIO directly to 3.3V Raspberry Pi GPIO. Always use a dedicated logic level converter or a verified voltage divider for the TX/RX lines.

Hardware Spec Sheet & Parts List

This guide targets the Arduino Uno R3 (ATmega328P) and the Raspberry Pi 4 Model B running Raspberry Pi OS (Bookworm or later). The code and wiring also apply to the Pi 5 and Arduino Mega, provided you adjust the GPIO pin numbers.

ComponentExact Variant / ModelEst. Price (2026)Role in Build
MicrocontrollerArduino Uno R3 (Rev3, DIP ATmega328P)$27.00Reads analog sensors, handles real-time I/O
Single Board ComputerRaspberry Pi 4 Model B (4GB RAM)$55.00Runs Python data-logging and network stack
Logic Level ShifterSparkFun BSS138 Bi-Directional (BOB-12009)$3.50Steps 5V UART down to 3.3V safely
Sensor (Example)10kΩ Linear Potentiometer$1.50Provides variable analog voltage to A0
Wiring22 AWG Solid Core Jumper Wires$5.00Breadboard connections

Difficulty Rating: Intermediate (Requires basic Linux CLI navigation and breadboard wiring).

Pin Mapping & Wiring the Logic Level Shifter

The BSS138 breakout board has a low-voltage (LV) side and a high-voltage (HV) side. The LV side connects to the Pi; the HV side connects to the Arduino. UART requires crossed TX/RX lines: the transmitter of one device must connect to the receiver of the other.

Raspberry Pi 4 (3.3V)BSS138 ShifterArduino Uno R3 (5V)Signal Direction
Pin 1 (3.3V Power)LV (Low Voltage)-Power Reference
Pin 2 (5V Power)-5V PinPower Reference
Pin 6 (GND)GND (LV Side)GNDCommon Ground (Crucial!)
GPIO 14 (TXD / Pin 8)LV1HV1 -> Pin 0 (RX)Pi transmits to Arduino
GPIO 15 (RXD / Pin 10)LV2HV2 <- Pin 1 (TX)Arduino transmits to Pi

Wiring Steps

  1. Establish Common Ground: Connect the Pi's GND (Pin 6), the Arduino's GND, and both GND rails on the BSS138 shifter together. Without a shared ground reference, the serial data will be unreadable garbage.
  2. Power the Shifter: Connect Pi Pin 1 (3.3V) to the LV pin on the shifter. Connect the Arduino 5V pin to the HV pin on the shifter.
  3. 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).
  4. Connect Sensor: Wire the potentiometer's outer pins to 5V and GND on the Arduino, and the wiper (middle pin) to Arduino A0.

The Code: Bidirectional UART Sensor Relay

Before running code, you must enable the hardware UART on the Raspberry Pi. By default, the Pi routes the console login shell to the serial port. Open the terminal and run sudo raspi-config. Navigate to Interface Options -> Serial Port. Select No for 'Would you like a login shell to be accessible over serial?', and Yes for 'Would you like the serial port hardware to be enabled?'. Reboot the Pi.

For deeper configuration details on the Pi's PL011 vs mini UART mappings, refer to the official Raspberry Pi UART documentation.

Arduino Firmware (C++)

This code reads the analog sensor and sends a CSV-formatted string over Serial. It also listens for incoming 'START' or 'STOP' commands from the Pi. This targets the Uno R3 hardware serial port (Pins 0 and 1).

// Target: Arduino Uno R3 (ATmega328P)
// Pins: Hardware Serial (0=RX, 1=TX), A0 (Sensor)

const int SENSOR_PIN = A0;
bool isLogging = true;

void setup() {
  // 9600 baud is safe for long wires and the Pi's mini UART
  Serial.begin(9600);
  pinMode(SENSOR_PIN, INPUT);
}

void loop() {
  // Check for incoming commands from Raspberry Pi
  if (Serial.available() > 0) {
    String cmd = Serial.readStringUntil('\n');
    cmd.trim();
    if (cmd == 'STOP') isLogging = false;
    else if (cmd == 'START') isLogging = true;
  }

  // Read sensor and transmit if logging is active
  if (isLogging) {
    int rawValue = analogRead(SENSOR_PIN);
    float voltage = rawValue * (5.0 / 1023.0);
    
    // Send as CSV: raw_value,voltage,status
    Serial.print(rawValue);
    Serial.print(',');
    Serial.print(voltage, 2);
    Serial.print(',');
    Serial.println('ACTIVE');
  }
  
  delay(500); // 2Hz sampling rate
}

Raspberry Pi Python Script

Install the PySerial library on your Pi via pip3 install pyserial. This script targets /dev/ttyS0 (the mini UART mapped to GPIO 14/15 on the Pi 4). See the PySerial official documentation for advanced timeout configurations.

# Target: Raspberry Pi 4 Model B (Python 3.11+, Raspberry Pi OS Bookworm)
import serial
import time
import sys

PORT = '/dev/ttyS0'  # Use '/dev/ttyAMA0' if PL011 is explicitly enabled
BAUD_RATE = 9600

def main():
    try:
        # timeout=1 prevents the script from hanging indefinitely if Arduino resets
        ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
        print(f'Successfully opened {PORT} at {BAUD_RATE} baud.')
        
        # Send an initialization command to Arduino
        ser.write(b'START\n')
        time.sleep(1) # Wait for Arduino to process

        while True:
            if ser.in_waiting > 0:
                # Read line, decode bytes to string, strip whitespace
                line = ser.readline().decode('utf-8', errors='ignore').strip()
                
                if line:
                    parts = line.split(',')
                    if len(parts) == 3:
                        raw, volts, status = parts
                        print(f'[Sensor] Raw: {raw} | Volts: {volts}V | State: {status}')
                    else:
                        print(f'Malformed packet received: {line}')
            
            time.sleep(0.1) # Yield CPU

    except serial.SerialException as e:
        print(f'FATAL SERIAL ERROR: {e}')
        sys.exit(1)
    except KeyboardInterrupt:
        print('\nGraceful shutdown. Sending STOP command.')
        if 'ser' in locals() and ser.is_open:
            ser.write(b'STOP\n')
            ser.close()

if __name__ == '__main__':
    main()

Debugging: 'Permission Denied' and Serial Timeouts

When bridging an Arduino to a Raspberry Pi via GPIO UART, Linux permission and port-mapping issues are the primary culprits for failure. Below are the exact error strings and how to resolve them.

Exact Error String:
serial.serialutil.SerialException: [Errno 13] could not open port '/dev/ttyS0': [Errno 13] Permission denied: '/dev/ttyS0'

Ranked Causes & Fixes for Errno 13

  1. User lacks dialout group privileges (Most Likely): The default 'pi' or 'admin' user doesn't have hardware access rights. Fix: Run sudo usermod -a -G dialout $USER, then log out and log back in (or reboot) for the group policy to apply.
  2. Bluetooth is holding the UART port: On the Pi 4, the hardware UART is often routed to the Bluetooth module by default. Fix: Add dtoverlay=disable-bt to the bottom of /boot/firmware/config.txt and reboot.
  3. Serial Console is active: The OS is using the port for terminal output. Fix: Re-run sudo raspi-config and ensure the 'login shell over serial' option is disabled.
Exact Error String:
serial.serialutil.SerialException: device reports readiness to read but returned no data (device disconnected or multiple access on port?)

Ranked Causes for Readiness/No Data Error

  1. Missing Common Ground: The data lines are connected, but the ground reference is floating. The Pi sees voltage fluctuations but can't decode the bits. Fix: Verify continuity between Pi Pin 6 and Arduino GND using a multimeter (should read < 1 ohm).
  2. TX/RX Not Crossed: You wired TX to TX and RX to RX. Fix: Swap the HV1/HV2 wires on the Arduino side.
  3. Multiple Scripts Accessing Port: A zombie Python process from a previous run is still holding /dev/ttyS0 open. Fix: Run lsof | grep ttyS0, find the PID, and kill it with sudo kill -9 [PID].

The First Three Things to Check When It Fails

If you are getting zero output and no Python errors, run this physical checklist:

  1. Check the Baud Rate Match: Ensure both the Arduino Serial.begin() and the Python serial.Serial() are set to exactly 9600. Mismatched baud rates result in silent garbage data.
  2. Verify the Shifter Power Rails: Use a multimeter to probe the LV and HV pins on the BSS138 board. LV must read 3.2V-3.3V; HV must read 4.8V-5.0V. If HV is reading 3.3V, your Arduino 5V line is dead.
  3. Test with Loopback: Disconnect the Arduino. Use a jumper wire to short Pi GPIO 14 (TX) directly to Pi GPIO 15 (RX) through the LV side of the shifter. Run a Python script that writes and reads. If it echoes back, the Pi and shifter are fine; the issue is the Arduino code or wiring.

Extending and Simplifying the Build

Depending on your end goal, GPIO UART might not be the optimal topology. Here is how to adapt the architecture.

How to Simplify: The USB Bypass

If you do not need a strictly headless, embedded form factor, ditch the GPIO pins and logic shifter entirely. Plug a standard USB Type-B to Type-A cable directly from the Arduino Uno into the Raspberry Pi's USB port.

  • Pros: Automatic 5V to 3.3V logic shifting handled by the Arduino's onboard ATmega16U2 chip. No /dev/ttyS0 permission headaches; it mounts cleanly as /dev/ttyACM0.
  • Cons: Uses a USB port, slightly higher latency, and requires a beefier Pi power supply to source the Arduino's 50mA idle current.

How to Extend: RS-485 for Long Distance

Standard UART over GPIO or USB is limited to about 15 feet (5 meters) before signal degradation causes bit errors. If your Arduino is reading a soil moisture sensor at the end of a greenhouse and the Pi is in the house, use RS-485.

Add a MAX485 TTL-to-RS-485 module to both the Arduino and the Pi (via a USB-RS485 adapter on the Pi side). RS-485 uses differential signaling, allowing reliable Arduino to Raspberry Pi communication over twisted-pair cable up to 1,200 meters (4,000 feet) away, completely immune to the EMI generated by AC water pumps.

Frequently Asked Questions (FAQ)

Can I connect an Arduino to a Raspberry Pi without a logic level shifter?

No, not if you are using a standard 5V Arduino Uno or Mega. The 5V TX output will slowly destroy the Raspberry Pi's 3.3V tolerant RX GPIO pin. If you absolutely must avoid a shifter, you have two options: build a voltage divider using a 10kΩ and 20kΩ resistor on the Arduino TX line, or switch to a 3.3V native Arduino board like the Arduino Pro Mini (3.3V/8MHz variant) or an Arduino Nano 33 IoT.

How do I send data from Arduino to Raspberry Pi over WiFi?

If you want to eliminate wires entirely, replace the Arduino Uno with an ESP32 DevKit V1. The ESP32 has the same Arduino IDE programming environment but includes native 802.11 b/g/n WiFi. Instead of UART, you would program the ESP32 to publish sensor payloads to an MQTT broker (like Mosquitto) running on the Raspberry Pi over your local network. This is the preferred architecture for modern IoT sensor nodes.

Why is my Raspberry Pi not receiving serial data from the Arduino?

If the Python script runs without throwing an [Errno 13] exception but prints nothing, the most common culprit is the Arduino auto-reset feature. When the Pi opens the serial port (asserting the DTR line), it can trigger the Arduino's bootloader, causing it to pause for 2-3 seconds before running your loop(). Add a 2-second time.sleep(2) in your Python script immediately after calling serial.Serial() to allow the Arduino time to boot and begin transmitting.

Is USB or GPIO UART better for Arduino to Raspberry Pi communication?

Use USB for prototyping, desktop deployments, and when you want plug-and-play simplicity without worrying about Linux device tree overlays. Use GPIO UART for permanent, headless embedded installations (like a robotics chassis or a wall-mounted kiosk) where you want to minimize physical footprint, reduce power consumption by bypassing USB controllers, and secure the physical connection against vibration-induced disconnects.

For more embedded systems architecture and wiring guides, explore our Arduino Serial Reference archives and Raspberry Pi integration tutorials.