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.