>
section 8 of 183 min read

8. Drivers and Toolchain

8.1 Device drivers

A driver is a module that wraps a peripheral with a clean API:

c
// driver public API
void uart_init(uint32_t baud);
int  uart_tx(const uint8_t *buf, size_t len);
int  uart_rx(uint8_t *buf, size_t len, uint32_t timeout);

Inside, it bit-bangs registers, enables interrupts, manages buffers. Two philosophies:

  • Bare-metal. You write the registers yourself, every line. Maximum control, minimum overhead, painful for complex peripherals (USB, Ethernet, SDIO).
  • HAL (Hardware Abstraction Layer). Vendor library (STM32 HAL, ESP-IDF, Nordic SoftDevice) gives you HAL_UART_Transmit(...). Faster to write, slower binary, sometimes leaky.

Most production code today uses a HAL, sometimes augmented with a higher-level OS (Zephyr's device tree, Mbed-OS, Arduino). For tight inner loops you drop to bare metal.

8.2 Toolchain components

plaintext
   .c -> preprocessor -> compiler -> assembler -> linker -> .elf -> objcopy -> .bin/.hex
                                                  |
                                              .ld linker script

For ARM Cortex-M:

  • arm-none-eabi-gcc. Free, ubiquitous. The none and eabi reflect that there is no host OS and the embedded ABI is in use.
  • arm-none-eabi-as, ld, objcopy, gdb. Companion binutils.
  • Keil MDK / IAR EWARM. Commercial, very mature, used in safety-certified code. Better optimization in some cases, integrated debug.
  • LLVM/Clang. Increasingly used; great diagnostics.
  • Build systems. Make (classic), CMake (modern, cross-platform), SCons, Bazel for big shops, Ninja for speed. PlatformIO wraps these for hobbyists.

A linker script is the key file in embedded:

plaintext
MEMORY {
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    SRAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
 
SECTIONS {
    .text   : { *(.text*) *(.rodata*) } > FLASH
    .data   : AT(LOADADDR(.text)+SIZEOF(.text)) { *(.data*) } > SRAM
    .bss    : { *(.bss*) }            > SRAM
}

This tells the linker where code, initialized data, and zero-initialized data go in the chip's memory map. The startup code copies .data from flash to RAM and zeroes .bss before calling main.

8.3 Programming and debugging

  • JTAG. Four/five wires (TCK, TMS, TDI, TDO, optional TRST). Originally for boundary-scan production test. Lets you halt the CPU, read/write memory and registers, set breakpoints. Universal across silicon.
  • SWD. ARM's two-wire alternative (SWCLK, SWDIO). Functionally a JTAG subset. Almost all Cortex-M chips support it.
  • Programmers/debuggers. ST-Link, J-Link (Segger, gold standard), DAPLink, Black Magic Probe. Plug into the SWD header, talk to OpenOCD or proprietary software, drive the chip.
  • GDB + OpenOCD. Open-source duo. OpenOCD bridges between GDB on your PC and SWD on the chip. Set breakpoints, single-step, view memory, all from arm-none-eabi-gdb.
  • Semihosting. Special breakpoint-like instruction that asks the host debugger to do I/O. printf becomes a JTAG message to your terminal. Slow but priceless when no UART is wired.
  • ITM (Instrumentation Trace Macrocell). Cortex-M's high-speed printf channel over SWO pin. Microsecond-stamped, low overhead, lovely for profiling.
  • Logic analyzer. Saleae, DigiView, hobbyist clones. Capture multi-pin behavior, decode protocols. Indispensable for debugging serial buses.
  • Oscilloscope. For analog or fast digital edges.
  • Power profiler. Measure microamp-level current vs time. Critical for battery devices. Nordic Power Profiler Kit, Joulescope, Otii.

8.4 Modern practice

Version control in git. Continuous integration that builds and runs unit tests on a host (Ceedling, Unity, Google Test). Static analysis (cppcheck, clang-tidy, MISRA-C compliance for safety-critical). Hardware-in-the-loop test rigs that drive real boards through scripted scenarios. Code reviews that pay attention to ISR latency, stack usage, and integer overflow.

Code style standards: MISRA-C (automotive), CERT-C (security), AUTOSAR C++14 (cars), DO-178C (aviation), IEC 62304 (medical). Each forbids certain language features (no recursion, no malloc, no goto, ...) to make code more analyzable.