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.