Loading header...

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 ms system 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 ON
    • LED OFF
    • BLINK 500
    • STATUS

Architecture

flowchart LR TICK["Timer ISR\nincrements ms"] --> APP["main loop services"] UARTISR["UART RX ISR\npush byte"] --> RB["ring buffer"] RB --> PARSER["command parser"] PARSER --> LED["GPIO LED driver"] APP --> LED PARSER --> TX["UART transmit"]

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 ON forces the LED on.
  • LED OFF forces the LED off.
  • BLINK 500 toggles the LED every 500 ms.
  • STATUS returns a short status line.
  • UART receive overflow sets a flag and does not corrupt memory.

Verification Steps

  1. Host-test the ring buffer and parser.
  2. Host-test led_service by passing simulated now values.
  3. Compile target firmware with warnings as errors.
  4. Use a logic analyzer or oscilloscope to confirm blink period.
  5. Send UART commands from a terminal and verify responses.
  6. Flood RX input and confirm overflow is reported, not memory corruption.
  7. 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, and count during 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.

Further Reading