【Linux驱动开发】第23天:spi_driver 的 probe / remove 函数实现规范
probe 和 remove 是 SPI 从设备驱动的核心入口函数一、函数触发时机与核心职责
| 函数 | 触发时机 | 核心职责 |
|---|---|---|
probe | 设备树compatible匹配成功,SPI 核心层完成设备基础初始化后自动调用 | 设备身份校验、硬件初始化、资源申请、内核子系统注册 |
remove | 驱动卸载、设备被移除时自动调用 | 子系统注销、硬件关停、资源释放,是probe的逆操作 |
函数原型(Linux 5.15 标准):
// 成功返回0,失败返回负错误码(如-ENOMEM、-EIO、-ENODEV)intprobe(structspi_device*spi);// 执行清理,返回0或void(不同内核版本略有差异,统一按int返回0即可)intremove(structspi_device*spi);二、probe 函数标准实现(7步标准流程)
整体执行逻辑
入参校验 → SPI参数配置 → 分配私有数据 → 申请硬件资源 → 外设初始化 → 注册子系统 → 错误回滚步骤1:入参校验与设备信息提取
struct spi_device *spi是 SPI 从设备的核心描述符,内核已经根据设备树填充了基础参数。
- 可直接获取:片选号
spi->chip_select、SPI模式spi->mode、最大时钟spi->max_speed_hz、位宽spi->bits_per_word - 设备节点:
&spi->dev,所有dev_*打印、资源申请都基于此
staticintspi_demo_probe(structspi_device*spi){intret;structspi_demo_priv*priv;// 校验入参(防御式编程)if(!spi)return-ENODEV;dev_info(&spi->dev,"probe matched, cs=%d, mode=0x%x, max_freq=%dHz\n",spi->chip_select,spi->mode,spi->max_speed_hz);步骤2:SPI 通信参数配置(spi_setup)
向 SPI 控制器驱动提交当前设备的时序参数,控制器会据此配置硬件寄存器,第一次传输前必须调用。
- 设备树中的
spi-cpol、spi-cpha、spi-max-frequency会自动填充到spi_device - 驱动可动态修改参数,修改后必须重新调用
spi_setup生效
// 可选:显式配置SPI参数(设备树已配置的话可省略,推荐显式调用确保生效)spi->mode=SPI_MODE_3;// 模式3:CPOL=1, CPHA=1spi->bits_per_word=8;// 8位数据位宽spi->max_speed_hz=50000000;// 实际运行不超过设备树设定值ret=spi_setup(spi);if(ret<0){dev_err(&spi->dev,"spi setup failed, ret=%d\n",ret);returnret;}对应 I2C:无完全对等接口,I2C 时序参数由适配器驱动统一配置,从设备无需单独设置。
步骤3:分配私有数据并绑定
私有数据结构体是驱动的"全局变量容器",保存设备状态、硬件资源、数据缓存等,所有工业级驱动都会使用。
- 推荐使用
devm_kzalloc:随设备生命周期自动释放,remove无需手动释放,避免内存泄漏 - 通过
spi_set_drvdata将私有数据挂载到spi_device上,后续中断、回调中可通过spi_get_drvdata获取
// 分配私有数据结构体(devm托管,自动释放)priv=devm_kzalloc(&spi->dev,sizeof(*priv),GFP_KERNEL);if(!priv)return-ENOMEM;priv->spi=spi;// 保存spi_device指针spi_set_drvdata(spi,priv);// 绑定到spi_device,对应i2c_set_clientdata私有数据结构体示例:
structspi_demo_priv{structspi_device*spi;// 所属SPI设备structgpio_desc*reset_gpio;// 复位引脚structmutexlock;// 并发锁u8 tx_buf[256];// 发送缓存u8 rx_buf[256];// 接收缓存intdata;// 业务数据};步骤4:申请硬件资源(GPIO、中断等)
从设备树读取并申请外设所需的 GPIO、中断、电源等资源,优先使用 devm 托管接口。
- 复位引脚、中断引脚是 SPI 外设最常见的额外硬件资源
- 使用
devm_gpiod_get_optional读取设备树中的reset-gpios属性
// 申请复位GPIO(设备树中定义reset-gpios属性)priv->reset_gpio=devm_gpiod_get_optional(&spi->dev,"reset",GPIOD_OUT_HIGH);if(IS_ERR(priv->reset_gpio)){ret=PTR_ERR(priv->reset_gpio);dev_err(&spi->dev,"get reset gpio failed, ret=%d\n",ret);returnret;}// 初始化互斥锁(保护SPI并发访问)mutex_init(&priv->lock);步骤5:外设硬件初始化与身份校验
这是 probe 的核心功能:确认硬件真实存在,并将外设设置为工作状态。
- 执行硬件复位时序(拉复位→延时→释放复位)
- 发送初始化指令,配置外设寄存器
- 读取芯片 ID / JEDEC ID,验证设备身份,防止匹配到不存在的硬件
// 硬件复位时序if(priv->reset_gpio){gpiod_set_value(priv->reset_gpio,0);// 拉低复位msleep(10);gpiod_set_value(priv->reset_gpio,1);// 释放复位msleep(20);// 等待外设启动}// 关键:读取芯片ID,验证设备真实存在ret=spi_demo_read_chip_id(priv);if(ret<0){dev_err(&spi->dev,"chip id verify failed\n");return-ENODEV;}dev_info(&spi->dev,"chip id verified, device ready\n");最佳实践:不能只靠设备树
compatible匹配就认为设备存在,必须通过 SPI 通信读取 ID 校验,这是驱动鲁棒性的基本要求。
步骤6:注册内核子系统(按需)
根据外设类型,将设备注册到对应的内核子系统,向用户空间暴露操作接口。
- 传感器类:注册
hwmon子系统 - 触摸屏/按键类:注册
input子系统 - 通用设备:注册
misc杂项设备 - 存储类:注册
MTD/block子系统
// 示例:注册misc杂项设备priv->misc.minor=MISC_DYNAMIC_MINOR;priv->misc.name="spi_demo";priv->misc.fops=&spi_demo_fops;priv->misc.parent=&spi->dev;ret=misc_register(&priv->misc);if(ret<0){dev_err(&spi->dev,"misc register failed, ret=%d\n",ret);returnret;}步骤7:错误处理与逐级回滚
每一步失败都必须撤销之前的操作,避免资源泄漏。
- 使用
devm托管的资源(内存、GPIO、中断),内核会自动逆序释放,无需手动处理 - 非托管资源(如
misc_register)必须在错误分支手动注销
完整错误处理示例:
ret=misc_register(&priv->misc);if(ret<0){dev_err(&spi->dev,"misc register failed\n");// 非托管资源手动回滚// 若有更多非托管资源,按注册逆序依次注销returnret;}dev_info(&spi->dev,"probe success\n");return0;}三、remove 函数标准实现
remove是probe的严格逆操作,执行顺序与 probe 相反。
标准执行顺序
注销子系统 → 关停硬件 → 释放非托管资源 → devm资源自动释放完整实现示例
staticintspi_demo_remove(structspi_device*spi){structspi_demo_priv*priv=spi_get_drvdata(spi);// 1. 最先注销probe最后注册的子系统misc_deregister(&priv->misc);// 2. 关停外设硬件(拉复位、进入休眠)if(priv->reset_gpio)gpiod_set_value(priv->reset_gpio,0);// 拉复位,停止外设工作// 3. 销毁锁、清理缓存mutex_destroy(&priv->lock);// 4. devm托管的内存、GPIO、中断等会由内核自动释放,无需手动处理dev_info(&spi->dev,"remove done\n");return0;}关键说明
- 逆序原则:probe 中先执行的操作,remove 中后执行;probe 最后注册的资源,remove 最先注销。
- devm 优势:90% 以上的资源都可以用 devm 托管,remove 函数会非常简洁,且彻底避免内存泄漏。
- 并发安全:remove 执行时要确保没有正在进行的 SPI 传输、中断处理,必要时加标志位屏蔽后续操作。
四、完整工业级驱动模板
1. 驱动头文件与私有结构
#include<linux/module.h>#include<linux/spi/spi.h>#include<linux/gpio/consumer.h>#include<linux/miscdevice.h>#include<linux/mutex.h>#include<linux/delay.h>structspi_demo_priv{structspi_device*spi;structgpio_desc*reset_gpio;structmiscdevicemisc;structmutexlock;u32 chip_id;};2. 芯片ID读取函数(probe校验用)
#defineCMD_READ_ID0x9Fstaticintspi_demo_read_chip_id(structspi_demo_priv*priv){intret;u8 tx=CMD_READ_ID;u8 rx[3]={0};mutex_lock(&priv->lock);ret=spi_write_then_read(priv->spi,&tx,1,rx,3);mutex_unlock(&priv->lock);if(ret<0)returnret;priv->chip_id=(rx[0]<<16)|(rx[1]<<8)|rx[2];dev_info(&priv->spi->dev,"chip id: 0x%06x\n",priv->chip_id);// 校验ID是否合法(以W25Q128为例:0xEF4018)if(priv->chip_id!=0xEF4018)return-ENODEV;return0;}3. probe / remove 完整实现
staticconststructof_device_idspi_demo_of_match[]={{.compatible="winbond,w25q128"},{/* sentinel */}};MODULE_DEVICE_TABLE(of,spi_demo_of_match);staticintspi_demo_probe(structspi_device*spi){intret;structspi_demo_priv*priv;priv=devm_kzalloc(&spi->dev,sizeof(*priv),GFP_KERNEL);if(!priv)return-ENOMEM;priv->spi=spi;spi_set_drvdata(spi,priv);mutex_init(&priv->lock);// 配置SPI参数spi->mode=SPI_MODE_3;spi->bits_per_word=8;ret=spi_setup(spi);if(ret<0)returnret;// 申请复位GPIOpriv->reset_gpio=devm_gpiod_get_optional(&spi->dev,"reset",GPIOD_OUT_HIGH);if(IS_ERR(priv->reset_gpio))returnPTR_ERR(priv->reset_gpio);// 硬件复位if(priv->reset_gpio){gpiod_set_value(priv->reset_gpio,0);msleep(10);gpiod_set_value(priv->reset_gpio,1);msleep(20);}// 校验芯片IDret=spi_demo_read_chip_id(priv);if(ret<0){dev_err(&spi->dev,"invalid chip id\n");returnret;}dev_info(&spi->dev,"probe success\n");return0;}staticintspi_demo_remove(structspi_device*spi){structspi_demo_priv*priv=spi_get_drvdata(spi);if(priv->reset_gpio)gpiod_set_value(priv->reset_gpio,0);mutex_destroy(&priv->lock);dev_info(&spi->dev,"remove done\n");return0;}staticstructspi_driverspi_demo_driver={.driver={.name="spi_demo_w25q",.owner=THIS_MODULE,.of_match_table=spi_demo_of_match,},.probe=spi_demo_probe,.remove=spi_demo_remove,};module_spi_driver(spi_demo_driver);MODULE_LICENSE("GPL");MODULE_DESCRIPTION("Industrial grade SPI driver template");五、与 I2C 驱动 probe / remove 的对比
| 操作 | I2C 驱动 | SPI 驱动 |
|---|---|---|
| 入参设备结构体 | struct i2c_client *client | struct spi_device *spi |
| 设备绑定私有数据 | i2c_set_clientdata | spi_set_drvdata |
| 硬件参数配置 | 无单独配置,由适配器统一管理 | spi_setup单独配置每个设备 |
| 设备身份校验 | 读取寄存器 / SMBus 读ID | spi_write_then_read读芯片ID |
| 资源管理 | 通用 devm 接口 | 通用 devm 接口 |
| 子系统注册 | 完全一致 | 完全一致 |
| 驱动注册宏 | module_i2c_driver | module_spi_driver |
六、常见坑点与最佳实践
- 必须调用
spi_setup:即使设备树已配置参数,也建议显式调用,避免内核版本差异导致参数不生效。 - 必须做芯片ID校验:不能仅凭
compatible匹配就认为设备存在,否则硬件不存在时驱动会异常。 - 优先使用 devm:除了子系统注册类操作,尽量全部使用 devm 托管资源,简化 remove 并杜绝泄漏。
- 加锁保护 SPI 传输:SPI 控制器不支持并发访问,多线程场景下必须用互斥锁保护
spi_write_then_read等传输接口。 - probe 中避免长延时:超过 100ms 的初始化建议放到工作队列中异步执行,避免阻塞内核启动流程。
