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");
    }
}

Buildable Core Module

The following host-testable core avoids direct register access. Target firmware supplies gpio_led_write() and uart_write_string(); host tests can replace them with fakes.

#include <stdbool.h>
#include <stdint.h>
#include <string.h>

#define UART_RX_SIZE 64u
#define LINE_SIZE 32u

typedef struct {
    uint8_t data[UART_RX_SIZE];
    volatile uint8_t head;
    volatile uint8_t tail;
    volatile bool overflow;
} ring_buffer_t;

static ring_buffer_t uart_rx;

static bool elapsed(uint32_t now, uint32_t start, uint32_t interval)
{
    return (uint32_t)(now - start) >= interval;
}

bool rb_push_isr(ring_buffer_t *rb, uint8_t byte)
{
    uint8_t next = (uint8_t)((rb->head + 1u) % UART_RX_SIZE);

    if (next == rb->tail) {
        rb->overflow = true;
        return false;
    }

    rb->data[rb->head] = byte;
    rb->head = next;
    return true;
}

bool rb_pop(ring_buffer_t *rb, uint8_t *out)
{
    if (rb->tail == rb->head) {
        return false;
    }

    *out = rb->data[rb->tail];
    rb->tail = (uint8_t)((rb->tail + 1u) % UART_RX_SIZE);
    return true;
}

void app_service_uart(led_control_t *led)
{
    static char line[LINE_SIZE];
    static uint8_t used;
    uint8_t byte;

    while (rb_pop(&uart_rx, &byte)) {
        if ((byte == '\r') || (byte == '\n')) {
            if (used > 0u) {
                line[used] = '\0';
                process_command(line, led);
                used = 0u;
            }
        } else if (used < (LINE_SIZE - 1u)) {
            line[used++] = (char)byte;
        } else {
            used = 0u;
            uart_write_string("ERR LINE\r\n");
        }
    }
}

This module has clear ownership: the UART ISR writes head, the main loop writes tail, and overflow is reported instead of writing past the buffer.

Main Loop Integration

static volatile uint32_t system_ms;

void SysTick_Handler(void)
{
    system_ms++;
}

void UART_IRQHandler(void)
{
    uint8_t byte = UART_RX_REG;
    (void)rb_push_isr(&uart_rx, byte);
    UART_CLEAR_RX_INTERRUPT();
}

int main(void)
{
    led_control_t led = {
        .mode = LED_MODE_BLINK,
        .output_level = false,
        .period_ms = 500u,
        .last_toggle_ms = 0u,
    };

    board_init();

    for (;;) {
        uint32_t now = millis();
        led_service(&led, now);
        app_service_uart(&led);
        watchdog_service();
        enter_sleep_until_interrupt();
    }
}

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

Mind Map

mindmap root((GPIO Timer UART Capstone)) Core goal Non blocking LED Timer tick UART RX ISR Command parser Host tests Architecture Timer owns ms ISR pushes bytes Main parses lines GPIO driver writes LED UART TX reports status Commands LED ON LED OFF BLINK period STATUS ERR replies Design rules ISR short One writer per index Fixed buffers Period 50 to 10000 ms No blocking parser Calculations elapsed equals now minus start Toggle every period ms Buffer full when next equals tail Line limit 31 chars Verification Parser host tests Ring buffer overflow test Scope blink period UART command test Reset default state Common mistakes Long ISR Missing volatile Bad newline handling Wrong tick units Memory corruption on overflow