Android HAL 架构详解,底层开发不再难
HAL 基础概念
HAL 是个啥?
简单来说,HAL 就是 Android 系统里的一层 “翻译官”。它站在 Linux 内核驱动和 Android 运行时环境中间,把底层的硬件操作封装成上层能轻松调用的接口。想象一下,你家有台老式收音机,旋钮、开关一大堆,但你给它加了个遥控器 ——HAL 就是那个遥控器,让上层软件不用直接去 “拧旋钮”,而是通过标准化的按钮来控制硬件。
HAL 的定位很明确:
- 位置:跑在用户空间(User Space),不像 Linux 内核驱动那样扎根在内核空间(Kernel Space)。这种分离的好处是,改动 HAL 不会直接影响到内核的稳定性,升级维护也更灵活。
- 作用:把硬件的具体实现细节藏起来,向上抛出统一的接口。比如,不管你用的是高通的摄像头还是联发科的传感器,上层看到的都是同一个 “拍照” 按钮。
- 实现:通常是模块化的,每个硬件设备(像音频、摄像头、蓝牙)都有自己的 HAL 模块,通过标准接口跟上层打交道。
举个例子,Android 的相机功能靠 Camera HAL 模块支持。开发者调用 takePicture () 时,HAL 会默默地把这个请求翻译成具体的硬件指令,可能是打开 CMOS 传感器,调整曝光时间,最后抓取图像数据。整个过程对上层来说是透明的,硬件厂商也能在这层塞进自己的独门绝技。
为啥要有 HAL?
HAL 不是凭空冒出来的,它的设计背后有几个硬核目的,解决的是 Android 生态里真实存在的问题。咱们一条条来看:
- 硬件抽象化:抹平差异,统一体验。Android 设备千千万,硬件平台五花八门,有高通、联发科、三星 Exynos,还有各种小厂的芯片。如果没有 HAL,每换个硬件,上层框架就得重写一遍适配代码,那简直是噩梦。HAL 的核心任务就是把这些差异 “抽象” 掉,让上层看到的世界永远是标准化的。比如,音频播放的接口可能是 playAudio (),不管底下是 Realtek 芯片还是 Cirrus Logic 的解码器,上层都不用操心。
- 保护厂商机密:藏起你的 “独门配方”。硬件厂商总有些独家技术不想公开,比如某个摄像头的高级降噪算法,或者音频芯片的低功耗优化。这些代码如果直接写进 Linux 内核驱动,因为 GPL 协议的关系,可能得开源出来。HAL 跑在用户空间,厂商可以把核心逻辑塞在这里,既能保护知识产权,又不影响系统整体的开放性。
- 绕过 GPL 的 “传染” 风险:Linux 内核用的是 GPL 许可,任何直接改动内核的代码都可能被要求开源。Android 作为一个商业化系统,不希望被 GPL “绑架”。HAL 把硬件访问逻辑从内核剥离出来,放到了用户空间,这样既规避了法律风险,也给厂商留了操作空间。
- 灵活应对特殊需求:有些硬件有奇葩要求,比如某个传感器需要特殊的初始化流程,或者某个音频芯片得配合厂商的私有协议。HAL 就像个 “私人订制” 的舞台,厂商可以在这层实现自己的需求,而不干扰系统的其他部分。
总的来说,HAL 是个平衡大师。它既要让 Android 保持开放和一致,又得给硬件厂商留足创新的余地,还得解决法律和技术上的小麻烦。设计得真是巧妙!
HAL 在系统中的位置
在 Android 的架构图里,HAL 的角色很显眼。它蹲在 Linux 内核之上,Android Framework 之下,像个中间商,负责两边沟通。咱们可以这么拆解它的位置:
- 底下是 Linux 内核:提供基础的驱动支持,比如字符设备、块设备、I2C、SPI 这些硬件接口。内核只管最底层的硬件控制,具体怎么用它不管。
- 上面是 Framework:包括 Java 写的系统服务(像 AudioFlinger、CameraService)和 Native 层代码,负责把硬件能力暴露给应用。
- HAL 自己:用 C/C++ 写成动态链接库(.so 文件),通过标准接口跟 Framework 对接,同时调用内核的驱动接口操作硬件。
举个例子,音频播放的流程可能是这样的:
- 应用调用 MediaPlayer.play ()。
- Framework 里的 AudioFlinger 收到请求,转给音频 HAL 模块。
- 音频 HAL 操作内核驱动,打开声卡,推送音频数据。
- 声音从扬声器飘出来,用户啥也不知道。
这种分层设计让 Android 既稳定又灵活,HAL 功不可没。
HAL 工作原理
讲完了 HAL 的基本概念,咱们深入看看它是怎么干活的。HAL 的工作原理可以拆成几个关键部分:抽象接口、模块加载、通信机制和设备访问。每个部分都有自己的门道,下面咱们逐一展开。
抽象接口:硬件的 “通用语言”
HAL 的核心在于 “抽象”,而抽象靠的是精心设计的接口。这些接口就像是硬件和软件之间的契约,规定了双方怎么打交道。
接口的设计思路
HAL 的接口设计有两大特点:
- 面向对象:把硬件抽象成对象,每个对象有自己的方法和属性。比如,相机 HAL 可能有个 CameraDevice 对象,带 open ()、capture () 这样的方法。
- 模块化:每种硬件对应一个独立模块,互不干扰。比如音频 HAL 不会管传感器的事,各自为政,方便维护和扩展。
核心结构体
HAL 的接口靠几个关键结构体撑起来,定义在 hardware.h 头文件里:
- hw_module_t:描述一个硬件模块的基本信息,比如模块 ID、版本号,还有指向具体方法的指针。
struct hw_module_t {
uint32_t tag; // 固定标签,标识这是HAL模块
uint16_t module_api_version; // 模块API版本
const char *id; // 模块唯一标识符,如"audio"
const char *name; // 模块名字
struct hw_module_methods_t *methods; // 方法表指针
};
- hw_module_methods_t:定义模块的操作方法,通常只有一个 open () 函数,用来打开具体设备。
struct hw_module_methods_t {
int (*open)(const struct hw_module_t* module, const char* id,
struct hw_device_t** device);
};
- hw_device_t:描述单个设备的属性和方法,比如设备的版本号、关闭函数等。
struct hw_device_t {
uint32_t tag; // 固定标签
uint32_t version; // 设备版本
struct hw_module_t *module; // 所属模块
int (*close)(struct hw_device_t* device); // 关闭方法
};
这些结构体是 HAL 的骨架,上层通过它们调用硬件功能。比如,想用摄像头,Framework 会先找到 camera 模块的 hw_module_t,然后调用它的 open () 方法,拿到 hw_device_t,再操作具体功能。
版本与兼容性
HAL 接口还很注重版本管理。每个模块都有个版本号(module_api_version),上层会先检查这个版本,确保不会调到不兼容的实现。这设计保证了 Android 系统能一边支持新硬件,一边不抛弃老设备。
实例:相机 HAL 接口
假设我们要用相机 HAL,流程可能是这样的:
- Framework 调用 hw_get_module ("camera", &module),拿到 camera 模块的 hw_module_t。
- 用 module->methods->open () 打开设备,得到 hw_device_t。
- 通过 hw_device_t 里的方法(比如 take_picture)拍照片。
这套接口让上层完全不用管底层的 CMOS 传感器是咋工作的,抽象得干净利落。
模块加载:动态链接的魔法
HAL 模块不是一次性全加载进内存的,而是按需动态加载。这过程主要靠 hw_get_module () 函数完成,背后涉及不少细节。
加载步骤
- 找模块文件:系统会在几个预定义路径里找 HAL 模块的.so 文件,比如:
- /system/lib/hw/(系统默认路径)
- /vendor/lib/hw/(厂商自定义路径)
比如音频 HAL 可能是 audio.primary.default.so,路径清晰,名字直白。
- 检查版本:加载前,系统会读模块的 hw_module_t,比对版本号。如果版本不符(比如 Framework 要 2.0,模块是 1.0),就直接报错,避免不兼容的悲剧。
- 动态加载:用 dlopen () 把.so 文件拉进内存,再用 dlsym () 找到模块的入口点(通常是个叫 HAL_MODULE_INFO_SYM 的符号)。这步就像打开个黑盒子,把里面的功能掏出来。
- 初始化:拿到 hw_module_t 后,调用它的 open () 方法,初始化硬件,建立连接。
优化策略
- 延迟加载:模块只有在被需要时才加载,比如你不拍照,相机 HAL 就不占内存,省资源。
- 热插拔:支持运行时动态加载卸载,比如插上 USB 设备时加载对应的 HAL 模块。
实例:加载音频 HAL
假设你要播放音乐,系统会这么干:
- 调用 hw_get_module ("audio", &module)。
- 在 /vendor/lib/hw/ 找到 audio.primary.qcom.so。
- 检查版本,确认是 2.0,OK。
- dlopen () 加载,拿到 hw_module_t,再 open () 初始化声卡。
这套机制让 HAL 既灵活又高效,硬件支持像搭积木一样随插随用。
通信机制:HAL Binder 的 IPC 魔法
HAL 不只是个 “翻译官”,它还得是个高效的 “邮递员”,把上层的请求快速送到硬件,再把结果捎回来。这背后靠的是 Android 的进程间通信(IPC)机制,而 HAL 里的明星选手就是 HAL Binder。这玩意儿基于 Android 的 Binder 框架,又加了点自己的料(HIDL),让通信既快又清晰。
为啥用 Binder?
Android 的 IPC 有好几种选择,比如 Socket、共享内存,但 Binder 最终胜出,原因很简单:
- 效率高:数据传输只拷贝一次(Socket 要两次),省时间省内存。
- 安全性强:Binder 有身份验证机制,不怕随便哪个进程来捣乱。
- 灵活性好:支持复杂的数据结构,不像 Socket 只能传简单字节流。
HAL Binder 在传统 Binder 上加了个中间层 ——HIDL(Hardware Interface Definition Language),专门用来定义 HAL 和上层之间的接口。这让通信更规范,也更好维护。
通信的关键玩家
HAL Binder 的运作靠几个核心组件配合:
- hwservicemanager:这是 HAL 服务的 “大管家”,跑在系统里,负责管理所有 HAL 模块的注册和查找。上层想用某个 HAL(比如相机),就得先找 hwservicemanager 问:“相机 HAL 在哪?” 它返回一个服务句柄,上层才能接着聊。
- HwBinder 驱动:藏在内核空间,是 Binder 的 “加强版”。负责在进程间搬数据,把上层的请求送到 HAL 模块,再把结果送回去。HIDL 在这层把数据转成标准格式,确保两边都能看懂。
- HIDL Stub 和 Proxy:
- Stub:在 HAL 模块这边,负责实现具体的接口逻辑。比如,相机 HAL 的 capture () 方法就在 Stub 里写实现。
- Proxy:在上层 Framework 这边,负责把请求打包发出去,等结果回来再解包。
这俩就像电话线的两端,一个接一个说。
通信流程
假设你点了拍照,通信是怎么跑起来的?
- 服务注册:相机 HAL 启动时,跟 hwservicemanager 说:“我在这儿,名字叫 camera@2.1,有事找我!”
- 服务查找:Framework 里的 CameraService 问 hwservicemanager:“相机 HAL 在哪?” 拿到句柄。
- 发请求:Framework 通过 Proxy 调用 capture (),请求被序列化,经 HwBinder 送到相机 HAL。
- 处理请求:相机 HAL 的 Stub 收到后,操作硬件拍照片,把结果序列化返回。
- 结果返回:Framework 解包结果,交给应用,照片就出来了。
整个过程快得飞起,用户完全感觉不到中间的复杂。
HIDL 的妙处
HIDL 是通信的灵魂,它用类似 Java 的语法定义接口,长这样:
package android.hardware.camera@2.1;
interface ICamera {
capture() generates (CameraFrame frame); // 拍张照,返回帧数据
setExposure(int32_t value); // 设置曝光
}
- 清晰:接口一目了然,客户端和服务端都按这套规则玩。
- 跨语言:能生成 C++ 和 Java 代码,开发者挑自己擅长的用。
- 版本控制:@2.1 表明版本,上层看到不匹配的版本就知道别乱调。
优势总结
HAL Binder 这套机制牛在哪?
- 性能:一次拷贝,快如闪电。
- 规范:HIDL 让接口定义整齐划一,维护省心。
- 兼容:版本管理做得好,新老设备都能跑。
有了这套通信体系,HAL 就像打了通脉,上下贯通,硬件操作变得丝滑无比。
设备访问:摸到硬件的最后一公里
通信机制搭好了桥,接下来 HAL 得真正把手伸到硬件上。这部分是 HAL 的 “最后一公里”,直接决定硬件能不能被用起来。
访问的本质
HAL 的设备访问靠的是跟 Linux 内核驱动打交道。内核提供了基础接口(比如 /dev/ 下的设备节点),HAL 在这之上加了一层逻辑,把底层的 ioctl、read、write 封装成上层能用的功能。
实现细节
HAL 模块通常用 C/C++ 写,少不了跟指针、内存管理打交道。核心步骤是:
- 打开设备:通过 open () 调用内核驱动,比如 open ("/dev/audio", O_RDWR) 打开声卡。
- 配置硬件:用 ioctl () 设置参数,比如采样率、通道数。
- 数据交互:用 write () 发数据,read () 取数据,比如推送音频流或读取传感器值。
- 关闭设备:用 close () 释放资源。
实例:音频设备访问
假设我们要播放一段 PCM 音频,音频 HAL 的流程可能是:
int audio_device_open(struct hw_device_t *device) {
int fd = open("/dev/snd/pcmC0D0p", O_RDWR); // 打开声卡设备
if (fd < 0) return -errno;
// 配置硬件:44.1kHz采样率,立体声
ioctl(fd, SNDCTL_DSP_SPEED, 44100);
ioctl(fd, SNDCTL_DSP_CHANNELS, 2);
device->fd = fd; // 保存文件描述符
return 0;
}
int audio_write(struct hw_device_t *device, char *buffer, size_t size) {
return write(device->fd, buffer, size); // 推送音频数据
}
上层调用 audio_write (),HAL 直接把数据写到声卡,扬声器就响了。硬件厂商可能在这儿加点私货,比如用 DSP 优化音质,但上层完全不用管。
技能要求
写 HAL 模块得有点硬功夫:
- C/C++ 基础:指针、结构体、内存分配得玩得溜。
- Linux 驱动知识:懂得怎么跟字符设备、I2C、SPI 交互。
- 调试能力:硬件问题不好查,logcat 和 gdb 得用熟。
灵活性与一致性
HAL 这层设计既要保证上层调用的一致性(接口不变),又得给厂商留空间(实现随便改)。比如,同样是 setExposure (),高通可能调寄存器 A,三星调寄存器 B,HAL 把这些差异藏得严严实实。
到这儿,HAL 的工作原理就齐活了:抽象接口定义规则,模块加载搭舞台,通信机制跑数据,设备访问干实事。接下来,咱们看看几个具体的 HAL 模块,感受下它们是怎么落地的。
HAL 关键组件
HAL 是个模块化的体系,每种硬件都有自己的 “代言人”。咱们挑三个常见的聊聊:音频 HAL、相机 HAL 和传感器 HAL。每个模块都有自己的门道,既有共性又有个性。
音频 HAL:让声音飞起来
音频 HAL 是 Android 里最忙碌的模块之一。无论是听歌、打电话还是刷视频,声音都得靠它传出来。
核心功能
音频 HAL 得干这些活:
- 初始化:打开声卡,设置硬件状态。
- 流管理:处理输入(麦克风)和输出(扬声器、耳机)的音频流。
- 格式转换:把 MP3、AAC 解码后的 PCM 数据适配硬件支持的格式。
- 效果处理:加点混响、降噪,提升音质。
实现细节
音频 HAL 的实现通常跟内核的 ALSA(Advanced Linux Sound Architecture)驱动对接。代码可能长这样:
struct audio_device {
struct hw_device_t common;
int fd; // 声卡文件描述符
int sample_rate; // 采样率
int channels; // 声道数
};
static int audio_hw_init(struct audio_device *dev) {
dev->fd = open("/dev/snd/pcmC0D0p", O_RDWR);
if (dev->fd < 0) return -errno;
dev->sample_rate = 44100;
dev->channels = 2;
ioctl(dev->fd, SNDCTL_DSP_SPEED, &dev->sample_rate);
ioctl(dev->fd, SNDCTL_DSP_CHANNELS, &dev->channels);
return 0;
}
DSP 优化
现代音频 HAL 常利用数字信号处理器(DSP)做硬件加速。比如,高通的 Hexagon DSP 能实时处理均衡器效果,CPU 只管发号施令。
实例:播放音乐
用户点开音乐 App,流程是:
- AudioFlinger 调用音频 HAL 的 open_output_stream ()。
- HAL 初始化声卡,设置 44.1kHz 立体声。
- App 推送 PCM 数据,HAL 用 write () 写到声卡。
- 扬声器响起来,完美!
音频 HAL 得平衡性能和功耗,尤其在低端设备上,厂商可能得费心思优化。
相机 HAL:定格世界的眼睛
相机 HAL 是摄影爱好者的好朋友。它把复杂的摄像头硬件变成简单的接口,让你轻松拍出大片。
核心功能
相机 HAL 得搞定这些:
- 初始化:启动摄像头,配置传感器和 ISP(图像信号处理器)。
- 图像采集:抓取原始数据(RAW 或 YUV)。
- 参数控制:调曝光、对焦、白平衡。
- 效果处理:加 HDR、夜景模式这些花活。
实现细节
相机 HAL 常跟内核的 V4L2(Video4Linux2)驱动配合。代码可能这样:
struct camera_device {
struct hw_device_t common;
int fd; // 摄像头设备描述符
};
static int camera_open(struct hw_module_t *module, struct hw_device_t **device) {
struct camera_device *cam = malloc(sizeof(*cam));
cam->fd = open("/dev/video0", O_RDWR);
if (cam->fd < 0) {
free(cam);
return -errno;
}
// 配置640x480,YUV格式
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
ioctl(cam->fd, VIDIOC_S_FMT, &fmt);
*device = &cam->common;
return 0;
}
多摄支持
现代相机 HAL 得管好几个镜头(广角、超广角),还得支持实时切换。
实例:拍张照
你按下快门,流程是:
- CameraService 调用相机 HAL 的 capture ()。
- HAL 配置传感器,抓取 YUV 数据。
- ISP 处理后返回 JPEG,上层存成照片。
厂商可能在这儿加 AI 算法,比如人脸识别啥的,全看硬件实力。
传感器 HAL:感知世界的触角
传感器 HAL 让手机变得 “聪明”,能感知光线、加速度、陀螺仪这些信息。
核心功能
它得干这些:
- 初始化:激活传感器,设置采样频率。
- 数据采集:实时抓取传感器读数。
- 事件处理:把数据打包成事件发给上层。
- 数据融合:结合多个传感器算出方向、步数。
实现细节
传感器 HAL 常跟内核的 IIO(Industrial I/O)子系统交互。代码可能是:
struct sensor_device {
struct hw_device_t common;
int fd; // 传感器设备描述符
};
static int sensor_activate(struct sensor_device *dev) {
dev->fd = open("/sys/bus/iio/devices/iio:device0", O_RDWR);
if (dev->fd < 0) return -errno;
// 设置采样率10Hz
int rate = 10;
write(dev->fd, "sampling_frequency", &rate, sizeof(rate));
return 0;
}
低功耗
现代传感器 HAL 支持硬件 FIFO,数据先存芯片里,CPU 睡着时也能工作。
实例:计步器
你走路时:
- SensorService 调用传感器 HAL 的 activate ()。
- HAL 从加速度计取数据,算出步数。
- 上层收到事件,步数 + 1。
传感器 HAL 在物联网时代越来越重要,厂商还得考虑功耗和精度。
接口定义:HIDL 的 “语言艺术”
HAL 的接口定义是整个体系的灵魂,它决定了上层和底层怎么 “说话”。早期的 HAL 用的是 C 语言的结构体和函数指针,后来 Google 觉得这套玩意儿太原始,维护起来费劲,就推出了 HIDL(Hardware Interface Definition Language,硬件接口定义语言)。这东西是 HAL 的 “新语言”,让接口定义变得优雅又高效。
HIDL 是啥?
HIDL 是一种专门为 HAL 设计的接口描述语言(IDL),有点像 Java 和 C++ 的混合体。它的目标是:
- 规范化:让 HAL 接口清清楚楚,不乱写一气。
- 跨语言:能生成 C++ 和 Java 代码,开发者随便挑。
- 分离性:把 HAL 实现和 Android 框架解耦,方便系统升级。
HIDL 文件以.hal 为后缀,通常存在 hardware/interfaces/ 目录下。比如相机 HAL 的接口可能在 hardware/interfaces/camera/2.1 / 里。
HIDL 长啥样?
HIDL 的语法简单直白,看个例子:
package android.hardware.audio@2.0;
interface IAudio {
// 初始化音频设备
init(int32_t sampleRate, int32_t channels) generates (bool success);
// 播放音频数据
playAudio(vec<uint8_t> buffer) generates (int32_t bytesWritten);
};
- package:定义接口的命名空间,像 Java 的包名,@2.0 是版本号。
- interface:定义接口名,比如 IAudio。
- 方法:像 init () 和 playAudio (),generates 后面是返回值类型。
这代码的意思是:音频 HAL 提供两个功能,一个是初始化(传采样率和声道数,返回是否成功),一个是播放(传音频数据,返回写了多少字节)。
HIDL 咋用?
写好.hal 文件后,得靠工具把它变成代码。Google 提供了 hidl - gen,一键生成实现框架。比如上面那个 IAudio,跑一下 hidl - gen,会吐出这些文件:
- IAudio.h:C++ 接口的纯虚类,定义方法原型。
- BpAudio.h:客户端的代理(Proxy),负责发 IPC 请求。
- BnAudio.h:服务端的存根(Stub),等着实现逻辑。
- AudioAll.cpp:客户端和服务端的默认实现。
开发者拿到这些,只需要在 Stub 里填上具体逻辑就行。比如:
class AudioImpl : public IAudio {
public:
Return<bool> init(int32_t sampleRate, int32_t channels) override {
// 初始化硬件
int fd = open("/dev/snd/pcmC0D0p", O_RDWR);
ioctl(fd, SNDCTL_DSP_SPEED, &sampleRate);
ioctl(fd, SNDCTL_DSP_CHANNELS, &channels);
return fd >= 0;
}
Return<int32_t> playAudio(const hidl_vec<uint8_t>& buffer) override {
// 播放数据
return write(fd, buffer.data(), buffer.size());
}
private:
int fd = -1;
};
HIDL 的好处
为啥 HIDL 这么受欢迎?
- 省事:自动生成代码,少写一堆 IPC boilerplate。
- 清晰:接口定义一目了然,维护方便。
- 版本控制:@2.0 升级到 @2.1 时,老接口还能用,保证兼容性。
- 独立运行:HAL 模块可以作为独立服务跑,Framework 通过 Binder 访问,不用硬绑在一起。
实例:相机 HAL 的 HIDL
相机 HAL 的接口可能长这样:
package android.hardware.camera@2.1;
interface ICamera {
capture() generates (CameraFrame frame); // 拍照
setExposure(int32_t value); // 设置曝光
};
struct CameraFrame {
vec<uint8_t> data; // 图像数据
int32_t width; // 宽度
int32_t height; // 高度
};
上层调用 capture (),HAL 返回个 CameraFrame,里面装着拍好的照片数据。厂商可以在 setExposure () 里实现自己的曝光算法,完全不影响接口标准。
HIDL 就像 HAL 的 “外交官”,让上下层沟通顺畅,还给厂商留了发挥空间。
实现规范:HAL 开发的 “硬规矩”
HAL 不是随便写写就行的,Google 定了不少规矩,确保模块既统一又靠谱。这些规范主要藏在 hardware.h 头文件里,是每个 HAL 开发者必须啃的硬骨头。
核心数据结构
HAL 的实现绕不开这几个结构体:
- hw_module_t:模块的 “身份证”,记录 ID、名字、版本,还有方法表。
struct hw_module_t {
uint32_t tag = HAL_MODULE_INFO_TAG; // 固定值,标识HAL模块
uint16_t module_api_version; // 模块API版本
const char *id; // 比如"audio"
struct hw_module_methods_t *methods; // 方法指针
};
- hw_device_t:设备的 “操作手册”,定义怎么打开、关闭、操作硬件。
struct hw_device_t {
uint32_t tag = HAL_DEVICE_INFO_TAG;
struct hw_module_t *module; // 所属模块
int (*close)(struct hw_device_t*); // 关闭函数
};
开发要求
模块文件
每个 HAL 模块是个.so 文件,比如 audio.primary.qcom.so。得有个入口符号 HAL_MODULE_INFO_SYM,指向 hw_module_t 实例。
加载方式
用 hw_get_module () 动态加载,不能硬编码路径。系统会从 /system/lib/hw/ 或 /vendor/lib/hw/ 找。
接口规范
老式 HAL 用 C 结构体,新式 HAL 用 HIDL 定义接口。方法得线程安全,不能随便崩。
版本管理
module_api_version 得填对,比如 HARDWARE_MODULE_API_VERSION (2, 0) 表示 2.0 版。上层会检查版本,不匹配就拒绝加载。
实例:音频 HAL 规范
一个简单的音频 HAL 实现:
static struct hw_module_methods_t audio_module_methods = {
.open = audio_device_open
};
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HAL_MODULE_INFO_TAG,
.module_api_version = HARDWARE_MODULE_API_VERSION(1, 0),
.id = "audio",
.name = "Primary Audio HAL",
.methods = &audio_module_methods
};
int audio_device_open(const struct hw_module_t* module, const char* id,
struct hw_device_t** device) {
struct audio_device* dev = malloc(sizeof(struct audio_device));
dev->common.tag = HAL_DEVICE_INFO_TAG;
dev->common.module = (struct hw_module_t*)module;
dev->common.close = audio_device_close;
*device = &dev->common;
return 0;
}
HAL_MODULE_INFO_SYM 是入口,系统加载时认它。audio_device_open 创建设备实例,上层用它操作硬件。
为啥这么严?
这些规范保证了:
- 一致性:不同厂商的 HAL 都能被 Framework 认出来。
- 可维护性:代码结构统一,接手不头疼。
- 兼容性:老模块在新系统里还能跑。
HAL 的实现规范就像盖房子的图纸,照着来就不会歪。
HAL 开发流程
讲完了 HAL 的原理和组件,咱们换个视角,看看怎么亲手写一个 HAL。从环境搭建到接口设计,再到模块实现和调试,这是个完整的实战过程。咱们一步步来,别急,慢慢啃。
环境搭建:先把工具备齐
想开发 HAL,得先把家当准备好。HAL 开发不像写 App 那么简单,得深入系统底层,工具得齐全。
基本步骤
- 装 IDE:Android Studio 是首选,带 Gradle 和 AVD(虚拟设备),调试方便。或者用 VS Code,轻量但得自己配环境。
- 下源码:从 AOSP(Android Open Source Project)官网拉代码:
repo init -u https://android.googlesource.com/platform/manifest
repo sync
国内慢的话,可以用清华镜像,速度飞起。
3. 配编译环境:
- JDK:装个 OpenJDK 11,Android 14 用这个。
- NDK:HAL 用 C/C++ 写,NDK 得装,跑 ndk - build 编译。
- 工具链:Python、Git、Make 这些得有,Ubuntu 上跑 sudo apt - get install build - essential 全装上。
- 读文档:Google 的 HAL 开发指南(source.android.com/devices/hal)得看,讲得很细。还有 hardware/libhardware/include/hardware/ 里的头文件,核心规范都在这儿。
实例环境
假设你用 Ubuntu 20.04:
sudo apt update
sudo apt install openjdk - 11 - jdk git python3 repo
wget https://dl.google.com/android/ndk/android - ndk - r25b - linux - x86_64.zip
unzip android - ndk - r25b - linux - x86_64.zip - d ~/ndk
export PATH=$PATH:~/ndk/android - ndk - r25b
源码拉下来后,跑 source build/envsetup.sh 初始化环境,再 lunch 选个目标(比如 aosp_arm64 - eng),就齐活了。
小贴士
- 硬盘得留够空间,AOSP 源码加编译产物轻松上百 G。
- 网速慢就用代理,别卡在 repo sync 上。
环境搭好,就像开了个工作坊,接下来就能动手设计接口了。
接口设计:画好 HAL 的 “蓝图”
接口设计是 HAL 开发的起点,决定了模块长啥样、怎么用。HIDL 是主角,咱们拿它开刀。
设计原则
- 面向对象:把硬件当对象,方法清晰。
- 简洁:别塞太多功能,够用就好。
- 版本化:留升级空间,别一改就崩。
实战:设计音频 HAL 接口
假设我们要写个简单的音频 HAL,接口可能是:
package android.hardware.simpleaudio@1.0;
interface ISimpleAudio {
// 初始化音频,设置采样率和声道
init(int32_t sampleRate, int32_t channels) generates (bool success);
// 播放一段音频数据
play(vec<uint8_t> buffer) generates (int32_t bytesPlayed);
// 停止播放
stop() generates (bool success);
};
- init:初始化硬件,传参数,返回是否成功。
- play:播放 PCM 数据,返回播放的字节数。
- stop:停下来,清空状态。
生成代码
写好后,跑 hidl - gen:
hidl - gen - o output - L c++ - impl - r android.hardware:hardware/interfaces \
android.hardware.simpleaudio@1.0
生成的文件会出现在 output/android/hardware/simpleaudio/1.0 / 里,包括:
- ISimpleAudio.h:接口定义。
- SimpleAudio.cpp:默认实现,得自己改。
实现接口
在 SimpleAudio.cpp 里填逻辑:
Return<bool> SimpleAudio::init(int32_t sampleRate, int32_t channels) {
fd_ = open("/dev/snd/pcmC0D0p", O_RDWR);
if (fd_ < 0) return false;
ioctl(fd_, SNDCTL_DSP_SPEED, &sampleRate);
ioctl(fd_, SNDCTL_DSP_CHANNELS, &channels);
return true;
}
Return<int32_t> SimpleAudio::play(const hidl_vec<uint8_t>& buffer) {
if (fd_ < 0) return -1;
return write(fd_, buffer.data(), buffer.size());
}
用 ioctl 配置硬件,write 推送数据,简单粗暴。
模块实现:从蓝图到实物
接口设计好了,就像画了个图纸,接下来得把 HAL 模块真刀真枪地写出来。这部分是 HAL 开发的 “造房子” 阶段,得把抽象的接口变成能跑的代码。
实现步骤
- 定义接口:上次咱们写了 android.hardware.simpleaudio@1.0 的 HIDL 接口
- 编写实现类:在生成的
SimpleAudio.cpp
文件基础上,完成所有接口方法的实现。除了之前的init
和play
方法,还需要实现stop
方法。以下是完整的实现示例:
#include <hidl/HidlSupport.h>
#include <hidl/HidlTransportSupport.h>
#include <android/hardware/simpleaudio/1.0/ISimpleAudio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/soundcard.h>
using android::hardware::simpleaudio::V1_0::ISimpleAudio;
using android::hardware::Return;
using android::hardware::Void;
using android::sp;
class SimpleAudio : public ISimpleAudio {
public:
SimpleAudio() : fd_(-1) {}
Return<bool> init(int32_t sampleRate, int32_t channels) override {
fd_ = open("/dev/snd/pcmC0D0p", O_RDWR);
if (fd_ < 0) return false;
ioctl(fd_, SNDCTL_DSP_SPEED, &sampleRate);
ioctl(fd_, SNDCTL_DSP_CHANNELS, &channels);
return true;
}
Return<int32_t> play(const hidl_vec<uint8_t>& buffer) override {
if (fd_ < 0) return -1;
return write(fd_, buffer.data(), buffer.size());
}
Return<bool> stop() override {
if (fd_ >= 0) {
close(fd_);
fd_ = -1;
return true;
}
return false;
}
private:
int fd_;
};
- 创建服务实例:在
main
函数中创建SimpleAudio
类的实例,并将其注册为服务。这样,其他组件就可以通过 HIDL 接口来访问这个 HAL 模块。
int main() {
android::hardware::configureRpcThreadpool(1, true /*callerWillJoin*/);
sp<ISimpleAudio> service = new SimpleAudio();
if (service->registerAsService() != android::OK) {
return 1;
}
android::hardware::joinRpcThreadpool();
return 0;
}
编译和部署
- 编写 Android.mk 或 Android.bp 文件:根据你的开发环境和习惯,编写编译脚本。以下是一个简单的
Android.mk
示例:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := android.hardware.simpleaudio@1.0-service
LOCAL_INIT_RC := android.hardware.simpleaudio@1.0-service.rc
LOCAL_SRC_FILES := \
SimpleAudio.cpp \
main.cpp
LOCAL_SHARED_LIBRARIES := \
libhidlbase \
libhidltransport \
libutils \
liblog
include $(BUILD_EXECUTABLE)
- 编译模块:在 AOSP 环境中,使用
mm
或m
命令来编译这个 HAL 模块。
source build/envsetup.sh
lunch <your_target>
mm
- 部署到设备:将编译好的二进制文件推送到设备上,并确保其具有执行权限。
adb push out/target/product/<your_device>/system/bin/android.hardware.simpleaudio@1.0-service /system/bin/
adb shell chmod +x /system/bin/android.hardware.simpleaudio@1.0-service
调试和测试:确保 HAL 模块正常工作
调试方法
- 日志输出:在代码中添加日志输出,使用 Android 的
__android_log_print
函数来记录关键信息。例如:
#include <android/log.h>
#define LOG_TAG "SimpleAudioHAL"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
// 在方法中添加日志
Return<bool> init(int32_t sampleRate, int32_t channels) override {
fd_ = open("/dev/snd/pcmC0D0p", O_RDWR);
if (fd_ < 0) {
LOGD("Failed to open audio device: %d", errno);
return false;
}
ioctl(fd_, SNDCTL_DSP_SPEED, &sampleRate);
ioctl(fd_, SNDCTL_DSP_CHANNELS, &channels);
return true;
}
- 使用调试工具:可以使用
adb logcat
来查看设备的日志信息,帮助定位问题。同时,也可以使用 GDB 等调试工具进行更深入的调试。
测试方法
- 编写测试用例:使用 Google 的 CTS(Compatibility Test Suite)或 VTS(Vendor Test Suite)来编写测试用例,确保 HAL 模块的功能符合规范。以下是一个简单的测试用例示例:
#include <android/hardware/simpleaudio/1.0/ISimpleAudio.h>
#include <hidl/GtestPrinter.h>
#include <hidl/ServiceManagement.h>
#include <gtest/gtest.h>
using android::hardware::simpleaudio::V1_0::ISimpleAudio;
using android::hardware::Return;
using android::sp;
class SimpleAudioTest : public testing::Test {
protected:
virtual void SetUp() override {
audioService = ISimpleAudio::getService();
ASSERT_NE(audioService, nullptr);
}
sp<ISimpleAudio> audioService;
};
TEST_F(SimpleAudioTest, InitTest) {
bool success;
Return<bool> ret = audioService->init(44100, 2);
ret >> success;
EXPECT_TRUE(success);
}
TEST_F(SimpleAudioTest, PlayTest) {
android::hardware::hidl_vec<uint8_t> buffer;
buffer.resize(1024);
int32_t bytesPlayed;
Return<int32_t> ret = audioService->play(buffer);
ret >> bytesPlayed;
EXPECT_GT(bytesPlayed, 0);
}
TEST_F(SimpleAudioTest, StopTest) {
bool success;
Return<bool> ret = audioService->stop();
ret >> success;
EXPECT_TRUE(success);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
android::hardware::addGtestPrinter();
return RUN_ALL_TESTS();
}
- 运行测试用例:在设备上运行测试用例,检查测试结果。如果测试失败,根据日志信息进行调试和修复。
通过以上的开发流程,你就可以完成一个简单的 HAL 模块的开发、调试和测试工作。在实际开发中,还需要考虑更多的因素,如性能优化、兼容性等。