第三讲:编写第一个字符设备驱动
从零开始创建一个简单的虚拟传感器驱动,实现 open、read、ioctl 等基本操作,并注册到 PX4 设备树。
好,咱们直接动手。这一讲的目标很明确——写一个能跑起来的字符设备驱动。说白了,就是让 PX4 认识你的虚拟传感器,能打开它、读数据、还能通过 ioctl 调调参数。
我记得刚接触 PX4 驱动时,最头疼的就是不知道从哪下手。设备树、注册函数、文件操作接口……一堆概念堆在一起。后来我发现,其实核心就三件事:定义操作、注册设备、实现回调。今天我们就按这个思路来。
1. 驱动骨架:先搭个空壳
每个字符设备驱动,本质上就是一组函数指针的集合。你想想看,Linux 内核怎么知道你的设备能干啥?就是靠 file_operations 这个结构体。
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#define DEVICE_NAME "px4_virt_sensor"
#define CLASS_NAME "virt_sensor"
static int major_num;
static struct class *virt_sensor_class = NULL;
static struct device *virt_sensor_device = NULL;
static int virt_sensor_open(struct inode *inodep, struct file *filep) {
pr_info("virt_sensor: device opened\n");
return 0;
}
static int virt_sensor_release(struct inode *inodep, struct file *filep) {
pr_info("virt_sensor: device closed\n");
return 0;
}
static struct file_operations fops = {
.open = virt_sensor_open,
.release = virt_sensor_release,
};
嗯,这里要注意:pr_info 是内核打印函数,调试时特别好用。我习惯在 open 和 release 里加打印,这样能确认驱动是否被正确调用。
2. 实现 read 操作:让数据流动起来
虚拟传感器嘛,总得吐出点数据。我们模拟一个温度传感器,每次读取返回一个递增的整数。
static int sensor_value = 25; // 初始温度
static ssize_t virt_sensor_read(struct file *filep, char __user *buffer,
size_t len, loff_t *offset) {
char msg[32];
int msg_len;
sensor_value += 1; // 模拟温度变化
msg_len = snprintf(msg, sizeof(msg), "%d\n", sensor_value);
if (copy_to_user(buffer, msg, msg_len)) {
pr_err("virt_sensor: copy_to_user failed\n");
return -EFAULT;
}
return msg_len;
}
这里有个坑,我曾经踩过——copy_to_user 必须检查返回值。内核空间和用户空间的内存不能直接赋值,必须用这个专用函数。如果返回非零,说明拷贝失败,得返回 -EFAULT。
然后把 read 挂到 fops 里:
static struct file_operations fops = {
.open = virt_sensor_open,
.release = virt_sensor_release,
.read = virt_sensor_read,
};
3. ioctl 操作:让用户能控制你
光能读还不够,有时候用户想重置传感器值,或者改变模拟步长。这时候 ioctl 就派上用场了。
#define IOCTL_RESET_VALUE _IO('v', 1)
#define IOCTL_SET_STEP _IOW('v', 2, int)
static int step = 1;
static long virt_sensor_ioctl(struct file *filep, unsigned int cmd,
unsigned long arg) {
int tmp;
switch (cmd) {
case IOCTL_RESET_VALUE:
sensor_value = 25;
pr_info("virt_sensor: value reset to 25\n");
break;
case IOCTL_SET_STEP:
if (copy_from_user(&tmp, (int __user *)arg, sizeof(tmp))) {
return -EFAULT;
}
step = tmp;
pr_info("virt_sensor: step set to %d\n", step);
break;
default:
return -ENOTTY;
}
return 0;
}
注意 ioctl 的命令定义格式:_IO、_IOW、_IOR 分别对应无参数、写参数、读参数。我个人习惯用 _IOW 传参数,因为大多数控制操作都需要输入值。
同样,把 ioctl 挂到 fops:
static struct file_operations fops = {
.open = virt_sensor_open,
.release = virt_sensor_release,
.read = virt_sensor_read,
.unlocked_ioctl = virt_sensor_ioctl,
};
4. 注册到 PX4 设备树
驱动写好了,怎么让 PX4 知道它?答案是设备树。在 PX4 的 boards/ 目录下找到你的板子配置文件,添加节点。
virt_sensor: virt-sensor {
compatible = "px4,virt-sensor";
status = "okay";
};
然后在驱动初始化时,通过 of_match_table 匹配:
static const struct of_device_id virt_sensor_of_match[] = {
{ .compatible = "px4,virt-sensor" },
{ /* end of list */ }
};
MODULE_DEVICE_TABLE(of, virt_sensor_of_match);
static int __init virt_sensor_init(void) {
// 注册字符设备
major_num = register_chrdev(0, DEVICE_NAME, &fops);
if (major_num < 0) {
pr_err("virt_sensor: failed to register device\n");
return major_num;
}
// 创建类
virt_sensor_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(virt_sensor_class)) {
unregister_chrdev(major_num, DEVICE_NAME);
return PTR_ERR(virt_sensor_class);
}
// 创建设备
virt_sensor_device = device_create(virt_sensor_class, NULL,
MKDEV(major_num, 0), NULL,
DEVICE_NAME);
if (IS_ERR(virt_sensor_device)) {
class_destroy(virt_sensor_class);
unregister_chrdev(major_num, DEVICE_NAME);
return PTR_ERR(virt_sensor_device);
}
pr_info("virt_sensor: driver loaded, major=%d\n", major_num);
return 0;
}
static void __exit virt_sensor_exit(void) {
device_destroy(virt_sensor_class, MKDEV(major_num, 0));
class_destroy(virt_sensor_class);
unregister_chrdev(major_num, DEVICE_NAME);
pr_info("virt_sensor: driver unloaded\n");
}
module_init(virt_sensor_init);
module_exit(virt_sensor_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("PX4 Driver Course");
MODULE_DESCRIPTION("A virtual sensor driver for PX4");
核心流程总结:
- 定义 file_operations 结构体,挂载 open/read/ioctl/release
- 实现 read 时用 copy_to_user 传递数据
- 实现 ioctl 时用 copy_from_user 接收参数
- 在设备树中添加 compatible 节点
- 初始化时注册字符设备、创建 class 和 device
5. 驱动加载与测试
编译后,用 insmod 加载模块:
# insmod virt_sensor.ko
# dmesg | tail
[ 1234.5678] virt_sensor: driver loaded, major=240
# 创建设备节点
# mknod /dev/px4_virt_sensor c 240 0
# 测试 read
# cat /dev/px4_virt_sensor
26
27
28
# 测试 ioctl
# ./test_ioctl reset
# cat /dev/px4_virt_sensor
25
调试小技巧: 如果 cat 时卡住,检查 read 函数是否返回了正确的长度。我曾经因为返回值写错,导致用户态一直读不到数据,排查了半天才发现是少了个换行符。
注意: 在 PX4 真实环境中,设备节点通常由 udev 自动创建。但开发阶段手动 mknod 更可控。另外,ioctl 的命令号不要和其他驱动冲突,建议用 _IO 宏生成唯一命令。
6. 知识体系图
下面这张图展示了本章的核心逻辑,从用户态到内核态的完整链路:
这张图把整个调用链路画清楚了。用户态应用通过系统调用进入 VFS,VFS 根据设备号找到对应的驱动,然后调用你注册的 file_operations 函数。说白了,驱动就是个中间人,负责把内核和硬件(或者虚拟硬件)连接起来。
好了,这一讲的内容就是这些。代码量不大,但每个细节都值得推敲。尤其是 copy_to_user 和 ioctl 命令定义,这两个地方最容易出 bug。我当年第一次写驱动时,就因为 ioctl 命令号写错,导致应用层一直返回 -1,查了两天才发现是宏定义拼写错误……嗯,都是经验。
公众号:蓝海资料掘金营,微信deep3321