1. 设备树基础与内核处理机制第一次接触设备树时我完全被那些嵌套的节点和属性搞懵了。直到在IMX6ULL项目上实际调试LED驱动时才真正理解设备树的价值。简单来说设备树就是告诉内核硬件长什么样的配置文件。比如LED连接在哪个GPIO引脚按键的中断号是多少这些过去写在C文件里的硬件信息现在都转移到设备树里了。设备树源文件(.dts)的语法其实很有规律。每个硬件模块对应一个节点(node)节点里用属性(property)描述硬件特征。举个例子下面是描述UART设备的典型写法uart1: serial02020000 { 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) | (330), 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 设备树调试方法查看编译后的dtbfdtdump /boot/imx6ull.dtb | less运行时检查节点ls /proc/device-tree/ cat /proc/device-tree/leds/led0/gpios确认驱动匹配cat /sys/kernel/debug/device_component5.2 典型问题解决问题1驱动probe函数没被调用检查/sys/firmware/devicetree/base下节点是否存在确认compatible字符串完全匹配检查status属性是否为okay问题2GPIO申请失败先用gpiod_direction_input()测试GPIO是否可用检查pinctrl配置是否正确确认GPIO没有被其他驱动占用问题3中断不触发用cat /proc/interrupts查看中断计数检查设备树interrupts属性格式确认GPIO中断类型(边沿/电平)设置正确记得第一次调试IMX6ULL按键驱动时中断死活不触发最后发现是设备树里interrupts属性少了一个参数。这种问题通过对比芯片手册和成功案例最容易定位。
嵌入式Linux驱动开发进阶:设备树与按键驱动的实战解析
发布时间:2026/5/20 6:01:21
1. 设备树基础与内核处理机制第一次接触设备树时我完全被那些嵌套的节点和属性搞懵了。直到在IMX6ULL项目上实际调试LED驱动时才真正理解设备树的价值。简单来说设备树就是告诉内核硬件长什么样的配置文件。比如LED连接在哪个GPIO引脚按键的中断号是多少这些过去写在C文件里的硬件信息现在都转移到设备树里了。设备树源文件(.dts)的语法其实很有规律。每个硬件模块对应一个节点(node)节点里用属性(property)描述硬件特征。举个例子下面是描述UART设备的典型写法uart1: serial02020000 { 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) | (330), 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 设备树调试方法查看编译后的dtbfdtdump /boot/imx6ull.dtb | less运行时检查节点ls /proc/device-tree/ cat /proc/device-tree/leds/led0/gpios确认驱动匹配cat /sys/kernel/debug/device_component5.2 典型问题解决问题1驱动probe函数没被调用检查/sys/firmware/devicetree/base下节点是否存在确认compatible字符串完全匹配检查status属性是否为okay问题2GPIO申请失败先用gpiod_direction_input()测试GPIO是否可用检查pinctrl配置是否正确确认GPIO没有被其他驱动占用问题3中断不触发用cat /proc/interrupts查看中断计数检查设备树interrupts属性格式确认GPIO中断类型(边沿/电平)设置正确记得第一次调试IMX6ULL按键驱动时中断死活不触发最后发现是设备树里interrupts属性少了一个参数。这种问题通过对比芯片手册和成功案例最容易定位。