Linux Spidev SPI Ioctl Guide- Free Linux Device Drivers Course

Linux Spidev SPI Ioctl Guide
Free Linux Device Drivers Course — SPI Device Drivers, Part 6
Level: Intermediate
Kernel: 6.x
Reading Time: 14 min

Every kernel-space SPI driver you have written so far in this free Linux kernel development course talks to a physical chip from inside the kernel. But sometimes you don’t want to write a kernel driver at all — you just want a quick, safe way to poke an SPI sensor from a normal Linux application. That is exactly the job of spidev, and the way an application talks to spidev is through a small but powerful spidev ioctl linux kernel interface exposed at /dev/spidevB.D.

In this lecture of our free embedded Linux course we open up that interface, walk through every spidev ioctl command, build a full-duplex transfer with SPI_IOC_MESSAGE(N), and write an original userspace test utility for kernel 6.x. This lecture also closes out the SPI Device Drivers chapter of our free embedded systems course.

free linux kernel development course
free linux device drivers course
free embedded systems course
free embedded linux course
spidev ioctl linux kernel

What You Will Learn

  • Why spidev exists and when to use it instead of a kernel driver
  • How /dev/spidevB.D device nodes are created on modern kernels
  • Every spidev ioctl command: mode, bits per word, speed, bit order
  • struct spi_ioc_transfer field by field
  • Full-duplex chained transfers with SPI_IOC_MESSAGE(N)
  • An original spidev test utility, built and run end to end

Prerequisites

  • SPI bus fundamentals (lddch8_1)
  • spi_driver / spi_device basics (lddch8_2, lddch8_3)
  • Device tree SPI registration (lddch8_4)
  • struct spi_transfer / spi_message (lddch8_5)
  • Comfortable reading and compiling C on Linux

Why Use spidev Instead of a Kernel Driver

Writing a full spi_driver is the right long-term answer for production hardware, but it is slow to iterate on: every change means rebuilding a module and reloading it. spidev gives you a generic character-device driver that binds to an SPI device and forwards raw transfers to userspace. That makes it ideal for:

Use Case Why spidev Fits
Prototyping a new sensor No kernel crash risk; iterate in a normal C or Python program
One-off factory test scripts Fast to write, no module build step
Bring-up before the real driver exists Confirm wiring and register map before writing spi_driver code
Simple microcontroller protocols Protocol still changing often during development

What spidev cannot do is react to interrupts inside the kernel, sit underneath another kernel subsystem such as regmap, or run with hard real-time guarantees — for that you still need the kernel-space spi_driver pattern from the earlier lectures in this chapter.

The /dev/spidevB.D Device Node

Once the spidev driver binds to an SPI device, the kernel creates a character device node named /dev/spidevB.D, where B is the SPI bus (controller) number and D is the chip-select index. Opening that node with open() and closing it with close() behave exactly as you’d expect for a character device.

Device Tree Compatible String Change

On older kernels it was common to see a device tree node with compatible = "spidev"; to force-bind the generic driver. That shortcut is no longer accepted by mainline kernels — spidev must now match a real, specific SPI device name in its ID tables, so production device trees should describe the actual chip instead of asking for spidev directly. Boards used purely for spidev testing typically add the device through an overlay that declares the real compatible string the driver already recognises, or a controller-specific test-only compatible string documented for that board.

Kernel vs Userspace SPI Access

Kernel spi_driver

probe() → spi_sync() → interrupt/IRQ handling → sysfs / other kernel subsystems

Userspace spidev app

open(“/dev/spidev0.0”) → ioctl() config → SPI_IOC_MESSAGE(N) → close()

Understanding spidev Ioctl Commands in Linux Kernel

Every configuration knob you set through struct spi_device in kernel space has a matching spidev ioctl linux kernel request in userspace, defined in linux/spi/spidev.h. Each setting has a paired WR (write/set) and RD (read/get) request.

Ioctl Request Purpose Argument Type
SPI_IOC_WR_MODE / SPI_IOC_RD_MODE SPI clock mode (single byte: CPOL/CPHA bits) __u8
SPI_IOC_WR_MODE32 / SPI_IOC_RD_MODE32 Full 32-bit mode word (adds CS_HIGH, 3WIRE, LOOP, NO_CS, READY flags) __u32
SPI_IOC_WR_LSB_FIRST / SPI_IOC_RD_LSB_FIRST Bit order — 0 means MSB first, non-zero means LSB first __u8
SPI_IOC_WR_BITS_PER_WORD / SPI_IOC_RD_BITS_PER_WORD Word size for the bus, commonly 8 __u8
SPI_IOC_WR_MAX_SPEED_HZ / SPI_IOC_RD_MAX_SPEED_HZ Maximum bus clock speed in Hz __u32

Standard read() and write() calls on the device node also work, but they are strictly half-duplex, and the controller deselects the chip select line between the two calls. That is fine for very simple devices, but most real sensors need composite, full-duplex traffic — which is exactly what the next section covers.

Full-Duplex SPI Transfers Using SPI_IOC_MESSAGE Ioctl

The SPI_IOC_MESSAGE(N) request is the userspace mirror of the kernel’s spi_sync() call you studied in lddch8_5. It accepts an array of N transfer descriptors and executes them back to back, keeping the chip selected across the whole array unless a transfer explicitly asks to toggle it. This lets you build request/response protocols — send a register address, then read the reply — in a single atomic bus operation.

struct spi_ioc_transfer Fields Explained

Field Meaning
tx_buf Pointer (cast to unsigned long) to the bytes to transmit, or 0 for a receive-only transfer
rx_buf Pointer (cast to unsigned long) to the buffer receiving bytes, or 0 for a transmit-only transfer
len Number of bytes in this transfer
speed_hz Per-transfer clock override, 0 to use the device default
delay_usecs Delay inserted after this transfer, before the next one starts
bits_per_word Per-transfer word size override, 0 to use the device default
cs_change Non-zero toggles chip select after this transfer instead of leaving it asserted

This structure is a userspace mirror of the kernel struct spi_transfer, so if you have already worked through lddch8_5 the fields will feel immediately familiar.

Writing a spidev Ioctl Linux Kernel Test Program

Below is an original test utility, ep_spidev_util.c, written for this course. It opens a spidev node, configures mode/speed/bits, and then issues a two-transfer full-duplex message: transfer 1 sends a one-byte “read register” command, transfer 2 clocks out three dummy bytes while capturing the device’s reply.

/* ep_spidev_util.c - original spidev ioctl demo for EmbeddedPathashala */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

#define EP_SPI_DEVICE   "/dev/spidev0.0"
#define EP_READ_REG_CMD 0x9F   /* example "read ID" command byte */

static int ep_configure_spi(int fd)
{
    uint8_t mode        = SPI_MODE_0;
    uint8_t bits        = 8;
    uint32_t speed_hz    = 1000000; /* 1 MHz */

    if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0)
        return -1;
    if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits) < 0)
        return -1;
    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed_hz) < 0)
        return -1;

    return 0;
}

static int ep_read_id(int fd)
{
    uint8_t cmd_buf = EP_READ_REG_CMD;
    uint8_t rx_buf[3] = {0};
    struct spi_ioc_transfer tr[2];

    memset(tr, 0, sizeof(tr));

    tr[0].tx_buf        = (unsigned long)&cmd_buf;
    tr[0].len           = 1;
    tr[0].cs_change     = 1;    /* keep CS asserted into next transfer */
    tr[0].delay_usecs   = 5;
    tr[0].bits_per_word = 8;

    tr[1].rx_buf        = (unsigned long)rx_buf;
    tr[1].len           = sizeof(rx_buf);
    tr[1].bits_per_word = 8;

    if (ioctl(fd, SPI_IOC_MESSAGE(2), tr) < 1)
        return -1;

    printf("Device ID bytes: %02X %02X %02X\n",
           rx_buf[0], rx_buf[1], rx_buf[2]);
    return 0;
}

int main(void)
{
    int fd = open(EP_SPI_DEVICE, O_RDWR);

    if (fd < 0) {
        perror("open spidev");
        return 1;
    }

    if (ep_configure_spi(fd) < 0) {
        perror("configure spi");
        close(fd);
        return 1;
    }

    if (ep_read_id(fd) < 0) {
        perror("SPI_IOC_MESSAGE");
        close(fd);
        return 1;
    }

    close(fd);
    return 0;
}

Building and Running the Utility

$ gcc -Wall -o ep_spidev_util ep_spidev_util.c
$ sudo ./ep_spidev_util
Device ID bytes: EF 40 18

The exact bytes you see depend entirely on the chip wired to that bus and chip-select line — the point of this utility is the ioctl sequence, not the specific register map. If open() fails with ENOENT, the device node does not exist yet — check that a real spidev-compatible device is described for that bus/chip-select in your device tree. If it fails with EACCES, add your user to the group that owns /dev/spidev* or run with elevated privileges during testing.

Common Mistakes When Using spidev Ioctl

Mistake Fix
Forgetting to zero-initialize struct spi_ioc_transfer Always memset() the array first — future kernel fields default to zero this way
Passing pointers directly instead of casting to unsigned long tx_buf/rx_buf are 64-bit fields; always cast through (unsigned long)
Assuming read()/write() are full duplex They are half duplex only; use SPI_IOC_MESSAGE for full duplex
Checking ioctl() return value against 0 SPI_IOC_MESSAGE(N) returns the number of bytes transferred on success
Leaving cs_change unset when chaining related transfers Without cs_change the chip may be deselected between transfers on some controllers

Best Practices for spidev Programming

  • Read back settings with the RD_* ioctls after writing them — not every controller supports every mode combination.
  • Keep transfer buffers as local stack arrays or heap buffers that outlive the ioctl call; never point into freed memory.
  • Use SPI_IOC_MESSAGE for anything that needs the chip select held across multiple transfers.
  • Treat spidev programs as prototypes — plan to migrate stable protocols into a real spi_driver.

Performance Considerations

Every ioctl() call crosses the user/kernel boundary, so batching related transfers into one SPI_IOC_MESSAGE(N) call is significantly cheaper than issuing several separate calls. Also remember that speed_hz is a request, not a guarantee — the controller driver rounds it to the nearest divisor it can actually generate.

Security Considerations

Access to /dev/spidevB.D is access to raw hardware — anything reachable over that bus can be reset, reconfigured, or bricked by a bad transfer. Restrict device node permissions to a dedicated group rather than leaving them world-writable, and never expose spidev access to an untrusted process or container.

Real-World Use Cases

  • Bring-up scripts that verify wiring before the production spi_driver is written
  • Factory calibration tools that talk to ADCs or DACs over SPI
  • Python/C prototyping tools for display or flash-memory experiments

Chapter Recap: SPI Device Drivers From Start to Finish

SPI Device Drivers Chapter, Lectures 1-6

  • lddch8_1: SPI bus fundamentals
  • lddch8_2: spi_device, CPOL/CPHA, spi_driver registration
  • lddch8_3: probe/remove lifecycle, spi_device_id
  • lddch8_4: device tree SPI registration
  • lddch8_5: spi_transfer / spi_message, spi_sync()
  • lddch8_6: spidev, ioctl commands, SPI_IOC_MESSAGE (this lecture)

What’s Next: Regmap API

Writing register-read/register-write boilerplate by hand for every SPI or I2C driver gets repetitive fast. The next chapter of this free Linux kernel development course introduces the Regmap API, which gives you a single, higher-level register access layer that works transparently across SPI, I2C, and memory-mapped devices.

Key Takeaways

  • spidev exposes SPI hardware to userspace through /dev/spidevB.D
  • Every spi_device setting has a matching spidev ioctl linux kernel request
  • SPI_IOC_MESSAGE(N) is the userspace equivalent of kernel spi_sync()
  • Zero-initialize struct spi_ioc_transfer and cast pointers to unsigned long
  • spidev is for prototyping and bring-up, not a replacement for a real spi_driver

Conclusion

spidev closes an important gap between kernel-space SPI drivers and quick, safe experimentation. With the ioctl commands and SPI_IOC_MESSAGE pattern from this lecture, you can bring up new SPI hardware from a plain userspace program before committing to a full spi_driver. That wraps up the SPI Device Drivers chapter of this free embedded Linux course — next we move into the Regmap API to simplify register access across both SPI and I2C.

FAQ

What is spidev in the Linux kernel?

spidev is a generic kernel driver that exposes an SPI device to userspace as a character device node, letting applications configure and transfer data over SPI using ioctl() calls instead of writing a kernel driver.

Where does the spidev device node appear?

As /dev/spidevB.D, where B is the SPI bus/controller number and D is the chip-select index the device is wired to.

Can I still bind spidev using compatible = “spidev” in device tree?

No. Modern kernels require a real, specific SPI device compatible string that spidev’s ID tables recognise; the generic “spidev” compatible string is no longer accepted.

What is the difference between read()/write() and SPI_IOC_MESSAGE on spidev?

read() and write() are half-duplex and deselect the chip between calls. SPI_IOC_MESSAGE(N) executes a chained array of transfers, supports full duplex, and can keep chip select asserted across multiple transfers.

Why does SPI_IOC_MESSAGE need pointers cast to unsigned long?

tx_buf and rx_buf in struct spi_ioc_transfer are fixed-width 64-bit fields so the structure has a stable layout across 32-bit and 64-bit userspace; casting through unsigned long avoids pointer-truncation warnings and bugs.

Is spidev suitable for production drivers?

It works for production tools and utilities, but for hardware that needs interrupts, power management, or integration with other kernel subsystems, a proper spi_driver is the better long-term choice.

What does cs_change do in struct spi_ioc_transfer?

Setting cs_change toggles the chip-select line after that particular transfer instead of leaving it asserted into the next one in the same SPI_IOC_MESSAGE call.

Why did my ioctl(SPI_IOC_MESSAGE…) call return -1 with EACCES?

Your user does not have permission to open or use that device node; check the node’s owning group or run the test program with elevated privileges during development.

Continue the Free Linux Kernel Development Course

Leave a Reply

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