Capstone: GPIO, Timer, Interrupt, and UART
This capstone combines the core Embedded C skills from this section. You will build a small firmware design that blinks an LED without blocking, receives UART bytes into a ring buffer from an interrupt, and accepts text commands to control behavior.
Learning Objectives
By the end of this capstone, you should be able to:
- combine GPIO, timer, interrupt, and UART driver patterns;
- keep interrupt work short;
- use a ring buffer between ISR and main loop;
- test most application logic without hardware;
- document expected behavior and debug evidence.
Prerequisites
- GPIO register driver exercise.
- Ring buffer exercise.
- Interrupt-shared data and critical sections.
- Timers and non-blocking state machines.
- Basic UART transmit/receive concepts.
Concrete Task
Build firmware with these features:
- a
1 mssystem tick; - non-blocking LED blink with configurable period;
- UART RX interrupt pushes bytes into a ring buffer;
- main loop parses commands ending in newline;
- commands:
LED ONLED OFFBLINK 500STATUS
Architecture
Core Data Types
typedef enum {
LED_MODE_FORCED_OFF = 0,
LED_MODE_FORCED_ON,
LED_MODE_BLINK
} led_mode_t;
typedef struct {
led_mode_t mode;
bool output_level;
uint32_t period_ms;
uint32_t last_toggle_ms;
} led_control_t;
LED Service
static bool elapsed(uint32_t now, uint32_t start, uint32_t interval)
{
return (uint32_t)(now - start) >= interval;
}
void led_service(led_control_t *led, uint32_t now)
{
if (led->mode == LED_MODE_FORCED_ON) {
gpio_led_write(true);
led->output_level = true;
return;
}
if (led->mode == LED_MODE_FORCED_OFF) {
gpio_led_write(false);
led->output_level = false;
return;
}
if (elapsed(now, led->last_toggle_ms, led->period_ms)) {
led->output_level = !led->output_level;
gpio_led_write(led->output_level);
led->last_toggle_ms = now;
}
}
UART RX ISR
void UART_IRQHandler(void)
{
uint8_t byte = UART_RX_REG;
if (!rb_push(&uart_rx_buffer, byte)) {
uart_overflow_flag = true;
}
}
The ISR reads the byte, pushes it, records overflow if needed, and exits.
Command Parser Sketch
void process_command(char *line, led_control_t *led)
{
if (strcmp(line, "LED ON") == 0) {
led->mode = LED_MODE_FORCED_ON;
uart_write_string("OK LED ON\r\n");
} else if (strcmp(line, "LED OFF") == 0) {
led->mode = LED_MODE_FORCED_OFF;
uart_write_string("OK LED OFF\r\n");
} else if (strncmp(line, "BLINK ", 6) == 0) {
uint32_t period = parse_u32(&line[6]);
if ((period >= 50u) && (period <= 10000u)) {
led->period_ms = period;
led->mode = LED_MODE_BLINK;
uart_write_string("OK BLINK\r\n");
} else {
uart_write_string("ERR PERIOD\r\n");
}
} else if (strcmp(line, "STATUS") == 0) {
uart_write_string("OK STATUS\r\n");
} else {
uart_write_string("ERR COMMAND\r\n");
}
}
Expected Behavior
- On boot, LED blinks at the default period.
LED ONforces the LED on.LED OFFforces the LED off.BLINK 500toggles the LED every500 ms.STATUSreturns a short status line.- UART receive overflow sets a flag and does not corrupt memory.
Verification Steps
- Host-test the ring buffer and parser.
- Host-test
led_serviceby passing simulatednowvalues. - Compile target firmware with warnings as errors.
- Use a logic analyzer or oscilloscope to confirm blink period.
- Send UART commands from a terminal and verify responses.
- Flood RX input and confirm overflow is reported, not memory corruption.
- Reset the board and confirm default state is deterministic.
Common Failure Symptoms
| Symptom | Likely cause |
|---|---|
| LED stops responding during UART input | blocking parser or long ISR |
| occasional corrupted command | ring buffer overflow or missing terminator handling |
| blink period wrong | tick units or wraparound error |
| UART receives first byte only | interrupt flag not cleared |
| release build fails but debug works | missing volatile or timing assumption |
Debugging Guidance
- Toggle a spare GPIO at ISR entry and exit to measure ISR length.
- Log parser states only from the main loop, not the ISR.
- Test command parsing with plain strings before connecting UART.
- Inspect ring buffer
head,tail, andcountduring overflow tests. - Check MCU clock configuration before blaming timer code.
Extension Challenge
Add a SAVE command that stores the blink period in nonvolatile memory. On boot, validate the stored value before using it. If validation fails, fall back to the default period and report the reason in STATUS.
Concise Explained Solution
The design separates timekeeping, byte reception, parsing, and LED control. ISRs only capture events: the timer increments time and the UART ISR pushes received bytes. The main loop owns parsing and mode changes. This keeps interrupt latency short and makes the behavior testable on a host machine.
Summary
This capstone is a small but realistic embedded firmware architecture. It uses register-level drivers, volatile ISR-shared data, a ring buffer, a non-blocking timer state machine, and defensive command parsing. The same pattern scales to buttons, sensors, motor commands, and communication protocols.