欢迎访问我的网站,希望内容对您有用,感兴趣的可以加入我们的社群。

DRM framebuffer显示图像

Linux 迷途小书童 1年前 (2023-07-10) 1166次浏览 0个评论

在嵌入式设备上进行启动(bring-ups)时,一般使用如下命令来测试显示接口是否正常工作

$ dd if=/dev/urandom of=/dev/fb0

这将在显示屏上显示随机数据,并出现许多灰色、红色、蓝色和绿色的点。这并不能告诉我们是否一切正常,因为可能是分辨率不对或某些时序太紧(tight),但它已经显示了显示控制器是否工作,以及显示屏是否获得了一些可理解的数据。

不过,该示例仅适用于 Linux 下的帧缓冲设备驱动程序。对于其它系统来说,这意味着上述方法仅适用于专有驱动程序,而不适用于基于上游的驱动程序。现代 Linux 图形驱动提供了一个 DRM 接口到用户空间,因此不提供帧缓冲设备。我们不能只是简单地将数据转储到一个字符设备上以显示在显示器上。相反,我们需要建立一个完整的流水线,告诉 CRTC 应该使用什么样的帧缓冲作为输入。

DRM framebuffer

该图显示了可由 DRM 配置的三个不同阶段。第一阶段 CRTC(阴极射线管控制器,名称来源于老式阴极射线管显示器)将内存作为输入,并根据帧缓冲和平面创建图像。平面可以覆盖其他平面(如主平面、光标平面、视频平面等),CRTC 阶段的输出结果将是修改后的内存缓冲区,其中包含应显示的图像。并非所有 DRM 驱动程序都支持多平面。最少需要一个平面,即主平面。在本示例中,主平面表示 dumb framebuffer

第二阶段,编码器将内存中的数据转换为实际的显示信号。例如,一个编码器为计算机显示器生成 DVI 信号,而另一个编码器为 LVDS 显示器生成 LVDS 信号。

最后是连接器,它可以显示是否连接了显示器以及显示器支持的模式/分辨率(例如通过 EDID)。连接器也可以是虚拟连接器,如果信号连接到一个简单的显示器而没有信号化(例如 LVDS 或并行 RGB 面板)。

DRM-framebuffer

本文的目的是介绍如何编写一个简单的应用程序,使我们能够以类似于使用旧的帧缓冲设备接口的方式测试显示器。替换上面的 dd 命令如下

$ dd if=/dev/urandom | drm-framebuffer -d <dri device> -c <connector>

在本节中,我们将讨论创建帧缓冲区和设置屏幕像素的步骤。

获取帧缓冲

要获取帧缓冲,我们首先需要访问直接渲染管理器(Direct Rendering Manager)。为此,DRM 驱动程序将提供一个直接渲染接口,该接口位于 /dev/dri/cardX 下。通过该设备,我们可以访问显示控制器。显示控制器可以是 GPU 的一部分,也可以只是一个 2D 图形控制器。

/* Open the dri device /dev/dri/cardX */
fd = open(dri_device, O_RDWR);
if (fd < 0) {
    printf("Could not open dri device %s\n", dri_device);
    return -EINVAL;
}

现在我们可以访问直接渲染管理器了,我们想通过名称访问连接器。为此,我们首先需要知道图形控制器所拥有的资源

/* Get the resources of the DRM device (connectors, encoders, etc.)*/
res = drmModeGetResources(fd);
if (!res) {
    printf("Could not get drm resources\n");
    return -EINVAL;
}

现在我们可以检查该卡上的每个连接器是否与我们要使用的匹配

/* Search the connector provided as argument */
drmModeConnectorPtr connector = 0;
for (int i = 0; i < res->count_connectors; i++) {
    char name[32];

    connector = drmModeGetConnectorCurrent(fd, res->connectors[i]);
    if (!connector)
        continue;

    snprintf(name, sizeof(name), "%s-%u", connector_type_name(connector->connector_type),
            connector->connector_type_id);

    if (strncmp(name, connector_name, sizeof(name)) == 0)
            break;

    drmModeFreeConnector(connector);
}

连接器还将告诉我们该显示器的首选分辨率是多少

/* Get the preferred resolution */
drmModeModeInfoPtr resolution = 0;
for (int i = 0; i < connector->count_modes; i++) {
        resolution = &connector->modes[i];
        if (resolution->type & DRM_MODE_TYPE_PREFERRED)
         break;
}

我们希望从 CPU 访问帧缓存。为此,大多数 DRM 驱动程序都支持帧缓存类型的 dumb frame buffer 。这种类型的帧缓冲可以从 CPU 端访问,但不一定从 GPU 端访问。在这个简单的测试应用中,我们并不需要 3D 加速。

fb->dumb_framebuffer.height = resolution->vdisplay;
fb->dumb_framebuffer.width = resolution->hdisplay;
fb->dumb_framebuffer.bpp = 32;

err = ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &fb->dumb_framebuffer);
if (err) {
    printf("Could not create dumb framebuffer (err=%d)\n", err);
    goto cleanup;
}

err = drmModeAddFB(fd, resolution->hdisplay, resolution->vdisplay, 24, 32,
        fb->dumb_framebuffer.pitch, fb->dumb_framebuffer.handle, &fb->buffer_id);
if (err) {
    printf("Could not add framebuffer to drm (err=%d)\n", err);
    goto cleanup;
}

dumb framebuffer 的大小与显示分辨率相同,每像素32位,但色深只有24位。因此格式为 BGR(A),但不使用 alpha 通道。

为了显示帧缓冲,我们需要将其添加到 CRTC 中。这就是我们需要获取 CRTC 的原因

encoder = drmModeGetEncoder(fd, connector->encoder_id);
if (!encoder) {
    printf("Could not get encoder\n");
    err = -EINVAL;
    goto cleanup;
}

/* Get the crtc settings */
fb->crtc = drmModeGetCrtc(fd, encoder->crtc_id);

要在 dumb framebuffer 中绘制某些内容,我们需要访问实际内存。我们可以通过将 dumb framebuffer 映射到一个虚拟内存区域来实现

err = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &mreq);
if (err) {
    printf("Mode map dumb framebuffer failed (err=%d)\n", err);
    goto cleanup;
}

fb->data = mmap(0, fb->dumb_framebuffer.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, mreq.offset);
if (fb->data == MAP_FAILED) {
    err = errno;
    printf("Mode map failed (err=%d)\n", err);
    goto cleanup;
}

通过访问 fb->data,我们现在可以控制帧缓冲区,并通过修改内存在其中绘制。然而,我们还没有设置任何数据,这就是为什么我们还没有将帧缓冲添加到 CRTC 中。

最后一步是暂时放弃文件描述符的主控功能。通过这样做,我们允许其他应用程序更改 DRM 管道。如果我们想在另一个显示器上启动应用程序,这是必要的。然而,在大多数情况下,只有一个应用程序控制 DRM 管道。这通常是窗口系统(如 WestonKwinMutter 等)的合成器。

/* Make sure we are not master anymore so that other processes can add new framebuffers as well */
drmDropMaster(fd);

写入帧缓冲区

设置好流水线后,我们最终需要向帧缓存填充内容。在本应用中,我们用从 stdin 读取的数据填充显示。这样我们就可以通过管道将数据传输到帧缓冲

size_t total_read = 0;
while (total_read < fb->dumb_framebuffer.size)
    total_read += read(STDIN_FILENO, &fb->data[total_read], fb->dumb_framebuffer.size - total_read);

我们通过向 fb->data 写入内容来向帧缓冲区写入内容,fb->data 是指向 dumb framebuffer 虚拟内存的指针。完成缓冲区的填充后,我们可以通过调用 drmModeSetCrtc 将帧缓冲区分配给 CRTC。我们首先通过将 CRTC 帧缓冲区分配为0来清除显示,然后再分配我们的 dumb framebuffer

/* Make sure we synchronize the display with the buffer. This also works if page flips are enabled */
drmSetMaster(fb->fd);
drmModeSetCrtc(fb->fd, fb->crtc->crtc_id, 0, 0, 0, NULL, 0, NULL);
drmModeSetCrtc(fb->fd, fb->crtc->crtc_id, fb->buffer_id, 0, 0, &fb->connector->connector_id, 1, fb->resolution);
drmDropMaster(fb->fd);

以上是在基于 DRMLinux 驱动上显示图像的主要步骤。

使用方法

要使用 drm-framebuffer 应用程序,我们首先需要确保没有合成器阻塞 DRM 接口(master)。这意味着我们需要停止所有窗口管理器,如 X11Weston 等。如果我们运行 X11,也可以直接切换到虚拟控制台。我们通常可以通过按 CTRL+ALT+F2 来实现。这样,我们就切换到了虚拟控制台,X11 也就放弃了主控功能。现在我们可以使用 imagemagick 读取图像,并将其转换为原始 RGBA 图像。然后将该图像导入 drm-framebuffer,这样就可以在显示器上显示图像了。下面的命令将图像调整为720像素宽度,然后将其放在全高清图像的中心。

$ convert -resize 720x embear.png - | convert -extent 1920x1080 -gravity Center - bgra:- | drm-framebuffer -d /dev/dri/card1 -c HDMI-A-1

DRM framebuffer显示图像

适用场景

通过这个简单的应用程序,我们可以直接在帧缓冲上绘图。当还没有可用的框架,而我们只是想看看显示界面是否正常工作时,这在调试过程中会很有帮助。在开发应用程序时,我们通常不会直接使用 DRM,而是使用像 QtGTK 这样的框架。框架将处理内存分配并为我们设置管道。

本文翻译自 https://embear.ch/blog/drm-framebuffer

喜欢 (0)

您必须 登录 才能发表评论!