Understanding Linux SPI Drivers from Scratch: A Complete Guide

This article explains the Linux SPI driver framework, layering, device‑tree matching, and data‑transfer process, providing clear examples, key APIs, and step‑by‑step instructions to help beginners grasp SPI communication and resolve common development issues.

Deepin Linux
Deepin Linux
Deepin Linux
Understanding Linux SPI Drivers from Scratch: A Complete Guide

In embedded Linux development, SPI is a high‑speed serial bus widely used for Flash, displays, sensors, and ADCs. While many developers know the basic protocol, the Linux SPI driver can seem complex due to its layered architecture, bus matching, and device‑tree configuration.

1. Review of the SPI Bus Protocol

1.1 What is SPI?

SPI (Serial Peripheral Interface) is a full‑duplex, synchronous communication bus developed by Motorola in the 1980s, used to connect microcontrollers with peripherals such as sensors, memory, and displays.

SPI follows a master‑slave model: the master (e.g., a microcontroller) generates the clock (SCK) and initiates transfers, while slaves respond to the clock and exchange data.

The hardware requires only four lines—SCK, MOSI, MISO, and SS/CS—saving pins and simplifying PCB layout.

1.2 SPI Hardware Signals

MISO carries data from slave to master, MOSI carries data from master to slave, SCK provides the timing, and SS/CS selects the target slave.

1.3 SPI Communication Process

Data transfer uses shift registers. For example, when the master sends 0x55 and the slave sends 0xAA, the communication proceeds through four stages:

Chip‑select stage: the master pulls the target SS/CS low.

Clock and data stage: the master generates SCK; on each edge, a bit is shifted out via MOSI and a bit is received via MISO.

Data reception stage: after 8 clock cycles, both sides have exchanged one byte.

End stage: the master releases SS/CS.

2. Linux SPI Driver Architecture

2.1 Layered Design

The driver consists of three layers: the core layer, the master‑controller driver layer, and the device driver layer. The core provides a unified API to device drivers and abstracts the hardware specifics of different master controllers.

The master‑controller driver handles low‑level controller registers and signal generation, while the device driver focuses on a specific peripheral (e.g., a temperature sensor) and uses the core API to communicate.

2.2 Core Layer Functions

Key APIs include: spi_register_driver(struct spi_driver *sdrv): registers a device driver with the kernel. spi_sync(struct spi_device *spi, struct spi_message *msg): performs a synchronous transfer, blocking until completion.

// Example of driver registration
static struct spi_driver spi_sensor_driver = {
    .probe  = sensor_probe,
    .remove = sensor_remove,
    .driver = {
        .name  = "spi_sensor",
        .owner = THIS_MODULE,
    },
};
module_init(sensor_init);
module_exit(sensor_exit);

2.3 Master‑Controller (spi_master) Structure

The spi_master structure describes the controller’s capabilities: bus_num: unique identifier for the controller. num_chipselect: number of chip‑select lines supported. setup: function pointer to configure device parameters (mode, speed, etc.). transfer: function pointer that performs the actual data transfer.

// Define master number and chip‑select count
.bus_num = 0,
.num_chipselect = 4,
.setup = my_spi_setup,
.transfer = my_spi_transfer,

2.4 Device Driver (spi_driver) Essentials

The spi_driver structure links a driver to the core: id_table: array of supported device IDs. probe: called after a successful match to initialize the device.

// Device ID table example
static const struct spi_device_id spi_sensor_ids[] = {
    { "temp_sensor", 0 },
    { "imu_sensor", 1 },
    {},
};
MODULE_DEVICE_TABLE(spi, spi_sensor_ids);

3. Matching Mechanisms

3.1 Device‑Tree Matching

In Device‑Tree‑based systems, each SPI device has a node with a compatible property. The driver provides an of_match_table to match these strings.

spi1: spi@02100000 {
    compatible = "fsl,imx6ul-ecspi", "fsl,imx6q-ecspi";
    reg = <0x02100000 0x4000>;
    status = "okay";
    flash: flash@0 {
        compatible = "micron,n25q128a13ef";
        reg = <0>;
        spi-max-frequency = <50000000>;
    };
};

The driver’s OF table:

static const struct of_device_id my_spi_flash_of_match[] = {
    { .compatible = "micron,n25q128a13ef" },
    {},
};
MODULE_DEVICE_TABLE(of, my_spi_flash_of_match);

3.2 ACPI Matching

On x86 platforms, ACPI IDs (HID/CID) are used. The driver defines an acpi_device_id array.

static const struct acpi_device_id my_spi_acpi_ids[] = {
    { "ACPI0001", 0 },
    {},
};
MODULE_DEVICE_TABLE(acpi, my_spi_acpi_ids);

3.3 Traditional ID Matching

If no Device‑Tree or ACPI is present, the kernel matches the device’s modalias against the driver’s id_table.

4. Practical Example: Temperature Sensor Driver

4.1 Hardware and Kernel Preparation

Verify the four SPI signals (SCK, MOSI, MISO, SS/CS) and power connections. Use a multimeter to check continuity and avoid shorts.

Enable the SPI subsystem and the specific controller in make menuconfig, optionally turning on debugging or DMA support.

4.2 Define ID Tables and OF Match

static const struct spi_device_id my_temp_sensor_ids[] = {
    { "my_temp_sensor", 0 },
    {},
};
MODULE_DEVICE_TABLE(spi, my_temp_sensor_ids);

static const struct of_device_id my_temp_sensor_of_match[] = {
    { .compatible = "vendor,my-temp-sensor" },
    {},
};
MODULE_DEVICE_TABLE(of, my_temp_sensor_of_match);

4.3 Implement Probe and Data Transfer

static int my_temp_sensor_probe(struct spi_device *spi)
{
    struct my_temp_sensor_private *priv;
    priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;
    spi_set_drvdata(spi, priv);

    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 1000000;
    ret = spi_setup(spi);
    if (ret) {
        dev_err(&spi->dev, "spi setup failed: %d
", ret);
        return ret;
    }
    // Additional hardware init here
    return 0;
}

Reading temperature data:

struct spi_transfer tr = {
    .tx_buf = NULL,
    .rx_buf = priv->rx_buffer,
    .len    = sizeof(priv->rx_buffer),
};
struct spi_message msg;
spi_message_init(&msg);
spi_message_add_tail(&tr, &msg);
ret = spi_sync(spi, &msg);
if (ret) {
    dev_err(&spi->dev, "SPI read failed: %d
", ret);
    return ret;
}
uint16_t temp_raw = (priv->rx_buffer[0] << 8) | priv->rx_buffer[1];
float temperature = convert_temp(temp_raw);

4.4 Register Driver and Device‑Tree Node

static struct spi_driver my_temp_sensor_driver = {
    .driver = {
        .name           = "my_temp_sensor_driver",
        .owner          = THIS_MODULE,
        .of_match_table = my_temp_sensor_of_match,
    },
    .id_table = my_temp_sensor_ids,
    .probe    = my_temp_sensor_probe,
    .remove   = my_temp_sensor_remove,
};
module_init(my_temp_sensor_init);
static int __init my_temp_sensor_init(void)
{
    return spi_register_driver(&my_temp_sensor_driver);
}

Device‑Tree snippet for the sensor:

spi1: spi@02100000 {
    compatible = "fsl,imx6ul-ecspi", "fsl,imx6q-ecspi";
    reg = <0x02100000 0x4000>;
    status = "okay";
    temp_sensor: temp_sensor@0 {
        compatible = "vendor,my-temp-sensor";
        reg = <0>;
        spi-max-frequency = <1000000>;
    };
};

4.5 Build, Debug, and Optimize

Makefile example:

obj-m += my_temp_sensor_driver.o
all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

After building, load the module and use dmesg | grep spi to inspect kernel logs. Add printk statements for deeper debugging.

Performance can be improved by selecting appropriate SPI mode and clock, minimizing memory copies, and ensuring proper resource management to avoid leaks and contention.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

LinuxSPIKernel DriverDevice TreeEmbedded Linux
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.