如何通过 Android 的 Hardware Abstraction Layer(HAL)访问 I2C 设备?

解读

面试官问“如何通过 HAL 访问 I2C 设备”,并不是想听“用 Android Things 的 PeripheralManager 打开 I2C 设备节点”这种应用层答案,而是考察候选人是否真正走过“内核驱动 → HAL 实现 → HIDL/AIDL 接口 → System Service → JNI → Framework API”这一整条国产量产路线。国内手机、车载、TV 厂商普遍把 I2C 传感器(如光线、接近、ToF、摄像头 EEPROM、触控板、MCU 升级通道)做成私有 HAL,然后给上层 App 或系统服务暴露一个带权限校验的接口。能否把这条链路的每一层关键节点、SELinux 适配、签名权限、开机挂载、热插拔、FCC 认证锁频都说清楚,是区分“写过 demo”和“做过量产”的核心标准。

知识点

  1. Linux 内核层

    • 基于 i2c-core 编写 i2c_driver,在 dts 中声明 reg、中断、供电、复位脚,compatible 字段与驱动匹配;
    • 实现 regmap-i2c 或 i2c_transfer 裸接口,提供 sysfs 节点 /sys/bus/i2c/devices/xx-00yy/ 或自定义属性;
    • 配置 kernel-5.4+ 的 i2c-gpio 模拟总线时,注意 gpio 上升沿 300 ns 时序满足 FCC 认证要求;
    • 打开 CONFIG_I2C_CHARDEV,生成 /dev/i2c-N 节点,供用户空间 HAL 调用 ioctl(I2C_RDWR)。
  2. HAL 形态选择

    • 新平台(Android 10+)用 AIDL HAL,接口定义在 hardware/interfaces/i2c/1.0/II2cDevice.aidl;
    • 老平台(Android 8/9)用 HIDL,放在 hardware/interfaces/i2c/1.0/II2cDevice.hal;
    • 车载项目常把 I2C 封装进 vehicle HAL 的自定义 property,走 CarService 统一管控。
  3. HAL 实现

    • 在 hardware/xxx/i2c/ 目录实现 II2cDevice 接口,打开 /dev/i2c-N,使用 ioctl(I2C_SLAVE) 设置从机地址;
    • 读写前用 ioctl(I2C_FUNCS) 校验适配器是否支持 I2C_FUNC_I2C;
    • 对 16 bit 寄存器地址场景,构造 i2c_msg[2]:第一条写 reg 高 8 位,第二条读数据;
    • 用 android::base::unique_fd 管理 fd,避免 HAL 崩溃后句柄泄漏;
    • 在 SELinux 域 hal_i2c_default 中,允许访问 /dev/i2c-N、/sys/class/leds/xxx/brightness、/proc/irq/xxx;
    • 在 ueventd.rc 中把 /dev/i2c-N 0660 system system,防止被普通进程抢占。
  4. 系统服务封装

    • 在 frameworks/base/services/core/ 新增 I2cService,通过 ServiceManager.addService("i2c_service") 注册;
    • JNI 层实现 android_server_I2cService.cpp,把 AIDL 接口转成 native 调用;
    • 对第三方 APK 暴露 @SystemApi 接口,声明 android.permission.ACCESS_I2C_DEVICE,权限级别 system|signature|privileged,防止被商店应用滥用;
    • 在 SystemConfig 的 privapp-permissions-platform.xml 中把白名单写死,避免 CTS 失败。
  5. 应用层调用

    • 系统 App 通过 ServiceManager.getService("i2c_service") 拿到 II2cService,调用 transferRead(int bus, int addr, byte[] reg, byte[] buf);
    • 普通 App 只能走厂商自定义的 SDK(如 OPPO SensorSDK、小米 SensorServiceProxy),内部再鉴权 UID;
    • 在 Android 12+ 隐私沙盒环境下,I2C 访问被归为“环境传感器”组,需要用户后台权限弹窗。
  6. 性能与稳定性

    • 对 400 kHz 快速模式,把一次读操作限制在 8 byte 以内,防止 watchdog hard reset;
    • 在 HAL 内部用 std::lock_guard 保护 ioctl,避免多线程并发导致仲裁失败;
    • 打开 I2C_M_IGNORE_NAK 标志位时,必须在 3 ms 内超时退出,否则屏幕指纹 MCU 会拉低 I2C 总线造成整机死机;
    • 用 Systrace ATRACE_BEGIN("I2C") 打标签,方便量产线抓 trace 分析丢包。
  7. 国内合规

    • 通过 SRRC 认证时,需保证 I2C 射频校准通道在飞行模式仍可访问,因此 HAL 实现里要检查 Settings.Global.AIRPLANE_MODE_ON,不能简单返回 SecurityException;
    • 出口海外机型需加入 GMS 要求,确保 I2C 接口不会被当成“隐藏 API”扫描,否则无法过 GTS。

答案

以 Android 13 量产项目为例,完整流程如下:

  1. 内核
    在 dts 中新增
    i2c3: i2c@4a880000 {
    clock-frequency = <400000>;
    eeprom@50 {
    compatible = "vendor,eeprom";
    reg = <0x50>;
    };
    };
    编写驱动 vendor_eeprom.c,实现 regmap_read/write,生成 /dev/i2c-3 与 /sys/bus/i2c/devices/3-0050/eeprom。

  2. HAL
    定义 hardware/interfaces/i2c/1.0/II2cDevice.aidl:
    interface II2cDevice {
    byte[] read(int bus, int addr, int len);
    int write(int bus, int addr, in byte[] buf);
    }
    在 hardware/xxx/i2c/1.0 实现 I2cDevice.cpp:
    fd = open("/dev/i2c-3", O_RDWR);
    ioctl(fd, I2C_SLAVE, addr);
    read(fd, buf, len);
    编译生成 android.hardware.i2c@1.0-service.rc,指定 selinux 域 hal_i2c_default。

  3. SELinux
    在 hal_i2c_default.te 添加:
    allow hal_i2c_default i2c_device:chr_file { open read write ioctl };
    编译 sepolicy 后刷机,ls -Z /dev/i2c-3 显示 u:object_r:i2c_device:s0。

  4. System Service
    新增 I2cService.java,声明 @SystemApi,接口方法带 @RequiresPermission(android.permission.ACCESS_I2C_DEVICE)。
    在 SystemServer.java 启动:
    ServiceManager.addService("i2c", new I2cService(context));

  5. APK 调用
    系统 Settings App 在 AndroidManifest 声明 android:sharedUserId="android.uid.system",通过:
    II2cDevice dev = II2cDevice.Stub.asInterface(ServiceManager.getService("i2c"));
    byte[] id = dev.read(3, 0x50, 16);
    读取摄像头 EEPROM 的模组 ID,用于工厂校准。

  6. 验证
    adb shell cmd I2cService read 3 0x50 16
    返回 0x06 0x04 … 与示波器抓取波形一致,证明整条 HAL 链路打通。

拓展思考

  1. 如果后续把 I2C 传感器搬到折叠屏副屏 NFC 通道,而副屏热插拔会动态切换 i2c-5 与 i2c-6,HAL 如何监听 uevent 并自动重新 open fd?
    答:在 HAL 内部注册 NetlinkListener,解析 UEVENT=add/remove@/devices/platform/soc/xxx/i2c-5,当收到 remove 时关闭旧 fd,收到 add 时重新 probe 并通知 System Service 重新绑定。

  2. 当 Android 14 强制启用 AIDL exclusive 且移除 HIDL 支持,老项目如何平滑迁移?
    答:在 hardware/interfaces 新建 aidl 目录,把原有 .hal 转成 .aidl,同时保留 1.0 版本号,利用 android.hardware.i2c@1.0::II2cDevice* 适配层,确保 OTA 升级后旧系统镜像仍可调用新 HAL,实现国内厂商常用的“双版本兼容”策略。

  3. 若 I2C 设备挂在 PMIC 的 SSC 总线(高通 SPMI 模拟 I2C),频率仅 100 kHz,而工厂测试需要 1 MHz 高速烧录,如何在不改硬件的前提下提速?
    答:在 kernel 驱动里动态切换 clock-frequency 属性,通过 qpnp-pmic 提供的 pwm-mode 寄存器,把 SSC 总线切到 GPIO 模拟 I2C,并在 HAL 中暴露 setBusSpeed(int khz) 接口,工厂 APK 烧录完成后切回 100 kHz,兼顾认证与效率。