10. DRM图形显示框架

Linux系统的图像子系统软件框架比较复杂,涉及到GUI(图形用户界面)、3D application、DRM/KMS、hardware等, 看下下面的图片简单了解下(有兴趣也可以去了解下DRI框架,Direct Rendering Infrastructure):

broken

而在Linux显示设备驱动开发时,通常关注FBDEV(Framebuffer Device), DRM/KMS子系统。

在FrameBuffer Device驱动框架下,我们能够快速开发出可供简单使用的显示驱动。 但是随着芯片显示外设的性能逐渐增强、3D渲染及GPU的引入,FrameBuffer框架看起来似乎就有些落伍了, 最直接的体现,就是在传统的框架下,对于许多芯片显示外设的新特性如: 显示覆盖(菜单层级)、GPU加速、硬件光标等功能并不能得到很好得支持, 并且FrameBuffer框架将底层的显存通过用户空间/dev/fb接口,暴露给了用户空间, 这很容易导致不同的应用程序在操作显存时,产生访问冲突,而且这种方式看起来似乎不是那么安全。

在这背景下,就需要一个现代的图形显示框架来解决这些问题,那么DRM(Direct Rendering Manager,直接图形管理器)诞生。

10.1. DRM框架简述

那么DRM图形显示框架是怎么解决FrameBuffer框架遇到的困境呢? DRM将现代显示领域中会涉及的一些操作进行分层并使这些模块独立, 如过上层应用想操作显存、显示效果抑或是GPU,都必须在一些框架的约束下进行,我们可以来了解一下。

我们可以从用户空间、内核空间的两个角度去了解DRM框架:

用户空间(libdrm driver):

  • Libdrm(DRM框架在用户空间的Lib)

内核空间(DRM driver):

  • KMS(Kernel Mode Setting,内核显示模式设置)

  • GEM(Graphic Execution Manager,图形执行管理器)

broken

通常用DRM/KMS来指代整个DRM subsystem,但是KMS和DRM driver只是整个DRM subsystem的其中2个部分。

10.1.1. Libdrm

DRM框架在用户空间提供的Libdrm,对底层接口进行封装,主要是对各种IOCTL接口进行封装,向上层提供通用的API接口, 用户或应用程序在用户空间调用libdrm提供的库函数,即可访问到显示的资源,并对显示资源进行管理和使用。

这样通过libdrm对显示资源进行统一访问,libdrm将命令传递到内核最终由DRM驱动接管各应用的请求并处理, 可以有效避免访问冲突。

10.1.2. KMS(Kernel Mode Setting)

KMS属于DRM框架下的一个大模块,主要负责两个功能:显示参数设置及显示画面控制。 这两个基本功能可以说是显示驱动必须基本的能力,在DRM框架下, 为了将这两部分适配得符合现代显示设备逻辑,又分出了几部分子模块配合框架。

10.1.2.1. DRM FrameBuffer

DRM FrameBuffer是一个软件抽象,硬件无关的基本元素,描述了图层显示内容的信息(width, height, pixel_format,pitch等)。

10.1.2.2. Planes

基本的显示控制单位,每个图像拥有一个Planes,Planes的属性控制着图像的显示区域、图像翻转、色彩混合方式等, 最终图像经过Planes并通过CRTC组件,得到多个图像的混合显示或单独显示的等等功能。

10.1.2.3. CRTC

CRTC的工作,就是负责把要显示图像,转化为底层硬件层面上的具体时序要求,还负责着帧切换、电源控制、色彩调整等,可以连接多个 Encoder ,实现复制屏幕功能。

10.1.2.4. Encoder

转换输出器,负责电源管理、显然输出需要不同的信号转换器,将内存的像素转换成显示器需要的信号。

10.1.2.5. Connector

Connector连接器负责硬件设备的接入,比如HDMI,VGA等,可以获取到设备EDID , DPMS连接状态等等。

上述的这些组件,最终完成了一个完整的DRM显示控制过程,如下图所示:

broken

上面CRTC、Planes、Encoder、Connector这些组件是对硬件的抽象,即使没有实际的硬件与之对应,在软件驱动中也需要实现这些,否则DRM子系统无法正常运行。

10.1.3. GEM(generic DRM memory-management)

顾名思义,GEM负责对DRM使用的内存(如显存)进行管理,是一个软件抽象。

GEM框架提供的功能包括:

  • 内存分配和释放

  • 命令执行

  • 执行命令时的管理

10.2. 驱动简述

我们通过简单讲解了DRM驱动的框架,简单地带领大家认识了DRM框架下对显示功能的实现方法。 实际的代码细节远比上述给大家介绍的内容复杂得多,给大家讲解框架组件功能只是起到一个抛砖引玉的作用。

片上负责显示功能的驱动,这些一般由芯片厂商rockchip来负责实现,完成一个DRM-Host,主机驱动代码一般位于 drivers/gpu/drm/xxx/ 目录下,这里xxx代指芯片厂商如ST、NXP。 如果对详细源码细节感兴趣的同学,可以在内核源码目录 drivers/gpu/drm 中,查看具体的驱动实现。

在驱动中rockchip的显示驱动使用component框架,显示驱动为master,该显示驱动下的设备称为component。 在display_subsystem设备节点中的ports节点就是关联的component,看设备树源码实际指向vop_out节点, (rk3568的VOP是各种输出图像的接口),在该节点下有三个port,应该同时可以输出三视频信号。 设备树如下(具体请看arch/arm64/boot/dts/rockchip/rk3568.dtsi):

arch/arm64/boot/dts/rockchip/rk3568.dtsi
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
    display_subsystem: display-subsystem {
            compatible = "rockchip,display-subsystem";
            memory-region = <&drm_logo>, <&drm_cubic_lut>;
            memory-region-names = "drm-logo", "drm-cubic-lut";
            ports = <&vop_out>;
            devfreq = <&dmc>;

            route {
                    route_dsi0: route-dsi0 {
                            status = "disabled";
                            logo,uboot = "logo.bmp";
                            logo,kernel = "logo_kernel.bmp";
                            logo,mode = "center";
                            charge_logo,mode = "center";
                            connect = <&vp0_out_dsi0>;
    /*..................*/
    };
};
/*..................*/
vop: vop@fe040000 {
    compatible = "rockchip,rk3568-vop";
    reg = <0x0 0xfe040000 0x0 0x3000>, <0x0 0xfe044000 0x0 0x1000>;
    reg-names = "regs", "gamma_lut";
    rockchip,grf = <&grf>;
    interrupts = <GIC_SPI 148 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru ACLK_VOP>, <&cru HCLK_VOP>, <&cru DCLK_VOP0>, <&cru DCLK_VOP1>, <&cru DCLK_VOP2>;
    clock-names = "aclk_vop", "hclk_vop", "dclk_vp0", "dclk_vp1", "dclk_vp2";
    iommus = <&vop_mmu>;
    power-domains = <&power RK3568_PD_VO>;
    status = "disabled";

    vop_out: ports {
        #address-cells = <1>;
        #size-cells = <0>;
        vp0: port@0 {
        /*..................*/
        vp1: port@1 {
        /*..................*/
        vp2: port@2 {
        /*..................*/
    };
};

平台驱动rockchip-drm匹配到设备树,会到设备树dts查找ports节点和iommus节点, 使用component_master_add_with_match函数注册自己到component框架中,设置了rockchip_drm_ops,其component可以通过component_add函数增加, master匹配上所有component后,会调用master的bind回调函数,最后通过drm_dev_register()函数注册到DRM core。 部分驱动源码如下(具体请参考drivers/gpu/drm/rockchip/rockchip_drm_drv.c):

drivers/gpu/drm/rockchip/rockchip_drm_drv.c
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static const struct component_master_ops rockchip_drm_ops = {
    .bind = rockchip_drm_bind,
    .unbind = rockchip_drm_unbind,
};

static int rockchip_drm_platform_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct component_match *match = NULL;
    int ret;

    ret = rockchip_drm_platform_of_probe(dev);
#if !IS_ENABLED(CONFIG_DRM_ROCKCHIP_VVOP)
    if (ret)
        return ret;
#endif

    match = rockchip_drm_match_add(dev);
    if (IS_ERR(match))
        return PTR_ERR(match);

    ret = component_master_add_with_match(dev, &rockchip_drm_ops, match);
    if (ret < 0) {
        rockchip_drm_match_remove(dev);
        return ret;
    }
    dev->coherent_dma_mask = DMA_BIT_MASK(64);

    return 0;
}

static struct platform_driver rockchip_drm_platform_driver = {
    .probe = rockchip_drm_platform_probe,
    .remove = rockchip_drm_platform_remove,
    .shutdown = rockchip_drm_platform_shutdown,
    .driver = {
        .name = "rockchip-drm",
        .of_match_table = rockchip_drm_dt_ids,
        .pm = &rockchip_drm_pm_ops,
    },
};

下面我们以lubancat2为例,其rk3568支持多种显示接口:

broken

lubancat2引出的接口有MIPI DSI和HDMI。以HDMI显示接口为例,默认的设备树(rk3568-lubancat2.dts)是开启了HDMI,设备树中关于HDMI的描述如下:

arch/arm64/boot/dts/rockchip/rk3568.dtsi
 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
28
29
30
31
32
33
34
35
36
37
38
39
40
hdmi: hdmi@fe0a0000 {
            compatible = "rockchip,rk3568-dw-hdmi";
            reg = <0x0 0xfe0a0000 0x0 0x20000>;  /*寄存器物理基地址0xfe0a0000  内存映射长度0x20000*/
            interrupts = <GIC_SPI 45 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&cru PCLK_HDMI_HOST>,
                     <&cru CLK_HDMI_SFR>,
                     <&cru CLK_HDMI_CEC>,
                     <&pmucru PLL_HPLL>,
                     <&cru HCLK_VOP>;
            clock-names = "iahb", "isfr", "cec", "ref", "hclk";
            power-domains = <&power RK3568_PD_VO>;
            reg-io-width = <4>;     /*寄存器读写访问宽度 4字节*/
            rockchip,grf = <&grf>;
            #sound-dai-cells = <0>;
            pinctrl-names = "default";
            pinctrl-0 = <&hdmitx_scl &hdmitx_sda &hdmitxm0_cec>;
            status = "disabled";

            ports {
                    #address-cells = <1>;
                    #size-cells = <0>;

                    hdmi_in: port {  /*绑定vop2的hdmi接口的端节点*/
                            reg = <0>;
                            #address-cells = <1>;
                            #size-cells = <0>;

                            hdmi_in_vp0: endpoint@0 {
                                    reg = <0>;
                                    remote-endpoint = <&vp0_out_hdmi>;
                                    status = "disabled";
                            };
                            hdmi_in_vp1: endpoint@1 {
                                    reg = <1>;
                                    remote-endpoint = <&vp1_out_hdmi>;
                                    status = "disabled";
                            };
                    };
            };
    };

hdmi的平台驱动在rockchip_drm_init中注册,匹配设备树时,对应的驱动会调用,调用dw_hdmi_rockchip_probe函数,最后通过component_add函数注册component, 设置component_ops操作函数,当master的bind回调之后,就会调用各component的bind回调,即这里的dw_hdmi_rockchip_bind函数,部分代码如下:

drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static const struct component_ops dw_hdmi_rockchip_ops = {
    .bind   = dw_hdmi_rockchip_bind,
    .unbind = dw_hdmi_rockchip_unbind,
};

static int dw_hdmi_rockchip_probe(struct platform_device *pdev)
{
    pm_runtime_enable(&pdev->dev);
    pm_runtime_get_sync(&pdev->dev);

    return component_add(&pdev->dev, &dw_hdmi_rockchip_ops);
}
/*...........*/
struct platform_driver dw_hdmi_rockchip_pltfm_driver = {
    .probe  = dw_hdmi_rockchip_probe,
    .remove = dw_hdmi_rockchip_remove,
    .shutdown = dw_hdmi_rockchip_shutdown,
    .driver = {
        .name = "dwhdmi-rockchip",
        .of_match_table = dw_hdmi_rockchip_dt_ids,
        .pm = &dw_hdmi_pm_ops,
    },
};

在dw_hdmi_rockchip_bind函数中会对hdmi进行初始化,寻找crtc,初始化Encoder等。

10.3. 屏幕显示测试

测试在DRM驱动框架下的驱动效果,我们进行HDMI屏幕的测试,使用的是rk3568-lubancat2.dts设备树,默认已经开启HDMI。

10.3.1. Libdrm

编写一个libdrm的测试程序较为复杂,这里我们使用libdrm官方的测试工具来进行测试,我们可以在这里下载源码并进行交叉编译出测试工具,以供在开发板上使用: libdrm .

新版的libdrm使用meson+ninja的构建方式,而不是老版的autotools,没有基础的同学构建新版libdrm会比较痛苦。 建议直接使用我们给大家编译好的测试程序,测试程序位于配套例程 linux_driver/framework_drm/modetest

如果要自己编译libdrm,可以参考下面命令:

git clone https://gitlab.freedesktop.org/mesa/drm

sudo apt -y install python3-pip cmake git ninja-build

python3 -m pip install meson  /*安装之后,重启板卡*/

meson . build && ninja -C build

编译之后在build/tests/modetest/下会有modetest程序, 对libdrm测试程序感兴趣的同学,可以下载libdrm源码解压,在其目录/drm/tests/modetest/下,查看modetest.c文件,此为测试程序源码。

10.3.2. 实验操作

将上述的modetest测试程序上传至开发板中,可能需要chmod添加可执行权限,自己编译的就直接使用,然后使用下面命令关闭图形界面:

#命令关闭图形界面
sudo systemctl set-default multi-user.target

sudo reboot

#测试完后,开启图像界面使用命令:
sudo systemctl set-default graphical.target

直接使用命令 ./modetest 运行modetest程序:

broken

待程序检测执行完毕,会列举出开发板上的DRM框架下的显示设备。 其中一些字样如Encoders、Connectors、CRTCs经过前面的介绍,大家都应该有了一点印象。

DRM框架下的显示过程如下图示:

broken

我们要做的,就是找出前面终端中打印的HDMI屏幕对应的connectors、CRTCs的ID,使用命令:

./modetest -M rockchip  -e
./modetest -M rockchip -c
broken broken

图中状态为connected的connectorsID为150,name是HDMI-A-1。 CRTCs对象中,唯一个id为71的CRTC,参数也为我们的HDMI屏幕参数。

则我们可以执行如下命令进行测试:

./modetest -M rockchip  -s  150@71:1920x1080

其中150、71分别为我们屏幕的Connectors ID、CRTCs ID,实验现象如图示:

broken

在终端中按下回车键退出测试。 关于测试的更多相关内容,可以参考 linux DRM/KMS 测试工具 modetest .

文章末尾补充小知识,虽说DRM功能符合现代显示设备的需求,但是仍有众多的老设备及软件需要Framebuffer的支持。 所以在DRM框架下,有部分代码用于实现在DRM框架下,去模拟FB设备。

在rockchip提供的显示驱动代码中,也有模拟FB设备的相关代码, 参见drivers/gpu/drm/rockchip/rockchip_drm_fb.c文件,最终效果就是设备目录下,出现熟悉的身影 /dev/fb0

我们可以通过传统测试FrameBuffer设备的方式,使用如下命令来测试它:

# 会得到类似花屏的效果
cat /dev/random > /dev/fb0