从点屏的角度看drm子系统

从点屏的角度看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加加进图里,这两个在点屏中也起很关键的作用。这里给出些简单的介绍

基本概念说明
FramebufferFramebuffer是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 = <&reg_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运作流程了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值