2、硬件抽象层(HAL)解析:Gralloc模块详解、HWC接口定义与版本演进、FB设备与DRM/KMS、显示设备枚举与初始化

好,我们直接进入正题。硬件抽象层,也就是HAL,是Android显示系统中承上启下的关键一层。说白了,它把上层的复杂逻辑和底层的硬件驱动隔离开。你上层应用不用管我用的什么屏幕、什么GPU,只要按标准接口调用就行。今天我就带你把这层扒开,看看里面到底是怎么运作的。

2.1 Gralloc模块详解

Gralloc,全称是Graphics Memory Allocator,图形内存分配器。它的核心任务就两个:分配图形缓冲区,以及管理这些缓冲区的访问权限。我刚开始接触时觉得这玩意儿不就是个内存分配吗?后来踩了坑才发现,这里面的门道深着呢。

核心职责:

  • 分配图形缓冲区(比如你屏幕上的每一帧画面)
  • 管理CPU和GPU对缓冲区的访问同步
  • 处理不同硬件模块(如显示控制器、GPU、视频编解码器)之间的内存共享

Gralloc的接口定义在hardware/libhardware/include/hardware/gralloc.h中。嗯,这里要注意,从Android 8.0开始,Google引入了Gralloc HAL 1.xGralloc HAL 2.x两个版本。我个人习惯把1.x叫做“老派做法”,2.x叫做“新派做法”。

2.1.1 Gralloc HAL 1.x

这个版本比较传统。它通过一个gralloc_module_t结构体暴露接口,里面包含了allocfreelockunlock这些函数指针。我当年在移植一款老平台时,用的就是这套接口。

typedef struct gralloc_module_t {
    struct hw_module_t common;
    int (*alloc)(struct gralloc_module_t const* module,
                 int w, int h, int format, int usage,
                 buffer_handle_t* handle, int* stride);
    int (*free)(struct gralloc_module_t const* module,
                buffer_handle_t handle);
    int (*lock)(struct gralloc_module_t const* module,
                buffer_handle_t handle, int usage,
                int l, int t, int w, int h,
                void** vaddr);
    int (*unlock)(struct gralloc_module_t const* module,
                  buffer_handle_t handle);
} gralloc_module_t;

你看,alloc函数需要传入宽、高、像素格式、使用标志。其中usage这个参数特别关键,它告诉Gralloc这块缓冲区要给谁用——是给CPU读写,还是给GPU渲染,还是给显示控制器直接扫描输出。不同的usage,底层分配的内存类型完全不同。

避坑指南:我曾经在一个项目里,因为usage标志没设置对,导致GPU渲染完的画面,显示控制器读出来全是花屏。查了两天才发现,是缓冲区没有分配在连续物理内存上。所以,usage一定要和实际使用场景匹配。

2.1.2 Gralloc HAL 2.x

到了2.x版本,接口变了。它不再用函数指针结构体,而是用HIDL接口定义。你想想看,这样更灵活,也更容易做版本兼容。2.x引入了IAllocatorIMapper两个核心接口。

  • IAllocator:负责分配和释放缓冲区。它返回一个BufferDescriptor,描述缓冲区的属性。
  • IMapper:负责缓冲区的映射、锁定、缓存同步等操作。

我个人觉得2.x最大的改进是解耦了分配和映射。在1.x里,alloclock是绑在一起的,你分配完还得自己操心怎么映射。2.x把这两件事分开了,逻辑更清晰。

2.2 HWC接口定义与版本演进

HWC,全称Hardware Composer,硬件合成器。它的工作就是把多个图层合成一帧画面,然后送出去显示。为什么需要它?因为纯用GPU合成太费电了,硬件合成器效率高得多。

HWC的版本演进,我大致梳理了一下:

版本 核心变化 我的评价
HWC 1.0 基本合成功能,支持overlay 够用,但太死板
HWC 1.1 增加VSync控制 终于能管刷新率了
HWC 1.2 支持虚拟显示、动态合成策略 灵活性大增
HWC 1.3 增加色彩管理、HDR支持 画质开始被重视
HWC 1.4 增加显示属性查询、帧率控制 更细粒度的控制
HWC 1.5 增加客户端合成提示 给GPU留了后路
HWC 2.0 全面重构,使用HIDL接口 这才是现代HWC的样子

为什么会有这么多版本?说白了,显示需求在变。从最早的简单UI合成,到现在的HDR视频、多窗口、虚拟现实,HWC必须跟着进化。我记得在HWC 1.2刚出来时,我还在想“虚拟显示有什么用”?后来做投屏功能时,才发现这东西太香了。

HWC 2.0的核心接口:

// 来自 hardware/interfaces/graphics/composer/2.1/IComposer.hal
interface IComposer {
    createDisplay(...);
    destroyDisplay(...);
    setClientTarget(...);
    setOutputBuffer(...);
    validateDisplay(...);
    presentDisplay(...);
    // ... 还有很多
};

HWC 2.0把合成流程分成了validatepresent两步。先validate,看看当前图层能不能用硬件合成。如果不行,就告诉SurfaceFlinger:“兄弟,这几个图层你拿GPU合成吧,剩下的我来。”这种协作模式,既保证了效率,又保证了兼容性。

2.3 FB设备与DRM/KMS

讲到底层驱动了。FB设备,就是/dev/fb0,是Linux传统的帧缓冲设备。DRM/KMS,是Direct Rendering Manager / Kernel Mode Setting,现代Linux显示子系统。

FB设备简单粗暴。你往/dev/fb0里写像素数据,它就能显示出来。但缺点也很明显:不支持多图层、不支持VSync精确控制、扩展性差。我早期做嵌入式开发时,用的就是FB设备,那时候觉得“能显示就行”。

DRM/KMS就高级多了。它提供了:

  • CRTC:显示控制器,负责生成时序信号
  • Encoder:编码器,把数字信号转成物理接口信号(如HDMI、DP)
  • Connector:连接器,代表物理接口
  • Plane:图层,可以叠加多个图层
  • Framebuffer:内存中的图像数据

你想想看,DRM/KMS把显示链路拆成了这么多个组件,每个都可以独立配置。这给上层提供了极大的灵活性。比如你想实现双屏异显,在DRM下就是创建两个CRTC,分别绑定不同的Connector就行了。

个人经验:我在移植一个高通平台时,发现它的显示驱动同时支持FB和DRM。但FB设备只能显示一个图层,而DRM可以支持4个overlay plane。所以,只要硬件支持,我建议直接用DRM/KMS。FB设备,说白了就是历史遗留产物。

2.4 显示设备枚举与初始化

好了,现在我们要把整个显示链路串起来。系统启动时,显示设备是怎么被发现的?

流程大致是这样的:

  1. 内核启动,DRM驱动注册,枚举所有CRTC、Encoder、Connector、Plane
  2. HAL层(Gralloc和HWC)加载,查询DRM设备信息
  3. SurfaceFlinger启动,调用HWC的createDisplay创建逻辑显示设备
  4. HWC把逻辑显示设备映射到物理Connector上
  5. 开始接收帧数据,合成并输出

这里有个关键点:显示设备的枚举顺序。我曾经遇到过一个bug,系统有两个HDMI接口,但每次开机,主屏幕和副屏幕的分配顺序都不一样。后来发现是DRM驱动枚举Connector的顺序不固定。解决方案是在HWC层加一个display priority配置,根据Connector的物理ID来固定顺序。

警告:显示设备初始化时,一定要处理好热插拔场景。比如HDMI线拔了又插,系统要能重新枚举设备。如果处理不好,就会出现“插上显示器没反应”的情况。我建议在HWC中注册一个热插拔回调,一旦检测到Connector状态变化,立即通知SurfaceFlinger重新配置。

嗯,到这里,硬件抽象层的核心内容就讲完了。Gralloc管内存,HWC管合成,DRM/KMS管驱动,显示设备枚举管发现。这四个模块配合好了,你的显示系统才能稳定高效地跑起来。下一章,我们会深入SurfaceFlinger,看看它怎么调度这些底层模块的。