免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Android AOSP集成i2c-tools:嵌入式I2C调试工具编译与SELinux配置实战

Android AOSP集成i2c-tools:嵌入式I2C调试工具编译与SELinux配置实战 1. 项目缘起为什么要在Android源码里折腾i2c-tools搞Android底层开发或者硬件驱动的朋友对I2C总线肯定不陌生。这玩意儿在嵌入式系统里就像设备之间的“悄悄话”通道传感器、触摸屏、EEPROM这些外设很多都靠它和主控芯片比如我们的SoC通信。调试I2C设备时最头疼的就是“它到底在不在线上”、“地址对不对”、“读写数据对不对”。这时候Linux社区的老兵——i2c-tools套件就成了我们手里的“听诊器”和“万用表”。i2c-tools包含几个核心命令i2cdetect扫描总线上的设备i2cget读一个寄存器i2cset写一个寄存器i2ctransfer进行复杂的组合读写。在标准的Linux发行版里apt-get install i2c-tools一下就装好了。但在Android环境下事情就复杂了。我们面对的是一个高度定制化的、为移动设备优化的Linux内核和文件系统。你手上的Android设备无论是手机还是开发板其出厂系统镜像里大概率不会包含这些调试工具。每次调试都要先刷一个带busybox或者自己交叉编译好的二进制文件推上去麻烦不说权限还可能受限。所以一个更“原生”、更彻底的做法出现了直接把i2c-tools的源码集成到AOSPAndroid Open Source Project的编译体系里让它成为系统镜像的一部分。这样编译出来的i2c-tools可执行文件可以直接在设备的/system/bin或/vendor/bin下找到拥有合适的权限与系统内核版本完美匹配用起来那叫一个顺手。这不仅仅是方便调试对于需要深度定制硬件驱动的产品开发阶段它更是不可或缺的验证和测试手段。今天我就结合多次在真实项目从手机到各种IoT设备中集成的经验手把手带你走通这条路并分享那些官方文档不会告诉你的“坑”和技巧。2. 环境准备与源码获取找到正确的“零件”在开始动手修改AOSP这座“大厦”之前我们得先准备好正确的“建筑材料”。这里的关键是获取与你的Android系统版本和内核匹配的i2c-tools源码。2.1 确定AOSP版本与内核首先你需要知道你正在编译的AOSP版本比如Android 10/Q 11/R 12/S等。不同版本的AOSP在external目录的组织结构、编译工具链尤其是Soong/Bp替代Android.mk的进度上可能有细微差别。你可以通过查看build/make/core/version_defaults.mk或执行repo manifest来确认。更重要的是内核版本。i2c-tools与Linux内核的I2C子系统紧密相关。虽然高版本的i2c-tools通常向下兼容但使用与你的内核版本相近的源码能最大程度避免潜在的API不匹配问题。你可以通过设备上的/proc/version或者内核源码目录下的Makefile查看内核版本如4.19,5.4,5.10等。2.2 获取i2c-tools源码官方源码托管在kernel.org的git仓库https://git.kernel.org/pub/scm/utils/i2c-tools/i2c-tools.git。我强烈建议你根据内核版本选择一个稳定的tag进行下载而不是直接用master分支。例如对于内核4.19/5.4可以选择v4.3标签对于更新的内核可以选择v4.4或更高。# 克隆仓库 git clone https://git.kernel.org/pub/scm/utils/i2c-tools/i2c-tools.git # 进入目录并查看标签 cd i2c-tools git tag -l | grep ^v4 # 切换到特定标签例如v4.3 git checkout v4.3为什么强调用tag因为master分支可能包含一些依赖更新内核头文件的新特性在较旧的内核上编译可能会报错。使用对应时代的tag能省去很多不必要的麻烦。2.3 规划AOSP集成目录AOSP的external目录专门用于存放这类上游的开源项目。我们将在external下创建一个新的目录来存放我们的i2c-tools。通常我会直接命名为i2c-tools保持清晰。# 假设你的AOSP根目录是 /aosp cd /aosp/external mkdir i2c-tools接下来我们把刚才下载的i2c-tools源码不包括.git目录复制到这个新目录中。cp -r /path/to/your/downloaded/i2c-tools/* /aosp/external/i2c-tools/现在/aosp/external/i2c-tools目录下应该会有tools/包含i2cdetect,i2cget等工具的源码、lib/i2c库、include/头文件以及Makefile等文件。原生的Makefile是针对桌面Linux的我们不能直接用需要为AOSP的编译系统编写新的蓝图。3. 编写编译蓝图Android.bp文件详解AOSP的编译系统已经从传统的Android.mk逐渐转向基于Go语言的Soong构建系统其配置文件是Android.bp。我们的目标是为i2c-tools中的每个可执行工具i2cdetect,i2cget,i2cset,i2ctransfer以及它们依赖的库编写正确的Android.bp文件。3.1 分析源码结构首先我们看看tools/目录下的源码。每个工具基本上都是一个独立的C文件例如i2cdetect.c它们都链接了libi2c.so这个库源码在lib/目录下。因此我们的编译步骤是将lib/下的源码编译成一个共享库libi2c.so。将每个工具源码编译成可执行文件并链接libi2c.so。3.2 编写libi2c的Android.bp在/aosp/external/i2c-tools根目录下创建Android.bp文件。// 首先编译静态库 libi2c供后续工具链接 cc_library_static { name: libi2c_static, vendor_available: true, // 允许vendor分区使用 recovery_available: true, // 允许recovery模式使用 srcs: [ lib/smbus.c, lib/i2c-dev.c, // 注意通常我们只需要这两个文件。 // 原lib/下的Makefile可能会编译更多文件但核心功能在这两个里。 ], cflags: [ -Wall, -Werror, -DANDROID, // 定义Android宏有时源码需要这个来适配 -I$(LOCAL_PATH)/include, // 包含头文件路径 ], export_include_dirs: [include], // 对外暴露头文件路径 target: { android: { shared_libs: [liblog], // 在Android上可能需要链接liblog }, }, } // 然后编译共享库 libi2c.so有些场景可能需要动态链接 cc_library_shared { name: libi2c, vendor_available: true, recovery_available: true, static_libs: [libi2c_static], // 直接复用上面的静态库内容 export_static_lib_headers: [libi2c_static], target: { android: { shared_libs: [liblog], }, }, }这里有个关键点i2c-tools的库源码通常不依赖Android特有的东西但为了更好的兼容性我们链接了liblog。vendor_available: true这个属性至关重要它意味着这个库可以被vendor分区设备制造商定制软件所在的分区的模块使用。因为I2C设备驱动通常都在vendor分区调试工具也需要能访问这些驱动。3.3 编写各个工具的Android.bp接下来我们为每个工具编写编译规则。由于每个工具都很类似我们可以写一个通用的模板或者分别编写。这里为了清晰我们分别编写并放在同一个Android.bp文件里。// 编译 i2cdetect cc_binary { name: i2cdetect, vendor: true, // 标记为vendor模块会编译到vendor分区 init_rc: [i2cdetect.rc], // 可选如果需要开机自启动或设置权限可以配.rc文件 srcs: [tools/i2cdetect.c], static_libs: [libi2c_static], // 静态链接libi2c避免运行时依赖库找不到 cflags: [ -Wall, -Werror, -DANDROID, -I$(LOCAL_PATH)/include, ], target: { android: { shared_libs: [liblog], }, }, } // 编译 i2cget cc_binary { name: i2cget, vendor: true, srcs: [tools/i2cget.c], static_libs: [libi2c_static], cflags: [ -Wall, -Werror, -DANDROID, -I$(LOCAL_PATH)/include, ], target: { android: { shared_libs: [liblog], }, }, } // 编译 i2cset cc_binary { name: i2cset, vendor: true, srcs: [tools/i2cset.c], static_libs: [libi2c_static], cflags: [ -Wall, -Werror, -DANDROID, -I$(LOCAL_PATH)/include, ], target: { android: { shared_libs: [liblog], }, }, } // 编译 i2ctransfer cc_binary { name: i2ctransfer, vendor: true, srcs: [tools/i2ctransfer.c], static_libs: [libi2c_static], cflags: [ -Wall, -Werror, -DANDROID, -I$(LOCAL_PATH)/include, ], target: { android: { shared_libs: [liblog], }, }, }重要决策静态链接 vs 动态链接我在这里选择了static_libs: [libi2c_static]即静态链接。这是我在实际项目中的强烈建议。为什么部署简单生成的可执行文件是独立的推送到设备就能运行不需要考虑目标设备上是否存在/vendor/lib/libi2c.so。避免依赖冲突如果系统或其他vendor模块也提供了libi2c.so可能会引起版本冲突或符号冲突。静态链接彻底避免了这个问题。体积可控每个工具大小增加有限对于调试工具来说完全可以接受。当然如果你希望多个工具共享同一个动态库以节省系统空间可以使用shared_libs: [libi2c]但你必须确保libi2c.so被正确打包到系统镜像vendor.img的对应目录。3.4 处理头文件与可能的编译错误把源码复制过来直接编译大概率会报错。常见的错误包括找不到linux/i2c-dev.h等头文件这是因为AOSP的Bionic C库可能没有完整的内核头文件。i2c-tools源码包里的include/linux/i2c-dev.h和include/linux/i2c.h就是用来解决这个问题的。我们之前通过-I$(LOCAL_PATH)/include已经包含了这个路径。确保这些头文件内容完整必要时可以从你设备的内核头文件kernel/msm-xxx/include/uapi/linux/中复制一份更新。某些函数或宏未定义例如I2C_SLAVE_FORCE等。这些宏定义在内核头文件中。你需要检查include/linux/i2c-dev.h的版本是否足够新。一个实用的方法是直接从你正在编译的内核源码树中的include/uapi/linux/i2c-dev.h复制过来替换。这是保证兼容性最稳妥的方法。smbus.h路径问题源码中可能包含#include smbus.h这个文件在lib/目录下。我们需要在cflags中增加-I$(LOCAL_PATH)/lib。修改后的cflags应该类似这样cflags: [ -Wall, -Werror, -DANDROID, -I$(LOCAL_PATH)/include, -I$(LOCAL_PATH)/lib, // 添加lib目录以找到smbus.h ],4. 集成编译与镜像打包让工具“长”到系统里编写好Android.bp只是第一步接下来要让AOSP的编译系统认识并编译它们最后把生成的可执行文件放到正确的系统镜像中。4.1 触发编译在AOSP根目录下使用编译命令来专门编译我们的模块或者编译整个系统。# 方法一单独编译i2cdetect模块推荐快速验证 source build/envsetup.sh lunch aosp_arm64-eng # 选择你的目标设备编译产品 make i2cdetect # 方法二编译整个系统这些工具会随着vendor镜像一起构建 make -j$(nproc)如果编译成功你会在输出目录找到生成的可执行文件例如out/target/product/device_name/vendor/bin/i2cdetect因为我们在Android.bp中声明了vendor: true所以它们会被安装到vendor分区下的bin目录。这是Android O8.0之后推荐的做法将设备特有的工具和驱动放在vendor分区。4.2 验证模块是否在镜像中编译完成后我们可以检查生成的vendor.img是否包含了我们的工具。# 解压vendor.img (假设是ext4格式) sudo mount -o loop out/target/product/device_name/vendor.img /mnt ls -la /mnt/bin/i2c* sudo umount /mnt # 或者使用Android提供的工具simg2img和debugfs如果vendor.img是sparse格式 simg2img out/target/product/device_name/vendor.img vendor_raw.img debugfs -R ls -l /bin vendor_raw.img | grep i2c4.3 处理SELinux权限问题关键步骤这是集成过程中最容易踩坑的地方。即使你的二进制文件被正确打包进了vendor.img在设备上运行时也可能因为SELinux权限问题而失败表现为Permission denied或者根本无法打开/dev/i2c-*设备节点。你需要为这些工具定义SELinux策略。这通常涉及修改设备树的SELinux策略文件。路径通常位于device/manufacturer/device_name/sepolicy/vendor/为可执行文件定义类型在file.te或vendor_file.te中添加type i2ctools_exec, exec_type, vendor_file_type, file_type;为进程定义域在domain.te中添加type i2ctools, domain; type i2ctools_exec, exec_type, vendor_file_type, file_type; # 允许从i2ctools_exec文件进入i2ctools域 init_daemon_domain(i2ctools)允许域访问I2C设备这是核心。在i2ctools.te新建此文件中添加# 允许i2ctools域对i2c_device类型的chr_file进行读写、打开等操作 allow i2ctools i2c_device:chr_file { open read write ioctl getattr }; # 通常还需要一些基本权限 allow i2ctools self:capability { dac_override sys_rawio }; allow i2ctools vendor_toolbox_exec:file { execute execute_no_trans };i2c_device这个类型标签需要查看你的设备内核是如何给I2C设备节点打标签的可以通过ls -lZ /dev/i2c-*在已启动的设备上查看或者在内核的ueventd配置里找。有时标签可能是sysfs_i2c或别的需要根据实际情况调整。关联文件与域在file_contexts中添加/vendor/bin/i2cdetect u:object_r:i2ctools_exec:s0 /vendor/bin/i2cget u:object_r:i2ctools_exec:s0 /vendor/bin/i2cset u:object_r:i2ctools_exec:s0 /vendor/bin/i2ctransfer u:object_r:i2ctools_exec:s0修改完SELinux策略后必须重新编译bootimage或vendorimage因为策略文件被打包在vendor.img或boot.img中。然后刷机测试。注意SELinux策略调试非常繁琐。如果初期只想验证工具是否工作可以在userdebug或eng版本的设备上通过adb shell setenforce 0临时将SELinux设置为宽容模式Permissive。但这只是调试手段产品最终必须运行在强制模式Enforcing下并拥有正确的策略。5. 实战调试工具使用详解与排坑指南假设你已经成功将包含i2c-tools的系统镜像刷入设备并通过adb shell连接。让我们看看怎么用这些工具以及会遇到哪些典型问题。5.1 扫描I2C总线上的设备i2cdetect这是第一步用于探测总线上有哪些设备响应。# 查看系统中有哪些I2C总线适配器 adb shell ls /dev/i2c-* # 输出可能是 /dev/i2c-0, /dev/i2c-1 等 # 使用i2cdetect扫描某条总线例如i2c-1 adb shell i2cdetect -l # 列出所有适配器 adb shell i2cdetect -y 1 # 快速扫描总线1 (假设总线索引是1) # 使用 -y 选项可以避免交互式确认适合脚本调用。 # 输出是一个矩阵显示从0x03到0x77的地址。有设备响应的地址会显示为数字如UU表示被驱动占用--表示无响应。 # 例如看到 0x50 显示为 50表示有一个设备在地址0x50上。常见问题与排查/dev/i2c-*不存在说明内核I2C适配器驱动没有正确启用或注册。检查内核配置CONFIG_I2C_CHARDEV是否开启以及对应的I2C控制器驱动是否编译并加载dmesg | grep i2c。扫描不到任何设备但硬件确定连接上拉电阻I2C总线需要上拉电阻通常4.7kΩ。用示波器或逻辑分析仪检查SCL和SDA线是否有上拉波形是否干净。设备地址确认你理解的设备地址如0x50是7位地址。i2cdetect显示的是7位地址。有些设备手册给出的是8位读写地址如0xA0写0xA1读需要右移一位0xA0 1 0x50才是7位地址。供电与初始化设备是否已供电有些传感器需要特定的初始化序列通过GPIO拉高复位或使能脚后才能响应I2C。扫描结果显示UU表示这个地址已经被内核中的一个驱动程序“占用”了。这意味着该设备已经有内核驱动在管理你通常无法再通过i2c-tools直接访问除非卸载那个驱动。这其实是好事说明设备驱动工作正常。5.2 读写寄存器i2cget与i2cset找到设备地址后就可以读写其寄存器了。这要求你手上有该设备的 datasheet数据手册知道寄存器的地址、宽度8位/16位和含义。# 从总线1上的设备0x50读取8位寄存器0x00的值 adb shell i2cget -f -y 1 0x50 0x00 b # 参数解释 # -f: 强制访问即使设备被驱动占用也尝试访问慎用可能干扰正常驱动 # -y: 非交互模式 # 1: 总线编号 # 0x50: 设备7位地址 # 0x00: 要读取的寄存器地址 # b: 读取一个字节byte。也可以是 w16位字 iI2C块读取等。 # 向总线1上的设备0x50在寄存器0x01写入8位值0xAB adb shell i2cset -f -y 1 0x50 0x01 0xAB b # 同样b表示写入一个字节。可以写w16位 iI2C块写入等。实操心得寄存器地址模式有些设备使用8位寄存器地址有些使用16位。i2cget/i2cset的b和w参数不仅指数据宽度有时也影响发送的寄存器地址长度。例如对于16位寄存器地址的设备你可能需要用i2cget -y 1 0x50 0x1234 w来读取地址0x1234的16位数据。具体必须参考数据手册的“读写时序图”。-f标志的危险性如果设备已经被一个内核驱动管理i2cdetect显示UU使用-f强制访问可能会破坏驱动内部状态导致系统异常。仅在没有驱动管理或调试驱动本身时使用。验证写入写入后立即用i2cget读回来验证这是硬件调试的基本操作。5.3 复杂传输i2ctransfer对于需要连续读写多个寄存器或者使用复合格式如SMBus Block Read的设备i2ctransfer更强大。# 示例向设备0x50写入两个字节到寄存器0x10然后从同一地址开始读回4个字节 # 这模拟了一个“写寄存器地址后读数据”的典型操作 adb shell i2ctransfer -f -y 1 w20x50 0x10 0x00 r4 # 参数解释 # w20x50: 向地址0x50发起一个包含2个字节的写操作。字节是后面的0x10和0x00。 # r4: 紧接着发起一个读操作读取4个字节。 # 整个命令会先发送 0x10 0x00设置寄存器指针然后重新START再读取4字节数据。排坑记录我曾经调试一个陀螺仪它的某些配置寄存器是16位地址数据也是16位。使用i2cset直接写总是失败。后来发现是时序问题。该设备要求写操作时先发高8位寄存器地址再发低8位地址然后才是数据的高8位和低8位。使用i2ctransfer可以精确控制发送的字节序列adb shell i2ctransfer -f -y 1 w40x6a 0x12 0x34 0xab 0xcd这条命令一次性发送了4个字节0x12寄存器高字节0x34寄存器低字节0xab数据高字节0xcd数据低字节。完美匹配了数据手册的时序要求。而i2cset的w模式可能无法生成这种精确的字节流。所以对于非常规的I2C时序i2ctransfer是你的首选工具。6. 进阶集成到系统测试与自动化将i2c-tools集成进系统后它的价值不止于手动调试。我们可以把它用于自动化测试。6.1 编写Shell测试脚本在设备上创建一个Shell脚本比如/vendor/bin/test_i2c_sensor.sh利用i2c-tools进行基本的通信测试。#!/vendor/bin/sh # 测试I2C总线1上的0x50设备假设是一个EEPROM BUS1 ADDR0x50 REG_ID0xFA # 假设厂商ID寄存器地址 # 1. 扫描总线确认设备存在 echo Scanning I2C bus $BUS... i2cdetect -y $BUS | grep -q $ADDR if [ $? -eq 0 ]; then echo Device found at 0x$(printf %02x $ADDR). else echo ERROR: Device not found at 0x$(printf %02x $ADDR)! exit 1 fi # 2. 读取厂商ID寄存器 echo Reading Manufacturer ID register... VAL$(i2cget -f -y $BUS $ADDR $REG_ID b) if [ $? -eq 0 ]; then echo Register 0x$(printf %02x $REG_ID) value: $VAL # 可以在这里判断VAL是否符合预期例如等于0xC2 if [ $VAL 0xc2 ]; then echo PASS: Manufacturer ID correct. else echo FAIL: Unexpected Manufacturer ID. exit 1 fi else echo ERROR: Failed to read register. exit 1 fi echo I2C basic test passed. exit 0然后你可以在Android的启动脚本如init.rc或init.device.rc中在某个阶段如on boot或on charger执行这个脚本将结果输出到log或特定文件用于生产环节的快速硬件检测。6.2 通过Android Test Framework集成更正规的做法是编写一个Native Test比如一个GTest在测试用例中调用i2c-tools的可执行文件通过system()或popen()或者直接链接libi2c.a静态库调用其API如i2c_smbus_read_byte_data进行测试。这样可以将I2C通信测试集成到CTS/VTS等自动化测试框架中。不过需要注意的是直接调用二进制可能涉及权限问题测试进程的SELinux域必须有相应的权限。而链接静态库的方式则需要将测试程序也集成到AOSP编译系统中并妥善处理头文件依赖。7. 经验总结与避坑指南回顾整个集成过程有几个关键点值得再次强调源码版本匹配使用与内核版本匹配的i2c-toolstag能避免绝大多数编译和运行时兼容性问题。静态链接优先对于这种系统调试工具静态链接生成独立二进制文件部署和运行的复杂度最低。SELinux是最大的“拦路虎”90%的“明明编译进去了却运行不了”的问题都源于SELinux策略。务必预留时间处理te文件。调试时善用dmesg | grep avc查看SELinux拒绝日志它是你编写策略的最佳指南。善用硬件调试工具当软件层面一切就绪但通信仍失败时逻辑分析仪是你的终极武器。抓取SCL/SDA的实际波形对照数据手册的时序图可以清晰看到起始信号、地址、ACK/NACK、数据位是否完全正确。我遇到过因为PCB走线过长导致信号边沿不佳最终在从设备端看不到ACK的情况就是靠波形分析定位的。理解设备地址务必分清7位地址和8位读写地址。这是新手最常犯的错误。i2cdetect显示的是7位地址。谨慎使用-f在生产设备或驱动已加载时避免使用强制访问标志以免引发系统不稳定。将i2c-tools集成到AOSP看似只是添加一个外部应用实则串联了AOSP编译系统、内核驱动、SELinux安全模型和硬件调试等多个层面的知识。走通这个过程你对Android系统底层的理解会深刻很多。下次再遇到I2C设备不听话你手里的工具和心中的思路都会清晰不少。
返回列表