10. DRM图形显示框架¶
Linux系统的图像子系统软件框架比较复杂,涉及到GUI(图形用户界面)、3D application、DRM/KMS、hardware等, 看下下面的图片简单了解下(有兴趣也可以去了解下DRI框架,Direct Rendering Infrastructure):
而在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,图形执行管理器)
通常用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显示控制过程,如下图所示:
上面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):
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):
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支持多种显示接口:
lubancat2引出的接口有MIPI DSI和HDMI。以HDMI显示接口为例,默认的设备树(rk3568-lubancat2.dts)是开启了HDMI,设备树中关于HDMI的描述如下:
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函数,部分代码如下:
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程序:
待程序检测执行完毕,会列举出开发板上的DRM框架下的显示设备。 其中一些字样如Encoders、Connectors、CRTCs经过前面的介绍,大家都应该有了一点印象。
DRM框架下的显示过程如下图示:
我们要做的,就是找出前面终端中打印的HDMI屏幕对应的connectors、CRTCs的ID,使用命令:
./modetest -M rockchip -e
./modetest -M rockchip -c
图中状态为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,实验现象如图示:
在终端中按下回车键退出测试。 关于测试的更多相关内容,可以参考 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