SPI(Serial Peripheral Interface)作为一种同步串行通信协议,在嵌入式领域扮演着至关重要的角色。Linux内核从早期版本就开始支持SPI子系统,但直到4.x系列才形成了相对成熟稳定的架构。选择4.9.88这个长期支持(LTS)版本进行研究具有特殊意义——它既保留了经典SPI框架的稳定特性,又引入了若干关键性改进。
在实际项目中,我发现这个版本的SPI子系统最显著的特点是它的分层架构已经非常清晰:
注意:虽然4.9.88是LTS版本,但其SPI子系统与最新内核仍有差异。例如缺少SPI memory控制器等新特性,但在常规应用中完全够用。
在深入研究代码后,我绘制了关键数据结构的关系图(以下为文字描述):
spi_master结构体是控制器的软件抽象,每个物理SPI控制器对应一个实例spi_device代表连接的从设备,包含CS引脚、模式等配置spi_message是传输的基本单位,可包含多个spi_transfer这种设计最精妙之处在于spi_message的链表结构。在一次项目调试中,我需要连续发送配置命令后读取传感器数据。通过构建包含两个transfer的message,实现了无间隔的连续传输,避免了传统方式中手动控制CS引脚的电平切换。
数据在SPI子系统中的流动路径值得深入研究:
transfer_one_message回调处理我曾遇到过传输速率不稳定的问题,通过插入printk语句跟踪发现是DMA缓冲区对齐问题。在4.9.88版本中,需要特别注意dma_alloc_coherent申请的内存地址是否满足硬件要求。后来我在驱动初始化时添加了对齐检查代码:
c复制if (!IS_ALIGNED(dma_buf, 4)) {
dev_warn(&pdev->dev, "DMA buffer not 4-byte aligned!");
return -EINVAL;
}
编写控制器驱动时,寄存器操作是最基础也是最重要的部分。以常见的Synopsys DesignWare SPI控制器为例:
c复制static void dw_spi_hw_init(struct dw_spi *dws)
{
spi_enable_chip(dws, 0); // 先禁用控制器
// 设置时钟分频
dw_writew(dws, DW_SPI_BAUDR, div);
// 配置传输模式
cr0 = (dws->mode << SPI_CR0_MODE_SHIFT) |
(dws->type << SPI_CR0_TYPE_SHIFT);
dw_writew(dws, DW_SPI_CTRLR0, cr0);
spi_enable_chip(dws, 1); // 重新启用
}
这段代码看似简单,但有几个关键细节:
在高速传输场景下,DMA能显著降低CPU负载。4.9.88内核的SPI DMA支持已经比较完善,但需要特别注意:
我曾通过调整DMA watermark将传输效率提升了30%:
c复制// 在probe函数中添加
dws->dma_tx.watermark = 16;
dws->dma_rx.watermark = 12;
Linux SPI设备驱动的标准模板包含以下要素:
c复制static const struct of_device_id mydev_dt_ids[] = {
{ .compatible = "vendor,mydevice" },
{}
};
static struct spi_driver mydev_driver = {
.driver = {
.name = "mydevice",
.of_match_table = mydev_dt_ids,
},
.probe = mydev_probe,
.remove = mydev_remove,
};
在真实项目中,我发现很多开发者容易忽略of_match_table的配置。在设备树普及的今天,正确设置compatible字符串是驱动能正常加载的前提。
SPI传输模式的选择直接影响性能。通过实测比较不同模式:
| 模式 | 吞吐量(MB/s) | CPU占用率 |
|---|---|---|
| 轮询 | 2.1 | 100% |
| 中断 | 1.8 | 45% |
| DMA | 3.5 | 15% |
虽然DMA模式性能最优,但在小数据量传输时反而会有额外开销。我的经验法则是:
512字节:启用DMA
根据实际项目经验整理的典型问题:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 传输超时 | 时钟配置错误 | 检查分频系数 |
| 数据错位 | 模式不匹配 | 确认CPOL/CPHA |
| 随机错误 | 电源噪声 | 增加去耦电容 |
| DMA失败 | 内存不对齐 | 检查dma_alloc参数 |
当软件调试手段失效时,硬件工具能提供关键信息。我的调试步骤通常是:
在一次棘手的问题排查中,正是通过逻辑分析仪发现CS信号有约50ns的抖动,最终定位到是PCB走线过长导致。这个案例让我养成了在驱动中添加软件延时补偿的习惯:
c复制static void mydev_cs_control(struct spi_device *spi, bool on)
{
// 添加CS切换延时
if (!on) ndelay(100);
gpio_set_value(cs_pin, !on);
if (on) ndelay(100);
}
对于大数据量传输,传统的copy_to_user方式会成为性能瓶颈。可以通过mmap实现零拷贝:
c复制static int mydev_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct mydev_data *data = filp->private_data;
return dma_mmap_coherent(&data->pdev->dev, vma,
data->dma_buf, data->dma_handle, vma->vm_end - vma->vm_start);
}
这种技术在高帧率图像传感器数据传输中效果显著,我在一个工业相机项目中实测吞吐量提升了5倍。
当单个SPI控制器挂载多个设备时,合理的仲裁策略很重要。我总结的最佳实践包括:
一个典型的调度函数实现:
c复制static void schedule_spi_transfers(struct work_struct *work)
{
list_sort(NULL, &queue_head, compare_priority);
list_for_each_entry(msg, &queue_head, queue) {
if (time_after(jiffies, deadline))
break;
spi_async(msg->spi, msg);
}
}
在最近的一个物联网网关项目中,我们需要同时管理多个SPI设备:
遇到的挑战包括:
解决方案是实现了动态时钟调整机制:
c复制static void adjust_clock(struct spi_device *spi)
{
struct spi_master *master = spi->master;
switch (spi->chip_select) {
case 0: // 无线模块需要高速
master->max_speed_hz = 8000000;
break;
case 1: // 传感器需要精确时钟
master->max_speed_hz = 1000000;
break;
}
spi_setup(spi); // 重新配置
}
这个案例让我深刻理解了spi_setup()调用的重要性——它不只是初始化时使用,运行时配置变更也需要调用。