1. Linux SPI驱动架构深度解析:从核心层到硬件实现
在嵌入式Linux开发中,SPI总线驱动是最常见的外设驱动之一。理解SPI子系统的完整调用链路,对于开发高质量的设备驱动和排查硬件通信问题至关重要。本文将以i.MX平台为例,深入剖析从应用层调用spi_write()到最终硬件寄存器操作的全过程。
SPI驱动架构采用典型的分层设计:
- 核心层(drivers/spi/spi.c):提供统一的API接口和核心逻辑
- 控制器驱动层(如drivers/spi/spi-imx.c):实现具体硬件操作
- 设备驱动层(如oled_drv.c):提供业务功能接口
这种分层设计使得驱动开发可以各司其职:硬件厂商负责控制器驱动,设备厂商基于标准API开发设备驱动,应用开发者则无需关心底层细节。
2. 完整调用链路拆解
2.1 应用层调用入口
设备驱动通常会封装SPI核心层提供的API,形成更符合业务需求的接口。以OLED屏幕驱动为例:
c复制// oled_drv.c
static void spi_write_datas(const unsigned char *buf, int len)
{
spi_write(oled, buf, len); // oled是已注册的spi_device指针
}
这里的关键点在于:
spi_device结构体在probe时由内核创建并初始化- 每个spi_device都关联到具体的SPI控制器(master)
- 数据传输方向由业务需求决定(本例为只写)
2.2 SPI核心层处理流程
2.2.1 spi_write()的转换
c复制// drivers/spi/spi.c
int spi_write(struct spi_device *spi, const void *buf, size_t len)
{
struct spi_transfer t = {
.tx_buf = buf, // 发送缓冲区
.len = len, // 传输长度
};
return spi_sync_transfer(spi, &t, 1); // 同步传输
}
这个函数完成了三件事:
- 将简单写入操作封装为标准spi_transfer结构
- 指定传输方向为发送(tx_buf非空)
- 触发同步传输流程
注意:spi_transfer是SPI传输的最小单位,可以指定tx/rx缓冲区、长度、时钟速度等参数
2.2.2 消息构造与同步
c复制// drivers/spi/spi.c
int spi_sync_transfer(struct spi_device *spi,
struct spi_transfer *xfers,
unsigned int num_xfers)
{
struct spi_message msg;
spi_message_init(&msg); // 初始化消息
spi_message_add_tail(&xfers[0], &msg); // 添加transfer
return spi_sync(spi, &msg); // 同步传输
}
消息处理的关键设计:
- 一个spi_message可以包含多个spi_transfer
- 消息通过链表管理所有transfer
- 同步传输通过完成量(completion)实现
2.2.3 总线锁与队列处理
c复制// drivers/spi/spi.c
int spi_sync(struct spi_device *spi, struct spi_message *message)
{
mutex_lock(&spi->master->bus_lock_mutex); // 获取总线锁
ret = __spi_sync(spi, message); // 实际同步操作
mutex_unlock(&spi->master->bus_lock_mutex);
return ret;
}
总线锁的作用:
- 防止多个设备同时访问SPI总线
- 保证传输原子性
- 控制器可能支持多硬件片选,但总线是共享的
2.2.4 传输队列化机制
现代SPI驱动普遍采用队列化传输模型:
c复制// drivers/spi/spi.c
static int __spi_sync(...)
{
if (master->transfer == spi_queued_transfer) {
// 队列化传输路径
status = __spi_queued_transfer(spi, message, false);
__spi_pump_messages(master, false); // 处理消息队列
wait_for_completion(&done); // 等待完成
} else {
// 传统直接传输路径
status = spi_async_locked(spi, message);
}
}
队列化传输的优势:
- 提高总线利用率
- 支持消息优先级
- 简化驱动并发处理
2.3 控制器驱动实现
2.3.1 关键函数指针注册
控制器驱动的核心是实现并注册一系列回调函数:
c复制// drivers/spi/spi-imx.c
static int spi_imx_probe(struct platform_device *pdev)
{
master->transfer_one = spi_imx_transfer_one;
master->prepare_transfer_hardware = spi_imx_prepare_hardware;
master->unprepare_transfer_hardware = spi_imx_unprepare_hardware;
return spi_register_master(master);
}
这些函数指针构成了核心层与硬件驱动的桥梁:
- transfer_one:实现单次传输
- prepare/unprepare:硬件准备/释放
- 其他可选回调:设置片选、DMA配置等
2.3.2 传输核心实现
i.MX SPI控制器的传输函数:
c复制// drivers/spi/spi-imx.c
static int spi_imx_transfer_one(...)
{
// 1. 配置传输参数
spi_imx->speed_hz = transfer->speed_hz;
spi_imx->bits_per_word = transfer->bits_per_word;
// 2. 硬件配置
spi_imx_config(spi_imx);
// 3. 选择传输方式
if (spi_imx->usedma) {
ret = spi_imx_dma_transfer(spi_imx, transfer);
} else {
spi_imx_push(spi_imx); // PIO模式
wait_for_completion(&spi_imx->xfer_done);
}
return ret;
}
传输方式选择策略:
- DMA:适合大数据量传输(通常>32字节)
- PIO:小数据量更高效
- 实际阈值因控制器而异
2.3.3 硬件寄存器操作
配置SPI控制器的典型操作:
c复制// drivers/spi/spi-imx.c
static int spi_imx_config(struct spi_imx_data *spi_imx)
{
// 计算时钟分频
clkdiv = spi_imx_clkdiv_2(spi_imx->spi_clk, spi_imx->speed_hz);
// 配置控制寄存器
ctrl |= (spi_imx->bits_per_word - 1) << MX51_ECSPI_CTRL_BL_OFFSET;
if (spi_imx->spi->mode & SPI_CPHA) ctrl |= MX51_ECSPI_CTRL_PHA;
if (spi_imx->spi->mode & SPI_CPOL) ctrl |= MX51_ECSPI_CTRL_POL;
// 写入寄存器
writel(ctrl, spi_imx->base + MX51_ECSPI_CTRL);
writel(clkdiv, spi_imx->base + MX51_ECSPI_CONFIG);
}
关键寄存器操作:
- 时钟配置:根据请求速度计算分频值
- 模式设置:CPOL/CPHA相位配置
- 数据位宽:bits_per_word参数处理
2.3.4 中断驱动传输
PIO模式下的中断处理流程:
c复制// drivers/spi/spi-imx.c
static irqreturn_t spi_imx_isr(int irq, void *dev_id)
{
// 读取接收数据
while (spi_imx->txfifo > 0) {
spi_imx->rx(spi_imx); // 从RX FIFO读取
spi_imx->txfifo--;
}
// 检查是否完成
if (spi_imx->count == 0) {
spi_imx_intctrl(spi_imx, 0); // 禁用中断
complete(&spi_imx->xfer_done); // 通知完成
} else {
spi_imx_push(spi_imx); // 继续发送
}
return IRQ_HANDLED;
}
中断处理要点:
- 读取RX数据并减少FIFO计数
- 检查传输完成条件
- 完成通知或继续发送
3. 关键数据结构解析
3.1 核心数据结构关系
c复制struct spi_device {
struct spi_master *master; // 关联的控制器
// 设备特定参数...
};
struct spi_master {
int (*transfer_one)(struct spi_master *master,
struct spi_device *spi,
struct spi_transfer *transfer);
// 其他操作回调...
};
struct spi_transfer {
const void *tx_buf;
void *rx_buf;
unsigned len;
// 传输参数...
};
struct spi_message {
struct list_head transfers; // transfer链表
void (*complete)(void *context); // 完成回调
// 状态信息...
};
3.2 i.MX专用数据结构
c复制struct spi_imx_data {
void __iomem *base; // 寄存器基地址
struct clk *clk; // 时钟
// 传输状态
unsigned count;
const u8 *tx_buf;
u8 *rx_buf;
// DMA相关
struct completion dma_tx_completion;
struct completion dma_rx_completion;
// 硬件特性
unsigned int txfifo; // FIFO大小
// ...
};
4. 开发实践与调试技巧
4.1 驱动开发注意事项
-
时钟配置:
- 确保SPI控制器时钟使能
- 正确计算分频值以满足设备要求
- 实测时钟信号确认无抖动
-
GPIO复用:
- 正确配置引脚复用为SPI功能
- 检查硬件手册确定引脚编号
- 必要时配置上拉/下拉电阻
-
DMA缓冲区:
- 使用dma_alloc_coherent分配DMA缓冲区
- 确保缓冲区按cache行对齐
- 必要时进行cache同步操作
4.2 常见问题排查
-
无数据传输:
- 检查片选信号是否正常
- 确认SPI控制器已使能
- 验证时钟信号是否存在
-
数据错位:
- 检查CPOL/CPHA模式设置
- 确认bits_per_word匹配设备要求
- 验证MSB/LSB顺序设置
-
性能问题:
- 检查是否启用DMA传输
- 优化transfer拆分策略
- 调整FIFO阈值设置
4.3 调试手段
-
内核日志:
bash复制echo 7 > /proc/sys/kernel/printk # 提高日志级别 dmesg | grep spi # 过滤SPI相关日志 -
逻辑分析仪:
- 抓取实际总线波形
- 验证时序参数
- 检查数据内容
-
sysfs调试接口:
bash复制ls /sys/bus/spi/devices/ # 查看SPI设备 cat /sys/kernel/debug/spi/spiX/regs # 查看寄存器(需内核配置)
5. 性能优化策略
5.1 传输模式选择
| 传输模式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| PIO | 小数据量(<32B) | 延迟低 | CPU占用高 |
| DMA | 大数据量(>32B) | 解放CPU | 设置复杂 |
| 轮询 | 极低延迟需求 | 响应快 | 浪费CPU |
5.2 消息合并优化
通过合并多个小transfer减少开销:
c复制// 不推荐:多个小transfer
spi_message_init(&msg);
spi_message_add_tail(&xfer1, &msg);
spi_message_add_tail(&xfer2, &msg);
spi_sync(spi, &msg);
// 推荐:合并为一个transfer
struct spi_transfer xfer = {
.tx_buf = big_buf,
.len = total_len,
};
spi_sync_transfer(spi, &xfer, 1);
5.3 异步传输模式
对于实时性要求不高的场景,可以使用异步API提高吞吐量:
c复制static void complete_cb(void *context)
{
// 处理传输完成
}
void async_transfer(struct spi_device *spi)
{
spi_message_init(&msg);
msg.complete = complete_cb;
msg.context = dev;
spi_message_add_tail(&xfer, &msg);
spi_async(spi, &msg); // 非阻塞调用
}
6. 跨平台开发考量
虽然SPI核心层提供了统一接口,但不同平台仍有差异需要注意:
-
时钟配置方式:
- i.MX使用分频系数
- 某些平台使用直接频率值
-
FIFO特性:
- FIFO深度不同(4/8/16/64等)
- 触发阈值可调性
-
DMA支持:
- 通道分配机制
- 突发传输能力
-
电源管理:
- 运行时PM实现
- 挂起/恢复处理
在编写跨平台驱动时,建议:
- 使用SPI核心层提供的通用API
- 通过设备树传递硬件特性参数
- 对平台特定代码进行良好封装