SPI Communication
SPI (Serial Peripheral Interface) provides high-speed, full-duplex communication with peripherals like displays, flash memory, and sensors.
SPI Overview
flowchart LR
subgraph MCU["MCU (Master)"]
MOSI[MOSI]
MISO[MISO]
SCK[SCK]
CS[CS Pins]
end
subgraph Devices
D1[Flash]
D2[Display]
D3[ADC]
end
MOSI --> D1 & D2 & D3
D1 & D2 & D3 --> MISO
SCK --> D1 & D2 & D3
CS -->|CS0| D1
CS -->|CS1| D2
CS -->|CS2| D3
SPI Signals
| Signal | Direction | Description |
|---|---|---|
| MOSI | Master → Slave | Master Out, Slave In |
| MISO | Slave → Master | Master In, Slave Out |
| SCK | Master → Slave | Serial Clock |
| CS/SS | Master → Slave | Chip Select (active low) |
Devicetree Configuration
&spi1 {
status = "okay";
cs-gpios = <&gpio0 4 GPIO_ACTIVE_LOW>,
<&gpio0 5 GPIO_ACTIVE_LOW>;
flash: w25q128@0 {
compatible = "jedec,spi-nor";
reg = <0>; /* CS index */
spi-max-frequency = <40000000>;
};
display: ili9341@1 {
compatible = "ilitek,ili9341";
reg = <1>; /* CS index */
spi-max-frequency = <20000000>;
cmd-data-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;
};
};
Basic Setup
Using SPI DT Spec
#include <zephyr/kernel.h>
#include <zephyr/drivers/spi.h>
#define FLASH_NODE DT_NODELABEL(flash)
/* Get SPI spec from devicetree */
static const struct spi_dt_spec flash = SPI_DT_SPEC_GET(
FLASH_NODE,
SPI_WORD_SET(8) | SPI_TRANSFER_MSB, /* 8-bit words, MSB first */
0 /* Hold time after CS (us) */
);
void main(void)
{
if (!spi_is_ready_dt(&flash)) {
printk("SPI device not ready\n");
return;
}
/* Ready to use */
}
Manual Configuration
const struct device *spi = DEVICE_DT_GET(DT_NODELABEL(spi1));
struct spi_config spi_cfg = {
.frequency = 4000000, /* 4 MHz */
.operation = SPI_WORD_SET(8) | SPI_TRANSFER_MSB | SPI_OP_MODE_MASTER,
.slave = 0,
.cs = {
.gpio = GPIO_DT_SPEC_GET(DT_NODELABEL(gpio0), 4),
.delay = 0,
},
};
SPI Modes
SPI has 4 modes based on clock polarity (CPOL) and phase (CPHA):
flowchart TD
subgraph Mode0["Mode 0 (CPOL=0, CPHA=0)"]
M0[Sample on rising edge]
end
subgraph Mode1["Mode 1 (CPOL=0, CPHA=1)"]
M1[Sample on falling edge]
end
subgraph Mode2["Mode 2 (CPOL=1, CPHA=0)"]
M2[Sample on falling edge]
end
subgraph Mode3["Mode 3 (CPOL=1, CPHA=1)"]
M3[Sample on rising edge]
end
/* Mode selection in operation flags */
SPI_MODE_CPOL /* Clock polarity: idle high */
SPI_MODE_CPHA /* Clock phase: sample on second edge */
/* Common mode configurations */
#define SPI_MODE_0 (0) /* CPOL=0, CPHA=0 */
#define SPI_MODE_1 (SPI_MODE_CPHA) /* CPOL=0, CPHA=1 */
#define SPI_MODE_2 (SPI_MODE_CPOL) /* CPOL=1, CPHA=0 */
#define SPI_MODE_3 (SPI_MODE_CPOL | SPI_MODE_CPHA) /* CPOL=1, CPHA=1 */
SPI Operations
Buffer Setup
/* TX and RX buffers */
uint8_t tx_data[] = {0x9F, 0x00, 0x00, 0x00}; /* Command + dummy bytes */
uint8_t rx_data[4];
/* Buffer descriptors */
struct spi_buf tx_buf = {
.buf = tx_data,
.len = sizeof(tx_data)
};
struct spi_buf rx_buf = {
.buf = rx_data,
.len = sizeof(rx_data)
};
/* Buffer sets */
struct spi_buf_set tx = {
.buffers = &tx_buf,
.count = 1
};
struct spi_buf_set rx = {
.buffers = &rx_buf,
.count = 1
};
Transceive (Full Duplex)
/* Simultaneous TX and RX */
int ret = spi_transceive_dt(&flash, &tx, &rx);
if (ret < 0) {
printk("SPI error: %d\n", ret);
}
Write Only
/* TX only, ignore RX */
int ret = spi_write_dt(&flash, &tx);
Read Only
/* RX only (sends zeros) */
int ret = spi_read_dt(&flash, &rx);
Example: SPI Flash
#include <zephyr/kernel.h>
#include <zephyr/drivers/spi.h>
#define FLASH_NODE DT_NODELABEL(flash)
static const struct spi_dt_spec flash = SPI_DT_SPEC_GET(
FLASH_NODE, SPI_WORD_SET(8) | SPI_TRANSFER_MSB, 0);
/* Flash commands */
#define FLASH_CMD_READ_ID 0x9F
#define FLASH_CMD_READ 0x03
#define FLASH_CMD_WRITE_ENABLE 0x06
#define FLASH_CMD_PAGE_PROGRAM 0x02
#define FLASH_CMD_SECTOR_ERASE 0x20
#define FLASH_CMD_READ_STATUS 0x05
/* Read JEDEC ID */
int flash_read_id(uint8_t *id)
{
uint8_t cmd = FLASH_CMD_READ_ID;
struct spi_buf tx_bufs[] = {
{ .buf = &cmd, .len = 1 },
};
struct spi_buf rx_bufs[] = {
{ .buf = NULL, .len = 1 }, /* Skip command byte */
{ .buf = id, .len = 3 }, /* Read 3-byte ID */
};
struct spi_buf_set tx = { .buffers = tx_bufs, .count = 1 };
struct spi_buf_set rx = { .buffers = rx_bufs, .count = 2 };
return spi_transceive_dt(&flash, &tx, &rx);
}
/* Read data from flash */
int flash_read(uint32_t addr, uint8_t *data, size_t len)
{
uint8_t cmd[4] = {
FLASH_CMD_READ,
(addr >> 16) & 0xFF,
(addr >> 8) & 0xFF,
addr & 0xFF
};
struct spi_buf tx_bufs[] = {
{ .buf = cmd, .len = sizeof(cmd) },
};
struct spi_buf rx_bufs[] = {
{ .buf = NULL, .len = sizeof(cmd) }, /* Skip command */
{ .buf = data, .len = len },
};
struct spi_buf_set tx = { .buffers = tx_bufs, .count = 1 };
struct spi_buf_set rx = { .buffers = rx_bufs, .count = 2 };
return spi_transceive_dt(&flash, &tx, &rx);
}
/* Write enable */
int flash_write_enable(void)
{
uint8_t cmd = FLASH_CMD_WRITE_ENABLE;
struct spi_buf buf = { .buf = &cmd, .len = 1 };
struct spi_buf_set tx = { .buffers = &buf, .count = 1 };
return spi_write_dt(&flash, &tx);
}
/* Read status register */
int flash_read_status(uint8_t *status)
{
uint8_t cmd = FLASH_CMD_READ_STATUS;
struct spi_buf tx_buf = { .buf = &cmd, .len = 1 };
struct spi_buf rx_bufs[] = {
{ .buf = NULL, .len = 1 },
{ .buf = status, .len = 1 },
};
struct spi_buf_set tx = { .buffers = &tx_buf, .count = 1 };
struct spi_buf_set rx = { .buffers = rx_bufs, .count = 2 };
return spi_transceive_dt(&flash, &tx, &rx);
}
/* Wait for write/erase to complete */
int flash_wait_ready(k_timeout_t timeout)
{
int64_t end = k_uptime_get() + k_ticks_to_ms_ceil64(timeout.ticks);
while (k_uptime_get() < end) {
uint8_t status;
int ret = flash_read_status(&status);
if (ret < 0) {
return ret;
}
if (!(status & 0x01)) { /* WIP bit clear */
return 0;
}
k_msleep(1);
}
return -ETIMEDOUT;
}
void main(void)
{
if (!spi_is_ready_dt(&flash)) {
printk("SPI flash not ready\n");
return;
}
/* Read JEDEC ID */
uint8_t id[3];
if (flash_read_id(id) == 0) {
printk("Flash ID: %02x %02x %02x\n", id[0], id[1], id[2]);
}
/* Read first 16 bytes */
uint8_t data[16];
if (flash_read(0, data, sizeof(data)) == 0) {
printk("Data: ");
for (int i = 0; i < sizeof(data); i++) {
printk("%02x ", data[i]);
}
printk("\n");
}
}
Multiple Buffers
Chain multiple buffers in a single transaction:
uint8_t cmd[4];
uint8_t addr[3];
uint8_t data[256];
struct spi_buf tx_bufs[] = {
{ .buf = cmd, .len = sizeof(cmd) },
{ .buf = addr, .len = sizeof(addr) },
{ .buf = data, .len = sizeof(data) },
};
struct spi_buf_set tx = {
.buffers = tx_bufs,
.count = ARRAY_SIZE(tx_bufs)
};
spi_write_dt(&device, &tx);
Operation Flags
/* Word size */
SPI_WORD_SET(8) /* 8-bit words */
SPI_WORD_SET(16) /* 16-bit words */
/* Bit order */
SPI_TRANSFER_MSB /* MSB first (default) */
SPI_TRANSFER_LSB /* LSB first */
/* Mode flags */
SPI_MODE_CPOL /* Clock idle high */
SPI_MODE_CPHA /* Sample on second edge */
SPI_MODE_LOOP /* Loopback mode */
/* Chip select control */
SPI_HOLD_ON_CS /* Keep CS active between transfers */
SPI_LOCK_ON /* Lock the bus */
/* Full duplex control */
SPI_HALF_DUPLEX /* Use 3-wire SPI */
Async SPI (Optional)
For non-blocking operations:
struct k_poll_signal spi_done;
struct spi_buf_set tx, rx;
void spi_callback(const struct device *dev, int result, void *data)
{
k_poll_signal_raise(&spi_done, result);
}
void async_transfer(void)
{
k_poll_signal_init(&spi_done);
int ret = spi_transceive_signal(spi_dev, &spi_cfg, &tx, &rx, &spi_done);
if (ret < 0) {
return;
}
/* Do other work... */
/* Wait for completion */
struct k_poll_event events[] = {
K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL,
K_POLL_MODE_NOTIFY_ONLY,
&spi_done),
};
k_poll(events, 1, K_FOREVER);
}
API Reference
/* Check readiness */
bool spi_is_ready_dt(const struct spi_dt_spec *spec);
/* Synchronous transfers */
int spi_transceive_dt(const struct spi_dt_spec *spec,
const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs);
int spi_write_dt(const struct spi_dt_spec *spec,
const struct spi_buf_set *tx_bufs);
int spi_read_dt(const struct spi_dt_spec *spec,
const struct spi_buf_set *rx_bufs);
/* Release bus (if SPI_LOCK_ON was used) */
int spi_release_dt(const struct spi_dt_spec *spec);
Best Practices
- Use SPI_DT_SPEC_GET() - Encapsulates bus, CS, and configuration
- Check spi_is_ready_dt() - Before any operations
- Match device requirements - Mode, frequency, word size
- Use appropriate CS timing - Some devices need setup/hold time
- Chain buffers - Instead of multiple transactions
- Handle errors - SPI can fail (bus busy, hardware error)
Next Steps
Learn about UART for serial communication.