Protocols / SPI

Examples

Two levels: bare-metal register access, and Linux userspace via spidev.

Bare-metal, generic register-style transfer

Pattern common to most MCU SPI peripherals (exact register names vary by vendor):

// Blocking, mode-0, single byte in/out
uint8_t spi_transfer(uint8_t data_out) {
    while (!(SPI->SR & SPI_SR_TXE))   // wait for TX buffer empty
        ;
    SPI->DR = data_out;               // write starts the clock

    while (!(SPI->SR & SPI_SR_RXNE))  // wait for RX buffer full
        ;
    return SPI->DR;                   // byte shifted in during the same clocks
}

void read_register(uint8_t reg_addr, uint8_t *buf, size_t len) {
    GPIO_CS_LOW();
    spi_transfer(reg_addr | 0x80);    // datasheet-defined "read" bit
    for (size_t i = 0; i < len; i++)
        buf[i] = spi_transfer(0x00);  // dummy byte to generate clocks
    GPIO_CS_HIGH();
}

Linux userspace — spidev

spidev exposes an SPI bus as /dev/spidevB.C (bus B, chip-select C) once the corresponding device tree overlay or board file binds it. See Linux → Drivers for how that binding works.

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

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

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

uint32_t speed_hz = 1000000;
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed_hz);

uint8_t tx[2] = { 0x80, 0x00 };   // read register 0x00
uint8_t rx[2] = { 0 };

struct spi_ioc_transfer tr = {
    .tx_buf = (unsigned long)tx,
    .rx_buf = (unsigned long)rx,
    .len    = sizeof(tx),
    .speed_hz = speed_hz,
    .bits_per_word = 8,
};
ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
// rx[1] now holds the register value
What I learned

spidev always does a full-duplex transfer — even a "write only" call still clocks bytes into rx_buf, and even a "read only" call still shifts something out of tx_buf (usually zeroed). Plan buffer sizes accordingly.