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.
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
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 GPIODependency 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
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
| Action | AVR register | STM32 equivalent |
|---|---|---|
| as output | DDRB |= (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
/* 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);
}/* 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
- Add hal_button_read() to hal.h and implement it three ways (PC fake input·STM32 IDR·AVR PINx)
- Audit that app.c has no register code at all
- 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
[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 ↗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