资讯动态

RK3588平台Android 12字符设备驱动开发实战与SELinux权限处理

发布时间:2026/10/24 4:41:37 来源:尧图企业网站定制
最近在做一版基于RK3588的工控方案系统选定Android 12外设是一个非标准的并行接口模块走不了串口也靠不上现有驱动框架只能在内核态写一个字符设备驱动把硬件接口包起来再向上层APP暴露统一读、写和IO控制能力。前后花了大半个月把整条链路调通交叉编译环境、字符设备框架、用户态测试程序、还有Android独有的SELinux权限。这里面的坑比单纯写Linux驱动多不少值得单独整理一篇。这篇东西主要写给两类人一类是有Linux驱动基础、但第一次接触Android系统集成的人另一类是系统工程师被设备节点权限和SELinux折腾到头秃的人。我会从拿到SDK开始讲到驱动核心代码再到用户态验证最后把权限和签名这些Android特有的问题单独拎出来说。不要指望一篇看完就能成驱动大师但至少让你少走我走过的弯路。1. 方案设计为什么在RK3588/Android 12上做字符设备1.1 世界不只有GPIO和串口你需要一个字符设备做RK3588方案的朋友都知道这芯片接口多、性能猛官方SDK里已经自带了一大堆驱动GMAC、PCIe、USB3.0、MIPI CSI/DSI、I2C/SPI/UART等等。那为什么还需要自己写字符设备驱动我遇到的情况很典型一个非标工业模块25根信号线的并行接口数据宽度16bit外加CLK和几根控制线。它既不是标准总线设备也不符合现有子系统的设备模型。用户态直接操作GPIO也不是不行但时序抖动太厉害中断响应不及时寄存器读写没法保证原子性。这类非标准并行外设就是字符设备驱动的最佳使用场景。另外还有一种常见需求产品预研阶段硬件方案还没定型需要在Android系统上快速验证某个Sensor、某个执行器、或者一组自定义协议的外设。这时候写一个通用的字符设备驱动把硬件寄存器映射和时序控制放在内核态用户态只负责业务逻辑是最稳妥也最灵活的做法。1.2 在Android上做驱动和纯Linux环境有三个本质区别同样的字符设备驱动在Ubuntu上能跑搬到Android上就一堆问题原因有三点这点想不清楚后面会非常痛苦。第一个区别是编译链全局绑定。Android SDK的内核必须用SDK自带的编译工具链、编译器版本、内核配置。随便拿一个桌面Linux的内核源码树用环境里的gcc编译出来的.ko多半在你的RK3588板子上加载失败——version magic和你板子上跑的kernel不匹配insmod直接报“invalid module format”。第二个区别是SELinux强制访问控制。Android 12默认Enforcing模式即使你的驱动代码没问题、设备节点存在、open也成功了应用层调用read/write依然可能被拒绝logcat里看到一大片avc denied。这不是驱动代码的事是安全策略没放行。第三个区别是镜像签名和分区结构。如果只做内核模块方案Android 12下并非不能加载.ko但要确保内核开启了module支持且关闭了签名校验或者你把.ko打进vendor_boot等分区。很多量产方案甚至直接把驱动编进内核不走insmod这就是为什么你需要掌握两种编译方式的切换。1.3 大框架选型传统cdev还是misc设备我的建议字符设备驱动的实现手法有两类一类是传统的cdev接口自己去alloc_chrdev_region、cdev_init、cdev_add还要处理device_create创建设备节点另一类是miscdevice它本质上是主设备号10的字符设备注册简单一个struct miscdevice就能搞定。很多人问该用哪个。我的建议是正式产品用cdev快速验证用misc。cdev虽然代码多几行但你能完全掌控设备号分配、动态创建设备节点、以及将来的多实例扩展。miscdevice适合Demo注册代码极少但主设备号固定为10次设备号只有255个可用而且设备节点默认权限受misc类约束模板痕迹重。在RK3588 Android 12这种量产系统上我建议按标准cdev来写并且把设备节点appops这类用户态操作都考虑进去后面接入系统服务会更顺。2. 环境准备先把编译链路跑通2.1 梳理SDK目录结构和内核部分拿到RK3588 Android 12 SDK后别急着写代码先把目录结构认全。一般你会看到kernel-5.10或kernel-4.19看SDK版本device/rockchip/rk3588external/里面往往放着内核模块需求的外设库prebuilts/gcc/linux-x86/aarch64/交叉编译工具链out/编译产物输出内核部分主要关注kernel-5.10这个目录。RK3588的Android 12 SDK常见内核版本是Linux 4.19新一点的会切到5.10。两个版本在驱动接口上略有差异比如设备树API和GPIO框架但字符设备的核心API是稳定不变的你可以放心学。编译前先确认一件事你的SDK是不是完整自带工具链。如果是从某厂商同步来的全量SDK通常prebuilts目录下会自包含gcc交叉工具链不需要额外安装。如果没有那你就要用Android源码树里的clang或自行准备aarch64交叉编译工具链但强烈推荐用SDK自带那一套避免工具链版本不一致导致的头文件问题。2.2 内核编译的第一步用SDK脚本走通默认流程先不要碰自己的驱动把官方SDK默认的内核编一遍确认整个编译链路是正常的。这一遍的意义不是浪费时间而是给你的后续排障建立一个基准。我这边是用如下方式处理的cd kernel-5.10 export ARCHarm64 export CROSS_COMPILE$(pwd)/../prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu- make rockchip_defconfig make -j12如果你的SDK用的是clang混合编译可能还需要类似这样的变量export CCclang export LDld.lld这一步能编过说明交叉工具链和配置没问题。编完后你会得到kernel.img、resource.img这些产物。不同SDK版本的输出路径可能不一样我见过放在kernel-5.10/out/的也见过和boot.img一起打包到rockdev目录的。多花十分钟找一下产物后面做模块验证时要用到。2.3 给驱动单独建一个子目录别塞进杂乱路径我不建议把驱动源码直接顺手丢进drivers/char/的某一处然后改Kconfig除非你打算长期维护并且产品线会复用。做单个外设适配时更好的是在kernel/drivers/下建一个你自己的目录比如drivers/mydemo/里面放KconfigMakefilerk3588_char_demo.c对应的设备树节点说明文档这样一个目录就是一个独立功能包将来复制到别的平台也方便。Kconfig写成这样config RK3588_CHAR_DEMO tristate RK3588 char device demo driver depends on ARM64 default m help A simple character device driver demo for RK3588.Makefile则非常简单obj-$(CONFIG_RK3588_CHAR_DEMO) rk3588_char_demo.o编内核的时候这个模块默认会编成.ko。如果你以后想直接编进内核可以把default改为y。我强烈建议调试期用模块方式改代码后只需要重新编.ko然后push到板子不需要反复整包烧录迭代效率高很多。2.4 模块交叉编译的坑不依赖kernel源码树绝对编不出来新手最容易犯的错装了交叉工具链自己写个Makefile把内核源码include进来就直接编译。然后遇到一堆type redefinition、implicit declaration人直接懵了。内核模块编译必须依赖一个配置完整且stale标记正确的内核源码树因为驱动的头文件很多是编译时生成的比如autoconf.h、utsrelease.h。正常操作是进入内核源码目录先编一次内核然后再用M编译外部模块cd kernel-5.10 make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- Mdrivers/mydemo modules编出来的rk3588_char_demo.ko是给你板子用的。注意这里的交叉前缀取决于你的SDK工具链建议先用file命令验证一下产物file drivers/mydemo/rk3588_char_demo.ko确认输出是ARM aarch64架构再往板子上推。3. 驱动核心实现从设备号到file_operations3.1 file_operations驱动的“函数表”字符设备驱动说穿了就是一套回调函数用户态进程调用open、read、write、ioctl时通过设备文件fd转入内核态内核VFS层最终调用你注册的这些回调。struct file_operations就是给内核的“函数表”。我习惯先定好这个表再写业务逻辑static const struct file_operations rk3588_demo_fops { .owner THIS_MODULE, .open rk3588_demo_open, .read rk3588_demo_read, .write rk3588_demo_write, .unlocked_ioctl rk3588_demo_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl rk3588_demo_ioctl, #endif .release rk3588_demo_release, };这里有个很多人不知道的细节Android用户态既有64位也有32位程序如果上层App以32位so库去调用ioctl内核会走compat_ioctl而不是unlocked_ioctl。如果你的App架构不确定最好直接把compat_ioctl也指向同一个函数否则32位进程调用ioctl会返回ENOTTY。3.2 设备号、cdev注册和设备类传统字符设备的注册过程标准且固定。我的封装逻辑是这样的#define DEVICE_NAME rk3588_demo #define CLASS_NAME rk3588_demo_cls static int rk3588_demo_major 0; static struct class *rk3588_demo_class; static struct cdev rk3588_demo_cdev; static int __init rk3588_demo_init(void) { dev_t devno; int ret; ret alloc_chrdev_region(devno, 0, 1, DEVICE_NAME); if (ret) return ret; rk3588_demo_major MAJOR(devno); cdev_init(rk3588_demo_cdev, rk3588_demo_fops); rk3588_demo_cdev.owner THIS_MODULE; ret cdev_add(rk3588_demo_cdev, devno, 1); if (ret) goto err_cdev_add; rk3588_demo_class class_create(THIS_MODULE, CLASS_NAME); if (IS_ERR(rk3588_demo_class)) { ret PTR_ERR(rk3588_demo_class); goto err_class_create; } device_create(rk3588_demo_class, NULL, devno, NULL, DEVICE_NAME); pr_info(rk3588_demo registered, major%d\n, rk3588_demo_major); return 0; err_class_create: cdev_del(rk3588_demo_cdev); err_cdev_add: unregister_chrdev_region(devno, 1); return ret; }这里做的是动态主设备号分配具体设备号以/dev/rk3588_demo生成为准。如果板子上已经有同名节点冲突你可以先ls /dev看一下或者改成固定主设备号但我强烈建议动态分配省心。3.3 ioctl命令号怎么定义才不容易冲突用户态和驱动之间的交互read/write传数据固然简单但要实现读寄存器、设模式、使能中断这类控制操作还是得用ioctl。命令号设计要用心不建议随便写个整数当cmd。Linux内核社区约定用_IO/_IOR/_IOW宏来组合命令号格式是type一个ASCII字符尽量选跟你设备相关的比如示例里的‘D’nr命令序号size参数大小#define RK3588_DEMO_GET_REG _IOR(D, 1, unsigned int) #define RK3588_DEMO_SET_REG _IOW(D, 2, unsigned int) #define RK3588_DEMO_ENABLE _IO(D, 3) #define RK3588_DEMO_DISABLE _IO(D, 4)这样做的最大好处是命令号自带参数长度信息内核在copy_from_user之前会尽量校验同时不同驱动之间撞命令号的概率也小得多。有人偷懒直接#define CMD_READ 0x100短期能用一旦系统里另一个驱动用了同样的nr你的ioctl会把不相干的请求也接下来那时候你查bug查到怀疑人生。3.4 硬件访问实例用ioremap映射寄存器区域如果你要操作的硬件不是标准外设大概率需要直接访问物理寄存器。内核里不能直接解引用物理地址需要用ioremap把物理地址映射到内核虚拟地址空间。RK3588上常见做法是拿到外设的寄存器物理基址然后在内核驱动里ioremap。我常用的模式#define REG_BASE_PHYS 0xFE010000 // 某个外设物理基址替换为你的实际地址 #define REG_MAP_SIZE 0x1000 #define REG_CTRL 0x00 #define REG_STATUS 0x04 #define REG_DATA 0x08 static void __iomem *reg_base; static int rk3588_demo_probe_io(void) { reg_base ioremap(REG_BASE_PHYS, REG_MAP_SIZE); if (!reg_base) return -ENOMEM; return 0; } static void rk3588_demo_cleanup_io(void) { if (reg_base) iounmap(reg_base); }操作寄存器时用readl/writelstatic unsigned int rk3588_demo_get_reg(void) { return readl(reg_base REG_DATA); } static void rk3588_demo_set_ctrl(int en) { if (en) writel(0x1, reg_base REG_CTRL); else writel(0x0, reg_base REG_CTRL); }这里要特别注意物理地址别写错写错地址导致访问不存在的内存区域板子轻则卡死重则挂总线。建议先对照原理图和芯片数据手册拿到准确基址再在设备树里定义描述用platform_driver的方式去匹配资源不要手写魔法数。但作为快速验证demo手写实际地址可以用我仍是建议你把设备信息的获取改成设备树方式这样硬件变更时不用重编驱动。3.5 并发与睡眠一个容易被忽略的质量问题open之后会有多个应用同时read/write同一个节点这时你的驱动里如果有静态变量就会出现竞态。解决手段无外乎三种互斥量、自旋锁、原子变量。我做的这个demo很简单用一个mutex保护整个驱动实例即可static DEFINE_MUTEX(rk3588_demo_lock); static ssize_t rk3588_demo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) { unsigned int val; if (mutex_lock_interruptible(rk3588_demo_lock)) return -ERESTARTSYS; val rk3588_demo_get_reg(); if (copy_to_user(buf, val, min(count, sizeof(val)))) { mutex_unlock(rk3588_demo_lock); return -EFAULT; } mutex_unlock(rk3588_demo_lock); return sizeof(val); }如果驱动里要等待中断或等待硬件状态位记住一定不要用忙等也不要在持有自旋锁的时候调用可能睡眠的函数。我在早期开发中踩过spinlock下调用msleep导致系统hung住的坑当时连watchdog都救不回来只能拔电。所以睡眠类互斥用mutex短临界区才用spinlock。4. 用户态测试程序验证驱动行为是否正常4.1 用Android NDK工具链编译测试程序驱动编译好了之后要在板子上验证。这需要交叉编译一个用户态测试小程序。这里强调一下用Android NDK自带的工具链来编不要用aarch64-linux-gnu-gcc编完再拷进Android动态链接库的不兼容会让你头大。建议在SDK源码里的external或者单独一个目录里放一份测试代码然后直接用NDK的clang来编译。export NDK_PATH/path/to/ndk ${NDK_PATH}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang \ -o rk3588_demo_test rk3588_demo_test.c然后adb push到/data/local/tmp/执行。注意Android 12上/data/local/tmp默认权限一般是能执行的如果不行chmod 755。4.2 测试程序代码open、ioctl、read一条龙用户态测试别搞太复杂把读写路径都覆盖到即可。我的测试代码如下#include stdio.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include stdlib.h #include stdint.h #define DEV_PATH /dev/rk3588_demo #define RK3588_DEMO_GET_REG _IOR(D, 1, unsigned int) #define RK3588_DEMO_SET_REG _IOW(D, 2, unsigned int) #define RK3588_DEMO_ENABLE _IO(D, 3) int main(void) { int fd; unsigned int val; fd open(DEV_PATH, O_RDWR); if (fd 0) { perror(open); return 1; } ioctl(fd, RK3588_DEMO_ENABLE); ioctl(fd, RK3588_DEMO_SET_REG, 0x1234); ioctl(fd, RK3588_DEMO_GET_REG, val); printf(get reg 0x%x\n, val); close(fd); return 0; }编译完推上去后执行如果一切正常你会看到dmesg里有注册信息程序打印出寄存器读回值。如果读回的是0x1234说明驱动回归验证通过如果读回随机值就要检查硬件地址、读写时序和寄存器配置了。4.3 设备节点不存在自动生成和手动mknod你会在有些不带Init脚本的板子上发现/dev/rk3588_demo根本不存在。虽然驱动里device_create会在正常情况下创建它但如果你的系统跑的是最小化init可能需要手动创建设备节点。最简单的方法是临时用mknod建立节点mknod /dev/rk3588_demo c 240 0这里的240是alloc_chrdev_region动态分配出来的主设备号值以你板子上dmesg的打印为准。不过每次都手动mknod太原始了正规做法是把设备节点信息写进ueventd或者init.rc。Android系统偏好用SELinux来管理设备节点的上下文这个后面会展开讲。5. Android 12特有的系统接入SELinux、节点权限和签名5.1 Enforcing模式下的一切Permission deniedRK3588 Android 12默认的SELinux是enforcing。你的驱动即使内核态完全正常用户态进程访问设备节点时报权限错误十有八九是SELinux拦截了这在logcat里会看到类似的日志avc: denied { open } for pid1234 commrk3588_demo_test namerk3588_demo devtmpfs scontextu:r:shell:s0 tcontextu:object_r:device:s0 tclasschr_file permissive0处理思路分三步。第一步查看target contexts配置。Android 12中设备节点要有正确SELinux标签这通常在external/sepolicy或system/sepolicy目录下file_contexts里定义。你需要添加类似/dev/rk3588_demo u:object_r:rk3588_demo_device:s0第二步你需要定义这个新的设备类型以及访问权限规则。在sepolicy目录下新建一个te文件比如rk3588_demo_device.te内容大致如下type rk3588_demo_device, dev_type; allow shell rk3588_demo_device:chr_file rw_file_perms; allow untrusted_app rk3588_demo_device:chr_file open read write ioctl;这里注意如果只是shell测试好办但如果系统APP要访问你得给对应domain加allow。而且要开启ioctl权限时必须给白名单不能用ioctl权限的allowall否则会被checkreqprot和ioctl bypass机制拦得体无完肤。第三步动态加载SELinux策略到板子上验证。快速验证方式是把板子临时切到permissive模式看业务是否通然后切回enforcing再调整策略。切permissive的命令adb root adb shell setenforce 0生产环境不建议permissive但调试阶段它能快速缩小问题范围。每次改完sepolicy之后用make sepolicy重新生成然后整体编boot.img/vendor.img烧入或者在你熟悉的SDK编译流程里导出policy文件再用adb push替换临时生效。5.2 设备节点权限不是随便chmod就能长期解决的除了SELinux还有一套常规的Unix文件权限也就是设备节点在/dev下面的mode和owner。Android里ueventd会根据ueventd.rc中的配置来设置设备节点权限和属主。在RK3588 SDK里设备权限默认由ueventd的规则决定。如果你写的是自定义设备进系统后设备节点的权限可能默认是root拥有、600权限普通进程无法打开。需要一个途径去修改权限快速验证adb shell下不生效之后直接在init.rc里临时chmod /dev/rk3588_demo 666。正式方案在ueventd.rc里给该设备加一条规则/dev/rk3588_demo 0666 root root当然结合SELinux来看光给权限标签还不行你的文件和你的策略要一致否则不同方式创建的设备节点会带上错上下文各种奇奇怪怪问题会冒出来。这几个文件必须协同修改建议整理成一张对照表放进你项目的自检清单里。5.3 内核模块的加载策略insmod还是编进内核这里我把话说重点量产产品尽量避免依赖外置可加载内核模块。原因有两个一个是执行性Android 12内核模块机制比较复杂如果内核开启了CONFIG_MODULE_SIG你insmod任意未签名模块会直接被拒必须在内核里加签名或者强制关闭模块签名选项而这会降低整体的安全属性。另一个是稳定性外置模块一旦丢失或者更新后与内核版本不匹配可能引发启动后功能缺失甚至panic。我更建议调试阶段用insmod/LKM方式快速验证到产品固化阶段把驱动直接编译进内核也就是把Kconfig里的默认值改成内置y重新生成boot.img确保驱动随内核一起启动不依赖根文件系统里的ko文件。调试阶段如果坚持用modprobe先把模块push进/vendor/lib/modules或者/system/lib/modules视SDK而定然后depmod更新依赖关系这些步骤也能做但优先级不高。5.4 Android 12的AVB与dm-verity对驱动开发的影响很多第一次做Android驱动的人会遇到一个怪现象第一次烧录后能加载.ko但下次改动后加载新的.ko就失败。这往往不是权限问题是dm-verity或AVB校验导致分区只读导致你push到/vendor的模块文件实际没有被真正替换。遇到这种问题先看挂载状态adb shell mount | grep vendor如果是ro可以尝试remount或直接绕过校验adb root adb remount如果remount后依然提示read-only大概率是分区是dynamic partition或启用了verify需要在构建时禁用AVB或者把你编译的内核模块纳入正式镜像打包。我在开发板阶段的经验是直接烧userdebug版本的镜像通常默认允许adb remount如果厂商的release版那就要走全套重打镜像流程。动手前第一件事永远先看当前镜像类型。userdebug和user版本对驱动开发和权限调试的影响是天壤之别。userdebug能救你一命user版会让你抓狂。6. 常见问题与排查实录我踩过的坑按优先级排序6.1 insmod失败version magic和符号版本我调试中遇到最多的问题是insmod报错主要分两类。第一类是rk3588_demo.ko: version magic 5.10.110-g9d3a4c5 should be 5.10.110这显然是编译环境和你板子上内核版本不一致。检查内核源码树的本地版本号配置确认你编内核、编模块用的是同一个CONFIG_LOCALVERSION。为了避免这种低级问题模块必须在你实际编译boot.img的内核源码树下构建不要省时间直接复用别人编好的内核目录。第二类是Unknown symbol xxx原因通常是模块里用到了一个未导出的内核API或者该API被GPL限制而你模块没标注GPL。我出过一次这种问题用了某个厂商私有头文件里的函数且那个函数没有EXPORT_SYMBOL链接阶段过了但运行时根本找不到。解决办法换API或把逻辑写进主内核不要依赖私有导出。6.2 一读寄存器就死机多半是地址映射问题内核态访问硬件寄存器引发死机这是最绝望的场景之一。有时候日志都来不及打印整板直接reset或者挂死。我排查顺序是先确认物理地址在芯片手册上存在且独占没有和别的外设地址重叠。检查ioremap返回的虚拟地址非空才继续用不然说明你的映射长度可能已经超出了页面范围。检查寄存器访问宽度。chip手册要求32位访问你非要用readb就是埋雷。确认时钟和电源域。RK3588很多外设需要开启对应的时钟和regulator才能访问寄存器内核在sysfs里会把未使能时钟的外设地址访问变成总线hang。这类问题用别家驱动的代码时特别常见因为人家probe里偷偷开了clock而你根本没看。建议一上来用devmem先把寄存器读一遍确认外设本身工作正常再谈驱动问题。6.3 ioctl数据交互的陷阱copy_to_user/copy_from_user不可乱用用户态和内核态传参是个容易出错的地方。不要直接接引用用户态指针也不要信任用户态传进来的指针。标准写法是copy_from_user和copy_to_user。很多人自己写的时候图省事直接*arg data编译能过一跑就扇Page Fault这类问题随机性强、难排查建议直接把正确写法焊死在代码模板里。另外要注意ioctl的参数在include/uapi/asm-generic/ioctl.h里对类型是有要求的不能传一个64位值在32位用户程序里拆包否则你定义的命令号会错位。如果你处理的参数比较大建议用指针传结构体。struct rk3588_demo_ioctl_arg { unsigned int reg_offset; unsigned int value; }; static long rk3588_demo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct rk3588_demo_ioctl_arg karg; struct rk3588_demo_ioctl_arg __user *uarg (void __user *)arg; switch (cmd) { case RK3588_DEMO_SET_REG: if (copy_from_user(karg, uarg, sizeof(karg))) return -EFAULT; writel(karg.value, reg_base karg.reg_offset); break; case RK3588_DEMO_GET_REG: karg.value readl(reg_base karg.reg_offset); if (copy_to_user(uarg, karg, sizeof(karg))) return -EFAULT; break; default: return -ENOTTY; } return 0; }6.4 中断上下文的东西别在process context硬等做工业外设的驱动频繁涉及中断和等待。最大坑是中断处理里用wake_up_interruptible唤醒等待队列业务线程里用wait_event_interruptible等事件。这本身没问题但如果你的硬件状态寄存器读方式不对永远等不来中断业务线程就卡死在wait_event里进程不可中断还会拖累系统负载。我调试时碰到过一次中断没触发status寄存器却已经置位事件被丢失wait_event永远睡下去。后面硬着头皮加了超时保护才定位到中断模式触发条件配置有问题。我的建议是所有wait_event类代码都带上超时版本比如wait_event_interruptible_timeout并且返回后检查是event满足还是timeout别把这个设计省略。对外设驱动而言超时兜底有时候比正确路径代码还重要。7. 实操备忘一些让项目进度更稳的小经验7.1 用dmesg分级打日志别用printk裸奔驱动调试期间printk是命根子但一出问题全是printk输出的话日志会爆炸。我习惯把驱动里日志按等级划分初始化用pr_info、寄存器读写用pr_debug、错误用pr_err然后通过动态调试开关控制pr_debug输出。Android系统下查看内核日志adb shell dmesg | grep -i rk3588_demo注意Android 12的logd会吞掉一部分dmesg如果你发现日志不全优先用串口看。RK3588的UART调试口在调试驱动时非常好用尤其内核panic时串口日志才是第一手资料。7.2 把板子做成可复现状态区分修改点和产物这次项目让我最大的感受是RK3588 Android 12的驱动调试链路太长从内核、模块到SELinux策略每一步都可能出错。所以一定要养成做清单的习惯我每调一个外设驱动都会维护下面三张表第一张是编译产物表记录每次编译所用的SDK目录、内核版本、.ko路径、boot.img路径、烧录方式。第二张是权限策略表记录设备节点路径、SELinux标签、te文件修改点、ueventd权限值、涉及的policy文件。第三张是硬件寄存器表每个寄存器功能、物理地址、偏移、读写属性、初始值。有这三张表出任何问题都能快速定位到改动点避免反复烧板浪费时间。7.3 上线前一定做的三项验证最后说说验证。驱动能在自己手里跑起来不算完要上线到真实业务环境前我会强制自己过三关第一关是并发压测。多开几个进程同时访问设备节点持续读写和ioctl观察有没有死锁、数据错乱、内核警告。第二关是异常路径验证。拔掉外设电源、手动触发硬件故障看驱动能不能正常返回错误码而不是panic或hang死。这一关很多人跳过但恰恰是最能暴露驱动鲁棒性不足的。第三关是长时间稳定性测试。连续跑72小时看内存有没有泄漏、中断有没有丢失、等待队列有没有卡死。字符设备驱动在产品上常见的问题都是长时间运行后才暴露的短期功能通过并不代表可以交付。我在这次RK3588的驱动开发里最大的体会是写字符设备驱动本身不难难的是让它在Android这个复杂系统里稳定、合规、可靠地工作。如果你也是第一次在这种平台上做类似工作建议从最简单的实例开始把insmod到SELinux整条链路先走通再去碰真实硬件。先跑通一个假设备再换真外设这个成本付出非常值得。顺带一提如果你手头有多个外设都要做类似驱动建议把公共部分抽成模板比如字符设备注册、mutex管理、ioctl通用解析框架一套模板打天下。我在后续的两个项目里都直接复用这套框架开发效率提升非常明显。等你积累自己的驱动模板库以后这类工作的周期可以从半个月压缩到两三天。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑