Learning Goals 5 min
Yesterday you saw why pins 0 and 1 collide with USB upload. Today you free them. The SoftwareSerial library bit-bangs a UART on any pair of digital pins — your hardware UART stays connected to the USB debug monitor, and a second "virtual" UART talks to your peripheral. By the end of this lesson you will:
- Create a
SoftwareSerialobject on any two pins (e.g. D2 RX, D3 TX) and use it like a secondSerialport. - Run two UARTs simultaneously: the hardware
Serialto your laptop's monitor, and theSoftwareSerialto a peripheral (today: a wired loopback or a second UNO). - Explain three real limitations of
SoftwareSerialon a UNO: max ~57600 baud reliably, only one SoftwareSerial port can receive at a time, and it blocks interrupts during transmit.
Warm-Up 10 min
If you have your 2-UNO command pipe from yesterday, leave it disconnected for now. Today we add a second "virtual" UART so the hardware UART stays free for upload + Serial Monitor.
Why "Software" serial
The hardware UART is built into the microcontroller as a dedicated peripheral — pins 0 and 1 on the UNO, no software cost. SoftwareSerial implements the protocol entirely in software: a single digital pin watches for the start-bit falling edge, the CPU then times each bit by counting cycles, and reads the pin at the centre of each bit-time. Same wire protocol; no special hardware.
That trick lets you put a UART on any digital pin, with the cost of CPU cycles spent watching the pin.
Quick question
If SoftwareSerial can put UART on any pin, why does the UNO bother having a hardware UART at all? Why not use software for everything?
Reveal
Three reasons: speed (hardware reliably handles 115200+ baud; software tops out around 57600), no CPU load (the hardware UART sends/receives in the background while your code does other things), and interrupt safety (the hardware UART doesn't hog interrupts, but SoftwareSerial does — see §3). For everything other than "I've already used pins 0 and 1", hardware UART wins.
New Concept · A second virtual UART 25 min
Step 1 · Include the library
#include <SoftwareSerial.h>Built into the IDE. No install.
Step 2 · Create an object
SoftwareSerial mySerial(2, 3); // RX, TXOrder matters: RX first, TX second. Pin 2 will be the receive pin (incoming wire from another device's TX); pin 3 will transmit.
Step 3 · begin()
void setup() {
Serial.begin(9600); // hardware UART -> USB monitor
mySerial.begin(9600); // software UART -> peripheral
}Both UARTs can use the same baud rate (and usually should, to avoid confusion).
Step 4 · print(), println(), available(), read()
Same API as the hardware Serial:
mySerial.println("Hello peripheral!");
while (mySerial.available()) {
char c = mySerial.read();
Serial.print("from peripheral: ");
Serial.println(c);
}Note the bridging pattern: read from the software UART, write to the hardware UART (so you can watch what the peripheral sent in the Serial Monitor).
The three limitations to remember
- Speed.
SoftwareSerialworks up to 115200 baud in theory; in practice on a UNO, 57600 is the highest you can trust. Use 9600 or 38400 for problem-free links. - One receiver active at a time. You can create multiple
SoftwareSerialobjects, but only one can listen at any moment. CallportA.listen()to make that one the active receiver; data on the others is dropped. (Sending is fine on any of them, any time.) - Interrupt-heavy. Receiving a byte requires the library to handle a pin-change interrupt for each bit. While that's happening, your own interrupt handlers (e.g. for encoders) can be delayed. For combinations of
SoftwareSerial+ encoder counters, you may see encoder counts drop.
Which pins can be SoftwareSerial RX on a UNO?
The library needs pin-change interrupts on the RX pin. On the UNO that means any of pins 2–13 (and analog A0–A5). Pins 0 and 1 are reserved for the hardware UART. So in practice: pick any pair from 2..13 for RX/TX. Common choice is D2 (RX), D3 (TX).
What does "upload first, wire second" mean now?
Since we're not using pins 0/1 for the peripheral, you can leave the peripheral wired during upload. Pins 2/3 don't share with the USB chip — no upload conflict. This is the headline practical benefit.
Worked Example · USB monitor + peripheral, simultaneously 25 min
You'll wire one UNO with both UARTs running: hardware UART to USB (so you can type into the Serial Monitor), software UART to a peripheral. To avoid needing a real peripheral, the peripheral is just a loopback — pin 2 and pin 3 jumpered directly. Everything you send via software UART comes back to it.
Step 1 — wiring
- Jumper D2 to D3 (yes, just a wire). This is the loopback.
- USB cable to the Arduino. That's it.
Step 2 — sketch
// L03-16 · SoftwareSerial bridge — HW Serial <-> SW Serial loopback
#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3); // RX = D2, TX = D3 (jumpered together)
void setup() {
Serial.begin(9600);
mySerial.begin(9600);
Serial.println("# Type chars into the monitor. They go out SW, loop back, "
"and are printed with [echo] prefix.");
}
void loop() {
// 1. Anything I type goes from USB to SW
while (Serial.available()) {
char c = Serial.read();
mySerial.write(c);
}
// 2. Anything that comes in on SW (the loopback echo) is reported on USB
while (mySerial.available()) {
char c = mySerial.read();
Serial.print("[echo] ");
Serial.println(c);
}
}Step 3 — try it
Open Serial Monitor at 9600 baud. Type a + Enter. You should see:
[echo] a [echo] (newline)
Each character you type travels: USB → hardware Serial RX → your sketch → SW Serial TX (D3) → jumper → SW Serial RX (D2) → your sketch → hardware Serial TX → USB. A full round trip with two UART instances in use simultaneously.
Step 4 — break the loopback
Pull the jumper between D2 and D3. Type characters again. The echo lines stop appearing — nothing's coming back to the SW Serial RX because the wire is gone. Hardware Serial still works fine; the two are independent.
Step 5 — change the SW baud, observe garbage
Change mySerial.begin(9600) to mySerial.begin(2400). Re-upload. Hardware Serial is still at 9600 but software is now mis-rated. Type a character: you'll see [echo] followed by garbage. (The hardware UART decodes the character fine; the software UART transmits at 2400 baud but its receive side expects 9600 — mismatch.) Restore mySerial.begin(9600) and move on.
Step 6 — wire it to a real peripheral
If you have a second UNO, replace the jumper with the cross-wired UART of L03-15 §4 — but use pins 2/3 on the first UNO and pins 0/1 (hardware UART) on the second. You now have two UNOs talking, with the first one ALSO connected to its USB Serial Monitor at the same time. Type on the first UNO's monitor; the characters appear on the second UNO's LED (or its monitor, if you have a separate USB port for it).
Try It Yourself 15 min
Goal: Move the software UART to pins D4 (RX) and D5 (TX). Re-jumper, re-upload, re-test. Confirm the choice of pins doesn't affect behaviour (as long as the RX pin supports pin-change interrupts).
Hint
SoftwareSerial mySerial(4, 5); // was (2, 3)Don't forget to move the jumper too.
Goal: Create two SoftwareSerial objects on different pin pairs (e.g. (2,3) and (4,5)). Send a string out one, see it loop back in the other (cross-jumper D3↔D4, then D2↔D5 won't work as both can't listen). Better: jumper D3 to D4 (TX of port A to RX of port B); then anything you write on port A is read on port B.
Hint
SoftwareSerial portA(2, 3); // RX=D2, TX=D3
SoftwareSerial portB(4, 5); // RX=D4, TX=D5
void setup() {
Serial.begin(9600);
portA.begin(9600);
portB.begin(9600);
portB.listen(); // only one can be the active receiver
}
void loop() {
portA.println("Hi B");
delay(1000);
while (portB.available()) {
Serial.print((char)portB.read());
}
}Notice portB.listen() — without it, neither one is actively receiving. This is the "one receiver active at a time" limitation in practice.
Goal: Build a small "command translator": anything typed in the USB monitor that ends with \n is parsed; if it's the keyword FORWARD the sketch sends the single character F over the SW UART; BACKWARD → B; STOP → S; anything else → error. Sets up the protocol layer for the Bluetooth car (L03-25 / L03-28).
Hint
String line;
while (Serial.available()) {
char c = Serial.read();
if (c == '\n') {
if (line == "FORWARD") mySerial.write('F');
else if (line == "BACKWARD") mySerial.write('B');
else if (line == "STOP") mySerial.write('S');
else Serial.println("# unknown");
line = "";
} else {
line += c;
}
}This is the "reader / parser / writer" pattern that'll come back for every protocol in L3.
Mini-Challenge · Plan your UART pin map 10 min
You'll soon be juggling lots of peripherals: HC-05 Bluetooth (L03-23, needs SoftwareSerial), an ESP8266 (L03-30, needs SoftwareSerial), the L298N (digital pins), an OLED (I²C — different pins, lesson 17–19). Plan an example pin map for a UNO carrying:
- Hardware UART (D0, D1) → USB / debug (no peripheral here)
- L298N motor driver (4 control pins + 2 enable PWM pins)
- An HC-05 Bluetooth module on SoftwareSerial
- An I²C OLED (uses A4, A5 — different from digital pins)
Pick non-conflicting pins for each. Write the assignment in your notebook as a table.
Reveal one workable assignment
| Pin | Use |
|---|---|
| D0, D1 | USB Serial Monitor |
| D2, D3 | HC-05 RX, TX (SoftwareSerial) |
| D4 | L298N IN3 |
| D5 | L298N ENA (PWM) |
| D6 | L298N ENB (PWM) |
| D7 | L298N IN4 |
| D8, D9 | L298N IN1, IN2 |
| A4, A5 | OLED SDA, SCL (I²C — fixed on UNO) |
D10–D13 free for buttons, LEDs, future expansion.
Recap 5 min
SoftwareSerial creates a second "virtual" UART on any pair of digital pins, so the hardware UART stays free for USB debug. Same API as Serial — begin, println, available, read. The constructor order is RX first, TX second. Three real limits to remember: max ~57600 baud reliable, only one receiver active at a time, and interrupt overhead during receive. For Bluetooth modules (L03-23) and ESP-AT firmware (L03-30), SoftwareSerial is the standard pattern on the UNO. Tomorrow we leave point-to-point UART behind and meet the multi-drop bus: I²C.
SoftwareSerial- Built-in Arduino library that bit-bangs a UART on any digital pin pair. Pros: any pin. Cons: speed-limited, interrupt-heavy, one receiver at a time.
- Bit-banging
- Implementing a hardware protocol in software by directly toggling pins at the right times. Cheap (no special hardware) but expensive (CPU cycles, timing fragility).
- Pin-change interrupt
- A microcontroller feature that fires when any pin in a group changes state.
SoftwareSerialuses one on its RX pin to catch the start-bit falling edge. - Hardware UART
- The dedicated peripheral inside the microcontroller that sends and receives UART frames without CPU intervention. UNO has one; Mega has four; ESP32 has three.
- Listen / active receiver
- The currently-receiving
SoftwareSerialport. Only one can be active; switch withport.listen(). - Loopback
- Wiring a TX pin directly to its own RX pin (or another instance's RX). Lets you test the UART round-trip without a second board.
- Hardware UART vs SoftwareSerial trade-off
- Hardware: faster, no CPU cost, no interrupt overhead — but fixed to pins 0/1. Software: any pin — but slower, CPU-intensive, interrupt-heavy.
Homework 5 min
- Save the SW-Serial loopback sketch as
swserial-loopback.ino. - Add a comment block at the top of the sketch listing the three SoftwareSerial limitations from §3. Future-you will appreciate them.
- Read ahead to ARD-L03-17 (I²C Protocol Concepts). No build prep needed; we'll wire the OLED in lesson 19.
Bring back next class:
- Your annotated SW-Serial sketch.
- The pin-map table from §6 — we'll add I²C to it tomorrow.