Skip to content

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

int spi_open(spi_t *spi, const char *device, const spi_config_t *config);

Open an SPI device and configure mode, speed, and bits-per-word via ioctl.

spi_close

void spi_close(spi_t *spi);

Transfer Operations

spi_transfer (Full-Duplex)

int spi_transfer(spi_t *spi, const void *tx_buf, void *rx_buf, size_t len);

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

int spi_write_then_read(spi_t *spi, const void *cmd, size_t cmd_len,
                        void *resp, size_t resp_len);

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

int spi_write_reg(spi_t *spi, uint8_t reg, uint8_t value);

Write a single byte to a register address. Sends [reg, value] as 2-byte transfer.

spi_read_reg

int spi_read_reg(spi_t *spi, uint8_t reg, uint8_t *value);

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:

sudo modprobe spidev
ls /dev/spidev*  # Should show spidev0.0, spidev0.1, etc.

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

Test Output

$ ./build/spi_demo
[spi] Opened /dev/spidev0.0 (mode=0, speed=8000000 Hz, bits=8)
[spi] Full-duplex: TX=aa bb cc dd, RX=00 ff 42 18
[spi] Register read: WHO_AM_I=0x33 ✓
[spi] Write-then-read: cmd=0xa8, resp=6 bytes
[spi] Stats: 4 transfers, 16 bytes TX, 12 bytes RX
[PASS] All spi tests passed