第三讲:字符设备驱动基础
各位同学,今天我们来聊聊字符设备驱动的基础。说实话,这部分内容是我当年入门嵌入式驱动开发时,啃得最久的一块硬骨头。但一旦啃下来,后面的路就顺多了。
Linux设备驱动模型:别被名字吓到
Linux设备驱动模型,说白了就是一套管理硬件的框架。你想想看,系统里插了那么多设备——USB、I2C、SPI、PCI——总得有个统一的方式去管理它们吧?
这个模型的核心思想其实很简单:把设备抽象成文件。对,你没听错。在Linux世界里,一切皆文件。你的鼠标是文件,键盘是文件,连你插的U盘也是文件。
我个人习惯把设备驱动模型理解成三层结构:
- 总线层:负责设备和驱动之间的匹配。比如你插了个USB鼠标,USB总线就会去找对应的驱动。
- 设备层:描述硬件本身的信息,比如厂商ID、设备ID、中断号等。
- 驱动层:真正干活的部分,负责初始化硬件、处理数据。
我在项目中遇到过一个问题:一个I2C传感器死活不工作,查了两天才发现是设备树里把地址写错了。嗯,这种低级错误,犯过一次就记住了。
file_operations结构体:驱动的灵魂
这个结构体,可以说是字符设备驱动的核心。它定义了用户空间程序怎么跟你的硬件打交道。
来看一个典型的file_operations结构体:
struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.llseek = my_llseek,
};
每个成员都是一个函数指针。当用户程序调用open()时,内核就会调用你注册的my_open函数。说白了,这就是一张函数跳转表。
常用的操作有这些:
| 操作 | 说明 | 我踩过的坑 |
|---|---|---|
| open | 打开设备,做初始化 | 记得检查设备是否已被占用 |
| release | 关闭设备,释放资源 | 别忘了释放申请的内存 |
| read | 从设备读取数据 | 注意用户空间指针的合法性 |
| write | 向设备写入数据 | 数据长度检查不能省 |
| llseek | 改变文件读写位置 | 字符设备一般不支持随机访问 |
模块加载与卸载:驱动的一生
Linux驱动有两种加载方式:编译进内核,或者作为模块动态加载。我们做开发时,99%的情况都用模块方式。为什么?方便调试啊!改个代码重新编译模块,insmod一下就行了,不用重启整个系统。
模块的生命周期很简单:
- 加载:调用
module_init指定的函数 - 运行:等待用户程序调用
- 卸载:调用
module_exit指定的函数
来看一个标准的模块框架:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
static int __init my_init(void)
{
printk(KERN_INFO "Hello, driver world!\n");
return 0;
}
static void __exit my_exit(void)
{
printk(KERN_INFO "Goodbye, driver world!\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character driver");
这里有几个细节要注意:
__init和__exit宏:告诉内核这些函数只在初始化/退出时使用,用完后可以释放内存MODULE_LICENSE:必须声明,否则内核会报"tainted"警告printk:内核空间的打印函数,相当于用户空间的printf
printk,但正式发布前记得删掉或改成pr_debug。我曾经有个驱动,上线后dmesg被刷爆了,全是调试信息...
Hello World驱动:从零开始
好了,理论说完了,我们来写一个真正的Hello World驱动。这个驱动什么都不做,只是注册一个设备节点,让用户程序能打开它。
完整代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "hello_dev"
#define CLASS_NAME "hello_class"
static int major;
static struct class *hello_class;
static struct device *hello_device;
static int hello_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device opened\n");
return 0;
}
static int hello_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "Device closed\n");
return 0;
}
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
};
static int __init hello_init(void)
{
// 动态分配主设备号
major = register_chrdev(0, DEVICE_NAME, &hello_fops);
if (major < 0) {
printk(KERN_ALERT "Failed to register device\n");
return major;
}
// 创建设备类
hello_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(hello_class)) {
unregister_chrdev(major, DEVICE_NAME);
return PTR_ERR(hello_class);
}
// 创建设备节点
hello_device = device_create(hello_class, NULL,
MKDEV(major, 0), NULL, DEVICE_NAME);
if (IS_ERR(hello_device)) {
class_destroy(hello_class);
unregister_chrdev(major, DEVICE_NAME);
return PTR_ERR(hello_device);
}
printk(KERN_INFO "Hello driver loaded, major=%d\n", major);
return 0;
}
static void __exit hello_exit(void)
{
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, DEVICE_NAME);
printk(KERN_INFO "Hello driver unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded AI Driver Expert");
MODULE_DESCRIPTION("Hello World Character Driver");
编译和测试步骤:
- 写一个Makefile,编译生成.ko文件
sudo insmod hello.ko加载模块- 查看
/dev/hello_dev设备节点是否创建成功 - 用
cat /dev/hello_dev测试打开设备 sudo rmmod hello卸载模块
关键点总结:
- register_chrdev分配主设备号,0表示让内核自动分配
- class_create和device_create自动创建设备节点
- 卸载时一定要按相反顺序释放资源
为什么会强调卸载顺序?我曾经在项目里先卸载了设备类,再卸载字符设备,结果内核直接panic了。从那以后,我每次写驱动都会在注释里标清楚资源释放的顺序。
好了,这一讲的内容就到这里。字符设备驱动是嵌入式AI芯片驱动开发的基础,后面的I2C、SPI、DMA等复杂驱动,都是在这个框架上搭建的。下一讲我们会深入设备树,看看怎么让驱动自动匹配硬件。