
做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把图形缓冲区交给SurfaceFlingerSurfaceFlinger决定是交给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 ComposerHWC和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现代显示驱动的基石DRMDirect Rendering Manager最初是为了解决图形加速和权限问题而诞生的Linux子系统后来逐渐演变成显示控制的标准框架。KMSKernel Mode Setting则是负责显示模式设置的模块也是我们这些做驱动的人日常打交道最多的部分。KMS抽象了四个核心对象CRTC、Encoder、Connector、Plane另外还有一个帧缓冲FrameBuffer。如果拿画图来类比CRTC就是一个“总调度扫描输出”的角色它决定什么时候从显存里取数据、按什么时序往屏幕扫Encoder负责把像素数据转换成具体接口需要的信号比如MIPI DSI、eDP、HDMIConnector描述了物理输出口和屏幕的连接状态同时承载了分辨率、刷新率这些模式信息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合成还是硬件合成HWCHardware 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 ARCHarm64 export CROSS_COMPILEaarch64-linux-gnu- make defconfig make -j$(nproc) Image dtbs如果只是验证DRM驱动很多时候你不需要完整刷机只需要把新内核烧写进去。对于树莓派这类带有稳定DRM支持的平台可以直接用主线内核对于手机SoC平台则要仔细核对厂商的dts配置。环境搭建阶段最常见的坑是工具链版本太老导致部分DRM头文件宏定义不兼容以及设备树编译工具版本过低。遇到这类问题升级工具链基本都能解决。4.2 读懂设备树与驱动probe流程显示驱动本质上还是Linux platform驱动而设备树是描述硬件拓扑的灵魂。一个典型的MIPI DSI接口的显示面板设备树节点里通常会包含panel0 { 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/DSImodetest测试花屏/条纹MIPI lane数、速率、像素时钟核对panel规格书时序modetest颜色不对像素格式、颜色空间转换检查framebuffer格式dumpsys亮度不可调backlight节点、PWM配置写brightness节点示波器量PWM掉帧/卡顿Vsync中断、合成策略、BufferQueuesystrace/Perfettodumpsys SF屏幕闪一下才亮复位时序、使能时序查GPIO时序、面板上电时序要求6. 学习规划与资源建议6.1 分阶段路线与时间安排如果你打算系统走一遍Android显示驱动这条路我给一个可执行的四阶段计划每阶段都以能独立完成一个验证动作为目标而不是“读完某本书”。阶段时间目标验收动作阶段一内核基础与DRM入门1-2周理解platform驱动、DTSDRM核心对象关系编译内核并能在板子上跑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这套通用骨架换平台只是换皮真正穿心的是一个可迁移的调试思维。学完这一路回看你最初面对的黑屏和花屏你会发现自己从“不知道去哪查问题”变成了“明确知道该拒哪一层、查哪个寄存器”这个变化就是这条路线给你最大的回报。