Raspberry Pi CM5 IoT In C - Getting Started With SPI
Written by Harry Fairhead   
Monday, 29 December 2025
Article Index
Raspberry Pi CM5 IoT In C - Getting Started With SPI
SPI Protocol
SPIDev
Full Listing

SPIDev

The interface to the SPI driver is generally referred to as SPIdev and there is a header file, spidev.h, which provides all of the definitions you need to make use of it. When you load the SPI driver to install SPI Channel n a number of character devices are created in /dev of the general form spidevn.m where n is the channel number and m is the chip select line used to control the device. For example, the basic SPI driver uses channel 0, i.e. SPI0 with two chip select lines, and thus you will find spidev0.0 and spidev0.1 which control the SPI device connected to SPI0 on chip select 0 and 1 respectively. By default, spidev0.0 uses GPIO8 pin 26 and spidev0.1 uses GPIO7 pin 28 for chip selects.

To work with an SPI device all you have to do is use ioctl to send requests to the relevant file. If you want to know more about ioctl see Chapter 4.

There are a range of configuration requests:

  • SPI_IOC_WR_MODE    	Sets mode
  • SPI_IOC_WR_LSB_FIRST  	Sets LSB first or last
  • SPI_IOC_WR_BITS_PER_WORD Sets number of bits per word 
  • SPI_IOC_WR_MAX_SPEED_HZ	Sets SPI clock if possible

There are also requests with WR replaced by RD which read, rather than write, the configuration.

Note: SPI_IOC_WR_LSB_FIRST and SPI_IOC_RD_LSB_FIRST are not supported on Pi OS.

Once you have the SPI interface set up, you can send and receive data using the SPI_IOC_MESSAGE request. This is slightly different from other requests in that a macro is used to construct a request that also specifies the number of operations needed.

Each operation is defined by a struct:

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

The fields are fairly obvious. The tx_buf and rx_buf are byte arrays used for the transmitted and received data and they can be the same array. The len field specifies the number of bytes in each array. The speed_hz field modifies the SPI clock. The delay_usecs field sets a delay before the chip select is deselected after the transfer. The cs_change field is true if you want the chip select to be deselected between each transfer.

The best way to find out how this all works is to write the simplest possible example.

A Loopback Example

Because of the way that data is transferred on the SPI bus, it is very easy to test that everything is working without having to add any components. All you have to do is connect MOSI to MISO so that anything sent is also received in a loopback mode. There is an official example program to implement a loopback, but it is complicated for a first example and has a bug. Our version will be the simplest possible and, hopefully, without bugs.

First connect GPIO9 to GPIO10 using a jumper wire and start a new project. The program is very simple. First we check that the SPI bus is loaded:

checkSPI0();

and next we open spdev0.0:

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

As this is a loopback test we really don't need to configure the bus as all that matters is that the transmit and receive channels have the same configuration. However, we do need some data to send:

uint8_t tx[] = {0xAA};
uint8_t rx[] = {0};

The hex value AA is useful in testing because it generates the bit sequence 10101010, which is easy to see on a logic analyzer.

To send the data we need an spi_ioc_transfer struct:

struct spi_ioc_transfer tr =
    {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = 1,
        .delay_usecs = 0,
        .speed_hz = 500000,
        .bits_per_word = 8,
    };

We can now use the ioctl call to send and receive the data:

int status = ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
if (status < 0)
        printf("can't send data");

Finally we can check that the send and received data match and close the file.



Last Updated ( Wednesday, 31 December 2025 )