June 30, 2026

3m 12s

OS Abstraction and HALs: Same Mistake, Different Layer

In one system I worked on, the team had a clear goal of making the software portable across platforms:

  • Linux on multiple SoCs,
  • FreeRTOS on STM32,
  • Bare metal implementations,
  • and more over time.

Different customers wanted different targets.

So the team did what teams often do. They built a comprehensive OS abstraction layer. Threading, memory management, synchronization, timing; everything was wrapped behind a clean interface.

It was a great idea with good intentions, but also a real investment in time and complexity. 

The team spent significant effort building and maintaining the abstraction layer, and every time a new platform or OS was introduced, it had to be expanded further. In practice, it made the system harder to understand, harder to debug, and harder to port.

That also meant additional testing and regression effort to ensure the abstraction continued to behave correctly across platforms.

The abstraction layer accumulated layers of complexity, almost like a Coral Reef.

That experience led me to a simple conclusion: The OS is the wrong thing to abstract.

What Gets Hidden

Threading and scheduling can be treated as implementation details at the level of individual components, but their effects on system behavior cannot be hidden.

When you hide them behind an abstraction layer, you don’t remove complexity. You move it out of sight.

Now your system has:

  • hidden execution paths
  • hidden scheduling behavior
  • hidden latency characteristics

And in embedded systems, execution doesn’t just come from threads. Work is often kicked off by interrupts, introducing additional entry points with their own timing constraints.

If your architecture doesn’t make these paths explicit, the system becomes harder to reason about.

Where Systems Actually Fail

The same system also had a data pipeline. Data arrived in bursts from external hardware, was decoded, and pushed through processing stages.

A natural fit for an event-driven design!

But the real behavior of the system wasn’t defined by events. It was defined by:

  • bursty arrival patterns
  • processing latency
  • contention between stages
  • how work accumulated under load

The problems didn’t show up in threads or mutexes. They showed up in:

  • how bursts were handled
  • where backpressure occurred
  • how the system behaved under sustained load

By abstracting execution, the system made these behaviors implicit and harder to analyze.

The Same Mistake, One Layer Down

This same pattern shows up in hardware abstraction layers (HALs).

Many HALs focus on normalizing how you talk to hardware:

  • set or clear a GPIO 
  • start a peripheral 
  • transfer bytes over SPI 

At that level, the goal is consistency. Different platforms expose different registers and APIs, so the HAL provides a uniform interface.

But that’s not where systems fail.

The application doesn’t care how to set or reset a pin, and in many cases, it doesn’t even care that a pin exists.

It cares about what the system is doing:

  • enabling a device 
  • sampling a sensor 
  • controlling an actuator 

A HAL that abstracts register access still leaves the real behavior implicit.

It can make drivers look clean, but it doesn’t make the system easier to reason about. It often pushes complexity up into the application, where it becomes noise.

Two Kinds of Abstraction: Interface and Behavior-Oriented

It helps to separate two very different goals.

Interface abstraction focuses on mechanisms:

  • “How do I talk to this device?”
  • “What API do I call?”

Behavior-oriented abstraction focuses on meaning:

  • “What is the system doing?”
  • “How does data flow through it?”
  • “Where does work accumulate under load?”
  • “What role does this device play in the system?”

Many OS abstraction layers and HALs stop at the first level, but the real complexity lives in the second.

What to Make Explicit Instead

In my own work, I’ve moved toward making system behavior visible:

  • explicit queues between stages 
  • explicit ownership of data 
  • explicit thread or task assignment 
  • bounded flow and backpressure 
  • explicit representation of what the system is doing (sampling, controlling, reacting) 
  • clear mapping between devices and their role in the system 

This shifts the focus from mechanisms to behavior.

Porting becomes more mechanical. You map threads to tasks, queues to RTOS primitives, interrupts to entry points, and device interactions to platform-specific implementations. You’re not rediscovering hidden execution semantics—you’re preserving an explicit design.

The Underlying Pattern

OS abstraction hides execution, while HALs hide plumbing. Neither addresses the level where systems actually break.

Abstraction isn’t the problem. Abstracting below the level of system behavior is.

If you want systems that are understandable, debuggable, and portable, the goal isn’t to hide the OS or the hardware.

The goal is to make execution and data flow explicit—and keep them that way.