从点屏的角度看drm子系统
1.简介
关于drm子系统的介绍,网上一搜其实挺多的,也有确实有不少文章将drm系统介绍得不错,但当我遇到一个显示驱动问题时,想根据对drm子系统的理解去修复bug,却发现根本无从下手。仔细思考后发现其实我还是对drm一无所知,“drm是怎么怎样使屏幕亮起来”这种问题我好像也不知道,也难怪会无从下手。所以本文就是以drm子系统如何点亮屏为目标,来介绍其工作流程。
2.环境
硬件平台:瑞萨RZ/G2L
内核版本:5.10
linux内核源码仓库:https://github.com/renesas-rz/rz_linux-cip
分支:rz-5.10-cip1
点亮的屏是mipi接口的屏,并且需要发送dcs命令(相当于I2C初始化一些寄存器,该命令一遍通过mipi接口发送)
遇到的驱动问题:使用G2L来点mipi屏,所需要点亮的屏幕还需要发送dcs命令,但瑞萨的驱动里根本就没有实现发送dcs命令的功能,也没给发送dcs命令做抽象。所以这编文章也是为了讲清这个问题如果我要自己修复,我要从那里下手。(该问题最终是瑞萨解决的,整个点屏流程也是参考他们的实现方法,但瑞萨花了一个月才提供补丁,这个也是我想自己修复的原因)
3.drm结构
这图片在drm的介绍中应该经常出现,我们对drm子系统的初步印象也大概就是这个。但不知道为什么不把brigde和Panel加加进图里,这两个在点屏中也起很关键的作用。这里给出些简单的介绍
基本概念 说明 Framebuffer Framebuffer是CRTC输出的像素源的抽象内存。 Plane 图层,SoC内部VOP模块win图层的抽象。 CRTC(Cathode Ray Tube Controller) CRTC(阴极射线管控制器)即显示控制器,内部VOP模块或者VOP2中Video Port的抽象 Encoder 输出显示数据编码器,指的是RGB、LVDS、DSI、eDP、DP、HDMI、CVBS、VGA等格式编码。 Connector 连接器,指的是encoder和panel之间交互的接口部分。比如RGB、DSI、HDMI等引脚。 Bridge 桥接设备,一般用于注册Encoder后面另外再转接芯片,如DSI2HDMI转换芯片。 Panel 泛指屏幕,各种LCD显示设备的抽象。 Backlight 作为Panel背光设备。
还有一些其他概念可以参考https://www.cnblogs.com/arnoldlu/p/17978715, 该博客介绍了不少DRM概念,他还给出linux kenerl文档的链接,我这些阅读drm源码主要的参考资料就是linux kenerl文档,基本全部api和变量都有解释,在kernel里有看不懂的东西时非常值得去kernel文档查查。
4. 硬件初步挂载
4.1 硬件组成
这里特别强调下硬件和drm子系统的关系,如果没法把硬件和drm子系统联系起来,也就没办法把drm子系统弄明白。我这里的硬件主要涉及到
du:G2L的显示处理单元
dsi:G2L的mipi dsi控制单元
panel:所接的mipi屏
backlight:背光控制
画成图首先形成的概念就是我们有这4个东西,然后要靠这四个东西点亮屏
这些硬件对应的设备树可以参考下面
du: display@feb00000 {
compatible = "renesas,du-rzg2l";
reg = <0 0x10890000 0 0x10000>;
interrupts = <GIC_SPI 152 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cpg CPG_MOD R9A07G044_LCDC_CLK_A>,
<&cpg CPG_MOD R9A07G044_LCDC_CLK_P>,
<&cpg CPG_MOD R9A07G044_LCDC_CLK_D>;
clock-names = "aclk", "pclk", "dclk";
resets = <&cpg R9A07G044_LCDC_RESET_N>;
vsps = <&vspd0 0>;
power-domains = <&cpg>;
status = "disabled";
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0 {
reg = <0>;
du_out_rgb: endpoint {
};
};
port@1 {
reg = <1>;
du_out_dsi0: endpoint {
remote-endpoint = <&dsi0_in>;
};
};
};
};
dsi0: dsi@10860000 {
compatible = "renesas,r9a07g044-mipi-dsi";
reg = <0 0x10860000 0 0x10000>, /* LINK */
<0 0x10850000 0 0x10000>; /* DPHY */
clocks = <&cpg CPG_MOD R9A07G044_MIPI_DSI_PLLCLK>,
<&cpg CPG_MOD R9A07G044_MIPI_DSI_SYSCLK>,
<&cpg CPG_MOD R9A07G044_MIPI_DSI_ACLK>,
<&cpg CPG_MOD R9A07G044_MIPI_DSI_PCLK>,
<&cpg CPG_MOD R9A07G044_MIPI_DSI_VCLK>,
<&cpg CPG_MOD R9A07G044_MIPI_DSI_LPCLK>;
clock-names = "pllclk", "sysclk", "aclk",
"pclk", "vclk", "lpclk";
resets = <&cpg R9A07G044_MIPI_DSI_CMN_RSTB>,
<&cpg R9A07G044_MIPI_DSI_ARESET_N>,
<&cpg R9A07G044_MIPI_DSI_PRESET_N>;
reset-names = "cmn_rstb", "areset_n", "preset_n";
power-domains = <&cpg>;
status = "disabled";
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0 {
reg = <0>;
dsi0_in: endpoint {
remote-endpoint = <&du_out_dsi0>;
};
};
port@1 {
reg = <1>;
dsi0_out: endpoint {
};
};
};
};
&du {
status = "okay";
};
&dsi0 {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
ports {
port@1 {
dsi0_out: endpoint {
remote-endpoint = <&panel_in>;
data-lanes = <1 2 3 4>;
};
};
};
panel@0 {
compatible = "boe,himax8279d10p";
reg = <0>;
dsi-lanes = <4>;
reset-gpios = <&pinctrl RZG2L_GPIO(10, 0) GPIO_ACTIVE_HIGH>; //TFT_RST
enable-gpios = <&pinctrl RZG2L_GPIO(44, 2) GPIO_ACTIVE_HIGH>; //TFT_EN
backlight = <&backlight>;
status = "okay";
port {
panel_in: endpoint {
remote-endpoint = <&dsi0_out>;
};
};
};
};
backlight: backlight{
compatible = "pwm-backlight";
status = "okay";
pwms = <&gpt6 0 1000000>;
power-supply = <®_3p3v>;
enable-gpios = <&pinctrl RZG2L_GPIO(44, 3) GPIO_ACTIVE_HIGH>; //BL_EN // P44_3, backlight_PWREN
// brightness-levels = <0 2 8 16 32 64 128 255>;
brightness-levels = < 0 1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18 19
20 21 32 23 24 25 26 27 28 29
30 31 42 33 34 35 36 37 38 39
40 41 52 43 44 45 46 47 48 49
50 51 52 53 54 55 56 57 58 59
60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79
80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99
100>;
default-brightness-level = <100>;
};
通过阅读设备树可以看到du,dsi,panel和backlight这几个设备,再观察port的连接情况,还可以看到这些设备的连接逻辑,以及panel里还去获取了backlight
du<--->dsi<--->panel
panel需要补充说明的一个点,由于使用的panel驱动功能不是很完成,时序,dcs命令实际是写在panel源码里,具体的内容参考下方,如果驱动支持从设备树获取数据,实际上panel设备树的内容也是会被放入这些结构体中
static const struct drm_display_mode default_display_mode = {
.clock = 159420,
.hdisplay = 1200,
.hsync_start = 1200 + 80,
.hsync_end = 1200 + 80 + 60,
.htotal = 1200 + 80 + 60 + 24,
.vdisplay = 1920,
.vsync_start = 1920 + 10,
.vsync_end = 1920 + 10 + 14,
.vtotal = 1920 + 10 + 14 + 4,
};
static const struct panel_cmd boe_himax8279d8p_on_cmds[] = {
{ 0xB0, 0x05 },
{ 0xB1, 0xE5 },
{ 0xB3, 0x52 },
{ 0xC0, 0x00 },
{ 0xC2, 0x57 },
{ 0xD9, 0x85 },
{ 0xB0, 0x01 },
{ 0xC8, 0x00 },
{ 0xC9, 0x00 },
{ 0xCC, 0x26 },
{ 0xCD, 0x26 },
{ 0xDC, 0x00 },
.....
}
4.2 backlight初始化
源码在 drivers/video/backlight/pwm_bl.c , 背光初始化其实很简单,流程如下
pwm_backlight_probe
->pwm_backlight_parse_dt //解释设备树
->devm_pwm_get //获取pwm
->pwm_init_state //初始化pwm
->pwm_apply_state //设置pwm状态,这里基本就等于打开背光了
->backlight_device_register //注册背光设备
->backlight_update_status //根据设置的默认亮度,关系背光状态
其实没什么好将的,单单看函数名就知道在做什么了。此外可以了解下下面这个结构体,suspend 和resume 会被drm调用用来休眠和唤醒
static const struct dev_pm_ops pwm_backlight_pm_ops = {
#ifdef CONFIG_PM_SLEEP
.suspend = pwm_backlight_suspend,
.resume = pwm_backlight_resume,
.poweroff = pwm_backlight_suspend,
.restore = pwm_backlight_resume,
#endif
};
4.3 dsi初始化
dis初始化的源码在 drivers/gpu/drm/rcar-du/rzg2l_mipi_dsi.c , 流程参考下方
rzg2l_mipi_dsi_probe
->mipi_dsi_host_register //注册mipi_dsi_host 设备
->drm_bridge_add //添加drm_bridge
主要就是做流程里的两件事
mipi_dsi_host_register,展开点讲,注册时还会去找mipi_dsi_device ,而mipi_dsi_device 就在panel里生成的。此外mipi_dsi_host_register还注册了struct mipi_dsi_host_ops,里面的attach和transfer都会在点屏时用上,这个后面在讲
static const struct mipi_dsi_host_ops rzg2l_mipi_dsi_host_ops = {
.attach = rzg2l_mipi_dsi_host_attach,
.detach = rzg2l_mipi_dsi_host_detach,
.transfer = rzg2l_mipi_dsi_host_transfer,
};
drm_bridge_add ,也有一套函数(struct drm_bridge_funcs),其中attach,mode_set,enable,点屏时会用上,也是后面用到了再讲
static const struct drm_bridge_funcs rzg2l_mipi_dsi_bridge_ops = {
.attach = rzg2l_mipi_dsi_attach,
.detach = rzg2l_mipi_dsi_detach,
.mode_set = rzg2l_mipi_dsi_mode_set,
.enable = rzg2l_mipi_dsi_enable,
.disable = rzg2l_mipi_dsi_disable,
.mode_valid = rzg2l_mipi_dsi_bridge_mode_valid,
};
这样注册下来后,对dsi的认知就变成了下图
4.4 panel初始化
这里用的是 drivers/gpu/drm/panel/panel-boe-himax8279d.c , 流程参考下方
panel_probe(struct mipi_dsi_device *dsi)
->of_device_get_match_data //从设备树获取数据
->mipi_dsi_set_drvdata //将数据设置到 mipi_dsi_device 里
->panel_add
->drm_panel_init(&pinfo->base, dev, &panel_funcs, DRM_MODE_CONNECTOR_DSI); //初始化drm_panel
->drm_panel_of_backlight //drm_plane 获取背光
->drm_panel_add //将drm_panel 加入到drm子系统中(这里具体操作是把drm_panel加入到一个链表中)
->mipi_dsi_attach //将mipi_dsi_device加入到mipi_dsi_host中
drm_panel_init:要强调下起注册的panel_funcs,后面会用上
static const struct drm_panel_funcs panel_funcs = {
.disable = boe_panel_disable,
.unprepare = boe_panel_unprepare,
.prepare = boe_panel_prepare,
.enable = boe_panel_enable,
.get_modes = boe_panel_get_modes,
};
在dsi注册mipi_dsi_host时便会生成mipi_dsi_device,这是便会挂载panel,并初始化drm_panel, 并最后调用mipi_dsi_attach。
mipi_dsi_attach会回调,mipi_dsi_host的attach函数, 该函数会将部分mipi_dsi_device的数据传给mipi_dsi_host
到这后,我们对这部分的认知变成了下图
4.5 du初始化
du初始化涉及很多文件,这里主要提供入口文件位置 drivers/gpu/drm/rcar-du/rcar_du_drv.c , 此外我这里主要考虑硬件流程,会简化涉及应用层的部分(涉及应用层部分,我还没仔细看,而且简化并不影响理解点屏流程),初始化流程如下:
rcar_du_probe
->rcar_du_modeset_init //kms初始化
->drmm_mode_config_init //初始化mode_config
->drm_property_create_range //drm_property生成
->drm_vblank_init //初始化垂直中断
->rcar_du_planes_init //初始化drm_plane,这里不展开说明
->rcar_du_vsps_init //初始化合成器,这里不展开
->rcar_du_cmm_init //初始化颜色管理器,这里不展开
->rcar_du_crtc_create //初始化crtc,这里不展开
->rcar_du_encoders_init //初始化 drm_encoder ,部分芯片有多个encoder
->drm_simple_encoder_init //初始化 drm_encoder
->drm_encoder_helper_add //drm_encoder 添加辅助api
->drm_bridge_attach(encoder, bridge, NULL, 0) //将encoder和bridge连接起来
->drm_mode_config_reset //重置所有硬件软件状态
->drm_kms_helper_poll_init //初始化热插拔
->drm_dev_register //注册drm设备
->drm_fbdev_generic_setup //注册fbdev
这里再张开说明下drm_bridge_attach,
1.在这个函数调用前,会通过设备树的port来确认encode后面接的bridge是哪个。
2.此外drm_bridge_attach函数还会回调drm_bridge注册时添加的drm_bridge_funcs.attach,里面还会再添加connector
drm_bridge_attach
->drm_bridge_funcs.attach
->rzg2l_mipi_dsi_find_panel_or_bridge //确认桥后面接的是panel还是bridge
->drm_connector_init //初始化connector
->drm_connector_helper_add //添加connector的辅助函数
->drm_connector_attach_encoder //connector绑定encoder
rzg2l_mipi_dsi_find_panel_or_bridge :确认桥后面接的是panel还是bridge是通过设备树的port确认的
drm_connector_helper_add,drm_connector_helper_add:会添加一套辅助函数,后面会用上
static const struct
drm_connector_helper_funcs rzg2l_mipi_dsi_conn_helper_funcs = {
.get_modes = rzg2l_mipi_dsi_connector_get_modes,
.atomic_check = rzg2l_mipi_dsi_connector_atomic_check,
};
static const struct drm_connector_funcs rzg2l_mipi_dsi_conn_funcs = {
.reset = drm_atomic_helper_connector_reset,
.fill_modes = drm_helper_probe_single_connector_modes,
.destroy = drm_connector_cleanup,
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
};
drm_connector_attach_encoder: bridge对于drm的线路来说应该是透明的,所以connector才会绑上encoder
4.6 挂载完
上面初始化走完,便会得到下图结构
到这为止我们好像大致了解了整个结构的样子,但仔细想想
1.panel的时序是什么时候被设置进硬件里,这个问题好像不是知道
2.我要修复的dcs发送问题,dcs是什么时候发送,好像也不知道
3.屏是什么时候完全被点亮的,好像就只知道背光是什么时候亮的,液晶什么时候亮的还是不知道
后面便是讲解这部分
5.热插拔
5.1 热插拔
在du初始化流程中,有写到一个函数 drm_kms_helper_poll_init ,其作用是启动热插拔检查,会循环运行output_poll_execute函数,该函数在drivers/gpu/drm/drm_probe_helper.c
output_poll_execute
->drm_connector_list_iter_begin //开始遍历drm子系统链表里的connector,并进行热插拔检查
->drm_helper_probe_detect
->connector->helper_private->detect_ctx(connector, ctx, force) //由于我这里是mipi直连实际没有热插拔功能,所以并没有该检查
->connector->funcs->detect(connector, force) //由于我这里是mipi直连实际没有热插拔功能,所以并没有该检查
->drm_connector_list_iter_end //结束遍历drm子系统链表里的connector
->drm_kms_helper_hotplug_event
->drm_sysfs_hotplug_event //发出uevent通知用户空间
->mode_config.funcs->output_poll_changed(dev); //我这里没有实现
->drm_client_dev_hotplug
->drm_client_dev->funcs->hotplug(client); //触发drm_client_dev的hotplug函数
先直接说结论,最终会进入了drm_client_dev->funcs->hotplug(client);。但drm_client_dev是哪里来的,这个问题很容易就冒出来,好像前面初始化一轮下来根本就没见过。这里再回头看du初始化流程,有个drm_fbdev_generic_setup,但由于看到了fbdev很容易就让人觉得它和硬件关系不大导致被忽略掉,这里展开drm_fbdev_generic_setup
drm_fbdev_generic_setup
->drm_client_init(dev, &fb_helper->client, "fbdev", &drm_fbdev_client_funcs)
->drm_fbdev_client_hotplug //执行热插拔
->drm_client_register(&fb_helper->client) //注册drm_client_dev
drm_client_init: 这个函数会初始化结构体 “struct drm_fb_helper *fb_helper;”,该结构里就有drm_client_dev
drm_fbdev_client_funcs:具体实现如下
static const struct drm_client_funcs drm_fbdev_client_funcs = {
.owner = THIS_MODULE,
.unregister = drm_fbdev_client_unregister,
.restore = drm_fbdev_client_restore,
.hotplug = drm_fbdev_client_hotplug,
};
该结构体里的hotplug就是output_poll_execute调用的drm_client_dev->funcs->hotplug(client)
总的来看,无论是output_poll_execute调用hotplug,drm_fbdev_generic_setup里直接使用drm_fbdev_client_hotplug,在点屏流程中都是要进入drm_fbdev_client_hotplug。此外这里还要区分第一次进入drm_fbdev_client_hotplug和第二次及其之后进入drm_fbdev_client_hotplug
第一次进入drm_fbdev_client_hotplug,其主要目的是生成framebuffer,一般都是在drm_fbdev_generic_setup里完成第一次
drm_fbdev_client_hotplug
->drm_fb_helper_prepare
->drm_fb_helper_init
->drm_fb_helper_initial_config
->__drm_fb_helper_initial_config_and_unlock
->drm_client_modeset_probe
->drm_fb_helper_single_fb_probe
->drm_setup_crtcs_fb
->register_framebuffer //注册framebuffer
第二次进入drm_fbdev_client_hotplug,主要目的是相应热插拔事件,并提交相关配置
drm_fbdev_client_hotplug
->drm_fb_helper_hotplug_event
->drm_client_modeset_probe
->connector->funcs->fill_modes //获取全部 drm_display_mode
-> modeset->mode = drm_mode_duplicate(dev, mode); //将drm_display_mode 复制到 drm_mode_set里
->drm_setup_crtcs_fb
->drm_fb_helper_set_par
->__drm_fb_helper_restore_fbdev_mode_unlocked
->drm_client_modeset_commit_locked
-> drm_client_modeset_commit_atomic(client, true, false);
-> __drm_atomic_helper_set_config(mode_set, state) //将drm_mode_set转成drm_atomic_state
-> drm_atomic_commit
->drm_mode_config->funcs->atomic_commit(state) //提交相关设置
前面通过connector->funcs->fill_modes获取到全面modes,在通过一系列转换后,又开始进入到另外一个结构体的funcs,drm_mode_config这个结构体其实在前面也见到过,也是在du的初始化里,有个drmm_mode_config_init ,就是用来初始化 drm_mode_config
后面还会再详细展开connector->funcs->fill_modes,这个函数简单来讲就是获取全部显示模式,那我们的屏幕数据理论上讲就是通过这里传入的
5.2 atomic_commit
现在回头看下初始化drm_mode_config时注册的funcs,有以下两个结构体
static const struct drm_mode_config_helper_funcs rcar_du_mode_config_helper = {
.atomic_commit_tail = rcar_du_atomic_commit_tail,
};
static const struct drm_mode_config_funcs rcar_du_mode_config_funcs = {
.fb_create = rcar_du_fb_create,
.atomic_check = rcar_du_atomic_check,
.atomic_commit = drm_atomic_helper_commit,
};
drm_atomic_helper_commit(struct drm_device *dev, struct drm_atomic_state *state, bool nonblock)的功能是提交drm_atomic_state(drm_atomic_state里会存放crtc,plane,connector的状态)。接下来展开drm_atomic_helper_commit
drm_atomic_helper_commit
->drm_atomic_helper_setup_commit //往crtc,plane,connector提交状态
->drm_atomic_helper_prepare_planes //调用plane的prepare_fb函数
->drm_atomic_helper_swap_state //对state处理,前面commit了新的state,这里需要在state内部处理state
->drm_atomic_state_get //获取state,前面处理了以下,所以再次get
->commit_tail
-> drm_atomic_helper_wait_for_fences //等待fences
-> drm_atomic_helper_wait_for_dependencies //等待之前的commit完成
-> dev->mode_config.helper_private->funcs->atomic_commit_tail //将state提交到硬件
->drm_atomic_helper_commit_cleanup_done //释放commit
->drm_atomic_state_put //释放state
这里重点看atomic_commit_tail,它要将新的state提交到硬件上。atomic_commit_tail的实现在前面有写出,为rcar_du_atomic_commit_tail,所以在这里展开rcar_du_atomic_commit_tail
rcar_du_atomic_commit_tail
->drm_atomic_helper_commit_modeset_disables //modeset 提交时,关闭输出。这里面会回调很多关闭相关的函数
->disable_outputs //关闭输出
->crtc_set_mode //crtc设置模式
-> encoder->helper_private->atomic_mode_set //我这未实现
->drm_bridge_chain_mode_set //调用bridge的mode_set
->drm_atomic_helper_commit_planes //plane提交新状态
->drm_atomic_helper_commit_modeset_enables //modeset 提交时,使能输出。
-> crtc->helper_private->atomic_enable //回调crtc的atomic_enable
-> drm_bridge_chain_get_first_bridge //获取第一个bridge,我这只有一个bridge,所以后面的操作也只有一个bridge在执行
-> drm_atomic_bridge_chain_pre_enable //使全部桥执行pre_enable
-> encoder->helper_private->atomic_enable //执行encoder的atomic_enable
-> drm_atomic_bridge_chain_enable //使全部桥执行enable
->drm_atomic_helper_commit_hw_done //设置硬件提交完成
->drm_atomic_helper_wait_for_flip_done //等待flip完成
->drm_atomic_helper_cleanup_planes //完成commit后,清理plane
流程都进行了简单说明了,这里的大部分函数都涉及大量的回调,这里之后只考虑和点屏相关性较高的进行展开,主要是以及几个
drm_bridge_chain_mode_set :将mode设置到bridge,桥就在dsi硬件中,mode里存放了要怎么显示的信息
drm_atomic_bridge_chain_enable:与上面一样,主要因素是bridge在dsi硬件,bridge使能会带动dsi硬件使能
5.3小结
再把热插拔考虑进来后整体的结构图变成了
在把数据流向考虑进来
到这里我们其实以及了解到了不少情况,剩下的疑问可以细化成以下问题
1.drm_connector是从哪里获取到drm_display_mode的,现在即没直接和drm_panel有联系,也没间接的联系
2.bridge获取到的state后面是在哪里传化回显示器的时序
3.bridge是如何使能的,是怎样把屏点亮的
对于这几个问题接下来会解析
1.connector->funcs->fill_modes
2.drm_bridge_chain_mode_set
3.drm_atomic_bridge_chain_enable
6. 点屏
6.1 获取屏幕时序
获取屏幕时序的功能在connector->funcs->fill_modes,通过初始化注册的func可知,其具体实现为drm_helper_probe_single_connector_modes,接下来分析该函数
drm_helper_probe_single_connector_modes
->(*connector_funcs->get_modes)(connector)
->rzg2l_mipi_dsi_connector_get_modes
->drm_panel_get_modes
->panel->funcs->get_modes
->boe_panel_get_modes //具体的drm_panel获取drm_display_mode
->drm_connector_list_update //更新drm_connector的drm_display_mode列表
基本看流程就知道做了什么事,这里再加附上boe_panel_get_modes的源码
static int boe_panel_get_modes(struct drm_panel *panel,
struct drm_connector *connector)
{
struct panel_info *pinfo = to_panel_info(panel);
const struct drm_display_mode *m = pinfo->desc->display_mode;
struct drm_display_mode *mode;
mode = drm_mode_duplicate(connector->dev, m);
if (!mode) {
dev_err(pinfo->base.dev, "failed to add mode %ux%u@%u\n",
m->hdisplay, m->vdisplay, drm_mode_vrefresh(m));
return -ENOMEM;
}
drm_mode_set_name(mode);
drm_mode_probed_add(connector, mode);
connector->display_info.width_mm = pinfo->desc->width_mm;
connector->display_info.height_mm = pinfo->desc->height_mm;
connector->display_info.bpc = pinfo->desc->bpc;
return 1;
}
drm_panel获取drm_display_mode (时序)的位置其实就是最开始初始化panel时,写时序的地方,后续这个drm_display_mode 便通过drm_mode_probed_add被存入connector里,然后就有了热插拔里的数据传输流程。
另外这里解释下结构图里的深蓝色箭头,connector需要获取到drm_panel才执行得了panel->funcs->get_modes。在这之前drm_panel有将存入到dsi中,这里connector才获取得到drm_panel
这步流程后结构图更新为
6.2 设置屏幕时序
设置屏幕时序这步看drm_bridge_chain_mode_set
drm_bridge_chain_mode_set
-> bridge->funcs->mode_set(bridge, mode, adjusted_mode);
->rzg2l_mipi_dsi_mode_set
rzg2l_mipi_dsi_mode_set就是bridge->funcs->mode_set,这个可以去看dsi初始化,附上rzg2l_mipi_dsi_mode_set源码
static void rzg2l_mipi_dsi_mode_set(struct drm_bridge *bridge,
const struct drm_display_mode *mode,
const struct drm_display_mode *adjusted_mode)
{
struct rzg2l_mipi_dsi *mipi_dsi = bridge_to_rzg2l_mipi_dsi(bridge);
mipi_dsi->display_mode = *adjusted_mode;
}
这一步只是简单的赋值,但时序本来就是要传给dsi硬件的,现在数据就已经就位了
6.3点亮屏幕
点亮屏幕这步看drm_atomic_bridge_chain_enable
drm_atomic_bridge_chain_enable
-> bridge->funcs->atomic_enable(bridge)
->rzg2l_mipi_dsi_enable
->rzg2l_mipi_dsi_startup
->rzg2l_mipi_dsi_set_display_timing
->drm_panel_prepare
->panel->funcs->prepare(panel)
->boe_panel_prepare
->drm_panel_enable
-> panel->funcs->enable(panel)
-> boe_panel_enable
->rzg2l_mipi_dsi_start_hs_clock
->rzg2l_mipi_dsi_start_video
这里面rzg2l_mipi_dsi开头的函数,具体的实现基本都涉及寄存器的具体操作,这里就不详细展开了
rzg2l_mipi_dsi_set_display_timing:这个函数里从dsi里获取了刚刚设置好的时序,并将时序设置进具体的寄存器里
rzg2l_mipi_dsi_start_video:这个函数会操作具体的寄存器启动视频传输
6.4 dcs发送
再这之外来解析下boe_panel_prepare和boe_panel_enable
static int boe_panel_prepare(struct drm_panel *panel)
{
struct panel_info *pinfo = to_panel_info(panel);
int err;
if (pinfo->prepared)
return 0;
//gpiod_set_value(pinfo->pp18_gpio, 1);
/* T1: 5ms - 6ms */
//usleep_range(5000, 6000);
//gpiod_set_value(pinfo->pp33_gpio, 1);
/* reset sequence */
/* T2: 14ms - 15ms */
usleep_range(14000, 15000);
gpiod_set_value(pinfo->enable_gpio, 1);
/* T3: 1ms - 2ms */
usleep_range(1000, 2000);
gpiod_set_value(pinfo->enable_gpio, 0);
/* T4: 1ms - 2ms */
usleep_range(1000, 2000);
gpiod_set_value(pinfo->enable_gpio, 1);
/* T5: 5ms - 6ms */
usleep_range(5000, 6000);
/* send init code */
err = send_mipi_cmds(panel, pinfo->desc->on_cmds);
if (err < 0) {
dev_err(panel->dev, "failed to send DCS Init Code: %d\n", err);
goto poweroff;
}
err = mipi_dsi_dcs_exit_sleep_mode(pinfo->link);
if (err < 0) {
dev_err(panel->dev, "failed to exit sleep mode: %d\n", err);
goto poweroff;
}
/* T6: 120ms - 121ms */
usleep_range(120000, 121000);
err = mipi_dsi_dcs_set_display_on(pinfo->link);
if (err < 0) {
dev_err(panel->dev, "failed to set display on: %d\n", err);
goto poweroff;
}
/* T7: 20ms - 21ms */
usleep_range(20000, 21000);
pinfo->prepared = true;
return 0;
poweroff:
disable_gpios(pinfo);
return err;
}
static int boe_panel_enable(struct drm_panel *panel)
{
struct panel_info *pinfo = to_panel_info(panel);
int ret;
if (pinfo->enabled)
return 0;
usleep_range(120000, 121000);
ret = mipi_dsi_dcs_set_display_on(pinfo->link);
if (ret < 0) {
dev_err(panel->dev, "failed to set display on: %d\n", ret);
return ret;
}
pinfo->enabled = true;
return 0;
}
boe_panel_prepare里可以看到些gpio进行时序操作,还能看到用mipi接口发送命令(send_mipi_cmds),这个就是dcs命令发送,且设置相关的数据发送完后都会使用mipi_dsi_dcs_set_display_on函数,通过dcs发送显示
再往内部分析,都是使用mipi_dsi_dcs_write_buffer来发送dcs命令
mipi_dsi_dcs_write_buffer
-> mipi_dsi_device_transfer
->dsi->host->ops->transfer(dsi->host, msg)
->rzg2l_mipi_dsi_host_transfer
这里可以得知其通过mipi_dsi_host里的transfer函数进行数据传输。rzg2l_mipi_dsi_host_transfer里已经涉及具体的寄存器操作了,这里就不展开了。
最后在给结构图添加下发送dcs命令的调用情况
7. 结语
至此,在drm子系统中,屏幕是怎么亮起来的这个点已经描述完,而我所遇到的dcs发送问题也基本找到了处理位置,虽然后面的操作硬件发送dcs命令依旧是个难点,但至少drm子系统不再是干扰。不过整篇文章虽然写了不少东西,但对于drm子系统来说仍是冰山一角,如果想再进一步了解,还是需要更加详细的阅读drm源码。我这篇文章就到这了,应该足够让人初步认识到drm运作流程了。