[Linux 基础] — Linux DRM(三)RK 平台 DRM 代码分析 – Rockchip

一、概览

不知道大家是否还记得,之前我有引用 Wiki 中对 DRM 的介绍,这里我们再回顾一下:DRM 由两个部分组成:一是 Kernel 的子系统,这个子系统对硬件 GPU 操作进行了一层框架封装。二是提供了一个 libdrm 库,里面封装了一系列 API,用来进行图像显示。整体来看和 Android 上所采用的 Direct Frame Buffer 差不多。Android Kernel 走的是 FB 的框架,并在 HAL 抽象出一个 FBDEV,来进行 FB IOCTL 统一管理。DRM 就相当于直接对图形设备集中处理,并且多出了一个 libdrm库。

其整体脉络如下:

源码文件

Component framework

在讲述启动过程之前,先简单了解一下 Component framework。

因为 drm 下挂了许多的设备,启动顺序经常会引发各种问题:

  • 一个驱动完全有可能因为等另一个资源的准备,而 probe deferral,导致顺序不定
  • 子设备没有加载好,主设备就加载了,导致设备无法工作
  • 子设备相互之间可能有时序关系,不定的加载顺序,可能带啦有些时候设备能工作,有些时候又不能工作
  • 现在现在编 kernel 是多线程编译的,编译前后顺序也会影响驱动的加载顺序。

这时就需要有一个统一管理的机制,将所有设备统合起来,按照一个统一的顺序加载,Display-subsystem 正式用来解决这个问题的,依赖于 Component 的驱动,通过这个驱动,可以把所有的设备以组件的形式加在一起,等所有的组件加载完毕后,统一进行 bind/unbind。

代码路径:drivers/base/component.c

以下为 rockchip drm master probe 阶段 Component 主要逻辑,为了减小篇幅,去掉了无关的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int rockchip_drm_platform_probe(struct platform_device *pdev)
{
    for(int=0;; i++){
        /* ports 指向了 vop 的设备节点 */
        port = of_parse_phandle(np, "ports", i);
        component_match_add(dev, &match, compare_of, port->parent);
    }
    for(i=0; ; i++) {
        port = of_parse_phandle(np, "ports", i);
        component_match_add(dev, &match, compre_of, port->parent);
    }
    for(i=0;; i++) {
        port = of_parse_phandle(np, "ports", i);
        /* 搜查 port 下的各个 endpoint,将它们也加入到 match 列表 */
        rockchip_add_endpoints(dev, &match,port);
    }
    return component_master_add_with_match(dev, &rockchip_drm_ops, match);
}

static void rockchip_add_endpoints(...)
{
    for_each_child_of_node(port, ep) {
        remote = of_graph_get_remote_port_parent(ep);
        /* 这边的 remote 即为和 vop 关联的输出设备,即为 edp,mipi,或 hdmi */
        component_match_add(dev, match, compare_of, remote);
    }
}

启动过程

图自 makyzq gitbook:

基于 Component 矿建,在 probe 阶段:

  • 解析 dts 中的各个设备的信息
  • 加到 Component match 列表中
  • 设备加载完毕后,master 设备进行 bind

RK DRM Device Driver

device tree

display_subsystem: display-subsystem {
compatible = "rockchip,display-subsystem";
ports = <&vopl_out>, <&vopb_out>;
status = "disabled";
};

- compatible: Should be "rockchip, display-subsystem"
- ports: Should contain a list of phandles pointing to display interface port of vop devices. vop definitions as defined in
kernel/Documentation/devicetree/bindings/display/rockchip/rockchip-vop.txt

drm driver

代码路径:

static struct drm_driver rockchip_drm_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM |
DRIVER_PRIME | DRIVER_ATOMIC |
DRIVER_RENDER,
.preclose = rockchip_drm_preclose,
.lastclose = rockchip_drm_lastclose,
.get_vblank_counter = drm_vblank_no_hw_counter,
.open = rockchip_drm_open,
.postclose = rockchip_drm_postclose,
.enable_vblank = rockchip_drm_crtc_enable_vblank,
.disable_vblank = rockchip_drm_crtc_disable_vblank,
.gem_vm_ops = &rockchip_drm_vm_ops,
.gem_free_object = rockchip_gem_free_object,
.dumb_create = rockchip_gem_dumb_create,
.dumb_map_offset = rockchip_gem_dumb_map_offset,
.dumb_destroy = drm_gem_dumb_destroy,
.prime_handle_to_fd = drm_gem_prime_handle_to_fd,
.prime_fd_to_handle = drm_gem_prime_fd_to_handle,
.gem_prime_import = drm_gem_prime_import,
.gem_prime_export = drm_gem_prime_export,
.gem_prime_get_sg_table = rockchip_gem_prime_get_sg_table,
.gem_prime_import_sg_table = rockchip_gem_prime_import_sg_table,
.gem_prime_vmap = rockchip_gem_prime_vmap,
.gem_prime_vunmap = rockchip_gem_prime_vunmap,
.gem_prime_mmap = rockchip_gem_mmap_buf,
#ifdef CONFIG_DEBUG_FS
.debugfs_init = rockchip_drm_debugfs_init,
.debugfs_cleanup = rockchip_drm_debugfs_cleanup,
#endif
.ioctls = rockchip_ioctls,
.num_ioctls = ARRAY_SIZE(rockchip_ioctls),
.fops = &rockchip_drm_driver_fops,
.name = DRIVER_NAME,
.desc = DRIVER_DESC,
.date = DRIVER_DATE,
.major = DRIVER_MAJOR,
.minor = DRIVER_MINOR,
};

vop driver

代码路径:

drivers/gpu/drm/rockchip/rockchip_drm_vop.c

drivers/gpu/drm/rockchip/rockchip_vop_reg.c

结构体:

struct vop;
// vop 驱动根结构,一个 vop 对应一个 struct vop 结构

struct vop_win;
// 描述图层信息,一个硬件图层对应一个 struct vop_win 结构

寄存器读写:为了兼容各种不同版本的 vop,vop 驱动里面使用了寄存器级的抽象,由一个结构体来保存抽象关系,这样主题逻辑只需要操作抽象后的功能定义,由抽象的读写接口根据抽象关系写到真实的 vop 硬件中。

示例:

static const struct vop_win_phy rk3288_win23_data = {
.enable = VOP_REG(RK3288_WIN2_CTRL0, 0x1, 4),
}

static const struct vop_win_phy rk3368_win23_data = {
.enable = VOP_REG(RK3368_WIN2_CTRL0, 0x1, 4),
}

rk3368 和 rk3288 图层的地址分布不同,但在结构定义的时候,可以将不同的硬件图层 bit 映射到同一个 enable 功能上,这样 vop 驱动主体调用 VOP_WIN_SET(vop, win, enable, 1) 时就能操作到真实的 vop 寄存器了。

2.1、设备文件 cardX

DRM 处于内核空间,这意味着用户空间需要通过系统调用来申请它的服务。不过 DRM 并没有定义它自己的系统调用。相反,它遵循 “Everything is file” 的原则,通过文件系统,在 /dev/dri/ 目录下暴露了 GPU 的访问方式。DRM 会检测每个 GPU,并生成对应的 DRM 设备,创建设备文件 /dev/dri/cardX 与 GPU 相接。X 为 0-15 的数值,默认是 Card0.

用户空间的程序如果希望访问 GPU 则必须打开该文件,并使用 ioctl 与 DRM 通信。不同的 ioctl 对应 DRM API 的不同功能。

2.2、用户空间的内存操作

我们定义一个外部的内存结构来更好的描述如何进行 userspace 的 drm 操作。首先使用到 DRM 相关操作的时候需要引用 drm.h。

1
2
3
4
5
6
7
8
9
10
#include <drm.h>

struct bo {
    int fd;
    void *ptr;
    size_t size;
    size_t offset;
    size_t pitch;
    unsigned handle;
};

2.2.1、获取设备节点

1
bo->fd = open("/dev/dri/card0", O_RDWR, 0);

2.2.2、分配内存空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct drm_mode_create_dumb arg;
int handle, size, pitch;
int ret;

memset(&arg, 0, sizeof(arg));

arg.bpp = bpp;
arg.width = width;
arg.height = height;

ret = drmIoctl(bo->fd, DRM_IOCTL_MODE_CREATE_DUMB, &arg);

if(ret) {
    fprintf(stderr, "failed to create dumb buffer: %s\n", strerror(errno));
    return ret;
}

bo->handle = arg.handle;
bo->size = arg.size;
bo->pitch = arg.pitch;

2.2.3、映射物理内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct drm_mode_map_dumb arg;
void *map;
int ret;

memset(&arg, 0, sizeof(arg));

arg.handle = bo->handle;

ret = drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &arg);
if(ret){
    return ret;
}

map = drm_mmap(0, bo->size, PROT_READ | PROT_WRITE, MAP_SHARED, FD, arg.offset);
if(map == MAP_FAILED){
    return -EINVAL;
}
bo->ptr = map;

2.2.4、解除物理内存映射

1
2
drm_munmap(bo->ptr, bo->size);
bo->ptr = NULL;

2.2.5、释放内存

1
2
3
4
5
6
7
8
9
10
11
struct drm_mode_destroy_dumb arg;
int ret;

memset(&arg, 0, sizeof(arg));

arg.handle = bo->hand;

ret = drmIoctl(bo->fd, DRM_IOCTL_MODE_DESTROY_DUMB, &arg);
if(ret) {
    fprintf(stderr, "failed to destroy dumb buffer: %s\n", strerror(errno));
}

2.2.6、释放 gem handle

1
2
3
4
5
6
7
struct drm_gem_close args;

memset(&arg, 0, sizeof(args));

args.handle = bo->handle;

drmIoctl(bo->fd, DRM_IOCTL_GEM_CLOSE, &args);

2.2.7、export dmafd

1
2
3
4
5
6
int export_dmafd;

ret = drmPrimeHandleToFD(bo->fd, bo->handle, 0, &export_dmafd);

//drmPrimeHandleToFD 是会给 drm_buf 加引用计数的
//使用完 export_dmafd 后,需要使用 close(export_dmafd) 来减掉引用计数

2.2.8、import dmafd

1
2
3
4
ret = drmPrimeFDToHandle(bo->fd, import_dmafd, &bo->handle);

// drmPrimeHandleToFd 是会给 dma_buf 加引用计数的
// 使用完 bo->handle 后,需要对 handle 减引用,参看 free gem handle 部分。

2.2、DRM libdrm

libdrm 被创建以用于方便用户空间和 DRM 子系统的联系。它仅仅只提供了一些函数的包装(C),这些函数是为 DRM API 的每一个 ioctl、敞亮、结构体而写。使用 libdrm 这个库不仅仅避免了将内核接口直接暴露给用户空间,也有代码复用等常见有点。

2.3、DRM 代码结构

分为两个部分:通用的 DRM Core 和适配于不同类型硬件的 DRM Driver。

  1. DRM Core 提供了不同 DRM 驱动程序可以注册的基本框架,并且为用户空间提供了具有通用,独立于硬件功能的最小 ioctl 集合。
  2. DRM Driver 实现了 API 的硬件依赖部分。它提供了没有被 DRM Core 覆盖的其余 ioctl 的实现,它也可以拓展 API,提供额外的 ioctl。比如某个特定的 DRM Driver 提供了一些增强的 API,用户空间的 libdrm 也需要额外的 libdrm-driver 拓展,用来使用这些额外的 ioctl。

2.4、DRM API

DRM Core 向用户空间应用程序导出了多个接口,让相应的 libdrm 包装成函数后来使用。

DRM Driver 导出的特定设备的接口,可以通过 ioctls 和 sysfs 来供用户空间使用。

2.5、DRM-Master 和 DRM-Auth

DRM API 中有几个 ioctl 由于并发问题仅限于用户空间单个进程使用。为了实现这种限制,将 DRM 设备分为 Master 和 Auth。上述的那些 ioctl 只能被 DRM-Master 的进程调用。打开了 /dev/dri/cardX 的进程的文件句柄会被标志为 master,特别是第一个调用该 SET_MASTER ioctl 的进程。如果不是 DRM-Master 的进程在使用这些限制 ioctl 的时候会返回错误。进程也可以通过 DROP_MASTER ioctl 放弃 Master 角色,来让其他进程变成 Master。

X Server 或者其他的 Display Server 通常会是他们所管理的 DRM 设备的 DRM-Master 进程。当 DRM 设备启动的时候,这些 Display Server 打开设备节点,获取 DRM-Master 权限,直到关闭设备。

对于其他的用户空间进程,还有一种办法可以获得 DRM 设备的这些受限权限,这就是 DRM-Auth。它是一种针对 DRM 设备的验证方式,用来证明该进程已经获得了 DRM-Master 对于他们去访问受限 ioctls 的许可。

步骤:

  • DRM Client 使用 GET_MAGIC ioctl 从 DRM 设备获取一个 32bit 整型的 token。并通过任何方式(通常是 IPC)传递给 DRM-Master。
  • DRM-Master 进程使用 AUTH-MAGIC ioctl 返回 token 给 DRM 设备。
  • 设备将 DRM-Master 所给的 token 和 Auth 的进行对比。通过的话就赋予进程文件句柄特殊的权限。

#

DRM 在源码中的架构(图自 Mark.Yao):

使用 DRM 访问 Video Card(图自 Wikipedia):

没有 DRM 时,用户空间进程访问 GPU 的方式,如上图。

有 DRM 后,用户空间访问 GPU 的方式,如上图。