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

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.

HALabstractionportabilitymodularitylayeringAVR
Duration
~2 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.

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

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
Example code / lecture materials

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

View on GitHub ↗