Skip to main content
Clean Architecture 70 XP · 7 min

Clean Embedded Architecture: Avoiding Firmware

Software doesn't wear out, but hardware becomes obsolete — a Hardware Abstraction Layer prevents business logic from becoming firmware trapped on a specific processor.

Showing
Ad (728×90)

Why this matters

Robert Martin draws a sharp distinction between firmware and software. Firmware is code so deeply coupled to hardware that it cannot run anywhere else. Software can be ported. The disturbing trend is that developers who are not writing embedded systems are writing firmware anyway — they couple their business logic to a specific database, framework, or runtime until it can never be extracted.

The Hardware Abstraction Layer (HAL) sits between business logic and hardware-specific code. The business logic imports only the abstract HAL interface; the concrete HAL implementation handles the actual hardware calls. The OSAL (OS Abstraction Layer) does the same thing for RTOS calls.

The key test: can your business logic run on your development laptop without special hardware? If the answer is no, you have firmware. The HAL pattern breaks the dependency so your business rules can be tested with a mock HAL on any machine, and the hardware-specific adapter is swappable when the hardware platform changes.

The problem

Business logic directly calls hardware drivers. The motor control logic is welded to a specific board. Impossible to unit test without real hardware. Impossible to reuse on a different platform.

Bad

import RPi.GPIO as GPIO  # hardware-specific import
import time

GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.OUT)

class ConveyorBelt:
    def start(self, duration_seconds: float) -> None:
        GPIO.output(18, GPIO.HIGH)   # RPi-specific hardware call
        time.sleep(duration_seconds)
        GPIO.output(18, GPIO.LOW)    # RPi-specific hardware call

    def emergency_stop(self) -> None:
        GPIO.output(18, GPIO.LOW)    # RPi-specific hardware call

# To run tests: need a physical Raspberry Pi connected to a motor.
# To port to STM32 or Arduino: rewrite ConveyorBelt from scratch.
# The business logic has become firmware.
import { gpio } from "rpi-gpio"; // hardware-specific library

export class ConveyorBelt {
  private readonly PIN = 18;

  async start(durationMs: number): Promise {
    await gpio.setup(this.PIN, gpio.DIR_OUT);
    await gpio.write(this.PIN, true);   // hardware-specific call
    await new Promise(r => setTimeout(r, durationMs));
    await gpio.write(this.PIN, false);  // hardware-specific call
  }

  async emergencyStop(): Promise {
    await gpio.write(this.PIN, false);  // hardware-specific call
  }
}

// To run tests: need a physical device with GPIO pins.
// To port to a different board: rewrite ConveyorBelt from scratch.
// The business logic has become firmware.

The solution

A MotorController HAL interface separates the business logic from hardware calls. MockMotorController enables unit tests on any laptop. Switching boards means writing a new adapter — business logic is untouched.

Good

from abc import ABC, abstractmethod
import time

# hal/motor_controller.py  ← Hardware Abstraction Layer (stable interface)
class MotorController(ABC):
    @abstractmethod
    def turn_on(self) -> None: ...

    @abstractmethod
    def turn_off(self) -> None: ...

# hal/rpi_motor_controller.py  ← RPi adapter (volatile, hardware-specific)
import RPi.GPIO as GPIO

class RaspberryPiMotorController(MotorController):
    PIN = 18
    def __init__(self):
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(self.PIN, GPIO.OUT)
    def turn_on(self)  -> None: GPIO.output(self.PIN, GPIO.HIGH)
    def turn_off(self) -> None: GPIO.output(self.PIN, GPIO.LOW)

# hal/mock_motor_controller.py  ← test double (runs on any machine)
class MockMotorController(MotorController):
    def __init__(self): self.is_on = False
    def turn_on(self)  -> None: self.is_on = True
    def turn_off(self) -> None: self.is_on = False

# domain/conveyor_belt.py  ← pure business logic, no hardware import
class ConveyorBelt:
    def __init__(self, motor: MotorController):
        self._motor = motor

    def start(self, duration_seconds: float) -> None:
        self._motor.turn_on()
        time.sleep(duration_seconds)
        self._motor.turn_off()

    def emergency_stop(self) -> None:
        self._motor.turn_off()

# Tests run on any laptop — no hardware needed.
# Port to STM32: write STM32MotorController. ConveyorBelt unchanged.
// hal/MotorController.ts  ← HAL interface (stable)
export interface MotorController {
  turnOn(): Promise;
  turnOff(): Promise;
}

// hal/RpiMotorController.ts  ← RPi adapter (volatile, hardware-specific)
import { gpio } from "rpi-gpio";
import { MotorController } from "./MotorController";

export class RpiMotorController implements MotorController {
  private readonly PIN = 18;
  async turnOn():  Promise {
    await gpio.setup(this.PIN, gpio.DIR_OUT);
    await gpio.write(this.PIN, true);
  }
  async turnOff(): Promise { await gpio.write(this.PIN, false); }
}

// hal/MockMotorController.ts  ← test double (runs on any machine)
import { MotorController } from "./MotorController";

export class MockMotorController implements MotorController {
  public isOn = false;
  async turnOn():  Promise { this.isOn = true;  }
  async turnOff(): Promise { this.isOn = false; }
}

// domain/ConveyorBelt.ts  ← pure business logic, no hardware import
import { MotorController } from "../hal/MotorController";

export class ConveyorBelt {
  constructor(private readonly motor: MotorController) {}

  async start(durationMs: number): Promise {
    await this.motor.turnOn();
    await new Promise(r => setTimeout(r, durationMs));
    await this.motor.turnOff();
  }

  async emergencyStop(): Promise { await this.motor.turnOff(); }
}

// Tests run on any laptop — no hardware needed.
// Port to new board: write new adapter. ConveyorBelt unchanged.

Key takeaway

If your business logic can't run on your development laptop without special hardware, it has become firmware — not software. The Hardware Abstraction Layer is the same pattern as Ports and Adapters applied to physical devices: define an abstract interface in the domain, implement it once per hardware target, and test business logic with a mock implementation that runs anywhere.

Done with this lesson?

Mark it complete to earn XP and track your progress.