4、硬件抽象层(HAL):HAL设计原则、音视频驱动封装、输入设备抽象、硬件加速接口实现

好,咱们进入第四章。硬件抽象层,简称HAL。

说实话,HAL这东西,在机顶盒中间件里属于那种「平时没人夸,出事全怪它」的角色。你想想看,上层应用跑不动了,UI卡了,视频花屏了,最后追查下来,十有八九是HAL层没兜住。

我做了这么多年机顶盒,见过太多团队把HAL当成「随便封装一下」的活儿。结果呢?换一颗芯片,整个中间件要重写一半。嗯,今天咱们就来聊聊,怎么把HAL做扎实。

4.1 HAL设计原则:别让上层知道底层长什么样

HAL的核心目标就一句话:隔离变化

底层芯片换了,驱动接口变了,上层应用代码一行都不应该改。这是底线。

我个人习惯,设计HAL时遵循三个原则:

  • 单一职责:一个HAL模块只干一件事。比如视频输出HAL只管投屏,别掺和音频。
  • 接口稳定:对外暴露的API一旦定下来,就不要轻易改。我见过一个项目,HAL接口改了三次,上层代码跟着重构了三次,那叫一个酸爽。
  • 可替换性:底层实现可以随时换。今天用海思,明天换晶晨,HAL层接口不变。

核心原则:上层不感知底层细节。底层不依赖上层逻辑。

举个例子,视频播放的HAL接口,我通常会这样设计:

// hal_video.h
typedef struct {
    int (*init)(void);
    int (*play)(const char *url);
    int (*stop)(void);
    int (*set_window)(int x, int y, int w, int h);
} hal_video_ops_t;

int hal_video_register(hal_video_ops_t *ops);

你看,上层只需要调用 hal_video_play(),至于底层是用硬解还是软解,是走HDMI还是CVBS,上层完全不用管。这就是抽象的价值。

4.2 音视频驱动封装:把「脏活」包起来

音视频驱动,是HAL层里最头疼的部分。为什么?因为每家芯片的驱动接口都不一样,而且文档经常写得不清不楚。

我记得有一次,对接某款芯片的音频驱动,它的初始化函数居然叫 audio_start(),但实际功能是初始化。你说坑不坑?

所以,封装音视频驱动时,我建议做三件事:

  1. 统一调用模型:不管底层怎么折腾,上层看到的都是 open/close/start/stop 这一套。
  2. 错误码归一化:底层返回-1、-2、0xFF,统统映射成HAL层定义的统一错误码。
  3. 状态机管理:音视频设备是有状态的。初始化、空闲、播放中、暂停、错误……每个状态能做什么操作,必须定义清楚。

小技巧:我习惯在HAL层加一个「驱动健康检查」接口。每隔几秒ping一下底层驱动,如果没响应,主动上报。这个习惯救过我两次,一次是HDMI热插拔导致驱动死锁,另一次是音频DSP固件崩溃。

音频驱动的封装示例:

// hal_audio.c
static int audio_dev_fd = -1;

int hal_audio_init(int sample_rate, int channels) {
    audio_dev_fd = open("/dev/audio", O_RDWR);
    if (audio_dev_fd < 0) {
        return HAL_ERR_DEVICE_NOT_FOUND;
    }
    // 设置采样率、声道数
    ioctl(audio_dev_fd, AUDIO_SET_SAMPLE_RATE, sample_rate);
    ioctl(audio_dev_fd, AUDIO_SET_CHANNELS, channels);
    return HAL_OK;
}

int hal_audio_play(const uint8_t *data, int len) {
    if (audio_dev_fd < 0) return HAL_ERR_NOT_INIT;
    int ret = write(audio_dev_fd, data, len);
    return (ret == len) ? HAL_OK : HAL_ERR_WRITE_FAILED;
}

4.3 输入设备抽象:遥控器、键盘、鼠标,一视同仁

机顶盒的输入设备五花八门。红外遥控器、蓝牙遥控器、USB键盘、空中鼠标、甚至手机投屏控制……

你想想看,如果上层应用要分别处理每种设备的输入事件,那代码得写成什么样?

所以,HAL层要做的事就是:把所有输入设备统一成一种事件

我通常的做法是定义一个标准输入事件结构体:

// hal_input.h
typedef enum {
    KEY_PRESS,
    KEY_RELEASE,
    KEY_REPEAT
} hal_key_event_t;

typedef struct {
    int key_code;       // 统一键值
    hal_key_event_t event;
    uint64_t timestamp; // 时间戳
} hal_input_event_t;

// 上层注册回调
typedef void (*hal_input_callback_t)(hal_input_event_t *evt);
int hal_input_register_callback(hal_input_callback_t cb);

底层驱动上报的是原始键值,HAL层负责映射成统一键值。比如红外遥控器的0x45映射成 KEY_VOLUME_UP,蓝牙遥控器的0x01也映射成 KEY_VOLUME_UP

注意:键值映射表一定要做成可配置的。我曾经在一个项目里把映射表写死在代码里,结果客户换了遥控器,我不得不重新编译整个固件。从那以后,我都是用配置文件或者数据库来管理键值映射。

输入设备抽象还有一个容易被忽略的点:多点触控和手势。现在很多机顶盒支持触摸板或者空中鼠标,HAL层需要把触摸坐标、滑动方向、点击手势都转换成标准事件。嗯,这块细节很多,咱们后面有专门章节讲。

4.4 硬件加速接口实现:把GPU和VPU用起来

机顶盒的硬件加速,主要分两块:GPU(图形加速)和VPU(视频编解码加速)。

说白了,没有硬件加速,机顶盒就是个高级计算器。UI渲染靠CPU?4K视频软解?想都别想。

HAL层在硬件加速上的职责,是提供一套统一的加速接口,让上层中间件和App能方便地调用底层硬件能力。

以视频解码为例,HAL层通常需要封装:

  • 解码器初始化(指定编码格式、分辨率)
  • 码流输入(喂数据)
  • 解码帧输出(拿到YUV或RGB数据)
  • 显示控制(设置显示窗口、缩放、旋转)

代码示例:

// hal_vpu.h
typedef struct {
    int codec_type;   // H264, H265, AVS2...
    int width;
    int height;
    int bitrate;
} hal_vpu_config_t;

typedef void* hal_vpu_handle_t;

hal_vpu_handle_t hal_vpu_init(hal_vpu_config_t *cfg);
int hal_vpu_decode(hal_vpu_handle_t handle, uint8_t *stream, int len);
int hal_vpu_get_frame(hal_vpu_handle_t handle, uint8_t **yuv, int *stride);
int hal_vpu_release_frame(hal_vpu_handle_t handle);
int hal_vpu_deinit(hal_vpu_handle_t handle);

GPU加速方面,HAL层主要提供OpenGL ES或者Vulkan的底层封装。不过说实话,很多机顶盒的GPU驱动本身就支持标准API,HAL层更多是做适配和性能调优。

经验之谈:硬件加速接口最容易出问题的不是功能,而是内存管理。解码器输出的YUV数据、GPU渲染的帧缓冲,都是大块内存。如果HAL层不做好内存池管理和零拷贝机制,性能会一塌糊涂。

我曾经遇到一个案例:4K视频播放时,每解码一帧都要从VPU拷贝到CPU内存,再从CPU内存拷贝到GPU显存。结果呢?4K@30fps都跑不满,卡成PPT。后来改成DMA直接传输,零拷贝,瞬间流畅了。

所以,设计硬件加速接口时,一定要把「零拷贝」作为默认选项,而不是优化选项。

小结

这一章咱们聊了HAL层的四个核心模块:设计原则、音视频驱动封装、输入设备抽象、硬件加速接口。

说白了,HAL层就是中间件的「翻译官」和「防火墙」。翻译官负责把底层芯片的方言翻译成标准普通话,防火墙负责把底层的变化隔离在上层之外。

下一章,咱们会深入中间件的核心——播放器引擎。到时候再聊。