Skip to main content

Embedded and Firmware: Resource Constraints and Hardware Abstraction

Managing bare-metal constraints, bootloaders, power management, and hardware portability

TL;DR

Embedded systems manage extreme constraints: 32KB RAM, 256KB flash (typical microcontroller), 8-80MHz CPU, milliwatt to watt power budgets. Bare-metal (no OS) minimizes overhead (~200 bytes) but requires hand-crafted event loops. RTOS (FreeRTOS, Zephyr) adds scheduling and concurrency, but consumes ~5-20KB. Hardware Abstraction Layer (HAL) isolates board-specific code from application (GPIO, UART, SPI). Bootloaders initialize hardware, verify firmware signatures, enable OTA updates. Power management (sleep modes, dynamic voltage scaling) critical for battery life (weeks vs days).

Learning Objectives

  • Design firmware for extreme resource constraints
  • Choose bare-metal vs RTOS architecture
  • Implement hardware abstraction layers for portability
  • Understand bootloader design and OTA updates
  • Optimize for power consumption and battery life
  • Master interrupt-driven, event-based execution
  • Debug embedded systems with limited tools

Motivating Scenario

You're designing a wearable heart rate monitor: CR2032 battery (30 mAh, 1.5V), ARM Cortex-M0+ (32KB RAM, 256KB flash), must last 7 days (168 hours). Continuous polling drains battery in 8 hours. Solution: interrupt-driven (sleep 99% of time, wake on sensor data), RTOS for task scheduling with sleep hooks, dynamic power management (scale voltage/frequency), HAL to abstract sensor I/O (portable across chip revisions). Result: 7-day battery life, firmware updates over Bluetooth, works on multiple hardware revisions.

Core Concepts

Embedded systems optimize for minimal resource footprint and deterministic, real-time behavior:

Bare-Metal: No OS, direct hardware control. Minimal overhead, maximum complexity, requires hand-crafted main loop.

RTOS (Real-Time Operating System): Scheduler, multitasking, inter-task communication, deterministic timing. ~5-20KB overhead, enables structured concurrency.

HAL (Hardware Abstraction Layer): Interface between hardware (GPIO, UART, SPI, ADC) and application. Enables hardware portability and modularity.

Bootloader: First code to run (from flash address 0x0000). Initializes RAM, stack, clock, verifies firmware signature, jumps to application.

Interrupt Handlers (ISRs): Respond immediately to hardware events (button, timer, UART data). Keep minimal; defer work to tasks or main loop.

Power Modes: Sleep (CPU off, timers/interrupts active), idle (waiting for interrupt), active (full CPU). Switch based on workload to extend battery.

Embedded system layers: Bootloader, HAL, RTOS, Application

Key Terminology

Stack vs Heap: Stack (automatic, fast, limited, grows downward), heap (malloc, slow, fragmentation). Embedded systems minimize heap; prefer static allocation.

ISR (Interrupt Service Routine): Hardware-triggered handler. Must be fast (microseconds), re-entrant. Defer heavy work to tasks.

Context Switch: Saving CPU state (registers, program counter), switching to another task. Overhead ~100-500 cycles. Minimize frequency for real-time systems.

Watchdog Timer: Hardware timer that resets system if firmware doesn't "pet" it periodically. Prevents infinite loops/hangs.

Memory Alignment: 32-bit CPU accesses aligned addresses (4-byte boundaries) faster. Misaligned access is slower or trapped.

Linker Script: Defines memory layout: code (flash), initialized data, zero-initialized data (BSS), heap, stack.

Bare-Metal vs RTOS Comparison

Bare-Metal
  1. No operating system, direct hardware control
  2. Super-linear interrupt handlers, tight timing
  3. Minimal overhead (~200 bytes)
  4. Complex event loop, state machine required
  5. Difficult to manage multiple concurrent tasks
  6. Best for: Simple sensors, single-task devices, ultra-low power
  7. Example: Smart button, basic thermometer
RTOS (FreeRTOS, Zephyr)
  1. Scheduler, multitasking, synchronization primitives
  2. Task-based concurrency (queues, semaphores, mutexes)
  3. ~5-20KB overhead, adds ~100-500 cycles per switch
  4. Cleaner code, easier to manage multiple tasks
  5. Deterministic timing (priority-based scheduling)
  6. Best for: Multi-sensor devices, complex logic, real-time requirements
  7. Example: Wearable, IoT gateway, drone controller

Practical Examples

// Minimal bare-metal firmware for ARM Cortex-M0+
// Heart rate monitor with BLE module

#include <stdint.h>

// Hardware definitions (from datasheet)
#define SYST_RVR 0xE000E014 // SysTick Reload Value
#define SYST_CVR 0xE000E018 // SysTick Current Value
#define SYST_CSR 0xE000E010 // SysTick Control/Status

// Sensor reading structure
typedef struct {
uint16_t heart_rate;
uint16_t temperature;
uint32_t timestamp;
} SensorData;

// Global state (no heap, static memory)
static SensorData sensor_data = {0, 0, 0};
static uint32_t tick_count = 0;
static uint8_t ble_buffer[64];
static uint8_t ble_index = 0;

// Linker-provided symbols
extern uint32_t _etext;
extern uint32_t _sdata;
extern uint32_t _edata;
extern uint32_t _sbss;
extern uint32_t _ebss;

// System initialization
void SystemInit(void) {
// Enable clock to peripherals
// Configure oscillator/PLL
// Set up stack pointer (done by bootloader)
}

// Reset handler (entry point after bootloader)
void Reset_Handler(void) {
// Copy initialized data from flash to RAM
for (uint32_t *src = &_etext, *dst = &_sdata; dst < &_edata; ) {
*dst++ = *src++;
}

// Zero-initialize BSS section
for (uint32_t *dst = &_sbss; dst < &_ebss; ) {
*dst++ = 0;
}

// Initialize system
SystemInit();

// Configure SysTick (1 ms tick)
uint32_t ticks = 64000; // 64 MHz / 1000
*(volatile uint32_t *)SYST_RVR = ticks - 1;
*(volatile uint32_t *)SYST_CSR = 0x07; // Enable SysTick, interrupt

// Call main
main();

// Hang if main returns
while (1);
}

// SysTick interrupt handler (1 ms)
void SysTick_Handler(void) {
tick_count++;

// Every 100 ms: read sensors
if (tick_count % 100 == 0) {
sensor_data.heart_rate = read_heart_rate();
sensor_data.temperature = read_temperature();
sensor_data.timestamp = tick_count;
}

// Every 1 second: transmit BLE packet
if (tick_count % 1000 == 0) {
ble_transmit(&sensor_data);
}
}

// UART interrupt handler
void UART0_Handler(void) {
uint8_t c = read_uart_char();

// Store in ringbuffer
if (ble_index < sizeof(ble_buffer)) {
ble_buffer[ble_index++] = c;

if (c == '\n') {
process_ble_command(ble_buffer, ble_index);
ble_index = 0;
}
}
}

// Main event loop
int main(void) {
while (1) {
// Sleep until interrupt (wakes on SysTick or UART)
asm("wfi"); // Wait for interrupt

// Process any deferred work
// (in bare-metal, most work done in ISRs)
}
return 0;
}

// HAL functions (hardware abstraction)
uint16_t read_heart_rate(void) {
// Read ADC channel 0 (PPG sensor)
// Apply digital filter
// Return BPM
return 0;
}

uint16_t read_temperature(void) {
// Read temperature sensor (I2C)
return 0;
}

void ble_transmit(SensorData *data) {
// Send data packet over UART to BLE module
}

void process_ble_command(uint8_t *cmd, uint8_t len) {
// Handle incoming BLE commands (reset, firmware update, etc.)
}

Characteristics:

  • All timing under ISR control (tick-driven)
  • State machine in main loop
  • No malloc (all static memory)
  • Approximately 2 KB code size with minimal overhead
  • Difficult to add new features because the event loop becomes complex

Common Patterns and Pitfalls

Design Checklist

  • Are memory requirements quantified (RAM, flash, stack depth)?
  • Is bare-metal vs RTOS justified by requirements?
  • Is HAL separating hardware from application logic?
  • Are critical sections (ISR access to variables) protected?
  • Are interrupts kept minimal (deferred work to tasks)?
  • Is watchdog timer implemented and monitored?
  • Are clock/power configurations verified?
  • Is bootloader protecting against bad firmware?
  • Can firmware be updated without field technician (OTA)?
  • Is power consumption optimized (sleep modes, scaling)?
  • Are stack/heap sizes validated under peak load?
  • Is the linker script memory layout correct?

Self-Check

  1. Bare-metal vs RTOS? Bare-metal: less overhead but complex event loops. RTOS: easier task management, some overhead (~5-20 KB).
  2. Why keep ISR minimal? Blocks everything else, disables other interrupts. Set flag, defer work to task.
  3. How to minimize power? Sleep when idle (CPU off), dynamic voltage scaling, clock gating, efficient algorithms.
  4. What's a HAL? Hardware abstraction layer. Interface between hardware and application, enables portability.
  5. Bootloader purpose? Init hardware, verify firmware, jump to app, handle OTA updates, detect corruption.
info

One Takeaway: Embedded is about extreme trade-offs. Every byte, every cycle, every mA matters. Start with constraints, not features. Use HAL for portability, RTOS for concurrency, and aggressive power management for battery life.

Next Steps

  • RTOS: FreeRTOS, Zephyr, Mbed OS, RIOT
  • HAL/SDKs: ARM Cortex-M CMSIS, STM32CubeMX, Nordic SDK, nRF SDK
  • Bootloader: MCUboot, coreboot, U-Boot, NXP mcuboot
  • Power Analysis: Oscilloscope measurements, energy profilers, simulation
  • Testing: Unit tests, hardware-in-loop (HIL), fault injection
  • Tools: GCC/Clang, OpenOCD, J-Link, stlink

References