SPI Core Concepts
SPI (Serial Peripheral Interface) is a synchronous, full-duplex serial communication protocol originally developed by Motorola. It enables MCUs or MPUs to exchange data with peripherals such as flash memory, ADCs, network controllers, or other processors. Key attributes include high-speed operation, serial data transfer, and the ability to transmit and receive simultaneously.
Hardware Characteristics
Wiring Configurations
- 3-Wire Mode: Uses CLK, CS, and a bidirectional MOSI/MISO line. Communication is half-duplex, with data sent and received in a time-shared manner on a single data pin.
- 4-Wire Mode: Adds a dedicated MISO line (CLK, CS, MOSI, MISO) to achieve full-duplex communication.
- Note: On controllers with QSPI support (data lines D0–D7), the mapping typically assigns D0 to MOSI and D1 to MISO when operating in standard SPI mode.
Clock Polarity and Phase
Four operation modes are defined by the combinations of CPOL (clock polarity) and CPHA (clock phase). The specific mode must match the attached peripheral device's requirements.
Driver Architecture (Linux 5.10.165, DesignWare SPI Core)
Key Source Files and Their Roles
spi-dw-mmio.c
Parses the device tree node for the SPI controller, registers it as a platform device, and initializes a dw_spi_mmio structure. Resources such as register addresses and clocks are mapped into the embedded dw_spi member defined in spi-dw.h.
Principal structure:
struct dw_spi_mmio {
struct dw_spi dws;
struct clk *clk;
struct clk *pclk;
void *priv;
struct reset_control *rstc;
};
spi-dw-core.c
Implements operations on struct dw_spi, manages interrupt handling, and prepares the struct spi_controller *master embedded inside dw_spi.
Principal structure:
struct dw_spi {
struct spi_controller *master;
void __iomem *regs;
unsigned long paddr;
int irq;
u32 fifo_len;
u32 max_mem_freq;
u32 max_freq;
u32 caps;
u32 reg_io_width;
u16 bus_num;
u16 num_cs;
void (*set_cs)(struct spi_device *spi, bool enable);
void *tx;
unsigned int tx_len;
void *rx;
unsigned int rx_len;
u8 buf[SPI_BUF_SIZE];
int dma_mapped;
u8 n_bytes;
irqreturn_t (*transfer_handler)(struct dw_spi *dws);
u32 current_freq;
u32 cur_rx_sample_dly;
u32 def_rx_sample_dly_ns;
struct spi_controller_mem_ops mem_ops;
struct dma_chan *txchan;
u32 txburst;
struct dma_chan *rxchan;
u32 rxburst;
u32 dma_sg_burst;
unsigned long dma_chan_busy;
dma_addr_t dma_addr;
const struct dw_spi_dma_ops *dma_ops;
struct completion dma_completion;
#ifdef CONFIG_DEBUG_FS
struct dentry *debugfs;
struct debugfs_regset32 regset;
#endif
};
spidev.c
Provides a generic character device interface for SPI slave devices. The standard file_operations structure maps user-space calls to the SPI bus.
static const struct file_operations spidev_fops = {
.owner = THIS_MODULE,
.write = spidev_write,
.read = spidev_read,
.unlocked_ioctl = spidev_ioctl,
.compat_ioctl = spidev_compat_ioctl,
.open = spidev_open,
.release = spidev_release,
.llseek = no_llseek,
};
spi.c
The SPI subsystem core. It handles controller and device registration, manages message queues, and provides allocation routines for spi_controller and spi_device.
Core Data Structures
Representation of an SPI controller:
struct spi_controller {
struct device dev;
struct list_head list;
s16 bus_num;
u16 num_chipselect;
/* ... hook functions ... */
};
Representation of an SPI slave device:
struct spi_device {
struct device dev;
struct spi_controller *controller;
struct spi_controller *master;
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
bool rt;
u32 mode;
#define SPI_CPHA 0x01
#define SPI_CPOL 0x02
#define SPI_MODE_0 (0|0)
#define SPI_MODE_1 (0|SPI_CPHA)
#define SPI_MODE_2 (SPI_CPOL|0)
#define SPI_MODE_3 (SPI_CPOL|SPI_CPHA)
/* ... additional mode flags and fields ... */
int irq;
void *controller_state;
void *controller_data;
char modalias[SPI_NAME_SIZE];
const char *driver_override;
int cs_gpio;
struct gpio_desc *cs_gpiod;
struct spi_delay word_delay;
struct spi_statistics statistics;
};
Representation of an SPI transaction message:
struct spi_message {
struct list_head transfers;
struct spi_device *spi;
unsigned is_dma_mapped:1;
void (*complete)(void *context);
void *context;
unsigned frame_length;
unsigned actual_length;
int status;
struct list_head queue;
void *state;
struct list_head resources;
};
Initialization Flow Overview
The kernel boots and the SPI controller's platform driver (dw-mmio) probes the hardware, allocates and initializes a dw_spi and its master spi_controller. Afterwards, child devices described in the device tree are instantiated as spi_device structures and bound to protocol drivers like spidev.
User-Space Programming
Transfer Structure spi_ioc_transfer
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;
__u8 tx_nbits;
__u8 rx_nbits;
__u8 word_delay_usecs;
__u8 pad;
};
Step-by-Step API Usage
Open the device node to the specific SPI bus and chip select:
int fd = open("/dev/spidev0.0", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
Configure mode, bits per word, and maximum speed using ioctl:
uint8_t mode = SPI_MODE_0;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_RD_MODE, &mode);
uint8_t bpw = 8;
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bpw);
ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bpw);
uint32_t speed = 1000000; /* 1 MHz */
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
Perform a read operation from a peripheral register:
uint8_t read_register(int fd, uint8_t reg_addr) {
uint8_t out_buf[3] = {0x03, reg_addr, 0x00}; /* read cmd, addr, dummy */
uint8_t in_buf[3] = {0};
struct spi_ioc_transfer xfer = {
.tx_buf = (unsigned long)out_buf,
.rx_buf = (unsigned long)in_buf,
.len = 3,
};
int status = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
if (status >= 3) {
return in_buf[2];
}
return 0xFF; /* error */
}
Perform a write operation to a peripheral register:
int write_register(int fd, uint8_t reg_addr, uint8_t value) {
uint8_t out_buf[3] = {0x02, reg_addr, value}; /* write cmd, addr, data */
struct spi_ioc_transfer xfer = {
.tx_buf = (unsigned long)out_buf,
.len = 3,
};
int status = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
if (status >= 3) {
return 0; /* success */
}
return -1; /* error */
}