【Linux驱动开发】第8天:platform平台驱动深度解析——设计目的+probe/remove函数全解

目录

  1. platform平台驱动设计目的:解决传统驱动的5大致命痛点
  2. [传统驱动 vs platform驱动:代码对比直观感受](#传统驱动 vs platform驱动:代码对比直观感受)
  3. probe函数深度解析:作用+执行时机+参数+返回值+编写规范
  4. remove函数深度解析:作用+执行时机+编写规范
  5. platform驱动完整实战模板(带完整错误处理)
  6. 核心总结+面试必背考点

1. platform平台驱动设计目的:解决传统驱动的5大致命痛点

1.1 先搞清楚:什么是platform平台总线?

platform总线是Linux内核专门为片上系统(SoC)中的集成外设设计的虚拟总线。

为什么叫"虚拟总线"?因为它不像I2C、SPI、USB那样有真实的物理总线和硬件协议,它只是内核抽象出来的一个软件层,目的就是为了解决传统驱动的硬编码问题。

1.2 传统字符设备驱动的5大致命痛点

先回顾一下之前写的传统字符设备驱动,它存在以下无法解决的问题:

❌ 痛点1:硬件信息硬编码,可移植性为0

传统驱动把所有硬件信息(寄存器基地址、中断号、GPIO号)都直接写死在代码里:

c 复制代码
// 传统驱动硬编码硬件信息
#define REG_BASE 0x12340000
#define IRQ_NUM  5

如果换一个开发板,硬件地址变了,就要修改驱动代码,重新编译。一个驱动只能在一个开发板上运行,完全没有可移植性。

❌ 痛点2:一个驱动对应一个设备,代码复用性极差

如果有10个不同厂家的UART控制器,传统驱动就要写10个几乎一样的驱动,每个驱动里硬编码不同的硬件信息,代码重复率超过90%,维护成本极高。

❌ 痛点3:不支持热插拔

传统驱动在模块加载时就初始化硬件,模块卸载时就关闭硬件,无法支持设备的动态插入和拔出。

❌ 痛点4:资源管理混乱

传统驱动自己申请和管理所有资源(内存、中断、时钟),没有统一的资源管理机制,很容易出现资源冲突和内存泄漏。

❌ 痛点5:无法适配设备树

现代Linux系统全部采用设备树来描述硬件信息,传统硬编码驱动无法和设备树配合使用,已经被内核社区彻底淘汰。

1.3 platform驱动如何完美解决这些问题?

platform驱动通过**"硬件信息与驱动逻辑彻底分离"**的设计思想,完美解决了上述所有问题:

传统驱动痛点 platform驱动解决方案
硬件信息硬编码 硬件信息全部放在设备树中,驱动从设备树动态获取
代码复用性差 一个驱动可以支持所有符合匹配规则的设备
不支持热插拔 原生支持热插拔,设备插入自动调用probe,拔出自动调用remove
资源管理混乱 内核提供统一的资源管理API,自动管理资源
无法适配设备树 原生支持设备树,是现代Linux驱动的标准写法

一句话总结platform驱动的核心价值:
写一次驱动,跑遍所有符合匹配规则的硬件


2. 传统驱动 vs platform驱动:代码对比直观感受

我们用最简单的代码对比,让你一眼看出两者的本质区别。

2.1 传统字符设备驱动(硬编码版)

c 复制代码
// 硬编码硬件信息
#define DEV_NAME "my_uart"
#define REG_BASE 0x12340000
#define IRQ_NUM 5

static int __init uart_init(void)
{
    // 硬编码申请资源
    request_mem_region(REG_BASE, 0x1000, DEV_NAME);
    request_irq(IRQ_NUM, uart_irq, 0, DEV_NAME, NULL);
    
    // 注册字符设备
    alloc_chrdev_region(...);
    cdev_init(...);
    cdev_add(...);
    
    return 0;
}

static void __exit uart_exit(void)
{
    // 硬编码释放资源
    release_mem_region(REG_BASE, 0x1000);
    free_irq(IRQ_NUM, NULL);
    
    // 注销字符设备
    cdev_del(...);
    unregister_chrdev_region(...);
}

module_init(uart_init);
module_exit(uart_exit);

2.2 platform驱动(设备树版)

c 复制代码
// 匹配表:告诉总线我要匹配什么样的设备
static const struct of_device_id uart_match[] = {
    { .compatible = "vendor,uart-16550" },
    { }
};

static int uart_probe(struct platform_device *pdev)
{
    // 从设备树动态获取硬件信息
    struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    int irq = platform_get_irq(pdev, 0);
    
    // 申请资源
    devm_request_mem_region(&pdev->dev, res->start, resource_size(res), pdev->name);
    devm_request_irq(&pdev->dev, irq, uart_irq, 0, pdev->name, pdev);
    
    // 注册字符设备
    alloc_chrdev_region(...);
    cdev_init(...);
    cdev_add(...);
    
    return 0;
}

static int uart_remove(struct platform_device *pdev)
{
    // 注销字符设备
    cdev_del(...);
    unregister_chrdev_region(...);
    
    // 资源自动释放(devm机制)
    return 0;
}

static struct platform_driver uart_driver = {
    .driver = {
        .name = "uart-16550",
        .of_match_table = uart_match,
    },
    .probe = uart_probe,
    .remove = uart_remove,
};

module_platform_driver(uart_driver);

2.3 核心区别一目了然

对比项 传统驱动 platform驱动
硬件信息 硬编码在驱动里 放在设备树中,动态获取
驱动入口 module_init probe函数
驱动出口 module_exit remove函数
资源管理 手动申请/释放 内核自动管理(devm机制)
可移植性 极差 极好,一次编写到处运行
设备支持 一个驱动对应一个设备 一个驱动对应多个设备

3. probe函数深度解析:作用+执行时机+参数+返回值+编写规范

probe函数是platform驱动的核心入口,也是整个platform驱动模型中最重要的函数,没有之一。

3.1 probe函数的核心作用

当总线成功匹配到设备和驱动后,自动调用probe函数,完成以下工作:

  1. 从设备树或platform_device中获取硬件信息(地址、中断号、GPIO等)
  2. 申请硬件资源(内存、中断、时钟、GPIO等)
  3. 初始化硬件设备
  4. 注册字符设备/块设备/网络设备
  5. 创建设备文件,对外提供服务

简单说:probe函数就是传统驱动的module_init函数,但它是在设备和驱动匹配成功后才执行

3.2 probe函数的执行时机

probe函数只会在以下两种情况下被调用:

  1. 驱动先加载,设备后注册:驱动加载时注册到总线,之后设备注册到总线,总线匹配成功后调用probe
  2. 设备先注册,驱动后加载:设备先注册到总线,之后驱动加载时注册到总线,总线匹配成功后调用probe

关键要点:

  • 驱动加载时不会立即执行probe,只有匹配到设备才会执行
  • 一个驱动匹配到多少个设备,就会执行多少次probe函数
  • 每个设备对应一个独立的probe执行上下文,互不干扰

3.3 probe函数的参数与返回值

函数原型
c 复制代码
int (*probe)(struct platform_device *pdev);
参数:struct platform_device *pdev

这是probe函数唯一的参数,也是最重要的参数,它包含了这个设备的所有硬件信息

  • 设备的名称、ID
  • 设备树节点指针
  • 所有资源信息(内存、中断、DMA等)
  • 设备的私有数据指针

所有硬件信息都从这个参数中获取,绝对不能在probe函数中硬编码任何硬件信息

返回值
  • 返回0:probe执行成功,设备和驱动绑定完成
  • 返回负数错误码:probe执行失败,设备和驱动绑定失败
  • 常见错误码:-ENOMEM(内存不足)、-EINVAL(参数无效)、-ENODEV(设备不存在)

3.4 probe函数的编写规范(必须遵守)

规范1:所有硬件信息必须从pdev中获取,绝对不能硬编码
c 复制代码
// ✅ 正确写法:从pdev中获取内存资源
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
void __iomem *base = devm_ioremap_resource(&pdev->dev, res);

// ❌ 错误写法:硬编码地址
void __iomem *base = ioremap(0x12340000, 0x1000);
规范2:优先使用devm系列API申请资源

devm系列API是内核提供的设备资源管理机制 ,它会在设备和驱动解除绑定时自动释放所有申请的资源,彻底避免内存泄漏。

c 复制代码
// ✅ 正确写法:使用devm申请资源,自动释放
devm_request_irq(&pdev->dev, irq, handler, 0, pdev->name, pdev);

// ❌ 错误写法:手动申请资源,容易忘记释放
request_irq(irq, handler, 0, pdev->name, pdev);
规范3:必须实现完整的错误处理,使用goto反向释放资源

probe函数中任何一步失败,都必须反向释放已经申请的所有资源,内核驱动标准的错误处理方式是使用goto。

c 复制代码
static int uart_probe(struct platform_device *pdev)
{
    int ret;
    void __iomem *base;
    int irq;

    // 步骤1:获取内存资源
    struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res) {
        dev_err(&pdev->dev, "获取内存资源失败\n");
        return -ENODEV;
    }

    // 步骤2:映射寄存器地址
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base)) {
        dev_err(&pdev->dev, "映射寄存器失败\n");
        return PTR_ERR(base);
    }

    // 步骤3:获取中断号
    irq = platform_get_irq(pdev, 0);
    if (irq < 0) {
        dev_err(&pdev->dev, "获取中断号失败\n");
        return irq;
    }

    // 步骤4:申请中断
    ret = devm_request_irq(&pdev->dev, irq, uart_irq, 0, pdev->name, pdev);
    if (ret < 0) {
        dev_err(&pdev->dev, "申请中断失败\n");
        return ret;
    }

    // 步骤5:注册字符设备
    ret = alloc_chrdev_region(&dev_num, 0, 1, pdev->name);
    if (ret < 0) {
        dev_err(&pdev->dev, "申请设备号失败\n");
        goto err_irq; // 反向释放已经申请的中断
    }

    // 步骤6:初始化cdev
    cdev_init(&cdev, &fops);
    ret = cdev_add(&cdev, dev_num, 1);
    if (ret < 0) {
        dev_err(&pdev->dev, "注册cdev失败\n");
        goto err_chrdev; // 反向释放已经申请的设备号
    }

    dev_info(&pdev->dev, "probe执行成功\n");
    return 0;

// 错误处理:反向释放资源
err_chrdev:
    unregister_chrdev_region(dev_num, 1);
err_irq:
    devm_free_irq(&pdev->dev, irq, pdev);
    return ret;
}
规范4:probe函数中不能执行耗时操作和睡眠操作

probe函数运行在进程上下文,可以睡眠,但不能执行耗时过长的操作,否则会影响系统启动速度。

规范5:使用dev_set_drvdata保存设备私有数据

如果需要在驱动的其他函数(如read、write、中断处理函数)中访问设备信息,使用dev_set_drvdata将私有数据指针保存到device结构体中。

c 复制代码
// 定义设备私有数据结构体
struct uart_dev {
    void __iomem *base;
    int irq;
    dev_t dev_num;
    struct cdev cdev;
};

// 在probe中保存私有数据
struct uart_dev *dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
dev_set_drvdata(&pdev->dev, dev);

// 在其他函数中获取私有数据
struct uart_dev *dev = dev_get_drvdata(&pdev->dev);

4. remove函数深度解析:作用+执行时机+编写规范

remove函数是probe函数的逆操作,负责清理probe函数中申请的资源。

4.1 remove函数的核心作用

当设备拔出或驱动卸载时,自动调用remove函数,完成以下工作:

  1. 注销字符设备/块设备/网络设备
  2. 关闭硬件设备
  3. 释放probe函数中申请的非devm管理的资源

4.2 remove函数的执行时机

remove函数只会在以下两种情况下被调用:

  1. 设备拔出:热插拔设备被拔出时,总线自动调用remove
  2. 驱动卸载:执行rmmod卸载驱动时,总线会调用所有绑定设备的remove函数

关键要点:

  • 有多少个设备绑定了这个驱动,就会执行多少次remove函数
  • remove函数执行完成后,设备和驱动解除绑定关系
  • devm管理的资源会在remove执行完成后自动释放,不需要手动释放

4.3 remove函数的参数与返回值

函数原型
c 复制代码
int (*remove)(struct platform_device *pdev);
参数:struct platform_device *pdev

和probe函数的参数完全一样,指向同一个设备的platform_device结构体。

返回值
  • 现代Linux内核中,remove函数的返回值已经被忽略,一般返回0即可
  • 早期内核中返回负数表示remove执行失败,但现在已经没有实际意义

4.4 remove函数的编写规范

规范1:必须和probe函数完全对应,反向释放所有非devm资源

remove函数的执行顺序必须和probe函数完全相反,释放所有probe函数中申请的非devm管理的资源。

c 复制代码
static int uart_remove(struct platform_device *pdev)
{
    struct uart_dev *dev = dev_get_drvdata(&pdev->dev);

    // 注销cdev(非devm资源,需要手动释放)
    cdev_del(&dev->cdev);
    
    // 释放设备号(非devm资源,需要手动释放)
    unregister_chrdev_region(dev->dev_num, 1);
    
    // devm管理的资源(内存、中断、寄存器映射)会自动释放,不需要手动处理

    dev_info(&pdev->dev, "remove执行成功\n");
    return 0;
}
规范2:绝对不能在remove函数中返回错误

虽然remove函数有返回值,但现代内核会忽略它,返回错误没有任何意义,反而可能导致资源泄漏。

规范3:remove函数中不能执行耗时操作和睡眠操作

和probe函数一样,remove函数运行在进程上下文,可以睡眠,但不能执行耗时过长的操作。


5. platform驱动完整实战模板(带完整错误处理)

这是工业界标准的platform驱动模板,包含了所有编写规范和完整的错误处理,可直接作为后续驱动开发的基础框架。

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/kernel.h>       // 补充:确保container_of等宏定义
#include <linux/mutex.h>        // 补充:互斥锁头文件

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver");
MODULE_DESCRIPTION("工业级标准platform字符设备驱动模板");
MODULE_VERSION("1.0");
MODULE_ALIAS("platform:my_platform_dev"); // 补充:支持模块自动加载

#define DEV_NAME "my_platform_dev"
#define BUF_SIZE 1024

// 设备私有数据结构体:所有设备相关的资源都放在这里
struct my_dev {
    dev_t dev_num;
    struct cdev cdev;
    struct class *dev_class;
    struct device *dev_device;
    char kernel_buf[BUF_SIZE];
    struct mutex buf_mutex;       // 新增:保护内核缓冲区的互斥锁
};

static int my_open(struct inode *inode, struct file *file)
{
    // 从cdev指针获取包含它的my_dev结构体指针(内核标准写法)
    struct my_dev *dev = container_of(inode->i_cdev, struct my_dev, cdev);
    // 将设备私有数据保存到file结构体,供read/write使用
    file->private_data = dev;
    return 0;
}

static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *offset)
{
    struct my_dev *dev = file->private_data;
    ssize_t ret;

    // 加锁保护共享缓冲区
    mutex_lock(&dev->buf_mutex);

    // 使用内核辅助函数,自动处理偏移量和边界检查,比手动写copy_to_user更安全
    ret = simple_read_from_buffer(buf, len, offset, dev->kernel_buf, BUF_SIZE);

    // 解锁
    mutex_unlock(&dev->buf_mutex);

    return ret;
}

static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *offset)
{
    struct my_dev *dev = file->private_data;
    ssize_t ret;

    // 加锁保护共享缓冲区
    mutex_lock(&dev->buf_mutex);

    // 使用内核辅助函数,自动处理偏移量和边界检查
    ret = simple_write_to_buffer(dev->kernel_buf, BUF_SIZE, offset, buf, len);

    // 解锁
    mutex_unlock(&dev->buf_mutex);

    return ret;
}

static int my_release(struct inode *inode, struct file *file)
{
    return 0;
}

// 文件操作集
static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .release = my_release,
};

// 设备树匹配表:必须与设备树节点的compatible属性完全一致
static const struct of_device_id my_match[] = {
    { .compatible = "myvendor,my-platform-dev" },
    { /* 必须以空结构体结尾 */ }
};
// 导出匹配表,让内核和udev能够识别驱动支持的设备
MODULE_DEVICE_TABLE(of, my_match);

static int my_probe(struct platform_device *pdev)
{
    int ret;
    struct my_dev *dev;

    // 1. 使用devm分配设备私有数据:设备解绑时自动释放
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev) {
        dev_err(&pdev->dev, "分配设备私有数据失败\n");
        return -ENOMEM;
    }

    // 2. 初始化互斥锁
    mutex_init(&dev->buf_mutex);

    // 3. 保存私有数据到device结构体:供remove和其他函数使用
    dev_set_drvdata(&pdev->dev, dev);

    // 4. 动态申请设备号
    ret = alloc_chrdev_region(&dev->dev_num, 0, 1, DEV_NAME);
    if (ret < 0) {
        dev_err(&pdev->dev, "申请设备号失败,错误码:%d\n", ret);
        return ret;
    }

    // 5. 初始化cdev结构体,绑定文件操作集
    cdev_init(&dev->cdev, &my_fops);
    dev->cdev.owner = THIS_MODULE;

    // 6. 向内核注册cdev字符设备
    ret = cdev_add(&dev->cdev, dev->dev_num, 1);
    if (ret < 0) {
        dev_err(&pdev->dev, "注册cdev失败,错误码:%d\n", ret);
        goto err_unregister_chrdev;
    }

    // 7. 使用devm创建设备类:设备解绑时自动销毁
    dev->dev_class = devm_class_create(&pdev->dev, THIS_MODULE, DEV_NAME);
    if (IS_ERR(dev->dev_class)) {
        dev_err(&pdev->dev, "创建设备类失败\n");
        ret = PTR_ERR(dev->dev_class);
        goto err_cdev_del;
    }

    // 8. 使用devm创建设备文件:设备解绑时自动销毁
    dev->dev_device = devm_device_create(&pdev->dev, NULL, dev->dev_num, NULL, DEV_NAME);
    if (IS_ERR(dev->dev_device)) {
        dev_err(&pdev->dev, "创建设备文件失败\n");
        ret = PTR_ERR(dev->dev_device);
        // 注意:devm_class_create已经自动管理,不需要手动销毁
        goto err_cdev_del;
    }

    dev_info(&pdev->dev, "驱动加载成功,主设备号:%d,次设备号:%d\n",
             MAJOR(dev->dev_num), MINOR(dev->dev_num));
    return 0;

// 错误处理:严格按照申请顺序反向释放
err_cdev_del:
    cdev_del(&dev->cdev);
err_unregister_chrdev:
    unregister_chrdev_region(dev->dev_num, 1);
    return ret;
}

static int my_remove(struct platform_device *pdev)
{
    struct my_dev *dev = dev_get_drvdata(&pdev->dev);

    // 手动释放非devm管理的资源
    cdev_del(&dev->cdev);
    unregister_chrdev_region(dev->dev_num, 1);

    // devm管理的资源(私有数据、互斥锁、设备类、设备文件)全部自动释放
    dev_info(&pdev->dev, "驱动卸载成功\n");
    return 0;
}

// platform驱动结构体
static struct platform_driver my_driver = {
    .driver = {
        .name = DEV_NAME,               // 用于传统名称匹配
        .of_match_table = my_match,     // 用于设备树匹配(优先级更高)
    },
    .probe = my_probe,
    .remove = my_remove,
};

// 简化的模块注册宏:自动生成module_init和module_exit
module_platform_driver(my_driver);

6. 核心总结+面试必背考点

核心总结

  1. platform驱动的核心设计目的是解决传统驱动的硬编码问题,实现硬件信息与驱动逻辑的彻底分离
  2. platform驱动是现代Linux驱动的标准写法,所有片上外设驱动都应该使用platform模型
  3. probe函数是platform驱动的核心入口,在设备和驱动匹配成功后自动执行
  4. remove函数是probe函数的逆操作,在设备拔出或驱动卸载时自动执行
  5. 优先使用devm系列API申请资源,避免内存泄漏
  6. probe函数必须实现完整的错误处理,使用goto反向释放资源

面试必背考点

  1. platform平台总线的设计目的是什么?它解决了传统驱动什么问题?
  2. probe函数的作用是什么?什么时候会被调用?
  3. remove函数的作用是什么?什么时候会被调用?
  4. platform驱动和传统字符设备驱动的核心区别是什么?
  5. devm系列API的作用是什么?有什么优点?
  6. probe函数的编写规范有哪些?
  7. 为什么probe函数中不能硬编码硬件信息?
  8. module_platform_driver宏做了什么?
相关推荐
曦夜日长1 小时前
Linux系统篇,开发工具(一):从入门到精通的软件安装yum使用
linux·运维·elasticsearch
司南-70491 小时前
如何下载无损 bilbili视频?
运维·服务器·动画·技术美术
无限进步_1 小时前
【Linux】从磁盘到文件系统——块、分区与inode
linux·运维·服务器
2401_853087881 小时前
国产化DevOps工具链实践:知识库与需求/任务/版本如何打通?
运维·网络·devops
噗噗121 小时前
企业微信 API 实战系列(一):构建基于“动态行为”的自动化公海流转系统
运维·自动化·企业微信
渡我白衣1 小时前
定时器与时间轮思想
linux·开发语言·前端·c++·人工智能·深度学习·神经网络
zt1985q1 小时前
本地部署开源数据库管理工具 DBeaver 并实现外部访问( Windows 版本)
运维·服务器·网络·数据库·网络协议
Marry Andy1 小时前
Atlas 800T A2部署qwen3-32b
linux·人工智能·语言模型·自然语言处理
珂玥c1 小时前
新增硬盘有脏数据如何处理——ubuntu16.04
linux·数据库·ide