第1章:字符设备驱动框架 — 从零开始搭建你的第一个驱动
大家好,我是你们的老朋友。今天咱们正式开始《AOSP内核驱动移植实战指南》的第一章。
字符设备驱动,说白了就是Linux驱动世界里最基础、最经典的一种模型。你想想看,从按键、LED、触摸屏到传感器,绝大多数外设本质上都是字符设备。我刚开始接触驱动开发时,第一个能跑起来的驱动就是字符设备驱动。那种在终端里看到自己写的驱动打印出"Hello World"的感觉,嗯,至今难忘。
1.1 什么是字符设备?
字符设备,就是按字节流方式读写数据的设备。它不像块设备那样有缓存和随机访问的概念,也不像网络设备那样有复杂的协议栈。字符设备就是简单、直接、顺序地处理数据。
常见的字符设备包括:
- 串口(UART)
- GPIO控制的LED
- 按键输入
- I2C/SPI传感器
- /dev/null、/dev/zero这类虚拟设备
在AOSP系统里,很多硬件抽象层(HAL)底层调用的就是字符设备驱动。所以,搞懂字符设备框架,就等于拿到了驱动开发的入场券。
1.2 file_operations结构体 — 驱动的"接口表"
驱动和用户空间怎么通信?答案就是通过file_operations结构体。它定义在linux/fs.h中,本质上是一个函数指针表。用户空间调用open()、read()、write()、ioctl()等系统调用时,内核会通过这个结构体找到对应的驱动函数。
我个人习惯把file_operations叫做"驱动的接口表"。你想想看,用户程序不知道驱动内部怎么实现,它只知道调用read()就能读到数据。这个"接口表"就是连接用户空间和内核空间的桥梁。
来看一个最简化的例子:
// 驱动中的打开函数
static int my_open(struct inode *inode, struct file *filp) {
printk(KERN_INFO "设备已打开\n");
return 0;
}
// 驱动中的读取函数
static ssize_t my_read(struct file *filp, char __user *buf,
size_t len, loff_t *off) {
printk(KERN_INFO "设备被读取\n");
return 0;
}
// 驱动中的写入函数
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t len, loff_t *off) {
printk(KERN_INFO "设备被写入\n");
return len;
}
// 驱动中的释放函数
static int my_release(struct inode *inode, struct file *filp) {
printk(KERN_INFO "设备已关闭\n");
return 0;
}
// 定义file_operations结构体
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
重要提醒:.owner = THIS_MODULE这行不能省。它告诉内核这个结构体属于哪个模块,防止模块被卸载时还有进程在使用它。我曾经见过有人漏掉这行,结果模块卸载时系统直接崩溃。
常用的file_operations成员包括:
| 成员 | 说明 | 典型用途 |
|---|---|---|
| open | 打开设备时调用 | 初始化硬件、分配资源 |
| release | 关闭设备时调用 | 释放资源、关闭硬件 |
| read | 从设备读取数据 | 读取传感器数据、按键状态 |
| write | 向设备写入数据 | 控制LED、发送指令 |
| unlocked_ioctl | 设备控制命令 | 设置参数、切换模式 |
| mmap | 内存映射 | 帧缓冲、DMA缓冲区 |
我的经验:在AOSP环境下调试驱动时,我建议你在open和release函数里加上printk打印。这样通过dmesg就能看到设备有没有被正确打开和关闭。我曾经花了一整天排查一个"设备打不开"的问题,最后发现是HAL层忘记调用open()了。
1.3 设备号 — 驱动的"身份证"
每个字符设备在内核里都有一个唯一的编号,叫做设备号。它由两部分组成:
- 主设备号:标识设备类型(比如串口是4,I2C是89)
- 次设备号:标识同类设备中的不同实例(比如串口0、串口1)
设备号在内核中用dev_t类型表示,它是一个32位的整数。高12位是主设备号,低20位是次设备号。
注册设备号有两种方式:
方式一:静态分配(手动指定)
#define MY_MAJOR 200
#define MY_MINOR 0
dev_t devno = MKDEV(MY_MAJOR, MY_MINOR);
register_chrdev_region(devno, 1, "my_device");
方式二:动态分配(推荐)
dev_t devno;
alloc_chrdev_region(&devno, 0, 1, "my_device");
int major = MAJOR(devno);
int minor = MINOR(devno);
避坑指南:我曾经在项目里用静态分配,指定了主设备号200。结果在另一台设备上,200已经被别的驱动占用了。从那以后,我全部改用动态分配。动态分配的好处是内核会自动找一个空闲的设备号给你,省心又安全。
设备号用完后一定要记得注销:
unregister_chrdev_region(devno, 1);
1.4 cdev结构体 — 驱动的"内核对象"
cdev是内核里表示字符设备的核心结构体。它把设备号、file_operations和驱动模块绑定在一起。
使用cdev的步骤很简单:
- 分配
cdev结构体 - 初始化
cdev - 将
cdev添加到内核
看代码:
static struct cdev my_cdev;
// 初始化cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
// 将cdev添加到内核
int ret = cdev_add(&my_cdev, devno, 1);
if (ret) {
printk(KERN_ERR "cdev_add失败\n");
return ret;
}
卸载驱动时,记得移除cdev:
cdev_del(&my_cdev);
关键点:cdev_add必须在设备号注册成功之后调用。顺序搞反了,内核会报错。我刚开始学的时候犯过这个错误,查了半天日志才发现是顺序问题。
1.5 设备节点 — 用户空间的"入口"
驱动注册好了,用户程序怎么访问它?答案是通过设备节点。设备节点就是/dev目录下的一个文件,比如/dev/my_device。
创建设备节点有两种方式:
方式一:手动创建(调试用)
mknod /dev/my_device c 200 0
其中c表示字符设备,200是主设备号,0是次设备号。
方式二:自动创建(生产环境推荐)
使用udev或mdev自动创建设备节点。驱动里需要做两件事:
- 创建
class结构体 - 创建设备
static struct class *my_class;
// 在模块初始化函数中
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
return PTR_ERR(my_class);
}
device_create(my_class, NULL, devno, NULL, "my_device");
// 在模块卸载函数中
device_destroy(my_class, devno);
class_destroy(my_class);
这样,当驱动加载时,udev会自动在/dev下创建my_device节点。用户程序直接open("/dev/my_device", O_RDWR)就能访问了。
我的建议:在AOSP系统里,设备节点的权限很重要。默认情况下,/dev/my_device可能只有root能访问。如果想让普通应用也能用,可以在device_create之后用chmod修改权限,或者在udev规则里设置。我在做某个平板项目时,就因为权限问题折腾了两天,最后发现是udev规则没写对。
1.6 完整的驱动框架 — 串起来看看
好了,知识点都讲完了。咱们把它们串起来,写一个完整的字符设备驱动框架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "my_device"
static dev_t devno;
static struct cdev my_cdev;
static struct class *my_class;
static int my_open(struct inode *inode, struct file *filp) {
printk(KERN_INFO "%s: 设备打开\n", DEVICE_NAME);
return 0;
}
static int my_release(struct inode *inode, struct file *filp) {
printk(KERN_INFO "%s: 设备关闭\n", DEVICE_NAME);
return 0;
}
static ssize_t my_read(struct file *filp, char __user *buf,
size_t len, loff_t *off) {
printk(KERN_INFO "%s: 读取 %zu 字节\n", DEVICE_NAME, len);
return 0;
}
static ssize_t my_write(struct file *filp, const char __user *buf,
size_t len, loff_t *off) {
printk(KERN_INFO "%s: 写入 %zu 字节\n", DEVICE_NAME, len);
return len;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
};
static int __init my_init(void) {
int ret;
// 1. 分配设备号
ret = alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
if (ret) {
printk(KERN_ERR "设备号分配失败\n");
return ret;
}
printk(KERN_INFO "主设备号: %d\n", MAJOR(devno));
// 2. 初始化并添加cdev
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, devno, 1);
if (ret) {
printk(KERN_ERR "cdev添加失败\n");
unregister_chrdev_region(devno, 1);
return ret;
}
// 3. 自动创建设备节点
my_class = class_create(THIS_MODULE, "my_class");
if (IS_ERR(my_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
return PTR_ERR(my_class);
}
device_create(my_class, NULL, devno, NULL, DEVICE_NAME);
printk(KERN_INFO "%s: 驱动加载成功\n", DEVICE_NAME);
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, devno);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(devno, 1);
printk(KERN_INFO "%s: 驱动卸载成功\n", DEVICE_NAME);
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
1.7 本章小结
这一章我们走完了字符设备驱动的完整流程。你想想看,其实就四个步骤:
- 定义
file_operations结构体,实现open、read、write等函数 - 注册设备号(推荐动态分配)
- 初始化
cdev并添加到内核 - 创建设备节点(推荐自动创建)
这个框架是所有字符设备驱动的基础。不管你是写LED驱动、按键驱动还是传感器驱动,底层都是这套东西。下一章,我会带你在这个框架基础上,加入真正的硬件操作,让驱动"活"起来。
嗯,今天就到这里。记住,驱动开发没有捷径,多写多练才是王道。咱们下章见。
公众号:蓝海资料掘金营,微信deep3321