3、Linux内核驱动基础:字符设备驱动模型、platform驱动、设备树(DTS)解析、GPIO与中断处理
各位同学,今天我们来啃一块硬骨头——Linux内核驱动基础。说实话,这部分内容在车载传感器开发中,几乎是绕不过去的坎。我当年刚入行时,被设备树折腾得够呛,后来才慢慢摸清门道。今天我把这些经验掰开揉碎了讲给你听。
3.1 字符设备驱动模型:最基础的驱动形态
字符设备驱动,说白了就是按字节流读写数据的设备驱动。像串口、GPIO、I2C这些,都属于字符设备。为什么叫“字符”?因为它一次传输一个字符(字节),不像块设备那样按块操作。
驱动开发的核心,就是实现 file_operations 结构体。这个结构体定义了设备文件的操作方法,比如 open、read、write、ioctl 等。我习惯把它叫做“驱动与用户空间的桥梁”。
核心要点:字符设备驱动的注册流程
// 1. 定义 file_operations 结构体
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
// 2. 注册字符设备
static int __init my_init(void) {
int ret;
dev_t dev_num;
// 分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
if (ret < 0) {
printk("Failed to allocate device number\n");
return ret;
}
// 初始化 cdev 并添加到内核
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
return 0;
}
// 3. 卸载时清理
static void __exit my_exit(void) {
dev_t dev_num = MKDEV(major, 0);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
}
嗯,这里要注意:cdev_add 之后,设备就“活”了。用户空间可以通过 open("/dev/my_device") 来访问。我在项目中遇到过一个问题——忘记检查 alloc_chrdev_region 的返回值,结果设备号冲突,驱动加载失败。这种低级错误,排查起来还挺费时间的。
3.2 platform驱动:设备与驱动的“红娘”
你想想看,在传统的嵌入式系统中,设备信息是硬编码在板级文件里的。每次换一个板子,就得改代码,多麻烦。platform 驱动机制就是为了解决这个问题——它把设备和驱动分离,通过名字匹配来“牵线搭桥”。
说白了,platform 驱动就是一套“设备-驱动”的匹配框架。设备端提供资源信息(地址、中断号等),驱动端提供操作函数。内核负责匹配,匹配成功就调用驱动的 probe 函数。
个人经验:我建议你在写 platform 驱动时,先把设备树节点写好,再写驱动。这样思路更清晰,不容易漏掉资源。
// platform_driver 结构体
static struct platform_driver my_platform_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_device",
.of_match_table = my_of_match,
},
};
// probe 函数:驱动匹配成功后的入口
static int my_probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
int irq;
// 获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
// 获取中断号
irq = platform_get_irq(pdev, 0);
// 注册字符设备
// ...
return 0;
}
我曾经在调试一个车载摄像头驱动时,发现 probe 函数一直没被调用。查了半天,原来是设备树里的 compatible 属性和驱动里的 of_match_table 没对上。这种问题,说白了就是名字写错了,但排查起来真让人头大。
3.3 设备树(DTS)解析:硬件配置的“说明书”
设备树,英文叫 Device Tree,是描述硬件信息的文本文件。它用树形结构来描述 CPU、内存、外设等硬件资源。内核启动时,会解析 DTB(设备树二进制文件),然后根据这些信息来初始化设备。
为什么需要设备树?因为 ARM 架构下,硬件平台五花八门。没有设备树之前,每个平台都得写一堆板级文件,维护起来简直是噩梦。设备树把硬件描述和内核代码分离开,换平台只需要换 DTB 文件就行。
设备树的基本语法:
/dts-v1/;
/ {
model = "My Car Sensor Board";
compatible = "vendor,my-board";
// CPU 节点
cpu@0 {
compatible = "arm,cortex-a7";
reg = <0x0>;
};
// 外设节点
i2c@1c00000 {
compatible = "vendor,i2c-controller";
reg = <0x1c00000 0x1000>;
interrupts = <0 31 4>;
clock-frequency = <100000>;
// 子节点:I2C 设备
temperature-sensor@48 {
compatible = "vendor,tmp102";
reg = <0x48>;
interrupt-parent = <&gpio1>;
interrupts = <5 2>;
};
};
// GPIO 控制器
gpio1: gpio@1c80000 {
compatible = "vendor,gpio-controller";
reg = <0x1c80000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
};
};
在驱动中解析设备树,常用的 API 有:
of_find_node_by_path():通过路径查找节点of_property_read_u32():读取 32 位整数属性of_get_named_gpio():获取 GPIO 编号irq_of_parse_and_map():解析中断并映射
避坑指南:我曾经在解析设备树时,忘记检查 of_find_node_by_path 的返回值。结果节点不存在时,内核直接 panic 了。记住:设备树解析函数的返回值一定要检查,尤其是 IS_ERR() 和 NULL 的情况。
3.4 GPIO与中断处理:与硬件“握手”
GPIO 是嵌入式开发中最基础的外设。控制一个 LED、读取一个按键,都离不开 GPIO。在 Linux 内核中,GPIO 操作有标准的 API 接口。
中断处理则是驱动开发的另一个核心。传感器数据变化、按键按下,都需要通过中断来通知 CPU。轮询?那太浪费 CPU 资源了,尤其是在车载系统中,实时性要求很高。
GPIO 操作示例
// 申请 GPIO
int gpio_num = of_get_named_gpio(np, "led-gpios", 0);
if (gpio_num < 0) {
dev_err(dev, "Failed to get GPIO\n");
return gpio_num;
}
// 设置方向为输出
ret = gpio_direction_output(gpio_num, 0);
if (ret < 0) {
dev_err(dev, "Failed to set GPIO direction\n");
return ret;
}
// 输出高电平
gpio_set_value(gpio_num, 1);
// 释放 GPIO
gpio_free(gpio_num);
中断处理示例
// 申请中断
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(dev, "Failed to get IRQ\n");
return irq;
}
// 注册中断处理函数
ret = request_irq(irq, my_irq_handler,
IRQF_TRIGGER_RISING,
"my_device", dev);
if (ret) {
dev_err(dev, "Failed to request IRQ\n");
return ret;
}
// 中断处理函数
static irqreturn_t my_irq_handler(int irq, void *dev_id) {
struct my_device *dev = dev_id;
// 读取传感器数据
// 处理数据
// 唤醒等待队列
return IRQ_HANDLED;
}
// 释放中断
free_irq(irq, dev);
个人经验:中断处理函数里不要做耗时操作。我习惯在中断里只做“标记”,把实际的数据处理放到工作队列或 tasklet 中。这样能保证中断响应速度,避免丢失中断。
为什么会这样?因为中断上下文是不能睡眠的。如果你在中断里调用 msleep() 或 mutex_lock(),内核会直接报错。嗯,这个坑我踩过,调试时系统莫名其妙卡死,最后发现是中断里调了 kmalloc(GFP_KERNEL)——这个函数可能会睡眠。
3.5 实战经验总结
好了,讲了这么多,我来总结几个关键点:
- 字符设备驱动是基础,
file_operations结构体要烂熟于心 - platform 驱动是设备与驱动的桥梁,匹配机制要理解透彻
- 设备树是硬件配置的说明书,解析时要仔细检查返回值
- GPIO 和中断是驱动与硬件交互的“双手”,中断上下文不能睡眠
我在车载项目中,经常需要同时处理多个传感器。每个传感器都有自己的中断,GPIO 资源也有限。这时候,设备树就派上大用场了——硬件配置一目了然,换传感器只需要改 DTS 文件,驱动代码基本不用动。
最后说一句:驱动开发没有捷径,多写代码、多调试、多踩坑,自然就熟练了。下次我们讲 I2C 和 SPI 总线驱动,这两个在车载传感器中也很常见。到时候再聊!