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.
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.
| Component | Exact Variant / Model | Est. Price (2026) | Role in Build |
|---|---|---|---|
| Microcontroller | Arduino Uno R3 (Rev3, DIP ATmega328P) | $27.00 | Reads analog sensors, handles real-time I/O |
| Single Board Computer | Raspberry Pi 4 Model B (4GB RAM) | $55.00 | Runs Python data-logging and network stack |
| Logic Level Shifter | SparkFun BSS138 Bi-Directional (BOB-12009) | $3.50 | Steps 5V UART down to 3.3V safely |
| Sensor (Example) | 10kΩ Linear Potentiometer | $1.50 | Provides variable analog voltage to A0 |
| Wiring | 22 AWG Solid Core Jumper Wires | $5.00 | Breadboard 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 Shifter | Arduino Uno R3 (5V) | Signal Direction |
|---|---|---|---|
| Pin 1 (3.3V Power) | LV (Low Voltage) | - | Power Reference |
| Pin 2 (5V Power) | - | 5V Pin | Power Reference |
| Pin 6 (GND) | GND (LV Side) | GND | Common Ground (Crucial!) |
| GPIO 14 (TXD / Pin 8) | LV1 | HV1 -> Pin 0 (RX) | Pi transmits to Arduino |
| GPIO 15 (RXD / Pin 10) | LV2 | HV2 <- Pin 1 (TX) | Arduino transmits to Pi |
Wiring Steps
- 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.
- 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.
- 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).
- 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.
serial.serialutil.SerialException: [Errno 13] could not open port '/dev/ttyS0': [Errno 13] Permission denied: '/dev/ttyS0'
Ranked Causes & Fixes for Errno 13
- 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. - 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-btto the bottom of/boot/firmware/config.txtand reboot. - Serial Console is active: The OS is using the port for terminal output. Fix: Re-run
sudo raspi-configand ensure the 'login shell over serial' option is disabled.
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
- 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).
- TX/RX Not Crossed: You wired TX to TX and RX to RX. Fix: Swap the HV1/HV2 wires on the Arduino side.
- Multiple Scripts Accessing Port: A zombie Python process from a previous run is still holding
/dev/ttyS0open. Fix: Runlsof | grep ttyS0, find the PID, and kill it withsudo 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:
- Check the Baud Rate Match: Ensure both the Arduino
Serial.begin()and the Pythonserial.Serial()are set to exactly 9600. Mismatched baud rates result in silent garbage data. - 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.
- 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/ttyS0permission 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.






