资讯详情

资讯详情

嵌入式Linux与安卓驱动开发实战:从设备树到HAL层全链路打通

1. 这个项目到底在解决什么问题嵌入式Linux和安卓驱动开发这两个词放在一起很多刚入行的朋友第一反应是“高大上”第二反应是“我该从哪下手”。我带过不少新人也面过不少候选人发现一个很普遍的现象大家在学校里学过操作系统原理也写过一些单片机程序但一到“给一块板子移植安卓系统并让它跑起来”这种任务面前就完全不知道从哪一步开始。这个实战项目的核心价值就是把这团乱麻理成一条清晰的线。先说清楚这个项目适合谁。如果你已经会写C语言对Linux基本命令不陌生知道什么是进程和线程但从来没有完整地做过一个嵌入式驱动项目那这个内容就是为你准备的。如果你已经工作了一两年一直在做应用层开发想往底层走但苦于没有完整的项目经验这个实战路径同样适用。甚至你是一个在校学生想找一个能写进简历、能在面试时讲清楚的项目这套东西也能直接拿来用。这个项目要解决的问题很具体让你从零开始理解一个安卓设备从硬件上电到系统跑起来中间到底经历了什么驱动在其中扮演了什么角色以及你如何亲手写一个能工作的驱动。不是那种“点个灯”的玩具驱动而是真正能和安卓系统交互的、有实际功能的驱动模块。做完之后你至少能说清楚下面这些问题设备树怎么描述硬件、内核模块怎么编译和加载、字符设备驱动怎么和用户空间通信、安卓的HAL层怎么和内核驱动对接。这些东西才是面试官真正想听的。我见过太多简历上写着“熟悉Linux驱动开发”的人一问细节就露馅。比如问他“你写的驱动怎么和安卓的Framework层通信”他只能含糊地说“通过系统调用”。再追问“那HAL层你改过吗”就答不上来了。这个实战项目的目标就是让你成为那个能答上来的人。下面我会把整个项目的设计思路、核心细节、实操步骤和踩坑经验全部拆开讲你跟着走一遍就能自己复现出来。2. 项目整体设计与技术选型思路2.1 为什么选嵌入式Linux加安卓这个组合嵌入式Linux是底层基础安卓是上层应用生态这两者的结合在智能硬件领域非常普遍。你看到的智能音箱、车载中控、工业平板、自助终端背后基本都是这个架构。选这个组合作为实战项目有几个很实际的考虑。第一资料相对丰富但又不至于烂大街。纯单片机项目太基础面试官没兴趣问纯应用开发又离底层太远体现不出你的系统能力。嵌入式Linux驱动开发正好卡在中间既能展示你对操作系统的理解又能证明你有动手能力。第二安卓的加入让项目有了“系统级”的复杂度。你要考虑的不只是一个驱动能不能跑还要考虑它怎么被安卓的上层服务调用怎么和电源管理配合怎么在系统启动时正确初始化。这些才是真正区分“学过”和“做过”的地方。第三从就业市场来看做嵌入式驱动开发的岗位一直有稳定需求尤其是涉及安卓系统的。因为安卓的生态太庞大了任何一家做智能硬件的公司都需要有人能把底层驱动和上层系统打通。你把这个项目吃透面试的时候就有东西可讲而且讲出来的东西是成体系的不是零散的知识点。2.2 项目架构的层次划分整个项目我把它分成四层来看这样你在做的时候心里有张图知道每一步在哪个位置。最底层是硬件层也就是你手里的开发板。我建议选一款主流的、资料相对齐全的板子比如基于某款国产处理器的开发板社区活跃遇到问题容易找到参考。不要一上来就选太冷门的芯片否则光是找手册就能耗掉你一半的精力。往上是内核层包括Linux内核本身和设备驱动。这一层是你主要的工作区域。你需要理解内核的启动流程、设备树的作用、驱动模块的加载机制。具体来说你要写一个字符设备驱动实现open、read、write、ioctl这些标准接口让用户空间能通过设备节点和硬件交互。再往上是安卓系统层包括HAL层和Framework层。HAL层是安卓为了屏蔽底层硬件差异而设计的抽象层你的驱动最终要通过HAL层暴露给上层。这一层你需要写一个简单的HAL模块让安卓的Service能调用到你的驱动功能。最上面是应用层你可以写一个简单的安卓App来验证整个链路是否打通。这个App不需要复杂能通过按钮触发驱动操作、能显示驱动返回的数据就够了。这四层打通你就有了一个完整的、可以讲清楚的项目。2.3 开发环境的选择与搭建逻辑开发环境这块我踩过不少坑这里直接给你一套经过验证的方案。主机系统用Ubuntu 20.04或者22.04不要用太新的版本否则一些交叉编译工具链可能会有兼容性问题。虚拟机或者物理机都行但建议至少分配4核CPU和8G内存因为编译安卓源码的时候资源消耗很大。交叉编译工具链的选择很关键。一般来说开发板厂商会提供对应的工具链优先用厂商提供的版本因为那是经过验证的。如果厂商没提供就用Linaro或者ARM官方发布的版本。这里有个细节要注意工具链的版本要和内核版本匹配。比如你用的是Linux 4.19的内核就不要用太新的GCC版本去编译否则可能会出现一些奇怪的编译错误。安卓源码的获取和编译是另一个大工程。如果你的目标只是验证驱动和HAL层的对接不需要编译完整的安卓系统可以用开发板厂商提供的现成镜像然后单独编译你的驱动模块和HAL模块推送到板子上替换。这样可以节省大量时间。但如果你想完整地走一遍流程那就需要下载AOSP源码配置好环境这个过程可能需要几个小时甚至更久取决于你的网络和机器性能。注意编译安卓源码对磁盘空间要求很高建议至少预留200G以上的空间而且要用SSD机械硬盘编译起来会非常慢。3. 核心细节解析与实操要点3.1 设备树硬件描述的关键机制设备树是嵌入式Linux开发中绕不开的一个概念。很多从单片机转过来的朋友不理解为什么要有这个东西。简单类比一下单片机开发的时候你写代码直接操作寄存器地址硬件信息是硬编码在代码里的。但Linux内核要支持成千上万种不同的硬件组合不可能为每个板子都编译一个内核。设备树的作用就是把硬件信息从内核代码里抽出来用一个独立的文件来描述。内核启动的时候读取这个文件就知道这块板子上有什么设备、地址是多少、中断号是多少。写设备树节点的时候有几个关键属性必须搞清楚。compatible属性是最重要的它用来匹配驱动。格式一般是“厂商,设备名”比如“myvendor,mydevice”。内核启动时会拿这个字符串去和驱动里注册的of_device_id表做匹配匹配上了才会调用驱动的probe函数。reg属性描述设备的寄存器地址范围interrupts属性描述中断信息。这些属性的具体格式需要参考内核文档里对应绑定文件的说明。我实际操作中发现一个容易出错的地方设备树节点的层级关系。比如你的设备挂在一个I2C总线上那你的设备节点就要写在I2C控制器的节点里面而不是写在根节点下面。这个层级关系决定了内核在解析设备树时能不能正确地找到你的设备。刚开始做的时候我建议你先找一个类似设备的设备树节点作为参考照着改不要自己凭空写。3.2 字符设备驱动的核心框架字符设备驱动是最基础也是最常用的驱动类型。它的核心就是实现一套文件操作接口让用户空间可以通过/dev下面的设备节点来访问硬件。这套接口包括open、release、read、write、ioctl、mmap等。不是每个都要实现但open、release、read、write这四个是最基本的。驱动注册的流程是这样的首先用alloc_chrdev_region动态分配一个设备号或者用register_chrdev_region注册一个指定的设备号。然后初始化一个cdev结构体用cdev_init把它和你的file_operations结构体关联起来再用cdev_add把它添加到内核。最后用class_create和device_create在/sys和/dev下面创建设备节点。这一套流程看起来步骤多但每一步都有明确的目的你照着写一遍就能记住。file_operations结构体里的函数实现是重点。open函数一般用来初始化硬件、申请资源。read函数把硬件的数据拷贝到用户空间这里必须用copy_to_user不能直接用memcpy否则会有安全问题。write函数反过来用copy_from_user把用户数据拷贝到内核空间。ioctl函数用来处理一些不适合用read/write完成的控制命令比如设置硬件参数、获取状态信息等。提示在read和write函数里一定要检查用户空间指针的有效性并且正确处理返回值。我见过有人在read里直接返回0表示成功结果上层应用一直读不到数据排查了半天才发现是返回值的问题。3.3 安卓HAL层的对接方式HAL层是安卓系统里比较特殊的一层。它的存在是为了让安卓的Framework层不直接依赖Linux内核接口从而保证安卓系统的可移植性。不同版本的安卓HAL的写法不太一样。早期的安卓用hw_get_module这种方式后来引入了HIDL再后来是AIDL。对于实战项目来说我建议从最简单的hw_get_module方式入手先把链路打通再去研究新的机制。写一个HAL模块核心是实现一个hw_module_t结构体和一个hw_device_t结构体。hw_module_t里的open函数负责打开你的设备节点初始化hw_device_t里的函数指针。安卓的Service通过hw_get_module拿到模块调用open拿到设备然后就可以调用你定义的函数了。这些函数最终会通过ioctl或者read/write和内核驱动通信。这里有个经验HAL层的代码编译成.so库之后要放到/system/lib/hw或者/vendor/lib/hw目录下命名规则是“模块名.设备名.so”。比如你的模块叫mydevice设备名是default那文件名就是mydevice.default.so。安卓在加载的时候会按照这个命名规则去查找。如果名字不对hw_get_module就会返回失败而且错误信息不太直观容易让人摸不着头脑。3.4 内核模块的编译与加载内核模块的编译需要内核源码树的支持。你的驱动代码里会包含一些内核头文件编译的时候需要指定内核源码的路径。Makefile的写法有固定的模板核心就是指定obj-m变量和内核源码目录。编译出来的.ko文件就是内核模块可以用insmod命令加载用rmmod卸载用lsmod查看已加载的模块。加载模块的时候内核会调用你的module_init函数。在这个函数里你要完成设备号的申请、cdev的注册、设备节点的创建等工作。卸载的时候module_exit函数负责清理这些资源。这里有个原则init函数里申请的资源exit函数里一定要释放否则模块卸载后资源泄漏下次再加载可能会失败。调试内核模块比调试用户空间程序要麻烦一些。printk是最常用的调试手段但要注意日志级别。默认情况下只有比console_loglevel更高级别的消息才会打印到控制台。你可以用dmesg命令查看内核日志或者调整/proc/sys/kernel/printk的值来改变日志级别。另外如果模块加载后系统崩溃了可能是空指针访问或者内存越界这时候需要用内核的oops信息来定位问题。4. 完整实操流程与关键环节实现4.1 开发环境搭建的详细步骤第一步安装Ubuntu系统。我推荐用20.04 LTS版本稳定性好社区支持也到位。安装完成后先更新软件源安装必要的工具包build-essential、git、vim、minicom、tftp-hpa、nfs-kernel-server等。这些工具在后面都会用到。第二步获取交叉编译工具链。如果开发板厂商提供了工具链直接解压到/opt目录下然后把bin目录加到PATH环境变量里。验证方法是在终端输入工具链的前缀比如arm-linux-gnueabihf-gcc -v如果能显示版本信息就说明配置成功了。第三步获取内核源码。可以从开发板厂商的Git仓库克隆也可以用内核官方的源码。用厂商的源码好处是已经包含了开发板的配置文件编译出来的内核可以直接用。进入内核源码目录后先执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- xxx_defconfig来生成默认配置然后make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- menuconfig可以进入图形化配置界面根据需要调整内核选项。第四步编译内核。执行make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- -j4开始编译-j4表示用4个线程并行编译可以根据你的CPU核心数调整。编译完成后在arch/arm/boot目录下会生成zImage文件在arch/arm/boot/dts目录下会生成设备树文件.dtb。第五步准备根文件系统。可以用BusyBox自己构建一个简单的根文件系统也可以用开发板厂商提供的现成文件系统。如果要用安卓系统那就需要单独准备安卓的镜像文件。4.2 设备树节点的编写与验证假设我们要添加一个自定义的字符设备挂在I2C总线上。首先找到I2C控制器的设备树节点在它下面添加子节点。代码大概长这样i2c1 { status okay; mydevice: mydevice50 { compatible myvendor,mydevice; reg 0x50; interrupt-parent gpio1; interrupts 12 IRQ_TYPE_EDGE_FALLING; reset-gpios gpio1 13 GPIO_ACTIVE_LOW; status okay; }; };这段代码里compatible属性是驱动匹配的关键reg属性指定了设备在I2C总线上的地址interrupts属性描述了中断信息reset-gpios指定了复位引脚。这些属性的具体含义需要参考内核文档里对应的绑定说明。写完设备树之后重新编译设备树文件替换到板子上启动系统。然后查看/proc/device-tree或者/sys/firmware/devicetree/base目录看看你的节点有没有被正确解析。如果节点存在但驱动没有加载那可能是compatible属性不匹配或者驱动没有编译进内核。注意修改设备树之后一定要重新编译并更新到板子上不要只改源码不更新镜像否则你调试半天发现改动根本没生效。4.3 字符设备驱动的代码实现下面是一个简化的字符设备驱动框架我把它拆成几个关键部分来讲。模块初始化和退出部分#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #define DEVICE_NAME mydevice #define CLASS_NAME myclass static int major_number; static struct class *myclass; static struct cdev mycdev; static struct device *mydevice; static int my_open(struct inode *inodep, struct file *filep) { printk(KERN_INFO mydevice: opened\n); return 0; } static ssize_t my_read(struct file *filep, char __user *buffer, size_t len, loff_t *offset) { char msg[] hello from kernel; int msg_len strlen(msg); if (*offset msg_len) return 0; if (copy_to_user(buffer, msg *offset, msg_len - *offset)) return -EFAULT; *offset msg_len - *offset; return msg_len - *offset; } static ssize_t my_write(struct file *filep, const char __user *buffer, size_t len, loff_t *offset) { char kbuf[256]; if (len sizeof(kbuf) - 1) len sizeof(kbuf) - 1; if (copy_from_user(kbuf, buffer, len)) return -EFAULT; kbuf[len] \0; printk(KERN_INFO mydevice: received %s\n, kbuf); return len; } static int my_release(struct inode *inodep, struct file *filep) { printk(KERN_INFO mydevice: closed\n); return 0; } static struct file_operations fops { .owner THIS_MODULE, .open my_open, .read my_read, .write my_write, .release my_release, };这段代码实现了最基本的open、read、write、release四个接口。read函数里用copy_to_user把内核数据拷贝到用户空间write函数里用copy_from_user把用户数据拷贝到内核空间。这两个函数是用户空间和内核空间数据交换的标准方式不能直接用memcpy替代。模块初始化和退出static int __init mydevice_init(void) { dev_t dev_num; int ret; ret alloc_chrdev_region(dev_num, 0, 1, DEVICE_NAME); if (ret 0) { printk(KERN_ERR mydevice: failed to allocate major number\n); return ret; } major_number MAJOR(dev_num); cdev_init(mycdev, fops); mycdev.owner THIS_MODULE; ret cdev_add(mycdev, dev_num, 1); if (ret 0) { unregister_chrdev_region(dev_num, 1); return ret; } myclass class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(myclass)) { cdev_del(mycdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(myclass); } mydevice device_create(myclass, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(mydevice)) { class_destroy(myclass); cdev_del(mycdev); unregister_chrdev_region(dev_num, 1); return PTR_ERR(mydevice); } printk(KERN_INFO mydevice: registered with major %d\n, major_number); return 0; } static void __exit mydevice_exit(void) { device_destroy(myclass, MKDEV(major_number, 0)); class_destroy(myclass); cdev_del(mycdev); unregister_chrdev_region(MKDEV(major_number, 0), 1); printk(KERN_INFO mydevice: unregistered\n); } module_init(mydevice_init); module_exit(mydevice_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(Your Name); MODULE_DESCRIPTION(A simple character device driver);init函数里按顺序完成了设备号申请、cdev注册、class创建、device创建。每一步都有错误处理失败的时候要回滚前面已经完成的操作。exit函数里按相反的顺序释放资源。这个顺序不能乱否则会出现资源泄漏或者内核崩溃。4.4 编译加载与功能验证Makefile的写法obj-m mydevice.o KERNEL_DIR ? /path/to/kernel/source ARCH ? arm CROSS_COMPILE ? arm-linux-gnueabihf- all: make -C $(KERNEL_DIR) M$(PWD) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) modules clean: make -C $(KERNEL_DIR) M$(PWD) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) clean把KERNEL_DIR替换成你实际的内核源码路径然后执行make命令就会生成mydevice.ko文件。把ko文件拷贝到板子上执行insmod mydevice.ko加载模块。然后用dmesg查看内核日志应该能看到注册成功的消息。用ls /dev/mydevice确认设备节点已经创建。接着可以写一个简单的测试程序#include stdio.h #include fcntl.h #include unistd.h #include string.h int main() { int fd open(/dev/mydevice, O_RDWR); if (fd 0) { perror(open); return 1; } char buf[256]; int n read(fd, buf, sizeof(buf)); if (n 0) { buf[n] \0; printf(read: %s\n, buf); } char *msg hello driver; write(fd, msg, strlen(msg)); close(fd); return 0; }交叉编译这个测试程序推送到板子上运行。如果能看到“read: hello from kernel”的输出并且dmesg里有“received hello driver”的日志说明驱动的基本功能已经正常工作了。4.5 HAL层与安卓系统的对接HAL层的代码结构相对固定。你需要定义一个hw_module_t结构体实现open函数在open函数里打开设备节点并初始化hw_device_t。hw_device_t里包含了你希望暴露给上层的函数指针。#include hardware/hardware.h #include fcntl.h #include errno.h struct mydevice_module { struct hw_module_t common; }; struct mydevice_device { struct hw_device_t common; int fd; }; static int mydevice_open(const struct hw_module_t *module, const char *id, struct hw_device_t **device) { struct mydevice_device *dev malloc(sizeof(*dev)); memset(dev, 0, sizeof(*dev)); dev-common.tag HARDWARE_DEVICE_TAG; dev-common.version 0; dev-common.module (struct hw_module_t *)module; dev-common.close mydevice_close; dev-fd open(/dev/mydevice, O_RDWR); if (dev-fd 0) { free(dev); return -errno; } *device dev-common; return 0; } static struct hw_module_methods_t mydevice_methods { .open mydevice_open, }; struct mydevice_module HAL_MODULE_INFO_SYM { .common { .tag HARDWARE_MODULE_TAG, .module_api_version 1, .hal_api_version 0, .id mydevice, .name My Device HAL, .author Your Name, .methods mydevice_methods, }, };编译成so库之后放到/vendor/lib/hw/目录下命名为mydevice.default.so。然后在安卓的Service里通过hw_get_module加载这个模块调用open拿到设备就可以通过函数指针调用你实现的函数了。提示不同安卓版本的HAL接口可能有差异建议先确认你的安卓版本对应的HAL规范再动手写代码。另外SELinux策略也可能影响HAL模块的加载如果加载失败先检查dmesg和logcat里有没有权限相关的错误。5. 常见问题与排查技巧实录5.1 驱动加载失败的问题排查驱动加载失败是最常见的问题表现是insmod命令返回错误或者dmesg里有报错信息。排查的时候按下面的顺序来。先看错误码。insmod失败的时候会返回一个错误码比如“Invalid parameters”通常是设备树匹配失败“Device or resource busy”通常是设备号冲突“Operation not permitted”可能是权限问题或者SELinux限制。根据错误码可以快速缩小排查范围。再看dmesg日志。内核在加载模块失败的时候会打印详细的错误信息包括出错的文件名和行号。比如“mydevice: probe of mydevice50 failed with error -22”说明probe函数返回了-22也就是-EINVAL通常是参数配置有问题。然后检查设备树。确认compatible属性和驱动里的of_device_id表是否匹配reg属性是否和硬件实际地址一致status属性是否是“okay”。可以用cat /proc/device-tree/路径/节点名/compatible来查看设备树里的实际值。最后检查内核配置。确认相关的驱动框架有没有编译进内核比如I2C控制器驱动、GPIO驱动等。如果这些基础驱动没有使能你的设备驱动也无法正常工作。5.2 读写数据异常的处理思路读写数据异常通常表现为read返回0或者负值write返回的值和预期不符或者数据内容不对。如果read返回0说明驱动认为没有数据可读。检查你的read函数里offset的处理逻辑是不是第一次读之后offset就超出了数据长度。如果read返回负值看错误码是什么-EFAULT通常是copy_to_user失败检查用户空间指针是否有效。如果write返回的值和写入长度不一致检查你的write函数里对len的处理。有些驱动会限制单次写入的最大长度超出部分会被截断。另外注意write函数的返回值应该是实际写入的字节数不是请求写入的字节数。如果数据内容不对最常见的原因是字节序问题或者数据对齐问题。嵌入式平台很多是ARM架构默认是小端序但有些硬件寄存器是大端序读写的时候需要做转换。另外如果数据结构里有padding用户空间和内核空间看到的布局可能不一致需要用__attribute__((packed))来取消对齐。5.3 系统启动卡死或崩溃的定位方法系统启动卡死或者崩溃是比较严重的问题通常和驱动初始化有关。定位方法如下。首先看串口输出。嵌入式开发板一般都有调试串口系统启动过程中的所有内核日志都会从串口输出。如果系统卡死串口最后输出的那几行就是线索。比如卡在“mydevice: initializing”说明你的init函数里某一步阻塞了。如果串口没有输出可能是内核根本没启动检查启动参数和设备树是否正确。如果串口有输出但突然中断可能是内核panic了查看oops信息里的PC值和调用栈能定位到出错的函数。常见的原因包括空指针访问、内存越界、死锁、中断处理函数里调用了可能睡眠的函数。我遇到过一次在probe函数里调用了msleep但probe是在原子上下文里执行的导致系统崩溃。后来改成在workqueue里做延时操作就正常了。注意内核崩溃的调试信息非常重要一定要完整保存下来。如果串口输出太快来不及看可以把日志重定向到文件或者用netconsole把日志通过网络发送到另一台机器上。5.4 常见问题速查表问题现象可能原因排查方法insmod返回“Invalid parameters”设备树compatible不匹配检查驱动of_device_id表和设备树节点insmod返回“Device or resource busy”设备号冲突用cat /proc/devices查看已用设备号read返回-EFAULTcopy_to_user失败检查用户空间指针有效性系统启动卡死init函数阻塞检查init函数里是否有睡眠操作HAL模块加载失败so库路径或命名错误确认文件名和存放目录数据内容错乱字节序或对齐问题检查数据结构的packed属性模块卸载后再次加载失败资源未完全释放检查exit函数是否释放了所有资源6. 从项目到面试的转化技巧6.1 如何把项目经验讲成面试官想听的故事做完这个项目之后你在简历上不能只写“实现了字符设备驱动”。这样写太单薄面试官看不出你的能力边界。我建议你这样描述基于某款嵌入式平台完成了从设备树配置、内核驱动开发、HAL层对接到应用验证的完整链路实现了某功能的端到端打通。面试的时候面试官通常会顺着简历往下问。你要准备好几个层次的回答。第一层是“做了什么”用一两句话概括项目目标和你的角色。第二层是“怎么做的”讲清楚技术选型的原因和关键实现细节。第三层是“遇到了什么问题怎么解决的”这是最能体现你能力的地方。第四层是“如果重新做你会怎么改进”展示你的反思和成长。我面过很多人发现一个规律能讲清楚“为什么”的人比只能讲“是什么”的人通过率高很多。比如你写了一个字符设备驱动面试官问你为什么用字符设备而不是块设备或者网络设备你要能说出字符设备适合流式数据、不需要缓冲、直接和用户空间交互这些特点。再比如问你为什么用ioctl而不是sysfs你要能说出ioctl适合传递二进制数据和控制命令sysfs适合展示简单的属性值。6.2 面试中高频追问点及应对策略根据我的经验嵌入式驱动开发的面试有几个问题是几乎必问的。第一个是“内核空间和用户空间的数据拷贝为什么不能用memcpy”。这个问题考的是你对地址空间隔离的理解。标准回答是用户空间的指针在内核空间不能直接解引用因为两者的地址映射不同而且用户空间指针可能无效直接访问会导致内核崩溃。必须用copy_to_user和copy_from_user这两个函数会做地址有效性检查并且在访问失败时返回错误码而不是崩溃。第二个是“中断处理函数里为什么不能睡眠”。这个问题考的是你对中断上下文的理解。中断处理函数运行在原子上下文不能调度不能睡眠不能获取可能睡眠的锁。如果需要在中断里做耗时操作应该用工作队列或者tasklet把操作推迟到进程上下文执行。第三个是“设备树和传统的platform_device有什么区别”。这个问题考的是你对内核演进的理解。传统方式把硬件信息硬编码在板级文件中每换一个板子就要改内核代码。设备树把硬件信息独立出来内核只负责解析实现了驱动和硬件的解耦。现在主流的ARM平台都采用设备树方式。第四个是“你写的驱动怎么和安卓系统对接”。这个问题考的是你对安卓架构的理解。回答的思路是驱动通过设备节点暴露接口HAL层通过标准接口打开设备节点并封装成安卓能调用的形式Framework层的Service通过HAL层提供的接口调用驱动功能最终应用层通过Service提供的API使用硬件。6.3 项目扩展方向与进阶建议这个项目做完之后你还可以往几个方向扩展让简历更有竞争力。第一个方向是增加电源管理。嵌入式设备对功耗很敏感你可以研究一下Linux的电源管理框架在你的驱动里实现suspend和resume回调让设备在系统休眠时进入低功耗模式。这个技能在车载和移动设备领域非常吃香。第二个方向是增加并发处理。现在的驱动都是多线程访问的你需要考虑竞态条件。可以研究一下自旋锁、互斥锁、原子操作的使用场景在你的驱动里加入适当的保护机制。面试的时候如果能讲清楚什么场景用什么锁会加分很多。第三个方向是增加调试接口。在/proc或者/sys下面创建调试节点输出驱动的内部状态信息。这个技能在实际工作中非常实用能大大提高调试效率。你可以实现一个/proc/mydevice/status节点显示设备打开次数、读写字节数、错误计数等信息。第四个方向是研究安卓的HIDL或者AIDL接口。如果你的目标岗位涉及较新的安卓系统那HIDL和AIDL是必须掌握的。你可以把现有的HAL模块改造成HIDL或者AIDL形式这样项目就更贴近实际生产环境了。我个人在实际操作中的体会是嵌入式驱动开发这个方向入门确实有一定门槛但一旦打通了从硬件到应用的全链路后面的路会越走越宽。关键是要动手做不要只看书。看十遍内核文档不如自己写一个驱动加载成功一次。踩过的坑越多面试的时候能讲的东西就越多。最后再分享一个小技巧每次调试成功之后把关键步骤和踩坑记录整理成文档面试前翻一遍比临时抱佛脚管用得多。
觉得有用,分享给同行:

为您的企业打造数字门面

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

立即咨询 →