1. SPI子系统概述与Linux-4.9.88版本特性
SPI(Serial Peripheral Interface)是一种同步串行通信接口协议,广泛应用于嵌入式系统中连接微控制器与各种外设。Linux内核从2.6版本开始引入SPI子系统,经过多年迭代,到4.9.88版本已经形成了一套完整的驱动框架。
这个长期支持版本(LTS)的SPI子系统有几个关键改进:
- 引入了更高效的DMA传输机制
- 优化了多设备片选(chip select)的管理策略
- 增强了对SPI从设备电源管理的支持
- 改进了SPI控制器的时钟配置精度
提示:选择4.9.88版本进行研究特别适合工业级应用开发,这个LTS版本在稳定性和新特性之间取得了良好平衡。
2. SPI子系统架构深度解析
2.1 核心组件与数据流
Linux SPI子系统采用典型的分层架构设计,主要包含以下组件:
-
硬件抽象层(Controller Driver)
- 直接操作SPI控制器硬件寄存器
- 处理时钟、模式等底层配置
- 实现DMA/中断/PIO传输方式
-
核心层(SPI Core)
- 提供统一的API接口
- 管理SPI总线及设备列表
- 实现传输队列调度
-
协议驱动层(Protocol Driver)
- 实现具体设备通信协议
- 如触摸屏、Flash、传感器等
-
用户空间接口
- 通过sysfs、debugfs暴露配置信息
- 提供spidev字符设备接口
数据传输流程示例:
code复制用户空间请求
↓
spidev或具体设备驱动
↓
SPI核心层(消息队列)
↓
控制器驱动(硬件操作)
2.2 关键数据结构解析
在include/linux/spi/spi.h中定义的核心结构体:
c复制struct spi_device {
struct device dev; // 设备模型基类
struct spi_master *master; // 所属控制器
u32 max_speed_hz; // 最大时钟频率
u8 chip_select; // 片选信号线
u8 bits_per_word; // 每字位数
u16 mode; // 时钟极性/相位等模式
// ...
};
struct spi_transfer {
const void *tx_buf; // 发送缓冲区
void *rx_buf; // 接收缓冲区
unsigned len; // 传输长度
// ...
};
struct spi_message {
struct list_head transfers; // 传输列表
// ...
};
注意:在4.9.88版本中,spi_master结构体已被重命名为spi_controller,但保留了兼容定义。
3. SPI控制器驱动开发实践
3.1 驱动注册与初始化
典型的控制器驱动注册流程:
c复制static int my_spi_probe(struct platform_device *pdev)
{
struct spi_controller *ctlr;
struct my_private_data *priv;
// 1. 分配控制器结构体
ctlr = spi_alloc_master(&pdev->dev, sizeof(*priv));
// 2. 初始化私有数据
priv = spi_controller_get_devdata(ctlr);
priv->regs = devm_platform_ioremap_resource(pdev, 0);
// 3. 设置操作方法
ctlr->setup = my_spi_setup;
ctlr->transfer_one = my_spi_transfer_one;
ctlr->prepare_transfer_hardware = my_spi_prepare_hw;
// 4. 注册控制器
return devm_spi_register_controller(&pdev->dev, ctlr);
}
关键操作函数说明:
setup(): 配置SPI模式、时钟等参数transfer_one(): 执行单次传输prepare_transfer_hardware(): 硬件准备回调
3.2 DMA传输实现技巧
在4.9.88版本中实现DMA传输的推荐做法:
- 在probe函数中申请DMA通道:
c复制priv->dma_tx = dma_request_chan(&pdev->dev, "tx");
priv->dma_rx = dma_request_chan(&pdev->dev, "rx");
- 实现DMA传输回调:
c复制static void my_spi_dma_callback(void *data)
{
struct spi_controller *ctlr = data;
complete(&priv->dma_completion);
}
- 在transfer_one中配置DMA描述符:
c复制struct dma_async_tx_descriptor *txdesc, *rxdesc;
txdesc = dmaengine_prep_slave_sg(priv->dma_tx, tx_sg, tx_nents,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
rxdesc = dmaengine_prep_slave_sg(priv->dma_rx, rx_sg, rx_nents,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
经验:DMA缓冲区必须使用dma_alloc_coherent()分配,普通kmalloc分配的缓冲区需要先映射。
4. SPI设备驱动开发指南
4.1 设备树配置示例
典型SPI设备在设备树中的定义:
code复制&spi1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi1_pins>;
flash@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <50000000>;
#address-cells = <1>;
#size-cells = <1>;
};
sensor@1 {
compatible = "vendor,spi-sensor";
reg = <1>;
spi-cpol; // 时钟极性高
spi-cpha; // 时钟相位第二个边沿采样
};
};
4.2 协议驱动实现要点
一个简单的SPI设备驱动框架:
c复制static int mydev_probe(struct spi_device *spi)
{
struct mydev_priv *priv;
// 1. 验证设备参数
if (spi->max_speed_hz > MAX_FREQ) {
dev_err(&spi->dev, "频率过高");
return -EINVAL;
}
// 2. 分配私有数据结构
priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
// 3. 初始化硬件
mydev_hw_init(spi);
// 4. 注册字符设备/输入子系统等
// ...
return 0;
}
static const struct of_device_id mydev_of_match[] = {
{ .compatible = "vendor,spi-sensor" },
{},
};
MODULE_DEVICE_TABLE(of, mydev_of_match);
static struct spi_driver mydev_driver = {
.driver = {
.name = "mydev",
.of_match_table = mydev_of_match,
},
.probe = mydev_probe,
.remove = mydev_remove,
};
module_spi_driver(mydev_driver);
5. 性能优化与调试技巧
5.1 传输性能调优
实测有效的优化手段:
- 批量传输:合并多个小传输为单个spi_message
c复制struct spi_message msg;
struct spi_transfer xfers[4];
spi_message_init(&msg);
for (i = 0; i < 4; i++) {
memset(&xfers[i], 0, sizeof(xfers[i]));
xfers[i].len = 16;
spi_message_add_tail(&xfers[i], &msg);
}
ret = spi_sync(spi, &msg);
- 调整队列深度:
c复制// 在控制器驱动中设置
ctlr->queue_depth = 16;
- 合理设置DMA阈值:
c复制// 当传输长度超过此值时启用DMA
ctlr->dma_threshold = 64;
5.2 调试方法大全
- sysfs调试接口:
code复制/sys/bus/spi/devices/spiX.Y/
├── modalias - 设备兼容性标识
├── of_node - 设备树节点链接
├── power - 电源管理状态
└── statistics/
├── transfers - 传输计数
└── errors - 错误计数
- 动态调试:
bash复制# 启用SPI核心调试信息
echo -n 'file spi.c +p' > /sys/kernel/debug/dynamic_debug/control
# 启用特定控制器驱动调试
echo -n 'file my_spi.c +p' > /sys/kernel/debug/dynamic_debug/control
- 示波器实测技巧:
- 测量SCK时钟频率是否与配置一致
- 检查CS信号在传输间隔是否保持无效状态
- 验证MOSI/MISO数据与软件发送/接收是否匹配
6. 常见问题解决方案
6.1 传输超时问题排查
典型症状:
- 内核日志出现"timeout"错误
- 系统响应变慢或卡死
排查步骤:
-
检查硬件连接:
- 确认所有信号线(SCK/MOSI/MISO/CS)连接正确
- 测量电源电压是否稳定
-
验证时钟配置:
c复制// 在控制器驱动中添加调试打印 dev_dbg(&ctlr->dev, "Requested freq: %u, actual: %u\n", spi->max_speed_hz, ctlr->max_speed_hz); -
检查DMA配置:
- 确保dmaengine驱动已加载
- 验证DMA缓冲区物理地址是否有效
6.2 数据错位问题处理
可能原因及解决方案:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 接收数据高位丢失 | bits_per_word配置错误 | 检查设备与驱动的位宽设置是否一致 |
| 数据字节顺序颠倒 | 端序不匹配 | 在驱动中添加swab16/swab32转换 |
| 偶发性数据错误 | 信号干扰 | 缩短走线长度,添加滤波电容 |
6.3 多设备冲突处理
当多个SPI设备共享总线时,需特别注意:
- 确保CS信号互斥:
c复制// 在设备驱动probe函数中添加
spi->controller->flags |= SPI_CONTROLLER_GPIO_SS;
spi->cs_gpio = of_get_named_gpio(np, "cs-gpio", 0);
- 合理设置传输延迟:
c复制// 在设备树中配置
spi-delay-us = <5>; // 片选激活延迟
- 实现正确的电源管理:
c复制static int mydev_runtime_suspend(struct device *dev)
{
struct spi_device *spi = to_spi_device(dev);
// 禁用片选
gpiod_set_value(spi->cs_gpiod, 1);
return 0;
}
在实际项目中,我发现SPI子系统的稳定性很大程度上取决于硬件设计的合理性。一个常见的陷阱是忽略了SPI总线的上拉电阻配置,这会导致在高频传输时出现数据错误。建议在硬件设计阶段就确保:
- 所有SPI信号线都有适当的上拉/下拉电阻
- 电源去耦电容尽可能靠近芯片引脚
- 信号走线长度尽量短且等长
对于需要长时间运行的工业应用,建议在驱动中添加健康监测机制,比如定期检查传输错误计数,并在超过阈值时自动重置SPI控制器。这可以显著提高系统的可靠性。