I’ve spent the last few years writing abstraction layers between hardware and application software. Part of my job is knowing what’s out there, what works, and what breaks when you need it most. So I went through five open-source hardware abstraction layers and evaluated them on the criteria that matter for production firmware.

Here’s what I found.

The Evaluation Criteria

I scored each HAL on five dimensions:

  1. Portability — How many MCU families does it support? How hard is it to add a new one?
  2. Performance — What’s the overhead vs direct register access?
  3. API Design — Is the API intuitive? Consistent? Does it leak hardware details?
  4. Documentation — Can a new developer figure it out without reading the source?
  5. Production Readiness — Is it used in real products? Are there gotchas?

Scale: 1 (poor) to 5 (excellent).


1. Zephyr RTOS Device Driver Model

What it is: Zephyr isn’t just an RTOS — it’s a full operating system with a driver model based on Linux’s device tree concept. Hardware is described in .dts files, and drivers bind to device tree nodes.

Example — GPIO:

#include <zephyr/drivers/gpio.h>

#define LED_NODE DT_ALIAS(led0)
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED_NODE, gpios);

void main(void) {
    gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
    while (1) {
        gpio_pin_toggle_dt(&led);
        k_msleep(500);
    }
}

What they got right:

  • Device tree separates hardware description from driver code. Your app code literally doesn’t know which MCU it’s running on.
  • Consistent API across all peripherals. GPIO, SPI, I2C, UART all follow the same pattern.
  • Massive MCU support — STM32, nRF, ESP32, NXP, TI, Renesas, Microchip, RISC-V families.
  • Active community, regular releases, Linux Foundation backing.

What they got wrong:

  • Learning curve is steep. Device tree overlay syntax is intimidating for embedded engineers coming from bare-metal.
  • Build system (west + CMake + Kconfig) is powerful but complex. First build can take 30 minutes of setup.
  • Overhead isn’t zero. The driver model uses function pointer dispatch, which adds a few cycles per call. Fine for most apps, not ideal for bit-banging at MHz speeds.
  • Flash footprint starts around 40-60KB minimum. Not viable for small Cortex-M0 parts with 32KB flash.

Scores:

Portability Performance API Design Documentation Production Ready
5 3 4 4 5

Verdict: Best choice if your project can afford the footprint and learning curve. The portability is unmatched.


2. CMSIS (Cortex Microcontroller Software Interface Standard)

What it is: ARM’s official standard for Cortex-M software interfaces. Defines core access functions, DSP intrinsics, RTOS API, and driver APIs.

Example — GPIO (CMSIS-Driver):

#include "Driver_GPIO.h"

extern ARM_DRIVER_GPIO Driver_GPIO0;

void led_init(void) {
    Driver_GPIO0.Setup(5, NULL);  // Pin 5
    Driver_GPIO0.SetDirection(5, ARM_GPIO_OUTPUT);
}

void led_toggle(void) {
    static int state = 0;
    Driver_GPIO0.SetOutput(5, state ^= 1);
}

What they got right:

  • It’s a standard. If every vendor implemented it, portability would be solved.
  • CMSIS-Core (register access, NVIC, SysTick) is universally used and excellent.
  • CMSIS-DSP is genuinely useful and well-optimized.

What they got wrong:

  • Almost nobody implements CMSIS-Driver in practice. Vendors ship their own HALs (STM32 HAL, nRF drivers) instead.
  • The driver API is over-abstracted. It tries to be generic enough for every peripheral on every chip, resulting in an API that’s awkward for all of them.
  • Documentation exists but is dense and spec-like, not tutorial-like.
  • CMSIS-RTOS is a wrapper API, not a real RTOS. It adds overhead without adding capability.

Scores:

Portability Performance API Design Documentation Production Ready
2 4 2 3 3

Verdict: CMSIS-Core is essential. CMSIS-Driver is a good idea that the industry ignored. Don’t build on it unless you want to maintain the vendor implementations yourself.


3. Arduino HAL

What it is: The most successful embedded abstraction layer in terms of adoption. digitalWrite(), analogRead(), Serial.begin() — you know it.

Example:

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    Serial.begin(115200);
}

void loop() {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(500);
    digitalWrite(LED_BUILTIN, LOW);
    delay(500);
    Serial.println("blink");
}

What they got right:

  • Simplicity. A beginner can blink an LED in 5 minutes. That’s an achievement.
  • Massive ecosystem. Libraries for every sensor, display, and communication module.
  • Runs on Arduino AVR, ESP32 (arduino-esp32), STM32 (STM32duino), nRF (Adafruit), RP2040. Genuine portability.

What they got wrong:

  • No DMA, no interrupts (without platform-specific extensions), no low-power modes. The API hides too much.
  • digitalWrite() is slow. On AVR, it’s ~50 clock cycles vs 2 for direct port manipulation. On ARM it’s better, but still has overhead from pin lookup.
  • Global state everywhere. One Serial object, one SPI object. Multi-instance is hacked in.
  • C++ requirement. Many embedded teams work in C. Arduino forces C++ with its class-based API.
  • Error handling is nonexistent. Functions either work or silently fail. No error codes, no status flags.

Scores:

Portability Performance API Design Documentation Production Ready
4 2 3 5 2

Verdict: Great for prototypes and education. Not for production firmware that needs DMA, interrupts, or low power. The API design philosophy (hide everything) is the opposite of what embedded needs (control everything).


4. libopencm3

What it is: An open-source, community-maintained firmware library for ARM Cortex-M microcontrollers. Lower level than STM32 HAL, higher level than direct register access.

Example — GPIO:

#include <libopencm3/stm32/rcc.h>
#include <libopencm3/stm32/gpio.h>

void led_init(void) {
    rcc_periph_clock_enable(RCC_GPIOA);
    gpio_mode_setup(GPIOA, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO5);
}

void led_toggle(void) {
    gpio_toggle(GPIOA, GPIO5);
}

What they got right:

  • Clean, C-only API. No opaque handles, no callback registration, no HAL_StatusTypeDef. Just function calls that map closely to hardware operations.
  • Thin. The overhead is minimal — most functions compile to a handful of register writes.
  • Supports STM32, EFM32, LPC, SAM, and some others. Not as wide as Zephyr but respectable.
  • Good linker scripts and startup code included. Build system is straightforward.

What they got wrong:

  • Community-maintained means inconsistent coverage. Some MCU families (STM32F1/F4) are well-supported. Others have gaps.
  • Not enough abstraction for easy porting. The API uses vendor-specific register names (GPIOA, RCC_GPIOA). Porting from STM32 to SAM requires changing every call.
  • No RTOS integration. It’s purely a peripheral library.
  • Documentation is sparse. You’ll read source code.

Scores:

Portability Performance API Design Documentation Production Ready
2 5 4 2 3

Verdict: Excellent for single-MCU projects where you want clean register access without the bloat of vendor HALs. Not a portability solution.


5. Tock OS

What it is: A secure embedded operating system written in Rust. Uses Rust’s type system and ownership model to enforce isolation between the kernel, drivers, and applications.

Example — GPIO (Tock capsule):

// Kernel-side driver (capsule)
impl<'a, G: hil::gpio::Pin> hil::gpio::Client for GpioDriver<'a, G> {
    fn fired(&self) {
        self.callback.map(|callback| {
            callback.schedule(0, 0, 0);
        });
    }
}

What they got right:

  • Memory safety guaranteed by the compiler. Buffer overflows, use-after-free, data races — caught at compile time.
  • Strong isolation model. Untrusted applications can’t crash the kernel or other apps.
  • The hardware interface layer (HIL) traits are well-designed. Clean separation between hardware-specific and hardware-independent code.
  • Academic rigor — published in SOSP, backed by serious research.

What they got wrong:

  • Rust on embedded is still maturing. Toolchain support, debugging tools, and community knowledge are improving but behind C.
  • Very limited MCU support compared to C-based alternatives. Primarily Nordic nRF52, some STM32, some RISC-V.
  • Overhead from the capsule/process model. More suitable for application processors (Cortex-M4+) than constrained MCUs.
  • Small community. If you hit a problem, there are fewer people who can help.
  • Hard to integrate with existing C codebases. If you have 50K lines of C firmware, Tock isn’t a migration target.

Scores:

Portability Performance API Design Documentation Production Ready
2 3 5 3 2

Verdict: The future of secure embedded systems, but not the present for most teams. If you’re starting a greenfield project on a supported MCU and your team knows Rust, it’s worth evaluating.


Summary Scorecard

HAL Portability Performance API Design Docs Production Total
Zephyr 5 3 4 4 5 21
CMSIS 2 4 2 3 3 14
Arduino 4 2 3 5 2 16
libopencm3 2 5 4 2 3 16
Tock 2 3 5 3 2 15

What I’d Actually Recommend

For a new production project that needs portability: Zephyr. The learning curve is real, but the portability payoff is worth it. The ecosystem is growing fast.

For a bare-metal project on a single MCU: libopencm3 (if your MCU is supported) or the vendor HAL with a thin abstraction layer on top.

For a prototype or proof-of-concept: Arduino. Get it working, then rewrite for production.

For security-critical applications on supported hardware: Look at Tock. It’s early but the safety guarantees are compelling.

For everyone: Don’t use CMSIS-Driver. Use CMSIS-Core (it’s great), but build your own driver abstraction or use Zephyr’s.

And regardless of which HAL you choose — keep vendor types out of your application code. That single rule does more for portability than any framework.


Pranav Jain builds the middleware between hardware and application software. Find him on GitHub.