
1. 为什么我要在Linux上折腾libdrm显示程序第一次接触DRM是在调试一块嵌入式开发板的屏幕点亮问题时。当时屏幕背光亮了但画面死活出不来串口日志里反复刷着drm相关的报错。那时候我对DRM的认知基本为零只知道它跟显卡驱动有关具体怎么用、怎么调完全是一头雾水。后来花了大概两周时间从阅读内核文档到翻libdrm源码再到自己写了一个最小可运行的显示程序才算真正把这条链路走通。这篇文章就是把我踩过的坑和最终跑通的方案完整记录下来。核心关键词是libdrm、DRM、Linux、显示程序和代码目标读者是那些需要在Linux环境下直接操作显示硬件的开发者比如做嵌入式显示、KMS调试、或者想理解图形栈底层原理的朋友。你不需要有图形学背景但需要熟悉C语言和基本的Linux系统编程。DRM全称Direct Rendering Manager是Linux内核中负责管理显示控制器和GPU的子系统。它向上提供统一的接口让用户态程序可以控制显示模式设置、帧缓冲管理、GPU命令提交等。而libdrm是DRM的用户态库封装了ioctl调用让我们不用直接跟内核打交道就能操作显示硬件。简单说内核里的DRM是大脑libdrm是手脚我们的程序通过libdrm告诉大脑该显示什么。为什么不用X11或者Wayland因为在很多嵌入式场景下根本没有完整的桌面环境你需要直接控制显示管线。比如工业HMI、车载仪表、广告机这些设备往往只需要一个全屏画面用DRM直接操作反而更轻量、更可控。而且理解了DRM再去看X11或Wayland的底层实现很多概念就豁然开朗了。下面我会从环境准备开始一步步搭建一个能在屏幕上显示纯色画面的程序。代码是完整的可以直接编译运行。但更重要的是我会解释每一步为什么这么做以及实际调试中可能遇到什么问题。2. 动手前的环境确认与依赖梳理2.1 检查内核DRM支持是否就绪在写代码之前必须先确认你的Linux内核已经启用了DRM并且对应的显示驱动已经加载。这一步很多人会跳过结果程序跑起来报Operation not permitted或者找不到设备节点白白浪费时间。打开终端先看看/dev/dri/目录下有什么ls -la /dev/dri/正常情况下你会看到card0、renderD128这样的设备节点。card0是显示控制节点renderD128是渲染节点。如果这个目录不存在说明内核没有加载DRM驱动或者驱动加载失败了。接着用dmesg看看驱动加载情况dmesg | grep -i drm你会看到类似这样的输出[drm] Initialized i915 1.6.0 20201103 for 0000:00:02.0 on minor 0 [drm] Initialized vkms 1.0.0 20191101 for vkms on minor 1这说明i915驱动Intel集成显卡和vkms虚拟KMS驱动都加载成功了。如果你用的是其他平台可能会看到msm高通、rockchip、sun4i等驱动名。注意如果你在虚拟机里跑可能需要先启用3D加速否则/dev/dri/下可能什么都没有。另外有些发行版默认不加载vkms需要手动modprobe vkms。2.2 安装libdrm开发包确认DRM就绪后接下来安装libdrm的开发文件。不同发行版的包名略有差异# Debian/Ubuntu系 sudo apt-get install libdrm-dev # Fedora/RHEL系 sudo dnf install libdrm-devel # Arch系 sudo pacman -S libdrm安装完成后检查头文件和库文件是否存在ls /usr/include/libdrm/ ls /usr/lib/x86_64-linux-gnu/libdrm.so*你应该能看到drm.h、drm_mode.h、xf86drm.h、xf86drmMode.h这些头文件以及libdrm.so和libdrm.so.2等库文件。这里有个细节值得说一下libdrm的API分为两层。底层是drm.h和xf86drm.h提供通用的ioctl封装上层是drm_mode.h和xf86drmMode.h提供KMSKernel Mode Setting相关的模式设置接口。我们做显示程序主要用上层接口但偶尔也需要底层接口来查询能力。2.3 权限问题为什么你的程序打不开设备节点很多人在这一步会遇到Permission denied。原因是/dev/dri/card0默认属于video组普通用户没有访问权限。解决办法有两个一是把当前用户加入video组sudo usermod -aG video $USER然后重新登录生效。二是直接用sudo运行程序但这样调试起来不方便而且有些环境下sudo会改变库搜索路径反而引入新问题。我个人的习惯是加入video组然后用普通用户调试。如果是在容器里跑记得把/dev/dri/设备映射进去并且确保容器内的用户有对应权限。提示如果你用的是Wayland桌面当前用户可能已经通过logind拿到了设备访问权限但直接跑程序仍然可能失败因为DRM master被桌面占用了。这种情况下建议切换到TTYCtrlAltF3再运行或者用vkms虚拟设备测试。3. 拆解一个最小DRM显示程序的完整骨架3.1 打开设备与获取资源句柄任何DRM程序的第一步都是打开设备节点获取一个文件描述符。这个fd就是后续所有操作的入口。#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/mman.h #include xf86drm.h #include xf86drmMode.h int main(int argc, char **argv) { const char *card /dev/dri/card0; int fd open(card, O_RDWR | O_CLOEXEC); if (fd 0) { perror(open card); return -1; } printf(Opened %s, fd%d\n, card, fd); // ... 后续操作 close(fd); return 0; }O_CLOEXEC标志很重要它保证在exec新程序时自动关闭这个fd避免泄漏到子进程。虽然我们这个简单程序用不到但养成习惯是好的。打开设备后需要获取DRM资源的句柄。所谓资源包括CRTC、连接器connector、编码器encoder、帧缓冲framebuffer等。用drmModeGetResources一次性拿到drmModeRes *res drmModeGetResources(fd); if (!res) { perror(drmModeGetResources); close(fd); return -1; } printf(CRTCs: %d, Connectors: %d, Encoders: %d\n, res-count_crtcs, res-count_connectors, res-count_encoders);drmModeRes结构体里包含了各类资源的ID数组。这里先打印数量确认设备确实有显示资源。如果count_crtcs为0说明这个设备不支持显示输出可能只是个渲染节点。3.2 找到能用的连接器和CRTC连接器代表物理显示接口HDMI、DP、MIPI DSI等CRTC代表显示控制器。一个CRTC可以驱动多个连接器但同一时刻通常只驱动一个。我们需要找到一个已连接且状态为connected的连接器然后为它分配一个CRTC。drmModeConnector *conn NULL; for (int i 0; i res-count_connectors; i) { drmModeConnector *c drmModeGetConnector(fd, res-connectors[i]); if (c c-connection DRM_MODE_CONNECTED c-count_modes 0) { conn c; break; } if (c) drmModeFreeConnector(c); } if (!conn) { fprintf(stderr, No connected connector found\n); drmModeFreeResources(res); close(fd); return -1; } printf(Using connector %d, modes: %d\n, conn-connector_id, conn-count_modes);这里有个容易踩的坑drmModeGetConnector返回的对象需要手动释放否则会内存泄漏。而且不是所有连接器都有有效模式必须检查count_modes 0。接下来选择显示模式。conn-modes数组里按优先级排列第一个通常是首选模式preferred mode也就是显示器的原生分辨率。我们直接用第一个drmModeModeInfo *mode conn-modes[0]; printf(Mode: %s %dx%d%d\n, mode-name, mode-hdisplay, mode-vdisplay, mode-vrefresh);然后找一个CRTC。最简单的方法是遍历res-crtcs用drmModeGetCrtc检查哪个CRTC当前没有绑定连接器或者直接选第一个可用的。更严谨的做法是检查CRTC的possible_crtcs位掩码但为了简单起见我们选第一个uint32_t crtc_id res-crtcs[0]; drmModeCrtc *crtc drmModeGetCrtc(fd, crtc_id); if (!crtc) { perror(drmModeGetCrtc); // 清理资源... return -1; }注意如果CRTC已经被其他程序占用比如桌面环境drmModeSetCrtc会失败并返回EBUSY。这时候要么切换到TTY要么用drmDropMaster释放控制权但后者需要你先成为DRM master。3.3 创建帧缓冲并映射内存有了CRTC和模式信息接下来创建帧缓冲。帧缓冲就是一块内存区域里面存放着要显示的像素数据。DRM要求帧缓冲通过drmModeAddFB注册并返回一个fb_id。首先创建一个dumb buffer哑缓冲这是DRM提供的一种最简单的内存分配方式struct drm_mode_create_dumb create {0}; create.width mode-hdisplay; create.height mode-vdisplay; create.bpp 32; // 每像素32位即ARGB8888 if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, create) 0) { perror(DRM_IOCTL_MODE_CREATE_DUMB); // 清理... return -1; } printf(Dumb buffer: handle%u, pitch%u, size%llu\n, create.handle, create.pitch, (unsigned long long)create.size);create.pitch是每行字节数通常等于width * 4但可能因为对齐要求而更大。create.size是总字节数。然后用这个dumb buffer创建帧缓冲uint32_t fb_id; if (drmModeAddFB(fd, create.width, create.height, 24, 32, create.pitch, create.handle, fb_id) 0) { perror(drmModeAddFB); // 清理... return -1; } printf(Framebuffer ID: %u\n, fb_id);drmModeAddFB的参数依次是fd、宽、高、深度24位色、bpp32位、pitch、handle、输出fb_id。深度和bpp的区别容易搞混深度指颜色位数bpp指每像素占用的总位数包括alpha通道。接下来把dumb buffer映射到用户空间这样我们才能往里面写像素struct drm_mode_map_dumb map {0}; map.handle create.handle; if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, map) 0) { perror(DRM_IOCTL_MODE_MAP_DUMB); // 清理... return -1; } uint8_t *fb_map mmap(0, create.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map.offset); if (fb_map MAP_FAILED) { perror(mmap); // 清理... return -1; } printf(Mapped framebuffer at %p\n, fb_map);map.offset是mmap的偏移量必须用DRM_IOCTL_MODE_MAP_DUMB返回的值不能自己猜。3.4 设置CRTC并提交画面现在帧缓冲准备好了内存也映射好了接下来把画面填成纯色然后让CRTC显示这个帧缓冲。先填充颜色。假设我们要显示红色ARGB8888格式红色分量在低字节uint32_t color 0x00FF0000; // ARGB: alpha0, red255, green0, blue0 uint32_t *pixels (uint32_t *)fb_map; for (int i 0; i create.width * create.height; i) { pixels[i] color; }然后调用drmModeSetCrtcif (drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, conn-connector_id, 1, mode) 0) { perror(drmModeSetCrtc); // 清理... return -1; } printf(CRTC set, display should show red screen\n);drmModeSetCrtc的参数fd、crtc_id、fb_id、x、y、连接器ID数组、连接器数量、模式。这里x和y都是0表示从左上角开始显示。如果一切顺利屏幕上应该会显示纯红色。程序可以在这里暂停几秒方便观察sleep(5);最后是清理工作。DRM资源需要按顺序释放munmap(fb_map, create.size); drmModeRmFB(fd, fb_id); struct drm_mode_destroy_dumb destroy {0}; destroy.handle create.handle; drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, destroy); drmModeFreeCrtc(crtc); drmModeFreeConnector(conn); drmModeFreeResources(res); close(fd);清理顺序很重要先解除映射再删除帧缓冲再销毁dumb buffer最后释放资源对象和关闭fd。顺序错了可能导致内核引用计数出错。4. 编译、运行与实测中的那些坑4.1 编译命令与Makefile编译这个程序需要链接libdrmgcc -o drm_demo drm_demo.c -ldrm如果头文件不在默认路径可能需要加-I/usr/include/libdrm。我建议写个简单的MakefileCC gcc CFLAGS -Wall -O2 -I/usr/include/libdrm LDFLAGS -ldrm drm_demo: drm_demo.c $(CC) $(CFLAGS) -o $ $ $(LDFLAGS) clean: rm -f drm_demo-Wall打开所有警告-O2开优化。虽然这个程序很小但养成好习惯没坏处。4.2 运行时的常见报错与排查报错一open card: No such file or directory说明/dev/dri/card0不存在。检查内核是否加载了DRM驱动用lsmod | grep drm看看。如果是虚拟机确认3D加速已启用。报错二open card: Permission denied权限问题把用户加入video组或者用sudo运行。但注意sudo可能改变LD_LIBRARY_PATH导致找不到libdrm。报错三drmModeGetResources: Invalid argument这个错误通常意味着设备节点不是显示设备可能是渲染节点。确认你打开的是card0而不是renderD128。报错四drmModeSetCrtc: Device or resource busyCRTC被占用了最常见的原因是桌面环境正在使用它。切换到TTY再运行或者用vkms虚拟设备测试。报错五屏幕显示花屏或颜色不对检查像素格式。drmModeAddFB的深度参数是24bpp是32对应ARGB8888。如果你填的颜色值字节序不对显示出来就是花的。x86是小端序ARGB8888在内存中的布局是B、G、R、A所以红色是0x00FF0000。4.3 用vkms虚拟设备做无头测试如果你没有物理显示器或者不想占用桌面可以用vkmsVirtual KMS驱动做测试。加载模块sudo modprobe vkms然后/dev/dri/下会多出一个card1或类似。修改程序里的设备路径为/dev/dri/card1运行后虽然看不到画面但可以通过dmesg确认CRTC设置成功。vkms还支持把输出写到文件方便验证画面内容。提示vkms默认可能只支持有限的模式如果count_modes为0可以手动指定一个标准模式比如1920x1080。5. 从纯色画面到实际显示进阶思路5.1 双缓冲与页面翻转上面的程序直接往帧缓冲里写数据然后设置CRTC。如果要在运行时更新画面直接改同一块内存会导致撕裂。正确的做法是创建两个帧缓冲交替使用通过drmModePageFlip实现页面翻转。drmModePageFlip是异步的提交后立即返回翻转完成时通过DRM事件通知。需要配合drmHandleEvent和poll使用。这个机制是DRM显示管线的核心理解了它才能做流畅的动画或视频播放。5.2 用MIPI DSI屏幕时的竖屏改横屏很多嵌入式设备用的是MIPI DSI屏幕默认可能是竖屏比如800x1280。如果想改成横屏显示有两种思路一是修改设备树里的panel时序参数把宽高对调二是在用户态用drmModeSetCrtc的旋转属性。后者需要驱动支持rotation属性可以通过drmModeObjectSetProperty设置。我实际调试过一块7寸MIPI屏设备树里配置的是竖屏但应用需要横屏。最后是在用户态通过设置CRTC的rotation属性为DRM_MODE_ROTATE_90实现的。注意旋转会带来性能开销如果驱动不支持硬件旋转可能会走软件路径。5.3 多图层与硬件合成DRM支持多个plane图层可以把不同的帧缓冲叠加显示。比如底层放视频上层放UI由显示控制器硬件合成。这比在CPU里合成再提交要高效得多。用drmModeGetPlaneResources获取plane列表用drmModeSetPlane配置每个图层的坐标、尺寸和格式。不过plane的使用比CRTC复杂得多需要处理格式兼容性、缩放限制、Z-order等问题。建议先把单CRTC单帧缓冲跑通再逐步加图层。6. 几个让我调试到凌晨的细节第一个是drmModeAddFB的pitch参数。我一开始以为pitch就是width * 4结果在某些驱动上因为对齐要求实际pitch会更大。如果传错了画面会错位。正确做法是用create.pitch而不是自己算。第二个是内存屏障。往帧缓冲写完数据后理论上需要确保数据对显示控制器可见。在x86上通常没问题但在ARM等弱内存序架构上可能需要加__sync_synchronize()或者用drmModeDirtyFB通知内核。我遇到过一次在ARM板上画面更新延迟好几帧的情况最后发现是缺少内存屏障。第三个是DRM master的概念。只有DRM master才能调用drmModeSetCrtc。桌面环境启动时会成为master你的程序如果直接跑会因为没有master权限而失败。用drmSetMaster可以抢占但会导致桌面环境失去控制。所以调试时最好切到TTY或者用独立的虚拟设备。第四个是资源释放的顺序。我写第一个版本时先close(fd)再drmModeFreeResources结果程序偶尔崩溃。后来查文档才知道drmModeFreeResources只是释放用户态内存不涉及fd但drmModeRmFB和DRM_IOCTL_MODE_DESTROY_DUMB需要fd有效。所以顺序必须是先释放内核资源再释放用户态资源最后关fd。这套代码我在三个平台上跑过x86的Intel核显、ARM的Rockchip RK3399、以及QEMU里的vkms。除了权限和CRTC占用问题核心逻辑完全一致。这也说明DRM的抽象层做得确实不错用户态代码可以跨平台复用。如果你想把画面换成图片或者渐变只需要修改填充像素的那段循环。如果要显示文字那就需要引入字体渲染库把文字栅格化后写入帧缓冲。再往后就是GPU渲染、EGL、GBM这些更上层的东西了。但不管走多远底层这套DRM流程始终是根基。