Peripheral control and communication
A program stops being an exercise when it lights a real LED. At the other end of the cable a microcontroller is waiting for bytes, and you have to agree on what each one means.
01From the program to the physical world
A program that only prints to the screen does not need to know anything about the hardware. As soon as you have to light an LED or read a sensor, three questions appear: where does the data go out, how fast, and what does each byte mean.
| Interface | Typical speed | Where it is used |
|---|---|---|
| Asynchronous serial (UART/USB-serial) | 9600 to 921,600 baud | The most common link between a PC and a microcontroller |
| I²C | 100 kHz to 1 MHz | Sensors and peripherals within a board |
| SPI | 1 to 50 MHz | Memories, displays, fast converters |
| Native USB | 12 to 480 Mbit/s | Instruments and data acquisition |
| Ethernet / TCP-IP | 10 to 1000 Mbit/s | Networked equipment, industry |
On the computer, the serial port is opened as if it were a file: you configure it, write to it and read from it.
In Python with pyserial it takes three lines; in C, with termios on Linux or
CreateFile on Windows, a few more, but the idea is the same.
import serial
p = serial.Serial("COM3", 115200, timeout=0.2)
p.write(b"\xAA\x02\x01\x64\x67")
response = p.read(5)
02The parameters that must be agreed on
Both ends have to be configured the same way or they will not understand each other. With 8N1 —eight data bits, no parity, one stop bit— each byte travels in ten bits:
- Speed in baud: 9600, 19,200, 38,400, 57,600, 115,200…
- Data bits, almost always 8.
- Parity: none, even or odd. It detects one flipped bit, not two.
- Stop bits: 1 or 2.
- Flow control: hardware (RTS/CTS) or none.
A microcontroller’s UART works at 3.3 or 5 V; a true RS-232 port uses ±12 V with inverted logic. Connecting them directly burns out the microcontroller. In between goes an adapter —a MAX232 or a USB-serial converter— and, if the voltages differ, a level shifter.
03Inventing a protocol
The port carries loose bytes; the meaning is supplied by the protocol. A minimal scheme that works in practice has four parts:
| Field | Purpose |
|---|---|
| Start byte | Marks where the frame begins, so that it can resynchronize |
| Command and length | What is requested and how many data bytes follow |
| Data | The content |
| Check | Detects whether something arrived wrong: sum, XOR or CRC |
Change the command and the data, choose the check method and try corrupting a byte to see whether the receiver notices.
04Sum, XOR and CRC
| Method | How it is calculated | What it detects |
|---|---|---|
| Checksum | \( \sum b_i \bmod 256 \) | One changed byte; not the order |
| XOR | \( b_1 \oplus b_2 \oplus \cdots \oplus b_n \) | One flipped bit per column; fails with two |
| CRC-8 | Binary division by a polynomial | All bursts of up to 8 bits |
| CRC-16 / CRC-32 | The same, with a longer polynomial | What is used in Modbus, USB and Ethernet |
The sum is the easiest to write and the weakest: if two bytes change in compensating ways, it does not notice. The CRC is a division of polynomials in binary arithmetic and detects practically any real transmission error; that is why every serious protocol uses it. It is implemented with a 256-entry table and is extremely fast, even on a small microcontroller.
05Waiting without getting stuck
The most common mistake when communicating with a device is to wait forever. There are three strategies, and it is worth knowing all three:
- Read with a timeout.
timeouton the port: if the device does not answer within 200 ms, the read returns empty and the program decides what to do. It is the bare minimum. - Dedicated thread. One thread reads the port and deposits the frames in a queue; the main program consumes it when it can. This is the producer-consumer scheme.
- Asynchronous input/output. The system signals when data is available, with nobody waiting.
The microcontroller has the same problem in reverse: if it services the port inside the main loop, it loses bytes while it does something else. That is why reception is done in the UART interrupt, which stores each byte in a circular buffer, and the main program processes the frame when it is complete.
06In the lab
Program the microcontroller to send back every byte it receives. From the computer, measure the round-trip time at 9600 and at 115,200 baud, and compare with 10 bits per byte.
Define and implement the protocol of this page at both ends: turn an output on and off, set a PWM and read an analog channel. Document the frame in a table.
Unplug the cable in the middle of a transmission, or lower the speed on one side. Check that the receiver discards malformed frames and resynchronizes on the start byte, instead of hanging.
07Common mistakes
- Different speeds at each end. Bytes arrive, but with garbage.
- Reading without a timeout and leaving the program hung forever.
- Assuming that one read returns the whole frame. It may come back split: you have to accumulate until you have the declared length.
- Connecting 5 V to a 3.3 V input.
- Forgetting the common ground between the two devices.
- Trusting that the channel is perfect and not checking anything.
08Self-assessment
How many bytes per second come in at 115,200 baud in 8N1?
Each byte takes up 10 bits, so 11,520 bytes/s, about 87 µs per byte.
What is the start byte of a frame for?
To be able to resynchronize: if a byte was lost, the receiver discards data until it finds the next start byte instead of interpreting garbage.
Why is the CRC better than the sum?
Because it detects bursts of errors and combinations that the sum does not see, such as two bytes that change in compensating ways.
Where is it best to receive the bytes on the microcontroller?
In the UART interrupt, storing them in a circular buffer. In the main loop they would be lost while the program does something else.
What happens if a 3.3 V UART and an RS-232 port are connected?
RS-232 uses ±12 V with inverted logic: it burns out the input. You need an adapter such as the MAX232.
A read returned 3 bytes of a 6-byte frame. What should be done?
Accumulate them and keep reading until the declared length is complete, or discard on timeout. A serial read does not guarantee delivering the whole frame at once.
09Further reading
- Jan Axelson. Serial Port Complete. 2nd ed., Lakeview Research, 2007. Everything about serial ports, USB-serial and programming them on the PC.
- Michael Barr and Anthony Massa. Programming Embedded Systems in C and C++. 2nd ed., O'Reilly, 2006. The microcontroller side: interrupts, buffers and peripheral drivers.
- pySerial documentation. pySerial Documentation. Current version. The standard library for talking to a serial port from Python.