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_thw_device_t是Android HAL层的标准结构体,所有HAL模块都必须遵循这个规范。

核心接口设计原则:

  • 每个HAL模块必须实现openclose两个基础函数
  • 参数传递尽量使用结构体,避免函数参数过多
  • 返回值统一: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来讲解,保证让你印象深刻。