很多人问我,Android显示驱动到底该怎么学。网上搜“Android 显示驱动”,出来的东西要么是零散的寄存器操作,要么是直接甩一份 vendor 代码让你自己看,真正能把“从0到1”这条路讲清楚的很少。这篇文章我会按自己这些年做底层驱动和系统移植的实际经验,把 Android 显示驱动的完整学习路线拆开讲:先看懂显示链路,再补内核基础,然后依次搞定设备树、显示控制器、MIPI DSI 面板驱动,最后打通 HWC 和 SurfaceFlinger。内容会偏向 Linux 内核 DRM/KMS 框架和 Android HAL 层,适合刚接触底层显示、想系统入门的人,也适合做 Framebuffer 驱动想往新架构转的同学。读完你至少能知道每一阶段该学什么、为什么学、怎么验证自己学懂了。
1. 先从整体看懂显示链路:Android显示驱动到底在驱动什么
1.1 从一条像素到屏幕的点亮过程
我见过很多新手一开始就抱着数据手册啃寄存器,结果越看越糊涂。原因很简单,显示驱动不是“点亮一个灯”,而是一条完整的像素生产流水线。你只要把 Android 显示链路想象成一条物流线就很好理解:App 界面是货物,SurfaceFlinger 是分拣中心,HWC 是调度员,KMS/DRM 是运输干线,最后面板控制器是收货站点。
具体到一次屏幕刷新:App 通过 HWUI 把 UI 绘制到 BufferQueue 里的 GraphicBuffer,SurfaceFlinger 把多个图层合成好,然后交给 HWC(Hardware Composer)决定哪些图层可以由显示硬件直接合成,哪些需要 GPU 合成。合成结果最终会被提交给内核里的 DRM/KMS 驱动,驱动把内存中的 Framebuffer 地址配置到显示控制器的图层寄存器,经 Video Pipe 或 Command Pipe 把像素数据通过 MIPI DSI、LVDS、eDP 或 HDMI 接口发送到面板,面板时序控制器(TCON)再把数据转换成屏幕上的亮度变化。
所以,所谓的“显示驱动”其实横跨了 Android 用户空间的 HWC、SurfaceFlinger,以及内核空间的 DRM、VOP/Display Controller、DSI/LVDS 控制器、背光驱动和电源管理。你在学习时如果只盯着某一层,很容易陷入“寄存器改对了但屏幕就是没反应”的窘境。很多工程师跳来跳去,根本原因是脑中缺少这条链路的总览图。
1.2 显示驱动开发者日常面对的四个层次
从我的经验来看,可以把 Android 显示驱动的日常开发工作分成四个层次,你可以用它来对照自己的位置:
- 第一层:面板驱动与背光。负责初始化屏幕模组(上电时序、初始化序列)、控制背光亮度、处理屏幕的睡眠唤醒。这一层和硬件关系最紧,经常要对着原理图量电压、看波形。
- 第二层:显示控制器驱动。在 DRM/KMS 框架里就是 CRTC、Encoder、Connector、Plane 的注册与回调实现。这一层决定图像怎么从内存取出来、叠加、缩放,最后变成某种接口协议上的数据流。
- 第三层:GPU 合成与 Framebuffer。主要涉及 Ion/DMA-BUF 内存管理,以及 SurfaceFlinger 向内核提交显示缓冲区的路径。小屏设备常走 GPU 合成,效率瓶颈很容易出现在这里。
- 第四层:HWC 与系统合成策略。严格来说 HWC 是 HAL 层,不算内核驱动,但它直接决定“硬件合成还是 GPU 合成”,在低端平台上写不好 HWC,CPU 占用率和功耗都会很难看。
这四个层次不是要你全部精通才动手,但必须知道彼此之间的接口长什么样。内核里的 DTS、DRM 驱动和用户空间的 HWC 实现、Gralloc 分配策略、SurfaceFlinger 的合成决策,它们之间的信息流是相互咬合的。我给新人的建议是:先以“能把一个 1080p 的屏幕从 uboot 到 Android 桌面完整点亮”为目标,逐层打通,再回头分析哪一层是关键瓶颈。
2. 学习路线第一阶段:打好内核与Linux图形基础
2.1 必修的内核知识清单
很多做应用开发的同学想转显示驱动,第一个拦路虎就是内核基础不熟。显示驱动本质上是一个 Linux 字符设备驱动加上一堆框架回调,所以下面的内核知识不是选修,而是必修:
- Linux 字符设备驱动模型:file_operations、miscdevice、platform_driver 的 probe 流程。
- 内核内存管理基础:物理内存分配、虚拟地址映射、DMA 一致性映射、IOMMU/SMMU 的基本概念。
- 中断与并发:显示控制器常用中断来通知帧完成(VBlank),所以 tasklet、workqueue、spinlock、mutex 这些并发原语必须能看懂。
- DMA-BUF 与 Dma-Buf Heap:Android 的 GraphicBuffer 最终就是通过 DMA-BUF 把 fd 传给内核,不理解这个就看不懂 framebuffer 的 fd 是怎么来的。
- 设备树(DT)的基础语法:compatible、reg、interrupt、clock、pinctrl、power-supply、backlight 等属性的含义。
我建议学习动作不只是看书,而是直接拿一块常见的开发板内核源码,在drivers/gpu/drm/或drivers/video/fbdev/下找一个简单的驱动从头到尾读一遍。把驱动里注册的 platform_driver、probe 里读取的资源、remove 里释放的资源,对照数据手册和 DTS 文件逐一对上号,这一步做完,你对“驱动”的理解会立刻不一样。
这里有一个常见的心理误区:总觉得要先读完一整本内核编程的书才能动手。其实没必要,显示驱动涉及的内核 API 相对固定,你只要在遇到不懂的 API 时去查include/linux/里的头文件和内核文档,理解它在当前路径下的用途,效率远比按章节啃书高。当然,前提是你至少得会看kmalloc/kzalloc/devm_kzalloc这类基础函数,否则代码都读不连贯。
2.2 从FrameBuffer到DRM/KMS的演进逻辑
老一批工程师接触 Android 显示驱动时,最先认识的通常是/dev/fb0,也就是 FrameBuffer 设备。Android 早期版本(2.x 到 4.x)大量使用 fbdev 接口:内核注册一个fb_info,用户空间通过mmap映射显存,再通过ioctl设置分辨率、位深、颜色格式。fbdev 接口简单直接,调试也容易,但越到后面越发现它的问题:它把显示控制器抽象得太粗,对“多层合成”“旋转”“缩放”“带宽管理”这些现代显示屏的常见需求支持很差。
于是 DRM/KMS(Direct Rendering Manager / Kernel Mode Setting)逐渐成为主流。DRM 架构里有几个核心对象:DRM Device、Plane、CRTC、Encoder、Connector、Framebuffer。你可以这么理解:
- Plane:相当于硬件图层,图像最终是要贴到某一个 Plane 上的。
- CRTC:显示控制器的主要输出通道,负责从内存读像素、做时序生成。
- Encoder:把 CRTC 输出的并行像素信号编码成某种接口协议。
- Connector:物理连接的抽象,比如 HDMI 口、eDP 口、DSI 口,它上面挂了面板和检测引脚。
- Framebuffer:一块具体的内存缓冲区描述,包含 pitch、offset、格式、尺寸。
这套抽象比 fbdev 细得多,而且最重要的是,它允许用户空间通过 atomic API 一次性提交多个 Plane、CRTC、Encoder 的状态,保证合成结果是“一帧”的,不会出现撕裂画面。现在 Android 的 HWC 和 SurfaceFlinger 已经完全基于 DRM 的 modset 接口来提交图层。
所以我把“从 fbdev 到 DRM/KMS”的演进逻辑放在第一阶段的核心位置,不是因为它考试常考,而是因为你在看老代码和移植新内核时,经常会同时遇到这两套东西。看得懂 fbdev,又能说出为什么要被 DRM 取代,你才能理解代码里各种 ifdef 和兼容逻辑存在的意义。我特别建议初学者去看 DRM 内核文档里关于 atomic mode setting 的说明,它把 commit、state、validate 这套设计的思路讲得很清楚,是理解现代显示驱动的钥匙。
3. 学习路线第二阶段:读懂设备树与显示控制器驱动
3.1 设备树里显示节点到底配置了些什么
到了实际移植一块屏幕或一个新平台时,设备树(DTS)就是你的第一战场。很多新人打开 DTS 里 display 相关的节点,看到几十个属性直接头大。其实把它们分类以后就会发现,显示节点的信息无外乎四类。
第一类是“连通性”,即这个显示控制器接在哪个总线、使用哪几个时钟、哪几个中断、GIC 的哪一个 SPI。比如clocks属性里通常会有dclk(像素时钟)、aclk(总线时钟)、hclk(AHB 时钟),它们的频率直接决定屏幕刷新率能不能稳定跑到 60Hz。第二类是“内存和 IOMMU”,比如iommus指向显示控制器使用的 SMMU 设备,表示显存访问需要经过地址转换;interconnects则描述显示控制器的内存带宽路径,在某些平台上带宽不够时,这里配置错了会导致高分辨率下屏幕闪烁。
第三类是“输出接口”,这部分一般会嵌套一个子节点,描述连接的 panel 或者 HDMI 控制器。DSI 接口的节点里通常会有>