第二章:字符设备驱动框架——从Hello World开始
说实话,我刚开始学驱动开发那会儿,最困惑的就是:驱动到底是个什么东西?
后来我明白了。驱动说白了就是一层软件,它告诉内核怎么跟硬件打交道。而字符设备驱动,是所有驱动里最基础、最容易上手的一种。今天我们就从它开始。
2.1 字符设备 vs 块设备 vs 网络设备
Linux把设备分成三大类。我简单说一下:
- 字符设备:按字节流读写,比如串口、LED、按键。你读一个字节就是一个字节。
- 块设备:按块读写,比如硬盘、eMMC。一次读512字节或4K。
- 网络设备:通过socket访问,比如以太网、WiFi。
我们做车规芯片,大部分外设都是字符设备。像GPIO、I2C、SPI、UART,全是字符设备。所以学好这个,后面就顺了。
2.2 file_operations 结构体——驱动的灵魂
每个字符设备驱动,核心就是一个结构体:struct file_operations。它定义了驱动能干什么。
我习惯把它叫做「驱动的函数表」。内核通过这个表,知道用户调用open()时该执行哪个函数,调用read()时又该执行哪个。
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 *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
// ... 还有很多
};
嗯,这里要注意:owner 一般填 THIS_MODULE,防止模块被卸载时还有人用。
我在项目中遇到过一个问题:有个同事忘了填 owner,结果模块卸载后,用户程序还在调用驱动,直接内核崩溃。从那以后,我每次写驱动第一件事就是检查这个字段。
2.3 主次设备号——驱动的身份证
每个字符设备在内核里都有一个编号。主设备号标识驱动类型,次设备号标识具体设备。
举个例子:
| 设备 | 主设备号 | 次设备号 |
|---|---|---|
| /dev/ttyS0 | 4 | 64 |
| /dev/ttyS1 | 4 | 65 |
| /dev/ttyUSB0 | 188 | 0 |
你看,同一个主设备号(4)对应同一个驱动(串口驱动),次设备号区分不同串口。
分配设备号有两种方式:
- 静态分配:自己指定主设备号。用
register_chrdev_region()。适合你知道哪些号没人用的情况。 - 动态分配:让内核帮你分配。用
alloc_chrdev_region()。我推荐新手用这个,省心。
2.4 设备节点的创建与注销
设备节点就是 /dev/xxx 那个文件。用户程序通过它访问驱动。
创建节点有两种方法:
- 手动创建:用
mknod /dev/hello c 240 0。调试时方便。 - 自动创建:用
class_create()+device_create()。产品中必须用这个。
我建议你直接学自动创建。为什么?因为车规芯片的板子,你不可能让产线工人去敲 mknod 命令。
static struct class *hello_class;
static struct device *hello_device;
// 在模块初始化时
hello_class = class_create(THIS_MODULE, "hello_class");
hello_device = device_create(hello_class, NULL, devno, NULL, "hello");
// 在模块退出时
device_destroy(hello_class, devno);
class_destroy(hello_class);
2.5 Hello World 驱动——完整的例子
好了,理论说完了。我们来写一个真正的驱动。它什么都不做,就是打印一句 "Hello World"。
你想想看,一个驱动再复杂,骨架也就是这样:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#define DEVICE_NAME "hello"
#define CLASS_NAME "hello_class"
static int major;
static struct class *hello_class;
static struct device *hello_device;
// open 函数
static int hello_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Hello World: device opened\n");
return 0;
}
// release 函数
static int hello_release(struct inode *inode, struct file *file) {
printk(KERN_INFO "Hello World: device closed\n");
return 0;
}
// file_operations 结构体
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
};
// 模块初始化
static int __init hello_init(void) {
dev_t devno;
// 动态分配设备号
alloc_chrdev_region(&devno, 0, 1, DEVICE_NAME);
major = MAJOR(devno);
// 注册字符设备
register_chrdev_region(devno, 1, DEVICE_NAME);
cdev_init(&hello_cdev, &hello_fops);
cdev_add(&hello_cdev, devno, 1);
// 创建设备节点
hello_class = class_create(THIS_MODULE, CLASS_NAME);
hello_device = device_create(hello_class, NULL, devno, NULL, DEVICE_NAME);
printk(KERN_INFO "Hello World driver loaded, major=%d\n", major);
return 0;
}
// 模块退出
static void __exit hello_exit(void) {
dev_t devno = MKDEV(major, 0);
device_destroy(hello_class, devno);
class_destroy(hello_class);
cdev_del(&hello_cdev);
unregister_chrdev_region(devno, 1);
printk(KERN_INFO "Hello World driver unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World driver");
这个驱动加载后,你在终端执行:
# insmod hello.ko
# echo "test" > /dev/hello
# cat /dev/hello
# rmmod hello
用 dmesg 就能看到打印信息。
- file_operations 是驱动的接口表
- 主次设备号是驱动的身份证
- 设备节点是用户访问驱动的入口
- 初始化顺序:分配设备号 → 注册cdev → 创建class → 创建device
- 退出顺序:销毁device → 销毁class → 删除cdev → 释放设备号
2.6 避坑指南
我做了这么多年驱动,有些坑是新人必踩的。今天先给你打个预防针:
- printk 不打印? 检查一下日志级别。用
dmesg看,别用cat /var/log/messages。 - insmod 失败? 看看内核版本对不对。车规芯片经常用老内核,别拿新内核的API去编。
- open 设备节点返回 -1? 权限问题。先
chmod 666 /dev/hello试试。 - 卸载模块时卡死? 有进程还在用你的设备。用
lsof查一下。
嗯,今天就到这儿。下一章我们讲如何给驱动添加读写功能,让驱动真正「动」起来。