ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DRM显示驱动解析:U-Boot阶段MIPI DSI与framebuffer实战

DRM显示驱动解析:U-Boot阶段MIPI DSI与framebuffer实战 1. DRM基础框架拆解1.1 从显示链路说起DRM到底管了哪几层最近在调一块MIPI DSI接口的显示屏又是熟悉的竖屏模组、又是熟悉的横屏需求干脆把DRM驱动这块的笔记整理出来。这篇先讲基础框架和U-Boot阶段的驱动解析kernel态的CRTC/Encoder/Connector完整联动留到下一篇。很多刚接触显示子系统的同学一上来就被DRMDirect Rendering Manager的抽象层绕晕。说白了DRM要解决的问题就是把显示这件事拆成几个互不干扰的环节显示控制器怎么把内存里的图像数据搬到屏幕上、什么时候搬、用什么格式搬、屏什么时候亮、什么时候灭、分辨率怎么切换、多屏怎么叠加。这些都是DRM/KMS的管理范围。整个Linux显示栈大致是这样分层应用层通过直接渲染接口或者标准显示接口访问显示资源内核层的DRM core负责资源分配和模式管理最底层是各个厂商的GPU/DPU驱动和panel驱动。DRM的厉害之处在于它定好了一套统一的抽象框架厂商只需要实现具体硬件相关的回调函数上层API完全不用关心底层是哪家的显示控制器。我在实际项目里最深的感受是DRM不是把显示变复杂了而是把原来各个厂商各自为政的显示驱动收拢成一套标准玩法。以前老平台用fbdevFrame Buffer Device写显示驱动每个厂商一套私有接口换一个平台几乎等于重写。DRM框架下核心的modeset逻辑是通用的厂商只需要关注自己的硬件差异点这个设计思路本身就是值得反复琢磨的。1.2 DRM的核心对象模型认识Device、Driver与FileDRM framework里最基础的三个对象是DRM Device、DRM Driver和DRM File。我刚开始看代码的时候总把前两个搞混后来在实践中才彻底搞清楚。DRM Device对应一个物理显示控制器实例。在嵌入式平台上一个DRM Device通常对应一个Display Controller比如Rockchip的VOP、NXP的LCDIF、Allwinner的DE2。它的内部维护着一堆资源池plane、crtc、encoder、connector都挂在它下面。设备树里能看到对应的compatible字符串比如rockchip,rk3568-vop内核就是通过这个匹配到相应的DRM驱动。DRM Driver则是驱动代码里实现的那一套操作集合注册的时候通过drm_driver结构体定义包含了file operations、ioctl回调、gem操作等。它更像是一份说明书描述了这个硬件能做什么、驱动怎么操作它。DRM Framework通过这份说明书来调用你的实现对外统一暴露/dev/dri/card0这样的设备节点。DRM File就更容易理解了每次进程open设备节点内核就创建一个DRM File对象。它管理的是这个会话创建的资源比如GEM handle映射、fence等。多进程同时操作GPU的时候每个进程的资源是隔离的DRM File就是这道隔离墙。提示调试的时候看到drm_open/drm_release被反复调用是正常的很多应用会频繁开关设备节点。如果怀疑资源泄漏重点查DRM File里的handle有没有正确释放而不是盯着drm_open的次数。1.3 KMS三大核心组件CRTC、Encoder、ConnectorKMSKernel Mode Setting是整个显示子系统的核心它把显示链路抽象成四类对象Plane平面、CRTC显示控制器、Encoder编码器、Connector连接器。这个链条理解起来最直观的办法是拿HDMI电视举例。Plane就是画面图层好比你在PS里新建的图层可以叠加、可以设透明度、可以指定缩放CRTC是显示控制器本身它负责把plane里的图像数据按一定时序扫描输出相当于总指挥Encoder负责把CRTC输出的并行数据编码成特定接口协议比如HDMI的TMDS信号、MIPI DSI的差分串行信号可以理解成信号翻译官Connector则是物理接口和屏的抽象比如HDMI接口上插了一台4K电视Connector就记录了这台电视的EDID和当前分辨率。我用实际项目里最常见的一条链路来对应RK3568的VOPCRTC从内存里取图数据送到MIPI DSI Encoder做协议转换最终通过MIPI DSI Connector连接到物理面板。整个链路在DTS里是这样对应的video_phy { status okay; }; dsi { status okay; #address-cells 1; #size-cells 0; panel0 { compatible boe,tv080wum-nl0; reg 0; enable-gpios gpio RK_PC7 GPIO_ACTIVE_HIGH; pinctrl-names default; pinctrl-0 lcd_panel_en_pin; backlight backlight; status okay; ports { #address-cells 1; #size-cells 0; port0 { reg 0; panel_in_dsi: endpoint { remote-endpoint dsi_out_panel; }; }; }; }; }; dsi_in_vp1 { status okay; };Framebuffer里面每个字节和屏幕像素的对应关系也是刚入行经常搞错的点。以24位色深为例如果数据传输格式是RGB888那么framebuffer一个像素占3个字节第一个字节是R第二个是G第三个是B。但很多显示控制器为了内存对齐会在每行末尾填充无效数据这就引入了一个非常重要的概念——stride行跨度。stride表示一行像素实际占用的内存字节数它可能大于width乘以bytes_per_pixel的值。比如一个1920x1080的32位色深的framebuffer每行实际占用的字节数可能是1920*4padding如果这时候你老老实实按1920*4去计算长度偏移量就会越算越歪。注意这是我实际踩过的坑。在U-Boot里显示内核logo的时候如果用错误的stride去计算地址偏移画面会呈斜条纹状撕裂错位看起来像是屏参没设对实际上只是内存寻址算错了。排查这种问题的时候先验证stride再检查时序参数能省不少时间。GEM和DMA-BUF这两个概念在DRM里通常是配合使用的。GEM负责管理显卡内存DMA-BUF则允许不同设备间共享内存比如GPU渲染完一帧图像通过DMA-BUF把内存共享给显示控制器之间不需要拷贝。这种零拷贝机制在视频播放、UI渲染这类高性能场景里几乎必不可少。1.4 GEM内存管理与FBDEV兼容层的存在价值前面提到DRM管理显存用的是GEMGraphics Execution Manager。GEM的核心思想是不直接操作物理内存而是操作GEM对象。一个GEM对象就是一块显存的抽象驱动负责管理它背后的物理页分配和映射。应用创建GEM对象后拿到的是一个handle通过handle可以映射到用户空间获得虚拟地址也可以导出成DMA-BUF fd分享给其他设备。在嵌入式平台尤其带MMU的SoC上用的最多的GEM实现是CMA helper也就是连续内存分配器。显示控制器做DMA时通常要求物理连续内存CMA区域正好解决碎片化问题。驱动里用drm_gem_cma_create创建对象以后通过dma_alloc_wc分配内存得到的是物理地址、内核虚拟地址、IOMMU设备地址三个地址。这三个地址在驱动里各有用途物理地址用来给需要物理地址的DMA控制器做地址转换内核虚拟地址用来给CPU读写framebufferIOMMU设备地址则是在开启IOMMU场景下给设备做DMA访问。U-Boot阶段的显示驱动和内核的DRM/GEM机制是两套体系。U-Boot里面没有GEM它通常直接分配一块连续内存当作framebuffer同时把这块内存的物理地址、大小等参数通过设备树或特定协议传给内核。内核如果支持simplefb就可以直接接管这块framebuffer继续显示实现U-Boot logo到内核console的无缝过渡。等到完整DRM驱动起来以后再重新分配显存把这个临时的simplefb替换掉。这块衔接逻辑在RK、NXP、全志等平台的BSP里都能看到。这也是为什么U-Boot阶段选framebuffer地址时特别忌讳随便挑一块内存用——一旦和内核其他模块的内存冲突启动阶段就会莫名其妙地死机。FBDEV兼容层之所以还存在是因为很多古老的应用和调试工具只认/dev/fb0你不可能要求所有用户的业务程序都改写成DRM接口。而且在内核启动早期DRM驱动还没完全初始化的时候FBDEV可以提前提供一个可用的显示输出方便排查启动问题。实际项目里我一般同时保留这两种接口DRM接口给新应用用FBDEV接口给老的测试程序和应急调试用。2. U-Boot阶段显示驱动的任务与设计约束2.1 U-Boot为什么也要管显示先回答一个很多人问过的问题U-Boot这种bootloader按理说引导内核启动就算完事了为什么还要花力气去搞显示驱动原因有三点。第一**开机画面Logo**是很多产品的硬需求。消费者买回家的设备通上电就应该看到品牌logo或者开机动画如果屏幕黑着十几秒体验会非常差。第二启动状态可视化非常有利于调试。开发阶段屏幕上能看到内核打印信息走到哪一步卡死一目了然不然就只能靠串口日志效率低太多。第三U-Boot和内核之间的显示接力需要一个桥梁。如果U-Boot阶段就把显示配置好了并且把framebuffer信息传给内核内核起来以后可以无闪烁地直接接管否则屏幕会先灭一下再亮非常影响观感。我举一个实际数据某项目里U-Boot阶段配好MIPI DSI屏幕并显示logo内核起来以后通过simplefb接管整个启动过程从按下电源键到进入桌面系统屏幕一直是亮的中间没有黑屏缝隙。如果没有U-Boot显示支持屏幕会大约黑1-2秒用户体感差异非常明显。2.2 U-Boot显示驱动的基本框架U-Boot的显示驱动框架整体比DRM简单得多它没有复杂的对象模型核心就是struct udevice加struct uclass_driver这套DMDriver Model机制。U-Boot里的显示驱动主要分三层顶层是显示驱动框架videouclass负责提供统一的显示接口比如video_puts输出文字、video_display设置显示模式。中间层是具体显示控制器驱动比如Rockchip的VOP驱动、NXP的DCU驱动它们实现backlight操作、mode设置、framebuffer地址配置等底层功能。再往下是显示接口驱动比如MIPI DSI controller驱动、HDMI驱动、LVDS驱动负责把上层的数据通过对应协议发出去。最底层是panel驱动负责初始化和控制具体的屏幕模组包括上电时序、初始化命令序列、背光控制等。这套分层的好处是明显的上层代码只需要调用uclass的标准接口无需关心底层是什么屏、什么接口。开发一个新的显示方案大多数情况下只需要写一个panel驱动VOP和DSI controller驱动复用现成的就行。以MIPI DSI屏幕为例U-Boot的显示初始化流程大致是static int dsi_panel_probe(struct udevice *dev) { struct panel_data *panel dev_get_plat(dev); struct udevice *dsi; int ret; ret uclass_get_device(UCLASS_VIDEO_BRIDGE, panel-dsi_id, dsi); if (ret) { debug(failed to get dsi device\n); return ret; } panel-dsi dsi; /* 1. 输出reset脉冲 */ panel_simple_reset(panel); /* 2. 上电时序先供VCC再供IOVCC */ regulator_set_enable(panel-vcc_supply, true); regulator_set_enable(panel-iovcc_supply, true); mdelay(panel-power_on_delay); /* 3. 初始化序列通过DSI发DCS命令 */ mipi_dsi_dcs_write_buffer(dsi, panel-init_seq, panel-init_seq_len); mdelay(120); /* 4. 打开背光 */ if (panel-backlight) { backlight_enable(panel-backlight); } return 0; }这套流程在DRM框架下用panel_simple_probe或panel_dsi_probe实现时底层逻辑基本是一致的差异主要在接口封装和资源管理方式上。3. U-Boot阶段DRM驱动解析核心环节3.1 设备树解析与platform适配U-Boot阶段对显示驱动的解析本质上是从设备树里找出display controller节点、DSI controller节点、panel节点并且把它们关联起来。这个过程用到的核心机制是U-Boot的uclass自动探测机制。以Rockchip平台为例U-Boot的设备树里通常这样组织dsi { compatible rockchip,rk3568-mipi-dsi; status okay; #address-cells 1; #size-cells 0; panel0 { compatible boe,tv080wum-nl0; reg 0; reset-gpios gpio3 RK_PB1 GPIO_ACTIVE_LOW; enable-gpios gpio3 RK_PB2 GPIO_ACTIVE_HIGH; backlight backlight; }; };内核匹配驱动的时候会沿着这个树结构逐级probe。先找到dsi节点匹配到U-Boot的DSI controller驱动然后遍历其子节点找到panel0节点匹配到panel驱动。这两个驱动的probe顺序是有讲究的DSI controller先probe因为它需要先初始化controller硬件panel驱动才能通过DSI总线发送命令。我在移植过程中遇到最多的坑是GPIO和regulator解析不出来。U-Boot对gpio-hog的支持、对regulator-fixed的模拟方式跟内核有差异有时设备树里写的内核没问题U-Boot就是找不到对应的clk或regulator。遇到这种情况建议先用dm tree命令查看系统的驱动模型树确认相关节点有没有被正确bind。U-Boot的DM框架会打印出所有设备节点的bind和probe状态这是排查设备树解析问题的第一利器。3.2 MIPI DSI panel初始化序列与命令传输MIPI DSI面板的初始化是U-Boot阶段驱动解析的核心难点之一。一块屏幕能不能点亮、颜色显示正不正常很大程度取决于初始化序列写得对不对。MIPI DSI是一种串行接口主机通过DCS命令和屏幕通信。初始化序列就是一系列DCS命令比如0x05进入睡眠模式、0x11退出睡眠模式、0x29打开显示等。大多数panel厂商会提供一个初始化序列这些序列通常以18 02 80 10这样的格式记录含义是发送一条长度2的命令命令内容为0x80 0x10。把厂商提供的初始化序列转成U-Boot能识别的数据格式看起来是个简单的活其实隐藏着不少细节。第一个坑是字节序和命令格式。有些厂商给的是大端格式有些是小端格式直接抄过去容易翻车。第二个坑是延时命令。初始化序列中有些步骤要求持续一定时间的延时比如退出睡眠模式以后要等120ms再发下一组命令。有些面板厂商会把延时也写成一条命令传给你的代码例如0x00 0x00代表延时需要认真解析并转成mdelay调用而不是把它真的当成DCS命令发出去。第三个坑是时序敏感度。某些屏对退出睡眠到发送第一帧数据之间的时间有严格要求太短了白屏太长了满屏雪花点。U-Boot里发送DCS命令的实际代码可以这样组织#define DSI_CMD_SLEEP 0x00 #define DSI_CMD_DELAY 0xFF struct dsi_cmd_seq { u8 cmd_type; u8 payload[32]; u8 len; }; static void panel_dsi_init(struct udevice *dsi) { struct dsi_cmd_seq init_seq[] { { DSI_CMD_SLEEP, {0x00}, 1 }, { DSI_CMD_DELAY, {120}, 1 }, // 120ms { DSI_CMD_SLEEP, {0x11}, 1 }, // exit sleep mode { DSI_CMD_DELAY, {120}, 1 }, { 0x06, {0xF0, 0x55, 0xAA}, 3 }, { 0x06, {0xF1, 0x55, 0xAA}, 3 }, // ... 其他厂商命令 { DSI_CMD_SLEEP, {0x29}, 1 }, // display on { DSI_CMD_DELAY, {30}, 1 }, }; for (int i 0; i ARRAY_SIZE(init_seq); i) { if (init_seq[i].cmd_type DSI_CMD_DELAY) { mdelay(init_seq[i].payload[0]); } else { mipi_dsi_generic_write(dsi, init_seq[i].payload, init_seq[i].len); } } }3.3 U-Boot中framebuffer的分配与显示刷新U-Boot里framebuffer的管理方式和内核DRM的GEM不同它简单直接得多。U-Boot在初始化video设备时会根据当前mode的width和height以及每像素字节数平均2或4字节分配一块内存区域作为framebuffer。很多平台还会把U-Boot的framebuffer地址固定下来而不是动态随机分配。这样做的原因是内核的simplefb需要知道这块物理内存的地址才能接管如果U-Boot每次启动都换地址内核设备树里就没法写死。举个例子某平台约定从物理地址0x10000000开始留出一块16MB内存给framebuffer那么U-Boot就把logo画在这儿内核simplefb读到这个地址后不需要做任何拷贝直接在这块framebuffer上继续输出屏幕完全不会闪烁。U-Boot显示刷新逻辑也有意思。它不像内核那样有完整的vsync同步机制很多U-Boot显示驱动里直接采用简单粗暴的整buffer刷新方式。比如video_sync接口的实现直接调用memcpy把缓存数据拷贝到framebuffer。这种方式虽然简单但效率很低如果屏幕分辨率高一帧数据几MB整帧拷贝很耗时。所以U-Boot里实际要显示动态内容比如开机动画时通常会做局部刷新优化只拷贝dirty区域。实操心得如果要在U-Boot里做复杂的开机动画建议别在framebuffer上折腾。直接把动画预渲染成多张图片通过U-Boot的boot_logo功能挨个刷控制好帧间隔效果比实时渲染稳定得多还省内存。3.4 从U-Boot到内核的显示交接simplefb与fdtU-Boot把屏幕点亮以后怎么让内核无缝接过去这是U-Boot阶段显示驱动解析的收尾环节也是决定屏幕会不会闪一下黑屏的关键。最常见的交接机制是simplefb。具体流程是U-Boot准备好framebuffer并显示画面后往传给内核的设备树DTS里动态添加一个simple-framebuffer节点该节点记录了framebuffer的物理地址、大小、宽高、色彩格式等信息。内核起来后在DRM正式驱动还没probe完成之前simplefb驱动会先认领这个节点把framebuffer内容继续显示在屏幕上保证显示不中断。等到正式DRM驱动初始化完成simplefb会被注销framebuffer管理权移交到真正显示驱动手里。U-Boot往里塞simplefb节点的代码大致长这样static void ft_fill_framebuffer(void *blob) { int nodeoff; nodeoff fdt_add_subnode(blob, 0, framebuffer10000000); if (nodeoff 0) return; fdt_setprop_string(blob, nodeoff, compatible, simple-framebuffer); fdt_setprop_u32(blob, nodeoff, width, fb_width); fdt_setprop_u32(blob, nodeoff, height, fb_height); fdt_setprop_u32(blob, nodeoff, stride, fb_stride); fdt_setprop_string(blob, nodeoff, format, r8g8b8a8); fdt_setprop_u32(blob, nodeoff, address, fb_addr); }移交过程中最容易遇到的问题是个“资源尺寸不匹配”simplefb解析到的stride和色彩格式要与U-Boot实际画的完全一致否则内核接管后画面会花屏或错位。第二个问题是framebuffer内存区域预留。这块内存必须在内核里预留出来不能被其他模块挪用。传统做法是给内核传memmap16M$0x10000000参数简单粗暴但在某些场景下有效。更稳妥的做法是让simplefb驱动请求/reserved-memory节点把framebuffer内存标记为reserved。这样内核其他模块就不会碰这块内存交接过程也干净利落。4. 竖屏模组改横屏显示从U-Boot到内核的配置实战4.1 物理竖屏装进横屏设备的场景分析最近一段时间被问得最多的问题是竖屏模组装进横屏结构后怎么改显示方向。这个需求在产品开发里特别常见窄边框的便携显示器、广告机、工控一体机经常手里只剩一批竖屏模组硬件结构又只能横着装软件上不做处理就只能看到歪着的画面。先理清这个问题的本质。物理竖屏比如720x1280装进横屏设备如果软件什么都不改画面输出就是720宽、1280高但设备结构是横向的用户看到的画面自动旋转了90度。解决思路无非两条路硬件改线或者软件旋转。硬件改线方案是在FPC排线层面动手把左右/上下的source线、gate线交换这样物理竖屏装上以后天然就是横屏显示。这种方案视觉效果最好、无性能损耗但需要模组厂配合定做排线周期长、成本高一般只适合批量很大的项目。软件旋转方案就是让显示控制器支持旋转输出或者直接在framebuffer层面做坐标变换。这种方案灵活不用动硬件适用于小批量或开发验证阶段。软件旋转又可以分三个层面实现应用层旋转界面直接按横屏设计、framebuffer旋转把内容转90度再写入显存、显示控制器旋转硬件旋转扫描方向。在Linux显示子系统里前两者好理解第三种是通过drm_plane的rotation属性来控制的这也是DRM框架比较方便的地方之一。4.2 U-Boot与kernel的旋转配置差异U-Boot阶段和内核阶段的旋转配置方式完全不同这也是很多人踩坑的地方。在U-Boot阶段显示驱动的目的是先亮屏、显示logo这时候做旋转最简单的方式是修改DTS里的面板宽高和扫描参数。比如720x1280的竖屏你把它当成1280x720的横屏来初始化如果panel初始化序列里没有写死source/gate方向U-Boot和显示控制器会用错误的时序去驱动这块物理竖屏很容易出现花屏、锯齿或者颜色错乱。这里我给出一个更稳妥的U-Boot处理思路保持物理分辨率不变让显示控制器直接支持旋转。在RK平台U-Boot里你可以通过修改VOP的dts配置或者驱动里的旋转寄存器来实现。比如VOP2支持afbc_rotation属性可以通过设置vop的rotate属性实现画面旋转90度。U-Boot部分比较关键的是要保证传入内核的simple-framebuffer节点信息与实际显示方向一致否则内核接管后画面突然反过来。内核阶段就灵活得多。DRM框架下的旋转通常有两种做法第一种是在DT里给plane节点加上rotation属性比如vop { plane { rotation DRM_MODE_ROTATE_90; }; };第二种是在用户态用libdrm或Wayland合成器来设置plane的rotation属性。前者是硬件旋转性能和效果都好但需要驱动支持后者是软件旋转通用性强但会增加CPU/GPU负载。在嵌入式设备上如果GPU能力足够我一般倾向用应用层或合成器层面旋转调式起来灵活不需要重新编内核。4.3 panel init序列对横竖屏的影响很多人调整横竖屏只盯着显示控制器的配置却忽略了panel本身。实际上很多竖屏模组之所以“默认竖着显示”是因为panel厂商写死了gate驱动方向。MIPI DSI的panel可以通过命令配置扫描方向不同厂商命令不一样有些面板控制器通过0x36命令设置Memory Data Access Control可以实现上下左右翻转和旋转但还有很多面板不支持这类命令。举一个具体例子。某项目的竖屏panel是720x1280通过DSI接口连接。它的init序列里有一条命令设置扫描方向0x36 0x00代表正常的竖屏扫描0x60代表横屏扫描MADCTL的MX和MY位组合而0xC0则代表上下左右完全翻转。如果你只是改了显示控制器的宽高而panel内部扫描方向没变就会出现画面方向正确但内容倒置的情况。两种转法叠加在一起效果反而错乱。所以做竖屏改横屏项目时我会建议按以下顺序排查先确认panel是否支持MADCTL类似命令。如果支持把扫描方向改对这是最底层的修正。再配置显示控制器的扫描方向或者宽高参数让数据读出来的顺序和panel匹配。最后考虑应用层的旋转逻辑如果硬件已经转对了应用层就不要再转否则会重复翻转。这里放一个我在RK平台DTS里实际用过的MIPI DSI横屏配置片段dsi { status okay; panel0 { compatible boe,tv080wum-nl0; reg 0; /* 竖屏720x1280物理上要做横屏 */ display-timings { native-mode timing0; timing0: timing0 { clock-frequency 69700000; hactive 1280; vactive 720; hback-porch 100; hfront-porch 68; vback-porch 20; vfront-porch 44; hsync-len 24; vsync-len 4; hsync-active 0; vsync-active 0; de-active 0; pixelclk-active 0; }; }; /* 通过MADCTL命令调整panel扫描方向 */ init-sequence [ 05 78 11 15 00 02 36 60 39 00 04 FF AA 55 A5 ... 05 14 29 ]; }; };这段配置里最关键的是hactive1280, vactive720与init序列中0x36 0x60配合使用。前者让显示控制器按横屏方式扫描后者让panel内部的gate/source方向也改成横屏扫描两个方向一致了画面才能正确显示。如果这里只改一个屏幕大概率花屏或者方向不对。提示MIPI DSI屏改成横屏以后记得同步检查触摸屏的坐标转换。这是屏转过来了但触摸没转的经典坑。触摸驱动里的坐标轴映射和panel扫描方向是两套独立逻辑改显示方向时必须一起改。4.4 热点问题延伸DRM数字激励器与DAM数字激励器的区别最近行业里在传drm数字激励器和dam数字激励器这两个术语有些工程师把它们当成显示领域的两个概念来讨论。这里需要澄清一下drm在这里并不是Linux Display Manager的缩写而是指数字信号处理中的数字激励器Digital Room Manager / Digital Exciter——它的作用是在音频信号中加入高频谐波来增强声音的临场感和清晰度多用于音响系统。而dam数字激励器应该是De-esser / Dynamics processor这类音频动态处理设备的误拼或缩写负责压缩、限幅、去齿音等处理。它们和Linux DRM显示子系统完全是两个世界的东西。大家在实际讨论中不要被搜索引擎的热词带偏方向看到drm数字激励器时先确定语境是音频设备还是Linux内核再往下聊。5. 常见问题与排查实战5.1 U-Boot阶段屏幕不亮的定位思路U-Boot阶段屏幕点不亮是显示驱动调试里最常遇到的入门问题解决思路其实是有章法的千万别一出问题就瞎猜。我每次会按下面这个顺序排查第一步看供电。panel有没有VCC、IOVCC、VLED三路供电电源轨有没有按要求的时间顺序上电。用万用表测是最直接的测不到正常电压说明供电电路根本没工作先查regulator配置和硬件。第二步看时序。供电波形是否正确reset引脚有没有按规格书要求拉低、延时、拉高。很多panel对上电时序有严格要求VCC起来后至少10ms才能拉resetreset低电平要保持至少10ms这些参数在规格书里都有拿示波器测一下是否满足。第三步看时钟。MIPI DSI的clock lane有没有输出用示波器看clock lane是否有波形。如果U-Boot里已经发了初始化命令但clock不出很可能是PLL配置没生效或者lane数量配置不对。前三步都没问题时再抓初始化命令的返回状态。U-Boot的MIPI DSI驱动通常支持读回panel的回复比如通过mipi_dsi_dcs_read读取一些寄存器值来确认panel有没有正确接收命令。如果你是用的RK平台还可以在驱动里把DSI的状态寄存器打印出来确认是不是处于正常状态。如果这些全查完还是黑屏最后一招是怀疑panel本身。拿一颗已知完好的panel替换测试如果替换后能点亮问题在自己这块panel上多半是静电损坏开箱即废的也有。5.2 framebuffer地址冲突导致的启动卡死U-Boot阶段framebuffer地址选不好最典型的症状就是单独启动U-Boot没问题单独启动内核也没问题但只要从U-Boot跳到内核系统就死机或者起不来。这种问题十有八九是U-Boot的framebuffer和内核的内存管理碰撞了。举个例子。某平台U-Boot把framebuffer放在0x10000000大小16MB但内核设备树里的内存节点把0x10000000到0x20000000这段空间作为普通可用内存分配。内核起来后页分配器很可能把这块区域分配给驱动或业务进程使用而显示控制器仍在对这块内存做DMA读操作结果就是写坏了别人的内存或者自己的显示数据被别人覆盖表现就是随机死机、花屏、系统异常。解决思路有两个一是U-Boot里把framebuffer放到内核保留区的低地址处比如保留给IOMMU、ATF、ramdisk的区域之前二是通过/reserved-memory节点把这个区域固定下来让内核无论如何不会去动它。在设备树里增加reserved-memory是更标准、更推荐的做法/reserved-memory { #address-cells 2; #size-cells 2; ranges; framebuffer10000000 { compatible shared-dma-pool; reusable; reg 0x0 0x10000000 0x0 0x01000000; }; };加了reusable属性以后内核在系统内存紧张时仍可使用这块内存但会确保显示控制器不在使用该区域时才分配出去比单纯用no-map更灵活。5.3 内核logo到console切换时的闪屏问题U-Boot logo显示正常内核起来后终端console刚开始打印时出现闪屏、黑屏、画面跳变这个现象在开发中也极其常见。它背后其实是simplefb和正式DRM驱动之间的交接没做好。最直接的原因是simplefb被注销的时机太早或者说正式DRM驱动的probe还没完成显示输出simplefb就已经退场了。内核的Drivers核心加载顺序、解耦挂起的时机、地区显示输出的初始化顺序这些都可能影响。如果发现黑屏间隙先检查dmesg里simplefb的移除日志和DRM驱动的加载日志之间的时间关系。另一种常见原因是分辨率或者时序切换。DRM驱动probe成功后按DTS里的display-timings重新配置屏幕如果新配置的timing和U-Boot下用的不一样面板就会重新初始化一次屏幕黑一下再亮。解决思路是让U-Boot下和kernel DTS里用同一组timing参数这样就不需要重新切换mode。但要注意即便参数相同有些驱动代码也会强制执行一次panel disable/enable流程这就需要改驱动在panel已经初始化过的情况下跳过重复初始化U-Boot阶段通过ATPAdvanced Transmit Path表或者特定属性传递状态都可以做到。5.4 DRM运行时常见的MIPI DSI信号质量问题MIPI DSI跑起来以后出现“偶尔花屏”、“边缘有雪花噪点”、“刷新到某个区域就闪”多半不是软件逻辑问题而是信号完整性问题。很多工程师在驱动上死磕很久最后发现是硬件布局的问题。常见信号质量问题有几类lane间延时差过大造成数据采样错误阻抗不匹配导致信号反射高速时钟下形成振铃电源纹波太大影响PHY的锁相环稳定表现为间歇性闪烁EMI干扰导致面板偶发误判命令。排查手法除了示波器看眼图还可以通过调低DSI时钟频率来验证。比如从800Mbps降到600Mbps如果花屏概率大幅降低基本可以确定是信号质量问题而不是软件bug。这时候再去检查MIPI走线的等长控制、阻抗匹配、参考地是否完整才能根本解决。软件上能做的只是加一些重传机制、降低频率、调整PHY的drive strength这些减缓措施信号质量问题最终还是要靠改板子。5.5 常见问题速查表现象可能原因排查方向U-Boot阶段屏幕完全不亮供电、时序、时钟链路问题测电压、测时序、测MIPI时钟U-Boot logo有显示但花屏framebuffer stride配置错误检查stride、宽高、像素格式是否匹配logo位置偏移或图像撕裂framebuffer物理地址不对核对BSP里U-Boot和内核的地址约定内核起来后闪一下黑屏simplefb注销和DRM probe的交接问题查dmesg时序统一timing参数DRM下画面倒置或旋转方向不对panel MADCTL和显示控制器方向不匹配先改panel扫描方向再改控制器偶尔花屏/雪花点MIPI DSI信号完整性问题调低频验证、查PCB走线、查电源纹波触摸方向不对触摸驱动坐标映射与显示方向不一致同步修改触摸轴映射6. 写在最后的一点经验做显示驱动这些年最深的体会是显示问题往往不是单一模块的问题而是供电时序初始化序列数据路径同步机制五重奏任何一个环节脱节都会表现出类似的症状——黑屏、花屏、闪烁。所以调试时不要一上来就认定是驱动代码问题先理清整个数据链路再逐段验证。U-Boot阶段的显示驱动尤其容易被轻视。大家总觉得反正内核起来以后一切都会变好但实践下来U-Boot阶段的显示调试反而是整个项目里最花时间的地方因为它缺少内核那么多好用的调试工具全靠示波器、print和几分耐心。把U-Boot阶段这条链路理清楚了内核阶段的DRM驱动理解起来反而顺理成章。我自己还保留一个习惯每次换新平台、新panel先把U-Boot阶段显示打通再谈其他外设。因为显示是一眼就能看得见的功能屏幕亮了就能确认基本系统在运行同时有个可视化的调试界面后续调内存、调存储、调网络都会舒服很多。希望这篇基础框架和U-Boot阶段的内容能帮到正在和屏幕上较劲的同行内核态DRM驱动的完整解析我们下一篇接着聊。
返回列表