A Practical Guide to SPI Driver Architecture and User-Space Programming in Linux

SPI Core Concepts

SPI (Serial Peripheral Interface) is a synchronous, full-duplex serial communication protocol originally developed by Motorola. It enables MCUs or MPUs to exchange data with peripherals such as flash memory, ADCs, network controllers, or other processors. Key attributes include high-speed operation, serial data transfer, and the ability to transmit and receive simultaneously.

Hardware Characteristics

Wiring Configurations

  • 3-Wire Mode: Uses CLK, CS, and a bidirectional MOSI/MISO line. Communication is half-duplex, with data sent and received in a time-shared manner on a single data pin.
  • 4-Wire Mode: Adds a dedicated MISO line (CLK, CS, MOSI, MISO) to achieve full-duplex communication.
  • Note: On controllers with QSPI support (data lines D0–D7), the mapping typically assigns D0 to MOSI and D1 to MISO when operating in standard SPI mode.

Clock Polarity and Phase

Four operation modes are defined by the combinations of CPOL (clock polarity) and CPHA (clock phase). The specific mode must match the attached peripheral device's requirements.

Driver Architecture (Linux 5.10.165, DesignWare SPI Core)

Key Source Files and Their Roles

spi-dw-mmio.c Parses the device tree node for the SPI controller, registers it as a platform device, and initializes a dw_spi_mmio structure. Resources such as register addresses and clocks are mapped into the embedded dw_spi member defined in spi-dw.h.

Principal structure:

struct dw_spi_mmio {
    struct dw_spi  dws;
    struct clk     *clk;
    struct clk     *pclk;
    void           *priv;
    struct reset_control *rstc;
};

spi-dw-core.c Implements operations on struct dw_spi, manages interrupt handling, and prepares the struct spi_controller *master embedded inside dw_spi.

Principal structure:

struct dw_spi {
    struct spi_controller  *master;
    void __iomem           *regs;
    unsigned long          paddr;
    int                    irq;
    u32                    fifo_len;
    u32                    max_mem_freq;
    u32                    max_freq;
    u32                    caps;
    u32                    reg_io_width;
    u16                    bus_num;
    u16                    num_cs;
    void (*set_cs)(struct spi_device *spi, bool enable);

    void              *tx;
    unsigned int       tx_len;
    void              *rx;
    unsigned int       rx_len;
    u8                 buf[SPI_BUF_SIZE];
    int                dma_mapped;
    u8                 n_bytes;
    irqreturn_t        (*transfer_handler)(struct dw_spi *dws);
    u32                current_freq;
    u32                cur_rx_sample_dly;
    u32                def_rx_sample_dly_ns;
    struct spi_controller_mem_ops mem_ops;

    struct dma_chan    *txchan;
    u32                txburst;
    struct dma_chan    *rxchan;
    u32                rxburst;
    u32                dma_sg_burst;
    unsigned long      dma_chan_busy;
    dma_addr_t         dma_addr;
    const struct dw_spi_dma_ops *dma_ops;
    struct completion  dma_completion;

#ifdef CONFIG_DEBUG_FS
    struct dentry *debugfs;
    struct debugfs_regset32 regset;
#endif
};

spidev.c Provides a generic character device interface for SPI slave devices. The standard file_operations structure maps user-space calls to the SPI bus.

static const struct file_operations spidev_fops = {
    .owner =          THIS_MODULE,
    .write =          spidev_write,
    .read =           spidev_read,
    .unlocked_ioctl = spidev_ioctl,
    .compat_ioctl =   spidev_compat_ioctl,
    .open =           spidev_open,
    .release =        spidev_release,
    .llseek =         no_llseek,
};

spi.c The SPI subsystem core. It handles controller and device registration, manages message queues, and provides allocation routines for spi_controller and spi_device.

Core Data Structures

Representation of an SPI controller:

struct spi_controller {
    struct device dev;
    struct list_head list;
    s16          bus_num;
    u16          num_chipselect;
    /* ... hook functions ... */
};

Representation of an SPI slave device:

struct spi_device {
    struct device          dev;
    struct spi_controller  *controller;
    struct spi_controller  *master;
    u32                    max_speed_hz;
    u8                     chip_select;
    u8                     bits_per_word;
    bool                   rt;
    u32                    mode;
#define SPI_CPHA       0x01
#define SPI_CPOL       0x02
#define SPI_MODE_0      (0|0)
#define SPI_MODE_1      (0|SPI_CPHA)
#define SPI_MODE_2      (SPI_CPOL|0)
#define SPI_MODE_3      (SPI_CPOL|SPI_CPHA)
    /* ... additional mode flags and fields ... */
    int            irq;
    void           *controller_state;
    void           *controller_data;
    char           modalias[SPI_NAME_SIZE];
    const char     *driver_override;
    int            cs_gpio;
    struct gpio_desc *cs_gpiod;
    struct spi_delay word_delay;
    struct spi_statistics statistics;
};

Representation of an SPI transaction message:

struct spi_message {
    struct list_head  transfers;
    struct spi_device *spi;
    unsigned          is_dma_mapped:1;
    void              (*complete)(void *context);
    void              *context;
    unsigned          frame_length;
    unsigned          actual_length;
    int               status;
    struct list_head  queue;
    void              *state;
    struct list_head  resources;
};

Initialization Flow Overview

The kernel boots and the SPI controller's platform driver (dw-mmio) probes the hardware, allocates and initializes a dw_spi and its master spi_controller. Afterwards, child devices described in the device tree are instantiated as spi_device structures and bound to protocol drivers like spidev.

User-Space Programming

Transfer Structure spi_ioc_transfer

struct spi_ioc_transfer {
    __u64   tx_buf;
    __u64   rx_buf;
    __u32   len;
    __u32   speed_hz;
    __u16   delay_usecs;
    __u8    bits_per_word;
    __u8    cs_change;
    __u8    tx_nbits;
    __u8    rx_nbits;
    __u8    word_delay_usecs;
    __u8    pad;
};

Step-by-Step API Usage

Open the device node to the specific SPI bus and chip select:

int fd = open("/dev/spidev0.0", O_RDWR);
if (fd < 0) {
    perror("open");
    return -1;
}

Configure mode, bits per word, and maximum speed using ioctl:

uint8_t mode = SPI_MODE_0;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_RD_MODE, &mode);

uint8_t bpw = 8;
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bpw);
ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bpw);

uint32_t speed = 1000000; /* 1 MHz */
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);

Perform a read operation from a peripheral register:

uint8_t read_register(int fd, uint8_t reg_addr) {
    uint8_t out_buf[3] = {0x03, reg_addr, 0x00}; /* read cmd, addr, dummy */
    uint8_t in_buf[3] = {0};

    struct spi_ioc_transfer xfer = {
        .tx_buf = (unsigned long)out_buf,
        .rx_buf = (unsigned long)in_buf,
        .len = 3,
    };

    int status = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
    if (status >= 3) {
        return in_buf[2];
    }
    return 0xFF; /* error */
}

Perform a write operation to a peripheral register:

int write_register(int fd, uint8_t reg_addr, uint8_t value) {
    uint8_t out_buf[3] = {0x02, reg_addr, value}; /* write cmd, addr, data */

    struct spi_ioc_transfer xfer = {
        .tx_buf = (unsigned long)out_buf,
        .len = 3,
    };

    int status = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
    if (status >= 3) {
        return 0; /* success */
    }
    return -1; /* error */
}

Tags: Linux SPI driver kernel spidev

Posted on Fri, 08 May 2026 18:50:31 +0000 by A584537