Connecting a microcontroller to a single-board computer is a rite of passage for embedded builders. When you need to control an Arduino from Raspberry Pi, you are essentially bridging a real-time hardware node with a high-level Linux host. The Pi handles the heavy lifting—networking, databases, and UI—while the Arduino manages strict timing, PWM, and analog reads.
This guide cuts through the forum noise and gives you the exact decision framework, wiring, and code to get USB serial communication running reliably, along with the specific Linux commands to fix the permissions errors that trip up 90% of beginners.
The Verdict: Protocol Decision Matrix
Before wiring anything, you need to pick your physical layer. Do not default to I2C just because it uses fewer wires; USB Serial is vastly superior for 95% of Pi-to-Arduino projects. Use this decision tree to lock in your approach.
| If your project requires... | Choose this protocol | Why? |
|---|---|---|
| Sending JSON, text logs, or >32 byte payloads | USB Serial (Default Pick) | Handles large buffers natively; no pull-up resistors needed; hot-swappable. |
| Sub-millisecond sensor polling on the same PCB | I2C (GPIO Pins) | Lower overhead for tiny payloads, but requires level shifting (Pi is 3.3V, Arduino is 5V). |
| Flashing Arduino firmware without a host PC | ISP (SPI Pins) | Allows the Pi to act as an AVR programmer via avrdude. |
Parts List & Pin Mapping for USB Serial
This build targets the Arduino Uno R3 (ATmega328P) and the Raspberry Pi 4 Model B (or Pi 5) running Raspberry Pi OS (Bookworm or later).
Required Components
- Host: Raspberry Pi 4 Model B (4GB) or Raspberry Pi 5
- Node: Arduino Uno R3 (Genuine or ATmega16U2-based clone)
- Cable: USB-A to USB-B cable (Must be a data cable, not a charge-only cable)
- Peripherals: 1x 220Ω resistor, 1x 5mm LED (for testing)
Pin Mapping Table
Because we are using USB, the physical data pins (D0/D1) are handled by the onboard ATmega16U2 USB-to-Serial chip. However, if you later pivot to I2C, you must use the correct GPIO mapping.
| Function | Arduino Uno R3 Pin | Raspberry Pi 4/5 Pin (Physical) | Notes |
|---|---|---|---|
| USB Data + | USB-B Connector | USB-A Port | Virtual mapping via /dev/ttyACM0 |
| USB Data - | USB-B Connector | USB-A Port | Virtual mapping via /dev/ttyACM0 |
| I2C SDA (Fallback) | A4 | Pin 3 (GPIO 2) | Warning: Requires bi-directional logic level shifter |
| I2C SCL (Fallback) | A5 | Pin 5 (GPIO 3) | Warning: Requires bi-directional logic level shifter |
| Test LED | D13 (via 220Ω) | N/A | Or use the onboard SMD LED on pin 13 |
Step-by-Step: Firmware and Host Code
We will write a C++ sketch for the Arduino that listens for a specific string command, and a Python script on the Pi that sends that command and reads the acknowledgment.
Step 1: Flash the Arduino Firmware
Upload this code to your Uno R3 using the Arduino IDE on your PC (or directly on the Pi if you have the IDE installed). Note the explicit pin definition and the trim() function, which strips hidden carriage returns that often break serial string matching.
// Target Board: Arduino Uno R3 (ATmega328P)
const int LED_PIN = 13;
bool ledState = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
Serial.begin(9600);
// Wait for serial port to connect. Needed for native USB boards.
while (!Serial) {
;
}
Serial.println("ARDUINO_READY");
}
void loop() {
if (Serial.available() > 0) {
String command = Serial.readStringUntil('\n');
command.trim(); // Critical: removes \r and whitespace
if (command == "TOGGLE_LED") {
ledState = !ledState;
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
// Send acknowledgment back to Pi
if (ledState) {
Serial.println("STATE:ON");
} else {
Serial.println("STATE:OFF");
}
}
}
}
Step 2: Configure the Raspberry Pi Environment
Open a terminal on your Pi. You need to install the pyserial library and ensure your user has permission to access the USB serial ports.
- Install pyserial:
sudo apt update && sudo apt install python3-serial -y - Add your user to the dialout group:
sudo usermod -a -G dialout $USER - Reboot the Pi or log out and back in for the group change to take effect.
- Plug in the Arduino and verify the port:
ls -l /dev/ttyACM*(It should list/dev/ttyACM0).
Step 3: Run the Python Host Script
Create a file named pi_controller.py and paste the following code. This script includes robust error handling for the most common serial connection failures.
import serial
import time
import sys
# Target Board: Raspberry Pi 4/5 running Raspberry Pi OS
PORT = '/dev/ttyACM0'
BAUD_RATE = 9600
def connect_arduino():
try:
# timeout=1 prevents readline() from blocking forever if Arduino crashes
ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
time.sleep(2) # Wait for Arduino auto-reset cycle to complete
return ser
except serial.SerialException as e:
print(f"[FATAL] Failed to open port: {e}")
sys.exit(1)
def main():
ser = connect_arduino()
print(f"Connected to {PORT} at {BAUD_RATE} baud.")
try:
while True:
# Send command to Arduino
ser.write(b'TOGGLE_LED\n')
time.sleep(0.5) # Allow Arduino time to process and respond
# Read response
if ser.in_waiting > 0:
response = ser.readline().decode('utf-8').strip()
print(f"Arduino says: {response}")
time.sleep(2) # Wait before next toggle
except KeyboardInterrupt:
print("\n[INFO] Stopping script...")
finally:
if 'ser' in locals() and ser.is_open:
ser.close()
print("[INFO] Serial port closed.")
if __name__ == '__main__':
main()
Run the script with python3 pi_controller.py. You should see the LED toggle every 2 seconds, with the Pi printing Arduino says: STATE:ON and STATE:OFF.
Debugging: Exact Error Strings and the First Three Checks
Serial communication on Linux is unforgiving. If your script fails, do not guess. Look at the exact traceback and follow this ranked troubleshooting path.
The First Three Things to Check
- The Cable: Is it a charge-only cable? If
ls /dev/ttyACM*returns "No such file or directory", swap the cable. Micro-USB cables from cheap desk fans often lack the D+/D- data wires. - Permissions: Did you forget to reboot after running
usermod? The Linux kernel will block the Python script from touching the hardware node if your user isn't in thedialoutgroup. - Baud Rate & Reset: Is the Pi reading garbage text? Ensure both scripts say
9600. Also, opening the serial port on the Pi triggers a hardware reset on the Arduino Uno. Thetime.sleep(2)in the Python script is mandatory to let the bootloader finish.
Exact Error Strings and Fixes
Error 1: Permission Denied
serial.serialutil.SerialException: [Errno 13] could not open port /dev/ttyACM0: [Errno 13] Permission denied: '/dev/ttyACM0'
- Cause: Your user lacks read/write access to the TTY device file.
- Fix: Run
sudo usermod -a -G dialout $USER, then reboot. Do not run your Python script withsudoas a shortcut; it masks the problem and creates root-owned log files.
Error 2: No Such File or Directory
serial.serialutil.SerialException: [Errno 2] could not open port /dev/ttyACM0: [Errno 2] No such file or directory: '/dev/ttyACM0'
- Cause: The kernel does not see a valid USB-Serial device.
- Fix: Run
dmesg | grep tty. If you seech341-converter, your Arduino is a clone using a CH340 chip. The port will likely be/dev/ttyUSB0instead ofttyACM0. Update your PythonPORTvariable accordingly.
Error 3: Unicode Decode Error
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte
- Cause: Baud rate mismatch, or the Arduino sent raw binary data while Python expected UTF-8 text.
- Fix: Verify
Serial.begin(9600)matchesBAUD_RATE = 9600. If sending raw bytes, change.decode('utf-8')to handle raw byte arrays in Python.
Extending or Simplifying the Build
Once you have basic USB serial working, you will inevitably want to scale the project. Here is how to pivot based on your end goal.
How to Extend: Add an MQTT Bridge
If you want to control the Arduino from your phone or integrate it with Home Assistant, do not write a custom TCP socket server on the Pi. Instead, install paho-mqtt on the Pi. Modify the Python script to subscribe to an MQTT topic (e.g., home/livingroom/light). When an MQTT message arrives, the Pi translates it and pushes TOGGLE_LED\n over the USB serial port to the Arduino. This keeps the Arduino completely isolated from the network stack.
How to Simplify: Use StandardFirmata
If you hate writing C++ and just want to treat the Arduino as a dumb I/O expander for Python, skip the custom serial parsing entirely.
- Open the Arduino IDE and flash the FirmataPlus or StandardFirmata example sketch to the Uno.
- On the Pi, install the pyFirmata library:
pip3 install pyfirmata. - Control pins directly from Python without writing a single line of Arduino C++:
from pyfirmata import Arduino, util
import time
board = Arduino('/dev/ttyACM0')
board.digital[13].mode = 1 # Set pin 13 to OUTPUT
while True:
board.digital[13].write(1)
time.sleep(1)
board.digital[13].write(0)
time.sleep(1)
Firmata is the ultimate shortcut for prototyping, though it consumes more SRAM on the ATmega328P and introduces slight latency compared to a tightly optimized custom serial loop. For production deployments, stick to the custom C++ and Python pyserial implementation detailed above.
References: PySerial Short Introduction, Arduino Serial Reference, Raspberry Pi OS Configuration Documentation.






