资讯动态

从0到1学Android显示驱动:DRM、KMS、HWC与虚拟显卡点亮全流程

发布时间:2026/10/23 1:28:00 来源:尧图企业网站定制
Android显示驱动这块我陆续带过不少人入门发现一个规律提起Binder、四大组件很多做上层的人还能说上两句一说到显示驱动大多数人的第一反应是“这玩意儿是不是要啃汇编和硬件手册”。其实恰恰相反Android显示驱动的学习路线完全可以按部就班、从0到1一步步走通而且它是我认为Android底层最有性价比的切入方向之一。这是系列的第8篇把一条我自己验证过、也带人走通过的学习路线完整写出来先补四块前置知识再沿着“屏幕怎么亮”这条主线读源码然后接上Android的HWC、Gralloc、SurfaceFlinger三大件最后用虚拟显卡把点亮流程跑通。适合三类人看写过Linux字符驱动想往显示方向转的、Android应用层想往底层走的、刚入职的驱动新人。1. 为什么说显示驱动是Android底层入门最划算的一条路1.1 强反馈是天然的练功环境学驱动的痛苦在于反馈太慢。一个字符驱动你写错了也就是设备节点不出现、read返回错误起码有内核日志可看。而显示驱动不一样对了屏幕瞬间亮错了直接黑屏连报错都往往只有几行。这种非黑即白的强反馈对学习者反而是好事——它能逼着你把整个链路里的每个环节都搞清楚。更重要的是显示链路从硬件面板、接口时序到内核DRM框架再到HAL层和系统合成器横跨了Android底层几乎每一层。你在学显示驱动的同时等于顺手把Linux设备模型、设备树、中断、DMA、电源管理、用户态ioctl这些知识全部过了一遍。这是很多垂直驱动比如触摸、音频、Sensor给不了的系统视角。1.2 学完这条路线你能建立怎样的系统图景我见过不少从显示驱动转去做其他方向的工程师普遍反映往后看Binder、看电源管理、看多媒体管道脑子里都有“一条数据从底层流到上层”的画面感。这就是显示驱动的价值它是少数把硬件行为和系统服务直接绑在一起的技术方向。再说现实一点只要是带屏的终端产品显示控制器、面板驱动、显示HAL这些岗位就会一直在招聘而且比很多细分领域岗位稳定。本篇路线图我会拆成六段来讲四条知识线、一条源码主线、三个上层组件、一套虚拟实验、一份避坑清单。下面直接进正文。2. 正式动手前把四条知识线按自己的节奏拉通很多人一上来就打开内核源码开读结果在drm_connector_init、drm_panel_funcs里面绕晕最后放弃。问题不在于代码难而在于你手里只有孤零零的一个文件缺了另外几条线的支撑。显示驱动不是只读一个目录它同时面对硬件、内核框架、图形栈、Android系统这四层。先把四条线各拉通一遍后面看任何代码都会顺很多。2.1 硬件线时序、接口与面板参数显示的本质是一行一行地把像素扫到屏幕上。像素时钟决定了每秒能扫多少个像素而每行除了有效像素区前面有前肩front porch后面有后肩back porch中间还要插同步脉冲每帧纵向也一样有效行之外还有行消隐区。为什么要有这些空白时间因为它们给了扫描电路和面板“换行”“换帧”的切换窗口。这里有一个必须会算的公式刷新率 像素时钟 ÷ (水平总像素 × 垂直总行数)举个例子1080p60的常规配置像素时钟约148.5MHz水平有效像素1920如果水平前肩88、后肩148、同步44那么水平总像素就是2200垂直有效行1080加上前后肩和同步共1125行。于是刷新率 148.5M ÷ (2200 × 1125) ≈ 60Hz。写驱动时我们需要把这一组porch值换算成显示控制器的寄存器值有些控制器直接写porch有些则要求换算成起始和结束位置这个我在第3章会再展开。嵌入式Android设备最常见的是MIPI DSI接口此外LVDS、eDP、HDMI也常见但移动端核心就是DSI。DSI又分command模式和video模式前者靠面板内存刷新后者靠主机持续送流对驱动来说command模式需要更完整的初始化命令序列video模式则更多依赖持续正确的时序信号。面板参数从哪来厂商datasheet、内核里现成的panel驱动、dts里的timing节点。这一阶段不需要背寄存器能读懂一份timing表能说明白porch含义就够了。2.2 内核线设备模型与设备树匹配显示控制器在内核里通常是一个平台设备platform_device驱动是一个平台驱动platform_driver二者通过设备树里的compatible字段匹配。很多新手第一次看显示驱动会觉得probe函数怎么这么长其实进入probe的前提是先确认设备树节点和驱动里的of_match_table是不是对上了。一个典型的显示控制器设备树节点长这样display-controller { compatible vendor,display-controller; reg 0x0 0xdead0000 0x0 0x1000; interrupts GIC_SPI 69 IRQ_TYPE_LEVEL_HIGH; clocks clk_disp_axi, clk_disp_pix; resets reset_display; status okay; };驱动侧对应static const struct of_device_id display_of_match[] { { .compatible vendor,display-controller }, { } }; MODULE_DEVICE_TABLE(of, display_of_match);匹配成功之后内核会调用驱动注册的probe函数然后才进入显示相关对象的初始化。这一线要补的知识包括platform bus的工作原理、设备树的读取方式、devm_资源管理、中断申请与时钟/复位控制。验证方法也很简单启动后进系统看/sys/firmware/devicetree/base下面的节点是否存在再配合dmesg | grep display确认probe是否进入。这条线不需要读很多代码但必须能独立回答“我的驱动有没有被匹配上”这个问题。2.3 图形线DRM/KMS的核心对象关系DRM框架是Linux显示驱动的核心KMS则是负责模式设置的部分。先把五个对象的关系焊死在脑子里Framebuffer内存里的像素数据块。Plane硬件图层把framebuffer绑定到某个显示层。CRTC显示控制器负责把plane的像素数据按时序扫出来。Encoder信号编码器把并行显示信号转换成MIPI/DSI或LVDS等接口信号。Connector物理连接器代表一块面板或一个接口负责上报连接状态和preferred mode。用流水线打个比方framebuffer是食材plane是灶头CRTC是掌勺厨师encoder是装盘工connector是把菜端到顾客面前的侍者。很多人觉得DRM难就是因为这几个对象的关系是网状而非线性的。更进阶一点现代DRM用atomic commit机制把多个对象的状态变更打包成一个原子事务要么全部生效要么全部不生效避免改到一半出现黑屏或撕裂。读源码时认准drm_atomic_commit这个入口从它倒着往回看能理清大半个KMS流程。2.4 系统线Android侧的HAL接口Android的图形栈在内核之上还叠了三大件SurfaceFlinger负责合成调度Hardware Composer(HWC)负责硬件合成Gralloc负责图形缓冲区分配。数据流向大致是App渲染好的buffer → SurfaceFlinger合成决策 → 通过HWC提交给DRM → 显示控制器扫描输出 → 面板显示。对驱动新人来说这一阶段只需要建立方位感不需要深读SurfaceFlinger源码。你先要能回答三个问题HWC怎么知道有哪些屏幕和分辨率Gralloc分配的buffer在驱动侧怎么变成DRM FramebufferSurfaceFlinger合成的节奏跟VSYNC有什么关系这些在第4章会展开。现在只要记住内核对HAL是透明的HWC最终通过/dev/dri/card0的ioctl和内核对话而你要写的驱动是这整条链上离寄存器最近的那一段。3. 源码主线怎么走跟着屏怎么亮读完整个驱动前置知识补完之后就可以开始读代码了。我的建议是不要按文件目录读而是按“一次点亮”这个完整流程去追。从开关机、驱动加载到第一个画面出现每一步都对应一段可追踪的代码路径。3.1 驱动入口probe与设备树节点首先看驱动加载的入口。一个显示控制器的probe函数通常做这几件事取内存资源并映射寄存器、获取时钟和复位信号、注册中断、初始化底层硬件、创建DRM设备、注册各个子模块。内核版本不同API会有变化比如老版本用drm_dev_alloc新版本倾向于devm_drm_dev_alloc还有不少平台用component机制把display相关的几个模块如主控、HDMI、DSI控制器拆开注册等所有component到齐后再绑定成一个master。一个简化版的probe骨架大概是static int display_probe(struct platform_device *pdev) { struct drm_device *drm; struct resource *res; void __iomem *base; res platform_get_resource(pdev, IORESOURCE_MEM, 0); base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(base)) return PTR_ERR(base); /* 获取时钟像素时钟、AXI时钟等 */ pix_clk devm_clk_get(pdev-dev, pix); /* 申请中断 */ irq platform_get_irq(pdev, 0); /* 初始化显示控制器寄存器 */ display_hw_init(base); /* 分配DRM设备 */ drm devm_drm_dev_alloc(pdev-dev, display_driver); ... return component_master_add_with_match(pdev-dev, display_master_ops, match); }读probe时不要死记顺序而是带着问题去追这几块寄存器是什么功能这个时钟给谁用这几个子驱动为什么不在probe里直接注册当你发现master和component的绑定逻辑时就已经抓住了多条平台共性的那根线。3.2 从framebuffer到CRTC的模式配置用户空间设置分辨率比如运行modetest -s 1920:108060最终会触发一个模式设置流程。在内核里这条路径是DRM_IOCTL_MODE_SETCRTC→drm_mode_setcrtc→ 用户传下来的drmModeModeInfo被整理成drm_display_mode→ 进入atomic提交。这里最关键的是理解drm_display_mode和控制器寄存器之间的换算。一个drm_display_mode里保存了clock、hdisplay、hsync_start、hsync_end、htotal、vdisplay等等字段。写驱动时通常要把这些字段换算成控制器的timing寄存器比如起始位置和结束位置或者前肩后肩长度。再回到那个计算案例1080p60像素时钟148.5MHzhtotal192088148442200vtotal108043651125刷新率确实约60Hz。改时序参数时最常见的失误是把porch算反或者把active区域写成了包含porch的区域结果画面偏移、四周出现黑边甚至直接超频导致花屏。所以在读mode_set相关代码时建议自己动手把模式参数抄一遍、换算一遍、再对着寄存器手册核对一遍这个动作能帮你把整条线焊死。3.3 面板点亮DSI与背光的先后顺序面板不会因为CRTC出信号就立刻亮它有自己的上电与初始化流程。内核的panel框架提供了一组回调prepare、enable、disable、unprepare。prepare阶段负责供电、复位时序、准备通信enable阶段才发送初始化命令并让面板进入显示状态。背光则是独立的通常由backlight子系统控制驱动在看屏是否点亮时要明确区分“信号有没有”和“背光亮没亮”。很多新手踩过这个坑面板发送初始化命令后显示内容出来一瞬又消失或者开机白屏闪烁。这往往是背光在panel enable之前就被点起来了或者reset引脚时序不对。正确顺序一般是先panel prepare → 再enable含初始化序列→ 最后开背光关屏则反过来先关背光 → disable → unprepare。对于MIPI DSI屏初始化序列通常由一组命令和延时组成比如sleep out、display on、亮度设置等。这些命令一般保存在panel驱动或者设备树里严格按datasheet顺序发送中间的毫秒级延时不能省。还有一个估算技巧DSI的lane速率大约等于像素时钟 × 每像素位数 ÷ lane数比如148.5MHz × 24bit ÷ 4lane单lane约891Mbps实际还要加上DSI协议开销。这个估算能帮你判断初始化失败是不是因为时钟没配够。4. 驱动往上接的三根线HWC、Gralloc与SurfaceFlinger内核对上层的接口是DRM但Android不会直接拿ioctl当饭吃中间还有HWC、Gralloc、SurfaceFlinger这三个组件。驱动工程师不必读遍所有源码但要清楚自己写的驱动是如何被上层调用的否则真机调试时你连日志该去哪一层抓都不知道。4.1 HWC硬件合成与图层管理HWC是HAL模块Android 8之后主要走HWC3。它做的事可以概括为一句话把SurfaceFlinger交下来的layer列表尽可能分配到硬件plane上去而不是全都走GPU合成。为什么需要它因为GPU合成要额外读写framebuffer费带宽费电硬件叠加则能把多个图层直接交给display controller处理。从驱动角度看HWC会通过DRM的atomic ioctl提交一组状态里面包含每个plane绑定的framebuffer id、crtc id、src/dst矩形。如果你的驱动没有正确上报plane的格式支持列表HWC发现某个layer的格式放不进硬件plane就会把该layer降级成client合成。表现就是功能正常但性能掉一截log里常会出现类似“unsupported format”的记录。做驱动时务必核对drm_plane上挂的format与modifier列表尤其要包含系统常用的RGBA、BGRA、YUV等格式以及平台特有的内存压缩格式。4.2 Gralloc图形缓冲区分配与格式协商Gralloc负责分配图形buffer它在底层最终会变成dma-buf再通过DRM的prime接口转为驱动侧的handle。这里有个特别容易被忽略的点stride。每行像素在内存里不一定连续紧凑存放而是要按硬件对齐要求来排布。drivers里常见的一个bug就是framework计算了stride但驱动侧又按紧凑布局去读结果图像右移或斜切。驱动要做的一是正确实现prime导入导出二是在前面提到的format/modifier列表上如实上报能力。如果驱动声称支持某种格式却没有对应的硬件处理通路真机会出现颜色错乱、翻转或直接合成失败。排查这类问题时可以先用上层工具看buffer的格式和stride再回内核看驱动收到的是不是同一个值两层对不上的时候问题往往一目了然。4.3 SurfaceFlinger合成调度的总控SurfaceFlinger是Android图形合成的大脑但它离驱动比较远不建议第一阶段就扑进它的源码。驱动工程师需要理解的是它的边界SF拿到所有layer的buffer之后把合成决策交给HWCHWC能做硬件叠加就做做不了就回退client合成。你写的驱动则决定了HWC能不能做硬件叠加。另一个和驱动强相关的点是VSYNC。SurfaceFlinger的帧率调度完全依赖vsync事件的节奏如果驱动侧vblank事件上报不准上层画面就会出现卡顿、掉帧甚至systrace里看到明显的等待超时。所以当你在systrace里看到DisplayComposition时间异常时第一反应不是去查SF而是回头确认驱动里的drm_crtc_send_vblank_event是否在正确时机被调用。5. 别只看书用虚拟显卡把第一次点亮跑出来读代码读到一定程度必须动手。但很多人觉得点亮屏幕需要一块真实开发板加一块屏幕门槛太高结果一直停留在“看源码”阶段。其实有一类免费且高效的入门工具虚拟显卡。用QEMU配合内核自带的虚拟GPU驱动不需要任何真实硬件就能把DRM的主干调用链完整跑一遍。5.1 环境准备与第一个DRM程序推荐用QEMU加内核自带的virtio-gpu驱动。virtio-gpu是一个真实的DRM驱动有完整的plane、crtc、encoder、connector注册流程只是底层不需要真实寄存器而是通过虚拟化接口与QEMU通信。对学框架来说这条路和真机几乎等价成本却低得多随便折腾不炸硬件日志还特别干净。准备步骤安装QEMU和libdrm工具比如qemu-system-x86_64包、libdrm-tools准备一个带图形支持的内核镜像和根文件系统启动时加上-device virtio-vga参数或者用-device virtio-gpu-pci进入系统后直接运行modetest -M virtio_gpu -p看所有显示对象的层级。modetest -M virtio_gpu -p这条命令会把每个crtc、encoder、connector、plane的id和能力列出来。接下来试着设置分辨率modetest -M virtio_gpu -s connector_id:1280x720看到屏幕分辨率变化的那一刻你就已经打通了“用户空间→DRM ioctl→驱动→虚拟显示设备”的闭环。更进一步可以用libdrm写一个几十行的小程序创建一个dumb buffermmap填充纯色然后调用drmModeSetCrtc把它显示出来。这个程序可以自己实现不用依赖现成工具。5.2 改一个虚拟面板观察参数影响虚拟环境最珍贵的地方在于可以安全地做“错误实验”。比如你想观察时序参数换算错了是什么效果可以在QEMU里尝试加载不同的分辨率或者通过内核启动参数video1280x72060等方式请求一个特定模式然后再用modetest查看模式表变化。推荐开一个内核调试参数drm.debug0xf。开了之后内核会打印大量与atomic state相关的日志每次提交plane、crtc、connector的old_state和new_state都会被dump出来。这个日志是理解“atomic提交到底提交了什么”的绝佳教材比闷头读源码效率高一个量级。你可以试着连续切换几次分辨率然后对照日志观察哪些字段发生了变化。有的虚拟显卡connector并不支持不存在的分辨率这正好让你理解mode validation的意义驱动不是你说要什么mode就给什么它要检查时钟范围、lane数、带宽上限不符合的mode会被拒掉。这就是真机上“为什么自定义分辨率不生效”的同一个机制。5.3 真板子上的点亮流程与虚拟环境的第一处不同虚拟环境练的是框架真板子多出来的全是硬件约束。手头如果有一块开发板和一块屏幕要把点亮流程拆成五步来做供电与复位、背光确认、probe与connector注册、mode设置、面板初始化。别看屏幕上只有一张图背后每一步都可以独立验证。我的建议是永远先做“分离定位”。背光先单独亮起来用modetest强制设置mode再逐段确认是电源、时钟、复位、MIPI数据还是背光的问题。如果手头有逻辑分析仪或示波器量一下MIPI时钟和数据线能明显区分“驱动根本没送数据”和“数据送了但面板没起来”。这一步不要靠瞎改寄存器碰运气先软件后硬件先观察后下结论。6. 学显示驱动真正会卡住你的地方节奏、坑与排查思路前面全是知识线最后我想说点更实际的大多数人不是被难度拦住的而是被节奏和排错方式拖垮的。6.1 建议的学习节奏与里程碑我给新人推荐一个六周路线每天能保证30到60分钟就够关键是连续第一周Linux设备模型、设备树、编译并运行一个带QEMU的内核环境。第二周读DRM核心对象文档跑通modetest -p知道crtc、encoder、connector、plane分别是谁。第三周读一个虚拟GPU驱动的probe和mode_set相关代码自己写一个libdrm小工具完成改分辨率、填色显示。第四周开drm.debug0xf对着atomic日志读代码理解每次提交的完整链路。第五周接触HWC和Gralloc搞清楚buffer从申请到上屏的路径在虚拟机上测试不同格式。第六周进阶到真板如果有硬件手动点亮一块真实屏幕记录完整的点亮日志。衡量里程碑能不能过就看三个标准能不能不看文档就说出atomic commit的流程modetest你熟不熟换一块面板驱动你能不能独立加进内核并让它出画面。能过一次后面就顺了。6.2 高频踩坑点与排查方式显示驱动排查有一个基本纪律改代码之前先收集信息。黑屏不代表“代码坏了”只是你还没定位到具体断点。我整理几个最常见的坑和对应的排查顺序现象优先排查常用手段完全黑屏无任何日志compatible是否匹配、probe是否执行dmesg有背光无画面面板初始化命令、reset时序、MIPI时钟对比datasheet时序量MIPI信号画面闪烁或撕裂vsync上报、buffer stride、plane格式检查vblank事件比对SF与内核层格式切分辨率后黑屏crtc off/on顺序、panel power cycle抓atomic log看mode_set是否生效休眠唤醒后不亮resume流程、时钟使能顺序、panel状态机对比suspend/resume回调顺序一次典型排查链路可以这样走新换了一颗MIPI屏上电后有背光但无画面。先看modetest -pconnector有没有识别出屏幕会发现它detect正常再看dmesgpanel probe也成功了。问题收敛到两点初始化命令序列和时钟。于是对照datasheet逐一检查reset拉高到sleep out的延时发现厂商要求80ms驱动里只给了10ms。修正后屏幕依然不亮继续量MIPI时钟发现lane速率只配到一半补上PLL配置后画面正常。整个过程没有玄学每一步都有日志或信号佐证。最后说一点个人体会。很多人问我“学显示驱动要多久才能上手”我的答案从来都是连续一两个月每天碰比断断续续学一年有效。哪怕每天只追一个函数、一次状态更新持续观察“从提交到screen显示”这条链的任何一个片段都会产生积累。真正让你记住这门技术的不是读了多少文档而是屏幕第一次因为你改的代码亮起来时那一瞬间的实感。找一块便宜的板子或者就用虚拟机从第一个modetest命令开始这条路没有你想的那么远。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑