Back to Technology

CMSIS Part 7: CMSIS-Driver — UART, SPI & I2C Interfaces

March 31, 2026 Wasil Zafar 28 min read

How CMSIS-Driver decouples middleware from silicon — the driver state machine, callback mechanism, and how UART/SPI/I2C drivers work identically whether targeting an STM32, NXP, or Nordic MCU.

Table of Contents

  1. Driver Abstraction Layer
  2. UART (USART) Driver
  3. SPI Driver
  4. I2C Driver
  5. Driver States & Events
  6. RTOS Integration
  7. Exercises
  8. Peripheral Driver Specification
  9. Conclusion & Next Steps
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.

CMSIS Mastery Series

Your 20-step learning path • Currently on Step 7
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.

Peripheral Driver Specification Generator

Document your CMSIS-Driver peripheral design. Download as Word, Excel, PDF, or PPTX.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

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.

Technology