To establish reliable Arduino to Arduino communication serial, connect the TX pin of the first board to the RX pin of the second, RX to TX, and crucially, tie their GND pins together. For bench setups where you still need the USB port for debugging, use the SoftwareSerial library on digital pins 10 and 11. If your boards are more than 1 meter apart, abandon standard UART and use RS-485 transceiver modules.
This guide provides the exact wiring, a robust packetized C++ codebase with timeout handling, and a diagnostic matrix to solve the most common serial failures.
The Decision Tree: Which Serial Protocol Should You Use?
Before wiring anything, select the correct physical layer. Standard UART (TTL serial) is not a bus; it is a point-to-point connection with strict distance and noise limitations. Use this decision table to lock in your hardware approach.
| Condition / Constraint | Protocol / Hardware | Verdict |
|---|---|---|
| Distance < 50cm, 1-to-1, USB debug not needed | Hardware UART (Pins 0/1) | Use only if you can unplug USB during operation. |
| Distance < 50cm, 1-to-1, need USB debug | SoftwareSerial (Pins 10/11) | DEFAULT PICK for standard bench projects. |
| Distance > 1 meter or noisy environment | RS-485 (MAX485 modules) | Required for long runs. See SparkFun's RS-485 guide. |
| Multi-node (>2 boards) on same bus | I2C or CAN bus | UART cannot handle multi-drop. Use I2C (<1m) or CAN (>1m). |
Parts List and Pin Mapping
This build targets the Arduino Uno R3 (Master) and the Arduino Nano V3 (Slave). Both utilize the ATmega328P microcontroller, but their USB-UART bridge chips differ, which impacts driver installation.
Bill of Materials
| Component | Exact Variant | Qty | Notes |
|---|---|---|---|
| Master Board | Arduino Uno R3 (ATmega16U2 USB chip) | 1 | Native driver support on Windows/Mac/Linux. |
| Slave Board | Arduino Nano V3 (CH340G USB chip) | 1 | Requires CH340 driver installation on most OS. |
| Jumper Wires | 22 AWG Stranded Male-to-Male | 3 | Do not use 28 AWG for runs over 10cm. |
| Breadboard | Standard 830-point solderless | 1 | Used to distribute the common ground. |
Pin Mapping Table
| Signal | Master (Uno R3) | Slave (Nano V3) | Wire Color (Standard) |
|---|---|---|---|
| Ground | GND (any) | GND (any) | Black |
| Master TX / Slave RX | D11 (Software TX) | D10 (Software RX) | Green |
| Master RX / Slave TX | D10 (Software RX) | D11 (Software TX) | White |
Wiring the Hardware
Follow these steps exactly. The most common cause of serial failure is skipping the ground connection.
- De-energize both boards. Unplug the USB cables from your PC or wall adapters.
- Establish the common ground. Insert a black jumper wire from any GND pin on the Uno to the GND rail on the breadboard. Insert a second black wire from the Nano GND to the same rail. Without a shared ground reference, the voltage levels representing '1' and '0' will float, resulting in garbage data.
- Wire the TX/RX crossover. Connect Uno Pin 11 (TX) to Nano Pin 10 (RX). Connect Uno Pin 10 (RX) to Nano Pin 11 (TX). TX must always connect to RX on the opposing board.
- Verify power isolation. Do not connect the 5V or VIN pins between the two boards if both are plugged into separate USB ports. Back-feeding 5V from one USB port into another can damage the PC's motherboard or the Arduino's voltage regulator.
Compilable UART Code with Error Handling
Raw string transmission is fragile. If a byte is dropped due to noise, the receiver will hang or misinterpret the data. The code below implements a basic packet structure: <START_BYTE> <PAYLOAD> <CHECKSUM>. It also includes a millis()-based timeout to prevent the receiver from blocking indefinitely if a packet is corrupted.
Target Boards: Arduino Uno R3 (Master) and Arduino Nano V3 (Slave). Both use the ATmega328P architecture.
Master Code (Uno R3)
The Master requests a sensor value (simulated here) from the Slave every 2 seconds and parses the response.
#include <SoftwareSerial.h>
// Pin definitions
const int RX_PIN = 10;
const int TX_PIN = 11;
SoftwareSerial mySerial(RX_PIN, TX_PIN);
const byte START_BYTE = 0xFE;
const unsigned long TIMEOUT_MS = 500;
void setup() {
Serial.begin(9600); // USB Debug
mySerial.begin(9600); // Inter-board Serial
Serial.println("Master Initialized.");
}
void loop() {
// Send request packet: START_BYTE, CMD (0x01), CHECKSUM
byte cmd = 0x01;
byte checksum = START_BYTE ^ cmd;
mySerial.write(START_BYTE);
mySerial.write(cmd);
mySerial.write(checksum);
// Wait for response with timeout
unsigned long startTime = millis();
bool packetComplete = false;
byte rxBuffer[3];
int bufferIndex = 0;
while (millis() - startTime < TIMEOUT_MS) {
if (mySerial.available()) {
rxBuffer[bufferIndex++] = mySerial.read();
if (bufferIndex == 3) {
packetComplete = true;
break;
}
}
}
if (packetComplete) {
byte rxChecksum = rxBuffer[0] ^ rxBuffer[1];
if (rxBuffer[0] == START_BYTE && rxChecksum == rxBuffer[2]) {
Serial.print("Valid Response Received: ");
Serial.println(rxBuffer[1]);
} else {
Serial.println("Error: Checksum mismatch.");
}
} else {
Serial.println("Error: Serial timeout. No response from Slave.");
}
delay(2000);
}
Slave Code (Nano V3)
The Slave listens for the Master's request, validates the checksum, and replies with a simulated sensor value.
#include <SoftwareSerial.h>
const int RX_PIN = 10;
const int TX_PIN = 11;
SoftwareSerial mySerial(RX_PIN, TX_PIN);
const byte START_BYTE = 0xFE;
void setup() {
Serial.begin(9600);
mySerial.begin(9600);
}
void loop() {
if (mySerial.available() >= 3) {
byte start = mySerial.read();
byte cmd = mySerial.read();
byte rxChecksum = mySerial.read();
byte calcChecksum = start ^ cmd;
if (start == START_BYTE && calcChecksum == rxChecksum) {
if (cmd == 0x01) {
// Simulate reading a sensor (e.g., value 42)
byte sensorVal = 42;
byte txChecksum = START_BYTE ^ sensorVal;
mySerial.write(START_BYTE);
mySerial.write(sensorVal);
mySerial.write(txChecksum);
}
} else {
Serial.println("Slave: Received corrupted packet.");
}
}
}
Debugging: The First Three Things to Check
When Arduino to Arduino communication serial fails, it almost always manifests in one of three ways. Use this ranked diagnostic matrix before rewriting your code.
1. Gibberish Output: ⸮⸮⸮ or ??
- Exact Error String: Serial monitor displays
⸮⸮⸮,??, or random high-ASCII characters. - Most Likely Cause: Baud rate mismatch between the transmitter and receiver, or between the board and the PC Serial Monitor.
- Second Cause: Missing common ground wire, causing the logic levels to float.
- Fix: Verify
mySerial.begin(9600)matches on both boards. Verify the dropdown in the Arduino IDE Serial Monitor is set to exactly 9600 baud. Check the GND continuity with a multimeter (should read < 1 ohm).
2. Compilation Failure on Nano/Uno
- Exact Error String:
'SoftwareSerial' does not name a typeor'mySerial' was not declared in this scope. - Most Likely Cause: Missing the library inclusion at the top of the sketch.
- Fix: Ensure
#include <SoftwareSerial.h>is the very first line of your code. Note: If you switch to an ESP32 or Arduino Due later,SoftwareSerialis deprecated or unsupported; you must use their native hardware UART (Serial1,Serial2) instead.
3. Total Silence (Timeouts)
- Exact Error String: Master prints
Error: Serial timeout. No response from Slave.while Slave prints nothing. - Most Likely Cause: TX and RX wires are swapped, or the code pin definitions do not match the physical wiring.
- Fix: Confirm physical Pin 11 on the Master goes to physical Pin 10 on the Slave. A common mistake is defining
SoftwareSerial mySerial(10, 11)but wiring them backwards. The constructor order is always(RX, TX).
Extending and Simplifying the Build
Once the baseline communication is working, you will need to adapt the design for production or scale. Here is how to modify the architecture based on your end goal.
How to Simplify (Drop SoftwareSerial)
SoftwareSerial consumes significant CPU cycles and disables interrupts while transmitting, which can interfere with timing-sensitive libraries (like Servo.h or IRremote). If your project is deployed and no longer needs USB debugging:
- Remove the
SoftwareSeriallibrary. - Move the physical wires to Hardware Pins 0 (RX) and 1 (TX).
- Replace all
mySerialcalls with the nativeSerialobject. - Warning: You must disconnect the wires from Pins 0 and 1 every time you upload new code via USB, as the PC's USB-UART bridge will fight your external Arduino for control of those pins, resulting in an
avrdude: stk500_recv(): programmer is not respondingupload error.
How to Extend (Long Distance and Multi-Drop)
Standard TTL UART (0V to 5V) is highly susceptible to electromagnetic interference (EMI) and voltage drop over long wires. If your distance exceeds 50cm:
- Up to 1200 meters: Add two MAX485 TTL to RS-485 converter modules (approx. $2 each). RS-485 uses differential signaling, which rejects common-mode noise. You will need to add a digital pin to control the DE/RE (Driver/Receiver Enable) pins on the modules to switch between transmit and receive modes.
- Adding more nodes: UART is strictly point-to-point. If you need a Master to talk to three Slaves, abandon UART and switch to I2C (using the Wire library) for distances under 1 meter, or implement an RS-485 bus with a software addressing protocol for longer distances.
- Data Integrity: The XOR checksum in the code above catches single-bit flips. For mission-critical data, replace the XOR logic with a CRC-8 algorithm to catch multi-bit burst errors common in electrically noisy environments (like near AC motors or relays).
For deeper reading on robust serial packet parsing, reference Nick Gammon's serial processing guide, which remains the definitive standard for state-machine based serial reception in the Arduino ecosystem.






