24. Capstone — Control LED·Timer via UART Commands
The final capstone. We weave together the pieces — USART (19)·interrupts (18)·ring buffer (22)·SysTick (16)·GPIO (14)·command parsing/modularity (22·23) — into one small firmware. Type on/off/blink/help in a PC terminal and the board parses it to control the LED and timer. The design key is role separation — the interrupt only receives, main only processes, the parser only parses. Each module is independently testable, and the command parser is verified on PC.
What you'll learn
- 1Integrate several peripherals·modules into one firmware
- 2Separate the roles of interrupt (receive) and main (process) (producer/consumer)
- 3Gather UART input with the ring buffer and assemble lines
- 4Parse commands and connect them to LED/timer actions
- 5Split the parser into a module and unit-test it on PC
Introduction
From lesson 1's Hello, Embedded C! through 8051·STM32 registers, interrupts·comms·timers, and debounce·ring buffer·HAL patterns — this caps the journey. After it, you can write firmware that is "direct-register yet cleanly structured".
Key concepts
1) Overall architecture and modules
[PC terminal] --UART--> RX interrupt --push--> [ring buffer] --pop--> main loop
│
assemble line → command_parse() → handle_command()
│
LED(PC13) / blink mode / UART response
SysTick(1ms) ──(toggle every 500ms if blink mode)──> LEDPure modules (ringbuffer·command) are chip-independent and test on PC directly. Only main.c handles hardware setup·integration.
2) Producer/consumer split and command protocol
| Command | Action | Response |
|---|---|---|
| on | LED on, blink off | LED ON |
| off | LED off, blink off | LED OFF |
| blink | auto-blink every 500ms | BLINK mode |
| help | command list | cmds: ... |
The RX interrupt (producer) only rb_push (short); main (consumer) rb_pop and confirms a line at \n/\r to parse·execute. blink_mode·g_ms are ISR·main shared, so volatile.
Core example
/* command.c — pure parser (hardware-independent, PC-testable) */
command_t command_parse(const char *line) {
if (line[0] == '\0') return CMD_NONE;
if (streq(line, "on")) return CMD_ON;
if (streq(line, "off")) return CMD_OFF;
if (streq(line, "blink")) return CMD_BLINK;
if (streq(line, "help")) return CMD_HELP;
return CMD_UNKNOWN;
}/* main.c — integration (excerpt) */
while (1) {
if (rb_pop(&rx_rb, &c)) {
if (c == '\n' || c == '\r') {
if (idx > 0u) { line[idx]='\0'; handle_command(command_parse(line)); idx=0u; }
} else if (idx < LINE_MAX-1u) line[idx++] = (char)c;
}
}Parser check: "on"→ON, "blink"→BLINK, "xyz"→UNKNOWN, ""→NONE. Build: gcc pc_test.c command.c -o pc_test. Type on/off/blink/help in the simulator's UART #1 to verify LED·blink·responses.
Common mistakes
Q. Commands get no response.
A. A line is confirmed only at a line end (\n/\r). Check the terminal sends a newline on Enter, and that RXNEIE and NVIC_EnableIRQ(USART1_IRQn) are on.
Q. Fast typing drops characters.
A. Check main calls rb_pop often enough and the ring buffer is big enough. Heavy work in the ISR misses the next byte (ISR push only).
Q. Testing the parser on the board is tedious.
A. That's why the parser is a pure module. Build command.c with pc_test.c via gcc to verify logic first, then flash the board.
Summary
- The capstone weaves USART·interrupt·ring buffer·SysTick·GPIO·parser into one
- The interrupt only receives (push); main does pop·assemble·parse·execute (split)
- Pure modules (ringbuffer/command) are unit-tested on PC, then flashed
- A command protocol lets you control·extend the firmware externally
- Direct register control and clean structure coexist — 24 lessons complete!
Exercises
- Add "fast"/"slow" commands that change the blink rate in the parser and handler
- Extend parsing to commands with arguments (e.g. "pwm 50")
- Sketch how to attach a next topic (I2C/SPI sensor·RTOS·DMA) to this capstone
All lecture materials and example code (with homework and answers) are openly available on GitHub.
View on GitHub ↗