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

复制代码
probe 和 remove 是 SPI 从设备驱动的核心入口函数

一、函数触发时机与核心职责

函数 触发时机 核心职责
probe 设备树 compatible 匹配成功,SPI 核心层完成设备基础初始化后自动调用 设备身份校验、硬件初始化、资源申请、内核子系统注册
remove 驱动卸载、设备被移除时自动调用 子系统注销、硬件关停、资源释放,是 probe 的逆操作

函数原型(Linux 5.15 标准):

c 复制代码
// 成功返回0,失败返回负错误码(如-ENOMEM、-EIO、-ENODEV)
int  probe(struct spi_device *spi);
// 执行清理,返回0或void(不同内核版本略有差异,统一按int返回0即可)
int  remove(struct spi_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_* 打印、资源申请都基于此
c 复制代码
static int spi_demo_probe(struct spi_device *spi)
{
    int ret;
    struct spi_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 生效
c 复制代码
    // 可选:显式配置SPI参数(设备树已配置的话可省略,推荐显式调用确保生效)
    spi->mode          = SPI_MODE_3;    // 模式3:CPOL=1, CPHA=1
    spi->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);
        return ret;
    }

对应 I2C:无完全对等接口,I2C 时序参数由适配器驱动统一配置,从设备无需单独设置。

步骤3:分配私有数据并绑定

私有数据结构体是驱动的"全局变量容器",保存设备状态、硬件资源、数据缓存等,所有工业级驱动都会使用

  • 推荐使用 devm_kzalloc:随设备生命周期自动释放,remove 无需手动释放,避免内存泄漏
  • 通过 spi_set_drvdata 将私有数据挂载到 spi_device 上,后续中断、回调中可通过 spi_get_drvdata 获取
c 复制代码
    // 分配私有数据结构体(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

私有数据结构体示例:

c 复制代码
struct spi_demo_priv {
    struct spi_device *spi;       // 所属SPI设备
    struct gpio_desc *reset_gpio; // 复位引脚
    struct mutex lock;            // 并发锁
    u8  tx_buf[256];              // 发送缓存
    u8  rx_buf[256];              // 接收缓存
    int data;                     // 业务数据
};

步骤4:申请硬件资源(GPIO、中断等)

从设备树读取并申请外设所需的 GPIO、中断、电源等资源,优先使用 devm 托管接口

  • 复位引脚、中断引脚是 SPI 外设最常见的额外硬件资源
  • 使用 devm_gpiod_get_optional 读取设备树中的 reset-gpios 属性
c 复制代码
    // 申请复位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);
        return ret;
    }

    // 初始化互斥锁(保护SPI并发访问)
    mutex_init(&priv->lock);

步骤5:外设硬件初始化与身份校验

这是 probe 的核心功能:确认硬件真实存在,并将外设设置为工作状态

  1. 执行硬件复位时序(拉复位→延时→释放复位)
  2. 发送初始化指令,配置外设寄存器
  3. 读取芯片 ID / JEDEC ID,验证设备身份,防止匹配到不存在的硬件
c 复制代码
    // 硬件复位时序
    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 子系统
c 复制代码
    // 示例:注册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);
        return ret;
    }

步骤7:错误处理与逐级回滚

每一步失败都必须撤销之前的操作,避免资源泄漏。

  • 使用 devm 托管的资源(内存、GPIO、中断),内核会自动逆序释放,无需手动处理
  • 非托管资源(如 misc_register)必须在错误分支手动注销

完整错误处理示例:

c 复制代码
    ret = misc_register(&priv->misc);
    if (ret < 0) {
        dev_err(&spi->dev, "misc register failed\n");
        // 非托管资源手动回滚
        // 若有更多非托管资源,按注册逆序依次注销
        return ret;
    }

    dev_info(&spi->dev, "probe success\n");
    return 0;
}

三、remove 函数标准实现

removeprobe 的严格逆操作,执行顺序与 probe 相反。

标准执行顺序

复制代码
注销子系统 → 关停硬件 → 释放非托管资源 → devm资源自动释放

完整实现示例

c 复制代码
static int spi_demo_remove(struct spi_device *spi)
{
    struct spi_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");
    return 0;
}

关键说明

  1. 逆序原则:probe 中先执行的操作,remove 中后执行;probe 最后注册的资源,remove 最先注销。
  2. devm 优势:90% 以上的资源都可以用 devm 托管,remove 函数会非常简洁,且彻底避免内存泄漏。
  3. 并发安全:remove 执行时要确保没有正在进行的 SPI 传输、中断处理,必要时加标志位屏蔽后续操作。

四、完整工业级驱动模板

1. 驱动头文件与私有结构

c 复制代码
#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>

struct spi_demo_priv {
    struct spi_device *spi;
    struct gpio_desc *reset_gpio;
    struct miscdevice misc;
    struct mutex lock;
    u32 chip_id;
};

2. 芯片ID读取函数(probe校验用)

c 复制代码
#define CMD_READ_ID 0x9F

static int spi_demo_read_chip_id(struct spi_demo_priv *priv)
{
    int ret;
    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)
        return ret;

    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;

    return 0;
}

3. probe / remove 完整实现

c 复制代码
static const struct of_device_id spi_demo_of_match[] = {
    { .compatible = "winbond,w25q128" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, spi_demo_of_match);

static int spi_demo_probe(struct spi_device *spi)
{
    int ret;
    struct spi_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)
        return ret;

    // 申请复位GPIO
    priv->reset_gpio = devm_gpiod_get_optional(&spi->dev, "reset", GPIOD_OUT_HIGH);
    if (IS_ERR(priv->reset_gpio))
        return PTR_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);
    }

    // 校验芯片ID
    ret = spi_demo_read_chip_id(priv);
    if (ret < 0) {
        dev_err(&spi->dev, "invalid chip id\n");
        return ret;
    }

    dev_info(&spi->dev, "probe success\n");
    return 0;
}

static int spi_demo_remove(struct spi_device *spi)
{
    struct spi_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");
    return 0;
}

static struct spi_driver spi_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

六、常见坑点与最佳实践

  1. 必须调用 spi_setup:即使设备树已配置参数,也建议显式调用,避免内核版本差异导致参数不生效。
  2. 必须做芯片ID校验 :不能仅凭 compatible 匹配就认为设备存在,否则硬件不存在时驱动会异常。
  3. 优先使用 devm:除了子系统注册类操作,尽量全部使用 devm 托管资源,简化 remove 并杜绝泄漏。
  4. 加锁保护 SPI 传输 :SPI 控制器不支持并发访问,多线程场景下必须用互斥锁保护 spi_write_then_read 等传输接口。
  5. probe 中避免长延时:超过 100ms 的初始化建议放到工作队列中异步执行,避免阻塞内核启动流程。
相关推荐
李子琪。2 小时前
云计算虚拟化技术全解析:从理论到实践
linux·centos·云计算
wuminyu2 小时前
markword在高并发场景下变化剖析
java·linux·c语言·jvm·c++
Cloud_Shy6182 小时前
Linux 用户管理知识与应用实践(二:用户相关命令与示例)
linux·运维·服务器·测试用例
小生不才yz2 小时前
Shell脚本精读 · S08-03 | 脚本模块化:`source` 与多文件组织
linux
想你依然心痛2 小时前
AtomCode在算法竞赛中的实战体验:LeetCode周赛辅助编程
linux·算法·leetcode
24计网1王仔寿2 小时前
Linux 系统运维全栈学习路线|从 Shell 脚本到容器云 OpenStack 完整学习指南
linux·学习·openstack
长明2 小时前
C#项目组织与概念梳理
后端·c#
vortex52 小时前
Shell 命令执行知识体系全景解析
linux·运维·bash·shell·命令行
EntyIU2 小时前
CentOS-高可用部署手册-MySQL双主RedisNginx
linux·mysql·centos