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.x和Gralloc HAL 2.x两个版本。我个人习惯把1.x叫做“老派做法”,2.x叫做“新派做法”。
2.1.1 Gralloc HAL 1.x
这个版本比较传统。它通过一个gralloc_module_t结构体暴露接口,里面包含了alloc、free、lock、unlock这些函数指针。我当年在移植一款老平台时,用的就是这套接口。
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引入了IAllocator和IMapper两个核心接口。
- IAllocator:负责分配和释放缓冲区。它返回一个
BufferDescriptor,描述缓冲区的属性。 - IMapper:负责缓冲区的映射、锁定、缓存同步等操作。
我个人觉得2.x最大的改进是解耦了分配和映射。在1.x里,alloc和lock是绑在一起的,你分配完还得自己操心怎么映射。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把合成流程分成了validate和present两步。先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 显示设备枚举与初始化
好了,现在我们要把整个显示链路串起来。系统启动时,显示设备是怎么被发现的?
流程大致是这样的:
- 内核启动,DRM驱动注册,枚举所有CRTC、Encoder、Connector、Plane
- HAL层(Gralloc和HWC)加载,查询DRM设备信息
- SurfaceFlinger启动,调用HWC的
createDisplay创建逻辑显示设备 - HWC把逻辑显示设备映射到物理Connector上
- 开始接收帧数据,合成并输出
这里有个关键点:显示设备的枚举顺序。我曾经遇到过一个bug,系统有两个HDMI接口,但每次开机,主屏幕和副屏幕的分配顺序都不一样。后来发现是DRM驱动枚举Connector的顺序不固定。解决方案是在HWC层加一个display priority配置,根据Connector的物理ID来固定顺序。
警告:显示设备初始化时,一定要处理好热插拔场景。比如HDMI线拔了又插,系统要能重新枚举设备。如果处理不好,就会出现“插上显示器没反应”的情况。我建议在HWC中注册一个热插拔回调,一旦检测到Connector状态变化,立即通知SurfaceFlinger重新配置。
嗯,到这里,硬件抽象层的核心内容就讲完了。Gralloc管内存,HWC管合成,DRM/KMS管驱动,显示设备枚举管发现。这四个模块配合好了,你的显示系统才能稳定高效地跑起来。下一章,我们会深入SurfaceFlinger,看看它怎么调度这些底层模块的。