CERA2

Datasheetcera2 · embedded

Hardware Abstraction Layer (HAL): What It Is, When to Use It

A hardware abstraction layer (HAL) is software that sits between physical hardware and the code that needs to use it. Instead of making an application learn a particular microcontroller’s registers and startup sequence, the HAL offers a stable interface and translates each request into hardware-specific work. That boundary can make a board change a contained engineering task rather than a rewrite.

What a hardware abstraction layer actually abstracts

An abstraction layer hides the working details of a subsystem and presents a simpler interface to the layer above it. Hardware abstraction applies that idea to devices: callers ask for a capability, not for the electrical or register-level sequence behind it. A Hardware Abstraction Layer provides one uniform programming interface across different hardware devices.

Consider a temperature-reading service. Its application-level request might be “read data.” A HAL implementation maps that request to the correct driver call for the selected board. The application does not need to know whether the data path uses a particular peripheral configuration or a vendor-specific register layout. That is useful only if the interface represents a real capability rather than merely renaming every underlying hardware detail.

How a HAL turns one request into device actions

In a typical operating-system stack, the order is Application → OS Services → HAL → Device Drivers → Hardware. The application calls the HAL interface, and device drivers communicate with the hardware directly. In between, the HAL normalizes access so higher-level code has one consistent place to ask for functionality.

Initialization shows why the boundary helps. Before a peripheral works reliably, a target may need power stabilization, clock configuration, bus setup, calibration, or device discovery. Instead of scattering those steps through application code, a HAL turns initialization into one defined routine. Middleware such as an RTOS, USB, Ethernet, or a file system can then use a common interface above the low-level drivers.

#include <stdint.h>

typedef enum { DIO_LOW = 0, DIO_HIGH = 1 } dioState_t;
typedef uint8_t dioPort_t;
typedef uint8_t dioPin_t;
typedef struct DioConfig DioConfig_t;   /* board-specific pin table */

/* dio.h: the interface the application sees */
typedef struct {
    void       (*init)(DioConfig_t const * const Config);
    void       (*write)(dioPort_t port, dioPin_t pin, dioState_t state);
    dioState_t (*read)(dioPort_t port, dioPin_t pin);
} dio_base;

/* board_a_dio.c: one implementation per target */
static void board_a_init(DioConfig_t const * const Config)
{
    (void)Config;   /* enable clocks, set pin modes */
}

static void board_a_write(dioPort_t port, dioPin_t pin, dioState_t state)
{
    (void)port; (void)pin; (void)state;   /* write the output register */
}

static dioState_t board_a_read(dioPort_t port, dioPin_t pin)
{
    (void)port; (void)pin;   /* read the input register */
    return DIO_LOW;
}

const dio_base dio_board_a = { board_a_init, board_a_write, board_a_read };

/* application code depends only on the interface */
void status_led_on(dio_base const *dio)
{
    dio->write(0u, 5u, DIO_HIGH);
}

In C, a struct of function pointers like this is a common HAL pattern. It gives interface-based polymorphism without C++, much like a virtual function table. The struct is allocated statically at link time, const-qualified so it can live in flash, and passed by pointer. That means no heap use and no runtime cost beyond the indirect call.

HAL, device drivers, and APIs solve different problems

The terms overlap in conversation, but they are not interchangeable. A device driver talks to the hardware directly. A HAL wraps drivers or defines their common interface. An API is the set of functions that application code calls. In some designs the HAL is the driver interface itself; in others it is a wrapper over existing drivers.

Layer Primary responsibility What it shields
Application or service Product behavior Board-level details
HAL Common hardware-facing interface Hardware-specific differences
Device driver Direct control of a device Register operations from upper layers
Hardware Physical operation Nothing above it

The dependency inversion principle applies here: high-level modules should not depend on low-level modules; both should depend on abstractions. No layer should reach upward or skip a layer. Shortcuts may look efficient at first, but they create dependencies that are hard to maintain when a board or driver changes.

HAL examples from microcontrollers to operating systems

Platform How its HAL works
STM32 HAL and LL ST’s HAL is the portable, transaction-oriented layer; LL drivers are thinner and closer to the registers.
Windows The NT kernel uses hal.dll between hardware and executive services; HAL routines carry the prefix “Hal”, such as HalGetInterruptVector. Since Vista, only ACPI-based HALs are supported.
Android Project Treble in Android 8.0 standardized the HAL. Since Android 13, AIDL is the standard way to define HALs; HIDL is deprecated.
Zephyr RTOS Hardware is described in devicetree (.dts) files, and drivers expose a consistent API across peripherals; supported targets include STM32, nRF, ESP32, NXP, and RISC-V.

What portability and host testing look like in embedded systems

In embedded systems, the strongest reason to use a HAL is decoupling. Application logic can be written without relying on one silicon family’s details. When a product line moves to different hardware, the implementation beneath the interface changes while the higher-level logic is reused. A defined HAL also makes an operating system easier to port across platforms.

For testing, that separation creates a useful boundary. With a mock implementation of the HAL interface on a host PC, application logic runs without target hardware. This does not prove that the driver controls a real board correctly, but it lets a team test decisions and error handling before hardware is attached. In the end, the result still depends on the quality of the drivers below the HAL.

When a HAL earns its cost—and when it does not

A HAL is valuable when the same product software must support several boards, when hardware changes are likely, or when host-based tests matter. Begin with the basic peripherals the product actually uses, then extend the interface only when a real requirement appears.

The cost is real. Extra abstraction adds complexity and can add performance overhead. For the C function-pointer technique, budget roughly one function call plus the memory for the pointers. Vendor HALs are often generic, use extra memory and CPU cycles, and can hide bugs if their source is not available. On AVR, Arduino’s digitalWrite() takes about 50 clock cycles, compared with 2 cycles for direct port manipulation; pin lookup and abstraction account for the 25× difference. In a high-frequency control loop or timing-critical interrupt, that trade-off can be unacceptable.

STM32 offers a practical hybrid. Its HAL is transaction-oriented, with calls such as HAL_ADC_Start_DMA. The LL drivers sit closer to the registers and expose smaller operations such as LL_ADC_Enable(ADC1); their unitary functions are static inline and compile to one or a few register accesses. Use HAL for system configuration, communications, and middleware, and reserve LL for high-frequency loops and timing-critical interrupts with clear peripheral ownership.

How to create a HAL for custom hardware without overbuilding it

  1. Identify capabilities, not chips. Define what upper layers need from each peripheral, such as initialize, read, or write.
  2. Keep each interface small. A good HAL interface exposes fewer than a dozen functions per peripheral, remains human-readable, and can be extended deliberately.
  3. Put board-specific work below the boundary. Connect each function to the appropriate driver and initialization routine for the target.
  4. Create a mock. Substitute it for the target-specific implementation on a host PC to test application logic without a board.
  5. Test the real implementation separately. Verify the driver and startup sequence on the target, because a mock cannot validate physical hardware behavior.

A C function-pointer struct is one implementation option, not a reason to make every hardware feature generic. On microcontrollers, the HAL often sits lower than in a desktop OS: Application → Service → Driver → HAL → CMSIS/register layer, with drivers for external chips built on top of the HAL. Whatever the order, dependencies point in one direction only. The CMSIS-Driver structs, such as ARM_DRIVER_USART, are a proven model for this kind of interface.

Common HAL myths that lead to fragile designs

A HAL makes all boards behave the same

No interface changes physical limits or peripheral behavior. A HAL can normalize access, but the implementation must still respect the selected hardware. A leaky abstraction exposes details callers begin to depend on; an over-abstracted one supports every mode and becomes nearly as complex as the hardware it was meant to hide.

A HAL is always too slow for real-time work

Not necessarily. The overhead belongs to a particular design and call path, not to the name HAL. Measure the relevant path, then decide where direct register access is justified.

Using a HAL means embedded C is obsolete

Embedded C is still used. A decade ago, C appeared in more than 80% of embedded projects; today the figure is 60–70%, while C++ has grown to 20–25%. A HAL interface can be written in C with function pointers or implemented differently in C++; the language choice does not remove the need for a clear boundary.

Frequently Asked Questions

What is an abstraction layer?

It is a simplified interface that hides the working details of a subsystem from the layers above it. In hardware work, it prevents application code from having to understand every device-specific detail.

What does HAL stand for?

HAL stands for Hardware Abstraction Layer: software between physical hardware and the software that runs on it.

How does a hardware abstraction layer improve portability?

It separates application logic from silicon-specific code. When different hardware is introduced, developers can change the lower implementation while preserving code written to the HAL interface.

How does a HAL relate to device drivers?

Drivers talk directly to hardware. A HAL normalizes driver access for higher-level code, and may wrap existing drivers or define the interface drivers implement.

Is embedded C still used?

Yes. Industry estimates place C in 60–70% of embedded projects, even as C++ use has grown to 20–25%.

What components and files make up a HAL?

A working HAL consists of interface definitions (usually header files with the API or function-pointer structs), one implementation per target that calls the device drivers, and initialization routines that bring the hardware into a defined state. A mock implementation for host tests is a useful extra.

Index

CompactPCI: PCI in a Eurocard

By the mid-nineties the PC industry had a fast, cheap, well-supported bus, and the industrial world had mechanics that could survive a factory floor. CompactPCI, published in 1995,

VMEbus: the rack that outlived its industry

VMEbus was specified in 1981 around the Motorola 68000 and the Eurocard mechanics that European instrument makers already had on the shelf. Forty years later there are still

PC/104: stacking instead of a backplane

PC/104 solved a mechanical problem before it solved an electrical one. An industrial controller that has to survive vibration cannot rely on a card edge sitting in a