Skip to main content

Linux SPI Driver Explained: Architecture and Driver Design

·684 words·4 mins
Linux SPI Device Driver Embedded Systems
Table of Contents

SPI (Serial Peripheral Interface) is one of the most widely used high-speed synchronous buses in embedded Linux systems. It connects SoCs to peripherals such as NOR Flash, EEPROMs, ADCs, displays, and IMU sensors, offering low latency and simple hardware requirements.

This article explains both SPI as a bus protocol and how Linux models SPI devices and drivers, ending with a practical sensor-driver example.

🔌 SPI Bus Fundamentals
#

Bus Signals and Roles
#

SPI follows a master–slave architecture and typically uses four signals:

  • MOSI (Master Out, Slave In) — Data from master to slave
  • MISO (Master In, Slave Out) — Data from slave to master
  • SCLK (Serial Clock) — Clock generated by the master
  • CS / nCS (Chip Select) — Active-low slave selection line

Each slave has a dedicated chip-select, allowing multiple devices to share the same data and clock lines.

Timing and Bit Exchange
#

SPI behaves like a bidirectional shift register:

  • One bit is transmitted and received per clock cycle
  • Data is usually sent MSB first
  • Transmission is full-duplex by design

While one side shifts data out, it simultaneously shifts data in.

SPI Modes (CPOL / CPHA)
#

SPI defines four timing modes using clock polarity and phase:

  • Mode 0: CPOL = 0, CPHA = 0 (idle low, sample on rising edge)
  • Mode 1: CPOL = 0, CPHA = 1
  • Mode 2: CPOL = 1, CPHA = 0
  • Mode 3: CPOL = 1, CPHA = 1 (idle high, sample on rising edge)

Correct mode selection is critical—mismatches cause subtle data corruption.

🧩 Linux SPI Framework Architecture
#

Linux cleanly separates SPI responsibilities into three layers:

  1. SPI Core
    Common infrastructure that manages devices, masters, and message scheduling.

  2. SPI Master (Controller) Driver
    Hardware-specific driver for the SoC SPI controller (e.g., i.MX ECSPI, SPI NOR controller).

  3. SPI Protocol / Device Driver
    Peripheral-specific driver (sensor, display, flash, etc.).

This separation allows the same device driver to work across multiple platforms.

🧱 Core SPI Data Structures
#

spi_device
#

Represents a physical SPI slave device. It contains:

  • Chip select number
  • SPI mode (CPOL / CPHA)
  • Maximum clock frequency
  • Pointer to the associated spi_master

Each SPI device typically corresponds to one node in the device tree.

spi_master / spi_controller
#

Represents the SPI controller hardware inside the SoC. It:

  • Queues SPI messages
  • Controls clock generation and chip-select timing
  • Handles PIO or DMA transfers

spi_transfer and spi_message
#

  • spi_transfer
    The smallest data unit: one TX buffer and/or RX buffer.

  • spi_message
    An ordered list of transfers that execute atomically.
    Once started, no other SPI message may interrupt it.

This design ensures protocol correctness for multi-phase commands.

🔄 SPI Data Transfer Workflow
#

A typical SPI transaction follows this pattern:

  1. Initialize a spi_message
  2. Configure one or more spi_transfer structures
  3. Append transfers to the message
  4. Submit using:
    • spi_sync() for blocking calls
    • spi_async() for non-blocking calls

The SPI core schedules execution, while the master driver handles hardware interaction.

🧪 Example: ICM20608 SPI Sensor Driver
#

The following example demonstrates reading multiple registers from an ICM20608 IMU using Linux SPI APIs.

static int icm20608_read_regs(struct icm20608_dev *dev,
                             u8 reg, void *buf, int len)
{
    int ret;
    u8 txdata[1];
    struct spi_message msg;
    struct spi_transfer xfer[2];
    struct spi_device *spi = dev->private_data;

    memset(xfer, 0, sizeof(xfer));

    /* Transfer 1: send register address */
    txdata[0] = reg | 0x80;   /* MSB=1 indicates read */
    xfer[0].tx_buf = txdata;
    xfer[0].len = 1;

    /* Transfer 2: read data */
    xfer[1].rx_buf = buf;
    xfer[1].len = len;

    spi_message_init(&msg);
    spi_message_add_tail(&xfer[0], &msg);
    spi_message_add_tail(&xfer[1], &msg);

    ret = spi_sync(spi, &msg);
    return ret;
}

Probe Function
#

static int icm20608_probe(struct spi_device *spi)
{
    spi->mode = SPI_MODE_0;
    spi_setup(spi);

    /* Hardware initialization */
    icm20608_reginit();
    return 0;
}

Key points illustrated here:

  • Multi-phase SPI operations are modeled as a single spi_message
  • Chip-select handling is usually automatic
  • The driver remains portable across SPI controllers

🧠 Summary
#

The Linux SPI subsystem provides a clean and scalable abstraction over diverse hardware controllers. By structuring communication as atomic spi_message objects composed of spi_transfer segments, Linux ensures correctness, flexibility, and performance.

For embedded Linux developers, mastering SPI means understanding message composition, timing modes, and driver layering—once these are clear, adding new SPI peripherals becomes straightforward and robust.

Related

Linux Driver mmap Explained: Zero-Copy User–Kernel IO
·525 words·3 mins
Linux Device Driver Mmap IO
Linux Boot Process Explained: From Power-On to Kernel
·657 words·4 mins
Linux Boot Kernel
Debian vs Ubuntu vs RHEL: Choosing the Right Linux Distro
·529 words·3 mins
Linux Debian Ubuntu RHEL