驱动框架入门:Linux设备驱动模型
说实话,我刚接触Linux驱动开发那会儿,最头疼的就是搞不懂设备驱动模型到底是个啥。一堆结构体、注册函数、回调接口,看着就头大。后来做了几个项目,踩了不少坑,才慢慢摸清楚门道。
这一章,我就带你从最核心的platform总线开始,一步步把Linux设备驱动模型拆开来看。你想想看,驱动开发说白了就是让内核认识你的硬件,然后应用程序能通过文件操作来使用它。就这么简单。
1. Linux设备驱动模型的核心三要素
Linux内核把设备驱动抽象成了三个角色:总线(Bus)、设备(Device)、驱动(Driver)。这三者怎么配合呢?
- 总线:负责匹配设备和驱动。比如platform总线,它会遍历所有注册的设备,看看有没有驱动愿意接管它。
- 设备:描述硬件的信息,比如寄存器地址、中断号、时钟频率等。设备本身不干活,它等着驱动来认领。
- 驱动:真正干活的代码。它实现了probe、remove等回调函数,负责初始化硬件、提供操作接口。
我个人习惯把这三者的关系想象成「婚介所」:总线是红娘,设备是待嫁的姑娘,驱动是来相亲的小伙子。红娘负责匹配,匹配上了就调用probe(相当于结婚),设备开始工作。
核心要点:驱动不直接操作设备,而是通过总线来绑定。这种解耦设计让代码复用性大大提高。
2. platform总线:最常用的虚拟总线
在嵌入式系统中,很多设备并不挂载在PCI、USB这类物理总线上。比如SoC内部的UART、I2C控制器、GPIO等。它们怎么注册呢?答案就是platform总线。
platform总线是Linux内核提供的一种虚拟总线。它不依赖具体的硬件连接,而是通过设备树或代码静态定义来匹配。
2.1 platform_device
设备端,我们需要填充一个platform_device结构体:
struct platform_device {
const char *name; // 设备名,用于匹配驱动
int id; // 设备ID,-1表示只有一个
struct device dev; // 内嵌的通用设备结构体
struct resource *resource; // 资源数组(地址、中断等)
int num_resources;
};
我记得第一次写platform设备时,忘了设置name字段,结果驱动死活匹配不上。查了两天才发现是名字拼写不一致。嗯,这里要注意:设备名和驱动名必须完全一致,大小写敏感。
2.2 platform_driver
驱动端,我们需要实现platform_driver结构体:
struct platform_driver {
int (*probe)(struct platform_device *);
int (*remove)(struct platform_device *);
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t);
int (*resume)(struct platform_device *);
struct device_driver driver;
};
probe函数是驱动的入口。当总线匹配成功时,内核会调用probe。你在probe里做硬件初始化、申请内存、注册字符设备等操作。
避坑指南:我曾经在probe里申请了DMA缓冲区,但忘了检查返回值。结果内存不足时直接崩溃。记住:probe里每一步都要做错误处理,一旦失败就返回负的错误码。
3. module_init/exit:驱动的加载与卸载
驱动写好了,怎么让内核加载它?答案就是module_init和module_exit宏。
static int __init my_driver_init(void)
{
return platform_driver_register(&my_platform_driver);
}
static void __exit my_driver_exit(void)
{
platform_driver_unregister(&my_platform_driver);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple platform driver");
这里有几个细节:
__init和__exit是内核的优化标记。__init标记的函数在初始化完成后会被释放,节省内存。- module_init不一定要做platform_driver_register,你也可以注册其他类型的驱动。
- MODULE_LICENSE必须声明,否则内核会报「tainted kernel」警告。
你可能会问:为什么用宏而不是直接定义函数指针?说白了,这是内核为了兼容不同编译器和架构做的封装。你照着写就行。
4. file_operations结构体:连接用户态和内核态
驱动最终是要给应用程序用的。应用程序通过open、read、write、ioctl等系统调用访问设备。这些系统调用怎么映射到驱动代码?答案就是file_operations结构体。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
// ... 还有很多
};
每个字段对应一个系统调用。比如应用程序调用read(),内核最终会调用你注册的read函数。
关键点:read/write函数的buf参数是__user指针,表示用户空间地址。你不能直接访问,必须用copy_to_user和copy_from_user来拷贝数据。我见过新手直接解引用__user指针,结果内核崩溃——因为用户空间的地址在内核态是无效的。
5. 字符设备驱动模板:一个完整的例子
光说不练假把式。下面我给你一个完整的字符设备驱动模板,结合platform总线:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major;
static struct class *my_class = NULL;
static struct device *my_device = NULL;
static struct cdev my_cdev;
// file_operations 实现
static int my_open(struct inode *inode, struct file *file)
{
pr_info("Device opened\n");
return 0;
}
static int my_release(struct inode *inode, struct file *file)
{
pr_info("Device closed\n");
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf,
size_t len, loff_t *offset)
{
char kernel_buf[100] = "Hello from kernel!";
size_t to_copy = min(len, strlen(kernel_buf));
if (copy_to_user(buf, kernel_buf, to_copy)) {
pr_err("Failed to copy to user\n");
return -EFAULT;
}
return to_copy;
}
static ssize_t my_write(struct file *file, const char __user *buf,
size_t len, loff_t *offset)
{
char kernel_buf[100];
if (len > sizeof(kernel_buf))
return -EINVAL;
if (copy_from_user(kernel_buf, buf, len)) {
pr_err("Failed to copy from user\n");
return -EFAULT;
}
pr_info("Received: %s\n", kernel_buf);
return len;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
// platform driver probe
static int my_probe(struct platform_device *pdev)
{
dev_t dev_num;
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("Failed to allocate device number\n");
return ret;
}
major = MAJOR(dev_num);
// 2. 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 3. 添加cdev到内核
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
pr_err("Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 4. 创建设备类
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
pr_err("Failed to create class\n");
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_class);
}
// 5. 创建设备节点
my_device = device_create(my_class, NULL, dev_num,
NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
pr_err("Failed to create device\n");
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_device);
}
pr_info("My char device probed successfully\n");
return 0;
}
static int my_remove(struct platform_device *pdev)
{
dev_t dev_num = MKDEV(major, 0);
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("My char device removed\n");
return 0;
}
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_platform_device",
.of_match_table = NULL, // 设备树匹配,暂不启用
},
};
static int __init my_driver_init(void)
{
return platform_driver_register(&my_platform_driver);
}
static void __exit my_driver_exit(void)
{
platform_driver_unregister(&my_platform_driver);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Driver Expert");
MODULE_DESCRIPTION("Platform based char device driver example");
这个模板涵盖了:
- platform_driver的注册和注销
- 字符设备号的申请和释放
- cdev的初始化和添加
- 设备类和设备节点的自动创建
- 基本的open/read/write/release实现
注意事项:
- probe函数中每一步都要检查返回值,失败时要回滚之前的所有操作。
- copy_to_user和copy_from_user可能会睡眠,不能在中断上下文调用。
- 设备节点名称不要和系统已有的冲突,否则insmod会失败。
6. 知识体系总览
下面这张图帮你理清整个驱动框架的脉络:
从这张图可以清楚看到:应用程序通过VFS调用file_operations,file_operations由字符设备驱动实现,而字符设备驱动又通过platform总线与硬件设备绑定。每一层各司其职,这就是Linux驱动模型的精髓。
7. 调试小技巧
最后分享几个我常用的调试方法:
- 查看设备是否注册成功:
ls /sys/bus/platform/devices/能看到所有platform设备。 - 查看驱动是否匹配:
ls /sys/bus/platform/drivers/能看到已注册的驱动。 - 查看设备节点:
ls -l /dev/my_char_dev确认主次设备号正确。 - 动态测试:用
echo "test" > /dev/my_char_dev和cat /dev/my_char_dev快速验证读写。
个人经验:我习惯在probe函数开头加一句pr_info("probe called for %s\n", pdev->name);。这样insmod后看dmesg,能第一时间知道probe有没有被调用。如果没调用,八成是设备名和驱动名不匹配。
好了,这一章的内容就到这里。你把这个模板理解透,再配合实际的硬件调试,Linux驱动开发的大门就算正式迈进去了。
公众号:蓝海资料掘金营,微信deep3321