4. SPI控制器驱动开发:从框架到DMA队列
好,咱们进入第四章。这一章是SPI驱动开发的核心环节——控制器驱动。说白了,就是让Linux内核认识你的SPI硬件,并且能高效地把数据送出去、收回来。
我个人习惯把控制器驱动分成三层来看:驱动框架绑定、控制器注册与注销、DMA传输队列管理。这三层搞定了,一个能用的SPI控制器驱动就成型了。
4.1 platform_driver框架:驱动怎么“挂”到内核里
现在的嵌入式SoC,SPI控制器大多挂在AMBA总线上,但在Linux驱动模型里,它们通常被抽象为platform_device。为什么?因为SPI控制器的地址、中断这些资源,都是通过platform机制传递的。
驱动入口很简单,就是一个platform_driver结构体:
static const struct of_device_id spi_imx_dt_ids[] = {
{ .compatible = "fsl,imx6ul-ecspi", .data = &imx6ul_ecspi_data },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_imx_dt_ids);
static struct platform_driver spi_imx_driver = {
.driver = {
.name = "spi_imx",
.of_match_table = spi_imx_dt_ids,
.pm = &spi_imx_pm_ops,
},
.probe = spi_imx_probe,
.remove = spi_imx_remove,
};
module_platform_driver(spi_imx_driver);
这里有个细节——of_match_table。设备树里写compatible = "fsl,imx6ul-ecspi",内核匹配到这个驱动就会调用probe。我在项目中遇到过有人把compatible字符串写错,结果驱动死活不加载,查了两天才发现少了个字母。嗯,这种坑踩过一次就记住了。
dev_info(&pdev->dev, "probe enter\n");。如果内核日志没看到这行,说明根本没匹配上,先查设备树和compatible。
4.2 SPI控制器注册与注销:让内核知道“我在这”
probe函数里最核心的一步,就是调用spi_register_controller()。这个函数会把你的控制器注册到SPI核心层,之后SPI设备才能找到它。
我一般会在probe里做这几件事,顺序很重要:
- 获取资源:通过
platform_get_resource()拿到寄存器基地址、中断号。 - ioremap:把物理地址映射到虚拟地址空间。
- 初始化硬件:复位控制器、配置时钟极性、设置FIFO阈值。
- 分配并填充spi_controller:设置transfer_one、setup、prepare_message等回调。
- 注册控制器:调用
devm_spi_register_controller()。
看一个典型的probe片段:
static int spi_imx_probe(struct platform_device *pdev)
{
struct spi_controller *ctlr;
struct spi_imx_data *spi_imx;
struct resource *res;
int ret;
ctlr = devm_spi_alloc_master(&pdev->dev, sizeof(*spi_imx));
if (!ctlr)
return -ENOMEM;
spi_imx = spi_controller_get_devdata(ctlr);
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
spi_imx->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(spi_imx->base))
return PTR_ERR(spi_imx->base);
spi_imx->irq = platform_get_irq(pdev, 0);
if (spi_imx->irq < 0)
return spi_imx->irq;
/* 设置控制器回调 */
ctlr->bus_num = pdev->id;
ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
ctlr->transfer_one = spi_imx_transfer_one;
ctlr->setup = spi_imx_setup;
ctlr->prepare_message = spi_imx_prepare_message;
ctlr->unprepare_message = spi_imx_unprepare_message;
ctlr->num_chipselect = 4;
ctlr->dev.of_node = pdev->dev.of_node;
/* 注册到SPI核心 */
ret = devm_spi_register_controller(&pdev->dev, ctlr);
if (ret) {
dev_err(&pdev->dev, "failed to register SPI controller\n");
return ret;
}
dev_info(&pdev->dev, "SPI controller registered, bus %d\n", ctlr->bus_num);
return 0;
}
devm_spi_register_controller()而不是spi_register_controller()。前者是设备资源管理版本,驱动卸载时会自动注销控制器,避免你忘记调用spi_unregister_controller()导致内存泄漏。我曾经在早期内核版本里吃过这个亏,后来就只用devm_系列函数了。
注销部分相对简单。如果用devm_系列,remove函数里只需要做硬件相关的清理,比如关闭时钟、释放DMA通道:
static int spi_imx_remove(struct platform_device *pdev)
{
struct spi_controller *ctlr = platform_get_drvdata(pdev);
struct spi_imx_data *spi_imx = spi_controller_get_devdata(ctlr);
/* 关闭控制器时钟 */
clk_disable_unprepare(spi_imx->clk);
/* 释放DMA资源 */
if (spi_imx->dma_rx)
dma_release_channel(spi_imx->dma_rx);
if (spi_imx->dma_tx)
dma_release_channel(spi_imx->dma_tx);
return 0;
}
4.3 DMA传输队列管理:让数据飞起来
SPI传输的瓶颈往往在CPU。每次发一个字节都要CPU去写寄存器,效率太低了。DMA就是来解决这个问题的——让DMA控制器帮你搬数据,CPU可以干别的事。
DMA传输队列管理,说白了就是怎么把多个SPI传输请求组织起来,交给DMA去执行。我习惯用struct spi_transfer里的tx_dma和rx_dma字段,配合DMA engine的API来操作。
先看一个DMA传输的初始化流程:
static int spi_imx_dma_init(struct spi_imx_data *spi_imx)
{
struct dma_slave_config rx_config = {0};
struct dma_slave_config tx_config = {0};
/* 请求DMA通道 */
spi_imx->dma_tx = dma_request_chan(&spi_imx->pdev->dev, "tx");
if (IS_ERR(spi_imx->dma_tx))
return PTR_ERR(spi_imx->dma_tx);
spi_imx->dma_rx = dma_request_chan(&spi_imx->pdev->dev, "rx");
if (IS_ERR(spi_imx->dma_rx)) {
dma_release_channel(spi_imx->dma_tx);
return PTR_ERR(spi_imx->dma_rx);
}
/* 配置DMA从设备参数 */
rx_config.direction = DMA_DEV_TO_MEM;
rx_config.src_addr = spi_imx->base_phys + MXC_CSPIRXDATA;
rx_config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
rx_config.src_maxburst = 8;
tx_config.direction = DMA_MEM_TO_DEV;
tx_config.dst_addr = spi_imx->base_phys + MXC_CSPITXDATA;
tx_config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
tx_config.dst_maxburst = 8;
dmaengine_slave_config(spi_imx->dma_tx, &tx_config);
dmaengine_slave_config(spi_imx->dma_rx, &rx_config);
return 0;
}
这里有个关键参数——src_maxburst和dst_maxburst。它决定了DMA每次触发传输多少个数据单元。我建议根据SPI FIFO深度来设置。比如FIFO深度是16,burst设成8就比较合理,既能充分利用FIFO,又不会让DMA占用总线太久。
接下来是传输队列管理的核心——transfer_one回调。当SPI核心层要发起一次传输时,会调用这个函数。我们需要在这里构造DMA描述符并提交:
static int spi_imx_transfer_one(struct spi_controller *ctlr,
struct spi_device *spi,
struct spi_transfer *transfer)
{
struct spi_imx_data *spi_imx = spi_controller_get_devdata(ctlr);
struct dma_async_tx_descriptor *tx_desc, *rx_desc;
unsigned long flags = DMA_CTRL_ACK | DMA_PREP_INTERRUPT;
/* 构造DMA传输描述符 */
tx_desc = dmaengine_prep_slave_sg(spi_imx->dma_tx,
transfer->tx_sg.sgl,
transfer->tx_sg.nents,
DMA_MEM_TO_DEV,
flags);
if (!tx_desc)
goto dma_err;
rx_desc = dmaengine_prep_slave_sg(spi_imx->dma_rx,
transfer->rx_sg.sgl,
transfer->rx_sg.nents,
DMA_DEV_TO_MEM,
flags);
if (!rx_desc)
goto dma_err;
/* 设置DMA完成回调 */
rx_desc->callback = spi_imx_dma_rx_callback;
rx_desc->callback_param = spi_imx;
/* 提交DMA传输 */
dmaengine_submit(tx_desc);
dmaengine_submit(rx_desc);
/* 启动DMA */
dma_async_issue_pending(spi_imx->dma_tx);
dma_async_issue_pending(spi_imx->dma_rx);
return 0;
dma_err:
dev_err(&spi_imx->pdev->dev, "DMA prep failed\n");
return -EIO;
}
- 描述符复用:如果传输频繁,可以考虑预分配DMA描述符池,避免每次传输都动态分配。
- 回调处理:DMA完成回调里要调用
spi_finalize_current_transfer()通知SPI核心层传输结束。 - 错误恢复:DMA传输失败时,需要重置DMA通道状态,必要时复位SPI控制器硬件。
你想想看,如果没有DMA,每次传输CPU都要中断处理每个字节。有了DMA队列管理,CPU只需要提交一次描述符,剩下的全是硬件在跑。对于大块数据(比如SPI Flash读写),性能提升是数量级的。
4.4 知识结构总览
下面这张图把本章的核心逻辑串起来了。从platform_driver匹配开始,到控制器注册,再到DMA队列管理,是一条完整的主线:
4.5 避坑指南与经验总结
最后,分享几个我在实际项目中踩过的坑:
| 问题场景 | 现象 | 根因与解决方案 |
|---|---|---|
| DMA传输卡死 | 传输发起后无完成中断 | 检查DMA通道的dma_cap_mask_t是否包含DMA_SLAVE能力;确认slave_config中的地址和宽度正确 |
| SPI时钟相位错误 | 从设备通信乱码 | 在setup回调里根据spi->mode正确配置CPOL/CPHA寄存器 |
| FIFO溢出 | 数据丢失或CRC错误 | DMA burst设得太大,超过FIFO深度;建议burst ≤ FIFO深度/2 |
| 驱动卸载崩溃 | rmmod时内核oops | remove函数里没有先停止DMA通道;调用dmaengine_terminate_sync()确保DMA停止 |
spi_unregister_controller()。第二次insmod的时候,内核报错说控制器已存在。从那以后,我所有新代码都强制用devm_spi_register_controller(),省心多了。
嗯,这一章的内容就到这里。platform_driver框架是骨架,控制器注册是血肉,DMA队列管理是灵魂。把这三块吃透了,SPI控制器驱动开发对你来说就是熟练工种了。