嵌入式Linux驱动开发进阶:设备树与按键驱动的实战解析
1. 设备树基础与内核处理机制
第一次接触设备树时,我完全被那些嵌套的节点和属性搞懵了。直到在IMX6ULL项目上实际调试LED驱动时,才真正理解设备树的价值。简单来说,设备树就是告诉内核"硬件长什么样"的配置文件。比如LED连接在哪个GPIO引脚?按键的中断号是多少?这些过去写在C文件里的硬件信息,现在都转移到设备树里了。
设备树源文件(.dts)的语法其实很有规律。每个硬件模块对应一个节点(node),节点里用属性(property)描述硬件特征。举个例子,下面是描述UART设备的典型写法:
uart1: serial@02020000 { compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart"; reg = <0x02020000 0x4000>; interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clks IMX6UL_CLK_UART1_IPG>, <&clks IMX6UL_CLK_UART1_SERIAL>; clock-names = "ipg", "per"; status = "disabled"; };这里有几个关键点需要注意:
- compatible属性是驱动匹配的身份证,格式通常是"厂商,芯片型号"
- reg属性描述寄存器地址范围,第一个数字是基地址,第二个是长度
- interrupts属性定义中断号,不同平台格式可能不同
内核启动时,uboot会把编译好的dtb文件传给内核。内核的解析过程很有意思:它先把每个节点转换成device_node结构体,然后对某些特定节点(主要是带compatible属性的)进一步转换为platform_device。这个过程可以通过在系统启动时查看/sys/firmware/devicetree/base目录来验证。
2. 设备树驱动开发实战
2.1 LED驱动改造
传统LED驱动需要手动写死GPIO引脚号,换成设备树方案后,驱动变得灵活多了。最近在IMX6ULL开发板上实践时,我这样定义LED节点:
leds { compatible = "gpio-leds"; led0 { label = "sys_led"; gpios = <&gpio5 3 GPIO_ACTIVE_LOW>; linux,default-trigger = "heartbeat"; }; };驱动代码中获取设备树参数的典型流程如下:
static int led_probe(struct platform_device *pdev) { struct device_node *np = pdev->dev.of_node; int ret, gpio; gpio = of_get_named_gpio(np, "gpios", 0); ret = devm_gpio_request_one(&pdev->dev, gpio, GPIOF_OUT_INIT_LOW, "led"); /* 其他初始化代码 */ }调试时经常会遇到驱动和设备树不匹配的情况。我的经验是:
- 先用
of_find_node_by_path()确认节点是否存在 - 用
of_get_property()检查属性值是否正确 - 查看/sys/devices/platform下的设备是否生成
2.2 设备树与驱动匹配机制
驱动匹配的核心在于compatible字符串。在写驱动时,我们需要定义of_device_id数组:
static const struct of_device_id led_ids[] = { { .compatible = "gpio-leds" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, led_ids);当compatible匹配时,内核会自动调用probe函数。有个容易踩的坑是:设备树里的status属性必须是"okay",否则节点会被忽略。我曾经花了半天时间调试一个驱动,最后发现是status设成了"disabled"。
3. 按键驱动开发全解析
3.1 四种读取方式对比
在IMX6ULL上实现按键驱动时,我尝试了所有四种读取方式:
| 方式 | 实时性 | CPU占用 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 查询 | 低 | 100% | 简单 | 简单测试 |
| 休眠-唤醒 | 中 | 0% | 中等 | 通用场景 |
| poll/select | 中 | 0% | 中等 | 多路复用 |
| 异步通知 | 高 | 0% | 复杂 | 实时性要求高场景 |
查询方式虽然简单,但实际项目中基本不会用,因为会占满CPU。最常用的是休眠-唤醒机制,下面重点分析这种实现。
3.2 休眠-唤醒机制实现
驱动框架分为三个层次:
- 上层提供file_operations结构体
- 中间层管理button_operations操作集
- 底层实现具体硬件操作
关键代码结构如下:
static ssize_t button_read(struct file *file, char __user *buf, size_t size, loff_t *off) { /* 没有数据时休眠 */ wait_event_interruptible(button_waitq, ev_press); /* 被唤醒后复制数据到用户空间 */ copy_to_user(buf, &key_value, 1); ev_press = 0; return 1; } static irqreturn_t button_isr(int irq, void *dev_id) { /* 记录按键值并唤醒进程 */ key_value = gpio_get_value(pin); ev_press = 1; wake_up_interruptible(&button_waitq); return IRQ_HANDLED; }调试时发现一个典型问题:按键抖动会导致多次中断。解决方法是在中断处理中添加防抖逻辑:
static irqreturn_t button_isr(int irq, void *dev_id) { /* 10ms后再次检测引脚电平 */ mod_timer(&debounce_timer, jiffies + msecs_to_jiffies(10)); return IRQ_HANDLED; } static void debounce_timer_func(unsigned long data) { if (gpio_get_value(pin) == stable_value) { key_value = stable_value; ev_press = 1; wake_up_interruptible(&button_waitq); } }4. IMX6ULL按键驱动实战
4.1 硬件配置要点
以GPIO5_IO01为例,完整配置流程包括:
- 使能时钟:CCM_CCGR1[CG15]位
- 设置复用模式:IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER1
- 配置输入方向:GPIO5_GDIR寄存器
寄存器操作有个安全技巧:先用ioremap映射寄存器地址:
static void __iomem *base; base = ioremap(0x20C406C, 0x10); /* CCM寄存器基地址 */ writel(readl(base) | (3<<30), base); /* 使能GPIO5时钟 */4.2 完整驱动实现
结合设备树的按键驱动核心结构:
static int button_probe(struct platform_device *pdev) { /* 从设备树获取GPIO号 */ button->gpio = of_get_named_gpio(np, "gpios", 0); /* 申请GPIO中断 */ irq = gpio_to_irq(button->gpio); ret = request_irq(irq, button_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "button", NULL); /* 初始化等待队列 */ init_waitqueue_head(&button->waitq); }测试时发现一个关键点:GPIO编号在设备树和系统中的转换。设备树里写的是<&gpio5 1>,对应Linux系统中的GPIO号需要通过of_get_named_gpio()获取。
5. 调试技巧与常见问题
5.1 设备树调试方法
查看编译后的dtb:
fdtdump /boot/imx6ull.dtb | less运行时检查节点:
ls /proc/device-tree/ cat /proc/device-tree/leds/led0/gpios确认驱动匹配:
cat /sys/kernel/debug/device_component
5.2 典型问题解决
问题1:驱动probe函数没被调用
- 检查/sys/firmware/devicetree/base下节点是否存在
- 确认compatible字符串完全匹配
- 检查status属性是否为"okay"
问题2:GPIO申请失败
- 先用
gpiod_direction_input()测试GPIO是否可用 - 检查pinctrl配置是否正确
- 确认GPIO没有被其他驱动占用
问题3:中断不触发
- 用
cat /proc/interrupts查看中断计数 - 检查设备树interrupts属性格式
- 确认GPIO中断类型(边沿/电平)设置正确
记得第一次调试IMX6ULL按键驱动时,中断死活不触发,最后发现是设备树里interrupts属性少了一个参数。这种问题通过对比芯片手册和成功案例最容易定位。
