4. SPI驱动架构:Zephyr SPI驱动框架、spi_context结构体、驱动回调函数、DMA与PIO模式选择
4.1 Zephyr SPI驱动框架分层模型
Zephyr RTOS的SPI驱动采用经典的分层架构设计,从上到下依次为:API层、通用层、底层硬件驱动层。这种分层结构使得应用代码与硬件解耦,同时为不同MCU厂商的SPI外设提供了统一的接口规范。
架构核心原则:API层定义标准接口(如 spi_transceive、spi_write、spi_read),通用层管理 spi_context 状态机与缓冲区,底层驱动负责直接操作寄存器或DMA控制器。
4.2 spi_context结构体:驱动状态的核心载体
spi_context 是Zephyr SPI通用层中最重要的数据结构,它贯穿整个SPI事务的生命周期。每个SPI设备实例都拥有一个独立的 spi_context 实例,用于跟踪当前传输状态、缓冲区指针、配置参数以及回调信息。
| 字段名 | 类型 | 描述 |
|---|---|---|
| config | struct spi_config * | 指向当前SPI配置(频率、极性、相位、位序等) |
| tx_buf | const uint8_t * | 发送数据缓冲区指针 |
| rx_buf | uint8_t * | 接收数据缓冲区指针 |
| tx_len | size_t | 待发送数据长度(字节) |
| rx_len | size_t | 待接收数据长度(字节) |
| tx_count | size_t | 已发送字节计数 |
| rx_count | size_t | 已接收字节计数 |
| callback | spi_callback_t | 异步传输完成回调函数指针 |
| asynchronous | bool | 是否为异步传输模式 |
| dma_enabled | bool | 当前传输是否启用DMA |
关键设计点:spi_context 中的 tx_count 和 rx_count 用于驱动内部字节级进度追踪,底层驱动在每次中断或DMA完成时更新这两个计数器,当 tx_count == tx_len 且 rx_count == rx_len 时,事务被视为完成。
4.3 驱动回调函数机制
Zephyr SPI驱动支持两种传输模式:同步阻塞和异步回调。异步模式下,驱动通过回调函数通知应用层事务完成。回调函数通过 spi_context 中的 callback 字段注册,在底层驱动完成所有字节传输后被调用。
/* 异步传输回调函数原型 */
typedef void (*spi_callback_t)(const struct device *dev,
struct spi_config *config,
int status);
/* 异步传输调用示例 */
static void my_spi_callback(const struct device *dev,
struct spi_config *config,
int status)
{
if (status == 0) {
printk("SPI transaction completed successfully\n");
} else {
printk("SPI transaction failed with error %d\n", status);
}
}
/* 发起异步传输 */
struct spi_buf_set tx_bufs = { ... };
struct spi_buf_set rx_bufs = { ... };
spi_transceive_async(&spi_dev, &spi_cfg, &tx_bufs, &rx_bufs, my_spi_callback);
注意:回调函数在中断上下文中执行,因此必须保持轻量——不要在回调中执行阻塞操作、内存分配或长时间计算。如果需要处理大量数据,建议使用信号量或工作队列将处理任务推迟到线程上下文。
4.4 DMA与PIO模式选择
Zephyr SPI驱动支持两种数据传输模式:PIO(Programmed I/O)和DMA(Direct Memory Access)。选择哪种模式直接影响CPU占用率、吞吐量和延迟特性。
| 特性 | PIO模式 | DMA模式 |
|---|---|---|
| CPU占用 | 高(每字节都需要CPU干预) | 低(CPU仅在启动和完成时介入) |
| 吞吐量 | 受限于CPU频率,适合短数据 | 高,适合大数据块(>64字节) |
| 延迟 | 低(无DMA配置开销) | 中等(DMA配置有固定开销) |
| 适用场景 | 短事务、低延迟敏感、寄存器读写 | 大数据块、高吞吐、低CPU占用需求 |
| 内存开销 | 低 | 需要DMA描述符和缓冲区对齐 |
| 配置复杂度 | 简单(直接操作SPI数据寄存器) | 复杂(DMA通道、描述符链、中断协调) |
4.5 驱动初始化与设备树绑定
在Zephyr中,SPI驱动的初始化通过设备树(Devicetree)完成硬件描述,驱动代码通过宏自动生成设备实例。底层驱动在初始化阶段完成以下工作:
- 解析设备树:读取 reg、clocks、interrupts 等属性,获取基地址、时钟频率、中断号。
- 配置GPIO:通过 pinctrl 设置SCK、MOSI、MISO、CS引脚的复用功能。
- 初始化spi_context:清零状态、设置默认配置指针。
- 注册中断:为SPI外设中断(TX/RX/ERROR)注册ISR。
- DMA通道申请:如果设备树中定义了 dmas 属性,则申请DMA通道并配置描述符。
/* 设备树节点示例(dts文件) */
&spi1 {
status = "okay";
compatible = "st,stm32-spi";
pinctrl-0 = <&spi1_sck_pa5 &spi1_miso_pa6 &spi1_mosi_pa7>;
pinctrl-names = "default";
cs-gpios = <&gpiob 6 GPIO_ACTIVE_LOW>;
dmas = <&dma1 3 0x20440 &dma1 2 0x20480>;
dma-names = "tx", "rx";
clock-frequency = <8000000>;
};
/* 驱动初始化宏展开(简化示意) */
#define SPI_DEVICE_INIT(n) \
static struct spi_config spi_cfg_##n = { ... }; \
static struct spi_context spi_ctx_##n = { 0 }; \
DEVICE_DT_INST_DEFINE(n, \
spi_stm32_init, \
NULL, \
&spi_ctx_##n, \
NULL, \
POST_KERNEL, \
CONFIG_SPI_INIT_PRIORITY, \
&spi_stm32_api);
性能优化提示:在设备树中正确配置 dmas 属性是启用DMA模式的前提。如果未定义 dmas,驱动将自动回退到PIO模式。对于大数据量传输(如显示驱动、SD卡读写),务必在设备树中使能DMA支持。
4.6 驱动回调函数链与中断处理流程
Zephyr SPI驱动的中断处理采用分层回调机制:底层ISR → 通用层回调 → 应用层回调。这种设计使得通用层可以在不修改底层驱动的情况下,统一处理事务完成逻辑。
4.7 性能优化实践:DMA与PIO的切换策略
在实际嵌入式系统中,SPI通信的数据长度往往动态变化。Zephyr驱动允许在运行时根据事务长度动态选择PIO或DMA模式,但需要底层驱动实现 transceive 函数时支持两种路径。
推荐策略:在驱动初始化时,根据设备树中是否定义了 dmas 属性来决定是否启用DMA支持。在每次传输前,比较事务长度与阈值(通常为64字节),动态选择传输路径。阈值可通过Kconfig配置,例如
CONFIG_SPI_DMA_THRESHOLD=64。
/* 动态模式选择伪代码 */
static int spi_stm32_transceive(const struct device *dev,
const struct spi_config *config,
const struct spi_buf_set *tx_bufs,
const struct spi_buf_set *rx_bufs)
{
struct spi_context *ctx = dev->data;
size_t total_len = spi_context_total_len(ctx);
if (ctx->dma_supported && (total_len > CONFIG_SPI_DMA_THRESHOLD)) {
/* 使用DMA路径 */
ctx->dma_enabled = true;
return spi_stm32_transceive_dma(dev, config, tx_bufs, rx_bufs);
} else {
/* 使用PIO路径 */
ctx->dma_enabled = false;
return spi_stm32_transceive_pio(dev, config, tx_bufs, rx_bufs);
}
}
通过这种动态切换机制,系统可以在短事务中享受PIO的低延迟优势,在长事务中利用DMA的高吞吐和低CPU占用,实现整体性能的最优化。