公司动态

Android HAL模块开发实战:从编译集成到本地测试的完整指南

📅 2026/8/26 22:40:59
Android HAL模块开发实战:从编译集成到本地测试的完整指南
1. 项目缘起与目标为什么我们要“从上到下”写HAL在Android开发的深水区特别是涉及底层硬件交互时我们常常会听到“HAL”Hardware Abstraction Layer硬件抽象层这个词。很多开发者尤其是应用层出身的对它的印象可能停留在“一个需要实现的接口文件”或者“一个神秘的.so库”。网上关于HAL的教程要么是直接贴出hardware/libhardware/include/hardware/hardware.h的源码分析讲得云里雾里要么就是提供一个简单的“Hello World”例子告诉你实现hw_module_t和hw_device_t就完事了但和真实的硬件、真实的Android框架如何联动却语焉不详。这就导致了一个尴尬的局面看懂了概念但不知道从何下手或者照着例子写了一个却不知道怎么把它集成到AOSPAndroid Open Source Project里更不知道怎么让上层的Java/Kotlin代码调用到它。这个系列文章就是想彻底解决这个问题。我们不满足于只讲“是什么”更要讲清楚“怎么用”以及“为什么这么用”。所以这个“从上到下”的路径非常关键。它意味着我们的思考和实践路径是先明确上层应用或框架需要什么再设计HAL接口去满足它最后实现底层的驱动或模拟逻辑。这是一种以终为始、需求驱动的开发方式远比从底层寄存器开始盲写要高效和清晰。本篇作为第三部分我们将聚焦于如何将我们编写的HAL模块编译、集成到AOSP系统中并编写一个简单的本地测试程序Native Test来验证其功能。这是将代码从“孤岛”变为“系统一部分”的关键一步。2. 构建系统集成Android.bp与HAL模块的编译在AOSP中一切组件的编译都由Soong构建系统替代了旧的Make管理其配置文件是Android.bp。我们的HAL模块要想被编译必须在合适的目录下提供正确的Android.bp文件。2.1 HAL模块的存放位置与目录结构首先我们需要把HAL模块的源码放到AOSP源码树中正确的位置。按照AOSP的约定硬件相关的HAL实现通常放在hardware/目录下可以是芯片厂商的目录如hardware/mediatek也可以是通用或自己开发的目录。为了演示我们创建一个独立的目录。假设我们的项目名为myhaldemoHAL接口头文件为my_hal.h实现文件为my_hal_default.cpp。一个合理的目录结构如下AOSP_ROOT/ ├── hardware/ │ └── interfaces/ # 官方HIDL接口定义如果使用HIDL │ └── myhaldemo/ # 我们自定义的HAL项目目录 │ ├── Android.bp # 模块构建蓝图 │ └── default/ # 默认实现 │ ├── Android.bp # 默认实现的构建蓝图 │ ├── my_hal_default.cpp │ └── my_hal_default.h └── system/ └── libhidl/ # HIDL运行时等对于不使用HIDL的Legacy HAL基于hw_module_t我们通常将实现放在hardware/libhardware/modules/目录下或者像上面一样自建目录。为了更贴近现代AOSP的模块化思想我们采用自建目录的方式。2.2 编写Android.bp文件Android.bp文件定义了模块的类型、源码、依赖和编译属性。对于我们的Legacy HAL共享库模块核心是cc_library_shared。1. 实现层的Android.bp (hardware/myhaldemo/default/Android.bp)这个文件负责将我们的C源码编译成动态库.so。// 这个模块将编译成一个名为 my.hal.demo1.0-impl.so 的动态库 // 命名遵循了一定的模式接口名版本-实现类型 cc_library_shared { name: my.hal.demo1.0-impl, relative_install_path: hw, // 安装到 /system/lib(64)/hw/ 或 /vendor/lib(64)/hw/ proprietary: true, // 如果是厂商实现通常设为true srcs: [my_hal_default.cpp], header_libs: [my.hal.demo1.0-impl_headers], // 依赖我们生成的头文件见下文 shared_libs: [ liblog, // Android日志库 libhardware, // HAL核心库定义了hw_module_t等 libbase, libutils, ], cflags: [ -Wall, -Werror, -Wextra, ], export_include_dirs: [.], // 导出本目录头文件给依赖者 }关键点解析name: 模块的名称也是最终生成的库文件名不含.so。遵循interfaceversion-suffix的命名约定有助于系统管理。relative_install_path: “hw”: 这是至关重要的一步。Android系统在查找Legacy HAL模块时默认会在/system/lib(64)/hw/和/vendor/lib(64)/hw/等路径下搜索名为module_id.so的库。hw这个子目录就是信号。shared_libs: 列出了编译和运行时所需的共享库。libhardware是必须的它包含了hw_get_module等函数的实现。2. 接口/头文件层的Android.bp (hardware/myhaldemo/Android.bp)我们的HAL模块需要对外提供头文件my_hal.h以便测试程序或其他模块调用。我们需要定义一个头文件库模块。// 定义一个导出头文件的库 cc_library_headers { name: my.hal.demo1.0-impl_headers, export_include_dirs: [include], // 假设头文件在 include/ 子目录下 vendor_available: true, // 允许vendor分区模块使用 } // 可选定义一个整体包装模块方便通过 myhaldemo 这个名字引入 cc_defaults { name: myhaldemo_defaults, header_libs: [my.hal.demo1.0-impl_headers], shared_libs: [ my.hal.demo1.0-impl, ], }我们需要将my_hal.h头文件放入hardware/myhaldemo/include/目录中。这样当其他模块如我们的测试程序依赖my.hal.demo1.0-impl_headers时就能找到这个头文件。2.3 将HAL模块添加到产品配置中编译出的HAL库需要被包含进具体的系统镜像如system.img或vendor.img中。这需要在设备的产品配置文件.mk或.bp中声明。通常在设备目录下的device.mk或AndroidProducts.mk如果是Make系统或product_name.mk中你会看到PRODUCT_PACKAGES变量。我们需要把我们的模块加进去。对于Soong系统更常见的做法是在产品定义文件如aosp_arm64.mk中或者直接在模块的Android.bp中通过vendor: true等属性控制其安装分区。但为了确保被包含最直接的方式是在产品的device.mk中添加# 在 device/xxx/yyy/device.mk 中 PRODUCT_PACKAGES \ my.hal.demo1.0-impl这样在执行lunch选择了对应产品后编译系统就会将我们的HAL模块包含在编译清单中。实操心得很多时候HAL模块编译成功但运行时找不到问题就出在relative_install_path: “hw”没设置或者库没有被正确添加到产品包中。你可以编译后到out/target/product/device_name/system/lib64/hw/或vendor对应路径下检查是否有my.hal.demo1.0-impl.so文件这是最直接的验证方法。3. 编写本地测试程序Native Test在等待HAL模块被集成到完整系统镜像并刷机测试之前编写一个本地测试程序是快速验证逻辑正确性的高效手段。我们可以在主机端使用Android NDK工具链或直接在模拟器/设备的ADB Shell中编译并运行这个测试程序。3.1 测试程序的设计思路测试程序的目标是模拟上层如一个Native Service或直接一个可执行文件如何加载并调用我们的HAL。核心步骤就是使用hw_get_module加载我们的HAL模块。打开设备获取device操作句柄。调用HAL接口中定义的函数如我们之前设计的get_example_data。检查返回值打印结果。3.2 创建测试程序源码我们在hardware/myhaldemo/下创建一个test/目录并添加测试程序。文件hardware/myhaldemo/test/my_hal_test.cpp#include stdio.h #include stdlib.h #include string.h #include errno.h #include log/log.h // Android Log #include hardware/hardware.h #include hardware/my_hal.h // 我们的HAL头文件 #define LOG_TAG “MyHalTest” int main() { int ret 0; const hw_module_t *module NULL; my_hal_device_t *dev NULL; ALOGI(“Starting My HAL Test…”); // 1. 加载HAL模块 // MY_HAL_MODULE_ID 是在 my_hal.h 中定义的字符串如 “myhaldemo” ret hw_get_module(MY_HAL_MODULE_ID, module); if (ret ! 0) { ALOGE(“Failed to get HAL module %s, error: %d (%s)”, MY_HAL_MODULE_ID, ret, strerror(-ret)); return -1; } ALOGI(“Successfully loaded module: %s”, module-name); // 2. 打开设备 // MY_HAL_DEVICE_ID 通常也是 “myhaldemo” ret module-methods-open(module, MY_HAL_DEVICE_ID, (hw_device_t **)dev); if (ret ! 0 || dev NULL) { ALOGE(“Failed to open device %s, error: %d”, MY_HAL_DEVICE_ID, ret); return -1; } ALOGI(“Successfully opened device. API version: %d”, dev-common.version); // 3. 调用HAL接口函数 my_hal_data_t data; memset(data, 0, sizeof(data)); ret dev-get_example_data(data); if (ret 0) { ALOGI(“HAL call succeeded!”); ALOGI(“ Data id: %d”, data.id); ALOGI(“ Data value: %f”, data.value); ALOGI(“ Data info: %s”, data.info); } else { ALOGE(“HAL call ‘get_example_data’ failed, error: %d”, ret); } // 4. 关闭设备 (如果HAL实现提供了close函数通常由open返回的dev-common.close指向) if (dev-common.close ! NULL) { dev-common.close((hw_device_t *)dev); ALOGI(“Device closed.”); } else { ALOGI(“No close function provided.”); } ALOGI(“My HAL Test finished.”); return 0; }3.3 为测试程序编写Android.bp测试程序通常被编译成一个可执行文件cc_binary或一个本地测试模块cc_test。我们将其编译为可执行文件方便直接推送到设备运行。文件hardware/myhaldemo/test/Android.bpcc_binary { name: “my_hal_test”, srcs: [“my_hal_test.cpp”], header_libs: [“my.hal.demo1.0-impl_headers”], // 引用我们的头文件 shared_libs: [ “liblog”, “libhardware”, “libutils”, “my.hal.demo1.0-impl”, // 依赖HAL实现库这样编译时能链接但运行时仍需设备上有此库。 ], cflags: [ “-Wall”, “-Werror”, “-Wextra”, ], // 这个测试程序更适合在已有完整系统环境设备上运行而不是主机。 // 因此我们通常不设置 host_supported: true。 }3.4 编译与运行测试方法一在完整的AOSP编译环境中确保你的HAL模块和测试程序都已添加到产品配置PRODUCT_PACKAGES中或者至少确保它们的路径被构建系统扫描到通常放在hardware/下即可。在AOSP根目录执行source build/envsetup.sh lunch aosp_x86_64-eng # 以x86_64模拟器为例 make -j4 my_hal_test这会编译my_hal_test可执行文件及其所有依赖包括我们的HAL库。编译完成后启动模拟器emulator 将测试程序推送到模拟器并运行adb push out/target/product/generic_x86_64/system/bin/my_hal_test /data/local/tmp/ adb shell chmod x /data/local/tmp/my_hal_test adb shell /data/local/tmp/my_hal_test查看日志输出adb logcat -s MyHalTest方法二使用NDK独立工具链更灵活如果你不想每次都编译整个AOSP可以使用NDK的工具链在主机上交叉编译测试程序。但这需要你手动管理头文件和库的路径并且最终运行仍然需要设备上有对应的HAL库。踩坑记录测试程序运行时最常见的错误是hw_get_module返回-ENOENT找不到模块。请按以下顺序排查库名是否正确hw_get_module会尝试加载/system/lib(64)/hw/module_id.so。请确认你编译出的库文件名不含.so是否与MY_HAL_MODULE_ID严格一致并且安装在hw子目录下。库是否存在使用adb shell ls -l /system/lib64/hw/或/vendor/lib64/hw/查看。依赖是否满足使用adb shell ldd /system/lib64/hw/your_module.so检查HAL库本身的动态依赖是否都满足。权限问题确保库文件有正确的执行权限ls -l查看。4. 深入HAL模块的加载机制与调试技巧理解了如何编译和运行之后我们有必要深入一层看看hw_get_module背后发生了什么以及当它失败时我们有哪些强有力的调试手段。4.1 hw_get_module的搜索路径与过程hw_get_module的函数原型在hardware/libhardware/include/hardware/hardware.h中。它的核心逻辑是它首先会构造一个库名格式为id.variant.so其中variant来自ro.hardware、ro.product.board、ro.board.platform等系统属性用于支持硬件变体。如果找不到则尝试id.default.so。它会在一个预定义的路径列表中进行搜索。这个列表通常包括/system/lib64/hw/(或/system/lib/hw/)/vendor/lib64/hw/(或/vendor/lib/hw/)/odm/lib64/hw/(或/odm/lib/hw/) 具体路径和顺序可能因Android版本和设备而异找到库文件后它会调用dlopen打开该库并查找一个名为HAL_MODULE_INFO_SYM_AS_STR即”HMI”的符号这个符号应该指向一个hw_module_t结构体。如果找到并验证版本等信息通过就将这个模块结构体返回给调用者。这就是为什么我们的Android.bp中必须设置relative_install_path: “hw”以及库的name不含.so必须与module_id匹配的根本原因。4.2 高级调试手段Log与Strace当你的HAL模块加载失败或行为异常时仅靠测试程序的日志可能不够。1. 增加HAL内部的详细日志在你的HAL实现文件如my_hal_default.cpp中在open函数、各个操作函数里加入更详细的ALOGD或ALOGV级别的日志。重新编译并推送库后使用adb logcat -s your_log_tag过滤查看。注意可能需要设置系统属性persist.log.tag.your_tagD来启用DEBUG级别日志。2. 使用Strace跟踪系统调用strace可以跟踪一个进程的所有系统调用这对于诊断dlopen失败、文件找不到等问题非常有效。# 在设备上运行 adb shell strace -f -e tracefile,openat,stat /data/local/tmp/my_hal_test 21 | grep -E “\.so|hw/”这条命令会过滤出测试程序在运行过程中访问的所有文件特别是.so库和hw目录你可以清晰地看到它尝试了哪些路径以及失败的原因如ENOENT。3. 检查系统属性硬件变体variant依赖于系统属性。你可以通过adb shell getprop查看ro.hardware、ro.product.board等值。这决定了hw_get_module是否会去寻找id.variant.so。如果你的库名叫myhaldemo.default.so但系统属性决定的variant是goldfish那么它就会去找myhaldemo.goldfish.so而失败。确保你的库名匹配或存在兜底的default变体。4.3 模拟器与真机环境的差异在模拟器如aosp_x86_64-eng上测试HAL是最方便的因为你可以轻松编译和推送文件。但需要注意模拟器的/system分区通常是只读的在运行时无法直接adb push覆盖。你需要重新编译系统镜像make -j4并重启模拟器或者将库推送到/vendor分区如果支持并使用adb remount仅适用于eng或userdebug版本。真机环境更加复杂。你可能需要将HAL库编译进vendor.img并且需要设备的vendor分区签名密钥才能刷入。对于深度调试使用userdebug版本的设备并具有root权限会方便很多。权限问题在Android 8.0Oreo及更高版本中由于Treble项目对/vendor分区库的访问有严格的SELinux策略。如果SELinux拒绝访问你会在adb logcat中看到avc: denied的警告。这时需要调整SELinux策略文件这属于更高级的集成话题。经验之谈在开发初期强烈建议在x86或x86_64的模拟器eng版本上进行。它绕过了很多真机的限制如签名、SELinux让你能快速聚焦于HAL逻辑本身的正确性。等到基本功能稳定后再迁移到真机或特定的硬件平台上进行适配和深度集成。5. 从Native Test到框架集成下一步的方向通过本地测试程序验证了HAL的基本功能后我们的HAL模块仍然是一个“孤岛”。要让Android框架Java/Kotlin世界真正使用它我们还需要搭建一座“桥”。这座桥通常是一个系统服务System Service。5.1 可能的集成路径JNI Native Service编写一个C/C的系统服务作为binder服务或hidl/aidl服务这个服务在初始化时加载我们的HAL (hw_get_module)并将HAL的功能封装成Binder接口。然后在SystemServer中启动这个服务。上层应用通过Binder IPC调用这个服务服务再调用HAL。这是Legacy HAL集成到新框架的常见方式。HIDL/AIDL Service这是Android 8.0之后更现代、更受推荐的架构。将HAL接口用HIDLHardware Interface Definition Language或AIDLAndroid Interface Definition Language重新定义。我们的Legacy HAL实现则作为这个HIDL/AIDL服务的底层“后端”Passthrough HAL。框架层绑定这个HIDL/AIDL服务来使用硬件功能。这种方式更好地实现了框架与硬件的解耦。直接通过JNI调用不推荐在极少数简单场景下一个本地应用如一个native可执行文件或通过System.loadLibrary加载了JNI库的应用可以直接调用hw_get_module。但这通常仅限于系统底层工具不适合提供给普通APP使用。5.2 一个简单的JNI封装示例假设我们要为我们的HAL创建一个最简单的JNI封装让一个Java测试程序可以调用。1. 创建JNI库创建一个新的模块例如libmyhal_jni.so。它的Android.bp需要依赖我们的HAL实现库和头文件。2. 实现JNI函数在JNI的C代码中实现JNI_OnLoad并在其中缓存jclass和jmethodID。然后实现具体的本地方法例如// Java端public native String getHalInfo(); JNIEXPORT jstring JNICALL Java_com_example_myhaltest_MainActivity_getHalInfo(JNIEnv* env, jobject /* this */) { // 这里调用 hw_get_module, open, dev-get_example_data // … return env-NewStringUTF(data.info); }3. Java层调用在Java类中声明native方法并使用System.loadLibrary(“myhal_jni”)加载库。这个过程比本地测试复杂得多涉及到JNI编程规范、线程安全、异常处理等。但它清晰地展示了从Java世界到HAL的完整调用链。5.3 后续学习建议完成本系列的“从上到下”的例子后你应该已经掌握了HAL模块从接口设计、实现、编译、测试的基本闭环。要更进一步深入研究HIDL/AIDL学习如何定义.hal或.aidl接口文件并生成对应的C/Java框架代码。这是现代Android硬件交互的标准方式。理解Binder IPC这是Android系统服务的通信基石。了解IBinder、BnInterface、BpInterface等概念。学习一个真实的HAL例子在AOSP源码中找一个相对简单的HAL实现比如hardware/libhardware/modules/hello如果还存在或者传感器、灯光等HAL跟踪它的代码流看它是如何被框架服务调用的。掌握SELinux策略编写当你的服务或HAL需要访问特定资源时必须为其配置正确的SELinux域和权限。HAL开发是连接Android浩瀚应用生态与具体硬件世界的桥梁。打通这个链条不仅能让你对Android系统的理解更深一层也能让你在物联网、嵌入式、车载信息娱乐系统等需要深度定制的领域拥有强大的竞争力。希望这个从需求出发以实践贯穿的系列能为你打下坚实的基础。