SPI (Serial Peripheral Interface)¶
Overview¶
Serial Peripheral Interface (SPI) is a synchronous, full-duplex master-slave bus. This implementation provides a Linux spidev userspace driver with full-duplex transfers, chained multi-transfer operations, and register read/write helpers.
Status: Implemented
Full implementation with all four clock modes, full-duplex transfer, chained transfers, and register access patterns.
Protocol Specification¶
Signal Lines¶
┌──────────────────────────────────────────────────────────────────┐
│ │
│ Master Slave │
│ ┌──────────┐ ┌──────────┐ │
│ │ │── SCLK ──────────────────►│ │ │
│ │ │── MOSI ──────────────────►│ │ │
│ │ │◄── MISO ─────────────────│ │ │
│ │ │── CS (active low) ───────►│ │ │
│ └──────────┘ └──────────┘ │
│ │
│ Full-duplex: one bit shifted in and one bit shifted out per │
│ clock cycle. MOSI and MISO transfer simultaneously. │
└──────────────────────────────────────────────────────────────────┘
Clock Modes (CPOL | CPHA)¶
Mode 0 (CPOL=0, CPHA=0):
SCLK: ────┐ ┌───┐ ┌───┐ ┌───┐ ┌────
└───┘ └───┘ └───┘ └───┘
Sample: ↑ ↑ ↑ ↑ (rising edge)
Mode 1 (CPOL=0, CPHA=1):
SCLK: ────┐ ┌───┐ ┌───┐ ┌───┐ ┌────
└───┘ └───┘ └───┘ └───┘
Sample: ↑ ↑ ↑ ↑ (falling edge)
Mode 2 (CPOL=1, CPHA=0):
SCLK: ────┘ └───┘ └───┘ └───┘ └────
┌───┐ ┌───┐ ┌───┐ ┌───┐
Sample: ↑ ↑ ↑ ↑ (falling edge)
Mode 3 (CPOL=1, CPHA=1):
SCLK: ────┘ └───┘ └───┘ └───┘ └────
┌───┐ ┌───┐ ┌───┐ ┌───┐
Sample: ↑ ↑ ↑ ↑ (rising edge)
| Mode | CPOL | CPHA | Clock Idle | Sample Edge |
|---|---|---|---|---|
| 0 | 0 | 0 | Low | Rising |
| 1 | 0 | 1 | Low | Falling |
| 2 | 1 | 0 | High | Falling |
| 3 | 1 | 1 | High | Rising |
Full-Duplex Transfer¶
CS: ─────┐ ┌─────
└───────────────────────────────────────┘
SCLK: ─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─
│ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ │
MOSI: ─────╳═══╳═══╳═══╳═══╳═══╳═══╳═══╳═══╳─────
D7 D6 D5 D4 D3 D2 D1 D0
MISO: ─────╳═══╳═══╳═══╳═══╳═══╳═══╳═══╳═══╳─────
D7 D6 D5 D4 D3 D2 D1 D0
API Reference¶
Data Structures¶
typedef enum {
SPI_CLK_MODE_0 = 0, /* CPOL=0, CPHA=0 */
SPI_CLK_MODE_1 = 1, /* CPOL=0, CPHA=1 */
SPI_CLK_MODE_2 = 2, /* CPOL=1, CPHA=0 */
SPI_CLK_MODE_3 = 3, /* CPOL=1, CPHA=1 */
} spi_mode_t;
typedef struct {
uint32_t speed_hz; /* Clock frequency */
uint8_t mode; /* SPI mode (0-3) */
uint8_t bits_per_word; /* Bits per word (usually 8) */
bool lsb_first; /* LSB first (false = MSB first) */
bool cs_high; /* CS active high (false = active low) */
uint16_t delay_usecs; /* Delay after CS deassert */
} spi_config_t;
typedef struct {
int fd; /* Device file descriptor */
char device[64]; /* Device path */
spi_config_t config; /* Active configuration */
bool is_open;
uint64_t bytes_tx; /* Statistics */
uint64_t bytes_rx;
uint64_t transfers;
} spi_t;
Constants¶
#define SPI_MAX_TRANSFER_SIZE 4096 /* Max bytes per ioctl */
#define SPI_DEFAULT_SPEED_HZ 1000000 /* 1 MHz */
#define SPI_DEFAULT_BITS 8
Lifecycle¶
spi_open¶
Open an SPI device and configure mode, speed, and bits-per-word via ioctl.
spi_close¶
Transfer Operations¶
spi_transfer (Full-Duplex)¶
Simultaneous send and receive. For send-only, pass rx_buf = NULL. For receive-only, pass tx_buf = NULL (sends 0x00 on MOSI).
| Parameter | Type | Description |
|---|---|---|
tx_buf | const void * | Data to send (NULL = send zeros) |
rx_buf | void * | Buffer for received data (NULL = discard MISO) |
len | size_t | Bytes to transfer (max 4096) |
spi_write / spi_read¶
int spi_write(spi_t *spi, const void *data, size_t len);
int spi_read(spi_t *spi, void *buf, size_t len);
Convenience wrappers for unidirectional transfers.
spi_write_then_read¶
Two-phase chained transfer: write command bytes, then read response. CS stays asserted across both phases (atomic operation via SPI_IOC_MESSAGE(2)).
Register Access¶
spi_write_reg¶
Write a single byte to a register address. Sends [reg, value] as 2-byte transfer.
spi_read_reg¶
Read a single register. Sends reg | 0x80 (MSB set for read convention), then clocks out 1 response byte.
Usage Examples¶
Full-Duplex Transfer¶
#include "spi.h"
#include <stdio.h>
int main(void)
{
spi_t spi;
spi_config_t cfg = spi_default_config();
cfg.speed_hz = 8000000; /* 8 MHz */
cfg.mode = SPI_CLK_MODE_0;
int rc = spi_open(&spi, "/dev/spidev0.0", &cfg);
if (rc < 0) {
fprintf(stderr, "SPI open failed: %s\n", strerror(-rc));
return 1;
}
uint8_t tx[] = {0xAA, 0xBB, 0xCC, 0xDD};
uint8_t rx[4] = {0};
spi_transfer(&spi, tx, rx, 4);
printf("TX: %02x %02x %02x %02x\n", tx[0], tx[1], tx[2], tx[3]);
printf("RX: %02x %02x %02x %02x\n", rx[0], rx[1], rx[2], rx[3]);
spi_close(&spi);
return 0;
}
Read Sensor Register (e.g., LIS3DH Accelerometer)¶
#include "spi.h"
#include <stdio.h>
#define WHO_AM_I_REG 0x0F
#define EXPECTED_ID 0x33
int main(void)
{
spi_t spi;
spi_config_t cfg = spi_default_config();
cfg.speed_hz = 5000000;
cfg.mode = SPI_CLK_MODE_3; /* LIS3DH uses mode 3 */
spi_open(&spi, "/dev/spidev0.0", &cfg);
uint8_t who_am_i;
spi_read_reg(&spi, WHO_AM_I_REG, &who_am_i);
printf("WHO_AM_I: 0x%02x (expected 0x%02x) %s\n",
who_am_i, EXPECTED_ID,
who_am_i == EXPECTED_ID ? "✓" : "✗");
/* Read 6 bytes of accelerometer data (multi-byte read) */
uint8_t cmd = 0x28 | 0x80 | 0x40; /* Read + auto-increment */
uint8_t accel[6];
spi_write_then_read(&spi, &cmd, 1, accel, 6);
int16_t x = (int16_t)(accel[1] << 8 | accel[0]);
int16_t y = (int16_t)(accel[3] << 8 | accel[2]);
int16_t z = (int16_t)(accel[5] << 8 | accel[4]);
printf("Accel X=%d Y=%d Z=%d\n", x, y, z);
spi_close(&spi);
return 0;
}
Build & Run¶
# Compile
gcc -Wall -Wextra -Werror -O3 -std=c11 -o spi_demo spi.c
# Ensure spidev is loaded
sudo modprobe spidev
# Add user to spi group
sudo usermod -aG spi $USER
# Run
./spi_demo
Prerequisites¶
Kernel Module Required
The spidev kernel module must be loaded and the device tree must expose the SPI bus:
Performance¶
| Clock Speed | Transfer Rate | Notes |
|---|---|---|
| 1 MHz | 125 KB/s | Default, safe for most devices |
| 8 MHz | 1 MB/s | Common for sensors |
| 20 MHz | 2.5 MB/s | Flash memory, displays |
| 50 MHz | 6.25 MB/s | High-speed ADCs |