目录
- platform平台驱动设计目的:解决传统驱动的5大致命痛点
- [传统驱动 vs platform驱动:代码对比直观感受](#传统驱动 vs platform驱动:代码对比直观感受)
- probe函数深度解析:作用+执行时机+参数+返回值+编写规范
- remove函数深度解析:作用+执行时机+编写规范
- platform驱动完整实战模板(带完整错误处理)
- 核心总结+面试必背考点
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函数,完成以下工作:
- 从设备树或platform_device中获取硬件信息(地址、中断号、GPIO等)
- 申请硬件资源(内存、中断、时钟、GPIO等)
- 初始化硬件设备
- 注册字符设备/块设备/网络设备
- 创建设备文件,对外提供服务
简单说:probe函数就是传统驱动的module_init函数,但它是在设备和驱动匹配成功后才执行。
3.2 probe函数的执行时机
probe函数只会在以下两种情况下被调用:
- 驱动先加载,设备后注册:驱动加载时注册到总线,之后设备注册到总线,总线匹配成功后调用probe
- 设备先注册,驱动后加载:设备先注册到总线,之后驱动加载时注册到总线,总线匹配成功后调用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函数,完成以下工作:
- 注销字符设备/块设备/网络设备
- 关闭硬件设备
- 释放probe函数中申请的非devm管理的资源
4.2 remove函数的执行时机
remove函数只会在以下两种情况下被调用:
- 设备拔出:热插拔设备被拔出时,总线自动调用remove
- 驱动卸载:执行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. 核心总结+面试必背考点
核心总结
- platform驱动的核心设计目的是解决传统驱动的硬编码问题,实现硬件信息与驱动逻辑的彻底分离
- platform驱动是现代Linux驱动的标准写法,所有片上外设驱动都应该使用platform模型
- probe函数是platform驱动的核心入口,在设备和驱动匹配成功后自动执行
- remove函数是probe函数的逆操作,在设备拔出或驱动卸载时自动执行
- 优先使用devm系列API申请资源,避免内存泄漏
- probe函数必须实现完整的错误处理,使用goto反向释放资源
面试必背考点
- platform平台总线的设计目的是什么?它解决了传统驱动什么问题?
- probe函数的作用是什么?什么时候会被调用?
- remove函数的作用是什么?什么时候会被调用?
- platform驱动和传统字符设备驱动的核心区别是什么?
- devm系列API的作用是什么?有什么优点?
- probe函数的编写规范有哪些?
- 为什么probe函数中不能硬编码硬件信息?
- module_platform_driver宏做了什么?