Zephyr SPI
Expert guidance for Zephyr's SPI driver subsystem covering synchronous and asynchronous transfer APIs, devicetree configuration, and common usage patterns.
Table of Contents
- API Selection
- Getting Device Reference
- Common Workflows
- Configuration
- Error Handling
- Troubleshooting
API Selection
Zephyr provides two SPI access methods. Choose based on requirements:
| API | Kconfig | Use Case | Blocking? |
|---|---|---|---|
| Synchronous | (default) | Simple transfers, blocking until complete | Yes |
| Asynchronous | CONFIG_SPI_ASYNC |
Non-blocking, callback/signal on completion | No |
Decision Tree
Simple blocking transfer? → Synchronous (spi_transceive)
Need non-blocking transfer? → Async (spi_transceive_cb / spi_transceive_signal)
High throughput with minimal CPU? → Async with DMA-capable driver
Multiple concurrent SPI operations? → Async with proper buffer management
Getting Device Reference
From Devicetree (Preferred - for SPI Devices)
Use SPI_DT_SPEC_GET to get a complete SPI specification including bus, config, and CS:
#include <zephyr/device.h>
#include <zephyr/drivers/spi.h>
/* Define the SPI device from devicetree */
#define MY_SPI_DEVICE DT_NODELABEL(my_spi_device)
static const struct spi_dt_spec spi_dev = SPI_DT_SPEC_GET(
MY_SPI_DEVICE,
SPI_OP_MODE_MASTER | SPI_WORD_SET(8) | SPI_TRANSFER_MSB
);
/* Runtime check (in main or init) */
if (!spi_is_ready_dt(&spi_dev)) {
printk("SPI device not ready\n");
return -ENODEV;
}
Direct Controller Access (Less Common)
/* Get SPI controller directly */
const struct device *spi = DEVICE_DT_GET(DT_NODELABEL(spi0));
if (!device_is_ready(spi)) {
return -ENODEV;
}
/* Manual config setup */
struct spi_config spi_cfg = {
.frequency = 1000000U, /* 1 MHz */
.operation = SPI_OP_MODE_MASTER | SPI_WORD_SET(8),
.slave = 0,
.cs = {0}, /* No GPIO CS, use hardware CS */
};
Common Workflows
1. Basic Transceive (Simultaneous TX/RX)
#include <zephyr/drivers/spi.h>
uint8_t tx_buf[] = {0x9F, 0x00, 0x00, 0x00}; /* Read ID command + dummy bytes */
uint8_t rx_buf[4];
struct spi_buf tx = {.buf = tx_buf, .len = sizeof(tx_buf)};
struct spi_buf rx = {.buf = rx_buf, .len = sizeof(rx_buf)};
struct spi_buf_set tx_set = {.buffers = &tx, .count = 1};
struct spi_buf_set rx_set = {.buffers = &rx, .count = 1};
int ret = spi_transceive_dt(&spi_dev, &tx_set, &rx_set);
if (ret < 0) {
printk("SPI transceive error: %d\n", ret);
}
/* rx_buf now contains response */
2. Write Only (No RX)
uint8_t cmd[] = {0x06}; /* Write enable command */
struct spi_buf tx = {.buf = cmd, .len = sizeof(cmd)};
struct spi_buf_set tx_set = {.buffers = &tx, .count = 1};
int ret = spi_write_dt(&spi_dev, &tx_set);
3. Read Only (TX Ignored)
uint8_t data[16];
struct spi_buf rx = {.buf = data, .len = sizeof(data)};
struct spi_buf_set rx_set = {.buffers = &rx, .count = 1};
int ret = spi_read_dt(&spi_dev, &rx_set);
4. Scatter-Gather (Multi-Buffer Transfer)
Use multiple buffers for command + address + data in a single CS assertion:
uint8_t cmd = 0x03; /* Read command */
uint8_t addr[3] = {0x00, 0x10, 0x00}; /* 24-bit address */
uint8_t data[64];
struct spi_buf tx_bufs[] = {
{.buf = &cmd, .len = 1},
{.buf = addr, .len = 3},
{.buf = NULL, .len = 64}, /* NULL = send zeros while receiving */
};
struct spi_buf rx_bufs[] = {
{.buf = NULL, .len = 1}, /* NULL = ignore received data */
{.buf = NULL, .len = 3},
{.buf = data, .len = 64},
};
struct spi_buf_set tx_set = {.buffers = tx_bufs, .count = 3};
struct spi_buf_set rx_set = {.buffers = rx_bufs, .count = 3};
int ret = spi_transceive_dt(&spi_dev, &tx_set, &rx_set);
5. Async Transfer with Callback
Requires CONFIG_SPI_ASYNC=y:
#include <zephyr/drivers/spi.h>
static volatile bool transfer_done;
static int transfer_result;
void spi_callback(const struct device *dev, int result, void *userdata)
{
transfer_result = result;
transfer_done = true;
}
int async_transfer(void)
{
uint8_t tx_data[] = {0x01, 0x02, 0x03};
uint8_t rx_data[3];
struct spi_buf tx = {.buf = tx_data, .len = sizeof(tx_data)};
struct spi_buf rx = {.buf = rx_data, .len = sizeof(rx_data)};
struct spi_buf_set tx_set = {.buffers = &tx, .count = 1};
struct spi_buf_set rx_set = {.buffers = &rx, .count = 1};
transfer_done = false;
int ret = spi_transceive_cb(spi_dev.bus, &spi_dev.config,
&tx_set, &rx_set, spi_callback, NULL);
if (ret < 0) {
return ret;
}
/* Do other work while transfer is in progress */
/* Wait for completion (or use k_poll) */
while (!transfer_done) {
k_yield();
}
return transfer_result;
}
- Full async API details: See references/async.md
Configuration
Kconfig Essentials
CONFIG_SPI=y # Enable SPI driver subsystem
CONFIG_SPI_ASYNC=y # Enable async API (callback/signal)
CONFIG_SPI_SLAVE=y # Enable slave mode (experimental)
CONFIG_SPI_EXTENDED_MODES=y # Enable dual/quad/octal line modes
CONFIG_SPI_SHELL=y # Enable SPI shell for debugging
- Full Kconfig reference: See references/kconfig.md
Devicetree Essentials
SPI Controller
&spi0 {
status = "okay";
pinctrl-0 = <&spi0_default>;
pinctrl-names = "default";
cs-gpios = <&gpio0 4 GPIO_ACTIVE_LOW>; /* CS on GPIO0 pin 4 */
my_spi_device: sensor@0 {
compatible = "vendor,sensor";
reg = <0>; /* CS index (matches cs-gpios) */
spi-max-frequency = <1000000>; /* 1 MHz max */
/* Optional SPI mode settings */
spi-cpol; /* Clock idle high (Mode 2 or 3) */
spi-cpha; /* Sample on second edge (Mode 1 or 3) */
};
};
SPI Mode Quick Reference
| Mode | CPOL | CPHA | DTS Properties |
|---|---|---|---|
| 0 | 0 | 0 | (default) |
| 1 | 0 | 1 | spi-cpha |
| 2 | 1 | 0 | spi-cpol |
| 3 | 1 | 1 | spi-cpol; spi-cpha |
- Full devicetree reference: See references/devicetree.md
Operation Flags
Common flags for spi_config.operation:
| Flag | Description |
|---|---|
SPI_OP_MODE_MASTER |
Controller/master mode (default) |
SPI_OP_MODE_SLAVE |
Peripheral/slave mode |
SPI_MODE_CPOL |
Clock polarity (idle high) |
SPI_MODE_CPHA |
Clock phase (sample on second edge) |
SPI_WORD_SET(n) |
Word size in bits (typically 8) |
SPI_TRANSFER_MSB |
MSB first (default) |
SPI_TRANSFER_LSB |
LSB first |
SPI_HOLD_ON_CS |
Keep CS active after transaction |
SPI_LOCK_ON |
Lock bus for multiple transactions |
SPI_CS_ACTIVE_HIGH |
CS is active high (unusual) |
Error Handling
Return Values
| Value | Meaning |
|---|---|
0 |
Success (master mode) |
> 0 |
Frames received (slave mode) |
-ENOTSUP |
Unsupported config (check operation flags) |
-EINVAL |
Invalid parameter in spi_config |
-EBUSY |
Bus locked by another caller |
-errno |
Other failure |
Releasing Locked Bus
If using SPI_LOCK_ON or SPI_HOLD_ON_CS, release when done:
spi_release_dt(&spi_dev);
Troubleshooting
Quick Reference
| Issue | Likely Cause | Fix |
|---|---|---|
| No data on bus | Wrong pins | Verify pinctrl in devicetree |
| Garbled data | Mode mismatch | Check CPOL/CPHA match device datasheet |
| CS not toggling | GPIO not configured | Add cs-gpios to controller node |
-ENOTSUP |
Unsupported operation | Check driver supports requested features |
-EBUSY |
Bus locked | Call spi_release() or remove SPI_LOCK_ON |
| Slow transfer | Low frequency | Increase spi-max-frequency in DTS |
| RX data offset | TX/RX length mismatch | Ensure buffer lengths account for command bytes |
Debug Checklist
- Device ready? Check
spi_is_ready_dt(&spec) - Pins correct? Verify pinctrl (SCK, MOSI, MISO, CS)
- Mode match? CPOL/CPHA must match peripheral device
- Frequency valid? Some devices have min/max limits
- CS polarity? Most devices are active-low (default)
- Word size? Most devices use 8-bit words
References
- references/api.md — Full API function reference
- references/async.md — Async API with callback/signal patterns
- references/kconfig.md — All SPI Kconfig options
- references/devicetree.md — Devicetree properties and examples
Source Locations
Key files in Zephyr source tree:
include/zephyr/drivers/spi.h— Public API headerdrivers/spi/— Driver implementationsdts/bindings/spi/spi-controller.yaml— Controller DTS bindingdts/bindings/spi/spi-device.yaml— Device DTS bindingsamples/drivers/spi_*— Sample applications