【Linux驱动开发】第23天:spi_driver 的 probe / remove 函数实现规范

【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-cpolspi-cphaspi-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 的核心功能:确认硬件真实存在,并将外设设置为工作状态

  1. 执行硬件复位时序(拉复位→延时→释放复位)
  2. 发送初始化指令,配置外设寄存器
  3. 读取芯片 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 函数标准实现

removeprobe的严格逆操作,执行顺序与 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;}

关键说明

  1. 逆序原则:probe 中先执行的操作,remove 中后执行;probe 最后注册的资源,remove 最先注销。
  2. devm 优势:90% 以上的资源都可以用 devm 托管,remove 函数会非常简洁,且彻底避免内存泄漏。
  3. 并发安全: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 *clientstruct spi_device *spi
设备绑定私有数据i2c_set_clientdataspi_set_drvdata
硬件参数配置无单独配置,由适配器统一管理spi_setup单独配置每个设备
设备身份校验读取寄存器 / SMBus 读IDspi_write_then_read读芯片ID
资源管理通用 devm 接口通用 devm 接口
子系统注册完全一致完全一致
驱动注册宏module_i2c_drivermodule_spi_driver

六、常见坑点与最佳实践

  1. 必须调用spi_setup:即使设备树已配置参数,也建议显式调用,避免内核版本差异导致参数不生效。
  2. 必须做芯片ID校验:不能仅凭compatible匹配就认为设备存在,否则硬件不存在时驱动会异常。
  3. 优先使用 devm:除了子系统注册类操作,尽量全部使用 devm 托管资源,简化 remove 并杜绝泄漏。
  4. 加锁保护 SPI 传输:SPI 控制器不支持并发访问,多线程场景下必须用互斥锁保护spi_write_then_read等传输接口。
  5. probe 中避免长延时:超过 100ms 的初始化建议放到工作队列中异步执行,避免阻塞内核启动流程。