资讯动态

Linux下从零编写libdrm显示程序:DRM核心概念与实战

发布时间:2026/9/19 15:54:52 来源:尧图企业网站定制
1. 为什么我要在Linux下折腾libdrm显示程序先说说我为什么会对libdrm这个东西产生兴趣。几年前我在做一个嵌入式项目板子跑的是Linux屏幕是一块MIPI DSI接口的竖屏客户要求改成横屏显示。当时第一反应是去改应用层结果发现应用层用的是Qt旋转一下确实能显示但性能掉得厉害帧率直接腰斩。后来顺着驱动层往上查才发现真正控制显示管线的是DRM子系统而用户态跟DRM打交道最直接的入口就是libdrm。从那以后我就养成了一个习惯凡是跟显示相关的底层问题先看DRM再看libdrm。这篇文章要聊的就是怎么从零开始在Linux环境下用libdrm写一个最简单的DRM显示程序。说它简单是因为它只做一件事把一块内存缓冲区的内容刷到屏幕上。但麻雀虽小五脏俱全DRM的核心概念——设备节点、资源枚举、CRTC、Encoder、Connector、Framebuffer、Dumb Buffer——全都会碰到。你把这套流程跑通了后面再去看 Weston、Xorg、KMS 那些东西会发现底层逻辑是一脉相承的。适合谁看如果你已经会写C语言能在Linux下用gcc编译程序对文件描述符、ioctl这些概念不陌生那这篇文章就是给你准备的。如果你连Linux都没装过建议先找个虚拟机把系统跑起来把常用的命令行操作熟悉一下再回来看。我不打算把每个系统调用都讲成教科书而是按照一个实际能跑通的程序来拆解每一步为什么这么做、踩过什么坑都会说清楚。需要提前说明的是DRM这套东西在不同内核版本、不同显卡驱动上行为会有差异。我下面给出的代码和步骤是在一台x86_64的机器上、用Intel核显、内核版本5.x以上的环境验证过的。如果你用的是NVIDIA闭源驱动或者比较老的ARM板子部分细节可能需要微调但整体思路是通用的。2. 动手之前先把DRM这套机制理清楚2.1 DRM到底管什么libdrm又扮演什么角色DRM全称Direct Rendering Manager直译过来就是直接渲染管理器。它最早是为了解决多个程序同时访问显卡导致的冲突问题后来逐渐演变成Linux下显示和渲染的统一框架。你可以把它想象成一个交通枢纽上面跑着各种应用下面连着各种显示硬件DRM负责调度谁在什么时候用哪块资源。内核里的DRM子系统分成两部分一部分是通用的DRM核心处理设备管理、内存管理、命令队列这些公共逻辑另一部分是各个显卡厂商提供的驱动模块比如i915对应Intel核显amdgpu对应AMD显卡。这些驱动会向用户态暴露一个字符设备节点通常是/dev/dri/card0有时候还会有/dev/dri/renderD128这种专门用于渲染的节点。libdrm就是用户态访问这些设备节点的封装库。它把那些又臭又长的ioctl调用包装成了相对友好的函数比如drmModeGetResources、drmModeSetCrtc。没有libdrm你也能写DRM程序但你就得自己拼ioctl结构体那工作量不是一般的大。libdrm本身不包含任何显卡特定的逻辑它只是把内核暴露的接口翻译了一遍所以它非常轻量编译出来也就几百KB。这里要区分一个概念DRM负责的是显示控制也就是把画面送到屏幕上而渲染是另一回事通常由OpenGL、Vulkan这些图形API负责。我们这个程序只做显示不做渲染所以只需要libdrm就够了不需要引入Mesa那一大套东西。2.2 五个核心对象CRTC、Encoder、Connector、Plane、FramebufferDRM的显示管线可以抽象成一条流水线数据从内存出发经过若干环节最终到达屏幕。这条流水线上有五个关键角色我一个个说。Framebuffer是内存里的一块像素数据你可以把它理解成一张画布。在DRM里framebuffer是一个抽象概念它指向一块实际的内存缓冲区同时描述了这块缓冲区的格式比如RGB888、ARGB8888、宽度、高度、pitch每行占多少字节。我们程序里要显示的内容就是往framebuffer对应的内存里写。CRTC全称Cathode Ray Tube Controller名字是历史遗留的现在早就不是显像管了但名字一直没改。它代表一个显示控制器负责从framebuffer里读取像素数据按照一定的时序比如行同步、场同步信号输出出去。一块显卡可能有多个CRTC对应多个显示输出。你可以把CRTC想象成一个扫描仪它一行一行地把framebuffer的内容扫出来。Encoder是编码器负责把CRTC输出的像素信号转换成特定接口需要的格式。比如HDMI、DisplayPort、MIPI DSI每种接口的电气特性和信号格式都不一样Encoder就是做这个转换的。一个CRTC可以接多个Encoder但同一时刻通常只用一个。Connector是连接器代表物理上的显示接口比如HDMI口、VGA口、笔记本的内屏排线。它负责检测显示器的插拔状态读取EDID信息里面包含显示器支持的分辨率、刷新率等。Connector和Encoder是多对多的关系一个Connector可以接受来自不同Encoder的信号。Plane是图层现代显示控制器都支持多个图层叠加。比如你可以有一个背景层、一个视频层、一个UI层硬件自动把它们合成后输出。最简单的用法是只用主图层Primary Plane把framebuffer直接贴上去。这五个对象的关系是Framebuffer提供数据Plane把Framebuffer挂到CRTC上CRTC通过Encoder和Connector把信号送出去。我们写程序的时候就是按照这个顺序去枚举资源、配置参数、最后提交。2.3 为什么选Dumb Buffer而不是GBM创建framebuffer需要一块内存缓冲区。在DRM里创建缓冲区有好几种方式最常用的是Dumb Buffer和GBM Buffer。Dumb Buffer是DRM提供的一种最基础的缓冲区它的特点是CPU可以直接访问通过mmap映射驱动保证它能被显示控制器读取但不保证它能被GPU高效渲染。名字里的“dumb”就是“笨”的意思因为它不支持硬件加速的渲染操作只能由CPU往里写数据。GBM全称Generic Buffer Management是Mesa提供的一套缓冲区管理接口它创建的缓冲区可以被GPU直接渲染也能被显示控制器扫描输出适合做高性能图形应用。我们这个程序只是演示显示流程不需要GPU渲染所以用Dumb Buffer最合适。它简单、直接、依赖少只要内核支持DRM基本都能用。而且Dumb Buffer的创建流程很清晰先通过ioctl申请一块内存然后mmap到用户空间往里面写像素最后创建framebuffer对象指向这块内存。注意Dumb Buffer在有些驱动上可能不支持比如某些NVIDIA的闭源驱动。如果你调用DRM_IOCTL_MODE_CREATE_DUMB返回错误可以试试用GBM或者换一台机器测试。3. 从零开始写一个能跑的DRM程序3.1 环境准备和依赖安装先把开发环境搭起来。我用的是一台装了Ubuntu 22.04的机器内核是5.15。如果你用的是其他发行版包管理命令可能不一样但需要装的库是相同的。第一步确认你的系统里有DRM设备节点。打开终端执行ls -l /dev/dri/正常的话你会看到card0、renderD128这样的节点。如果这个目录不存在说明你的内核没有加载DRM驱动或者你是在一个没有显卡的虚拟机里。这种情况下后面的步骤没法进行需要先解决驱动问题。第二步安装libdrm的开发包。在Debian/Ubuntu系上sudo apt update sudo apt install libdrm-dev在Fedora/RHEL系上sudo dnf install libdrm-devel安装完成后确认头文件位置ls /usr/include/libdrm/ ls /usr/include/xf86drm.h ls /usr/include/xf86drmMode.h你应该能看到xf86drm.h和xf86drmMode.h这两个关键头文件。前者包含基础的DRM操作接口后者包含模式设置Mode Setting相关的接口。第三步确认编译器可用gcc --version如果没有gcc装一下build-essential或者gcc包就行。3.2 打开设备节点并获取DRM资源程序的第一步是打开DRM设备节点。通常用/dev/dri/card0但如果你的系统有多个显卡可能需要试其他编号。打开的时候用O_RDWR标志因为后面要执行ioctl操作。int fd open(/dev/dri/card0, O_RDWR | O_CLOEXEC); if (fd 0) { perror(open /dev/dri/card0); return -1; }O_CLOEXEC这个标志是为了在执行exec时自动关闭文件描述符避免子进程继承不必要的句柄。这是个好习惯建议加上。打开设备后需要告诉DRM我们要用它的模式设置功能。在老版本的内核里需要调用drmSetMaster来获取主控权但新版本内核里这个操作已经不需要了直接调用drmModeGetResources就行。drmModeRes *res drmModeGetResources(fd); if (!res) { fprintf(stderr, drmModeGetResources failed\n); close(fd); return -1; }drmModeGetResources返回一个drmModeRes结构体里面包含了当前设备的所有显示资源CRTC列表、Connector列表、Encoder列表以及framebuffer的数量范围。这个结构体是后面所有操作的基础用完记得用drmModeFreeResources释放。拿到资源后先打印一下看看printf(CRTC count: %d\n, res-count_crtcs); printf(Connector count: %d\n, res-count_connectors); printf(Encoder count: %d\n, res-count_encoders);这一步很重要因为不同设备的资源数量差别很大。比如我的Intel核显有3个CRTC、5个Connector而有些嵌入式板子可能只有1个CRTC、1个Connector。你需要根据实际情况选择合适的对象。3.3 找到可用的Connector和CRTC接下来要找到一个已经连接了显示器的Connector。遍历res-connectors数组对每个Connector调用drmModeGetConnector获取详细信息drmModeConnector *conn NULL; for (int i 0; i res-count_connectors; i) { conn drmModeGetConnector(fd, res-connectors[i]); if (conn conn-connection DRM_MODE_CONNECTED conn-count_modes 0) { printf(Using connector %d, type %d\n, conn-connector_id, conn-connector_type); break; } if (conn) { drmModeFreeConnector(conn); conn NULL; } } if (!conn) { fprintf(stderr, No connected connector found\n); drmModeFreeResources(res); close(fd); return -1; }这里判断两个条件connection DRM_MODE_CONNECTED表示显示器已经插上并且被检测到了count_modes 0表示这个显示器至少支持一种显示模式。两个条件都满足才用。拿到Connector后需要找一个能驱动它的Encoder。Connector结构体里有个encoder_id字段如果这个字段不为0说明当前已经有一个Encoder绑定在这个Connector上了直接用就行。如果为0就需要遍历res-encoders找到possible_encoders里包含这个Connector的Encoder。drmModeEncoder *enc NULL; if (conn-encoder_id) { enc drmModeGetEncoder(fd, conn-encoder_id); } if (!enc) { for (int i 0; i res-count_encoders; i) { enc drmModeGetEncoder(fd, res-encoders[i]); if (enc) { if (enc-possible_crtcs (1 0)) { break; } drmModeFreeEncoder(enc); enc NULL; } } }possible_crtcs是一个位掩码表示这个Encoder可以连接到哪些CRTC。位0对应CRTC数组的第0个元素位1对应第1个以此类推。我这里简单起见直接选了位0实际项目中应该根据Connector和CRTC的匹配关系来选。找到Encoder后就能确定用哪个CRTC了。enc-crtc_id如果非0说明已经绑定了CRTC否则需要从res-crtcs里选一个possible_crtcs允许的。uint32_t crtc_id enc-crtc_id; if (crtc_id 0) { for (int i 0; i res-count_crtcs; i) { if (enc-possible_crtcs (1 i)) { crtc_id res-crtcs[i]; break; } } }到这里我们已经确定了三个关键IDconnector_id、crtc_id以及要用的显示模式。显示模式从conn-modes[0]取这是Connector支持的首选模式通常就是显示器的原生分辨率。3.4 创建Dumb Buffer并映射到用户空间现在开始创建缓冲区。Dumb Buffer的创建分两步先申请内存再映射。struct drm_mode_create_dumb create_arg {0}; create_arg.width conn-modes[0].hdisplay; create_arg.height conn-modes[0].vdisplay; create_arg.bpp 32; create_arg.flags 0; if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, create_arg) 0) { perror(DRM_IOCTL_MODE_CREATE_DUMB); return -1; }这里width和height取自显示模式bpp设为32表示每个像素32位也就是ARGB8888格式。创建成功后create_arg里的handle、pitch、size字段会被内核填上。handle是这块缓冲区的标识pitch是每行占的字节数注意它不一定等于width乘以4因为可能有对齐要求size是总字节数。接下来创建framebuffer对象把它和刚才申请的缓冲区关联起来uint32_t fb_id; if (drmModeAddFB(fd, create_arg.width, create_arg.height, 24, 32, create_arg.pitch, create_arg.handle, fb_id) 0) { perror(drmModeAddFB); return -1; }drmModeAddFB的参数里24表示每个像素的颜色深度是24位RGB各8位32表示每个像素占32位存储空间多出来的8位是alpha通道但我们不用。这两个值要跟前面的bpp对应好否则显示会出问题。然后把这快缓冲区映射到用户空间这样我们才能往里面写像素struct drm_mode_map_dumb map_arg {0}; map_arg.handle create_arg.handle; if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, map_arg) 0) { perror(DRM_IOCTL_MODE_MAP_DUMB); return -1; } uint8_t *fb_map mmap(NULL, create_arg.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map_arg.offset); if (fb_map MAP_FAILED) { perror(mmap); return -1; }map_arg.offset是内核返回的偏移量mmap的时候要传这个值而不是0。映射成功后fb_map就指向了那块显存往里面写数据就等于往屏幕上画。3.5 填充像素并提交显示现在往缓冲区里写点东西。最简单的做法是画一个渐变或者纯色。我这里画一个红绿蓝的渐变方便验证显示是否正常uint32_t *pixels (uint32_t *)fb_map; int width create_arg.width; int height create_arg.height; int pitch_pixels create_arg.pitch / 4; for (int y 0; y height; y) { for (int x 0; x width; x) { uint8_t r (x * 255) / width; uint8_t g (y * 255) / height; uint8_t b 128; pixels[y * pitch_pixels x] (0xff 24) | (r 16) | (g 8) | b; } }注意这里用pitch_pixels而不是width来算行偏移因为pitch可能大于width乘以4。如果直接用width图像会错位。写完之后调用drmModeSetCrtc把framebuffer提交上去if (drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, conn-connector_id, 1, conn-modes[0]) 0) { perror(drmModeSetCrtc); return -1; }这个函数的参数依次是设备fd、CRTC ID、framebuffer ID、显示起始坐标x和y、Connector ID数组、Connector数量、显示模式。调用成功后屏幕应该立刻显示出我们画的渐变图案。到这里一个最基础的DRM显示程序就完成了。完整的代码我会在下一节给出包括错误处理和资源释放。4. 完整代码和编译运行方法4.1 完整代码清单把上面的片段拼起来加上必要的头文件和错误处理就是下面这个程序。我把它保存为drm_demo.c#include stdio.h #include stdlib.h #include string.h #include fcntl.h #include unistd.h #include sys/mman.h #include sys/ioctl.h #include xf86drm.h #include xf86drmMode.h int main(int argc, char **argv) { const char *dev_path /dev/dri/card0; if (argc 1) { dev_path argv[1]; } int fd open(dev_path, O_RDWR | O_CLOEXEC); if (fd 0) { perror(open); return 1; } drmModeRes *res drmModeGetResources(fd); if (!res) { fprintf(stderr, drmModeGetResources failed\n); close(fd); return 1; } printf(Resources: %d CRTCs, %d Connectors, %d Encoders\n, res-count_crtcs, res-count_connectors, res-count_encoders); 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; printf(Connector %u connected, %d modes\n, c-connector_id, c-count_modes); break; } if (c) drmModeFreeConnector(c); } if (!conn) { fprintf(stderr, No connected connector\n); drmModeFreeResources(res); close(fd); return 1; } drmModeEncoder *enc NULL; if (conn-encoder_id) { enc drmModeGetEncoder(fd, conn-encoder_id); } if (!enc) { for (int i 0; i res-count_encoders; i) { drmModeEncoder *e drmModeGetEncoder(fd, res-encoders[i]); if (e) { if (e-possible_crtcs 1) { enc e; break; } drmModeFreeEncoder(e); } } } if (!enc) { fprintf(stderr, No suitable encoder\n); drmModeFreeConnector(conn); drmModeFreeResources(res); close(fd); return 1; } uint32_t crtc_id enc-crtc_id; if (crtc_id 0) { for (int i 0; i res-count_crtcs; i) { if (enc-possible_crtcs (1 i)) { crtc_id res-crtcs[i]; break; } } } if (crtc_id 0) { fprintf(stderr, No suitable CRTC\n); drmModeFreeEncoder(enc); drmModeFreeConnector(conn); drmModeFreeResources(res); close(fd); return 1; } drmModeModeInfo mode conn-modes[0]; printf(Mode: %dx%d %dHz\n, mode.hdisplay, mode.vdisplay, mode.vrefresh); struct drm_mode_create_dumb create_arg {0}; create_arg.width mode.hdisplay; create_arg.height mode.vdisplay; create_arg.bpp 32; if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, create_arg) 0) { perror(CREATE_DUMB); goto cleanup; } uint32_t fb_id; if (drmModeAddFB(fd, create_arg.width, create_arg.height, 24, 32, create_arg.pitch, create_arg.handle, fb_id) 0) { perror(drmModeAddFB); goto cleanup; } struct drm_mode_map_dumb map_arg {0}; map_arg.handle create_arg.handle; if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, map_arg) 0) { perror(MAP_DUMB); goto cleanup; } uint8_t *fb_map mmap(NULL, create_arg.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map_arg.offset); if (fb_map MAP_FAILED) { perror(mmap); goto cleanup; } uint32_t *pixels (uint32_t *)fb_map; int pitch_pixels create_arg.pitch / 4; for (int y 0; y (int)mode.vdisplay; y) { for (int x 0; x (int)mode.hdisplay; x) { uint8_t r (x * 255) / mode.hdisplay; uint8_t g (y * 255) / mode.vdisplay; uint8_t b 128; pixels[y * pitch_pixels x] (0xff 24) | (r 16) | (g 8) | b; } } if (drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, conn-connector_id, 1, mode) 0) { perror(drmModeSetCrtc); goto cleanup; } printf(Displaying gradient. Press Enter to exit.\n); getchar(); cleanup: if (fb_map fb_map ! MAP_FAILED) { munmap(fb_map, create_arg.size); } if (fb_id) { drmModeRmFB(fd, fb_id); } if (create_arg.handle) { struct drm_mode_destroy_dumb destroy_arg {0}; destroy_arg.handle create_arg.handle; drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, destroy_arg); } if (enc) drmModeFreeEncoder(enc); if (conn) drmModeFreeConnector(conn); drmModeFreeResources(res); close(fd); return 0; }4.2 编译命令和Makefile编译这个程序需要链接libdrm。最简单的命令是gcc -o drm_demo drm_demo.c -ldrm如果头文件不在默认路径可能需要加-I和-L参数。在大多数发行版上libdrm-dev装好后路径都是对的直接编译就行。为了方便我一般会写个简单的MakefileCC gcc CFLAGS -Wall -O2 LDFLAGS -ldrm drm_demo: drm_demo.c $(CC) $(CFLAGS) -o $ $ $(LDFLAGS) clean: rm -f drm_demo .PHONY: clean编译成功后直接运行sudo ./drm_demo为什么要sudo因为访问/dev/dri/card0通常需要root权限或者你的用户得在video组里。如果不想用sudo可以把自己加到video组sudo usermod -aG video $USER然后重新登录生效。4.3 运行效果和验证方法程序运行后屏幕上应该显示一个从左上角红色渐变到右下角绿色的图案蓝色分量固定为128。按回车键程序退出屏幕会恢复原来的显示内容因为CRTC被释放了。如果屏幕没有变化或者显示花屏可以从这几个方面排查第一确认你用的设备节点是对的。有些系统上card0可能对应的是没有连接显示器的显卡试试card1或者用ls /dev/dri/by-path/看看每个节点的物理路径。第二确认Connector确实处于连接状态。可以在程序里加打印看看选中的是哪个Connector。如果是笔记本通常eDP接口是内屏HDMI或DP是外接。第三检查pitch的处理。如果图像错位或者倾斜多半是pitch没算对。可以打印create_arg.pitch和width * 4对比一下如果不相等就说明有对齐填充。第四如果drmModeSetCrtc返回EINVAL可能是模式不支持或者CRTC已经被其他程序占用了。可以先关掉Xorg或者Weston再试。5. 踩过的坑和常见问题排查5.1 权限问题导致打开设备失败这是新手最容易遇到的问题。直接运行程序open返回Permission denied。原因很简单/dev/dri/card0的默认权限是root:video 660普通用户不在video组里就没法访问。解决办法有两个一是用sudo运行简单粗暴但不推荐长期这样二是把当前用户加入video组。执行sudo usermod -aG video $USER之后一定要重新登录或者用newgrp video开个新shell否则组权限不会生效。还有一种情况是SELinux或者AppArmor拦截了访问。如果确认权限没问题但还是打不开可以看看dmesg里有没有avc denied的记录。不过这种情况在桌面发行版上比较少见嵌入式系统上偶尔会遇到。5.2 找不到可用的Connector程序打印“No connected connector”说明没有检测到任何已连接的显示器。可能的原因显示器没开机、线没插好、或者驱动没正确识别。可以先在终端里执行cat /sys/class/drm/card0-*/status看看每个接口的状态。正常连接的话会显示connected没插线显示disconnected未知状态显示unknown。如果所有接口都是disconnected但显示器确实插着那可能是驱动问题。比如某些USB-C转HDMI的扩展坞需要特定的驱动支持才能识别。这种情况只能换接口或者更新内核。另外要注意有些虚拟机上模拟的DRM设备可能没有真实的Connector或者Connector状态始终是unknown。这种环境下跑不了这个程序需要找一台有真实显卡的机器。5.3 drmModeSetCrtc返回EINVAL的几种原因EINVAL是DRM程序里最常见的错误之一原因很多我列几个典型的模式不匹配传进去的drmModeModeInfo跟Connector实际支持的不一致。解决办法是直接用conn-modes[0]不要自己构造。CRTC已经被占用如果Xorg或者Weston正在运行它们已经控制了CRTC你再调用就会失败。可以先切换到文本终端CtrlAltF3停掉显示服务再试。framebuffer格式不对drmModeAddFB时传的depth和bpp跟实际缓冲区不匹配。比如创建Dumb Buffer时bpp是32AddFB时depth传了32而不是24有些驱动会拒绝。Connector和Encoder不匹配选错了Encoder导致CRTC无法驱动这个Connector。检查possible_crtcs和possible_clones字段。排查的时候可以在每次调用后打印errno结合strerror(errno)看具体描述。虽然DRM的错误信息不算特别详细但至少能缩小范围。5.4 显示花屏或颜色不对如果图像能显示但颜色明显不对比如红色和蓝色互换了那多半是像素格式的问题。ARGB8888和ABGR8888在内存里的字节顺序是不一样的。我上面的代码用的是(0xff 24) | (r 16) | (g 8) | b对应的是ARGB格式在小端机器上内存布局是B、G、R、A。如果你的设备用的是其他格式需要调整移位顺序。如果图像有斜纹或者错位检查pitch。Dumb Buffer的pitch是内核根据硬件对齐要求算出来的可能大于width * 4。写像素的时候必须用pitch来算行偏移不能用width。如果只有部分屏幕有内容或者图像被拉伸那可能是分辨率设置不对。确认mode.hdisplay和mode.vdisplay跟实际屏幕匹配。5.5 程序退出后屏幕没有恢复正常情况下程序退出时CRTC会被释放显示应该恢复到之前的状态。但如果程序异常崩溃没有执行清理代码CRTC可能还占着屏幕会一直显示最后的内容。这种情况下可以手动恢复切换到其他虚拟终端再切回来或者重启显示服务。如果是在嵌入式设备上可能需要重启。为了避免这个问题写代码的时候一定要用goto cleanup的方式统一处理错误路径确保任何情况下资源都能被释放。我上面的代码就是按这个模式写的。6. 这个基础框架还能怎么扩展把上面这个程序跑通之后你就有了一个DRM显示的最小可用原型。基于这个框架可以往几个方向扩展。第一个方向是双缓冲和页面翻转Page Flip。现在的代码是直接写framebuffer然后提交如果要在显示的同时更新内容会出现撕裂。解决办法是创建两个framebuffer一个正在显示另一个在后台绘制绘制完成后通过drmModePageFlip切换。这个机制在DRM里叫原子提交Atomic Commit是現代DRM推荐的方式。第二个方向是支持多种像素格式。现在只用了ARGB8888实际项目中可能需要RGB565、NV12视频常用等格式。drmModeAddFB2支持更丰富的格式指定比drmModeAddFB更灵活。第三个方向是结合GPU渲染。用Dumb Buffer的话只能CPU写像素性能有限。如果要跑OpenGL或者Vulkan需要用GBM创建缓冲区然后通过DRM的prime机制共享给显示控制器。这套流程稍微复杂一些但性能提升很明显。第四个方向是处理热插拔事件。现在的程序启动时枚举一次资源就完事了如果运行过程中显示器被拔掉或者换上新的程序不会感知。DRM提供了drmModeGetConnector的轮询机制和udev事件可以实现动态响应。我自己在实际项目里最常用的扩展是第一个和第三个。双缓冲解决撕裂问题GBM解决性能问题这两个加起来就能支撑一个基本的嵌入式显示应用了。至于更上层的窗口管理、输入处理那就是Weston或者Qt的事情不在libdrm的范畴里。最后分享一个小技巧调试DRM程序的时候如果不想每次都sudo可以临时改一下设备节点的权限sudo chmod 666 /dev/dri/card0。但这只是权宜之计重启后就失效了正式环境还是老老实实配用户组。另外drmModeGetResources返回的Connector顺序在不同内核版本上可能不一样不要假设第一个就是主显示器一定要根据connection状态和count_modes来判断。

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

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

免费获取报价