4. 驱动层设计:Linux/RTOS下的SPI控制器驱动框架
驱动层设计,说白了就是让操作系统认识你的硬件。我做了这么多年存储,发现很多工程师把精力都花在应用层,结果驱动一跑就崩。嗯,今天咱们就把SPI控制器驱动这个地基打牢。
4.1 驱动框架的整体脉络
先看一张图,把整个流程串起来。你想想看,一个驱动从加载到卸载,到底经历了什么?
核心流程:模块注册 → 设备匹配 → probe探测 → 设备初始化 → 业务操作 → remove卸载
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 是整个驱动的核心。说白了,就是给硬件分配软件资源。我建议按这个顺序来:
- 获取设备资源:从设备树中读取寄存器基址、中断号、DMA通道
- IO内存映射:将物理地址映射到虚拟地址空间
- 时钟与复位:使能SPI控制器时钟,释放复位信号
- 控制器初始化:配置SPI模式、时钟极性、数据位宽
- 中断注册:申请中断,绑定处理函数
- 设备注册:注册字符设备或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,能省你半天时间。