SPI Driver Architecture Explained – free linux device drivers tutorial

SPI Driver Architecture Explained – free linux device drivers tutorial
Free Linux device driver Development Course — Chapter 8, Part 2

In Part 1 of this free embedded systems course we covered SPI wiring, signal names, and topology. Now we go inside the kernel and build a real spi driver linux kernel module from scratch. You will learn the two core data structures every SPI driver is built around, how SPI clock modes actually work at the bit level, and how to register a modern driver on kernel 6.x using an original example — not copy-pasted textbook code.

spi driver linux kernel
spi_device spi_driver structure
free linux kernel development course
CPOL CPHA spi mode
free linux device drivers course

What You Will Learn

  • The fields inside struct spi_device and what each one means to your driver
  • The modern struct spi_driver registration model, including the void-returning remove() callback
  • How CPOL and CPHA combine to form the four SPI clock modes
  • How to move data with spi_sync(), spi_message, and spi_transfer
  • A complete, original spi driver linux kernel example you can build and load today

Prerequisites

  • Part 1 of this chapter (SPI bus fundamentals, wiring, chip select)
  • Comfort writing and building an out-of-tree kernel module (Makefile, insmod, dmesg)
  • A Linux system running kernel 6.x with kernel headers installed, and an SPI controller enabled (a Raspberry Pi with spi-bcm2835 or a similar board works well)

The struct spi_device: One Slave On The Bus

Every SPI peripheral your spi driver linux kernel module talks to is represented internally by an instance of struct spi_device, created for you automatically once the SPI core matches your driver against a real device (usually declared in the device tree). You never allocate this structure yourself in a client driver; you simply read the fields you need inside probe().

Field Meaning
controller The SPI controller (bus master) this device is wired to. Older code calls this field “master”; modern kernels use “controller” as the primary name.
max_speed_hz The maximum clock rate this specific chip supports. A driver can lower it per transfer but should never exceed this value.
chip_select Which chip-select line on the controller this device answers to. Active low by default.
mode Clocking behaviour flags — SPI mode (CPOL/CPHA), bit order, and chip-select polarity overrides.
bits_per_word How many bits make up one transferred word, almost always 8 for byte-oriented peripherals.
irq Interrupt line associated with this device, if the chip can signal the host asynchronously.

SPI Clock Modes: CPOL And CPHA

Getting the SPI mode wrong is the number one reason a brand-new spi driver linux kernel bring-up silently returns garbage data. The mode is built from two independent settings:

  • CPOL (Clock Polarity) — whether the clock idles low (0) or idles high (1) when no data is being transferred
  • CPHA (Clock Phase) — whether data is sampled on the first clock edge or the second clock edge of each bit period
Mode CPOL CPHA Kernel Macro Behaviour
0 0 0 SPI_MODE_0 Clock idles low, data sampled on the rising edge
1 0 1 SPI_MODE_1 Clock idles low, data sampled on the falling edge
2 1 0 SPI_MODE_2 Clock idles high, data sampled on the falling edge
3 1 1 SPI_MODE_3 Clock idles high, data sampled on the rising edge

The diagram below shows how the clock line’s idle state and active sampling edge change across the four modes, without any data bits drawn — just the concept of where the clock starts and which edge matters.

SPI Mode Clock Behaviour At A Glance
Mode 0
Clock idle: LOW
Sample on: rising edge
Mode 1
Clock idle: LOW
Sample on: falling edge
Mode 2
Clock idle: HIGH
Sample on: falling edge
Mode 3
Clock idle: HIGH
Sample on: rising edge

In practice, the overwhelming majority of real hardware you will meet uses SPI_MODE_0 or SPI_MODE_3. Always confirm the exact mode from the chip’s datasheet timing diagram rather than guessing.

The struct spi_driver: Registering Your Driver

Where spi_device represents one physical chip, struct spi_driver represents your driver’s registration with the SPI core — the probe/remove pair, the matching table, and the embedded struct device_driver. On modern kernels (5.18 and newer), the remove() callback is void-returning, matching the same modernization already applied to platform and I2C drivers in our earlier lectures.

Field Purpose
driver.name / driver.of_match_table Driver name and device tree compatible-string matching table
id_table Optional legacy spi_device_id table for non-device-tree matching
probe Called once the SPI core matches a real spi_device to this driver
remove Called on driver unbind; returns void on modern kernels, no error code expected

Original Example: ep_spi_temp_demo Driver

The example below is an original driver written for this course, modeling a fictional SPI temperature sensor that exposes an 8-bit register at address 0x00. It demonstrates the modern registration pattern, half-duplex register read using spi_write_then_read(), and a full-duplex transfer using spi_sync() with two chained spi_transfer descriptors.

// ep_spi_temp_demo.c
// Original example driver for EmbeddedPathashala - free linux kernel development course
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/of.h>

#define EP_TEMP_REG_ADDR   0x00

struct ep_spi_temp {
    struct spi_device *spi;
};

/* Half-duplex style read: write the register address, then read the value */
static int ep_temp_read_reg(struct spi_device *spi, u8 reg, u8 *value)
{
    u8 tx = reg;
    return spi_write_then_read(spi, &tx, 1, value, 1);
}

/* Full-duplex style transfer: MOSI and MISO active in the same spi_sync call */
static int ep_temp_full_duplex_demo(struct spi_device *spi)
{
    u8 tx_buf[2] = { EP_TEMP_REG_ADDR, 0x00 };
    u8 rx_buf[2] = { 0 };
    struct spi_transfer xfer = {
        .tx_buf = tx_buf,
        .rx_buf = rx_buf,
        .len = 2,
    };
    struct spi_message msg;

    spi_message_init(&msg);
    spi_message_add_tail(&xfer, &msg);

    return spi_sync(spi, &msg);
}

static int ep_spi_temp_probe(struct spi_device *spi)
{
    struct ep_spi_temp *priv;
    u8 temp_val;
    int ret;

    priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->spi = spi;
    spi_set_drvdata(spi, priv);

    /* Configure clocking before first transfer */
    spi->mode = SPI_MODE_0;
    spi->bits_per_word = 8;
    ret = spi_setup(spi);
    if (ret)
        return ret;

    ret = ep_temp_read_reg(spi, EP_TEMP_REG_ADDR, &temp_val);
    if (ret)
        return ret;

    dev_info(&spi->dev, "ep_spi_temp_demo: register value = 0x%02x\n", temp_val);

    ret = ep_temp_full_duplex_demo(spi);
    if (ret)
        dev_warn(&spi->dev, "full duplex demo transfer failed: %d\n", ret);

    return 0;
}

/* Modern SPI remove callback returns void on kernel 5.18+ */
static void ep_spi_temp_remove(struct spi_device *spi)
{
    dev_info(&spi->dev, "ep_spi_temp_demo: driver removed\n");
}

static const struct of_device_id ep_spi_temp_of_match[] = {
    { .compatible = "ep,spi-temp-demo" },
    { }
};
MODULE_DEVICE_TABLE(of, ep_spi_temp_of_match);

static struct spi_driver ep_spi_temp_driver = {
    .driver = {
        .name = "ep_spi_temp_demo",
        .of_match_table = ep_spi_temp_of_match,
    },
    .probe = ep_spi_temp_probe,
    .remove = ep_spi_temp_remove,
};
module_spi_driver(ep_spi_temp_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Original example SPI device driver for the free linux kernel development course");

A few points worth calling out about this spi driver linux kernel example:

  • spi_write_then_read() is a small convenience wrapper the kernel provides specifically for the write-then-read register pattern so common in SPI peripherals — you do not need to build a spi_message by hand for this case.
  • spi_sync() combined with a manually built spi_message/spi_transfer pair is the general-purpose way to run any custom transfer, including full-duplex transfers where tx_buf and rx_buf are both active on the same spi_transfer.
  • module_spi_driver() is the same boilerplate-eliminating macro pattern you already saw for platform and I2C drivers — it generates module_init()/module_exit() for you.
  • The remove callback returns void, not int, matching the kernel-wide modernization merged for SPI drivers.

Build And Test Workflow

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_spi_temp_demo.ko
$ dmesg | tail -5
[  102.441823] ep_spi_temp_demo: register value = 0x1a
$ sudo rmmod ep_spi_temp_demo
$ dmesg | tail -2
[  118.220110] ep_spi_temp_demo: driver removed

The exact register value printed depends entirely on your target hardware — on real silicon it will reflect whatever the chip actually returns for that register address.

Common Mistakes And Troubleshooting

Symptom Likely Cause Fix
probe() never called Device tree node compatible string does not match of_match_table Double-check the compatible string spelling on both sides
spi_sync() returns -ETIMEDOUT or garbage rx data Wrong SPI mode set for the chip Re-check CPOL/CPHA against the datasheet timing diagram and set spi->mode before spi_setup()
Build fails on remove() signature Older textbook code still declares an int-returning remove() Change the callback to return void, matching kernel 5.18 and newer
Random corrupted reads under load max_speed_hz set higher than the chip actually supports Lower max_speed_hz and retest incrementally

Best Practices, Performance, And Security Notes

  • Always call spi_setup() after changing spi->mode, bits_per_word, or max_speed_hz, so the controller driver reconfigures its hardware registers
  • Prefer devm_kzalloc() for driver-private memory so cleanup happens automatically on remove
  • Batch related register reads/writes into one spi_message with multiple spi_transfer entries instead of issuing many separate spi_sync() calls, which reduces per-transfer overhead
  • Validate any buffer length coming from user space before it reaches a spi_transfer, especially if you later expose this data through a character device or spidev-style interface

Summary And Key Takeaways

  • struct spi_device describes one physical peripheral; struct spi_driver describes your driver’s registration and probe/remove logic
  • SPI mode is built from CPOL and CPHA, giving four possible modes, with SPI_MODE_0 and SPI_MODE_3 being the most common in real hardware
  • spi_write_then_read() is the convenient half-duplex helper; spi_sync() with spi_message/spi_transfer is the general-purpose transfer API and supports full-duplex operation
  • The remove() callback is void-returning on modern kernels (5.18+), consistent with the platform and I2C driver modernization covered earlier in this free linux kernel development course

Frequently Asked Questions

What is the difference between spi_device and spi_driver?

struct spi_device represents one physical chip on the bus, created automatically by the kernel. struct spi_driver represents your code — the probe/remove callbacks and matching table that get bound to a matching spi_device.

Which SPI mode should I use if the datasheet does not specify one?

Try SPI_MODE_0 first, since it is the most common default across SPI peripherals. If reads look corrupted, SPI_MODE_3 is the next most likely candidate.

Do I need spi_setup() every time I send a transfer?

No. Call spi_setup() once after changing mode, bits_per_word, or max_speed_hz — typically during probe() — not before every single transfer.

Can one spi_message contain more than one spi_transfer?

Yes. This is exactly how half-duplex write-then-read sequences and complex multi-phase protocols are built — chain multiple spi_transfer structures onto the same spi_message with spi_message_add_tail().

Is spi_write_then_read() safe to use with large buffers?

It is intended for small register-style reads. For larger bulk transfers, build your own spi_message with appropriately sized spi_transfer buffers instead.

Why does my remove() function fail to compile on kernel 6.x?

Kernel 5.18 and newer expect spi_driver.remove() to return void. Older textbook and tutorial code that still returns int will fail to compile against modern kernel headers.

What comes next in this SPI chapter?

Upcoming lectures in this free linux device drivers course cover declaring SPI devices through the device tree and accessing SPI hardware directly from user space.

More Free Linux Kernel Lectures

Continue building your embedded Linux skills with more free lectures from EmbeddedPathashala.

Leave a Reply

Your email address will not be published. Required fields are marked *