Rockchip RK3399 - DRM驱动程序
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4
开发板
eMMC
:16GB
LPDDR3
:4GB
显示屏 :15.6
英寸HDMI
接口显示屏
u-boot
:2023.04
linux
:6.3
----------------------------------------------------------------------------------------------------------------------------
如果我们需要编写一个DRM
驱动,我们应该怎么做呢?具体流程如下:
(1) 定义struct drm_driver
,并初始化成员name
、desc
、data
、major
、minor
、driver_features
、fops
、dumb_create
等;
(2)调用drm_dev_alloc
函数分配并初始化一个struct drm_device
;
(3) 调用drm_mode_config_init
初始化drm_device
中mode_config
结构体;
(4) 调用drm_xxx_init
创建 framebuffer
、plane
、crtc
、encoder
、connector
这5个 drm_mode_object
;
在DRM
子系统中是通过component
框架完成各个功能模块的注册,比如在:
CRTC
驱动程序:包含了plane
和crtc
的初始化工作;HDMI
驱动程序:包含了encoder
和connector
的初始化工作;edp
驱动程序:包含了encoder
和connector
的初始化工作;- ......
(5) 调用drm_dev_register
注册drm_device
;
- 创建
drm
设备节点/dev/dri/card%d
; - 注册
plane
、crtc
、encoder
、connector
这4个drm_mode_object
;
一、显示子系统概述
显示子系统是Rockchip
平台显示输出相关软硬件系统的统称,linux
内核采用component
框架来构建显示子系统,一个显示子系统由显示处理器(vop
,video output processor
)、接口控制器(mipi
,lvds
,hdmi
、edp
、dp
、rgb
、BT1120
、BT656
、I8080
(MCU
显示接口)等)、液晶背光,电源等多个独立的功能模块构成。
那么问题来了,什么是显示处理器?
- 将在内存中的图像数据,转化为电信号送到显示设备称为显示控制器,比如早期的
LCDC
; - 后面进行了拓展,可以处理一些简单的图像,比如缩放、旋转、合成等,如瑞芯的
vop
,高通的sde
称为显示处理器;
显示处理器可以在没有CPU
参与的情况下可以做一些简单的图像处理,比如:
- 缩放,旋转等操作;
- 支持多层,并可以进行合成,支持
porter-duff
; - 支持多种显存格式(
ARGB888
,RGB565
, 包括GPU
输出的tile
格式以及fbdc
压缩格式)等; - 支持生成时序信号如
tcon
,送给如mipi
、lvds
等接口; - 支持多种分辨率;
1.1 硬件框图
整个显示系统的硬件框架如下图所示:
从上面的框图可以看到,在整个显示通路的最后端,是由RGA
,GPU
、VPU
组成的显示图形加速模块,他们是专门针对图像处理优化设计的硬件IP
,能够高效的进行图像的⽣成和进一步处理(比如GPU
通过opengl
功能提供图像渲染功能,RGA
可以对图像数据进行缩放,旋转,合成等2D
处理,VPU
可以高效的进行视频解码),从而减轻CPU
负担。
经过这些图像加速模块处理后的数据会存放在DDR
中,然后由VOP
读取,根据应用需求进行Alpha
叠加,颜色空间转换,gamma
矫正,HDR
转换 等处理后,再发送到对应的显示接口模块(HDMI
、eDP/DP
、DSI
、RGB/BT1120/BT656
、LVDS
), 这些接口模块会把接收到的数据转换成符合各⾃协议的数据流,发送到显示器或者屏幕上,呈现在最终用户眼前。
目前Rockchip
平台上存在两种VOP
架构:
VOP 1.0
:VOP 1.0
是用多VOP
的方式来实现多屏幕显示,即正常情况下,一个VOP
在同一时刻只能输出一路独立的显示时序,驱动一个屏幕显示独立的内容。如果需要实现双屏显示,则需要有两个VOP
来实现,所以在RK3288
,RK3399
,PX30
等⽀持双显的平台上,都有两个独立的VOP
;VOP 2.0
:VOP 2.0
采用了统一显示架构,即整个SoC
上只存在一个VOP
,但是在VOP
的后端设计了多路独立的Video Port
(简称VP
) 输出接口,这些VP
能够同时独立⼯作,并且输出相互独立的显示时序。比如在上面的VOP 2.0
框图中,有三个VP
,就能同时实现三屏异显;
1.1.1 RK3399
RK3399
有2个VOP
:
Video Output Processor
(VOP_BIG
):supports 4096x2160 with AFBC
;Video Output Processor
(VOP_LIT
):supports 2560x1600
;
支持的显示接口:
- 双通道
MIPI DSI
(4线/通道)显示接口; - 1个
eDP
显示接口; - 1个
DP
显示接口; - 1个
HDMI
显示接口;
RK3399
支持的最大输出分辨率和协议标准如下:
显示接口 | 最大输出 | 协议标准 |
---|---|---|
eDP | VOP BIG: 3840x2160@60hz VOP LITE: 2560x1600@60hz |
支持 DP1.2a 和 eDP1.3 协议标准 |
MIPI | 单通道:1920x1080@60hz 双通道:2560x1600@60hz |
支持 DSI v1.1,DCS v1.1,DPHY v1.1 协议标准 |
HDMI | VOP BIG::4096X2160@60hz VOP LITE: 2560x1600@60hz |
支持 HDMI 1.4a 和 2.0a 协议标准 |
DP | VOP BIG::4096X2160@60hz VOP LITE: 2560x1600@60hz |
支持 DP 1.2 协议标准 |
1.1.2 NanoPC T4
我们所使用的的NanoPC T4
开发板,视频输出支持:
LCD Interface
: 一个eDP 1.3
(4线,10.8Gbps
), 一个或2个4线MIPI DSI
;DP on Type-C
:DisplayPort 1.2 Alt Mode on USB Type-C
;HDMI
:HDMI 2.0a
, 支持4K@60Hz
显示,支持HDCP 1.4/2.2
;
1.2 DRM
加载顺序
DRM
驱动是由一系列相关功能模块的驱动的结合,它包含了vop
、mipi
、lvds
、hdmi
、edp
、dp
、backlight
等等显示通路上的依赖模块。只有这些相互依赖的模块都加载完整,整个drm
系统才算启动完成。
在《DRM
子系统》中我们介绍了如何去抽象显示硬件到具体的DRM object
;这里我们结合Rockchip
平台以MIPI DSI
显示接口为例来介绍显示硬件到具体的DRM object
抽象,
object | 说明 |
---|---|
plane | 图层;对Overlay硬件的抽象,同样需要访问Display Controller寄存器,因此也放在Display Controller驱动中 在Rockchip平台里对应SoC内部vop模块的win图层 |
crtc | 显示控制器;RGB timing的产生,以及显示数据的更新,都需要访问Dislay Controller硬件寄存器,因此放在Display Controller驱动中 在Rockchip平台里对应SoC内部的vop模块 |
encoder | 编码器;将RGB并行信号转换为DSI行信号,需要配置DSI硬件寄存器,因此放在DSI Controller驱动中 |
connector | 连接器;可以通过drm_panel来获取LCD的mode信息,但是encoder在哪,connector就在哪,因此放在DSI Controller驱动中 |
drm_panel | 用于获取LCD mode参数,并提供LCD休眠唤醒的回调接口,供encoder调用,因此放在LCD驱动中 |
bridge | 桥接设备;一般用于注册encoder后面另外再接的转换芯片,如DSI2HDMI转换芯片 |
接下来我们将会以RK3399 DRM
驱动为例对显示子系统的各个模块进行介绍;
驱动 | 文件清单 |
---|---|
core | drivers/gpu/drm/rockchip/rockchip_drm_drv.c |
framebuffer | drivers/gpu/drm/rockchip/rockchip_drm_fb.c |
gem | drivers/gpu/drm/rockchip/rockchip_drm_gem.c |
vop | drivers/gpu/drm/rockchip/rockchip_drm_vop.c drivers/gpu/drm/rockchip/rockchip_vop_reg.c drivers/gpu/drm/rockchip/rockchip_drm_vop2.c drivers/gpu/drm/rockchip/rockchip_vop2_reg.c |
lvds | drivers/gpu/drm/rockchip/rockchip_lvds.c |
rgb | drivers/gpu/drm/rockchip/rockchip_rgb.c |
mipi | drivers/gpu/drm/rockchip/dw-mipi-dsi-rockchip.c |
hdmi | drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c drivers/gpu/drm/rockchip/inno_hdmi.c |
edp | drivers/gpu/drm/rockchip/analogix_dp-rockchip.c drivers/gpu/drm/bridge/analogix/analogix_dp_core.c drivers/gpu/drm/bridge/analogix/analogix_dp_reg.c |
dp | drivers/gpu/drm/rockchip/cdn-dp-core.c drivers/gpu/drm/rockchip/cdn-dp-reg.c |
显示子系统各个模块驱动加载顺序如下图所示:
这里我们对驱动加载顺序图简单说明一下:
- 在各种
encoder driver
和crtc driver
的probe
函数中:通过component_add
将自己注册进系统; - 在
Rockchip DRM Master driver
的probe
函数中;- 通过
rockchip_drm_match_add
为每个component
(各种encoder
和crtc
)注册一个component_match_array
到component_match
; - 通过
component_master_add_with_match
触发各种encoder
和crtc
component
的bind
操作,例如vop_bind
、dw_hdmi_rockchip_bind
等;
- 通过
bind
的含义就是将DRM
框架里的组件关联在一起,以vop_bind
为例:VOP driver
对应crtc driver
,crtc
负责连接plane
和encoder
;vop_create_crtc
->drm_crtc_init_with_planes
初始化crtc
对象,并和plane
关联在一起;
- 剩下的就是边边角角的工作,例如注册
framebuffer
以兼容FBDEV
,显示logo
等。
因为这些复杂的依赖关系,在DRM
系统初始化的过程中,可能会出现某个资源暂时未就绪,而导致某个模块暂时无法顺利加载的情况。
为了解决这种问题,DRM
驱动利用了Linux
驱动中的deferred probe
机制,当发现某个依赖的资源未就绪的时候,驱动返回-EPROBE_DEFER(-517)
, 然后退出。Linux kernel
会在稍后再次尝试加载这个驱动,直到依赖的资源就绪,驱动顺利加载为止。
二、设备树配置
在RK3399
上,包含两个VOP
、以及1个MIPI
、1个DP
、1个eDP
、双通道MIPI DSI
(4线/通道)显示接口;根据不用的显示屏,我们选择不同的模块来组成显示通路。具体使用那些模块,以及这些模块之间如何衔接通过dts
配置。
2.1 display_subsystem
设备节点
在每⼀个⽀持DRM
显⽰功能的SoC
的核⼼设备树⾥⾯,都会有display_subsystem
节点:所有的子设备信息都通过设备树描述关联起来,这样系统开机后,就能统一的管理各个设备。
display_subsystem
设备节点定义在arch/arm64/boot/dts/rockchip/rk3399.dtsi
;
display_subsystem: display-subsystem {
compatible = "rockchip,display-subsystem";
ports = <&vopl_out>, <&vopb_out>;
};
该节点描述的是Rockchip DRM
主设备,也就是我们在component
框架中介绍的aggregate_device
,这是一个虚拟设备,用于列出组成图形子系统的所有vop
设备或其他显示接口节点。该设备节点对应的驱动代码位于drivers/gpu/drm/rockchip/rockchip_drm_drv.c
。
其中ports
属性描述vop
硬件资源,列出了指向各个vop
设备的phandle
,vopl_out
、vopb_out
对应着VOP_LITE
、VOP_BIG
。
更多属性信息可以参考:
Documentation/devicetree/bindings/display/rockchip/rockchip-drm.yaml
;Documentation/devicetree/bindings/display/rockchip/rockchip-vop.yaml
。
2.2 vop
设备节点
vop
设备节点描述了vop
硬件资源,控制vop
驱动的加载rockchip_drm_vop.c
、 rockchip_drm_vop2.c
。
以设备节点vopb_out
为例,vopb_out
设备节点定义在arch/arm64/boot/dts/rockchip/rk3399.dtsi
;
vopb: vop@ff900000 {
compatible = "rockchip,rk3399-vop-big";
reg = <0x0 0xff900000 0x0 0x2000>, <0x0 0xff902000 0x0 0x1000>;
interrupts = <GIC_SPI 118 IRQ_TYPE_LEVEL_HIGH 0>;
assigned-clocks = <&cru ACLK_VOP0>, <&cru HCLK_VOP0>;
assigned-clock-rates = <400000000>, <100000000>;
clocks = <&cru ACLK_VOP0>, <&cru DCLK_VOP0>, <&cru HCLK_VOP0>;
clock-names = "aclk_vop", "dclk_vop", "hclk_vop";
iommus = <&vopb_mmu>;
power-domains = <&power RK3399_PD_VOPB>;
resets = <&cru SRST_A_VOP0>, <&cru SRST_H_VOP0>, <&cru SRST_D_VOP0>;
reset-names = "axi", "ahb", "dclk";
status = "disabled";
vopb_out: port {
#address-cells = <1>;
#size-cells = <0>;
vopb_out_edp: endpoint@0 {
reg = <0>;
remote-endpoint = <&edp_in_vopb>;
};
vopb_out_mipi: endpoint@1 {
reg = <1>;
remote-endpoint = <&mipi_in_vopb>;
};
vopb_out_hdmi: endpoint@2 {
reg = <2>;
remote-endpoint = <&hdmi_in_vopb>;
};
vopb_out_mipi1: endpoint@3 {
reg = <3>;
remote-endpoint = <&mipi1_in_vopb>;
};
vopb_out_dp: endpoint@4 {
reg = <4>;
remote-endpoint = <&dp_in_vopb>;
};
};
};
子节点port
下的endpoint
描述的是vop
和显示接口的连接关系,vopb_out
节点下有vopb_out_edp
,vopb_out_mipi
,vopb_out_hdmi
,vopb_out_mipi1
、vopb_out_dp
五个节点,说明vopb
可以和mipi dsi0
、edp
、hdmi
、mipi dsi1
、dp
五个显示接口连接。
每个endpoint
通过remote-endpoint
属性和对应的显示接口组成一个连接通路,例如vopb_out_hdmi
---> hdmi_in_vopb
。
设备节点vopl_out
同理,这里就不在介绍了;
2.3 内核配置
make menuconfig
配置内核:
Device Drivers --->
Graphics support --->
<*> Direct Rendering Manager (XFree86 4.1.0 and higher DRI support) --->
<*> DRM Support for Rockchip (DRM_ROCKCHIP [=y])
[*] Rockchip VOP driver
[ ] Rockchip VOP2 driver
[*] Rockchip specific extensions for Analogix DP driver (ROCKCHIP_ANALOGIX_DP [=y])
[*] Rockchip cdn DP
[*] Rockchip specific extensions for Synopsys DW HDMI
[*] Rockchip specific extensions for Synopsys DW MIPI DSI
[*] Rockchip specific extensions for Innosilicon HDMI
[*] Rockchip LVDS support
[ ] Rockchip RGB support
三、DRM
驱动入口
DRM
驱动模块入口函数为rockchip_drm_init
,位于drivers/gpu/drm/rockchip/rockchip_drm_drv.c
;
#define ADD_ROCKCHIP_SUB_DRIVER(drv, cond) { \
if (IS_ENABLED(cond) && \
!WARN_ON(num_rockchip_sub_drivers >= MAX_ROCKCHIP_SUB_DRIVERS)) \
rockchip_sub_drivers[num_rockchip_sub_drivers++] = &drv; \
}
static int __init rockchip_drm_init(void)
{
int ret;
if (drm_firmware_drivers_only())
return -ENODEV;
// 1. 根据配置来决定是否添加xxx_xxx_driver到数组rockchip_sub_drivers
num_rockchip_sub_drivers = 0;
ADD_ROCKCHIP_SUB_DRIVER(vop_platform_driver, CONFIG_ROCKCHIP_VOP);
ADD_ROCKCHIP_SUB_DRIVER(vop2_platform_driver, CONFIG_ROCKCHIP_VOP2);
ADD_ROCKCHIP_SUB_DRIVER(rockchip_lvds_driver,
CONFIG_ROCKCHIP_LVDS);
ADD_ROCKCHIP_SUB_DRIVER(rockchip_dp_driver,
CONFIG_ROCKCHIP_ANALOGIX_DP);
ADD_ROCKCHIP_SUB_DRIVER(cdn_dp_driver, CONFIG_ROCKCHIP_CDN_DP);
ADD_ROCKCHIP_SUB_DRIVER(dw_hdmi_rockchip_pltfm_driver,
CONFIG_ROCKCHIP_DW_HDMI);
ADD_ROCKCHIP_SUB_DRIVER(dw_mipi_dsi_rockchip_driver,
CONFIG_ROCKCHIP_DW_MIPI_DSI);
ADD_ROCKCHIP_SUB_DRIVER(inno_hdmi_driver, CONFIG_ROCKCHIP_INNO_HDMI);
ADD_ROCKCHIP_SUB_DRIVER(rk3066_hdmi_driver,
CONFIG_ROCKCHIP_RK3066_HDMI);
// 2. 注册多个platform driver
ret = platform_register_drivers(rockchip_sub_drivers,
num_rockchip_sub_drivers);
if (ret)
return ret;
// 3. 注册rockchip_drm_platform_driver
ret = platform_driver_register(&rockchip_drm_platform_driver);
if (ret)
goto err_unreg_drivers;
return 0;
err_unreg_drivers:
platform_unregister_drivers(rockchip_sub_drivers,
num_rockchip_sub_drivers);
return ret;
}
module_init(rockchip_drm_init);
(1) 函数内部多次调用宏ADD_ROCKCHIP_SUB_DRIVER
,完成vop
、以及显示接口(lvds
、dp
、hdmi
、mipi dsi
)的添加。
咱们以hdmi
如下代码为例;
ADD_ROCKCHIP_SUB_DRIVER(dw_hdmi_rockchip_pltfm_driver,
CONFIG_ROCKCHIP_DW_HDMI);
展开得到:
if (IS_ENABLED(CONFIG_ROCKCHIP_DW_HDMI) &&
!WARN_ON(num_rockchip_sub_drivers >= MAX_ROCKCHIP_SUB_DRIVERS))
rockchip_sub_drivers[num_rockchip_sub_drivers++] = &dw_hdmi_rockchip_pltfm_driver;
如果定义了CONFIG_ROCKCHIP_DW_HDMI
,会将dw_hdmi_rockchip_pltfm_driver
保存到rockchip_sub_drivers
数组中。
#define MAX_ROCKCHIP_SUB_DRIVERS 16
// 数组长度为16
static struct platform_driver *rockchip_sub_drivers[MAX_ROCKCHIP_SUB_DRIVERS];
那么宏CONFIG_ROCKCHIP_DW_HDMI
到底是什么呢?
root@zhengyang:/work/sambashare/rk3399/linux-6.3# grep "CONFIG_ROCKCHIP_DW_HDMI" drivers/gpu/* -nR
drivers/gpu/drm/rockchip/Makefile:13:rockchipdrm-$(CONFIG_ROCKCHIP_DW_HDMI) += dw_hdmi-rockchip.o
可以看到宏CONFIG_ROCKCHIP_DW_HDMI
决定了是否将dw_hdmi-rockchip.c
编译到内核。
有关vop
、以及显示接口(lvds
、dp
、hdmi
、mipi dsi
)的驱动咱们在后面章节单独介绍。
(2)调用platform_register_drivers
注册num_rockchip_sub_drivers
个platform driver
;该函数内部遍历rockchip_sub_drivers
数组,多次调用platform_driver_register
注册platform driver
,函数定义在drivers/base/platform.c
;
(3) 最后调用platform_driver_register
注册rockchip_drm_platform_driver
。
3.1 rockchip_drm_platform_driver
dw_hdmi_rockchip_pltfm_driver
定义在drivers/gpu/drm/rockchip/rockchip_drm_drv.c
;
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,
},
};
3.1.1 of_match_table
其中of_match_table
用于设备树匹配,匹配设备树中compatible = "rockchip,display-subsystem"
的设备节点;
static const struct of_device_id rockchip_drm_dt_ids[] = {
{ .compatible = "rockchip,display-subsystem", },
{ /* sentinel */ },
};
3.1.2 rockchip_drm_platform_probe
在plaform
总线设备驱动模型中,我们知道当内核中有platform
设备和platform
驱动匹配,会调用到platform_driver
里的成员.probe
,在这里就是rockchip_drm_platform_probe
函数;
static const struct component_master_ops rockchip_drm_ops = {
.bind = rockchip_drm_bind,
.unbind = rockchip_drm_unbind,
};
// 校验display-subsystem设备节点的属性ports是否有效
static int rockchip_drm_platform_of_probe(struct device *dev)
{
struct device_node *np = dev->of_node;
struct device_node *port;
bool found = false;
int i;
if (!np)
return -ENODEV;
for (i = 0;; i++) {
// 获取ports属性第i个元素指向的设备节点
// 例如:ports = <&vopl_out>, <&vopb_out> 保存了指向vop设备的phandle,返回指向设备节点的指针
port = of_parse_phandle(np, "ports", i);
if (!port)
break;
// port的父设备节点存在,则不会进入
if (!of_device_is_available(port->parent)) {
of_node_put(port);
continue;
}
// 设置为true
found = true;
of_node_put(port);
}
if (i == 0) {
DRM_DEV_ERROR(dev, "missing 'ports' property\n");
return -ENODEV;
}
if (!found) {
DRM_DEV_ERROR(dev,
"No available vop found for display-subsystem.\n");
return -ENODEV;
}
return 0;
}
static int rockchip_drm_platform_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct component_match *match = NULL;
int ret;
// 校验display-subsystem设备节点的属性ports是否有效
ret = rockchip_drm_platform_of_probe(dev);
if (ret)
return ret;
// 构建一个带release函数的component_match
match = rockchip_drm_match_add(dev);
if (IS_ERR(match))
return PTR_ERR(match);
// 向系统注册一个aggregate_device,触发执行rockchip_drm_ops.bind,即rockchip_drm_bind
ret = component_master_add_with_match(dev, &rockchip_drm_ops, match);
if (ret < 0) {
rockchip_drm_match_remove(dev);
return ret;
}
return 0;
}
(1) 这里代码很简单,首先使用rockchip_drm_match_add
来构建一个带release
函数的component_match
;
static struct component_match *rockchip_drm_match_add(struct device *dev)
{
struct component_match *match = NULL;
int i;
// 循环遍历
for (i = 0; i < num_rockchip_sub_drivers; i++) {
// 获取第i个platform_driver
struct platform_driver *drv = rockchip_sub_drivers[i];
struct device *p = NULL, *d;
do {
d = platform_find_device_by_driver(p, &drv->driver);
put_device(p);
p = d;
if (!d)
break;
device_link_add(dev, d, DL_FLAG_STATELESS);
// component_match注册
component_match_add(dev, &match, component_compare_dev, d);
} while (true);
}
if (IS_ERR(match))
rockchip_drm_match_remove(dev);
return match ?: ERR_PTR(-ENODEV);
}
(2) 最后为设备pdev->dev
向系统注册一个aggregate_device
,其中系统可执行的初始化操作被设置为了rockchip_drm_ops
,我们需要重点关注bind
函数的实现,即rockchip_drm_bind
,这个我们单独小节介绍。
3.2 rockchip_drm_bind
我们定位到drivers/gpu/drm/rockchip/rockchip_drm_drv.c
文件的函数rockchip_drm_bind
,该函数用于绑定设备并初始化DRM
驱动;
static int rockchip_drm_bind(struct device *dev)
{
struct drm_device *drm_dev;
struct rockchip_drm_private *private;
int ret;
/* 1. Remove existing drivers that may own the framebuffer memory. */
ret = drm_aperture_remove_framebuffers(false, &rockchip_drm_driver);
if (ret) {
DRM_DEV_ERROR(dev,
"Failed to remove existing framebuffers - %d.\n",
ret);
return ret;
}
// 2. 动态分配并初始化struct drm_device实例
drm_dev = drm_dev_alloc(&rockchip_drm_driver, dev);
if (IS_ERR(drm_dev))
return PTR_ERR(drm_dev);
// 3. 设置drm设备驱动私有数据为drm设备
dev_set_drvdata(dev, drm_dev);
// 4. 动态分配rockchip_drm_private
private = devm_kzalloc(drm_dev->dev, sizeof(*private), GFP_KERNEL);
if (!private) {
ret = -ENOMEM;
goto err_free;
}
drm_dev->dev_private = private;
// 5. 初始化drm_device中mode_config结构体
ret = drmm_mode_config_init(drm_dev);
if (ret)
goto err_free;
// 初始化drm_device中mode_config的中成员
rockchip_drm_mode_config_init(drm_dev);
/* 6. Try to bind all sub drivers. 执行按顺序执行显示子系统的各个组件的bind函数 */
ret = component_bind_all(dev, drm_dev);
if (ret)
goto err_free;
// 7. 初始化IOMMU
ret = rockchip_drm_init_iommu(drm_dev);
if (ret)
goto err_unbind_all;
// 8.初始化vblank
ret = drm_vblank_init(drm_dev, drm_dev->mode_config.num_crtc);
if (ret)
goto err_iommu_cleanup;
// 9. 重置模式配置
drm_mode_config_reset(drm_dev);
/* 10.init kms poll for handling hpd */
drm_kms_helper_poll_init(drm_dev);
// 11.注册drm设备
ret = drm_dev_register(drm_dev, 0);
if (ret)
goto err_kms_helper_poll_fini;
// 12.设置帧缓冲设备
drm_fbdev_generic_setup(drm_dev, 0);
return 0;
err_kms_helper_poll_fini:
drm_kms_helper_poll_fini(drm_dev);
err_iommu_cleanup:
rockchip_iommu_cleanup(drm_dev);
err_unbind_all:
component_unbind_all(dev, drm_dev);
err_free:
drm_dev_put(drm_dev);
return ret;
}
函数主要步骤如下:
(1) 首先,通过调用drm_aperture_remove_framebuffers
函数,移除可能拥有framebuffer memory
的现有驱动程序;
为什么要执行这个操作呢?对于一个图形设备来说,可能有多个驱动程序提供支持,但在任何给定的事件只能有一个驱动程序处于活动状态。许多系统在引导过程的早期加载通用图形驱动程序,例如EFI-GOP
或VESA
,在后续的引导阶段,它们会将通用驱动程序替换为专用的、针对具体硬件的驱动程序。为了接管该设备,专用驱动程序首先必须移除通用驱动程序。DRM aperture
函数负责管理DRM framebuffer
内存的所有权和驱动程序之间的交接;
更多内容可以参考:Managing Ownership of the Framebuffer Aperture
。
(2) 接下来,使用drm_dev_alloc
函数为设备动态分配和初始化一个struct drm_device
结构体,并将其保存在drm_dev
指针中,其driver
被设置为rockchip_drm_driver
;
drm_dev = drm_dev_alloc(&rockchip_drm_driver, dev);
(3) 然后,使用dev_set_drvdata
函数将设备的驱动私有数据设置为drm_dev
;
dev_set_drvdata(dev, drm_dev);
(4) 使用devm_kzalloc
函数为drm_dev
动态分配一个rockchip_drm_private
结构体,并将其保存在drm_dev->dev_private
指针中;
private = devm_kzalloc(drm_dev->dev, sizeof(*private), GFP_KERNEL)
(5) 调用drmm_mode_config_init
函数和rockchip_drm_mode_config_init
函数对drm_dev
进行模式配置的初始化;
(6) 调用component_bind_all
函数,执行按顺序执行显示子系统的各个组件的bind
函数;
(7) 调用rockchip_drm_init_iommu
函数初始化IOMMU
;
(8) 调用drm_vblank_init
函数初始化vblank
;
(9) 调用drm_mode_config_reset
函数重置模式配置;
(10) 调用drm_kms_helper_poll_init
函数初始化KMS
轮询以处理HPD(Hot Plug Detect)
;
(11) 在所有准备工作都完成后,调用drm_dev_register
函数注册DRM
设备;
(12) 调用drm_fbdev_generic_setup
函数设置帧缓冲设备;
3.2.1 初始化drm
设备
一个drm
驱动的设备由struct drm_device
来表示,这里调用drm_dev_alloc
进行分配和初始化一个struct drm_device
实例,其driver
被设置为rockchip_drm_driver
,有关rockchip_drm_driver
单独小节介绍。
drm_dev_alloc
函数定义在drivers/gpu/drm/drm_drv.c
;
/**
* drm_dev_alloc - Allocate new DRM device
* @driver: DRM driver to allocate device for
* @parent: Parent device object
*
* This is the deprecated version of devm_drm_dev_alloc(), which does not support
* subclassing through embedding the struct &drm_device in a driver private
* structure, and which does not support automatic cleanup through devres.
*
* RETURNS:
* Pointer to new DRM device, or ERR_PTR on failure.
*/
struct drm_device *drm_dev_alloc(const struct drm_driver *driver,
struct device *parent)
{
struct drm_device *dev;
int ret;
// 动态分配一个struct drm_device
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return ERR_PTR(-ENOMEM);
// 初始化drm_device
ret = drm_dev_init(dev, driver, parent);
if (ret) {
kfree(dev);
return ERR_PTR(ret);
}
drmm_add_final_kfree(dev, dev);
return dev;
}
drm_dev_alloc
函数时一个已废弃版本,它不支持在驱动程序的私有结构中嵌入struct drm_device
来实现子类化,并且不支持通过devres
进行自动清理。
drm_dev_alloc
函数内部调用drm_dev_init
去初始化struct drm_device
实例。
3.2.2 模式配置初始化
下面我们分析一下drmm_mode_config_init
函数和rockchip_drm_mode_config_init
函数;
// 调用drm_mode_config_init初始化drm_device中mode_config
ret = drmm_mode_config_init(drm_dev);
if (ret)
goto err_free;
// 填充mode_config中min_width, min_height、max_width, max_height的值,这些值是framebuffer的大小限制
// 设置mode_config->funcs指针,本质是一组由驱动实现的回调函数
rockchip_drm_mode_config_init(drm_dev);
rockchip_drm_mode_config_init
函数定义在drivers/gpu/drm/rockchip/rockchip_drm_fb.c
,内容如下:
void rockchip_drm_mode_config_init(struct drm_device *dev)
{
dev->mode_config.min_width = 0;
dev->mode_config.min_height = 0;
/*
* set max width and height as default value(4096x4096).
* this value would be used to check framebuffer size limitation
* at drm_mode_addfb().
*/
dev->mode_config.max_width = 4096; // 设备上支持的最大帧缓冲区像素高度
dev->mode_config.max_height = 4096; // 设备上支持的最大帧缓冲区像素高度
dev->mode_config.funcs = &rockchip_drm_mode_config_funcs;
dev->mode_config.helper_private = &rockchip_mode_config_helpers;
dev->mode_config.normalize_zpos = true;
}
(1)drm
显示器模式设置mode_config
回调函数funcs
被设置为rockchip_drm_mode_config_funcs
;
static const struct drm_mode_config_funcs rockchip_drm_mode_config_funcs = {
.fb_create = rockchip_fb_create,
.atomic_check = drm_atomic_helper_check,
.atomic_commit = drm_atomic_helper_commit,
};
fb_create
回调接口用于创建framebuffer object
,该函数我们在后面GEM
章节单独介绍。
(2) drm
显示器模式设置mode_config
中间层私有数据helper_private
被设置为了rockchip_mode_config_helpers
;
static const struct drm_mode_config_helper_funcs rockchip_mode_config_helpers = {
.atomic_commit_tail = drm_atomic_helper_commit_tail_rpm,
};
3.2.3 component_bind_all
在component_bind_all
函数我们已经在《Rockchip RK3399 - component
框架》详细介绍了;
/**
* component_bind_all - bind all components of an aggregate driver
* @parent: parent device of the aggregate driver
* @data: opaque pointer, passed to all components
*
* Binds all components of the aggregate @dev by passing @data to their
* &component_ops.bind functions. Should be called from
* &component_master_ops.bind.
*/
int component_bind_all(struct device *parent, void *data)
{
struct aggregate_device *adev;
struct component *c;
size_t i;
int ret = 0;
WARN_ON(!mutex_is_locked(&component_mutex));
// 根据parent查找到aggregate_device
adev = __aggregate_find(parent, NULL);
if (!adev)
return -EINVAL;
/* Bind components in match order */
for (i = 0; i < adev->match->num; i++)
// duplicate为false时进入
if (!adev->match->compare[i].duplicate) {
c = adev->match->compare[i].component;
// 执行component的bind函数
ret = component_bind(c, adev, data);
if (ret)
break;
}
if (ret != 0) { // 正常不会走这里
for (; i > 0; i--)
if (!adev->match->compare[i - 1].duplicate) {
c = adev->match->compare[i - 1].component;
component_unbind(c, adev, data);
}
}
return ret;
}
首先通过设备parent
找到与之关联的的aggregate_device
,再按照aggregate_device
→component_match_array
→component
的顺序找到component
,然后就能调用component
的bind
函数。
3.2.4 注册drm
设备
最后调用drm_dev_register
注册drm
设备。
四、rockchip_drm_driver
接下来我们以源码rockchip_drm_drv.c
中的rockchip_drm_driver
全局变量作为切入点进行介绍;
static const struct drm_driver rockchip_drm_driver = {
.driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC,
.dumb_create = rockchip_gem_dumb_create,
.prime_handle_to_fd = drm_gem_prime_handle_to_fd,
.prime_fd_to_handle = drm_gem_prime_fd_to_handle,
.gem_prime_import_sg_table = rockchip_gem_prime_import_sg_table,
.gem_prime_mmap = drm_gem_prime_mmap,
.fops = &rockchip_drm_driver_fops,
.name = DRIVER_NAME,
.desc = DRIVER_DESC,
.date = DRIVER_DATE,
.major = DRIVER_MAJOR,
.minor = DRIVER_MINOR,
};
4.1 driver_features
- 添加上
DRIVER_GEM
标志位,告诉DRM Core
该驱动支持GEM
操作; - 添加上
DRIVER_MODESET
标志位,告诉DRM Core
该驱动支持kernel Mode Setting
操作; - 添加上
DRIVER_ATOMIC
标志位,告诉DRM Core
该驱动支持Atomic
操作。
4.2 宏变量
#define DRIVER_NAME "rockchip"
#define DRIVER_DESC "RockChip Soc DRM"
#define DRIVER_DATE "20140818"
#define DRIVER_MAJOR 1
#define DRIVER_MINOR 0
4.3 dumb_create
其中dumb_create
配置为rockchip_gem_dumb_create
,该函数用于分配物理内存dumb buffer
,gem
相关的内容后面我们单独章节介绍。
4.4、DRM
设备节点文件操作集
DRM
驱动程序必须定义文件操作结构,形成DRM
用户空间API
的入口点,在rockchip_drm_driver
中将DRM
设备节点文件操作集fops
设置为rockchip_drm_driver_fops
,内容如下:
DEFINE_DRM_GEM_FOPS(rockchip_drm_driver_fops);
DEFINE_DRM_GEM_FOPS
宏定义在include/drm/drm_gem.h
:
/**
* DRM_GEM_FOPS - Default drm GEM file operations
*
* This macro provides a shorthand for setting the GEM file ops in the
* &file_operations structure. If all you need are the default ops, use
* DEFINE_DRM_GEM_FOPS instead.
*/
#define DRM_GEM_FOPS \
.open = drm_open,\
.release = drm_release,\
.unlocked_ioctl = drm_ioctl,\
.compat_ioctl = drm_compat_ioctl,\
.poll = drm_poll,\
.read = drm_read,\
.llseek = noop_llseek,\
.mmap = drm_gem_mmap
/**
* DEFINE_DRM_GEM_FOPS() - macro to generate file operations for GEM drivers
* @name: name for the generated structure
*
* This macro autogenerates a suitable &struct file_operations for GEM based
* drivers, which can be assigned to &drm_driver.fops. Note that this structure
* cannot be shared between drivers, because it contains a reference to the
* current module using THIS_MODULE.
*
* Note that the declaration is already marked as static - if you need a
* non-static version of this you're probably doing it wrong and will break the
* THIS_MODULE reference by accident.
*/
#define DEFINE_DRM_GEM_FOPS(name) \
static const struct file_operations name = {\
.owner = THIS_MODULE,\
DRM_GEM_FOPS,\
}
宏展开后得到:
static const struct file_operations rockchip_drm_driver_fops = {
.owner = THIS_MODULE,
.open = drm_open,
.release = drm_release,
.unlocked_ioctl = drm_ioctl,
.compat_ioctl = drm_compat_ioctl,
.poll = drm_poll,
.read = drm_read,
.llseek = noop_llseek,
.mmap = drm_gem_mmap
}
强制要求的函数是drm_open
、drm_read
、drm_ioctl
和drm_compat_ioctl
(如果启用了CONFIG_COMPAT
);
drm_read
和drm_poll
提供了对DRM
事件的支持。DRM
事件是一种通用且可扩展的方式,通过文件描述符向用户空间发送异步事件。它们用于通过KMS API
发送vblank
事件和页面翻转完成事件。但是驱动程序也可以根据自己的需求使用它,例如用于信号渲染完成。
内存映射的实现方式将根据驱动程序如何管理内存而有所不同:
- 过时驱动程序将使用
drm_legacy_mmap
函数; - 现代驱动程序应该使用提供的特定于内存管理器的实现之一;对于基于
GEM
的驱动程序,可以使用drm_gem_mmap
。
4.4.1 drm_open
在上一篇博客中,我们说过当打开一个drm
设备时,比如/dev/dri/card0
,最终回调用drm driver
的文件操作集fops
中的open
函数,即drm_open
;drm_open
函数定义在drivers/gpu/drm/drm_file.c
;
/**
* drm_open - open method for DRM file
* @inode: device inode
* @filp: file pointer.
*
* This function must be used by drivers as their &file_operations.open method.
* It looks up the correct DRM device and instantiates all the per-file
* resources for it. It also calls the &drm_driver.open driver callback.
*
* RETURNS:
*
* 0 on success or negative errno value on failure.
*/
int drm_open(struct inode *inode, struct file *filp)
{
struct drm_device *dev;
struct drm_minor *minor;
int retcode;
int need_setup = 0;
// 根据设备节点获取struct drm_minor
minor = drm_minor_acquire(iminor(inode));
if (IS_ERR(minor))
return PTR_ERR(minor);
// 获取drm device
dev = minor->dev;
if (drm_dev_needs_global_mutex(dev))
mutex_lock(&drm_global_mutex);
if (!atomic_fetch_inc(&dev->open_count))
need_setup = 1;
/* share address_space across all char-devs of a single device */
filp->f_mapping = dev->anon_inode->i_mapping;
// 调用drm open helper函数
retcode = drm_open_helper(filp, minor);
if (retcode)
goto err_undo;
if (need_setup) {
retcode = drm_legacy_setup(dev);
if (retcode) {
drm_close_helper(filp);
goto err_undo;
}
}
if (drm_dev_needs_global_mutex(dev))
mutex_unlock(&drm_global_mutex);
return 0;
err_undo:
atomic_dec(&dev->open_count);
if (drm_dev_needs_global_mutex(dev))
mutex_unlock(&drm_global_mutex);
drm_minor_release(minor);
return retcode;
}
这里核心实现为drm_open_helper
;
/*
* Called whenever a process opens a drm node
*
* \param filp file pointer.
* \param minor acquired minor-object.
* \return zero on success or a negative number on failure.
*
* Creates and initializes a drm_file structure for the file private data in \p
* filp and add it into the double linked list in \p dev.
*/
int drm_open_helper(struct file *filp, struct drm_minor *minor)
{
struct drm_device *dev = minor->dev;
struct drm_file *priv;
int ret;
if (filp->f_flags & O_EXCL)
return -EBUSY; /* No exclusive opens */
if (!drm_cpu_valid())
return -EINVAL;
if (dev->switch_power_state != DRM_SWITCH_POWER_ON &&
dev->switch_power_state != DRM_SWITCH_POWER_DYNAMIC_OFF)
return -EINVAL;
drm_dbg_core(dev, "comm=\"%s\", pid=%d, minor=%d\n",
current->comm, task_pid_nr(current), minor->index);
// 动态分配并初始化struct drm_file
priv = drm_file_alloc(minor);
if (IS_ERR(priv))
return PTR_ERR(priv);
if (drm_is_primary_client(priv)) {
ret = drm_master_open(priv);
if (ret) {
drm_file_free(priv);
return ret;
}
}
// 保存为file的私有数据
filp->private_data = priv;
filp->f_mode |= FMODE_UNSIGNED_OFFSET;
priv->filp = filp;
mutex_lock(&dev->filelist_mutex);
list_add(&priv->lhead, &dev->filelist);
mutex_unlock(&dev->filelist_mutex);
return 0;
}
4.4.2 内存映射
nmap
用于将物理内存dumb buffer
映射到用户空间,让应用程序可以直接访问物理内存。
drm_gem_mmap
函数定义在drivers/gpu/drm/drm_gem.c
,内容如下:
/**
* drm_gem_mmap - memory map routine for GEM objects
* @filp: DRM file pointer
* @vma: VMA for the area to be mapped
*
* If a driver supports GEM object mapping, mmap calls on the DRM file
* descriptor will end up here.
*
* Look up the GEM object based on the offset passed in (vma->vm_pgoff will
* contain the fake offset we created when the GTT map ioctl was called on
* the object) and map it with a call to drm_gem_mmap_obj().
*
* If the caller is not granted access to the buffer object, the mmap will fail
* with EACCES. Please see the vma manager for more information.
*/
int drm_gem_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct drm_file *priv = filp->private_data;
struct drm_device *dev = priv->minor->dev;
struct drm_gem_object *obj = NULL;
struct drm_vma_offset_node *node;
int ret;
// 检查drm设备是否已被拔出
if (drm_dev_is_unplugged(dev))
return -ENODEV;
// 锁定VMA管理器
drm_vma_offset_lock_lookup(dev->vma_offset_manager);
node = drm_vma_offset_exact_lookup_locked(dev->vma_offset_manager,
vma->vm_pgoff,
vma_pages(vma));
if (likely(node)) {
obj = container_of(node, struct drm_gem_object, vma_node);
/*
* When the object is being freed, after it hits 0-refcnt it
* proceeds to tear down the object. In the process it will
* attempt to remove the VMA offset and so acquire this
* mgr->vm_lock. Therefore if we find an object with a 0-refcnt
* that matches our range, we know it is in the process of being
* destroyed and will be freed as soon as we release the lock -
* so we have to check for the 0-refcnted object and treat it as
* invalid.
*/
if (!kref_get_unless_zero(&obj->refcount))
obj = NULL;
}
// 解锁VMA管理器
drm_vma_offset_unlock_lookup(dev->vma_offset_manager);
if (!obj)
return -EINVAL;
if (!drm_vma_node_is_allowed(node, priv)) {
drm_gem_object_put(obj);
return -EACCES;
}
// Set up the VMA to prepare mapping of the GEM object using the GEM object's vm_ops
ret = drm_gem_mmap_obj(obj, drm_vma_node_size(node) << PAGE_SHIFT,
vma);
// GEM对象引用计数-1
drm_gem_object_put(obj);
return ret;
}
参考文章
[1] DRM (Direct Rendering Manager)
[2] linux Display driver example
[3] Linux DRM
那些事-component bind
解析
[5] rockchip
中的vop
[6] RK3399
探索之旅/Display
子系统/基础概念
[7] Linux MIPI DSI
驱动开发 | 基于RK3399
[8] DRM
驱动(一)之显示处理器介绍