Biko's House of Horrors

spidev

So you want to write some C/C++ code to interface with an SPI device on Linux, but don't know where to start? Look no further.

Since I didn't find a readable explanation of the Linux userspace API for the SPI bus driver (or, more likely, I wasn't searching hard enough), I decided to write my own.

I'm assuming basic familiarity with the SPI protocol here. If you're not familiar with it, go read the Wikipedia article — it does a pretty good job of explaining the fundamentals.

The device model

Like most everything on Linux, SPI devices are represented as files. Specifically:

$ ls -l /dev/spi*
crw-rw---- 1 root spi 153, 0 Apr 12 10:31 /dev/spidev0.0
crw-rw---- 1 root spi 153, 1 Apr 12 10:31 /dev/spidev0.1

Each spidevX.Y file represents a single SPI target device: X is the bus number (in this case we have just one bus), and Y is an "address" of a device on the bus. Since in SPI devices are addressed using a dedicated chip-select (CS) line, each "address" is a unique CS line on the board.

When communicating with the target device, the spidev driver (which implements the SPI protocol in Linux and exposes the aforementioned spidev* files) will assert and de-assert the CS lines as necessary. But we'll get back to that.

Simple I/O

Okay, so if we have files, can we read from them or write to them? We sure can.

For example:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void demo(void)
{
  int     device      = -1;
  char    to_write[]  = "Hello, world!";

  device = open("/dev/spidev0.0", O_RDWR);

  write(device, to_write, sizeof(to_write) - sizeof(char));
}

(Error checking and resource management omitted for brevity.)

This code will write the string "Hello, world!" to the first device on the first SPI bus. Specifically, this will:

  1. Assert the CS line for the device.
  2. Send the provided bytes on the MOSI line, while ignoring the data received on MISO.
  3. De-assert the CS line.

Similarly, we can read from the device:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void demo(void)
{
  int           device      = -1;
  unsigned char received[5] = {0};

  device = open("/dev/spidev0.0", O_RDWR);

  read(device, received, sizeof(received));
}

But wait! You can't just read from an SPI device! During an SPI transfer the controller simultaneously reads and writes bits on the bus. So to read 5 bytes we must also send 5 bytes.1 What read actually does is this:

  1. Assert the CS line.
  2. Send a zero byte for each byte that should be read from MISO, and put the byte from MISO in the output buffer.
  3. De-assert the CS line.

SPI parameters

What if we want to change the SPI mode? Easy:

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>

void demo(void)
{
  int     device  = -1;
  __u8    mode    = SPI_MODE_2;

  device = open("/dev/spidev0.0", O_RDWR);

  ioctl(device, SPI_IOC_WR_MODE, &mode);
}

We can also get the current mode using SPI_IOC_RD_MODE.

Here's a table describing all the available parameters:

SPI_IOC_(RD/WR)_%s Description Type
MODE SPI mode __u8
MODE32 SPI mode __u32
LSB_FIRST Non-zero to transmit words is LSB-first order __u8
BITS_PER_WORD Number of bits in an SPI transfer word __u8
MAX_SPEED_HZ Maximum clock speed in Hz (actual clock may be slower) __u32

Complex I/O

Okay, so we can read and write. But what if we want to do something more complex? For example, suppose we have a sensor and we want to send it a command and read the response. What we want to do is this:

  1. Assert the CS pin.
  2. Send the command while ignoring any output from the device (it is undefined, since we haven't finished sending the command yet).
  3. Send zeroes (or anything, really) while reading the output from the device (this is the command's response).
  4. De-assert the CS pin.

We could use read and write, but they toggle the CS pin, which might confuse the target device.

Luckily, there is a way to do this:

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>

void demo(void)
{
  int                     device        = -1;
  unsigned char           command[]     = { 0x13, 0x37 };
  unsigned char           response[2]   = { 0 };
  struct spi_ioc_transfer xfers[]       = {
    {
      .tx_buf = (__u64)&command[0],
      .len = sizeof(command)
    },
    {
      .rx_buf = (__u64)&response[0],
      .len = sizeof(response)
    }
  };

  device = open("/dev/spidev0.0", O_RDWR);

  ioctl(device, SPI_IOC_MESSAGE(2), xfers);
}

Okay, so what's going on here?

We have an array of spi_ioc_transfer structures. Here's the structure definition from spidev.h:

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;
};

The structure defines a transfer of len bytes: len bytes will be written on the MOSI line from the tx_buf buffer, and len bytes will be read from MISO to the rx_buf buffer. If tx_buf is NULL, then zeroes will be written instead. If rx_buf is NULL, the bytes read from MISO are ignored.

In our example, we are first writing 2 bytes and ignoring the MISO line, then writing 2 zeroes and reading from MISO. Here's what this looks like on the wire:

Saleae logic analyzer capture of the transfer

Crucially, during both transfers, the CS pin remains asserted, which is exactly what we wanted.

You may also notice that the spi_ioc_transfer struct has many more fields. You can read all about them the spidev.h header. For instance, speed_hz allows overriding the clock speed for a single transfer.

The cs_change field, though, is of particular interest.

Good news about CS

Okay, so we talked about how the spidev driver manages the CS line for us automatically. But what if we want more fine-grained control?

This is where cs_change comes into play. When it is set to a non-zero value, spidev changes the behaviour of the CS pin after the transfer is completed.

Unfortunately, the documentation for this field in spidev.h is wrong. The correct documentation is in the kernel spi.h header:

All SPI transfers start with the relevant chipselect active. Normally it stays selected until after the last transfer in a message. Drivers can affect the chipselect signal using cs_change.

  1. If the transfer isn't the last one in the message, this flag is used to make the chipselect briefly go inactive in the middle of the message. ...
  2. When the transfer is the last one in the message, the chip may stay selected until the next transfer. ...

In other words:

  1. If cs_change is non-zero in any but the last transfer, then before beginning the next transfer the driver will de-assert CS, and then assert it again.
  2. If cs_change is non-zero in the last transfer, then CS will not be de-asserted after the transfer ends.

The first case is useful if we want to send a batch of messages in a single ioctl call.

The second case is useful if we need to send a series of messages to a device, with later messages being dependent on responses to earlier messages, without de-asserting the CS pin between messages.

Bad news about CS

It's cool that spidev manages the CS line for us, but what if we have 20 devices we need to control and only 2 dedicated CS lines on the board?

You might be tempted to use the SPI_NO_CS flag as an argument to SPI_IOC_WR_MODE. However, people on the internet say this may not work on all boards.

Well, no one is stopping us from using any available GPIO pins as chip-selects! The idea is to use, for instance, /dev/spidev0.0 to access all devices on bus 0, but toggle the CS lines manually for each transfer.

Of course, if we opt for this manual control we may no longer connect anything to the dedicated CS lines on the bus: the spidev driver still "owns" these lines, and will toggle them during transfers.

Here's how you might do it on a Raspberry Pi with the libgpiod library, using GPIO pin 25 as the CS line:

#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <linux/types.h>
#include <linux/spi/spidev.h>

#include <gpiod.h>

void demo(void)
{
  struct  gpiod_chip *  gpio_chip   = NULL;
  struct  gpiod_line *  cs_pin      = NULL;

  int                     device        = -1;
  unsigned char           command[]     = { 0x13, 0x37 };
  unsigned char           response[2]   = { 0 };
  struct spi_ioc_transfer xfers[]       = {
    {
      .tx_buf = (__u64)&command[0],
      .len = sizeof(command),
    },
    {
      .rx_buf = (__u64)&response[0],
      .len = sizeof(response),
    }
  };

  // Open GPIO pin 25
  gpio_chip = gpiod_chip_open_by_name("gpiochip0");
  cs_pin = gpiod_chip_get_line(gpio_chip, 25);
  gpiod_line_request_output(cs_pin, "spi-test", 1);

  // Open SPI bus
  device = open("/dev/spidev0.0", O_RDWR);

  // Pull CS low, transfer, pull CS high
  gpiod_line_set_value(cs_pin, 0);
  ioctl(device, SPI_IOC_MESSAGE(2), xfers);
  gpiod_line_set_value(cs_pin, 1);
}

And here's how it looks on the wire:

Saleae logic analyzer capture of a transfer with manual CS management

Channel 3 here is the dedicated CS line, and Channel 4 is our manual CS line.

... And that's about it! This should be all you need to start writing SPI code on Linux.

Further reading