010、Camera HAL 开发环境搭建与代码导读
刚接手一个项目,Sensor 是 OV5640,平台是某国产 SoC。明明照着 vendor 给的手册搭环境,编译完刷进去,adb shell ls /vendor/lib/hw/里能看到camera.vendor.so,但用 Camera2 打开直接黑屏,logcat 里一句 “DeviceNotConfigured” 反复刷屏。后来折腾了两天,发现问题是编译时没把 vendor 依赖的私有库libcam_common.so一起拷到系统镜像里,运行时dlopen失败,HAL 模块加载不出来。这个坑让我意识到,开发环境搭建和代码导读不能跳过。
环境搭建先搞定三件事:编译工具链、AOSP 源码树、vendor 的私有库
说真的,Camera HAL 开发最烦的不是写代码,是环境。你以为把source build/envsetup.sh跑一遍就万事大吉了?太年轻。HAL 最底层跟 Linux Kernel 的 V4L2 打交道,上层要对接 Android 的 camera2 框架,中间还夹着一堆 vendor 自己封装的基础库。你手上的 SoC 厂商通常会给一个完整的 SDK 包,里面有预编译好的交叉编译器、AOSP 对应版本的源码补丁、还有一堆名字怪异的.so。第一步不是急着去看 HAL 代码,而是把我们需要的编译环境拉通。
拿我这边举例,Android 版本是 12,SoC vendor 要求用他提供的 NDK r23 和专用的 GCC 工具链,不能用 AOSP 默认的 clang。这个必须在build/envsetup.sh之后强制改CC和CXX环境变量。怎么改?在device/vendor/xxx/BoardConfig.mk里加:
TARGET_CC := vendor_prebuilt_cc TARGET_CXX := vendor_prebuilt_cxx别以为这行没用,踩过坑的人都知道,缺了这步,编出来的.so加载到真机上会直接SIGILL,因为 SoC 里有个自定义的乘加指令,发行版的 clang 不会生成。
还有编译命令。别用make systemimage这种整包编译,等你编完一次,半天过去了,然后告诉你某个头文件找不到。你会发现你改的 HAL 代码根本没触发重新编译。正确姿势是进入源码根目录,先lunch你的 product,然后m指定模块名,或者mm在模块路径下编译。我个人的习惯是:
source build/envsetup.sh lunch xxx-userdebug cd $ANDROID_BUILD_TOP/hardware/vendor/camera mma -j8这里mma背后会先编依赖,比如我们要用到的libcamera_metadata,它可能在system/media下。如果依赖没编,报错不是找不到函数,而是undefined reference tocreate_camera_metadata_from_blob。那是真头疼。所以第一次做环境,建议在源码根目录执行m libcamera_metadata libcutils libhardware -j8`,把基础库先编一遍。
代码导读,别从camera_hal.cpp开始读
很多新手拿到 HAL 程序,打开camera_hal.cpp看到一堆hw_module_methods_t就开始头晕。正确打开方式是先看入口点,也就是 HIDL/HIDL HAL 服务进程的 main 函数。现在 Android 8.0 之后,Camera HAL 被封装成一个独立的 vendor 进程,比如android.hardware.camera.provider@2.7-service.vendor。这个服务的main.cpp做了三件事:注册 HIDL 服务、启动线程池、等待客户端 binder 调过来。
从代码阅读路径上,我建议这样走第一遍:
- 先看
manifest.xml或hal.makefile里声明的版本,明确是 v3.4 还是 v3.7,或者还是老式的 v1。 - 找到
camera_module_t这个结构体,它就是 HIDL 层和传统 hardware lib 层的桥梁。 - 看
open()函数,这里面会初始化你的 sensor 管线,并且给camera3_device_ops_t赋值。 - 再看
configure_streams()、process_request()这三个函数,这基本就是 HAL 的全部业务逻辑了。
代码目录结构,命名可能有差异,但核心模块跑不了:
- include/ // 头文件,定义内部接口、数据结构、宏 - src/ // 源码 - hal/ // 上层的 camera3 device ops - pipeline/ // 底层 sensor 驱动封装,V4L2 ioctl 调用 - processing/ // ISP 相关算法封装 - metadata/ // 静态 metadata 和动态 metadata 的处理 - utils/ // 日志、锁、内存管理辅助 - test/ // 单元测试,编译成可执行文件单独跑src/hal/下的文件名字都很有规律:CameraModule.cpp对应模块入口,CameraDevice.cpp对应单路 camera 的设备操作。注意这里的命名跟 AOSP 注释要统一,好多厂商直接砍掉注释,包一层 C++ 类,再包一层 C 的接口,你看着复杂,其实就一个套路:C 函数把参数强制转换成内部类的指针,然后调用成员函数。
日志系统,搞不清楚会浪费一整个下午
HAL 里不能用printf,因为 HAL 跑在 vendor 进程里,stdout 跑到哪儿去你根本看不见。要用ALOGE/ALOGD/ALOGV,这些宏会打印到 logd 的 kernel 缓冲区。调试时需要设置setprop vendor.camera.hal.log_level 2,然后重启 camera provider 服务,或者直接重启机器。抓 log 用:
adb logcat -s CAM_HAL:CAM_PIPE:CAM_META如果厂商有专门的日志 tag,比如CamX或CamHAL3,那就用自己的。注意区分 vendor 进程的 log 和 framework 层的 log。HAL 层输出的 log 按 pid 过滤看不见,因为 HIDL 会隔层。我的经验是直接给硬编码一个 tag,比如#define TAG "CTAM_HAL",然后用:
ALOGE("%s: %s: failure, ret=%d", TAG, __func__, ret);这样抓 log 时adb logcat | grep CTAM_HAL一行不漏。
还有一个坑,ALOGV默认是关闭的,因为需要定义LOG_NDEBUG为 0。在你的 Android.bp 里要加:
cppflags: ["-DLOG_TAG=\"CTAM_HAL\"", "-DLOG_NDEBUG=0"]否则写ALOGV等于没写。我当年因为这个调试信息全部消失,以为代码没跑进去。
编译绕不开的几个坑,都给你列出来
第一,头文件路径。AOSP 源码树里面,有一个external/和vendor/下的 include 目录经常不被打进默认的 include path。比如你用到<linux/media.h>,系统里的版本太老,没有MEDIA_ENT_F_IO_V4L这个枚举。解决办法是直接在Android.bp里写:
local_include_dirs: ["kernel_includes/", "hardware/libhardware/include/"]指定到 vendor 提供的内核头文件目录。别把整个 kernel 源码 include 进去,会有重复定义。
第二,编译宏。有的 SoC 支持三路 virtual channel 摄像头,需要打开ENABLE_VIRTUAL_CHANNEL。这个宏必须在BoardConfig.mk里定义:
TARGET_VENDOR_CAMERA_DEFINES := -DENABLE_VIRTUAL_CHANNEL然后在模块的cflags里引用。如果你不知道有这个宏,代码里被#ifdef包裹的那段初始化逻辑永远不会编译,然后你看到的 sensor 配置总是错乱,颜色呈紫色。后来发现是没开启宏,导致 route 配置表选错。
第三,链接符号。HAL 依赖 vendor 的私有库,比如libcam_common.so,它不在 AOSP 源码里,是 vendor 编译好的。如果在链接时发现undefined reference toGetSensorRegTable,别慌,这是没让链接器找到这个库。在Android.bp` 里加:
shared_libs: [ "libcam_common", "libcutils", "libutils", "libhardware", "libcamera_metadata", ]但要注意,libcam_common必须已经在/vendor/lib里存在。如果你编译时链接的是libcam_common,而系统镜像里没有这个文件,就会遇到我开头的那个问题。检查方法很简单,编译完看out/target/product/xxx/vendor/lib/libcam_common.so在不在。
代码导读的切入点:open 流程到底干了啥
不要急着看 process_request 那个大管线的实现。先跑通 open/close 单测,能让 HAL 成功 open 一个 camera 设备,你就成功了一半。以 Camera3 为例,打开流程是:
camera_module_t->open() -> CameraDevice::open()CameraDevice::open里主要做:
- 初始化底层 sensor:
SensorV4L2::Open("/dev/video0") - 配置管道状态机:把
mState设为INITIALIZED - 把当前动态 metadata 中的
ANDROID_DEVICE_INFO_SUPPORTED_HARDWARE_LEVEL设为 LIMITED
这里的坑是,你必须等init()调用完之后才能访问 sensor。如果init()里有个wait_for_ready(10)超时,那 HAL 会卡在 open 阶段,然后camera_service那边会报camera device was externally scheduled to be opened。所以建议在 open 里只做资源初始化和状态机切换,不要做长时间的操作,比如 sensor 的 firmware 升级。
我刚接触 HAL 时,认为 open 就应该把 ISP 也配完,结果每次 open 都要等 2 秒,然后 framework 那里直接提示Failed to open camera。后来把 ISP 初始化移到第一个process_capture_request里用懒加载方式,才解决。切记 open 一定要快,快过 200 毫秒。
调试过程中为什么经常怀疑人生
因为 HAL 的崩溃不像应用层那样弹窗。一旦 HAL 进程死了,framework 会重启它,但你在 logcat 里只看到Client exited due to error -32。这几乎没用。正确方法是先关闭 SELinux,或者至少把 camera 相关的 te 策略改成 permissive。否则 HAL 打不开/dev/video0时会被 SELinux 挡,报avc: denied { read } for scontext=u:r:hal_camera。我现在通常开局就先切 permissive:
adb root adb shell setenforce 0调试完再恢复。千万别带着 permissive 交给测试,会很尴尬。
还有一个常用的招数,用 vendor 提供的 v4l2 测试工具,比如media-ctl或v4l2-ctl,先直接抓一帧 sensor 原始数据,确认 sensor 时序没问题,再回到 HAL 里去调试。HAL 也提供了一个-D参数可以 dump 出 YUV 帧到文件里,可以用adb pull拿出来用七牛/福昕看图?不,直接用 Python 脚本转换成 raw 文件。这些工具的作用是帮你把问题域切开:是 sensor 出图的问题,还是 HAL 软件的问题。
我的个人建议
写 HAL 一定要把环境搭建当成第一步重心,别急着写业务代码。我见过太多人,拿到源码就开始改 process_request,结果半天编不过,或者刷进去就崩。你还不如先把编译环境验证三遍:第一次用 mma 编 HAL,第二次用 mma 编依赖库,第三次整机刷机验证 open / close 是否正常。这三步跑通了,后面写代码才有底气。
代码导读上,强烈建议按照我上面给的顺序来,不要直接冲到最底层看 V4L2,那样很容易迷失在 ioctl 的海洋里。先明白这个 HAL 是干什么的,它返回给 framework 哪些能力,再从函数调用链去倒推实现。这样效率高很多。
最后一点,所有你加的调试打印不要用ALOGD,直接用ALOGE,即使不是错误。因为很多设备默认 log level 过滤掉了 verbose 和 debug,唯独 error 一定会打出来。等你调试完毕,再把这些错误级别的日志改成 debug 级别留给后续维护。这样能避免你写一大堆日志然后发现根本没输出。
对了,别忘记备份你调通的Android.bp和BoardConfig.mk,这些文件配置着环境最关键的变量。等后面升级 SDK 版本时,很多坑其实就藏在环境配置里,重新对一遍,能省一个星期。