资讯详情

资讯详情

嵌入式Linux与安卓驱动开发实战:从内核模块到HAL层适配

嵌入式Linux和安卓驱动开发这两个方向单独拎出来任何一个都够啃上一阵子。但现实里的岗位需求往往是交叉的——比如车载中控、智能家居面板、工业HMI这些产品底层跑的是Linux内核上层交互却是安卓那套框架中间那层驱动和HAL就成了连接两个世界的桥梁。我当初从单片机转过来的时候最大的感受就是写驱动不难难的是搞清楚这个驱动到底该挂在哪一层、用什么子系统、跟上层怎么对接。这个实战项目就是围绕这条链路展开的从内核模块的编译加载到设备树配置再到安卓HAL层的适配把整个流程串起来走一遍。适合有一定C基础、想往底层系统方向深入的朋友也适合已经做应用开发、想往下探一探系统边界的同学。1. 为什么嵌入式Linux驱动开发值得投入精力1.1 驱动开发在产业链中的实际位置很多人对驱动开发的理解停留在写寄存器这个层面觉得无非就是照着芯片手册往地址上写值。这个认知不能说错但太窄了。在真实的嵌入式Linux产品里驱动工程师做的事情远不止操作寄存器更多时候是在跟内核子系统打交道——你要理解input子系统怎么上报事件、i2c子系统怎么管理总线、gpio子系统怎么抽象引脚、dmaengine怎么搬运数据。这些子系统构成了内核的骨架驱动只是往骨架上挂肉。从岗位需求来看做驱动的人往往需要同时具备几个能力看得懂原理图、读得懂芯片手册、理得清内核框架、调得动硬件时序。这四样缺一个都会在实际项目中卡壳。我见过不少朋友能写应用层代码但一碰到probe函数不执行、设备树节点匹配不上、中断注册失败这类问题就束手无策根源就在于对内核启动流程和设备模型没有建立起完整的认知。嵌入式Linux驱动开发的另一个特点是经验壁垒高。应用层的问题可以通过搜索引擎快速找到答案但驱动层的问题往往跟具体硬件、具体内核版本、具体配置强相关网上能直接抄的答案很少。这就意味着一旦你在这个领域积累了足够的调试经验你的不可替代性会明显高于纯应用开发者。1.2 安卓驱动开发与纯Linux驱动的差异安卓虽然底层也是Linux内核但它在驱动层面做了不少自己的扩展。最典型的就是Binder驱动这是安卓IPC机制的基石不在主线内核里需要单独打补丁。另外像ashmem、logger这些安卓特有的驱动也都是主线内核没有的。所以做安卓驱动开发你不仅要懂标准Linux驱动模型还要理解安卓在电源管理、内存管理、进程通信这几个方面对内核做的定制。从实际工作内容来看安卓驱动工程师经常要处理的事情包括为新的传感器写HAL层适配、调试Display和GPU驱动的兼容性问题、优化电源管理相关的驱动逻辑、处理安卓特有的权限和SELinux策略。这些事情跟纯Linux驱动开发有重叠但侧重点不同。纯Linux驱动更关注硬件功能的正确实现安卓驱动则要在功能正确的基础上额外考虑与安卓框架的兼容性和性能表现。1.3 从学习曲线看投入产出比驱动开发的学习曲线确实陡前三个月可能连一个简单的字符设备都写不利索但一旦跨过那个门槛后面的路会越走越宽。我自己的经验是把学习过程分成三个阶段比较合理第一阶段是能写一个完整的字符设备驱动包括open、read、write、ioctl这些基本操作第二阶段是能基于设备树写平台驱动理解probe的触发时机和资源获取方式第三阶段是能跟内核子系统对接比如写一个input设备或者i2c从设备驱动。每个阶段大概需要一到两个月的业余时间投入前提是你有一套能实际跑起来的开发板。光看视频不动手看再多也白搭。这个实战项目的设计思路就是让你在每个阶段都有具体的代码可以跑、有具体的现象可以观察而不是停留在理论层面。2. 搭建一套能跑通驱动代码的开发环境2.1 开发板与内核源码的选择策略选开发板这件事我的建议是不要一上来就买最贵的但也不要贪便宜买那种资料残缺的。一块好的开发板应该满足几个条件——芯片手册齐全、内核源码有维护、社区活跃度尚可。市面上常见的几款芯片里全志、瑞芯微、恩智浦的文档相对完善社区资源也比较多。如果预算有限可以先从一块二手的开发板入手把环境跑通再说。内核源码的获取方式取决于你用的芯片。有些厂商会提供完整的内核源码包有些则只给patch。拿到源码后第一件事是确认版本号因为不同版本的内核在API上有差异比如gpio_request在新版本里已经被gpiod_get系列替代了。我建议初学者从4.19或5.4这两个LTS版本入手资料多、坑少、社区支持周期长。交叉编译工具链的选择也要注意。ARM架构常用的有arm-linux-gnueabihf-和aarch64-linux-gnu-两种前者对应32位ARM后者对应64位ARM。选错了工具链编译出来的内核根本跑不起来。确认方法很简单用file命令看一下目标平台的架构或者直接问开发板厂商要推荐的工具链版本。2.2 内核编译与模块加载的完整流程编译内核这件事第一次做的时候容易在配置环节卡住。make menuconfig打开的是一个基于ncurses的配置界面里面选项成千上万新手很容易懵。我的做法是先用厂商提供的默认配置文件通常是defconfig在此基础上只改必要的选项。比如要支持某个驱动就在Device Drivers菜单下找到对应的子菜单把它编译成模块选M或者编进内核选Y。编译命令本身不复杂make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- defconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j$(nproc) make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- modules但这里有几个容易踩的坑。第一-j后面的数字不要盲目设太大编译内核很吃内存并行度太高容易OOM。第二编译模块之前必须先编译内核因为模块编译依赖内核生成的符号表。第三如果改了内核配置记得重新编译内核和模块只编模块不编内核会导致版本不匹配。模块加载用insmod或modprobe前者需要指定完整路径后者会自动处理依赖关系。加载之后用dmesg看内核日志确认probe函数有没有执行、有没有报错。卸载用rmmod如果模块正在被使用会卸载失败这时候要先停掉使用它的进程。2.3 根文件系统与NFS挂载的实操细节开发阶段用NFS挂载根文件系统能省很多事改完代码不用重新烧录重启就能生效。配置NFS的步骤大致是在主机上安装NFS服务、编辑/etc/exports导出根文件系统目录、启动服务然后在开发板的启动参数里指定root/dev/nfs和nfsroot的地址。这里有个细节容易被忽略NFS版本的选择。有些开发板的u-boot默认用NFS v2但现代Linux发行版上的NFS服务默认可能只开v3或v4。版本不匹配的表现是内核启动到挂载根文件系统那一步就卡住报VFS: Unable to mount root fs via NFS。解决办法是在u-boot的bootargs里显式指定nfsvers3或者在NFS服务端配置里开启v2支持。另一个常见问题是权限。NFS导出的目录如果权限设置不对开发板挂载后可能无法写入导致系统启动过程中创建临时文件失败。建议在/etc/exports里加上rw,sync,no_root_squash这几个选项no_root_squash尤其重要否则开发板上的root用户会被映射成nobody很多操作都会失败。3. 从零写一个字符设备驱动的关键步骤3.1 字符设备驱动的骨架结构字符设备驱动是最基础的驱动类型理解了它后面学块设备和网络设备会容易很多。一个完整的字符设备驱动包含几个核心部分模块的加载和卸载函数、file_operations结构体的填充、设备号的申请和释放、以及cdev的注册和注销。先看模块加载函数。它做的事情通常是申请设备号、初始化cdev、把它添加到内核。设备号的申请有两种方式静态指定和动态分配。静态指定用register_chrdev_region需要你提前确定一个没被占用的主设备号动态分配用alloc_chrdev_region内核会自动找一个空闲的号给你。我推荐用动态分配省心且不容易冲突。static int __init mydev_init(void) { int ret; dev_t devno; ret alloc_chrdev_region(devno, 0, 1, mydev); if (ret 0) { printk(KERN_ERR alloc_chrdev_region failed\n); return ret; } mydev_major MAJOR(devno); cdev_init(mydev_cdev, mydev_fops); mydev_cdev.owner THIS_MODULE; ret cdev_add(mydev_cdev, devno, 1); if (ret 0) { unregister_chrdev_region(devno, 1); return ret; } return 0; }file_operations结构体是驱动跟用户空间交互的接口。最基本的几个成员是open、release、read、write如果需要更复杂的控制就加上unlocked_ioctl。每个函数的实现都要注意用户空间和内核空间的数据拷贝问题不能直接解引用用户空间传进来的指针必须用copy_to_user和copy_from_user。3.2 设备树节点的编写与匹配机制现代Linux驱动基本都走设备树这条路平台设备通过设备树节点来描述硬件资源驱动通过compatible属性跟节点匹配。写设备树节点的时候compatible属性的值很关键它决定了哪个驱动会跟这个节点绑定。通常格式是厂商前缀,设备型号比如myvendor,mydev。一个典型的设备树节点长这样mydev: mydev10000000 { compatible myvendor,mydev; reg 0x10000000 0x1000; interrupts 0 42 4; clocks clk_gate 5; status okay; };驱动这边需要定义一个of_device_id数组里面的compatible字符串要跟设备树里的一致static const struct of_device_id mydev_of_match[] { { .compatible myvendor,mydev }, { } }; MODULE_DEVICE_TABLE(of, mydev_of_match);匹配成功后驱动的probe函数会被调用。在probe里你可以通过platform_get_resource获取reg属性描述的内存区域通过platform_get_irq获取中断号通过devm_clk_get获取时钟。这些资源获取函数都有devm_前缀的版本它们会在驱动卸载时自动释放资源省去了手动清理的麻烦。3.3 中断处理与并发控制的实战要点中断处理是驱动开发里比较容易出问题的地方。申请中断用request_irq需要指定中断号、中断处理函数、触发方式和设备标识。中断处理函数运行在中断上下文不能睡眠不能调用可能引起阻塞的函数。如果中断处理逻辑比较复杂应该采用上半部下半部的拆分方式上半部只做最紧急的事情剩下的交给下半部去处理。下半部的实现方式有几种tasklet、工作队列、线程化中断。tasklet运行在软中断上下文仍然不能睡眠工作队列运行在进程上下文可以睡眠线程化中断则是把中断处理函数交给一个内核线程去执行本质上也是进程上下文。选择哪种方式取决于你的具体需求如果下半部需要访问可能睡眠的资源就必须用工作队列或线程化中断。并发控制是另一个绕不开的话题。驱动代码可能被多个进程同时打开也可能被中断处理函数和进程上下文同时访问如果不做保护就会出现竞态条件。常用的保护机制有自旋锁、互斥锁、信号量、原子操作。自旋锁适合保护短小的临界区在中断上下文里只能用自旋锁互斥锁适合保护可能睡眠的临界区但不能在中断上下文里使用。4. 安卓HAL层适配与驱动对接4.1 安卓HAL的基本架构与驱动的关系安卓的HAL层是连接框架层和内核驱动的中间层。框架层通过HAL接口调用硬件功能HAL层再通过系统调用或设备节点跟驱动交互。在安卓8.0之前HAL模块是以共享库的形式存在的框架层通过hw_get_module加载对应的HAL库。安卓8.0之后引入了Treble架构HAL被拆分成独立的进程通过HIDL或AIDL跟框架层通信。对于驱动开发者来说需要关注的是HAL层怎么跟驱动对接。以传感器为例驱动负责从硬件读取原始数据并通过input子系统或字符设备上报HAL层则负责把这些原始数据转换成安卓框架能理解的格式再通过HIDL接口上报给SensorService。这个过程中驱动和HAL之间的接口定义很关键通常是通过sysfs节点、ioctl或者input事件来传递数据。4.2 从内核驱动到安卓框架的数据通路以一个简单的GPIO按键为例完整的通路是这样的硬件按键产生电平变化GPIO驱动检测到变化后触发中断中断处理函数通过input子系统上报按键事件input子系统把事件写入/dev/input/eventX设备节点安卓的EventHub读取这个节点把事件分发给KeyCharacterMap最终传递给应用层。在这个通路里驱动工程师需要确保几件事中断触发方式配置正确上升沿、下降沿还是双边沿、按键去抖逻辑合理、input设备注册时上报的事件类型和键值正确。如果按键上报的键值跟安卓的KeyCode对不上应用层就收不到正确的事件。调试的时候可以用getevent命令查看input事件确认驱动上报的数据是否符合预期。对于更复杂的设备比如触摸屏通路会更长一些。触摸屏驱动通过i2c读取触摸坐标经过校准和滤波后通过input子系统上报ABS_MT_POSITION_X/Y事件安卓的InputReader解析这些事件并生成MotionEvent再分发给应用。这个过程中触摸屏的校准参数、上报速率、多点触控协议版本都会影响最终的用户体验。4.3 调试安卓驱动问题的常用手段调试安卓驱动问题logcat和dmesg是两个最常用的工具。logcat看的是用户空间和框架层的日志dmesg看的是内核空间的日志。遇到问题时先看dmesg确认驱动有没有正常加载、probe有没有成功、有没有报错信息再看logcat确认HAL层和框架层有没有收到数据。如果驱动加载正常但上层收不到数据可以在HAL层加日志确认HAL有没有被调用、有没有从驱动读到数据。如果HAL读到了数据但框架层没反应就要检查HIDL接口的实现和注册是否正确。这种逐层排查的方法虽然笨但最有效能快速定位问题出在哪一层。还有一个技巧是用strace跟踪HAL进程的系统调用看看它到底有没有打开设备节点、有没有执行read或ioctl操作。如果HAL根本没有打开设备节点那问题可能出在权限配置或SELinux策略上。安卓的SELinux策略比较严格新增的设备节点需要在file_contexts里配置安全上下文否则HAL进程没有权限访问。5. 驱动开发中那些文档不会告诉你的坑5.1 内核版本差异导致的API变更内核API的稳定性远没有应用层那么好不同版本之间函数签名、结构体成员、头文件路径都可能变化。比如gpio_request在4.19里还能用到了5.10就被标记为deprecated推荐用gpiod_get。再比如copy_from_user的返回值判断老版本返回未拷贝的字节数新版本行为一致但有些驱动代码写得不严谨容易出问题。应对这个问题的方法有几个第一养成看内核文档的习惯Documentation/目录下有大量说明第二用git log查看某个函数的变更历史了解它什么时候被修改过第三编译时打开-Werror让编译器帮你发现不兼容的地方。我自己的习惯是每换一个内核版本先把驱动代码编译一遍看看有没有warning有warning就及时处理不要等到运行时出问题再回头找。5.2 设备树配置错误引发的连锁反应设备树配置错误是驱动开发中最常见的问题之一而且症状往往很隐蔽。比如reg属性的地址范围写错了驱动可能能probe成功但读写寄存器时行为异常中断号写错了驱动可能根本收不到中断时钟配置不对外设可能工作不稳定。排查设备树问题首先要确认内核有没有正确解析设备树。可以在启动日志里搜索设备树节点的名字看看内核有没有识别到这个节点。如果节点存在但驱动没probe检查compatible属性是否匹配。如果probe了但功能异常用devm_ioremap之后的返回值确认内存映射是否成功用cat /proc/interrupts确认中断有没有注册上。还有一个容易忽略的点是设备树的覆盖机制。有些开发板支持设备树覆盖overlay在基础设备树之上动态修改节点。如果overlay写错了可能会导致节点属性被意外覆盖症状是驱动行为跟预期不符。调试的时候可以用/proc/device-tree目录查看实际生效的设备树内容确认属性值是否正确。5.3 电源管理与休眠唤醒的调试经验电源管理是嵌入式产品里很关键但容易被忽视的部分。驱动如果没处理好电源管理轻则待机功耗偏高重则系统无法正常休眠或唤醒。Linux的电源管理框架提供了runtime PM和system PM两套机制runtime PM用于设备空闲时自动进入低功耗状态system PM用于系统级别的休眠。实现runtime PM需要在驱动里调用pm_runtime_enable、pm_runtime_get_sync、pm_runtime_put_sync这些函数并在合适的时机调用pm_runtime_set_autosuspend_delay设置自动挂起延迟。调试的时候可以通过/sys/devices/.../power/runtime_status查看设备的runtime PM状态通过/sys/kernel/debug/pm_genpd查看电源域的状态。system PM的调试更复杂一些因为涉及到整个系统的状态保存和恢复。常见的坑包括唤醒源配置错误导致无法唤醒、休眠过程中某个驱动没有正确保存寄存器状态导致唤醒后外设异常、唤醒后时钟没有恢复导致系统跑飞。调试system PM问题可以在内核启动参数里加上no_console_suspend这样休眠过程中串口仍然可用能看到更多日志。6. 从项目实战到面试通关的能力映射6.1 面试官真正想考察的驱动能力驱动岗位的面试跟应用开发岗位有明显区别面试官更关注你对底层机制的理解深度而不是你会用多少框架。常见的问题包括字符设备和块设备的区别是什么、设备树的作用和匹配机制、中断上半部和下半部的区别、自旋锁和互斥锁的使用场景、内核启动流程的几个阶段。这些问题看起来是概念题但面试官往往会在你回答之后追问细节比如你说自旋锁不能睡眠那如果在自旋锁保护的临界区里调用了可能睡眠的函数会怎样这种追问才是真正拉开差距的地方。除了概念面试官还会考察你的调试能力。比如驱动加载后probe函数没执行你怎么排查、系统启动到某个阶段卡住了你怎么定位问题、设备工作不稳定偶尔丢数据你怎么分析。这类问题没有标准答案面试官看的是你的排查思路是否清晰、是否知道用哪些工具、是否能从现象反推到根因。6.2 项目经历在简历中怎么呈现才有说服力写简历的时候不要只写负责XX驱动的开发要写清楚你解决了什么具体问题、用了什么技术手段、达到了什么效果。比如为某型号触摸屏编写i2c驱动通过优化中断处理逻辑将触摸响应延迟从15ms降低到8ms这种描述比负责触摸屏驱动开发有说服力得多。如果有条件可以在简历里附上项目的代码仓库链接或者技术博客链接。面试官如果看到你有持续输出技术内容会对你加分不少。另外项目描述里最好能体现你对整个系统的理解而不只是你负责的那一小块。比如你写了一个传感器驱动可以顺带提一下这个驱动在安卓HAL层是怎么被调用的、数据是怎么流转到应用层的这样能展示你的全局视野。6.3 持续学习路线的规划建议驱动开发这个方向技术更新不算快但知识面很广。我的建议是沿着深度优先、广度跟进的思路来规划学习路线。深度方面先把一个子系统吃透比如input子系统或者i2c子系统理解它的框架设计、数据结构、核心流程。广度方面定期关注内核社区的动态看看新版本引入了哪些新特性、哪些旧API被废弃了。阅读内核源码是提升能力的必经之路但不要一上来就啃大部头。可以从某个具体驱动的源码入手比如drivers/input/keyboard/gpio_keys.c这个文件不长但涵盖了平台驱动、设备树、中断、input子系统等多个知识点。读懂之后再去看更复杂的驱动循序渐进。最后说一点个人体会驱动开发这个方向前期投入大、见效慢但一旦跨过门槛职业发展的天花板会比纯应用开发高不少。尤其是现在智能汽车、边缘计算、工业物联网这些领域对底层人才的需求在持续增长把驱动开发的能力打扎实机会不会少。关键是别停留在看视频、看文档的层面一定要有实际的硬件平台去跑代码、去调问题只有真正踩过坑、填过坑那些知识才会变成你自己的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →