3. HAL层代码结构解析:MTK HAL源码目录结构、核心头文件与接口定义、编译系统
好,咱们直接进入正题。这一章我打算带你把MTK 8676的HAL层代码结构彻底捋一遍。说实话,很多新手拿到源码包第一反应就是懵——目录一层套一层,文件多到数不清。我当年第一次接触MTK平台时也是这样,对着屏幕发呆了好久。
但别怕,代码结构这东西,说白了就是「套路」。摸清了套路,后面写代码就顺了。
3.1 源码目录结构:从根目录说起
我们先看看MTK 8676 HAL层源码的顶层目录长什么样。我个人习惯把整个HAL层想象成一棵倒着长的树——根在最上面,枝叶往下展开。
vendor/mediatek/proprietary/
├── audio/ # 音频HAL
├── camera/ # 相机HAL
├── display/ # 显示HAL
├── gps/ # GPS HAL
├── sensors/ # 传感器HAL
├── bluetooth/ # 蓝牙HAL
├── wifi/ # WiFi HAL
├── lights/ # 指示灯HAL
├── power/ # 电源管理HAL
├── thermal/ # 温控HAL
└── common/ # 公共库与工具
嗯,这里要注意:每个子目录内部的结构其实大同小异。我以audio/为例,展开给你看看:
audio/
├── include/ # 头文件
│ ├── aud_drv.h # 音频驱动接口
│ └── aud_hal.h # HAL层对外接口
├── src/ # 源文件
│ ├── aud_hal.cpp # HAL实现
│ └── aud_utils.cpp # 工具函数
├── Android.bp # 新版编译配置
└── Android.mk # 旧版编译配置(兼容)
为什么会这样设计?其实很简单——include放接口声明,src放实现,编译文件单独放。这是C/C++项目最经典的布局方式。我在项目中遇到过有人把实现和头文件混在一起,结果维护起来简直噩梦。
3.2 核心头文件与接口定义
头文件是HAL层的「脸面」。上层框架(比如Android的AudioFlinger)就是通过头文件里声明的接口来调用你的HAL实现的。
我们拿aud_hal.h来举例。这个文件定义了音频HAL对外暴露的所有函数:
// aud_hal.h
#ifndef __AUD_HAL_H__
#define __AUD_HAL_H__
#include <hardware/hardware.h>
#include <system/audio.h>
// 打开音频设备
int aud_hal_open(const struct hw_module_t* module,
const char* id,
struct hw_device_t** device);
// 关闭音频设备
int aud_hal_close(struct hw_device_t* device);
// 设置音频参数
int aud_hal_set_parameters(struct audio_hw_device* dev,
const char* kvpairs);
// 获取音频参数
char* aud_hal_get_parameters(const struct audio_hw_device* dev,
const char* keys);
#endif // __AUD_HAL_H__
你看,接口定义非常清晰。每个函数都有明确的输入输出。这里有个关键点——hw_module_t和hw_device_t是Android HAL层的标准结构体,所有HAL模块都必须遵循这个规范。
核心接口设计原则:
- 每个HAL模块必须实现
open和close两个基础函数 - 参数传递尽量使用结构体,避免函数参数过多
- 返回值统一:0表示成功,负数表示错误码
我曾经踩过一个坑——有个同事把参数直接写成了20多个函数参数,结果调用方传参顺序搞错了,查了两天才定位到问题。所以,能用结构体就别用散参数。
3.3 编译系统:Android.bp vs Android.mk
MTK 8676同时支持两种编译方式。新项目推荐用Android.bp,但老项目里Android.mk依然大量存在。你想想看,一个平台要兼容那么多历史代码,两种方式并存是必然的。
3.3.1 Android.bp(新式编译)
这是Google主推的编译方式,使用Blueprint语法。说白了就是JSON风格的配置,可读性更好。
// Android.bp
cc_library_shared {
name: "audio.primary.mt8676",
relative_install_path: "hw",
srcs: [
"src/aud_hal.cpp",
"src/aud_utils.cpp",
],
shared_libs: [
"libhardware",
"libcutils",
"liblog",
],
cflags: [
"-Wall",
"-Werror",
],
include_dirs: [
"include",
],
}
这里cc_library_shared表示编译成动态库。name是模块名,srcs是源文件列表,shared_libs是依赖的共享库。嗯,要注意relative_install_path这个字段——它决定了你的so文件最终安装到/vendor/lib64/hw/目录下。
3.3.2 Android.mk(旧式编译)
虽然老了,但很多MTK的底层库还在用。我建议你至少能看懂它:
// Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := audio.primary.mt8676
LOCAL_MODULE_RELATIVE_PATH := hw
LOCAL_SRC_FILES := \
src/aud_hal.cpp \
src/aud_utils.cpp
LOCAL_SHARED_LIBRARIES := \
libhardware \
libcutils \
liblog
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include
LOCAL_CFLAGS := -Wall -Werror
include $(BUILD_SHARED_LIBRARY)
我的建议:新写模块用Android.bp,维护老模块时保留Android.mk。两者可以共存,编译系统会自动选择。
3.4 编译流程与调试技巧
写完了代码怎么编译?我一般用以下命令:
# 单独编译HAL模块
mmm vendor/mediatek/proprietary/audio/
# 或者用make命令
make audio.primary.mt8676
# 编译后push到设备
adb push out/target/product/xxx/vendor/lib64/hw/audio.primary.mt8676.so /vendor/lib64/hw/
adb reboot
警告:千万不要直接修改系统分区里的so文件然后重启——我试过,变砖了。正确的做法是先adb remount,再push,然后重启服务(比如adb shell killall audioserver),而不是重启整个系统。
3.5 实战:快速定位接口定义
最后分享一个实用技巧。当你需要查找某个HAL接口的定义时,别傻傻地翻目录。用grep命令:
# 查找所有包含"aud_hal_open"的文件
grep -rn "aud_hal_open" vendor/mediatek/proprietary/audio/
# 查找结构体定义
grep -rn "struct audio_hw_device" hardware/libhardware/include/
这个命令我每天用几十次。说白了,搞嵌入式开发,grep就是你的第二双眼睛。
好了,这一章的内容就到这里。下一章我们会深入HAL层的初始化流程,看看系统启动时HAL模块是怎么被加载和初始化的。到时候我会拿一个实际项目中的bug来讲解,保证让你印象深刻。