去年拿到一块龙芯 3A5000 的开发板,系统起来不算难,麻烦的是图形界面。Xorg 日志翻来覆去看了好几遍,libdrm 也重编过,最后折腾了一圈才发现:问题根本不在用户态,而是内核里那块显示控制器驱动没有按预期工作。这一步步把问题挖到底之后,LSDC 这个名字才真正进入了我的视野。
LSDC 是龙芯 SoC 里显示控制器的缩写,也是 Linux 内核 DRM 子系统下对应驱动的名字。这篇文章就把我对 LSDC DRM 驱动的理解、源码层面的分析思路、以及实际调试时踩过的坑完整记录下来,给同样在做龙芯平台显示适配、或者想搞清楚一个 DRM 显示驱动是怎么从头搭起来的读者做个参考。
1. 为什么我建议入手龙芯显示适配时先读懂 LSDC
1.1 一块开发板的黑屏把我引到了 DRM 驱动层
先说个具体的场景。板子启动后,系统日志一切正常,串口能登录,但接上 HDMI 显示器就是没有信号。按常规思路,先怀疑 Xorg 配置,换过 xf86-video-modesetting,也试过不加载任何 xf86-video 驱动;再怀疑用户态渲染,直接跑modetest看内核输出能力,结果 modetest 根本枚举不出任何连接器。
这时候排查范围已经缩到内核了。lsmod里能看到驱动模块在,但dmesg | grep -i lsdc一看,驱动 probe 阶段就报错了。这个经历让我意识到一件事:在龙芯这种 SoC 平台上,显示链路从上到下的分层比普通 x86 平台更要靠底层驱动兜底,而 LSDC 就是这条链路里最接近硬件的那一环。
1.2 LSDC 这个名字背后的实际含义
龙芯 SoC 的图形方案分两块:一部分型号内部有独立的 GPU 核心,另一部分只带一个纯粹的显示控制器。LSDC 要管的就是后者——它本身不参与 3D 渲染,只管把内存里的帧数据按一定时序送到显示接口上。
换句话说,LSDC 驱动要做的事非常聚焦:驱动初始化、显存对象管理、模式设置、输出管线的连接与探测。它不需要实现复杂的 GPU 调度,也不需要 command buffer 提交,这让它的代码量比常见 GPU 驱动小很多,反而适合做 DRM 驱动入门和移植参考。
但"量小"不等于"简单"。显示控制器直接操作寄存器,所有的初始化顺序、时序计算、中断处理都要精确匹配硬件规格,任何一个疏忽都会直接表现为黑屏或者花屏,而且这种问题很难通过用户态手段定位。
1.3 读这份驱动的正确姿势
如果你之前只写过 userspace 程序,第一次打开内核 DRM 驱动源码可能会觉得绕:IOCTL 入口、文件操作、回调函数、原子提交……一层包一层。我的建议是不要按源码文件的顺序从头读到尾,而是按运营链路来读:先搞清设备是怎么被发现的,再追一个 framebuffer 从用户态提交到屏幕上显示要经过哪些代码路径,最后再回来看初始化细节。
接下来我会按照这个思路来拆解。
2. DRM 框架下的 LSDC:四个核心对象与两条分界线
2.1 Plane、CRTC、Encoder、Connector 在 LSDC 里的映射
DRM 框架把显示硬件抽象成四类对象:Plane、CRTC、Encoder、Connector。用大白话解释:
- Plane 是画面图层,比如主图层、光标图层,它把一块显存区域和屏幕上的显示区域关联起来。
- CRTC 是显示时序发生器,负责产生像素时钟、行场同步信号,决定当前屏幕显示的是哪个 Plane。
- Encoder 是信号编码转换单元,把 CRTC 输出的并行 RGB/时序信号转成 HDMI、VGA 或其他接口需要的信号。
- Connector 对应物理输出接口,负责探测有没有显示器连接、读取 EDID、返回显示器支持的显示模式。
LSDC 驱动的输出链路基本也是这套模型。你在代码里会看到struct lsdc_crtc、struct lsdc_encoder、struct lsdc_connector这一层封装,它们内部嵌着 DRM 框架定义的公共结构体,再通过 container_of 互相访问。
理解这个映射关系有什么用?Debug 的时候你能迅速判断问题出在哪个环节:同样是黑屏,CRTC 没使能、Encoder 没通电、Connector 没探测到显示器,表象可能完全一样,但排查路径完全不同。我后来定位黑屏问题时,第一件事就是问自己:modetest 里到底有没有列出 connector?没有——那就别急着查时序参数,先查管线枚举。
2.2 用户态与内核态的 ioctl 边界
LSDC 驱动负责接收用户态通过/dev/dri/card0发来的系统调用。libdrm 把drmModeGetResources、drmModeSetCrtc、drmModePageFlip这类操作封装成 ioctl,内核侧的drm_ioctl再根据命令字分发给驱动设置的回调。
这里有一条重要分界线:**DRM 核心框架处理通用的对象管理和资源分配,LSDC 驱动只需要实现硬件相关的回调。**比如 framebuffer 的创建、对象的引用计数、atomic 状态管理这些活儿,框架都帮你干了;LSDC 要做的只是实现struct drm_driver里的gem_create_object、prime_fd_to_handle,以及在 CRTC/Plane 回调里把具体的寄存器参数算好、写进去。
所以读 LSDC 源码不需要从头一个函数一个函数啃,先找到struct drm_driver定义,看它注册了哪些回调,哪些是驱动自己实现的,哪些是空操作,整个驱动的边界就清楚了。
2.3 寄存器资源的获取方式
驱动能干活的前提是能访问到硬件寄存器。LSDC 通常以 platform device 的形式挂在设备树里,设备树节点会描述寄存器起始地址、中断号等信息。驱动 probe 时,通过平台资源接口拿到这些信息,再映射到内核虚拟地址空间。
典型的代码风格大致如下:
static int lsdc_platform_probe(struct platform_device *pdev) { struct resource *res; void __iomem *reg_base; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); reg_base = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(reg_base)) return PTR_ERR(reg_base); /* 后续通过 reg_base + offset 访问各控制器寄存器 */ }这里有个经验值得提:内核里看到的寄存器地址已经经过了 CPU 地址映射,千万不要直接拿设备树里的物理地址当作驱动里 I/O 读写地址用。另外,显示控制器通常有多个功能块,比如主显示控制、HDMI 接口控制、I2C 控制器,它们在寄存器空间里各占一段偏移。驱动头文件里会维护对应的偏移定义,读源码时建议对照数据手册一起看,否则很难理解那些魔法数字的含义。
3. 从 probe 到上屏:初始化调用的关键链路
3.1 probe 阶段的标准舞步
一个 DRM 驱动的初始化流程,在框架层面其实有固定套路。LSDC 的 probe 函数大致遵循以下顺序:
- 先做硬件资源准备,映射寄存器、获取中断、初始化时钟相关配置。
- 分配并初始化
struct drm_device,把驱动私有数据挂进去。 - 注册 DRM 驱动回调,比如文件操作、GEM 相关的钩子。
- 创建各个输出组件(Plane/CRTC/Encoder/Connector)并建立连接关系。
- 调用
drm_dev_register让设备对用户态可见。 - 如果启用了 fbdev 模拟层,再初始化一下兼容 framebuffer。
这个顺序看起来平淡,但每一步的顺序都是有讲究的。比如第 4 步必须在第 5 步之前完成,因为drm_dev_register之后用户态就能打开设备了,如果组件还没注册好,用户态 ioctl 问框架要资源列表时只能拿到一个残缺的管线。
3.2 输出管线的搭建顺序
输出组件的创建顺序同样值得琢磨。我读驱动时注意到一个特征:Plane 和 CRTC 的初始化通常会放在 Encoder 前面。这背后的逻辑是——CRTC 要绑定到它支持的 Plane 上,在 DRM 的 atomic 框架里,Plane 和 CRTC 的关联关系需要提前建立起来。
用表格简化一下各组件在初始化阶段最常见的职责:
| 组件 | 初始化阶段主要任务 | 后续运行时的关键回调 |
|---|---|---|
| Plane | 声明支持的像素格式、创建属性 | atomic_update(把 framebuffer 地址写入寄存器) |
| CRTC | 保存硬件上下文、初始化时序链 | mode_set(计算并写入时序)、enable/disable(开关输出) |
| Encoder | 关联 CRTC 与 Connector | enable(启动信号链路)、atomic_check(校验连接关系) |
| Connector | 注册物理接口、获取 EDID 相关方法 | detect(探测显示设备)、get_modes(返回可用模式列表) |
在 LS2K2000 这类型号上,驱动甚至可能支持不止一个显示输出,比如 HDMI 和 VGA 各有一个 Connector。多个输出同时存在时,初始化流程会更复杂:要保证每个 Connector 都能通过 Encoder 找到对应的 CRTC,同时不能被框架错误地串线。这一步稍有疏漏,就会出现独显启动后 HDMI 有信号但 VGA 无信号这类"缺一路"的怪问题。
3.3 fbdev 兼容层:让字符终端也能显示
很多人不理解为什么 DRM 驱动里还带着fbdev emulation这种东西。原因很现实:系统启动阶段,引导程序要显示 logo,内核崩溃时要打 oops 到屏幕上,这些场景依赖一个简单直接的显示接口,而不是完整 DRM 框架。
LSDC 驱动通常会注册一个drm_fbdev_emulation或类似机制,在 DRM 设备之上模拟一个 /dev/fb0。这样一来,开机 logo、字符终端、甚至一些老的显示工具都能正常工作,而真正的图形栈(例如 Xorg 或 Wayland 合成器)仍然通过 DRM 主设备工作。
调试时这个兼容层特别有用。如果/dev/fb0上 printf 的内容能正常显示,说明底层显示管线已经通了,问题很可能出在用户态图形栈;如果 fb0 都黑屏,直接去查驱动初始化也不迟。
4. 一帧画面的旅程:从 DRM Framebuffer 到像素时钟
4.1 GEM 对象、显存与 framebuffer 的关系
用户态要显示一张画面,首先要创建一块放置像素数据的缓存。在 DRM 框架里,这块缓存被封装成 GEM 对象;再给 GEM 对象套上格式、分辨率等描述信息,就变成了 DRM Framebuffer。
LSDC 驱动平时用的是主平面,它把 Framebuffer 对应的内存地址取出来,通过 Plane 的更新回调写到显示控制器的起始地址寄存器里。这里要注意的是,显示控制器在扫描输出时访问的是物理地址,所以驱动必须保证这块内存在 DMA 访问期间是有效的,并且地址对齐满足硬件要求——这也是下一章要讲的显存分配问题的由来。
4.2 模式设置与时序计算
模式设置是 CRTC 回调里最重要的一块逻辑。当用户在 Xorg 或者通过drmModeSetCrtc切换分辨率时,内核会走一遍 atomic 模式设置流程。LSDC 得到struct drm_display_mode之后,要把它换算成自己控制器能认的时序参数。
换算的核心是把显示模式里的水平/垂直时序参数(active、blank 起始、sync 偏移等)转换成寄存器值。举个直观的例子:一个 1920x1080@60 的模式,实际行周期不是只有 1920 个像素,还要算上前后肩、同步脉冲,最终大概落在 2200 个像素时钟周期左右。像素时钟频率、Htotal、Vtotal 这三个参数决定了最终的刷新率。
这一部分出错最常见的结果不是死机,而是"画面偏移""屏幕左右滚动"这类看起来很奇怪的现象。说实话我第一次遇到屏幕右移出一大块黑边时完全没往时序寄存器想,后来对照数据手册逐个字段排查才发现是同步脉冲宽度配置不对。
4.3 Plane 更新与帧切换
LSDC 扫描显示时,默认读取的是主平面指到的那块显存地址。如果显示内容要更新,用户态只需要把新的 Framebuffer 提交上来,触发 Plane 的 atomic_update 回调,驱动在这里把新的 Framebuffer 地址写入控制器。
这里有一个关键细节:**写显存地址的时机要跟硬件扫描同步。**如果在控制器刚好扫到屏幕中间时改了显存地址,用户就会看到撕裂(tearing)。常见做法是等垂直消隐期再更新寄存器,或者启用控制器的双缓冲机制。LSDC 驱动设计中,帧切换路径上通常会把这种同步逻辑考虑进去,用户态配合 PageFlip 也能实现无撕裂的流畅切换。
4.4 连接器探测与 EDID 读取
显示器插入、拔出、开机时,内核需要对 Connector 做一次探测。探测的核心动作是:通过连接器对应的 I2C 通道读显示器 EDID,解析出它支持的分辨率和刷新率列表,再交给用户态去选择。
LSDC 驱动里往往也包含一个 I2C 适配器实现——可能挂在显示控制器的 I2C 控制器之上,也可能操作 GPIO 模拟 I2C。这部分代码虽然不起眼,但经常是"HDMI 为什么识别不到显示器"的根源。一开始我不理解为什么显示驱动还要管 I2C,后来调试一台电视不识别的问题,查到最后就是 EDID 读取时序没有按显示器的要求来。
5. 显存管理设计:LSDC 如何选择 TTM/GEM
5.1 龙芯 SoC 的显示内存格局
LSDC 驱动的显存管理之所以值得单独讲,是因为龙芯低端 SoC 的显示控制器大多没有独立显存。它扫描输出的数据要从系统内存里来,这就带来两个要求:内存物理连续、DMA 可访问。
这跟带独显 GPU 的平台完全不同。独显驱动管的是板载 VRAM,显存分配逻辑相对独立;LSDC 要管的则是"把系统内存里某一段划给显示控制器用"。如果这段内存不连续,或者分配时没有考虑 DMA 地址宽度限制,显示就会出现花屏甚至完全无法输出。
5.2 GEM 与 TTM 的取舍
DRM 框架下有两套显存管理思路:GEM 和 TTM。GEM 是通用图形内存管理,主打简洁;TTM 则能管理显存/系统内存之间的换入换出,多用于独立显卡。
LSDC 这种"没有独立显存"的显示控制器,通常更适合 GEM 路线:分配本质上就是dma_alloc_attrs或者drm_gem_cma_*,拿到一块物理连续的内存,然后按 GEM 对象包装起来,交给用户态做 mmap 或者显存导入导出。
如果驱动选择 TTM,一般是像样的独立 GPU 或者模拟 GPU 才会用到。LSDC 类驱动如果用 TTM 但又不做换出,那纯粹是给自己增加复杂度。读代码时,看到驱动用的是哪个管理模型,基本就能判断出作者的侧重点。
5.3 大块连续内存的分配实践
LSDC 需要的是一大块连续物理内存,这块内存要能被显示控制器直接按地址访问。实际驱动代码里常见的路径是:在 GEM 对象创建时走 DMA 分配器,得到总线地址和分配大小,然后把它映射成用户态可写的虚拟地址。
这里有一个容易踩的坑:**调试时要留意 32 位与 64 位地址空间的问题。**显示控制器的 DMA 地址宽度未必跟 CPU 一致,如果 SoC 的显示控制器只能寻址低 32 位物理地址,而系统内存分配出来的 DMA 地址落在 4G 以上,那即使驱动"看起来"分配成功了,显示画面依然是不确定的。好在现代龙芯平台大多已经放宽了地址宽度,但在做老旧板卡适配时,还是要主动确认这个细节。
6. 调试 LSDC 驱动的四个关键位置与踩坑记录
6.1 先看 dmesg 中的 lsdc 加载记录
调 LSDC 的第一步永远是dmesg | grep -i lsdc。驱动 probe 阶段如果失败,一般会留下比较明确的错误日志,比如资源获取失败、某个寄存器读写结果不符合预期、设备树 compatible 没匹配上。
如果 kernel log 里完全没有 lsdc 字样的记录,就要往前查:是不是设备树节点没使能?驱动是否编进了内核?模块是否被加载?我有一次遇到的"驱动不工作"到最后竟然是内核 config 里把模块编成了 built-in,但设备树 compatible 字符串没对上,整个 probe 回调直接被跳过了。
6.2 用 modetest 验证输出管线
用户态最趁手的探查工具是modetest,它是 libdrm 自带的测试程序。运行modetest -c能列出 Connector;modetest -p能看 Plane 信息;modetest -M可以指定驱动名称。
一个典型的验证流程是:先确认 Connector 存在,再确认它能在插上显示器之后枚举出正确的 mode 列表,然后用modetest -s指定 connector 和分辨率做一次测试显示。如果这步能点亮屏幕,说明从 GEM 到 CRTC 再到 Connector 的整条链路是通的;如果这步不行,再逐层拆。
我还习惯配合/sys/kernel/debug/dri/0/下面的调试节点来看寄存器或状态。很多 DRM 驱动在 debugfs 里导出了每个组件的状态字符串,对快速判断"驱动认为当前处于什么状态"非常有用。
6.3 黑屏、花屏定位顺序
遇到黑屏,我通常按这个顺序排查:
- 串口/日志层面确认驱动加载成功,无 probe 错误。
dmesg里看 Connector 是否检测到显示器,EDID 是否读到。- 用 modetest 手动触发一次模式设置,确认 CRTC 是否使能、时钟是否输出。
- 检查显存地址与 framebuffer 内容(可以在调试节点里 dump 一小块内存确认写入的像素数据正确)。
- 最后才是怀疑硬件链路,比如 HDMI 线缆、转接器、显示器输入源。
花屏则通常指向显存参数:要么内存地址不对,要么 stride 行距没对上。常见错误是把 1920x1080 的 stride 按 1920 算,忘了每个像素占 4 字节,结果画面整体切斜。
6.4 移植到新板卡时最容易忽略的环节
如果你想把 LSDC 驱动从参考板移植到新板卡,最值得注意的不是驱动源码本身,而是设备树描述。寄存器基地址、中断号、时钟来源、复位信号的描述都要跟着新板子重新核对。尤其是显示涉及到的时钟树,很多 SoC 的显示控制器需要外部源配置好正确的像素时钟才能工作,这种配置往往放在 bootloader 或者设备树 clk 节点里,驱动本身只管寄存器而不管 PLL 初始化的具体策略。
另外提醒一点:不同型号龙芯 SoC 的显示控制器寄存器偏移可能并不完全一致。移植时不要想当然地认为参考板上的偏移定义在新芯片上也一样,最好逐个功能块对照新芯片的数据手册确认,否则会因为一个看似不起眼的寄存器位被写错,换来一个莫名其妙的黑屏。
最后分享一个我自己养成的调试习惯:拿到一块新板子,我会只做最小化的验证——先把内核默认 framebuffer 的 logo 成功显示出来,再把 modetest 的测试画面点亮,这之后再考虑跑完整的图形栈。这两步通过,基本能判定 LSDC 驱动层的初始化、模式设置、显存分配都正常,剩下的问题就往用户态丢。别一上来就并行排查,否则黑屏问题很容易变成五六个变量搅在一起的泥潭。