← Back to Embedded C series
🔌
Applications
Applied · Prereq: lesson 22 · Applied · Prereq: lesson 23

23–24. HAL Abstraction · Module Separation · Portability · Capstone — Control LED·Timer via UART Commands

If register code (GPIOC->BRR=...) is scattered through app logic, switching chips means rewriting everywhere. A HAL (Hardware Abstraction Layer) splits "what to do" (turn on the LED) from "how" (write a specific register). We run the same app.c against three HAL implementations — PC (printf), STM32 (registers), and Arduino Uno (AVR ATmega328P) — and see the app code stay unchanged even as the architecture switches from ARM to AVR. 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.

HALabstractionportabilitymodularitylayeringAVRcapstoneintegrationcommand parserring bufferproducer-consumerfirmware
Duration
⏱ ~2 hours + ~2–3 hours
Level
📊 Advanced
Prerequisite
🎯 Lesson 22
OUTCOME
Split firmware into app/HAL/hardware layers with interface(header)/implementation(source) separation so swapping only the HAL implementation runs the same app on PC·STM32·AVR. / Integrate USART·interrupt·ring buffer·SysTick·GPIO·parser into one firmware, completing a cleanly structured command-control firmware via producer/consumer separation and pure-module unit tests.

What you'll learn

  • 1Explain why to split firmware into app/HAL/hardware layers
  • 2Separate interface (header) from implementation (source)
  • 3Port the same app across multiple HAL implementations
  • 4Keep the dependency direction (app→HAL, never reverse)
  • 5Unit-test app logic with a fake PC HAL
  • 6Integrate several peripherals·modules into one firmware
  • 7Separate the roles of interrupt (receive) and main (process) (producer/consumer)
  • 8Gather UART input with the ring buffer and assemble lines
  • 9Parse commands and connect them to LED/timer actions
  • 10Split the parser into a module and unit-test it on PC

23. HAL Abstraction · Module Separation · Portability

If register code (GPIOC->BRR=...) is scattered through app logic, switching chips means rewriting everywhere. A HAL (Hardware Abstraction Layer) splits "what to do" (turn on the LED) from "how" (write a specific register). We run the same app.c against three HAL implementations — PC (printf), STM32 (registers), and Arduino Uno (AVR ATmega328P) — and see the app code stay unchanged even as the architecture switches from ARM to AVR.

Introduction

When you switch from STM32 to 8051, or want to test on PC, register code embedded in the app forces you to fix everything. A HAL solves this by splitting the app from the implementation.

Key concepts

1) Layering and dependency direction

c
app (app.c)         "control LED per pattern" — knows no hardware
  ↓ (calls)
HAL interface (hal.h)  contract like "hal_led_set(on)"
  ↓
HAL implementation  hal_pc.c / hal_stm32.c / hal_arduino.c
  ↓
hardware            PC stdout / STM32 GPIO / AVR GPIO
⚠️

Dependency direction: app → HAL allowed, HAL → app forbidden. If the lower layer knows the upper, separation breaks and you can't reuse it.

2) Portability — swap only the implementation

c
PC test:  pc_test.c + app.c + hal_pc.c       (printf)
STM32:    main.c    + app.c + hal_stm32.c    (ARM, PC13)
Uno:      main      + app.c + hal_arduino.c  (AVR, PB5/D13)
/* app.c is byte-for-byte identical in all three */

3) AVR (Uno) register mapping

ActionAVR registerSTM32 equivalent
as outputDDRB |= (1<<5)GPIOC->CRH mode
High (on)PORTB |= (1<<5)GPIOC->BSRR/BRR
Low (off)PORTB &= ~(1<<5)GPIOC->BSRR/BRR

DDRx(direction)·PORTx(output)·PINx(input) are AVR GPIO's three registers. Using them directly instead of digitalWrite keeps the direct-register philosophy on AVR too. (The Uno is AVR, so it can't build in Keil — use avr-gcc/Arduino IDE.)

Core example

c
/* hal.h — contract (the app depends only on this) */
void hal_led_init(void);
void hal_led_set(uint8_t on);   /* 1=on, 0=off */

/* app.c — hardware-independent. Not one line of register code */
void app_blink_pattern(const uint8_t *p, uint16_t len) {
    uint16_t i; hal_led_init();
    for (i = 0; i < len; i++) hal_led_set(p[i] ? 1u : 0u);
}
c
/* the same hal.h implemented three ways */
void hal_led_set(uint8_t on){ printf("LED %s\n", on?"ON":"OFF"); }            /* PC */
void hal_led_set(uint8_t on){ if(on)GPIOC->BRR=(1u<<13);else GPIOC->BSRR=(1u<<13);} /* STM32 */
void hal_led_set(uint8_t on){ if(on)PORTB|=(1u<<5);else PORTB&=~(1u<<5);}      /* Uno AVR */

Verify: gcc pc_test.c app.c hal_pc.c -o pc_test → prints LED ON/OFF per pattern. In Keil, swap hal_pc.c for hal_stm32.c and the same app.c drives PC13.

Common mistakes

Q. I made a HAL but the app still has register code.

A. All register access must move into the HAL implementation. One line of GPIOC->... in app.c breaks portability.

Q. Linking several implementations together errors out.

A. The same functions get defined multiple times. Link exactly one (hal_pc.c for PC, hal_stm32.c for STM32, hal_arduino.c for Uno).

Q. Too much abstraction made it slow and complex.

A. Abstraction isn't free. Put a HAL only at boundaries that change often or need porting·testing, and don't over-layer (balance).

Summary

  • Splitting firmware into app/HAL/hardware boosts portability and testability
  • Header is the contract (interface), source the implementation — app depends only on the header
  • Swap only the HAL implementation when changing chips; keep the app
  • The same app.c runs on PC·STM32(ARM)·Uno(AVR)
  • Dependency direction is one-way: app→HAL (never reverse)

Exercises

  1. Add hal_button_read() to hal.h and implement it three ways (PC fake input·STM32 IDR·AVR PINx)
  2. Audit that app.c has no register code at all
  3. Explain which file to change to add a second LED

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.

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

c
[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)──> LED

Pure 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

CommandActionResponse
onLED on, blink offLED ON
offLED off, blink offLED OFF
blinkauto-blink every 500msBLINK mode
helpcommand listcmds: ...
ℹ️

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

c
/* 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;
}
c
/* 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

  1. Add "fast"/"slow" commands that change the blink rate in the parser and handler
  2. Extend parsing to commands with arguments (e.g. "pwm 50")
  3. Sketch how to attach a next topic (I2C/SPI sensor·RTOS·DMA) to this capstone
Example code / lecture materials

All lecture materials and example code (with homework and answers) are openly available on GitHub.

View on GitHub ↗

Comments 0

No comments yet. Be the first.

Only members can comment.

Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy