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
- Simple I/O
- SPI parameters
- Complex I/O
- Good news about CS
- Bad news about CS
- Further reading
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:
|
|
(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:
- Assert the CS line for the device.
- Send the provided bytes on the MOSI line, while ignoring the data received on MISO.
- De-assert the CS line.
Similarly, we can read
from the device:
|
|
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:
- Assert the CS line.
- Send a zero byte for each byte that should be read from MISO, and put the byte from MISO in the output buffer.
- De-assert the CS line.
SPI parameters
What if we want to change the SPI mode? Easy:
|
|
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:
- Assert the CS pin.
- Send the command while ignoring any output from the device (it is undefined, since we haven't finished sending the command yet).
- Send zeroes (or anything, really) while reading the output from the device (this is the command's response).
- 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:
|
|
Okay, so what's going on here?
We have an array of spi_ioc_transfer
structures. Here's the structure definition from
spidev.h
:
|
|
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:
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
.
- 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. ...
- When the transfer is the last one in the message, the chip may stay selected until the next transfer. ...
In other words:
- 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. - 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:
|
|
And here's how it looks on the wire:
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
- The
spidev.h
header documents all the parameters that can be set for a single transfer. - The
spi.h
kernel header provides more insight about the transfer structure. - The (somewhat lacking) documentation about the SPI userland API.
- The kernel documentation about SPI.