Linux SPI Message And Transfer- Free Linux Device Drivers Course

PREV_LEC  |  NEXT_LEC

SPI Device Driver Basics – free linux device driver tutorial

SPI Driver Architecture Explained – free linux device drivers tutorial

SPI Driver Probe and Remove Free Linux Device Drivers Course

SPI Device Tree Registration Guide – free linux device drivers course

Please complete all above lectures and continue this lecture
Linux SPI Message And Transfer
Free Linux Device Drivers Course — SPI Chapter, Lecture 5
Kernel 6.x Ready
100% Free
Hands-On Driver Code

spi message transfer linux kernel
free linux device drivers course
free embedded systems course
free linux kernel development course
free linux development course

Every byte that moves across an SPI bus in Linux travels inside two small but powerful
data structures: struct spi_transfer and struct spi_message.
Understanding spi message transfer linux kernel programming is the single
most important skill for writing real-world SPI client drivers, because almost every
sensor, flash chip, or display needs more than one transfer to complete a single logical
operation — for example, “send a register address” followed by “read back the value.”
In this free Linux device drivers course lecture, we break the SPI message pipeline down
into simple, practical steps and build an original hands-on driver on kernel 6.x.

What You Will Learn

  • The SPI I/O model: messages made of transfers
  • struct spi_transfer field by field
  • struct spi_message field by field (kernel 6.x)
  • spi_message_init() and spi_message_add_tail()
  • spi_sync() vs spi_async()
  • Modern helper: spi_sync_transfer()
  • Building a two-transfer register read driver
  • Common mistakes and best practices

Prerequisites

  • Completed lddch8_1 to lddch8_4 (SPI basics, spi_driver, probe/remove, device tree registration)
  • Comfortable with basic C and Linux kernel modules
  • A Linux kernel build environment (kernel 6.x headers) or a Raspberry Pi / BeagleBone with SPI enabled

Why SPI Message Transfer Linux Kernel Design Uses Two Layers

The SPI subsystem does not let a driver just “write some bytes.” Instead, it separates the job
into two layers so that the same API works whether the underlying controller is a simple
bit-banged GPIO bus or a DMA-driven hardware SPI block:

  • struct spi_transfer — one single full-duplex data movement: a buffer to send, a buffer to receive, and how many bytes.
  • struct spi_message — an atomic container that queues one or more transfers together. The bus stays reserved for your device until every transfer in the message finishes.
SPI Message Containing Multiple Transfers
struct spi_message
Transfer 1
write reg addr
Transfer 2
read data
Transfer 3
optional

The chip-select line stays active across all transfers unless cs_change tells it otherwise.

struct spi_transfer — Field by Field

Field Purpose
tx_buf Bytes to send out on MOSI. NULL for a read-only transfer.
rx_buf Buffer to receive bytes from MISO. NULL for a write-only transfer.
len Number of bytes moved in this transfer. tx_buf and rx_buf, if both used, must match this length.
speed_hz Overrides spi_device.max_speed_hz for just this transfer. 0 keeps the device default.
bits_per_word Overrides spi_device.bits_per_word for just this transfer. 0 keeps the device default.
cs_change If set on a non-final transfer, briefly deselects the chip mid-message; on the final transfer it can keep the chip selected into the next message.
delay_usecs Microsecond delay after this transfer, before the chip-select state changes or the next transfer starts.
error Kernel 6.x addition: reports SPI_TRANS_FAIL_NO_START or SPI_TRANS_FAIL_IO if this specific transfer failed inside a larger message.

Note: Always zero-initialize a spi_transfer (for example with memset or C99 designated initializers) before filling in the fields you need — unset fields must be zero for forward compatibility with newer kernels.

struct spi_message — Field by Field

Field Purpose
transfers Internal linked list of every spi_transfer queued into this message.
spi Points back to the spi_device this message targets.
complete / context Callback function and its argument, invoked automatically when an asynchronous message finishes.
frame_length Total byte count across all transfers, filled in automatically by the core.
actual_length Bytes actually transferred across all successful transfers.
status 0 on success, negative errno on failure.
prepared / optimized / pre_optimized Kernel 6.x internal state flags used by the SPI core’s message-optimization path; drivers should treat these as read-only.

Modernization note: older kernels exposed a raw is_dma_mapped flag for
manual DMA buffer mapping. On current kernel 6.x trees the core prefers scatter-gather DMA
handled through internal tx_sg/rx_sg tables, and message-level
optimization (spi_optimize_message()) is managed by the SPI core rather than
by individual drivers. As a driver author writing a client driver, you rarely need to touch
DMA mapping directly — the core and the controller driver handle it for you.

Building and Submitting an SPI Message

Every SPI transaction, from the simplest to the most complex, follows the same three steps:

  1. Initialize the message with spi_message_init(&msg).
  2. Add each transfer to the message tail with spi_message_add_tail(&xfer, &msg).
  3. Submit the message with either spi_sync() or spi_async().

spi_sync() vs spi_async() Comparison

Function Context Blocks Caller? When to Use
spi_sync() Sleepable (process) context only Yes, until the message completes Simple probe-time reads, ioctl handlers, anywhere sleeping is fine
spi_async() Can be used from atomic/interrupt context No, returns immediately High-throughput drivers, IRQ handlers, pipelines that chain transfers via the complete() callback

Internally, spi_sync() is simply a thin wrapper around spi_async() that
waits on a completion, so you get the same underlying queueing and controller behaviour either way.

Modern Convenience: spi_sync_transfer()

Writing spi_message_init() + spi_message_add_tail() by hand for every
transaction is repetitive. Kernel 6.x provides a shortcut, spi_sync_transfer(), that
takes an array of spi_transfer structures directly and builds/submits the message
for you in one call:

int spi_sync_transfer(struct spi_device *spi,
                       struct spi_transfer *xfers,
                       unsigned int num_xfers);

Use this whenever you have a fixed, known array of transfers and don’t need a custom completion
callback — it removes boilerplate without changing behaviour.

Hands-On: ep_spi_msg_demo Driver

Let’s build an original driver, ep_spi_msg_demo, that performs the most common
real-world SPI pattern: write a one-byte register address, then read back one byte of data,
as a single atomic spi_message with two transfers.

Device Tree Node

&spi0 {
    status = "okay";

    msgdemo@0 {
        compatible = "ep,spi-msg-demo";
        reg = <0>;
        spi-max-frequency = <1000000>;
    };
};

Driver Source

// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/mutex.h>
#include <linux/mod_devicetable.h>
#include <linux/sysfs.h>

#define EP_REG_ID   0x00

struct ep_spi_msg_priv {
    struct spi_device *spi;
    struct mutex lock;
};

/* Two-transfer register read: send reg addr, then read one byte back */
static int ep_spi_read_reg(struct ep_spi_msg_priv *priv, u8 reg, u8 *val)
{
    struct spi_transfer xfer[2] = { 0 };
    int ret;

    xfer[0].tx_buf = &reg;
    xfer[0].len    = 1;

    xfer[1].rx_buf = val;
    xfer[1].len    = 1;

    mutex_lock(&priv->lock);
    ret = spi_sync_transfer(priv->spi, xfer, ARRAY_SIZE(xfer));
    mutex_unlock(&priv->lock);

    return ret;
}

static ssize_t value_show(struct device *dev,
                           struct device_attribute *attr, char *buf)
{
    struct ep_spi_msg_priv *priv = dev_get_drvdata(dev);
    u8 val = 0;
    int ret;

    ret = ep_spi_read_reg(priv, EP_REG_ID, &val);
    if (ret)
        return ret;

    return sysfs_emit(buf, "0x%02x\n", val);
}
static DEVICE_ATTR_RO(value);

static struct attribute *ep_spi_msg_attrs[] = {
    &dev_attr_value.attr,
    NULL,
};
ATTRIBUTE_GROUPS(ep_spi_msg);

static int ep_spi_msg_probe(struct spi_device *spi)
{
    struct ep_spi_msg_priv *priv;

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

    priv->spi = spi;
    mutex_init(&priv->lock);
    spi_set_drvdata(spi, priv);

    dev_info(&spi->dev, "ep_spi_msg_demo probed on chip select %u\n",
              spi_get_chipselect(spi, 0));

    return 0;
}

static void ep_spi_msg_remove(struct spi_device *spi)
{
    dev_info(&spi->dev, "ep_spi_msg_demo removed\n");
}

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

static struct spi_driver ep_spi_msg_driver = {
    .driver = {
        .name = "ep_spi_msg_demo",
        .of_match_table = ep_spi_msg_of_match,
        .dev_groups = ep_spi_msg_groups,
    },
    .probe  = ep_spi_msg_probe,
    .remove = ep_spi_msg_remove,
};
module_spi_driver(ep_spi_msg_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("SPI multi-transfer message demo driver");

Build and Test Workflow

$ make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
$ sudo insmod ep_spi_msg_demo.ko
$ dmesg | tail -3
[  102.334112] ep_spi_msg_demo spi0.0: ep_spi_msg_demo probed on chip select 0

$ cat /sys/bus/spi/devices/spi0.0/value
0x1a

Every read of value triggers a fresh two-transfer SPI message: the register
address goes out on transfer 1, and the device’s reply comes back on transfer 2, with the
chip-select line held active across both because cs_change was left at its
default (0).

Common Mistakes in SPI Message Transfer Linux Kernel Code

Mistake Consequence Fix
Forgetting to zero-initialize spi_transfer Garbage stack values leak into speed_hz/bits_per_word, causing intermittent bus errors Use = { 0 } or memset before filling fields
Calling spi_sync() from an interrupt handler Kernel warning / possible sleep-in-atomic-context bug Use spi_async() with a completion callback instead
Mismatched tx_buf/rx_buf lengths Buffer overrun or truncated data Always size rx_buf and tx_buf to exactly len bytes
Reusing a spi_message while a previous submission is still pending Data corruption, use-after-free style bugs Wait for completion (spi_sync) or the complete() callback (spi_async) before reuse
Ignoring the returned status/error fields Silent partial failures go unnoticed Always check spi_sync()’s return value and message->status

Best Practices

  • Prefer spi_sync_transfer() or spi_write_then_read() for simple fixed-size transactions.
  • Protect multi-step register access sequences with a mutex, as shown in ep_spi_msg_demo.
  • Keep transfer buffers DMA-safe (kmalloc’d or devm_kzalloc’d, never stack buffers, if the controller may use DMA).
  • Set cs_change only when the device datasheet explicitly requires a mid-message chip-select toggle.

Performance Considerations

Every call to spi_sync() blocks the calling thread until the controller driver finishes the
entire message, including any inter-transfer delay_usecs. For high-frequency polling drivers,
batching multiple register operations into a single spi_message (as we did here) is far more
efficient than issuing separate spi_sync() calls per register, because it avoids repeated
bus arbitration and chip-select toggling overhead.

Security Considerations

Never trust a transfer length that originates from userspace (for example through an ioctl or
spidev) without validating it against your buffer size first. An oversized len value can drive
the controller to read or write past the end of a kernel buffer. Always clamp or reject
out-of-range lengths before building the spi_transfer.

Summary and Key Takeaways

  • spi_transfer describes one full-duplex data movement; spi_message atomically groups one or more transfers.
  • spi_message_init() + spi_message_add_tail() build a message manually; spi_sync_transfer() does it in one call for simple cases.
  • spi_sync() blocks and sleeps; spi_async() is non-blocking and callback-driven, safe in atomic context.
  • Kernel 6.x tracks per-transfer errors via the error field and manages DMA scatter-gather internally.
  • Always zero-initialize transfers and validate buffer lengths for safety.

Conclusion

Mastering spi message transfer linux kernel programming turns SPI driver-writing from guesswork
into a predictable, three-step recipe: build transfers, queue them into a message, submit the
message. Once this pattern clicks, every SPI sensor, flash chip, or display driver you write
afterward becomes a variation on the same theme. In the next lecture of this free Linux device
drivers course, we move from kernel-space messages to userspace SPI access through the spidev
interface.

Frequently Asked Questions

What is the difference between spi_transfer and spi_message in the Linux kernel?

spi_transfer represents one full-duplex data movement (a tx/rx buffer pair), while spi_message is a container that atomically groups one or more spi_transfer structures into a single bus transaction.

When should I use spi_sync() instead of spi_async()?

Use spi_sync() in process context when it’s acceptable for your driver to sleep until the transfer completes, such as during probe() or in a sysfs read. Use spi_async() when you need a non-blocking call or are in atomic/interrupt context.

What does cs_change do in struct spi_transfer?

cs_change controls chip-select behaviour around a transfer. On a non-final transfer it can briefly deselect the chip mid-message; on the final transfer it can keep the chip selected going into the next message, which is a performance hint on multi-device buses.

Is spi_sync_transfer() available on all kernel versions?

spi_sync_transfer() is a modern convenience helper present in current kernel 6.x trees. It wraps spi_message_init_with_transfers() and spi_sync() so you can submit an array of transfers in a single call.

Can I reuse the same spi_message for multiple transactions?

Only after the previous submission has fully completed. Reusing a message or its transfers while a prior spi_sync()/spi_async() call is still in flight can corrupt data or crash the kernel.

Do I need to manage DMA mapping manually for SPI transfers?

Generally no. On kernel 6.x the SPI core and controller driver handle scatter-gather DMA mapping internally for most client drivers; you only need to ensure your buffers are DMA-safe kernel memory.

What replaced struct spi_master in modern kernels?

struct spi_master was renamed to struct spi_controller, and spi->chip_select is now accessed through the spi_get_chipselect() accessor to support multi chip-select devices, as covered in lddch8_4.

Why does my SPI driver hang when I call spi_sync() from an interrupt handler?

spi_sync() can sleep while waiting for the transfer to complete, and sleeping is not allowed in atomic/interrupt context. Use spi_async() with a completion callback instead.

 

Continue the Free Linux Device Drivers Course

Next up: userspace SPI access through spidev.

PREV_LEC  |  NEXT_LEC

Leave a Reply

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