ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式驱动开发实战:从字符设备到设备树与中断处理

嵌入式驱动开发实战:从字符设备到设备树与中断处理 1. 嵌入式驱动开发到底在做什么很多人第一次听到“嵌入式驱动开发”这个词脑子里浮现的画面大概是一个人对着黑漆漆的终端敲着看不懂的寄存器地址旁边摆着一块布满针脚的开发板。这个印象不算错但只对了一半。驱动开发真正在做的事情是在硬件和操作系统之间架一座桥让上层的应用程序不需要知道底下那颗芯片的型号、引脚怎么接、时序怎么走就能正常读写数据、控制设备。我做了十多年嵌入式从裸机寄存器一路写到Linux内核模块踩过的坑比写过的驱动还多。这篇文章不打算照本宣科地讲什么“驱动模型分层”而是把我自己从入门到能独立负责一个完整驱动模块的经验拆开揉碎讲清楚。不管你是刚学完C语言想找方向的学生还是做了几年应用层想往下沉的老手都能从里面找到可以直接抄作业的东西。先说清楚范围。嵌入式驱动开发覆盖的面非常广从最简单的GPIO按键驱动到复杂的GPU驱动、网络协议栈驱动难度跨度极大。但底层逻辑是相通的理解硬件行为、匹配内核框架、处理并发与资源管理。这三件事做好了换什么平台、换什么外设你都能快速上手。2. 入门之前必须想清楚的几件事2.1 先搞清楚你面对的是哪一类驱动嵌入式驱动粗略分下来大概有这么几类难度和知识储备要求完全不同驱动类型典型外设核心难点适合阶段GPIO/中断类按键、LED、蜂鸣器中断处理、去抖、并发保护入门总线类I2C、SPI、UART时序理解、设备树匹配进阶存储类NAND、eMMC、SD卡块设备层、文件系统交互中高级网络类以太网、WiFi模组协议栈、DMA、缓冲区管理高级显示/GPU类LCD、GPU帧缓冲、内存管理、性能优化高级新手最容易犯的错误是一上来就想写GPU驱动或者网络驱动。不是说不行而是这些驱动涉及的内核子系统太多出了问题你根本不知道是哪一层的事。我的建议是从GPIO和中断开始把字符设备驱动的框架吃透再逐步往外扩。2.2 开发环境怎么搭才不折腾嵌入式开发最劝退新人的地方往往不是代码本身而是环境搭建。交叉编译器版本不对、内核头文件路径找不到、开发板和虚拟机网络不通随便一个都能耗掉你一整天。我自己的环境配置是这样的供你参考主机系统Ubuntu 20.04 LTS不要追新稳定比什么都重要交叉编译工具链优先用芯片原厂提供的比如ARM官方或者芯片厂商定制版不要自己瞎编内核源码必须和开发板上运行的内核版本完全一致差一个小版本都可能加载失败调试手段串口是命根子一定要确保串口能正常输出内核日志提示交叉编译工具链的路径一定要加到环境变量里并且确认${CROSS_COMPILE}前缀和你的工具链名字对得上。我见过太多人编译出来的驱动架构不对就是因为用了主机的gcc。2.3 内核源码到底要不要通读这个问题我被问过无数次。我的答案是不要通读但要会查。Linux内核几千万行代码你通读一遍黄花菜都凉了。正确的做法是写哪个驱动就把那个子系统相关的代码翻一遍。比如写I2C驱动你就去看drivers/i2c/目录下的核心层代码看别的厂商是怎么实现i2c_driver结构体的照葫芦画瓢。但有几样东西是必须熟悉的内核模块的加载卸载机制、字符设备注册流程、设备树的基本语法、以及并发控制的基本手段自旋锁、互斥锁、原子操作。这些是写任何驱动都绕不开的基础。3. 字符设备驱动从零写一个能跑的框架3.1 为什么从字符设备开始字符设备是Linux驱动里最简单也最基础的一类。它把设备抽象成一个文件上层用open、read、write、ioctl这些标准接口就能操作。按键、LED、传感器大部分都可以用字符设备框架来实现。写字符设备驱动核心就是填几个结构体、注册几个函数。但这里面有很多细节不注意就会踩坑。下面我按实际开发顺序一步步拆开讲。3.2 设备号的分配与注册每个字符设备都需要一个设备号分主设备号和次设备号。主设备号标识驱动次设备号标识具体设备。分配方式有两种// 方式一静态指定不推荐容易冲突 dev_t devno MKDEV(200, 0); register_chrdev_region(devno, 1, mydev); // 方式二动态分配推荐 alloc_chrdev_region(devno, 0, 1, mydev);动态分配的好处是内核帮你找一个没被占用的主设备号避免冲突。分配完之后用cat /proc/devices就能看到你的设备。3.3 cdev的初始化与绑定拿到设备号之后需要初始化cdev结构体把它和file_operations绑定起来struct cdev my_cdev; cdev_init(my_cdev, my_fops); my_cdev.owner THIS_MODULE; cdev_add(my_cdev, devno, 1);这里的file_operations是核心里面定义了上层调用open、read、write时实际执行的函数。你不需要实现所有函数用不到的留空就行。3.4 file_operations里必须实现的几个函数对于大多数字符设备驱动这几个函数是必须的open初始化设备申请资源release释放资源和open对应read从设备读数据到用户空间write从用户空间写数据到设备ioctl处理自定义命令比如设置参数这里有个新手常犯的错误read和write函数里的缓冲区是用户空间地址不能直接解引用必须用copy_to_user和copy_from_user。我见过有人直接memcpy结果内核直接崩溃。static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { char kbuf[64] hello; if (copy_to_user(buf, kbuf, strlen(kbuf))) return -EFAULT; return strlen(kbuf); }3.5 自动创建设备节点以前写驱动加载完之后要手动mknod创建设备节点麻烦得很。现在用class_create和device_create就能自动在/dev下生成节点struct class *my_class class_create(THIS_MODULE, myclass); device_create(my_class, NULL, devno, NULL, mydev);这样加载驱动之后/dev/mydev就自动出现了省事很多。4. 中断处理与按键驱动的实战细节4.1 中断注册的正确姿势按键驱动最核心的部分是中断处理。Linux里注册中断用request_irqrequest_irq(irq_num, my_handler, IRQF_TRIGGER_FALLING, my_key, NULL);这里有几个参数需要特别注意。IRQF_TRIGGER_FALLING表示下降沿触发具体用哪种触发方式要看硬件原理图。如果按键按下是低电平那就用下降沿或者低电平触发。搞错了要么一直触发要么一次都不触发。4.2 中断上下文里不能做什么中断处理函数运行在中断上下文这个时候不能睡眠、不能调用可能阻塞的函数。所以像copy_to_user、kmalloc不带GFP_ATOMIC标志这些都不能用。那按键按下了怎么通知上层两种方式阻塞式在read函数里等待用等待队列唤醒异步通知用fasync机制通过信号通知上层我一般用等待队列实现起来直观static DECLARE_WAIT_QUEUE_HEAD(key_wq); static volatile int key_pressed 0; static irqreturn_t key_handler(int irq, void *dev_id) { key_pressed 1; wake_up_interruptible(key_wq); return IRQ_HANDLED; } static ssize_t key_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { wait_event_interruptible(key_wq, key_pressed); key_pressed 0; // 返回按键值 }4.3 按键去抖的几种方案机械按键按下的时候会有抖动如果不处理一次按下可能触发好几次中断。常见方案有三种方案实现方式优缺点硬件去抖加RC电路可靠但增加成本中断延时中断里延时再读简单但占用中断时间定时器去抖中断启动定时器定时器里读推荐不阻塞中断我一般用定时器方案中断里只启动一个10ms的定时器定时器到期后再读引脚电平确认是按下还是抖动。4.4 并发保护不能省按键驱动里中断处理函数和read函数可能同时访问key_pressed变量这就涉及并发保护。简单场景用原子操作就够了static atomic_t key_pressed ATOMIC_INIT(0); atomic_set(key_pressed, 1); atomic_read(key_pressed);复杂场景比如要保护一个链表或者缓冲区就得用自旋锁或者互斥锁。中断上下文里只能用自旋锁因为互斥锁会睡眠。5. 设备树现代嵌入式驱动的必修课5.1 设备树解决了什么问题以前写驱动硬件信息都硬编码在代码里。换个板子引脚变了就得改代码重新编译。设备树把硬件描述从代码里剥离出来用一套独立的语法描述硬件内核启动时解析设备树驱动根据设备树里的信息来初始化。5.2 设备树的基本语法设备树文件后缀是.dts编译后变成.dtb。一个典型的按键节点长这样key_node { compatible mycompany,my-key; gpios gpio1 18 GPIO_ACTIVE_LOW; interrupt-parent gpio1; interrupts 18 IRQ_TYPE_EDGE_FALLING; status okay; };compatible属性最关键驱动里通过它来匹配设备。格式一般是厂商,设备名。5.3 驱动里怎么解析设备树驱动里用of_系列函数来读取设备树信息static int my_probe(struct platform_device *pdev) { struct device_node *np pdev-dev.of_node; int gpio of_get_named_gpio(np, gpios, 0); int irq gpio_to_irq(gpio); // ... }probe函数是平台驱动模型的核心设备树匹配成功后就会调用它。所有初始化工作都放在这里包括申请GPIO、注册中断、创建设备节点。5.4 设备树调试技巧设备树写错了驱动加载不上怎么排查几个实用命令ls /proc/device-tree/查看内核解析后的设备树dmesg | grep of_查看设备树相关的内核日志cat /sys/firmware/devicetree/base/...查看具体节点属性我踩过最坑的一次是设备树里GPIO编号写错了驱动一直probe失败查了半天才发现是引脚号算错了。所以写设备树的时候一定要对着原理图一个引脚一个引脚核对。6. 驱动调试那些教科书不讲的实战手段6.1 printk的等级与用法printk是驱动调试最常用的手段但它有等级之分printk(KERN_ERR this is error\n); printk(KERN_INFO this is info\n); printk(KERN_DEBUG this is debug\n);等级决定了这条日志会不会输出到控制台。默认情况下等级低于控制台等级的日志不会显示。可以通过echo 8 /proc/sys/kernel/printk临时调整。注意printk在中断上下文里可以用但频繁打印会严重影响性能生产环境一定要删掉或者改成pr_debug。6.2 用debugfs查看驱动状态debugfs是内核提供的一个调试文件系统驱动可以往里面导出内部状态。比如struct dentry *dir debugfs_create_dir(mykey, NULL); debugfs_create_u32(press_count, 0444, dir, press_count);这样在/sys/kernel/debug/mykey/press_count就能看到按键次数不用加打印就能监控驱动状态。6.3 常见问题速查表现象可能原因排查方向驱动加载失败设备树compatible不匹配检查dmesg日志中断不触发触发方式配置错误查原理图确认电平中断触发多次没有去抖加定时器去抖read阻塞不返回等待队列条件没满足检查wake_up调用系统崩溃用户空间地址直接访问改用copy_to/from_user设备节点不出现class或device创建失败检查返回值6.4 我的调试心得调试驱动最忌讳的就是瞎猜。遇到问题先看日志再看硬件手册最后才改代码。我见过太多人一上来就改代码改了半天发现是硬件接线的问题。另外每次只改一个地方。驱动调试涉及硬件、内核、驱动代码多个层面一次改多个地方出了问题你根本不知道是哪个改动导致的。7. 从驱动开发到系统级理解7.1 根文件系统挂载与驱动的关系驱动开发到一定阶段就不能只盯着单个驱动了。比如根文件系统挂载就和存储驱动、网络驱动密切相关。用NFS挂载根文件系统的时候网络驱动必须先工作正常否则内核根本找不到根文件系统。这个过程涉及内核启动流程、驱动加载顺序、文件系统初始化等多个环节。理解这些你才能定位“系统起不来”这类问题到底是哪一层的事。7.2 驱动开发者的能力进阶路线根据我自己的经历和带新人的经验嵌入式驱动开发的能力进阶大概分这么几个阶段第一阶段能照着模板写字符设备驱动会查手册会看日志第二阶段理解设备树、平台驱动模型能独立bringup一个外设第三阶段熟悉内核子系统能改核心层代码能优化性能第四阶段能设计驱动架构能带团队能解决跨模块的复杂问题每个阶段之间没有捷径就是多写、多看、多踩坑。我到现在也不敢说所有驱动都能写但至少遇到新外设我知道从哪里入手、怎么查资料、怎么验证。7.3 嵌入式AI与驱动开发的新交集最近两年嵌入式AI是个热词。但很多人不知道的是嵌入式AI的落地底层还是靠驱动。NPU驱动、GPU驱动、摄像头驱动、内存管理这些做不好上层模型跑得再快也没用。我最近在做一个边缘计算的项目需要在嵌入式设备上跑推理。模型本身不大但摄像头采集、内存拷贝、NPU调度这几个环节每一个都涉及驱动层面的优化。比如DMA缓冲区怎么分配、怎么减少内存拷贝次数这些直接决定了最终的帧率。所以如果你在做嵌入式AI不要只盯着模型和框架往下沉一层把驱动和硬件交互搞清楚往往能带来更大的性能提升。8. 一些零散但重要的经验8.1 代码风格与可维护性驱动代码是内核的一部分风格必须符合内核规范。缩进用Tab不用空格函数名用小写加下划线全局变量要加前缀。这些不是形式主义而是为了方便别人阅读和维护。另外错误处理一定要完整。每个可能失败的系统调用都要检查返回值失败后要释放已经申请的资源。我见过太多驱动因为错误处理不完整导致卸载模块时内存泄漏或者系统崩溃。8.2 版本管理驱动代码一定要用git管理每次改动都要有清晰的提交信息。因为驱动调试经常需要回退到某个能工作的版本没有版本管理你会很痛苦。8.3 文档与注释驱动代码里的注释重点写“为什么”而不是“是什么”。比如为什么这里要用自旋锁而不是互斥锁为什么这个延时是10ms而不是5ms。这些信息在几个月后你自己看代码的时候会非常有用。8.4 持续学习嵌入式驱动开发涉及的知识面太广了硬件、内核、编译工具、调试手段每一样都在更新。我的习惯是每周抽时间看看内核邮件列表看看最近有什么新特性、新驱动。不一定每个都看懂但保持对技术动态的敏感度遇到问题的时候至少知道去哪里找答案。最后分享一个我自己的小习惯每次解决一个比较棘手的问题之后我都会把排查过程和解决方法记下来。几年下来这个笔记成了我最有价值的资料库。下次遇到类似问题翻一下笔记就能快速定位比重新查一遍快得多。
返回列表