做Android驱动这几年,一直觉得Android显示驱动是最值得单独拿出来啃的一块硬骨头。别的模块比如Wi-Fi、蓝牙、Sensor,边界相对清晰,出了问题查起来有明确的方向;显示不一样,它是从内核一路捅到应用层的完整链路——Kernel侧的DRM/KMS、HAL层的HWC合成、Framework里的SurfaceFlinger、BufferQueue、Vsync信号,任何一个环节掉链子,用户感受到的就是黑屏、闪屏、掉帧、亮度失灵这种最直接的体验。这篇文章是系列里的第8篇,我不打算讲那种“把代码贴一遍”的保姆级教程,而是把我自己从0到1摸出来的学习路线、踩过的坑、以及能直接拿去用的排查方法整理出来,给准备入坑MTK/QCOM平台显示驱动、或者正卡在某个显示问题上的同学一份参考。看完你会知道:先学什么、后学什么、哪些概念必须抠到寄存器级,哪些用到再查就行。
1. 为什么显示驱动值得单独立一条学习路线
1.1 显示是一条用户能直接看见的完整链路
你随手在手机上刷一个页面,画面从App产生到屏幕点亮,中间至少经过四个域的协作。App侧的View系统绘制出内容,通过Surface把图形缓冲区交给SurfaceFlinger;SurfaceFlinger决定是交给GPU合成,还是直接命令HWC做硬件合成;硬件合成这件事最终要落到内核态的DRM驱动上,由它去配置显示控制器的时钟、时序、图层,最后通过MIPI DSI或DP/eDP接口把像素送给屏幕,同时还要管好背光的亮灭。
这条链路最大的特点就是长。任何一层的配置出错,都会以“屏幕表现异常”的形式暴露出来。比如你在应用层看到的掉帧,根因可能根本不是CPU性能不够,而是Vsync中断在驱动层就没被正确使能;又比如屏幕花屏,表面看像是硬件排线接触不良,实际上很可能是驱动里DTS配置的lane数或者像素时钟算错了。所以学习显示驱动,本质上是在建立一种“从现象反推链路”的能力。
1.2 学习显示驱动的差异化价值
相比其他驱动子方向,显示驱动的门槛和“护城河”都更高。一方面它要求你懂内核的设备模型、中断、DMA内存管理,又要懂Android经典的Binder架构和合成流程,还要懂MIPI、eDP这类显示接口协议以及LCD/OLED面板的时序要求,是典型的交叉领域。另一方面,SoC厂商的显示驱动代码量大、平台适配强,直接看一份高通或者联发科的Display驱动,如果没有知识框架打底,很容易一头扎进gpio、clk、pinctrl的细节里出不来。
适合做显示方向的,通常是已经有一年左右Linux驱动基础,至少熟悉设备树、platform总线、中断和字符设备驱动的同学。如果你对内核一无所知就开始看SurfaceFlinger,大概率会被各种概念绕晕。反过来说,如果你已经能把一个简单的LED字符驱动跑起来,再来学显示链路,会顺畅很多。这条路线的主要目标,是让你在面对“屏幕点不亮”“颜色不对”“刷新率上不去”这类问题时,能快速定位到正确的层,而不是靠猜。
2. 先画链路,再排学习顺序
2.1 Android显示链路到底分几层
我习惯把Android显示子系统从底往上分成五层,每一层都有自己负责的对象和接口。
第一层是硬件层,面板(Panel)本身、背光、触摸屏都在这里。第二层是内核驱动层,Linux内核里的DRM/KMS子系统承担了大部分工作,包括显示控制器、数据接口、面板驱动、背光驱动、帧缓冲管理。第三层是HAL层,Android在这里定义了Hardware Composer(HWC)和Gralloc这两个核心接口,用来屏蔽掉具体SoC的差异,向上层提供“图层合成”和“图形缓冲区分配”的能力。第四层是Framework层,SurfaceFlinger作为核心合成服务,负责管理所有Surface的层级、做最终的合成决策和帧调度;紧跟其后还有WindowManager、View系统。第五层就是App层了,你的Activity、View、Texture等都在这一层活动。
在开发过程中,你接触最多的其实是中间三层:内核的DRM驱动、HAL层的HWC和Gralloc、Framework层的SurfaceFlinger。App层和硬件层更多是“被服务方”和“服务对象”。所以学习时我会建议把精力集中在中间三层,外围两层保持理解即可。
2.2 推荐学习顺序:从内到外,还是从外到内?
有两种路线。一种是“自顶向下”,从SurfaceFlinger的源码开始读,逐渐往下追踪到HWC再到驱动;另一种是“自底向上”,先搞懂内核DRM驱动怎么把屏幕点亮,再去看HWC怎么复用底层能力,最后回到SurfaceFlinger理解帧调度。我个人的建议是先底后顶,原因很简单:上层的一堆机制,比如BufferQueue、Vsync、三缓冲,它们存在的理由都在底层。
举个例子,SurfaceFlinger通过HWC来提交图层,但HWC最后要调用的依然是DRM接口去配置硬件图层。如果不懂DRM里的Plane、Crtc、Connector这些对象,看HWC的实现时只能囫囵吞枣。反过来,如果你已经把DRM框架摸熟,再看HWC的代码,基本就是“把硬件合成能力封装成HAL接口”这么一件事,思路会非常清晰。
不过“先底后顶”不等于“只底不顶”。我见过有的驱动工程师把内核部分搞得滚瓜烂熟,但一提SurfaceFlinger就发怵。这种偏科会让你在调试问题的时候很吃亏:比如掉帧问题,它既有可能是内核Vsync中断丢失,也有可能是SurfaceFlinger合成超时,你不懂上层调度逻辑,就无法做二选一的判断。这条路线真正要建立的,是一条贯通五层的心智模型。
3. 核心知识点拆解:逐个击破
3.1 DRM/KMS:现代显示驱动的基石
DRM(Direct Rendering Manager)最初是为了解决图形加速和权限问题而诞生的Linux子系统,后来逐渐演变成显示控制的标准框架。KMS(Kernel Mode Setting)则是负责显示模式设置的模块,也是我们这些做驱动的人日常打交道最多的部分。KMS抽象了四个核心对象:CRTC、Encoder、Connector、Plane,另外还有一个帧缓冲FrameBuffer。
如果拿画图来类比,CRTC就是一个“总调度+扫描输出”的角色,它决定什么时候从显存里取数据、按什么时序往屏幕扫;Encoder负责把像素数据转换成具体接口需要的信号,比如MIPI DSI、eDP、HDMI;Connector描述了物理输出口和屏幕的连接状态,同时承载了分辨率、刷新率这些模式信息;Plane则是图层,一个CRTC上可以叠加多个Plane来实现硬件图层混合。
在这四个对象之上,还有一个drm_panel抽象,专门用来描述LCD/OLED面板本身。驱动里常见的操作包括:解析设备树里的panel timing(即每一行、每一帧的时序参数),在probe时注册drm_panel,在显示链路准备就绪后调用panel_prepare和panel_enable。当你看到一份内核驱动的源码,能迅速说清楚“这个平台有几个CRTC、哪个Encoder是MIPI输出的、Connector上挂着哪个panel”,说明你对DRM子系统的骨架已经有感觉了。
3.2 HWC与合成策略:GPU合成还是硬件合成
HWC(Hardware Composer)是Android定义的HAL模块,它向上屏蔽了SoC厂商的硬件合成能力。为什么要做合成?因为屏幕上同时可能有好几个窗口和应用图层,比如状态栏、壁纸、游戏画面、弹窗,CPU或者GPU把它们一个一个“画”到显存之后,还需要把这些图层混合起来变成一帧最终画面,这个过程就叫合成。
合成策略大体有三种:一是纯GPU合成,把所有图层用GPU绘制到一个目标缓冲区里;二是硬件合成,通过显示控制器的多个Plane直接在硬件上混合图层;三是混合合成,一部分图层由GPU预合成,再交给硬件做最终混合。具体走哪种策略,一方面看硬件能力(Plane数量够不够),另一方面由SurfaceFlinger根据图层属性动态决策。
在实际工作中,我建议多使用adb shell dumpsys SurfaceFlinger查看当前合成策略,这是判断性能问题最直接的入口。如果你发现本来应该走硬件合成的场景,结果全部走了GPU合成,就先不要急着优化GPU频率,而是去查是不是HWC的图层校验逻辑在拒绝某些图层,或者驱动返回值有问题。HWC 2.x的版本差异也要留意,高版本在多图层、立体显示、帧边界上做了更多规定,不同平台厂商的具体实现风格也不一样。
3.3 Vsync与BufferQueue:卡顿源头在这里
Android引入Vsync机制的初衷,是解决“显示画面撕裂”问题。屏幕扫描是一行一行进行的,如果在扫描中途缓冲区被改写了,就会出现上半部分和下半部分来自不同帧的情况,也就是撕裂。Vsync信号会在扫描消隐期触发,各个图层生产者(App渲染线程、虚拟显示等)都在这个信号驱动下提交新帧,从而保证缓冲区切换都发生在安全时刻。
Vsync信号的物理来源一般是显示控制器硬件,它会在每帧的VBlank(垂直消隐)期间产生一个硬件中断,这个中断一方面用于驱动调度Vsync,另一方面投递到SurfaceFlinger做帧触发。不要小看这个中断,我遇到过因为中断没有正确注册,导致整条Vsync链失效、界面卡到没法用的case。另一个关键概念是BufferQueue,它是生产者和消费者之间的一组缓冲区队列,Android默认支持双缓冲和三缓冲。三缓冲存在的原因,是当消费者来不及处理时,生产者还能有一块空闲缓冲区继续工作,不至于让界面因为“暂无缓冲区”而停顿。
判断掉帧问题,我最常用的是systrace:抓取一段包含Vsync、SF、App主线程的trace,就能看到某一帧是从哪里开始延时的。很多初学者一上来就盯着CPU频率,其实完全走错了方向。
3.4 Gralloc与显存分配
Gralloc是Android的图形缓冲区分配器。应用要绘制一张纹理、SurfaceFlinger要合成一帧图像,都需要先从显存里分配内存。这个模块在Android 8之后的架构里被拆分成了Allocator和Mapper两层,底层依赖于内核的DMA-BUF机制,常见的内存堆包括ION和后续的DMA-BUF Heaps。
为什么显存分配要单独搞一套,而不是直接用malloc?因为显示和GPU访问的内存,有连续物理内存、对齐、Cache属性、NUMA位置等特殊要求。比如给Display使用的帧缓冲区,往往需要物理连续(或者至少是I/O页表能覆盖的),还要求按行宽和像素格式做对齐。如果你分配到的buffer cache属性不对,画出来的画面会出现花屏或者颜色不对。
Gralloc调试时最常遇到的错误包括:分配失败(内存不足或者堆选择错误)、格式不支持(比如RGB888被用成了RGB565)、 cache buffer没有正确做CPU和GPU的同步。定位这类问题,打开/sys/kernel/debug/dri/0/下的相关节点,搜索dma-buf、fdinfo等关键字,往往能找到大量线索。
4. 从0到1实操路线:动手踩坑才算学
4.1 环境准备与内核编译
开始动手之前,先把环境准备好。代码获取这块,建议直接使用官方公开的AOSP源码,内核部分可以选对应SoC厂商提供的vendor内核分支,也可以自己下一份mainline内核,在你的开发板上把最基本DRM框架跑起来。编译环境最好用Ubuntu 18.04或20.04,编译内核前先确认交叉编译工具链,常见的是aarch64-linux-gnu-系列。
编译内核不需要等整个Android产物,通常只需要构建内核镜像,编译命令类似这样:
export ARCH=arm64 export CROSS_COMPILE=aarch64-linux-gnu- make defconfig make -j$(nproc) Image dtbs如果只是验证DRM驱动,很多时候你不需要完整刷机,只需要把新内核烧写进去。对于树莓派这类带有稳定DRM支持的平台,可以直接用主线内核;对于手机SoC平台,则要仔细核对厂商的dts配置。环境搭建阶段最常见的坑是:工具链版本太老导致部分DRM头文件宏定义不兼容,以及设备树编译工具版本过低。遇到这类问题,升级工具链基本都能解决。
4.2 读懂设备树与驱动probe流程
显示驱动本质上还是Linux platform驱动,而设备树是描述硬件拓扑的灵魂。一个典型的MIPI DSI接口的显示面板,设备树节点里通常会包含:
panel@0 { compatible = "manufacturer,model"; reg = <0>; reset-gpio = <&pio 45 GPIO_ACTIVE_HIGH>; enable-gpio = <&pio 46 GPIO_ACTIVE_HIGH>; backlight = <&backlight>; port { panel_in: endpoint { remote-endpoint = <&dsi_out>; }; }; };驱动probe的过程一般是:匹配compatible → 获取GPIO、电源、背光等资源 → 解析时序参数 → 注册drm_panel → 在DSI控制器驱动里把panel绑定到对应的connector和encoder上。这个过程可以从log里跟踪,最常见的日志是drm_panel的probe成功与失败记录。
初学阶段不要一上来就啃DTS的几百条配置,而是要学会“跳过无关内容”。先定位和显示相关的节点:DSI控制器、显示控制器、panel、backlight、及相关GPIO,搞清楚这些节点之间的引用关系,再逐段去查含义。这样做的效率远高于从上往下读文件。
4.3 用modetest把屏幕点亮
modetest是libdrm自带的一个测试工具,作用非常单纯:枚举DRM设备资源,并设置显示模式。它能把你的面板“绕过Android直接点亮”,这在驱动调试阶段几乎是必备技能。
在root权限下输入:
modetest -M rockchip -p会列出所有connector、encoder、crtc和plane,以及它们的当前状态和支持的分辨率。找到你的面板对应的connector后,指定crtc和模式来点亮:
modetest -M rockchip -s 32:1920x1080其中32是connector id,后面是分辨率。如果这条命令可以正常点亮面板,说明从DRM框架到面板硬件的主链路是没有问题的。接下来的问题排查,就应该集中在Android上层或者HWC层;如果连modetest都无法点亮,那问题就锁定在驱动、设备树、或者硬件连接上,不用再往上找。我在实际项目里几乎每次拿到一块新板子,都会先刷一个能稳定输出modetest的内核,作为触摸上层之前的基本健康验证。
4.4 从写一个DRM小应用理解全流程
modetest能点亮屏幕,但动态使用显示资源的能力是它给不了你的。自己动手写一个最小DRM应用,绝对是理解全流程的最佳方法。核心流程如下:
open("/dev/dri/card0", O_RDWR)打开DRM设备;- 用
DRM_IOCTL_MODE_GETRESOURCES拿到可用的connector、encoder、crtc资源; - 选择一个connector,再选一个兼容的crtc和encoder;
- 通过
DRM_IOCTL_MODE_CREATE_DUMB创建一块dumb缓冲区; mmap映射这块缓冲区,往里面填充颜色数据(比如渐变条);- 创建framebuffer对象,通过
DRM_IOCTL_MODE_SETCRTC绑定crtc和framebuffer。
int fd = open("/dev/dri/card0", O_RDWR); drmModeRes *res = drmModeGetResources(fd); /* 找到第一个connected的connector,选一个可用的crtc */ int ret = drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &connector_id, 1, &mode);这段代码跑通之后,你对DRM对象的理解会上升一个台阶,因为它强迫你亲手去处理“connector和crtc不匹配时怎么办”“dumb buffer的对齐要求是什么”“crtc的模式参数怎么设置”这类真实问题。这个小应用和SurfaceFlinger的关系也很直接:SurfaceFlinger最终也是通过类似的方式,把合成好的帧交给HWC,进而调用DRM接口上屏。只是人家用的是异步流式提交(Atomic Commit),框架更加复杂。
4.5 如何验证显示效果和测量刷新率
验证阶段,除了肉眼看屏幕,我建议学会用Android自带的调试工具把“看不见的链路状态”挖出来。adb shell dumpsys SurfaceFlinger可以查看当前每个图层的合成方式、缓冲区状态、帧率、掉帧数;adb shell dumpsys display可以看显示设备的连接和模式状态;systrace或者新一点的Perfetto能精确抓出每一帧到达SurfaceFlinger的时间点、合成耗时、上屏时间。刷新率是否满足,可以看dumpsys display里的Vsync周期,也可以在/sys/class/drm下的CRTC节点查看实际模式参数。掌握这几个工具,比盲目调参有用十倍。
5. 常见问题与排查技巧实录
5.1 黑屏/无信号的排查顺序
黑屏几乎是最常见的显示问题,但根因五花八门。我的排查顺序是固定的:先确认是整条链路都没启动,还是只是没有背光。没有背光往往表现为“用手电筒照屏幕能看到隐约画面”,这种就先去查背光驱动、PWM、背光使能GPIO。如果确实整条链路没输出,再看内核日志,搜索panel相关节点,确认panel_probe是否成功,DSI控制器是否完成初始化,crtc是否被正确绑定并处于enabled状态。
在这个过程里,GPIO状态检查是最容易被忽略又最关键的。我遇到过一块板子点不亮,所有人都在查MIPI时序,最后发现是面板复位脚在上电后被某个外设驱动误配置成了低电平,导致面板一直处于复位态。检查GPIO最简单的方法,是通过/sys/kernel/debug/gpio查看每个GPIO的当前状态和方向,确认它和硬件原理图一致。
5.2 雪花/花屏/水波纹
屏幕能亮,但是画面是雪花或者条纹,这类问题绝大多数出现在数据通道或者时序参数上。MIPI DSI接口的lane数、lane速率、像素格式必须和面板规格书完全匹配,比如面板要求4 lane、每lane 1.2Gbps,你在驱动里配成了2 lane,图像就会出现严重错位。
先算像素时钟再谈其他:
Pixel Clock = H_Total × V_Total × RefreshRateH_Total、V_Total要包含后肩、前肩、同步信号和有效区域的完整行/帧长度。如果时钟频率偏离了面板允许范围,屏幕就会表现出行不同步、滚动花屏等现象。花屏还有一个常见原因,是framebuffer的像素格式不对,比如面板是RGB888但驱动按RGB565去填充,颜色和位置都会乱。这类问题可以先看modetest输出的信息,再和面板规格书里的时序手册逐项对照。
5.3 亮度无法调节或调节异常
亮度调节基于背光驱动,常见路径是Framework通过BrightnessController设置亮度值,最终经HAL调用内核背光驱动。内核背光最常见的接口是/sys/class/backlight/下的设备节点,brightness负责写入当前亮度级别,max_brightness是最大级别,bl_power控制背光电源开关。
如果出现亮度无法调节,先看看写brightness是否有报错,节点是否存在。没有节点大概率是内核backlight驱动没有注册成功;有节点但写入无效,则去查驱动是否真的拿到了这个值,以及PWM输出是否正常。用万用表或者示波器量一下PWM脚,能直接确认驱动配置和硬件链路是不是一致。注意,OLED屏很多并没有传统意义上的背光,它的亮度调节是依靠调整RGB像素的gamma/灰度来实现的,这类问题就要往显示控制器和面板的gamma/spr相关驱动去排查。
5.4 掉帧与卡顿排查
掉帧问题的排查,重点并不在驱动代码的某一处,而是先分清掉帧发生的阶段。我用systrace抓一次交互操作,观察App渲染、BufferQueue队列状态、SurfaceFlinger合成、以及显示上屏的时间线。如果App渲染本身超时了,是性能问题;如果App渲染正常,但帧在SurfaceFlinger里等了一段时间才被处理,需要怀疑Vsync调度或者合成线程优先级;如果帧已经到了显示控制器,但上屏时刻拖延了,就要去看DRM atomic commit是不是被其他事务阻塞了。
另一个常见原因是图层合成策略被绕过。比如某个层带了特殊blend模式,HWC无法硬件合成,就会回退到GPU合成,合成耗时大增,自然掉帧。此时dumpsys SurfaceFlinger的图层列表里能看到每个层是Client合成还是Device合成,一眼就能看出来。
5.5 常见问题速查表
| 现象 | 优先怀疑方向 | 快速验证手段 |
|---|---|---|
| 整机黑屏、无背光 | 背光驱动、背光电源GPIO | 手电筒照屏幕;查/sys/class/backlight |
| 整机黑屏、有亮度 | DRM链路未启动、DSI未初始化 | dmesg查panel/DSI;modetest测试 |
| 花屏/条纹 | MIPI lane数、速率、像素时钟 | 核对panel规格书时序;modetest |
| 颜色不对 | 像素格式、颜色空间转换 | 检查framebuffer格式;dumpsys |
| 亮度不可调 | backlight节点、PWM配置 | 写brightness节点;示波器量PWM |
| 掉帧/卡顿 | Vsync中断、合成策略、BufferQueue | systrace/Perfetto;dumpsys SF |
| 屏幕闪一下才亮 | 复位时序、使能时序 | 查GPIO时序、面板上电时序要求 |
6. 学习规划与资源建议
6.1 分阶段路线与时间安排
如果你打算系统走一遍Android显示驱动这条路,我给一个可执行的四阶段计划,每阶段都以能独立完成一个验证动作为目标,而不是“读完某本书”。
| 阶段 | 时间 | 目标 | 验收动作 |
|---|---|---|---|
| 阶段一:内核基础与DRM入门 | 1-2周 | 理解platform驱动、DTS,DRM核心对象关系 | 编译内核并能在板子上跑modetest |
| 阶段二:驱动点屏实战 | 2-4周 | 看懂panel驱动和DSI控制器驱动,能配置时序 | 通过modetest点亮自己的面板 |
| 阶段三:HAL与Framework贯通 | 3-4周 | 理解HWC、Gralloc、SurfaceFlinger帧调度 | 能用dumpsys/systrace定位一帧掉帧原因 |
| 阶段四:综合调试与优化 | 长期 | 面对黑屏/花屏/掉帧能自主定位 | 独立完成一个显示问题的根治 |
每个阶段都不是线性推进更好地,第一和第二阶段往往可以重叠,因为在编译内核的过程中你就已经在和DTS打交道了。阶段三如果感觉难,可以先用Android模拟器或一台Pixel设备的线上代码来对照,不用急着绑定厂商BSP。
6.2 源码阅读顺序与个人体会
源码阅读是这条路线里最绕不开的一关。我推荐的阅读顺序是:先看kernel/drivers/gpu/drm/下的drm_panel.c、drm_atomic.c和drm_crtc.c,然后把hardware/interfaces/graphics/composer/和libs/input/、libs/gui/里的BufferQueue和SurfaceFlinger相关代码过一遍。阅读时不要试图一次读懂所有路径,而是针对一个具体场景,比如“按一下Home键,到画面刷新出来,走了哪些调用”,把这个场景的完整调用链读通,收获远大于把SurfaceFlinger从头到尾翻一遍。
最后说一点我的个人体会。做显示驱动头三个月,最容易产生的挫败感是“原来我以为我懂了,结果换个SoC平台又是陌生的驱动”。这是正常的,因为显示驱动的价值恰恰在于你透过不同厂商的代码,看到他们在解决同一个问题:如何把用户的帧,按时、按序、按正确的颜色送到屏幕。一旦你抓住了DRM对象模型和BufferQueue这套通用骨架,换平台只是换皮,真正穿心的是一个可迁移的调试思维。学完这一路,回看你最初面对的黑屏和花屏,你会发现自己从“不知道去哪查问题”变成了“明确知道该拒哪一层、查哪个寄存器”,这个变化,就是这条路线给你最大的回报。