
写这篇是因为自己当年啃显示驱动的时候被一堆名词“SurfaceFlinger”“HWC”“VSYNC”“DMA-BUF”砸得晕头转向网上的资料东一块西一块看完还是不知道从哪儿下手。这篇整理出来的路线是那会儿踩了无数坑之后重新梳理过的顺序按这个顺序走比直接扔进源码里瞎翻要省力得多。希望能帮到正准备入坑或者已经在坑里挣扎的人。1. 先搞清楚Android显示到底在说什么很多人听到“显示驱动”第一反应是去翻Linux内核里的DRM、DSI、LCD驱动觉得那是全部。但Android的显示链路和普通Linux不太一样它把上层逻辑又包了一层。如果不先把整个链路的位置搞清楚后面看代码会一直有种“我在哪、这函数是谁调过来的”的迷惘感。1.1 一条图钉下去从App到屏幕像素的完整路径Android上App画出来的画面不是直接写进显存的。一次完整的显示过程大致是App通过Skia/OpenGL ES把UI画到一块Buffer里这块Buffer由Surface/SurfaceControl管理SurfaceFlinger拿到这块Buffer之后选择合适时机VSYNC把多层Buffer做合成Composition合成好的结果交给硬件抽象层HWComposer最终由硬件控制器把像素推给屏幕。这一路上你听得最多的几个词就是SurfaceFlinger、HWComposer、VSYNC、Layer、BufferQueue、HAL。它们的关系可以简化成这样App - Surface - BufferQueue - SurfaceFlinger - HWComposer(HAL) - Kernel Display Driver - Panel也就是说显示驱动真正干的事是HWComposer往下一层到内核DRM/DPU/Panel的这一段而SurfaceFlinger往上的部分属于系统服务层平时调试也经常要越过HAL往上看一眼。所以学习路线里不能光盯内核要把上层的接口协议也弄明白否则HAL里那个“composer”到底怎么和上层对齐帧率、怎么上报能力你根本对不上号。1.2 这套体系为什么非要用HWC拆一层Android没有直接让SurfaceFlinger操作内核驱动而是中间加了一个HWComposer HAL层。这么做不是为了增加学习难度而是有实际工程考量合成方式因芯片而异有的SoC自带独立合成器DMA引擎有的GPU能力弱必须靠硬件叠图把这些差异藏在HAL后面SurfaceFlinger就只需要和标准接口打交道。显示时序、刷新率、待机唤醒这些硬件强相关的事情不适合放在框架层处理。高通、联发科、展锐这些供应商各自维护自己的HAL实现Android系统只要保证HAL接口版本兼容就行不需要为每个SoC改SurfaceFlinger。所以学习的时候别急着钻DRM驱动细节先理解HWC是Android和硬件之间的契约接口理清这个思路后面看各家BSP显示代码会觉得处处都是熟悉的套路。2. 基础地基图形栈里绕不开的那些概念显示驱动看着是硬件驱动但工作里天天打交道的都是图形概念。基础不牢的话你连log里报的“pixel format mismatch”都看不懂。这一节列的是我认为必须先过的坎优先级最高。2.1 颜色格式与内存排布NV12/RGBA8888背后的门道HWC合成的时候每个Layer都会带上一个Buffer描述里面包含width、height、stride、format。format常见有RGBA_888832位每个像素4字节常用于UI层CPU和GPU读写都友好。RGB_56516位省内存但色彩精度低。YUV_420NV12、NV21、YV12主要用于视频层Y通道和UV通道分开放。在驱动侧其实你更关心stride。它指一行像素实际占用的内存字节数不一定等于width × bpp因为硬件DMA对齐要求比如64字节对齐会让stride比理论值大。如果驱动用了stride去计算偏移而上层填的是width画面就会出现斜纹、撕裂甚至直接花屏。我建议入门的时候去adb shell里执行dumpsys SurfaceFlinger在里面找到层的Buffer信息去比对一下width、height、stride的实际数值观察YUV层和RGB层的stride差异。实际看一遍比背一百遍定义都管用。2.2 VSYNC、刷新率和掉帧到底谁在管VSYNC是Android显示链路的节奏源。所谓VSYNC就是硬件在扫描出一帧画面时产生的同步脉冲它标志着新一轮图帧开始提交的时机。SurfaceFlinger在vsync到来时去取最合适的Buffer并合成App在vsync到来时开始绘制下一帧这样不容易出现画面撕裂。而驱动侧的心跳其实来自内核的DRM管脚或发送器驱动通常通过dpms、vblank事件上报。调试掉帧问题时第一步分工非常关键dumpsys SurfaceFlinger里的帧统计能看到App/SF合成耗时。dumpsys gfxinfo能够看到App渲染的帧耗时分布。如果App和SF都很快但屏幕还是抖/卡那问题往往在驱动侧比如vblank中断没处理好、合成器带宽不够、DDR频率没能及时拉起来。这套分工思路能帮你快速缩小排查范围免得一直在某个层面死磕。2.3 BufferQueue和DMA-BUF不要混淆的两个“缓冲”上层有个BufferQueue它负责App和SurfaceFlinger之间Buffer的流转生产者App取一个空闲Buffer画完填好后交给消费者SurfaceFlinger。这是纯用户态的机制。内核侧有DMA-BUF它描述一块物理内存可以做dma映射、同步cache、共享给不同的设备。在Android显示栈里BufferQueue里的Buffer最终是一块匿名共享内存或dma-buf fd传递到HWC后驱动通过dma-buf拿到物理地址做scanout。很多新手会混淆这两个概念。我自己的理解BufferQueue是调度逻辑解决“谁在哪个时间用这块buffer”DMA-BUF是底层内存句柄解决“这块buffer怎么被硬件访问”。排查HWC提交connect耗时高时基本都是在处理dma-buf的同步和映射问题。3. 正式的代码之路从接口标准到内核驱动地基打完就可以正式开始看代码了。这条路我建议分成三站第一站是用户态HAL接口第二站是内核显示框架DRM第三站是具体硬件驱动实现。每站都有必须掌握的核心文件。3.1 第一站理解Composer HAL和AIDL接口Android版本演进过程中HWC的接口形式变过好几轮。老版本是HWC 1.x/2.0的C接口从Android 8开始HIDL版本的IComposerAndroid 13及以后默认是AIDL的IComposer。你在实际工作中遇到的源码可能是任意一种但学习重点不是背接口函数名而是理解下面这套流程SurfaceFlinger创建ComposerClient然后调用createDisplay/ createLayer来创建显示设备。每帧合成时SF会调用setLayerOutputColor、setLayerBuffer等一堆set方法最后通过validate和present两个调用来提交。validate阶段询问HAL“按我给的layer属性你能不能直接合成要不要我用GPU处理某些层”present阶段真正把结果呈现出去。这套“先validate再present”的设计目的是让HAL有机会反馈“某些layer没法硬件合成请SF走Client合成”。如果你自己写驱动必须正确实现validate里的合成判断逻辑否则要么性能崩塌要么显示错乱。实际操作建议在源码里搜“IComposerClient.aidl”先只看注释搞清楚每个方法的职责再看一个参考实现比如编译产物里的ComposerClient.cpp或硬件抽象层的默认实现。不要从setLayerCursorPosition这种小功能入手要从validate/present主路径进去。3.2 第二站内核DRM框架的核心角色到了内核层就绕不开DRMDirect Rendering Manager。Android显示驱动在现代SoC上基本都挂在DRM下面。你需要认识这些结构drm_device整个显示设备对应/dev/dri/card0。drm_crtc扫描输出控制器负责把framebuffer内容按时序送出去一个CRTC通常对应一条显示通道。drm_connector物理显示接口或面板比如DSI Connector、HDMI Connector代表一个“能接屏幕的口”。drm_encoder编码/传输单元负责把CRTC输出的信号编码成对应接口格式。drm_plane图层平面。主planePrimary输出framebuffer到CRTCoverlay plane可以在硬件上直接叠加别的图层对应Android里的多个Layer。学习DRM时最容易发的困惑是这几个对象之间的关系。打个粗浅的比方CRTC像投影仪的光机。Connector像投影仪的接口。Encoder像信号转换线。Plane像放在光机前面的几张透明胶片。数据从内存出来先到Plane由CRTC按像素时钟扫出来经过Encoder变成屏幕面板需要的信号最后从Connector接出去。Android的HWComposer HAL最终也是通过DRM的Atomic接口把一个个Layer map成Plane完成硬件合成的。3.3 第三站具体BSP驱动看什么平台级DRM框架理解之后再去碰板级BSP会从容很多。每一家的实现虽然千差万别但核心就那么几个文件位置dts/dtsi显示控制器节点、面板参数、reset gpio、背光gpio、供电时序。DRM encoder/connector驱动负责mode setting、panel初始化、时序配置。DPU/DC系列驱动有的厂商叫DPU有的叫DISPC负责实际图层混合、缩放、rotate。panel驱动初始化序列、显影时序、背光控制、休眠唤醒序列。clock和power驱动保证显示相关时钟和电源域按时打开很多“黑屏点不亮”问题到最后都查到这里。第一次看这类代码时不要试图把每个寄存器含义弄通先把从上到下的调用链走通drm_atomic_commit - crtc_atomic_enable - encoder_atomic_enable - panel_prepare - panel_enable等。找一条“显示打开”的路径顺着看一遍比看一百个子系统的代码更有效。4. 实操从零开始点亮一块屏幕的完整流程学显示驱动不能光看一定要亲手点亮屏幕。哪怕手上没有开发板用模拟器或虚拟机也能跑通部分链路但真正的成就感来自在板子上把一块LCD/AMOLED点亮。下面这套流程是我在Linux/Android平台都验证过的通用步骤大家根据自己的板子调整。4.1 环境准备和内核配置首先要有一块支持mainline内核的板子或一个能自己编内核的设备。如果预算有限用QEMU模拟DRM也可以练手但很多真实时序问题模拟不出来所以有真实板子最好。内核需要打开以下选项CONFIG_DRMy CONFIG_DRM_PANELy CONFIG_DRM_DW_HDMI如果走HDMI CONFIG_DRM_PANEL_SIMPLEy CONFIG_DRM_LOAD_EDID_FIRMWAREy然后通过设备树描述一块简单面板。刚开始不建议直接接复杂的高刷新率AMOLED先用一颗常见的、有现成初始化的RGB/LVDS/DSI面板例如ILITEK、Himax这类有参考驱动的屏幕。确认/dev/dri/card0存在后可以先在Android模式下验证也可以先用纯Linux userspace验证。我建议两条腿走先纯Linux下modetest看到vendor id再进Android走HWC看合成这样能把框架层和内核层分开调试。4.2 确认显示链路通路的第一个里程碑第一个里程碑不是看到Android UI而是看到一个纯色画面。具体步骤编译并烧录内核adb shell进入Android后先关掉SurfaceFlinger干扰adb shell stop用以下命令测试简单输出# 查看当前显示设备 adb shell ls /dev/dri/ # 查看connector信息 adb shell cat /sys/class/drm/card0-*/status如果驱动注册成功status应该是connected并且/ sys/class/drm下能看到对应connector节点。此时在内核启动log里应该能看到[drm] Initialized [drm] Cannot find any crtc or sizes如果没有看到先用modetest或cat /sys/kernel/debug/dri/0/state查看状态。这一步纯粹是验证最底层的DRM注册不代表能显示但它是后面一切的前提。4.3 点亮屏幕DPMS和framebuffer测试接着尝试真正的图像输出。在Android下如果surfaceflinger在跑直接往DRM写framebuffer可能会被干扰。推荐先停机再测试adb shell stop adb shell echo 1 /sys/class/backlight/xxx/brightness然后用一个自带的小工具modetest。有的Android系统里没有modetest可以提前push一个静态编译的busybox或modetest进去。经典测试方法是modetest -M msm -D card0 -s connector_id:mode_id更直接的办法是写一个简单的fbtest打开drm fd设置crtc和modemalloc一块buffer填充红色缓冲通过drmModePageFlip提交。如果屏幕出现红色说明从内核到面板这条链路已经打通这也是个人项目里“从0到1”最关键的时刻。4.4 在Android下验证HWComposer合成内核链路通了以后再把SurfaceFlinger拉起来看Android合成的帧能不能正常显示。这一步会暴露大量HWC HAL层面的问题例如Layer数量超过硬件合成上限某种格式如NV12、P010 HDR底层不支持但上层没协商对齐要求不对导致硬件扫描出的画面偏移idle timeout没有正确触发导致低功耗唤醒后画面保持异常。验证手段还是dumpsys SurfaceFlinger打开后的第一眼去看dumpsys SurfaceFlinger --latency这个命令能输出最近一帧的时序数据能看出App/SF/硬件的节奏是否正常。如果“Refresh Rate”一栏与实际不一致基本能判断HWC的时序上报有问题往这个方向查。5. 串起框架层SurfaceFlinger到底和驱动如何协作很多人在驱动里写好了寄存器序列点完屏就以为大功告成。但Android系统级联调时驱动经常要配合框架层做很多事。这里挑几个影响最大的协作点讲讲。5.1 Layer合成策略什么时候需要GPU帮忙SurfaceFlinger每帧会先收集所有可见Layer然后试探性地问HWC“你能不能帮我合成”HWC通过validate返回“需要Client合成这些Layer”的列表。驱动能硬解的layer数量、旋转能力、Z序支持情况都会影响这个策略。我们自己调试中遇到过一种奇怪现象打开某个应用时画面正常但打开分屏后有半块区域花屏。最后发现是该平台硬件合成最多只能3个layer一旦超过HWC返回的client合成区域没同步好SF把部分layer保留了GPU合成但有些图层没上报全。排查时可以看dumpsys SurfaceFlinger里的“visible layers”数量和HWCLayer列表对比一下HWC Layer: ... Client composition: (from GPU)如果两层都列了但没有预计的合成方式基本可以定位到HAL的validate逻辑有bug。5.2 Buffer生命周期管理与release fence显示驱动里还有一个高频出现的概念是fence。上层Buffer在被GPU/HWC读取期间不能随意释放和复用。Android通过acquire fence/release fence管理App拿到一个Buffer时需要等待previous release fenceApp画完后把Buffer还给SF时带上acquire fenceSF/HWC开始读取这个Buffer需要等待这个acquire fenceHWC读完并扫描完成通过release fence通知消费端可以复用。如果驱动没有正确实现fence回调后果极其典型画面卡顿、花屏、或者Buffer被提前复用导致内容残缺。建议先从Android的FenceTrace数据入手adb shell dumpsys SurfaceFlinger | grep -A50 FenceTrace看到很多signaled字样时去追driver的fence signal路径查DRM vblank或DPU done中断是否及时上报。这块属于比较隐蔽的问题排查经验越丰富越值钱。5.3 屏幕参数与显示模式协商Android 11以后越来越多的机型支持高刷新率、自适应刷新率LTPO。这些功能不是面板单方面能做的需要SF、HWC、内核一起协作SF通过HWC查询支持的mode列表根据用户设置或场景选择preferred mode切换mode的时机由内核或HAL触发同时要保证vsync稳定。如果你在调试高刷屏先确认下面几个信息是否一致dumpsys display | grep mModeId dumpsys SurfaceFlinger | grep refreshRate /sys/class/drm/card0-xxx/status 对应连接器的mode列表平时遇到“调了高刷没生效”“切换刷新率闪黑屏”等问题多半是mode切换流程没维护好或者panel需要在切换前后做特殊初始化这部分可以在内核的connector_mode_changed回调里断点排查。6. 调试三板斧dumpsys、trace、内核日志显示驱动调试时信息源非常重要。很多人出了问题一脸懵其实就是没有建立一套自己的排查顺序。我的经验是遵循“由上层往下看、先看协商再看状态、先看时序再看颜色”的原则。6.1 系统侧查状态三板斧第一招先说最直接的三条命令adb shell dumpsys SurfaceFlinger adb shell dumpsys display adb shell cat /sys/kernel/debug/dri/0/state这三条基本能覆盖Layer状态/合成方式、显示模式/刷新率、内核CRTC/plane/connector的实时状态。在情况不明时一次性全抓下来保存为txt再按时间对比是最高效的方式。有问题先dumpsys别一上来就翻驱动寄存器很多时候是上层协商出问题。6.2 Trace工具帮你看时序要深入定位掉帧和卡顿Systrace依然是挺好用的生产力工具。注意要抓的label有几类gfx/chendApp渲染耗时hwuiApp UI线程和渲染线程SurfaceFlinger合成dpcHWC和DRM的时序。抓取时如果能看到SF主线程和HWC线程之间的时间线错位就很容易判断是VSYNC不稳还是HWC阻塞。数据出来后要重点看以下几段的耗时-App onFrameDraw -SF acquireBuffer/composeSurfaces -HWC validate/present -DRM atomic commit在抓trace之前先热启动复现场景抓的时候尽量让问题规律重现不然trace背景噪音太多很难挑。自己调试时建议在HAL的validate和present两端加点trace按括号配对方式命名这样systrace里能直接看到每个layer的处理耗时。6.3 内核日志里哪些关键路径值得打点驱动工程师最喜欢也最头疼的就是printk。但显示驱动比较特殊因为打印时机不对可能让时序崩坏。我一般用ftrace来跟踪函数调用而不是直接乱加内核日志。重点跟踪这几个函数drm_atomic_commit drm_atomic_check_only drm_atomic_commit_tail plane_atomic_update encoder_atomic_enable panel_simple_enable通过ftrace看调用顺序和耗时可以快速定位哪一步卡了。如果怀疑是中断问题再跟踪vblank相关事件。Android本身也有类似的kernel trace机制直接在trace中看这几个函数输出就够了。7. 常见问题与排查技巧实录驱动调试最大的痛点是自己踩坑很多坑网上一搜搜不到准确答案。下面整理几个我遇到的典型问题以及排查心得。7.1 屏幕黑屏但设备正常运行这个症状太常见了。黑屏不代表没有输出可能只是背光没点、也可能完全没时序。按这个顺序查确认面板供电和背光gpio状态是否正常查drm state里的“active”是否为1查具体connector的“dpms on”属性手动打开背光看是否有微弱显示对比正常/异常时的dmesg差异。有时候是panel初始化序列里GPIO时序不对导致屏幕复位后进入休眠。这种问题在dmesg里可能没有任何报错只能靠示波器/逻辑分析仪去量reset和power引脚。7.2 画面撕裂/stuttering撕裂的本质是扫描输出和Buffer更新不同步。优先检查vsync是否由正确的vblank源驱动有的平台支持vidoe和command模式如果command模式没有正确控制读指针就会撕裂。可以先检测HWC是否开启PerfectDisplaySync等特性再查看SF的Presentation Statistics。通常“stuttering”还会伴随掉帧计数飙升此时去看adb shell dumpsys SurfaceFlinger | grep -i Statistics观察Frames Dropped这一项变化趋势如果掉帧集中在某个时间段去和trace对应。7.3 Buffer格式不支持该lay活明文报错一般不可怕可怕的是不报错直接花屏。如果花屏只在视频播放时出现先确认视频格式是不是NV12或P010再看HWC申报的supported formats是不是默认没加上NV12。很多平台需要通过修改HAL里的format query逻辑把NV12列入白名单否则上层协商时把它判为Client合成结果SF误判为可以直接硬件合成画面就花了。一个通用技巧看到花屏时先停掉SurfaceFlinger再播放视频看底层framebuffer是否显示正常。如果底层正常多半是合成策略/协商出问题如果底层也花那就是解码或内存管理的问题路径完全不同。7.4 休眠唤醒后屏幕闪绿/闪条纹这个问题高发于AMOLED/LCM的休眠唤醒时序不完善。常见原因panel_init_sequence在休眠后没有正确执行背光亮度在唤醒前过早点亮dsi高速传输lane没有从低功耗状态正确恢复。排查手段自动休眠唤醒几次后通过drm_debug查看panel register状态对比第一次开机成功和唤醒后失败时的dmesg差别重点检查sleep out、display on的时序间隔是否满足IC规格书要求。这个过程中不要去猜直接查panel datasheet里的time spec特别是t1/t2/t3这类间隔参数。8. 学习路线总结与建议整个路线走下来从“能点亮”到“能稳定商用”还要很久但核心的思维闭环已经建立先掌握上层接口协议再理解内核DRM框架然后用具体板卡把链路跑通最后形成自己的调试方法论。8.1 推荐的学习顺序我把这条路做成一个清单按顺序打勾即可理解显示链路App到屏幕跨越的几个层级掌握基础概念格式、VSYNC、BufferQueue、DMA-BUF通读Composer HAL的validate/present主流程熟悉DRM core的CRTC/Encoder/Connector/Plane模型跟一个具体SoC的DRM/DPU驱动代码调用栈在真实板卡上点亮第一块屏验证各环节与SurfaceFlinger联调观察合成策略和fence整理自己的调试工具包dumpsys、trace、ftrace。第3步和第4步顺序也可以换但无论如何不要跳过第2步因为你后面所有问题排查都建立在这些概念之上。8.2 关于“从0到1”最实在的一句话如果只能给一条建议我会说想办法让你的代码在裸环境下可复现别用“在我这边是好的”当作结论。显示问题最怕环境干扰我先在纯Linux下用modetest验证再进入Android验证HWC链路再恢复到正常使用。三步环境分开只要有一处出问题就能快速定位到子系统。这一点比记住再多的寄存器都管用。我自己当时最早点亮屏幕的那一刻用的其实只是内核自带的一个简单panel driver再把背光打开屏幕立刻亮起一块纯蓝色。那瞬间的兴奋感持续了很久但真正的大头还在后面。希望这份路线能帮你少走一些弯路如果你在实操中碰到什么诡异现象不妨回头把链路图再画一遍通常问题就藏在某个你忽略的中间层里。