1. SPI子系统架构与核心数据结构解析
在嵌入式Linux开发中,SPI总线是最常用的外设接口之一。作为资深嵌入式工程师,我将带大家深入剖析Linux SPI子系统的实现机制。这个子系统设计精妙,通过分层架构将硬件差异与通用逻辑分离,极大简化了驱动开发。
1.1 核心结构体设计原理
1.1.1 从设备抽象:spi_device
这个结构体代表连接到SPI总线上的物理设备,包含设备的所有硬件特性参数。在实际项目中,我们需要特别关注几个关键参数:
c复制struct spi_device {
struct device dev;
struct spi_controller *controller;
u32 max_speed_hz;
u8 chip_select;
u8 bits_per_word;
u16 mode;
//...
};
mode参数详解:在调试SPI设备时,模式设置错误是最常见的问题之一。SPI有四种工作模式,由CPOL和CPHA组合决定:
- MODE0:CPOL=0(空闲时SCK低电平),CPHA=0(第一个边沿采样)
- MODE1:CPOL=0,CPHA=1(第二个边沿采样)
- MODE2:CPOL=1(空闲时SCK高电平),CPHA=0
- MODE3:CPOL=1,CPHA=1
实战经验:很多SPI设备手册会明确要求使用特定模式。我曾遇到一个传感器必须在MODE3下工作,但默认配置是MODE0,导致数据读取全为0。通过示波器抓取波形才发现时钟相位不对。
1.1.2 驱动抽象:spi_driver
这个结构体实现了设备驱动的核心操作:
c复制struct spi_driver {
const struct spi_device_id *id_table;
int (*probe)(struct spi_device *spi);
int (*remove)(struct spi_device *spi);
struct device_driver driver;
};
probe函数要点:
- 通常先验证设备参数(如max_speed_hz是否支持)
- 初始化硬件(配置GPIO、中断等)
- 注册字符设备或其它内核接口
- 分配私有数据结构
避坑指南:在probe中一定要做好错误处理,每个可能失败的操作都要有对应的资源释放。我曾见过因为probe中部分失败但没清理资源,导致内核oops的情况。
1.1.3 控制器抽象:spi_controller
这个结构体抽象了SPI控制器硬件,是子系统中最为复杂的部分:
c复制struct spi_controller {
struct device dev;
s16 bus_num;
u16 num_chipselect;
u16 mode_bits;
u32 bits_per_word_mask;
u32 min_speed_hz;
u32 max_speed_hz;
int (*setup)(struct spi_device *spi);
int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
//...
};
关键能力参数:
- mode_bits:声明支持的SPI模式
- bits_per_word_mask:支持的数据位宽(如8位、16位)
- transfer:核心传输函数指针
性能优化点:现代控制器通常支持DMA传输,在处理大数据量时能显著降低CPU负载。在i.MX6ULL的ECSPI控制器中,启用DMA后传输速度可提升3-5倍。
1.2 数据传输核心结构
1.2.1 spi_transfer:最小传输单元
这个结构体描述一次原子性的数据传输操作:
c复制struct spi_transfer {
const void *tx_buf;
void *rx_buf;
unsigned len;
dma_addr_t tx_dma;
dma_addr_t rx_dma;
u8 bits_per_word;
u16 delay_usecs;
u32 speed_hz;
struct list_head transfer_list;
};
关键字段解析:
- tx_buf/rx_buf:数据缓冲区(虚拟地址)
- tx_dma/rx_dma:DMA物理地址(可选)
- cs_change:控制片选信号行为
- delay_usecs:传输间延时(某些设备需要)
实战技巧:对于全双工传输,tx_buf和rx_buf都需要有效指针;对于半双工,可以其中一个为NULL。我曾用逻辑分析仪抓取波形发现,当rx_buf为NULL时,控制器确实不会读取MISO数据。
1.2.2 spi_message:事务容器
这个结构体将多个spi_transfer组织成一个完整的事务:
c复制struct spi_message {
struct list_head transfers;
struct spi_device *spi;
void (*complete)(void *context);
void *context;
unsigned frame_length;
//...
};
典型工作流程:
- spi_message_init()初始化消息
- spi_message_add_tail()添加多个transfer
- spi_sync()或spi_async()提交传输
性能优化:通过合理设置transfer间的cs_change标志,可以减少片选切换时间。例如连续读取传感器多个寄存器时,保持片选有效可以提高吞吐量。
2. SPI子系统初始化与设备匹配
2.1 设备树配置详解
2.1.1 控制器节点配置
以i.MX6ULL为例,芯片级定义(imx6ull.dtsi):
dts复制ecspi3: ecspi@02010000 {
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02010000 0x4000>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_ECSPI3>;
clock-names = "ipg", "per";
dmas = <&sdma 7 7 1>, <&sdma 8 7 2>;
dma-names = "rx", "tx";
status = "disabled";
};
关键参数解析:
- reg:寄存器地址范围
- interrupts:中断号和触发方式
- dmas:DMA通道配置
- status:默认禁用
板级配置(.dts文件):
dts复制&ecspi3 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi3>;
cs-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>;
status = "okay";
icm20608@0 {
compatible = "invensense,icm20608";
reg = <0>;
spi-max-frequency = <8000000>;
interrupt-parent = <&gpio1>;
interrupts = <1 IRQ_TYPE_EDGE_RISING>;
};
};
配置要点:
- cs-gpios:指定片选GPIO
- 子节点reg属性必须与cs-gpios索引对应
- spi-max-frequency限制设备最大时钟
2.1.2 设备树匹配机制
驱动通过of_match_table声明兼容设备:
c复制static const struct of_device_id icm20608_dt_ids[] = {
{ .compatible = "invensense,icm20608" },
{}
};
MODULE_DEVICE_TABLE(of, icm20608_dt_ids);
static struct spi_driver icm20608_driver = {
.driver = {
.name = "icm20608",
.of_match_table = icm20608_dt_ids,
},
.probe = icm20608_probe,
.remove = icm20608_remove,
};
匹配流程:
- 内核扫描设备树节点
- 比较节点的compatible属性与驱动of_match_table
- 匹配成功后调用probe函数
2.2 核心层初始化流程
2.2.1 子系统初始化(spi_init)
这个函数在Linux启动早期被调用:
- 注册SPI总线类型(bus_register)
- 创建spi_master类(class_register)
- 初始化全局缓冲区
关键数据结构:
- spi_bus_type:管理所有SPI设备和驱动
- spi_master_class:提供/sys/class/spi_master接口
2.2.2 控制器注册(spi_register_controller)
控制器驱动在probe中调用此函数:
- 分配总线编号(bus_num)
- 初始化传输队列
- 创建内核工作线程
- 注册sysfs接口
- 扫描子设备(of_register_spi_devices)
并发控制:
- queue_lock:保护消息队列的自旋锁
- bus_lock_mutex:防止多设备并发访问总线
2.3 核心API工作原理
2.3.1 同步传输(spi_sync)
这个函数实现阻塞式传输:
c复制int spi_sync(struct spi_device *spi, struct spi_message *message)
{
DECLARE_COMPLETION_ONSTACK(done);
int status;
message->complete = spi_complete;
message->context = &done;
status = __spi_queued_transfer(spi, message, false);
if (status == 0)
wait_for_completion(&done);
return status;
}
执行流程:
- 初始化完成量(completion)
- 设置回调函数
- 将消息加入队列
- 等待传输完成
2.3.2 异步传输(spi_async)
非阻塞式传输接口:
c复制int spi_async(struct spi_device *spi, struct spi_message *message)
{
unsigned long flags;
int ret;
spin_lock_irqsave(&spi->controller->queue_lock, flags);
ret = __spi_async(spi, message);
spin_unlock_irqrestore(&spi->controller->queue_lock, flags);
return ret;
}
使用场景:
- 中断上下文中发起传输
- 需要高吞吐量的应用
- 配合DMA实现零拷贝传输
注意事项:异步传输必须设置message->complete回调,且不能在回调中睡眠。
3. 控制器驱动实现分析
3.1 控制器生命周期管理
3.1.1 初始化流程(spi_imx_probe)
以i.MX6ULL的ECSPI驱动为例:
- 获取设备树参数
- 分配spi_controller结构
- 初始化硬件寄存器
- 配置DMA通道(可选)
- 注册中断处理程序
- 启用时钟
关键代码片段:
c复制master = spi_alloc_master(&pdev->dev, sizeof(struct spi_imx_data));
spi_imx = spi_master_get_devdata(master);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
spi_imx->base = devm_ioremap_resource(&pdev->dev, res);
irq = platform_get_irq(pdev, 0);
ret = devm_request_irq(&pdev->dev, irq, spi_imx_isr, 0, dev_name(&pdev->dev), spi_imx);
master->bits_per_word_mask = SPI_BPW_RANGE_MASK(1, 32);
master->setup = spi_imx_setup;
master->transfer_one = spi_imx_transfer_one;
ret = spi_register_master(master);
3.1.2 传输实现(spi_imx_transfer_one)
这个函数处理单个spi_transfer:
- 配置硬件参数(时钟、模式等)
- 根据传输大小选择PIO或DMA模式
- 启动传输并等待完成
PIO模式关键代码:
c复制spi_imx->tx_buf = transfer->tx_buf;
spi_imx->rx_buf = transfer->rx_buf;
spi_imx->count = transfer->len;
reinit_completion(&spi_imx->xfer_done);
spi_imx_push(spi_imx);
spi_imx->devtype_data->intctrl(spi_imx, MXC_INT_TE);
timeout = wait_for_completion_timeout(&spi_imx->xfer_done, transfer_timeout);
3.2 中断处理机制
3.2.1 中断服务程序(spi_imx_isr)
处理传输完成和错误中断:
c复制static irqreturn_t spi_imx_isr(int irq, void *dev_id)
{
struct spi_imx_data *spi_imx = dev_id;
/* 读取接收数据 */
while (spi_imx->devtype_data->rx_available(spi_imx)) {
spi_imx->rx(spi_imx);
spi_imx->txfifo--;
}
/* 填充发送数据 */
if (spi_imx->txfifo && spi_imx->count) {
spi_imx_push(spi_imx);
return IRQ_HANDLED;
}
/* 传输完成 */
if (spi_imx->count == 0) {
spi_imx->devtype_data->intctrl(spi_imx, 0);
complete(&spi_imx->xfer_done);
}
return IRQ_HANDLED;
}
优化技巧:合理设置FIFO阈值可以减少中断次数。例如在i.MX6ULL上,当FIFO深度为64时,设置阈值为16可以在吞吐量和延迟间取得平衡。
3.3 DMA传输实现
3.3.1 DMA配置(spi_imx_sdma_init)
c复制static int spi_imx_sdma_init(struct device *dev, struct spi_imx_data *spi_imx)
{
spi_imx->dma_tx = dma_request_slave_channel(dev, "tx");
spi_imx->dma_rx = dma_request_slave_channel(dev, "rx");
master->can_dma = spi_imx_can_dma;
master->max_dma_len = SPI_IMX_MAX_DMA_LEN;
}
3.3.2 DMA传输(spi_imx_dma_transfer)
c复制desc_rx = dmaengine_prep_slave_sg(master->dma_rx,
transfer->rx_sg.sgl, transfer->rx_sg.nents,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
desc_tx = dmaengine_prep_slave_sg(master->dma_tx,
transfer->tx_sg.sgl, transfer->tx_sg.nents,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
dmaengine_submit(desc_rx);
dmaengine_submit(desc_tx);
dma_async_issue_pending(master->dma_rx);
dma_async_issue_pending(master->dma_tx);
spi_imx->devtype_data->trigger(spi_imx);
性能数据:在i.MX6ULL上测试,传输1KB数据:
- PIO模式:约120μs
- DMA模式:约35μs
4. 设备驱动开发实践
4.1 设备树配置实例
典型SPI设备节点:
dts复制&ecspi1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_ecspi1>;
cs-gpios = <&gpio4 26 GPIO_ACTIVE_LOW>;
status = "okay";
flash: m25p80@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <20000000>;
#address-cells = <1>;
#size-cells = <1>;
};
};
配置要点:
- cs-gpios必须与reg属性对应
- spi-max-frequency不要超过设备规格
- 对于存储设备需要定义address/size cells
4.2 驱动实现框架
4.2.1 驱动注册
c复制static struct spi_driver mydev_driver = {
.driver = {
.name = "mydev",
.of_match_table = mydev_of_match,
},
.probe = mydev_probe,
.remove = mydev_remove,
.id_table = mydev_ids,
};
module_spi_driver(mydev_driver);
4.2.2 probe函数实现
c复制static int mydev_probe(struct spi_device *spi)
{
struct mydev_priv *priv;
int ret;
/* 验证SPI配置 */
if (spi->max_speed_hz > MAX_SPEED) {
dev_err(&spi->dev, "Speed too high\n");
return -EINVAL;
}
/* 分配私有数据 */
priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
spi_set_drvdata(spi, priv);
priv->spi = spi;
/* 初始化硬件 */
ret = mydev_hw_init(priv);
if (ret)
return ret;
/* 注册字符设备 */
ret = mydev_register_cdev(priv);
if (ret)
goto err_hw;
return 0;
err_hw:
mydev_hw_cleanup(priv);
return ret;
}
4.3 数据传输示例
4.3.1 同步传输
c复制int mydev_read_reg(struct spi_device *spi, u8 reg, u8 *val)
{
struct spi_message msg;
struct spi_transfer xfer[2];
u8 tx_buf[2], rx_buf[2];
int ret;
spi_message_init(&msg);
memset(xfer, 0, sizeof(xfer));
tx_buf[0] = reg | 0x80; // 读命令
xfer[0].tx_buf = tx_buf;
xfer[0].len = 1;
spi_message_add_tail(&xfer[0], &msg);
xfer[1].rx_buf = rx_buf;
xfer[1].len = 1;
spi_message_add_tail(&xfer[1], &msg);
ret = spi_sync(spi, &msg);
if (ret == 0)
*val = rx_buf[0];
return ret;
}
4.3.2 异步传输
c复制static void mydev_complete(void *context)
{
struct completion *done = context;
complete(done);
}
int mydev_write_bulk(struct spi_device *spi, const void *buf, size_t len)
{
DECLARE_COMPLETION_ONSTACK(done);
struct spi_message msg;
struct spi_transfer xfer = {
.tx_buf = buf,
.len = len,
};
int ret;
spi_message_init(&msg);
msg.complete = mydev_complete;
msg.context = &done;
spi_message_add_tail(&xfer, &msg);
ret = spi_async(spi, &msg);
if (ret == 0)
wait_for_completion(&done);
return ret;
}
5. 调试技巧与性能优化
5.1 常见问题排查
5.1.1 传输失败排查步骤
- 检查电源和时钟
- 验证SPI模式设置
- 用示波器检查SCK、MOSI、MISO信号
- 检查片选信号是否正常
- 检查DMA缓冲区是否cache对齐
5.1.2 调试工具
- 逻辑分析仪:抓取SPI波形
- sysfs接口:/sys/bus/spi/devices/
- debugfs:/sys/kernel/debug/spi/
- ftrace:跟踪SPI函数调用
5.2 性能优化方法
5.2.1 减少传输开销
- 合并多个小transfer为一个
- 合理设置cs_change减少片选切换
- 使用DMA传输大数据块
5.2.2 时钟优化
- 在不违反设备规格下提高时钟频率
- 调整SPI控制器时钟分频
- 使用双/四线模式提高吞吐量
实测数据:在i.MX6ULL上优化前后对比:
- 优化前:1MB数据约120ms
- 优化后:1MB数据约35ms
5.3 电源管理
5.3.1 休眠唤醒处理
c复制static int mydev_suspend(struct device *dev)
{
struct spi_device *spi = to_spi_device(dev);
struct mydev_priv *priv = spi_get_drvdata(spi);
disable_irq(priv->irq);
mydev_power_down(priv);
return 0;
}
static int mydev_resume(struct device *dev)
{
struct spi_device *spi = to_spi_device(dev);
struct mydev_priv *priv = spi_get_drvdata(spi);
mydev_power_up(priv);
enable_irq(priv->irq);
return 0;
}
static const struct dev_pm_ops mydev_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(mydev_suspend, mydev_resume)
};
6. 高级功能实现
6.1 多从设备管理
6.1.1 设备树配置
dts复制&ecspi2 {
cs-gpios = <&gpio1 29 GPIO_ACTIVE_LOW>,
<&gpio1 28 GPIO_ACTIVE_LOW>;
status = "okay";
device1@0 {
compatible = "vendor,device1";
reg = <0>;
spi-max-frequency = <10000000>;
};
device2@1 {
compatible = "vendor,device2";
reg = <1>;
spi-max-frequency = <5000000>;
};
};
6.1.2 驱动实现要点
- 每个设备有独立的spi_device
- 通过chip_select区分设备
- 避免同时访问多个设备(SPI总线是共享的)
6.2 用户空间访问
6.2.1 spidev接口
dts复制spidev@0 {
compatible = "spidev";
reg = <0>;
spi-max-frequency = <1000000>;
};
用户空间API:
- open():打开设备节点
- ioctl():配置SPI参数
- read()/write():数据传输
6.2.2 自定义字符设备
提供更灵活的接口:
c复制static const struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.read = mydev_read,
.write = mydev_write,
.unlocked_ioctl = mydev_ioctl,
.open = mydev_open,
.release = mydev_release,
};
7. 实战经验总结
7.1 常见问题解决
-
数据错位问题:检查SPI模式设置,特别是CPHA参数。我曾遇到因为CPHA设置错误导致数据采样点不对的情况。
-
DMA传输失败:确保缓冲区是DMA可访问的,使用dma_alloc_coherent分配内存,或者手动调用dma_map_single。
-
性能瓶颈:通过ftrace发现大部分时间花费在中断处理上,通过增大FIFO阈值减少中断次数,吞吐量提升40%。
7.2 最佳实践
-
资源管理:始终使用devm_系列函数分配资源,避免资源泄漏。
-
错误处理:每个可能失败的操作都要有对应的清理代码。
-
并发控制:合理使用自旋锁和互斥锁保护共享数据。
-
电源管理:实现完整的suspend/resume回调,确保低功耗场景正常工作。
7.3 性能调优记录
在最近一个项目中,我们需要从SPI Flash快速读取大量数据。通过以下优化将读取速度从2.5MB/s提升到8MB/s:
- 启用DMA传输
- 将SPI时钟从10MHz提高到50MHz
- 使用四线模式(Quad SPI)
- 优化transfer布局,减少片选切换
最终用逻辑分析仪抓取的波形显示,总线利用率从30%提升到了85%。