在嵌入式开发中,直接操作显示设备是常见的需求。最近我在K510(DongshanPI-Vision)开发板上实践了通过DRM(Direct Rendering Manager)进行屏幕探测和显示控制的过程。DRM是Linux内核中负责管理图形显示的核心子系统,它提供了一套统一的接口来操作显示设备。
这个项目的核心目标是:通过编写一个探测程序drm_probe.c,获取DRM设备信息,包括连接器(connector)、编码器(encoder)和显示模式(mode)等,为后续的显示控制打下基础。整个过程让我对Linux下的显示子系统有了更深入的理解,特别是DRM/KMS(Kernel Mode Setting)的工作机制。
在开始代码分析前,有必要先了解DRM的几个核心概念:
Connector(连接器):代表物理显示接口,如HDMI、DSI、eDP等。它包含了显示器的连接状态、支持的显示模式等信息。
Encoder(编码器):负责将像素数据转换为特定接口(如HDMI、LVDS)所需的信号格式。
CRTC(显示控制器):负责实际的显示时序控制和帧缓冲管理。每个CRTC代表一个独立的显示管线。
Frame Buffer(帧缓冲):存储要显示的图像数据的内存区域。
这些组件之间的关系可以简单理解为:应用程序将图像数据写入帧缓冲,CRTC从帧缓冲读取数据并通过Encoder输出到Connector连接的显示器上。
在Linux系统中,DRM设备通常以/dev/dri/cardX的形式出现。我们的开发板上使用的是/dev/dri/card0。通过打开这个设备节点,我们可以获取对显示硬件的控制权。
探测程序的第一步是打开DRM设备节点:
c复制int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC);
if (fd < 0) {
printf("[DRM探测][错误] open 失败:%s\n", strerror(errno));
return 1;
}
这里使用了O_RDWR标志表示需要读写权限,O_CLOEXEC标志确保在执行exec系列函数时自动关闭文件描述符,这是一种良好的编程习惯。
成功打开设备后,我们需要获取DRM的全局资源:
c复制drmModeRes* res = drmModeGetResources(fd);
if (!res) {
printf("[DRM探测][错误] drmModeGetResources 失败:%s\n", strerror(errno));
close(fd);
return 1;
}
printf("[DRM探测] connectors=%d encoders=%d crtcs=%d fbs=%d\n",
res->count_connectors, res->count_encoders, res->count_crtcs, res->count_fbs);
drmModeGetResources函数返回一个包含所有DRM资源信息的结构体,包括连接器、编码器、CRTC和帧缓冲的数量。在我们的开发板上,输出显示有1个连接器、1个编码器和1个CRTC。
接下来是探测过程的核心部分——遍历所有连接器并获取其详细信息:
c复制for (int i = 0; i < res->count_connectors; i++) {
uint32_t cid = res->connectors[i];
drmModeConnector* conn = drmModeGetConnector(fd, cid);
if (!conn) continue;
// 打印连接器信息
printf("\n[Connector] id=%u 类型=%s(%u) 状态=%s 模式数=%d\n",
conn->connector_id,
conn_type_name(conn->connector_type),
conn->connector_type,
(conn->connection == DRM_MODE_CONNECTED) ? "已连接" :
(conn->connection == DRM_MODE_DISCONNECTED) ? "未连接" : "未知",
conn->count_modes);
// 遍历所有显示模式
for (int m = 0; m < conn->count_modes; m++) {
drmModeModeInfo* mode = &conn->modes[m];
int vrefresh = 0;
if (mode->htotal && mode->vtotal)
vrefresh = (int)((mode->clock * 1000LL) / (mode->htotal * mode->vtotal));
printf(" - mode[%d]: %s %dx%d refresh≈%dHz flags=0x%x type=0x%x\n",
m, mode->name, mode->hdisplay, mode->vdisplay, vrefresh, mode->flags, mode->type);
}
// 选择最佳模式
if (conn->connection == DRM_MODE_CONNECTED && conn->count_modes > 0) {
int local_best = -1;
int local_best_score = -1;
for (int m = 0; m < conn->count_modes; m++) {
int sc = mode_score(&conn->modes[m]);
if (sc > local_best_score) {
local_best_score = sc;
local_best = m;
}
}
printf("[Connector] 当前 encoder_id=%u\n", conn->encoder_id);
if (local_best >= 0 && local_best_score > best_score) {
best_score = local_best_score;
best_conn_id = conn->connector_id;
best_mode = conn->modes[local_best];
best_enc_id = conn->encoder_id;
}
}
drmModeFreeConnector(conn);
}
这段代码做了以下几件事:
选择最佳显示模式的逻辑封装在mode_score函数中:
c复制static int mode_score(const drmModeModeInfo* m) {
// 评分策略:
// 1) 优先 1920x1080
// 2) 否则按分辨率面积最大优先
int w = m->hdisplay;
int h = m->vdisplay;
int area = w * h;
if (w == 1920 && h == 1080) return 100000000 + area;
return area;
}
这个评分策略简单但实用:优先选择1920x1080分辨率(如果有),否则选择分辨率面积最大的模式。在实际应用中,可以根据需要调整评分策略,比如考虑刷新率、色彩深度等因素。
显示模式的刷新率是通过以下公式计算的:
c复制vrefresh = (int)((mode->clock * 1000LL) / (mode->htotal * mode->vtotal));
这里:
mode->clock是像素时钟频率,单位是kHzmode->htotal和mode->vtotal是水平和垂直总像素数vrefresh就是刷新率,单位是Hz在我们的开发板上,输出显示刷新率约为30Hz,这对于嵌入式应用通常是足够的。
完成所有连接器的探测后,我们需要确定使用哪个连接器和显示模式:
c复制if (!best_conn_id) {
printf("\n[DRM探测][错误] 没有找到"已连接且有模式"的 connector。\n");
drmModeFreeResources(res);
close(fd);
return 2;
}
drmModeConnector* best_conn = drmModeGetConnector(fd, best_conn_id);
if (!best_conn) {
printf("[DRM探测][错误] 读取 best connector 失败\n");
drmModeFreeResources(res);
close(fd);
return 3;
}
接下来需要找到与连接器关联的编码器和CRTC:
c复制uint32_t crtc_id = 0;
uint32_t enc_id = best_conn->encoder_id;
// 如果 connector->encoder_id 为 0,就尝试从 connector->encoders 列表选一个
if (enc_id == 0 && best_conn->count_encoders > 0) {
enc_id = best_conn->encoders[0];
}
drmModeEncoder* enc = NULL;
if (enc_id) enc = drmModeGetEncoder(fd, enc_id);
if (enc) {
crtc_id = enc->crtc_id;
}
这段代码处理了连接器可能没有关联编码器的情况,这种情况下会尝试从连接器支持的编码器列表中选择第一个可用的编码器。
最后,我们打印出探测到的配置信息:
c复制printf("\n[DRM探测][选择结果]\n");
printf(" - connector_id = %u\n", best_conn_id);
printf(" - mode = %s %dx%d\n", best_mode.name, best_mode.hdisplay, best_mode.vdisplay);
printf(" - encoder_id = %u\n", enc_id);
printf(" - crtc_id = %u\n", crtc_id);
if (!crtc_id) {
printf(" - 提示:crtc_id 为 0,可能需要更复杂的 encoder/crtc 匹配逻辑。\n");
}
在我们的开发板上,输出结果如下:
code复制[DRM探测][选择结果]
- connector_id = 49
- mode = 1080x1920 1080x1920
- encoder_id = 48
- crtc_id = 57
这表明:
DRM API使用后需要正确释放资源,否则会导致内存泄漏:
c复制if (enc) drmModeFreeEncoder(enc);
drmModeFreeConnector(best_conn);
drmModeFreeResources(res);
close(fd);
良好的资源管理习惯在嵌入式开发中尤为重要,因为嵌入式系统通常资源有限。
在实际产品代码中,错误处理应该更加完善。例如:
虽然我们的开发板只有一个显示接口,但在更复杂的系统中可能需要处理多个显示器。这时需要考虑:
完成屏幕探测后,下一步可以:
这些功能可以通过DRM的API逐步实现,最终构建一个完整的显示系统。