8.1 Device drivers
A driver is a module that wraps a peripheral with a clean API:
// 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
.c -> preprocessor -> compiler -> assembler -> linker -> .elf -> objcopy -> .bin/.hex
|
.ld linker scriptFor ARM Cortex-M:
- arm-none-eabi-gcc. Free, ubiquitous. The
noneandeabireflect 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:
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.
printfbecomes 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.