Series Progress: This is Part 7 of our 20-part CMSIS Mastery Series. Parts 1–6 covered the ecosystem, CMSIS-Core, startup code, RTOS2 threads, RTOS2 IPC, and CMSIS-DSP. Now we explore the driver abstraction layer that lets middleware run unchanged across silicon vendors.
1
Overview & ARM Cortex-M Ecosystem
CMSIS layers, Cortex-M families, memory map, toolchains
Completed
2
CMSIS-Core: Registers, NVIC & SysTick
core_cmX.h, register access, interrupt controller, SysTick timer
Completed
3
Startup Code, Linker Scripts & Vector Table
Reset handler, BSS init, scatter files, boot process
Completed
4
CMSIS-RTOS2: Threads, Mutexes & Semaphores
Thread management, synchronization primitives, scheduling
Completed
5
CMSIS-RTOS2: Message Queues & Event Flags
Inter-thread comms, ISR-to-thread, real-time design patterns
Completed
6
CMSIS-DSP: Filters, FFT & Math Functions
FIR/IIR filters, FFT, SIMD optimizations
Completed
7
CMSIS-Driver: UART, SPI & I2C
Driver abstraction layer, callbacks, DMA integration
You Are Here
8
CMSIS-Pack & Software Components
Pack files, device support, dependency management
9
Debugging with CMSIS-DAP & CoreSight
SWD/JTAG, HardFault analysis, ITM tracing
10
Portable Firmware: Multi-Vendor Projects
HAL vs CMSIS, cross-platform BSPs, reusable libraries
11
Interrupts, Concurrency & Real-Time Constraints
Interrupt latency, critical sections, lock-free programming
12
Memory Management in Embedded Systems
Static vs dynamic, heap fragmentation, memory pools
13
Low Power & Energy Optimization
Sleep modes, clock gating, tickless RTOS, power profiling
14
DMA & High-Performance Data Handling
DMA basics, peripheral transfers, zero-copy techniques
15
Security: ARMv8-M & TrustZone
Secure/non-secure worlds, secure boot, firmware protection
16
Bootloaders & Firmware Updates
OTA updates, dual-bank flash, fail-safe strategies
17
Testing & Validation
Unity/Ceedling unit tests, HIL testing, integration testing
18
Performance Optimization
Compiler flags, inline assembly, cache (M7/M33), profiling
19
Embedded Software Architecture
Layered design, event-driven, state machines, component-based
20
Tooling & Workflow (Professional Level)
CI/CD for embedded, MISRA, static analysis, Doxygen
Driver Abstraction Layer
Embedded middleware — USB stacks, TCP/IP stacks, file systems, protocol parsers — must communicate with hardware peripherals. If the middleware code calls vendor-specific HAL functions directly, porting it to a new MCU means editing the middleware itself. This violates the open-closed principle and makes the middleware untestable in isolation. CMSIS-Driver solves this with a hardware abstraction layer defined as a C struct of function pointers.
Every CMSIS-Driver interface follows the same pattern. A global constant struct of type ARM_DRIVER_xx (e.g., ARM_DRIVER_USART) exposes a fixed set of function pointers: GetVersion, GetCapabilities, Initialize, Uninitialize, PowerControl, and the peripheral-specific operations (Send, Receive, Transfer, etc.). The vendor writes one implementation struct per peripheral instance; middleware receives a pointer to that struct and calls through it without knowing the underlying silicon.
Portability in Practice: A CMSIS-Driver-compliant USB device stack calls pDriver->Send() and registers for ARM_USBD_EVENT_IN callbacks. Swap the pDriver pointer from an STM32 USBD driver to an NXP LPC USB driver — the USB stack recompiles unchanged. This is why CMSIS-Driver is the foundation of most commercial ARM middleware.
| Interface |
Header |
Transfer Mode |
DMA |
Async Events |
RTOS Safe |
| USART |
Driver_USART.h |
Send / Receive / Transfer |
Yes |
SEND_COMPLETE, RECEIVE_COMPLETE, RX_TIMEOUT |
Yes (non-blocking) |
| SPI |
Driver_SPI.h |
Send / Receive / Transfer |
Yes |
TRANSFER_COMPLETE, DATA_LOST |
Yes |
| I2C |
Driver_I2C.h |
MasterTransmit / MasterReceive |
Optional |
MASTER_DONE, SLAVE_RECEIVE, BUS_ERROR |
Yes |
| USB Device |
Driver_USBD.h |
EndpointTransfer |
Yes |
ENDPOINT_IN, ENDPOINT_OUT, RESET |
Yes |
| Ethernet |
Driver_ETH.h |
SendFrame / ReadFrame |
Yes (EMAC) |
RX_FRAME, TX_COMPLETE, LINK_CHANGE |
Yes |
UART (USART) Driver
The USART driver is the most commonly used CMSIS-Driver interface — almost every embedded project has at least one debug or communication UART. The key point to internalise is that Send() is non-blocking: it starts the transfer (via interrupt or DMA) and returns immediately. The actual completion is signalled by the ARM_USART_SignalEvent callback that you register during Initialize(). Your application must wait for this callback (via semaphore, event flag, or polling the GetStatus() function) before invoking the next Send().
/* ── uart_cmsis_driver.c ─────────────────────────────────────────────────
* CMSIS-Driver USART usage: Initialize, PowerControl, Send with
* callback, receive with ring buffer, error handling.
* ──────────────────────────────────────────────────────────────────────── */
#include "Driver_USART.h" /* ARM CMSIS-Driver header */
#include "cmsis_os2.h" /* RTOS2 for semaphore */
#include
#include
/* Reference the driver instance exposed by the vendor BSP.
* This is defined in the vendor's CMSIS-Driver implementation file. */
extern ARM_DRIVER_USART Driver_USART2;
static ARM_DRIVER_USART *const pUSART = &Driver_USART2;
/* Semaphore released by TX-complete callback */
static osSemaphoreId_t g_uart_tx_sem;
/* RX ring buffer */
#define RX_BUF_SIZE 256U
static uint8_t g_rx_ring[RX_BUF_SIZE];
static volatile uint32_t g_rx_head = 0U, g_rx_tail = 0U;
static uint8_t g_rx_byte; /* single-byte DMA/interrupt receive target */
/* ── Signal event callback — called from driver ISR context ─────────── */
static void usart_callback(uint32_t event)
{
if (event & ARM_USART_EVENT_SEND_COMPLETE) {
/* TX done — release semaphore to unblock waiting thread */
osSemaphoreRelease(g_uart_tx_sem);
}
if (event & ARM_USART_EVENT_RECEIVE_COMPLETE) {
/* Single byte received — store in ring buffer */
uint32_t next_head = (g_rx_head + 1U) % RX_BUF_SIZE;
if (next_head != g_rx_tail) { /* not full */
g_rx_ring[g_rx_head] = g_rx_byte;
g_rx_head = next_head;
}
/* Re-arm receive for next byte */
pUSART->Receive(&g_rx_byte, 1U);
}
if (event & ARM_USART_EVENT_RX_OVERFLOW) {
/* RX hardware FIFO overrun — increment error counter, re-arm */
pUSART->Receive(&g_rx_byte, 1U);
}
}
/* ── Initialisation ──────────────────────────────────────────────────── */
int32_t uart_init(uint32_t baud_rate)
{
int32_t status;
g_uart_tx_sem = osSemaphoreNew(1U, 0U, NULL); /* initially unavailable */
/* Step 1: Register callback and initialise driver hardware */
status = pUSART->Initialize(usart_callback);
if (status != ARM_DRIVER_OK) { return status; }
/* Step 2: Power on the peripheral */
status = pUSART->PowerControl(ARM_POWER_FULL);
if (status != ARM_DRIVER_OK) { return status; }
/* Step 3: Configure baud rate, 8N1, asynchronous UART */
status = pUSART->Control(
ARM_USART_MODE_ASYNCHRONOUS |
ARM_USART_DATA_BITS_8 |
ARM_USART_PARITY_NONE |
ARM_USART_STOP_BITS_1 |
ARM_USART_FLOW_CONTROL_NONE,
baud_rate);
if (status != ARM_DRIVER_OK) { return status; }
/* Step 4: Enable TX and RX */
pUSART->Control(ARM_USART_CONTROL_TX, 1U);
pUSART->Control(ARM_USART_CONTROL_RX, 1U);
/* Step 5: Arm the first receive (starts DMA or interrupt) */
pUSART->Receive(&g_rx_byte, 1U);
return ARM_DRIVER_OK;
}
/* ── Blocking send — waits for TX-complete callback ─────────────────── */
int32_t uart_send_blocking(const uint8_t *p_data, uint32_t length,
uint32_t timeout_ms)
{
int32_t status = pUSART->Send(p_data, length);
if (status != ARM_DRIVER_OK) { return status; }
/* Block until callback releases the semaphore */
osStatus_t os_status = osSemaphoreAcquire(g_uart_tx_sem, timeout_ms);
return (os_status == osOK) ? ARM_DRIVER_OK : ARM_DRIVER_ERROR_TIMEOUT;
}
PowerControl Sequence: Always call Initialize() before PowerControl(ARM_POWER_FULL) before Control(). Calling them out of order is undefined behaviour. On low-power exit, call PowerControl(ARM_POWER_LOW) to clock-gate the peripheral while preserving configuration — then ARM_POWER_FULL to resume. Never call Uninitialize() if you plan to reuse the driver; reserve it for teardown.
SPI Driver
SPI is a full-duplex synchronous bus: every clock cycle the master shifts one bit out on MOSI and simultaneously shifts one bit in on MISO. CMSIS-Driver reflects this with a single Transfer() function that takes both a TX buffer and an RX buffer simultaneously. For write-only operations (e.g., driving an LCD), pass NULL as the RX buffer; for read-only operations (rare in practice), pass NULL as TX.
Chip-select management is deliberately not part of the CMSIS-Driver SPI specification — it is handled at the application level using GPIO. This gives you full control over CS timing, multi-device buses with shared SPI, and advanced protocols that require CS held low across multiple transfers.
/* ── spi_cmsis_driver.c ──────────────────────────────────────────────────
* CMSIS-Driver SPI: master mode, DMA-backed Transfer, CS via GPIO.
* Demonstrates full-duplex Transfer and blocking wrapper via semaphore.
* ──────────────────────────────────────────────────────────────────────── */
#include "Driver_SPI.h"
#include "Driver_GPIO.h" /* or use direct register access for CS pin */
#include "cmsis_os2.h"
#include
extern ARM_DRIVER_SPI Driver_SPI1;
static ARM_DRIVER_SPI *const pSPI = &Driver_SPI1;
static osSemaphoreId_t g_spi_done_sem;
/* ── SPI event callback ──────────────────────────────────────────────── */
static void spi_callback(uint32_t event)
{
if (event & ARM_SPI_EVENT_TRANSFER_COMPLETE) {
osSemaphoreRelease(g_spi_done_sem);
}
if (event & ARM_SPI_EVENT_DATA_LOST) {
/* RX overrun or TX underrun — increment error counter */
}
}
/* ── One-time initialisation ─────────────────────────────────────────── */
int32_t spi_init(uint32_t clock_hz)
{
g_spi_done_sem = osSemaphoreNew(1U, 0U, NULL);
pSPI->Initialize(spi_callback);
pSPI->PowerControl(ARM_POWER_FULL);
/* Configure: master, CPOL=0, CPHA=0 (Mode 0), MSB first */
pSPI->Control(
ARM_SPI_MODE_MASTER |
ARM_SPI_CPOL0_CPHA0 | /* SPI Mode 0 */
ARM_SPI_MSB_LSB | /* MSB first */
ARM_SPI_SS_MASTER_SW | /* CS managed by software (GPIO) */
ARM_SPI_DATA_BITS(8),
clock_hz);
return ARM_DRIVER_OK;
}
/* ── Full-duplex Transfer with CS management ─────────────────────────── */
static void gpio_cs_assert(void) { /* pull CS low via GPIO register */ }
static void gpio_cs_deassert(void) { /* pull CS high via GPIO register */ }
int32_t spi_transfer_blocking(const uint8_t *p_tx, uint8_t *p_rx,
uint32_t length, uint32_t timeout_ms)
{
gpio_cs_assert();
int32_t status = pSPI->Transfer(p_tx, p_rx, length);
if (status != ARM_DRIVER_OK) {
gpio_cs_deassert();
return status;
}
/* Block until DMA transfer complete callback fires */
osStatus_t os_st = osSemaphoreAcquire(g_spi_done_sem, timeout_ms);
gpio_cs_deassert();
return (os_st == osOK) ? ARM_DRIVER_OK : ARM_DRIVER_ERROR_TIMEOUT;
}
/* ── Read a sensor register: TX register address, RX data byte ───────── */
int32_t spi_read_register(uint8_t reg_addr, uint8_t *p_value)
{
uint8_t tx_buf[2] = { reg_addr | 0x80U, 0x00U }; /* read flag = bit7 */
uint8_t rx_buf[2] = { 0U, 0U };
int32_t status = spi_transfer_blocking(tx_buf, rx_buf, 2U, 10U);
if (status == ARM_DRIVER_OK) {
*p_value = rx_buf[1]; /* first byte is dummy during address phase */
}
return status;
}
| SPI Pitfall |
Symptom |
Fix |
| Wrong CPOL/CPHA |
All bytes read back as 0x00 or 0xFF |
Check sensor datasheet for SPI mode; match ARM_SPI_CPOLx_CPHAy |
| CS deasserted too early |
Multi-byte reads corrupt after first byte |
Deassert CS only after Transfer complete callback, not in ISR |
| MSB/LSB mismatch |
Bit-reversed data; each byte mirrored |
Verify ARM_SPI_MSB_LSB vs ARM_SPI_LSB_MSB matches device spec |
| Clock too fast for long PCB trace |
Intermittent errors at high speeds |
Reduce clock; add series resistors on SCLK/MOSI; check signal integrity |
I2C Driver
I2C is a two-wire, multi-master, multi-slave bus with 7-bit (standard) or 10-bit (extended) device addressing. CMSIS-Driver separates master transmit and master receive into distinct calls: MasterTransmit() writes bytes to a slave, MasterReceive() reads bytes from a slave. For the common "write register address then read data" pattern, the two calls are combined with a repeated START condition — achieved by passing xfer_pending = true (value 1) to the first call, which suppresses the STOP condition and holds the bus, then xfer_pending = false (value 0) to the second call to release it.
/* ── i2c_cmsis_driver.c ──────────────────────────────────────────────────
* CMSIS-Driver I2C: 7-bit and 10-bit addressing, combined format read.
* Demonstrates MasterTransmit → MasterReceive with repeated START.
* ──────────────────────────────────────────────────────────────────────── */
#include "Driver_I2C.h"
#include "cmsis_os2.h"
#include
extern ARM_DRIVER_I2C Driver_I2C1;
static ARM_DRIVER_I2C *const pI2C = &Driver_I2C1;
static osSemaphoreId_t g_i2c_done_sem;
/* ── I2C event callback ──────────────────────────────────────────────── */
static void i2c_callback(uint32_t event)
{
if (event & ARM_I2C_EVENT_TRANSFER_DONE) {
osSemaphoreRelease(g_i2c_done_sem);
}
if (event & ARM_I2C_EVENT_TRANSFER_INCOMPLETE) {
/* NACK from slave or lost arbitration — signal with error code */
osSemaphoreRelease(g_i2c_done_sem);
}
if (event & ARM_I2C_EVENT_BUS_ERROR) {
/* SDA/SCL glitch or timeout — log and reset bus */
osSemaphoreRelease(g_i2c_done_sem);
}
}
/* ── Initialisation ──────────────────────────────────────────────────── */
int32_t i2c_init(uint32_t bus_speed)
{
g_i2c_done_sem = osSemaphoreNew(1U, 0U, NULL);
pI2C->Initialize(i2c_callback);
pI2C->PowerControl(ARM_POWER_FULL);
/* bus_speed: ARM_I2C_BUS_SPEED_STANDARD (100 kHz)
* ARM_I2C_BUS_SPEED_FAST (400 kHz)
* ARM_I2C_BUS_SPEED_FAST_PLUS (1 MHz) */
pI2C->Control(ARM_I2C_BUS_SPEED, bus_speed);
pI2C->Control(ARM_I2C_BUS_CLEAR, 0U); /* release SDA if stuck low */
return ARM_DRIVER_OK;
}
/* ── Combined format read: write register addr, repeated START, read data */
int32_t i2c_sensor_read(uint8_t slave_addr_7bit,
uint8_t reg_addr,
uint8_t *p_data,
uint32_t num_bytes,
uint32_t timeout_ms)
{
int32_t status;
osStatus_t os_st;
/* Phase 1: write register address with pending=true (no STOP) */
status = pI2C->MasterTransmit(slave_addr_7bit, ®_addr, 1U,
true /* xfer_pending: suppress STOP */);
if (status != ARM_DRIVER_OK) { return status; }
os_st = osSemaphoreAcquire(g_i2c_done_sem, timeout_ms);
if (os_st != osOK) { return ARM_DRIVER_ERROR_TIMEOUT; }
/* Check for NACK: slave didn't acknowledge register address */
ARM_I2C_STATUS i2c_status = pI2C->GetStatus();
if (i2c_status.bus_error || !i2c_status.busy == 0) {
/* NACK received — wrong address or register */
return ARM_DRIVER_ERROR;
}
/* Phase 2: repeated START + read (pending=false → send STOP after) */
status = pI2C->MasterReceive(slave_addr_7bit, p_data, num_bytes,
false /* xfer_pending: send STOP */);
if (status != ARM_DRIVER_OK) { return status; }
os_st = osSemaphoreAcquire(g_i2c_done_sem, timeout_ms);
return (os_st == osOK) ? ARM_DRIVER_OK : ARM_DRIVER_ERROR_TIMEOUT;
}
/* ── 10-bit addressing example ───────────────────────────────────────── */
int32_t i2c_write_10bit(uint16_t slave_addr_10bit,
const uint8_t *p_data, uint32_t length)
{
/* CMSIS-Driver uses bit 10 (0x0400) to signal 10-bit addressing */
uint32_t addr = (uint32_t)slave_addr_10bit | ARM_I2C_ADDRESS_10BIT;
int32_t status = pI2C->MasterTransmit((uint32_t)addr, p_data,
length, false);
if (status != ARM_DRIVER_OK) { return status; }
osSemaphoreAcquire(g_i2c_done_sem, 100U);
return ARM_DRIVER_OK;
}
Bus Stuck High/Low: I2C buses can get stuck if a transfer is aborted mid-byte — the slave holds SDA low expecting more clock pulses. Call pI2C->Control(ARM_I2C_BUS_CLEAR, 0) after any error to clock SCL up to nine times, which forces all slaves to release SDA. If the bus remains stuck, a hardware reset of the slave (via a dedicated NRST pin or power cycle) is required.
Driver States & Events
Every CMSIS-Driver follows a well-defined state machine. Understanding this model is essential because violating the state sequence causes undefined behaviour — the vendor implementation may silently fail or corrupt hardware state. The states are: Uninitialized → Initialized → Powered → Configured → Active.
Uninitialized
Uninitialized State
Driver struct exists but no hardware is configured. Only GetVersion() and GetCapabilities() are safe to call. Calling PowerControl() before Initialize() is an error — the callback pointer is not yet registered.
Initialized
Initialized State
Callback is registered and GPIO pins are mapped. Peripheral clock is still gated (low power). PowerControl(ARM_POWER_FULL) transitions to Powered. PowerControl(ARM_POWER_OFF) returns to Uninitialized.
Powered
Powered State
Peripheral clock is running. Control() may now be called to configure baud rate, mode, data bits. Calling Send() before Control() is undefined — the peripheral has no valid configuration.
Configured / Active
Configured & Active
Driver is ready for transfers. Send(), Receive(), and Transfer() start DMA or interrupt-driven operations. GetStatus() polls the current state. GetDataCount() returns bytes transferred so far in an active operation.
RTOS Integration
CMSIS-Driver is inherently asynchronous — every transfer function returns immediately and signals completion via callback. This is ideal for bare-metal polling loops, but RTOS applications usually want a simple blocking API: call a function, block the thread until done, return with the result. The bridge is a semaphore acquired in the calling thread and released in the driver callback.
The pattern from the UART and SPI sections above is the canonical approach. Below is a generic wrapper that codifies this pattern for any CMSIS-Driver that follows the callback convention — useful for building a BSP abstraction layer that all application modules consume.
/* ── cmsis_driver_rtos_wrapper.c ─────────────────────────────────────────
* Generic RTOS-blocking wrapper around a CMSIS-Driver using osSemaphore.
* The same pattern works for USART, SPI, I2C, and custom drivers.
* ──────────────────────────────────────────────────────────────────────── */
#include "cmsis_os2.h"
#include
/* ── Opaque driver context — one per peripheral instance ─────────────── */
typedef struct {
osSemaphoreId_t done_sem; /* released by callback on completion */
volatile int32_t last_event; /* last event code from callback */
} DriverCtx_t;
/* ── Callback invoked by the CMSIS-Driver ISR/DMA handler ────────────── */
/* Application wraps this in a driver-specific callback that casts
* arg back to DriverCtx_t and calls this function. */
void driver_ctx_signal(DriverCtx_t *ctx, int32_t event)
{
ctx->last_event = event;
osSemaphoreRelease(ctx->done_sem); /* unblock waiting thread */
}
/* ── Blocking wait — call after a non-blocking driver function ───────── */
int32_t driver_ctx_wait(DriverCtx_t *ctx, uint32_t timeout_ms)
{
osStatus_t st = osSemaphoreAcquire(ctx->done_sem, timeout_ms);
if (st != osOK) { return -1; /* timeout */ }
return ctx->last_event;
}
/* ── Initialise a context ────────────────────────────────────────────── */
void driver_ctx_init(DriverCtx_t *ctx)
{
ctx->done_sem = osSemaphoreNew(1U, 0U, NULL);
ctx->last_event = 0;
}
/* ── Example: I2C scanner using driver context ───────────────────────── */
/* Probes all 127 7-bit I2C addresses and logs which respond with ACK. */
void i2c_scanner(ARM_DRIVER_I2C *pI2C, DriverCtx_t *ctx)
{
uint8_t dummy = 0x00U;
for (uint8_t addr = 1U; addr < 127U; addr++) {
/* Attempt zero-length write — if slave ACKs, it exists */
pI2C->MasterTransmit(addr, &dummy, 1U, false);
int32_t event = driver_ctx_wait(ctx, 10U); /* 10 ms per probe */
if (event >= 0 && (uint32_t)event & ARM_I2C_EVENT_TRANSFER_DONE) {
/* Device responded — log address in hex */
/* printf("I2C device at 0x%02X\r\n", addr); */
}
/* ARM_I2C_EVENT_TRANSFER_INCOMPLETE indicates NACK (no device) */
}
}
Exercises
Exercise 1
Beginner
Implement an RTOS-Blocking UART Print Function
Build a function uart_print(const char *str) that wraps the CMSIS-Driver USART in a blocking API. The function should call pUSART->Send() to start the transfer, then use osSemaphoreAcquire() with a 100 ms timeout to block the calling thread until ARM_USART_EVENT_SEND_COMPLETE fires in the callback. Test it from two RTOS threads simultaneously and verify that the output is not interleaved (you will need a mutex to serialise access). Measure throughput: at 115200 baud, how many characters per second can you achieve with single calls vs block calls of 64 bytes each?
CMSIS-Driver USART
RTOS Blocking
osSemaphore
Exercise 2
Intermediate
Bit-Bang SPI via GPIO Compared to CMSIS-Driver DMA
Implement a software bit-bang SPI master using direct GPIO register manipulation: toggle SCLK, write MOSI, and read MISO bit-by-bit for 8-bit transfers. Time a 256-byte write to a SPI flash sector using DWT cycle counters. Then implement the identical transfer using the CMSIS-Driver SPI with DMA. Compare: (1) total clock cycles consumed, (2) CPU utilisation during transfer (bit-bang: 100% CPU; DMA: near 0%), (3) maximum achievable clock rate for both. Document the cross-over point — at what data rate does the DMA version stop being worth the driver setup overhead?
Bit-Bang SPI
DMA Transfer
CPU Utilisation
Exercise 3
Advanced
CMSIS-Driver I2C Scanner — Probe All 127 Addresses
Using the CMSIS-Driver I2C and the generic RTOS wrapper pattern, implement an I2C bus scanner that probes all 7-bit addresses (1–126). For each address, call MasterTransmit(addr, &dummy, 1, false) and wait up to 10 ms for the callback. Classify each address as ACK (device present), NACK (no device), or timeout (bus error). Print a formatted ASCII map of all responding addresses in the same style as the Linux i2cdetect tool. Then add 10-bit address support: probe the 10-bit extended range (0x000–0x3FF) using the ARM_I2C_ADDRESS_10BIT flag. Handle the edge case where address 0 (general call) may trigger all slaves simultaneously.
I2C Scanner
10-bit Addressing
Bus Error Handling
Peripheral Driver Specification
Use this tool to document your CMSIS-Driver peripheral configuration — interface type, baud/clock rate, DMA mode, callback events, RTOS integration approach, and error handling strategy. Download as Word, Excel, PDF, or PPTX for design documentation or BSP handoff.
Conclusion & Next Steps
In this article we have dissected the CMSIS-Driver abstraction layer from first principles to production usage:
- The ARM_DRIVER_xx struct-of-function-pointers pattern is the mechanism that decouples middleware from silicon — swap the struct pointer, recompile, run on new hardware.
- The USART driver lifecycle follows Initialize → PowerControl → Control → Send/Receive, with completion signalled asynchronously via the
SignalEvent callback.
- SPI Transfer() is full-duplex; chip-select is managed externally by GPIO — this keeps the driver clean and gives you full control over CS timing for multi-device buses.
- I2C combined format reads use
xfer_pending=true to suppress the STOP and issue a repeated START — the correct pattern for register-addressed sensor reads.
- The driver state machine (Uninitialized → Initialized → Powered → Configured → Active) must be respected; violating the sequence is undefined behaviour.
- A semaphore-based blocking wrapper turns asynchronous CMSIS-Driver into synchronous RTOS-friendly API calls — the canonical pattern for all production firmware.
Next in the Series
In Part 8: CMSIS-Pack & Software Components, we look at the distribution and dependency management layer: how device support packages are structured, how the cpackget tool resolves dependencies, and how to create your own pack for reusable BSP components.
Related Articles in This Series
Part 8: CMSIS-Pack & Software Components
Understand how device support packages, CMSIS-Driver implementations, and middleware components are bundled and consumed via the CMSIS-Pack format and cpackget tooling.
Read Article
Part 5: CMSIS-RTOS2 — Message Queues & Event Flags
The semaphore pattern used throughout this article to convert async CMSIS-Driver callbacks into blocking APIs — master it here with full context on RTOS2 IPC mechanisms.
Read Article
Part 9: Debugging with CMSIS-DAP & CoreSight
When UART bytes are corrupted and I2C acknowledges the wrong address — this article covers the SWD/JTAG debug tools, ITM printf tracing, and HardFault analysis you'll need to diagnose driver bugs.
Read Article