4. 驱动层设计:Linux/RTOS下的SPI控制器驱动框架

驱动层设计,说白了就是让操作系统认识你的硬件。我做了这么多年存储,发现很多工程师把精力都花在应用层,结果驱动一跑就崩。嗯,今天咱们就把SPI控制器驱动这个地基打牢。

4.1 驱动框架的整体脉络

先看一张图,把整个流程串起来。你想想看,一个驱动从加载到卸载,到底经历了什么?

核心流程:模块注册 → 设备匹配 → probe探测 → 设备初始化 → 业务操作 → remove卸载

SPI控制器驱动生命周期 module_init platform_driver_register probe() remove() probe()中完成:IO资源申请 → 时钟使能 → SPI控制器初始化 → 注册字符/块设备 remove()中完成:设备注销 → 资源释放 → 时钟关闭 probe()内部步骤 1. 获取设备资源 2. 映射寄存器地址 3. 初始化SPI控制器 remove()内部步骤 1. 停止数据传输 2. 注销设备节点 3. 释放IO/内存资源 常见坑点 资源泄漏 中断未释放 DMA残留

4.2 模块注册与卸载

驱动入口,就是 module_init 和 module_exit。我个人习惯把这两个函数写得尽量简洁,只做注册和注销,别掺和业务逻辑。

/* SPI控制器驱动入口/出口 */
static int __init spi_nvm_driver_init(void)
{
    int ret;
    
    ret = platform_driver_register(&spi_nvm_driver);
    if (ret) {
        pr_err("spi_nvm: platform driver register failed: %d\n", ret);
        return ret;
    }
    
    pr_info("spi_nvm: driver initialized\n");
    return 0;
}

static void __exit spi_nvm_driver_exit(void)
{
    platform_driver_unregister(&spi_nvm_driver);
    pr_info("spi_nvm: driver exited\n");
}

module_init(spi_nvm_driver_init);
module_exit(spi_nvm_driver_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("SPI NVM Controller Driver");

我的习惯:在 module_init 里加个打印,方便调试时确认驱动是否加载成功。曾经有一次,驱动死活加载不上,最后发现是 MODULE_LICENSE 写错了,内核直接拒绝加载。

4.3 platform_driver 结构体定义

这是驱动的身份证。内核通过 of_match_table 和设备树进行匹配,匹配上了就调用 probe。

/* 设备树匹配表 */
static const struct of_device_id spi_nvm_of_match[] = {
    { .compatible = "vendor,spi-nvm-ctrl", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_nvm_of_match);

/* platform_driver 结构体 */
static struct platform_driver spi_nvm_driver = {
    .driver = {
        .name = "spi_nvm",
        .of_match_table = spi_nvm_of_match,
        .pm = &spi_nvm_pm_ops,  /* 电源管理 */
    },
    .probe  = spi_nvm_probe,
    .remove = spi_nvm_remove,
};
成员 作用 注意事项
.driver.name 驱动名称,用于sysfs 不要与其他驱动重名
.of_match_table 设备树兼容性匹配 最后一项必须是空结构体
.probe 设备探测入口 失败必须返回负值错误码
.remove 设备移除入口 必须能处理probe未完成的情况
.pm 电源管理操作集 NVM设备建议实现suspend/resume

4.4 probe 函数深度解析

probe 是整个驱动的核心。说白了,就是给硬件分配软件资源。我建议按这个顺序来:

  1. 获取设备资源:从设备树中读取寄存器基址、中断号、DMA通道
  2. IO内存映射:将物理地址映射到虚拟地址空间
  3. 时钟与复位:使能SPI控制器时钟,释放复位信号
  4. 控制器初始化:配置SPI模式、时钟极性、数据位宽
  5. 中断注册:申请中断,绑定处理函数
  6. 设备注册:注册字符设备或MTD设备
static int spi_nvm_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct spi_nvm_dev *nvm;
    struct resource *res;
    int ret, irq;

    /* 1. 分配驱动私有数据结构 */
    nvm = devm_kzalloc(dev, sizeof(*nvm), GFP_KERNEL);
    if (!nvm)
        return -ENOMEM;
    platform_set_drvdata(pdev, nvm);

    /* 2. 获取IO资源并映射 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    nvm->regs = devm_ioremap_resource(dev, res);
    if (IS_ERR(nvm->regs))
        return PTR_ERR(nvm->regs);

    /* 3. 获取中断号 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    /* 4. 时钟使能 */
    nvm->clk = devm_clk_get(dev, "spi_clk");
    if (IS_ERR(nvm->clk))
        return PTR_ERR(nvm->clk);
    clk_prepare_enable(nvm->clk);

    /* 5. SPI控制器硬件初始化 */
    spi_nvm_hw_init(nvm);

    /* 6. 注册中断 */
    ret = devm_request_irq(dev, irq, spi_nvm_isr, 0,
                           "spi_nvm", nvm);
    if (ret) {
        dev_err(dev, "failed to request irq %d\n", irq);
        goto err_clk;
    }

    /* 7. 注册字符设备或MTD设备 */
    ret = spi_nvm_register_device(nvm);
    if (ret)
        goto err_clk;

    dev_info(dev, "SPI NVM controller probed\n");
    return 0;

err_clk:
    clk_disable_unprepare(nvm->clk);
    return ret;
}

我曾经踩过的坑:probe 函数里用了 devm_ 系列函数(设备资源管理),就不要再手动调用 kfree 或 iounmap。内核会在设备注销时自动释放。手动释放反而会导致 double-free,系统直接 panic。

4.5 remove 函数设计要点

remove 是 probe 的逆操作。但有个关键点:remove 被调用时,设备可能还在工作中。所以第一步必须是停止所有数据传输。

static int spi_nvm_remove(struct platform_device *pdev)
{
    struct spi_nvm_dev *nvm = platform_get_drvdata(pdev);

    if (!nvm)
        return 0;

    /* 1. 停止所有正在进行的传输 */
    spi_nvm_stop_all_transfers(nvm);

    /* 2. 注销设备节点 */
    spi_nvm_unregister_device(nvm);

    /* 3. 关闭时钟 */
    clk_disable_unprepare(nvm->clk);

    /* 4. 注意:devm_资源不需要手动释放 */
    dev_info(&pdev->dev, "SPI NVM controller removed\n");
    return 0;
}

关键原则:remove 必须幂等。也就是说,调用两次 remove 不能出问题。我一般会在 remove 开头加个 if (!nvm) return 0; 的检查。

4.6 设备树匹配与实例

驱动写好了,设备树也得配。这是驱动和设备之间的桥梁。

/* 设备树节点示例 */
&spi1 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi1_pins>;

    spi_nvm: spi-nvm@0 {
        compatible = "vendor,spi-nvm-ctrl";
        reg = <0x0>;           /* SPI片选地址 */
        spi-max-frequency = <50000000>;  /* 50MHz */
        spi-cpol;              /* 时钟极性 */
        spi-cpha;              /* 时钟相位 */
        
        /* 控制器寄存器基址由父节点提供 */
    };
};
设备树属性 对应驱动处理 说明
compatible of_match_table匹配 必须与驱动中定义一致
reg platform_get_resource SPI设备的片选地址
spi-max-frequency 设备私有数据读取 决定SPI时钟频率
spi-cpol/spi-cpha SPI模式配置 决定数据采样沿

4.7 RTOS下的驱动适配

在RTOS(如FreeRTOS、RT-Thread)下,驱动框架更轻量。没有platform总线那一套,但核心思想不变。

/* RT-Thread SPI设备驱动注册示例 */
static int rt_spi_nvm_init(void)
{
    struct rt_spi_bus *bus;
    
    /* 1. 分配SPI总线对象 */
    bus = (struct rt_spi_bus *)rt_malloc(sizeof(struct rt_spi_bus));
    if (!bus)
        return -RT_ENOMEM;
    
    /* 2. 初始化总线 */
    bus->parent.user_data = RT_NULL;
    rt_spi_bus_register(bus, "spi1", RT_DEVICE_FLAG_RDWR);
    
    /* 3. 注册SPI设备 */
    rt_hw_spi_device_attach("spi1", "spi10", GPIO_CS_PIN);
    
    return RT_EOK;
}
INIT_DEVICE_EXPORT(rt_spi_nvm_init);

我的建议:如果项目同时支持Linux和RTOS,把硬件初始化部分抽成独立函数。驱动框架层用宏或条件编译隔离。这样换平台时,只需要重写框架层,硬件操作代码直接复用。

4.8 调试与验证

驱动写完了,怎么验证?我一般用这几个方法:

  • 查看内核日志:dmesg | grep spi_nvm,确认probe成功
  • 检查设备节点:ls -l /dev/spi_nvm* 或 cat /proc/devices
  • SPI回环测试:将MOSI和MISO短接,发送数据看是否能正确接收
  • 逻辑分析仪抓波形:确认SCK、MOSI、CS时序符合预期

注意:probe失败时,一定要在 dmesg 中打印具体原因。我见过太多驱动,probe失败后只返回 -EINVAL,调试起来像大海捞针。多打一行 dev_err,能省你半天时间。