4. 驱动开发基础
驱动开发,说白了就是让内核认识你的硬件。我刚开始接触Linux驱动时,总觉得这东西玄乎——不就是写几个函数吗?后来踩了无数坑才明白,驱动模型的设计哲学,远比写代码本身重要。
这一章,我们聚焦三个核心:设备驱动模型、字符设备框架、platform驱动机制。这三块是嵌入式Linux驱动的基石,搞懂了它们,后面写任何驱动都有章可循。
4.1 Linux设备驱动模型:为什么需要它?
先问一个问题:为什么Linux不直接让驱动操作硬件,非要搞出一套设备模型?
你想想看,一个嵌入式系统里可能有几十个设备。每个设备有自己的地址、中断、时钟。如果没有统一的管理方式,驱动之间互相打架、资源冲突,那场面简直灾难。
Linux设备驱动模型的核心,就是把设备、驱动、总线三者解耦。它引入了几个关键概念:
| 概念 | 角色 | 我个人的理解 |
|---|---|---|
| bus_type | 总线类型 | 设备和驱动之间的“媒人” |
| device_driver | 驱动抽象 | 描述“谁能驱动这个设备” |
| device | 设备抽象 | 描述“这个设备长什么样” |
| class | 设备类 | 按功能分类,比如input、tty |
模型的核心流程是:总线匹配驱动和设备。当系统发现一个新设备时,总线会遍历所有注册的驱动,看看谁能驱动它。匹配上了,就调用驱动的probe函数。
关键点:驱动不直接操作设备,而是通过总线来“认识”设备。这种设计让驱动代码可以跨平台复用。
我在项目中遇到过一个问题:一个I2C触摸屏驱动在A平台跑得好好的,换到B平台就不工作了。排查了半天,发现是设备树里中断号配置错了。这就是设备模型的好处——硬件描述和驱动逻辑分离,改设备树就行,不用动驱动代码。
4.2 字符设备驱动框架:最基础的驱动形态
字符设备是Linux驱动里最简单的形式。它按字节流读写数据,典型的例子有串口、GPIO、LED。
字符设备驱动的核心结构是struct file_operations。这个结构体里放了一堆函数指针:open、read、write、ioctl、release……你实现这些函数,内核就知道怎么跟你的设备打交道。
来看一个最简单的字符设备驱动框架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major;
static struct class *my_class;
static struct cdev my_cdev;
static int my_open(struct inode *inode, struct file *file) {
pr_info("Device opened\n");
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf,
size_t len, loff_t *off) {
pr_info("Read called\n");
return 0;
}
static ssize_t my_write(struct file *file, const char __user *buf,
size_t len, loff_t *off) {
pr_info("Write called with %zu bytes\n", len);
return len;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
};
static int __init my_init(void) {
// 1. 分配设备号
major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) {
pr_err("Failed to register device\n");
return major;
}
// 2. 初始化cdev
cdev_init(&my_cdev, &fops);
cdev_add(&my_cdev, MKDEV(major, 0), 1);
// 3. 创建类
my_class = class_create(THIS_MODULE, CLASS_NAME);
device_create(my_class, NULL, MKDEV(major, 0),
NULL, DEVICE_NAME);
pr_info("Module loaded with major %d\n", major);
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, MKDEV(major, 0));
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev(major, DEVICE_NAME);
pr_info("Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
这段代码虽然简单,但包含了字符设备驱动的完整骨架:
- 分配设备号——告诉内核“我要用这个号码”
- 注册cdev——把file_operations挂到内核
- 创建设备节点——让用户空间能访问
我的习惯:写字符设备驱动时,先实现open和release,再补read/write。因为open/release是调试入口,能快速验证设备节点是否创建成功。
我曾经犯过一个低级错误:在write函数里忘了用copy_from_user,直接解引用用户空间指针。结果内核直接panic。嗯,这里要注意——用户空间指针不能在内核里直接访问,必须用专用的拷贝函数。
4.3 platform驱动机制:设备树时代的标配
字符设备驱动虽然简单,但它有个问题:设备信息写死在代码里。换一个板子,地址变了,就得改代码重新编译。
Platform驱动机制就是为了解决这个问题。它把设备信息放到设备树里,驱动只关心“怎么操作”,不关心“操作谁”。
Platform驱动的核心是两个结构体:
struct platform_driver——描述驱动本身struct platform_device——描述设备信息(通常由设备树生成)
匹配流程是这样的:
写platform驱动时,你只需要做两件事:
- 实现probe和remove函数——probe里做硬件初始化,remove里做清理
- 注册platform_driver——告诉内核“我能驱动哪些设备”
来看一个实际的platform驱动模板:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
static int my_probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
// 从设备树获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
// 映射物理地址到虚拟地址
base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
if (!base) {
dev_err(&pdev->dev, "ioremap failed\n");
return -ENOMEM;
}
dev_info(&pdev->dev, "Device probed at 0x%llx\n",
(unsigned long long)res->start);
return 0;
}
static int my_remove(struct platform_device *pdev) {
dev_info(&pdev->dev, "Device removed\n");
return 0;
}
static const struct of_device_id my_of_match[] = {
{ .compatible = "my,device" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
.of_match_table = my_of_match,
},
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");
我曾经踩过的坑:of_match_table里的compatible字符串必须和设备树里的一模一样,多一个空格都不行。有一次我设备树里写的是"my,device",驱动里写的是"my, device"(多了个空格),结果驱动死活不probe。查了两天才发现。
4.4 三种机制的对比与选择
学完这三个概念,你可能会问:什么时候用字符设备?什么时候用platform?
我的建议是:
| 场景 | 推荐机制 | 理由 |
|---|---|---|
| 纯软件虚拟设备 | 字符设备 | 不需要硬件资源,简单直接 |
| 挂载在总线上的硬件 | platform驱动 | 设备树描述硬件,驱动可复用 |
| 需要用户空间交互 | 字符设备 + platform | platform管理硬件,字符设备提供接口 |
说白了,字符设备是“接口”,platform是“机制”。它们不是互斥的,而是互补的。实际项目中,我经常在platform驱动的probe函数里注册一个字符设备,这样既利用了设备树的灵活性,又提供了用户空间接口。
核心总结:
- 设备驱动模型:解耦设备、驱动、总线
- 字符设备框架:提供file_operations给用户空间
- platform驱动:通过设备树匹配硬件,自动触发probe
嗯,这一章的内容就到这里。记住一个原则:驱动是服务于硬件的,不是服务于代码的。把硬件理解透了,驱动自然就好写了。