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字符串写错,结果驱动死活不加载,查了两天才发现少了个字母。嗯,这种坑踩过一次就记住了。

小技巧: 调试驱动加载时,可以在probe函数开头加一句 dev_info(&pdev->dev, "probe enter\n");。如果内核日志没看到这行,说明根本没匹配上,先查设备树和compatible。

4.2 SPI控制器注册与注销:让内核知道“我在这”

probe函数里最核心的一步,就是调用spi_register_controller()。这个函数会把你的控制器注册到SPI核心层,之后SPI设备才能找到它。

我一般会在probe里做这几件事,顺序很重要:

  1. 获取资源:通过platform_get_resource()拿到寄存器基地址、中断号。
  2. ioremap:把物理地址映射到虚拟地址空间。
  3. 初始化硬件:复位控制器、配置时钟极性、设置FIFO阈值。
  4. 分配并填充spi_controller:设置transfer_one、setup、prepare_message等回调。
  5. 注册控制器:调用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队列管理,是一条完整的主线:

SPI控制器驱动开发核心流程 platform_driver 匹配 probe: 资源获取 + 硬件初始化 spi_register_controller() transfer_one / prepare_message DMA描述符提交 + 队列管理 关键:devm_资源管理 → 避免泄漏 | DMA burst与FIFO深度匹配 → 性能最优

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停止
曾经踩过的坑: 有一次我在一个项目里用了非devm_版本的注册函数,结果驱动卸载时忘了调用spi_unregister_controller()。第二次insmod的时候,内核报错说控制器已存在。从那以后,我所有新代码都强制用devm_spi_register_controller(),省心多了。

嗯,这一章的内容就到这里。platform_driver框架是骨架,控制器注册是血肉,DMA队列管理是灵魂。把这三块吃透了,SPI控制器驱动开发对你来说就是熟练工种了。