1. 嵌入式显示调试中双 Capability 开启的真实痛点
在嵌入式显示驱动调试里,DRM_CLIENT_CAP_UNIVERSAL_PLANES和DRM_CLIENT_CAP_ATOMIC这两个 capability 是绕不开的门槛。它们决定了用户态能不能看到全部硬件图层、能不能用原子提交一次性下发配置。简单说,前者让你拿到所有 plane 的句柄,后者让你把 CRTC、Plane、Connector 的属性打包成一个请求提交,避免中间态闪烁。
适合谁看:正在调 Rockchip、i.MX、Allwinner、Intel i915 或 AMDGPU 显示通路的嵌入式工程师,以及写 Wayland compositor、KMS 测试工具、视频播放器渲染层的开发者。如果你只调过drmModeSetCrtc,没碰过 atomic,那这篇正好补齐。
我遇到最典型的问题是:代码里明明调了drmSetClientCap,drmModeGetPlaneResources返回的 plane 数量却只有 2 个(primary + cursor),overlay plane 一个都看不到。另一种情况是 atomic 提交返回-EINVAL,但单独设 CRTC 又能成功。根因往往不是驱动不支持,而是两个 capability 的开启顺序、返回值检查、以及 plane 枚举时机出了问题。
这篇按可跟做的顺序展开:先讲清楚两个 capability 的依赖关系,再给可复制的初始化片段,然后用drmModeGetPlaneResources验证 plane 枚举,接着对比原子提交前后的 plane 状态,最后把常见报错逐条排掉。全程围绕 Linux DRM 用户态初始化阶段,不涉及内核驱动改动。
需要说明的是,调试这类底层显示问题,除了本地环境,有时也需要一个稳定的模型对话入口来快速查 libdrm 头文件定义或内核 commit。我平时会把 TaoToken 的模型对话页开着,遇到drm_mode.h里某个 property 枚举值记不清时直接问,省去翻源码的时间。它的接入方式在第二节说明,不影响你本地 DRM 调试的主线。
2. TaoToken 前置准备:模型对话与 API Key 获取
这一节不是 DRM 的核心,但如果你在调试过程中需要快速确认 libdrm 版本差异、DRM_CLIENT_CAP_ATOMIC在内核哪个版本引入、或者某个 property 的语义,有一个能直接对话的模型入口会省很多事。TaoToken 提供模型对话、Coding Plan、API Keys 和接入文档几个入口,下面把地址和用途列清楚。
官网入口是https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,从这里可以进到控制台。API 基址是https://taotoken.net/api,注意这个不带 UTM 参数,配置到代码或工具里时用这个。
具体几个 deep link 的用途:
模型对话页用于临时提问,比如「DRM_MODE_ATOMIC_TEST_ONLY和DRM_MODE_ATOMIC_ALLOW_MODESET能否同时传」,直接问比翻 man page 快。地址是https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite。
Coding Plan 适合长期写 KMS 测试代码或 compositor 的场景,地址https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite。
控制台用于管理额度和查看调用,地址https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite。
API Keys 页面用于生成密钥,地址https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite。
如果你用 Claude Code 做辅助开发,Anthropic 兼容入口在https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude_code&utm_campaign=rewrite。
拿 Key 的步骤很直接:进 API Keys 页面生成一个,复制保存。然后在你的工具配置里填三件套——Base URL 填https://taotoken.net/api,Key 填刚生成的,Model ID 按文档里列出的填。这三件套缺一不可,尤其是 Model ID,填错会直接报模型不存在。
需要提醒的是,TaoToken 在这里的角色是辅助查资料和写测试代码,不是替代你的 DRM 调试环境。DRM 的 capability 开启、plane 枚举、atomic 提交验证,全部在本地 Linux 环境完成。下面进入正题。
3. 可复制配置:set_client_cap 调用片段与依赖顺序
先给结论:DRM_CLIENT_CAP_UNIVERSAL_PLANES和DRM_CLIENT_CAP_ATOMIC没有严格的先后依赖,但推荐先开 UNIVERSAL_PLANES 再开 ATOMIC。原因是 atomic 提交时你往往需要操作 overlay plane,而 overlay plane 只有在 UNIVERSAL_PLANES 开启后才会出现在drmModeGetPlaneResources的返回里。如果顺序反了,atomic 请求里引用了一个还没枚举到的 plane_id,提交会失败。
下面是一段可直接编译的初始化片段,包含头文件、打开设备、开启两个 capability、以及返回值检查。注意drmSetClientCap返回 0 表示成功,负数表示失败,必须逐个检查。
#include <xf86drm.h> #include <xf86drmMode.h> #include <fcntl.h> #include <stdio.h> #include <unistd.h> #include <errno.h> int drm_init(const char *dev_path) { int fd = open(dev_path, O_RDWR | O_CLOEXEC); if (fd < 0) { perror("open drm device failed"); return -1; } /* 先开启通用平面,确保后续能枚举到 overlay plane */ if (drmSetClientCap(fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) < 0) { perror("Failed to enable universal planes"); close(fd); return -1; } /* 再开启原子模式设置 */ if (drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1) < 0) { perror("Failed to enable atomic modesetting"); close(fd); return -1; } return fd; }这段代码里有两个细节容易被忽略。第一,open用了O_CLOEXEC,避免 fd 泄漏到子进程。第二,两个drmSetClientCap的返回值都检查了,失败时关闭 fd 再返回,防止资源泄漏。
如果你用 Meson 或 CMake 构建,依赖库是 libdrm。Meson 里写dependency('libdrm'),CMake 里写find_package(PkgConfig REQUIRED)加pkg_check_modules(DRM REQUIRED libdrm),然后链接drm。
关于内核版本,DRM_CLIENT_CAP_ATOMIC需要内核 4.2 及以上,DRM_CLIENT_CAP_UNIVERSAL_PLANES更早,3.15 左右就有了。嵌入式板子上如果内核是 4.4 或 4.9,两个都支持。但要注意,内核支持不代表你的 GPU 驱动实现了 atomic。比如某些老版本 Mali 或 PowerVR 的 DRM 驱动可能只实现了 legacy 路径,这时drmSetClientCap(fd, DRM_CLIENT_CAP_ATOMIC, 1)会返回-EINVAL。
还有一个配置层面的点:如果你用 systemd 或 udev 管理设备节点,确保运行用户对/dev/dri/card0有读写权限。嵌入式环境常用video组,把用户加进去,或者临时chmod 666调试。权限不足时open直接失败,报Permission denied,和 capability 无关。
配置片段给完了,下一节验证它是否真的生效。
4. 验证请求:drmModeGetPlaneResources 枚举与成功结果
开启 capability 之后,必须验证 plane 枚举结果。核心函数是drmModeGetPlaneResources,它返回一个drmModePlaneRes结构,里面count_planes是 plane 总数,planes是 plane id 数组。如果 UNIVERSAL_PLANES 没生效,这个数字通常只有 2(primary + cursor);生效后,overlay plane 会一起出现。
下面是一段枚举并打印每个 plane 信息的代码:
#include <xf86drm.h> #include <xf86drmMode.h> #include <stdio.h> void dump_planes(int fd) { drmModePlaneRes *plane_res = drmModeGetPlaneResources(fd); if (!plane_res) { perror("drmModeGetPlaneResources failed"); return; } printf("total planes: %u\n", plane_res->count_planes); for (uint32_t i = 0; i < plane_res->count_planes; i++) { uint32_t plane_id = plane_res->planes[i]; drmModePlane *plane = drmModeGetPlane(fd, plane_id); if (!plane) { continue; } printf("plane[%u] id=%u crtc=%u fb=%u possible_crtcs=0x%x\n", i, plane->plane_id, plane->crtc_id, plane->fb_id, plane->possible_crtcs); drmModeFreePlane(plane); } drmModeFreePlaneResources(plane_res); }成功结果长这样(以一块支持 3 个 plane 的嵌入式板子为例):
total planes: 3 plane[0] id=31 crtc=0 fb=0 possible_crtcs=0x1 plane[1] id=37 crtc=0 fb=0 possible_crtcs=0x1 plane[2] id=43 crtc=0 fb=0 possible_crtcs=0x1如果只看到 2 个 plane,说明 UNIVERSAL_PLANES 没生效。这时先确认drmSetClientCap返回值是不是 0,再确认内核版本和驱动。有些驱动即使内核支持,也需要在设备树或模块参数里显式开启 overlay plane。
验证 atomic 是否生效,不能只看 plane 枚举。你需要检查 CRTC 或 Plane 上是否存在名为"ATOMIC"的 property,或者尝试一次 test-only 提交。更直接的方法是调用drmModeAtomicAlloc分配请求,加一个属性,然后用DRM_MODE_ATOMIC_TEST_ONLY提交。如果返回 0,说明 atomic 路径可用;返回-EINVAL或-ENOTSUP,说明没生效。
drmModeAtomicReq *req = drmModeAtomicAlloc(); if (!req) { perror("drmModeAtomicAlloc failed"); return; } /* 这里以设置某个 plane 的 alpha 为例,prop_id 需提前查得 */ drmModeAtomicAddProperty(req, plane_id, prop_alpha_id, 0xFFFF); int ret = drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_TEST_ONLY, NULL); if (ret < 0) { perror("atomic test commit failed"); } else { printf("atomic test commit ok\n"); } drmModeAtomicFree(req);DRM_MODE_ATOMIC_TEST_ONLY只验证不应用,适合调试阶段。确认无误后再去掉这个 flag 做真实提交。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节把调试过程中真实会撞到的报错逐条拆开。注意,前几个是工具侧报错,后几个是 DRM 侧报错,分开看。
401 Unauthorized:如果你在用 TaoToken 的 API 辅助查资料,配置里 Key 填错或过期会报 401。检查 API Keys 页面生成的 Key 是否完整复制,Base URL 是否是https://taotoken.net/api。三件套里 Base URL、Key、Model ID 任何一个不对都会导致请求失败。401 和 DRM 无关,别往驱动方向查。
local proxy failed:这个报错通常出现在工具的网络配置层,表示本地代理连接失败。检查你的工具是否配置了本地代理端口,以及该端口是否有服务在监听。如果你没主动配代理,检查环境变量http_proxy、https_proxy是否被意外设置。清掉这些变量再试。
reading choices 相关报错:这类报错一般出现在模型返回结构解析阶段,比如返回体里没有choices字段。常见原因是 Model ID 填错,请求打到了不兼容的端点。确认 Model ID 和接入文档里列的一致,Base URL 没有多余路径。
OAuth 报错:如果你用 Claude Code 或类似工具,OAuth 流程失败通常是回调地址或 token 交换环节的问题。检查工具版本,确认 Anthropic 兼容入口配置正确。OAuth 和 DRM 调试完全无关,属于工具链配置问题。
下面转到 DRM 侧的真实报错。
drmSetClientCap返回 -EINVAL:说明驱动不支持该 capability。先确认内核版本,再确认驱动是否实现 atomic。用modetest -M <driver>看驱动名,然后查该驱动源码里是否有drm_atomic_helper相关调用。部分嵌入式 GPU 只支持 legacy,这时 atomic 无法开启,只能退回drmModeSetCrtc。
drmModeGetPlaneResources返回 NULL:检查 fd 是否有效,以及 UNIVERSAL_PLANES 是否开启成功。如果 fd 是 legacy 模式打开的,某些驱动会拒绝返回完整 plane 列表。
atomic 提交返回 -EINVAL:最常见原因是请求里引用了无效的 plane_id 或 prop_id。plane_id 必须来自drmModeGetPlaneResources的枚举结果,prop_id 必须来自drmModeObjectGetProperties。另一个原因是提交时缺少必要的 property,比如开启 plane 时必须同时设置 CRTC_ID 和 FB_ID。
atomic 提交返回 -ENOTSUP:驱动不支持该操作。检查是否在 test-only 模式下提交了需要 modeset 的配置,test-only 不允许改变需要 modeset 的属性。
画面黑屏但提交返回 0:说明配置生效了但内容不对。检查 FB_ID 是否指向有效的 framebuffer,以及 plane 的 src/dst 坐标是否在 CRTC 范围内。用DRM_MODE_ATOMIC_ALLOW_MODESET时,确保 CRTC 的 mode 已正确设置。
排查顺序建议:先确认两个 capability 返回值都是 0,再确认 plane 枚举数量符合预期,然后做 test-only 提交,最后做真实提交。每一步都打印返回值,别跳过检查。
6. 语义一致 CTA:接入文档与模型对话入口
DRM 调试过程中,遇到 libdrm 头文件里某个宏定义记不清、或者想确认某个内核版本的 atomic 支持情况,直接查文档比翻源码快。TaoToken 的接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有 Base URL、Key、Model ID 的完整配置说明。
如果你需要临时问一个具体问题,比如「DRM_MODE_ATOMIC_NONBLOCK在嵌入式驱动上是否可靠」,用模型对话页https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite直接问,比在邮件列表里等回复快。
长期写 KMS 测试代码或 compositor 的话,Coding Plan 入口在https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite,适合把查资料和写代码串起来。
最后回到 DRM 本身。双 capability 开启只是起点,真正难的是 atomic 提交时属性依赖关系的梳理。建议你从 test-only 提交开始,每次只加一个属性,确认返回 0 后再加下一个。这样出问题时能快速定位是哪个属性导致的-EINVAL。plane 枚举结果打印出来存一份,后面调试 overlay 合成时对照着看,比反复调drmModeGetPlaneResources省时间。