MTK平台原生EDP调试浅析

目录

一、引子

二、开发环境

三、原理浅析

1、DTS

2、Driver

(1)、mtk_dvo.c

(2)、mtk_drm_edp.c

(3)、panel-edp.c

四、总结


一、引子

继前面MTK GPIO调试的内容,现在来简单分析一下在MTK平台上EDP的调试。在一个新生项目的调试历程中,点亮EDP是代表着主板初步点亮的重要标志,通常也是项目开发的首要目标之一。我们以原生EDP为例,来看看MTK平台上EDP的调试流程。

二、开发环境

OS:Android14

Platform:Genio 720(MT8391)

Linux Version:6.1

SDK Version: 略

三、原理浅析

调试过程中涉及到的源文件包括DTS以及驱动源文件如下所示:

kernel/kernel_device_modules-6.1/arch/arm64/boot/dts/mediatek/aiot8391p2_64_bsp.dts

kernel/kernel_device_modules-6.1/drivers/gpu/drm/mediatek/mediatek_v2/mtk_drm_edp/panel-edp.c

kernel/kernel_device_modules-6.1/drivers/gpu/drm/mediatek/mediatek_v2/mtk_drm_edp/mtk_drm_edp.c

kernel/kernel_device_modules-6.1/drivers/gpu/drm/mediatek/mediatek_v2/mtk_drm_edp/mtk_dvo.c

1、DTS

我们来看一下节点上的配置:

&disp_dvo0 {
	status = "enable";
	ports {
		port {
			disp_dvo0_out: endpoint {
				remote-endpoint = <&edptx_in>;
			};
		};
	};
};

&edp_tx {
	pinctrl-names = "default";
	pinctrl-0 = <&edp_gpio_def_cfg>;
	status = "enable";
	max-lane-count = <4>;
	max-linkrate-mhz = <5400>;
	use-hpd = <1>;
	use-edid = <0>;
	external-monitor = <0>;

	ports {
		#address-cells = <1>;
		#size-cells = <0>;
		port@0 {
			reg = <0>;
			edptx_in: endpoint {
				remote-endpoint = <&disp_dvo0_out>;
			};
		};
		port@1 {
			reg = <1>;
			edptx_out: endpoint {
				remote-endpoint = <&panel_edp_in>;
			};
		};
	};
};

&panel_edp {
	status = "enable";
	enable-gpios = <&pio 108 0>;
	power3v3-gpios = <&pio 109 0>;
	power1v8-gpios = <&pio 110 0>;
	width-mm = <344>;
	height-mm = <194>;
	panel-timing {
		clock-frequency = <617760000>;
		hactive = <3840>;
		vactive = <2160>;
		hsync-len = <88>;
		hfront-porch = <176>;
		hback-porch = <472>;
		vfront-porch = <8>;
		vback-porch = <72>;
		vsync-len = <10>;
	};

	port {
		panel_edp_in: endpoint {
			remote-endpoint = <&edptx_out>;
		};
	};
};

从全局上看,这三个节点通过它们的 endpoint 和 remote-endpoint 属性紧密耦合,形成了一个完整的数据链,其数据流向如下所示:

SOC GPU -> disp_dvo0(OUT) -> edp_tx (IN) -> edp_tx(OUT) -> panel_edp(IN) -> EDP面板

1、disp_dvo0_out 链接到 edptx_in

SoC内部的GPU或显示处理单元DPU将其生成的图像数据流通过 disp_dvo0 接口输出,输出信号被发送到 edp_tx 模块的输入端口。

2、edptx_out 链接到 panel_edp_in

edp_tx 模块接收到原始图像数据后,会将其打包成符合eDP协议标准的差分信号。处理后的eDP信号通过其输出端口发送出去。最终,这些信号被传输到物理面板 pane l_edp 的输入端口。

&disp_dvo0是显示控制接口,这个节点代表SOC内部的显示控制器的一个输出接口;

&edp_tx是EDP发射器(TX)控制器,主要负责EDP协议和信号生成的硬件模块,例如很明显在节点中可以看到包括通道Lanes、最大通道链路速率、HPD以及EDID等诸如此类配置。而其中的ports定义有两个端口,这是因为此节点相当于一个数据中转站。

&panel_edp是物理显示面板,即用于配置显示器面板的重要参数,包括控制面板的电源和复位、分辨率和时序等这类关键参数,常规的EDP适配,均在此处做规格参数的更改确认。

2、Driver

(1)、mtk_dvo.c

这是显示输出控制器的驱动,实际上DVO就是SOC内部的一个硬件模块,作用是将处理好的图像数据转换成标准的视频时序信号。DVO的初始化、配置、时钟管理和中断处理等核心功能,在这里完成。这里不过多展开论述。

(2)、mtk_drm_edp.c

这个就是EDP TX发射器的驱动,控制集成在MTK SOC中的硬件模块,以驱动一块EDP接口的显示屏。在驱动里,其核心结构体如下所示

struct mtk_edp {
    bool enabled;           // 驱动使能状态
    bool need_debounce;     // 是否需要防抖处理(用于HPD的)
    bool powered;           // 电源状态
    int irq;                // 中断号
    u8 max_lanes;           // SoC支持的最大通道数 (1, 2, 4)
    u8 max_linkrate;        // SoC支持的最大链路速率 (转换为DP标准代码,如0x06, 0x0A, 0x14)
    struct clk *power_clk;  // 时钟资源
    u8 rx_cap[DP_RECEIVER_CAP_SIZE]; // 存储从显示器读取的DPCD能力信息
    u32 cal_data[MTK_DP_CAL_MAX];    // 存储PHY的校准数据(阻抗调整等)
    ...
    struct device *dev;     // 关联的Linux设备结构
    struct drm_bridge bridge; // 最重要的成员,将其注册到DRM框架,成为显示流水线的一环
    struct drm_dp_aux aux;  // 实现AUX通道通信,用于读写DPCD、EDID
    ...
    struct mtk_dp_train_info train_info; // 链路训练相关信息
};

这个是驱动的核心,保存了所有必要的状态信息和硬件资源。

驱动中与DTS的交互,通过特定的字符串来读取配置,与前面的DTS中示例的内容刚好呼应上:

#define MTK_EDP_MODE_EXTERNAL_MONITOR	"external-monitor"
#define MTK_EDP_MODE_USE_EDID			"use-edid"
#define MTK_EDP_MODE_USE_HPD			"use-hpd"
#define MTK_EDP_MAX_LANE_COUNT			"max-lane-count"
#define MTK_EDP_MAX_LINK_RATE			"max-linkrate-mhz"

驱动中通过mtk_edp_dt_parse函数,读取这些属性并做对应的配置。

static int mtk_edp_dt_parse(struct mtk_edp *mtk_edp,
			   struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	int ret;
	void __iomem *base;
	void __iomem *phy_base;
	u32 linkrate;
	u32 lane_count;
	u32 read_value;

	/* eDP MAC ioremap resource */
	base = devm_platform_ioremap_resource(pdev, 0);
	if (IS_ERR(base))
		return PTR_ERR(base);

	mtk_edp->regs = devm_regmap_init_mmio(dev, base, &mtk_edp_regmap_config);
	if (IS_ERR(mtk_edp->regs))
		return PTR_ERR(mtk_edp->regs);

	/* eDP PHY ioremap resource */
	mtk_edp->phy_regs = NULL;
	phy_base = devm_platform_ioremap_resource(pdev, 1);
	if (!IS_ERR(phy_base)) {
		mtk_edp->phy_regs = devm_regmap_init_mmio(dev, phy_base, &mtk_edp_phy_regmap_config);
		if (IS_ERR(mtk_edp->phy_regs))
			mtk_edp->phy_regs = NULL;
	}

	ret = device_property_read_u32(dev, MTK_EDP_MAX_LANE_COUNT, &lane_count);

	if (lane_count == 0 || lane_count == 3 || lane_count > 4) {
		dev_info(dev, "%s Invalid data lane size: %d\n", EDPTX_DEBUG_INFO, lane_count);
		return -EINVAL;
	}

	mtk_edp->max_lanes = lane_count;

	/* example: max-linkrate-mhz = 2700 == 2.7Gbps*/
	ret = device_property_read_u32(dev, MTK_EDP_MAX_LINK_RATE, &linkrate);
	if (ret) {
		dev_info(dev, "%s Failed to read max linkrate: %d\n", EDPTX_DEBUG_INFO, ret);
		return ret;
	}

	mtk_edp->max_linkrate = drm_dp_link_rate_to_bw_code(linkrate * 100);

	dev_info(dev, "%s SoC support max_lanes:%d, max_linkrate:0x%x\n",
			EDPTX_DEBUG_INFO, mtk_edp->max_lanes, mtk_edp->max_linkrate);

	ret = of_property_read_u32(dev->of_node, MTK_EDP_MODE_EXTERNAL_MONITOR, &read_value);
	mtk_edp->external_monitor = (!ret) ? !!read_value : false;

	ret = of_property_read_u32(dev->of_node, MTK_EDP_MODE_USE_EDID, &read_value);
	mtk_edp->use_edid = (!ret) ? !!read_value : false;

	ret = of_property_read_u32(dev->of_node, MTK_EDP_MODE_USE_HPD, &read_value);
	mtk_edp->use_hpd = (!ret) ? !!read_value : false;

	dev_info(dev, "%s use external monitor:%d, use edid:%d use hpd:%d\n", EDPTX_DEBUG_INFO,
			mtk_edp->external_monitor, mtk_edp->use_edid, mtk_edp->use_hpd);

	return 0;
}

另外一个要点,就是我们前面提过的数据中转站节点&edp_tx,与之相应的,该驱动的核心功能,换句话说也就是驱动的目的,就是建立起SOC和显示屏之间的一个稳定、高速的数据传输通道。故而驱动中mtk_edp_train_setting函数设置初始的链路速率和通道数量,并配置PHY。

static int mtk_edp_train_setting(struct mtk_edp *mtk_edp, u8 target_link_rate,
				u8 target_lane_count)
{
	int ret;

	pr_info("%s %s+\n", EDPTX_DEBUG_INFO, __func__);
	drm_dp_dpcd_writeb(&mtk_edp->aux, DP_LINK_BW_SET, target_link_rate);
	drm_dp_dpcd_writeb(&mtk_edp->aux, DP_LANE_COUNT_SET,
			   target_lane_count | DP_LANE_COUNT_ENHANCED_FRAME_EN);

	if (mtk_edp->train_info.sink_ssc)
		drm_dp_dpcd_writeb(&mtk_edp->aux, DP_DOWNSPREAD_CTRL,
				   DP_SPREAD_AMP_0_5);

	mtk_edp_set_lanes(mtk_edp, target_lane_count / 2);
	ret = mtk_edp_phy_configure(mtk_edp, target_link_rate, target_lane_count, FALSE,
								NULL, NULL);
	if (ret)
		return ret;

	dev_info(mtk_edp->dev,
		"Link train target_link_rate = 0x%x, target_lane_count = 0x%x\n",
		target_link_rate, target_lane_count);

	pr_info("%s %s-\n", EDPTX_DEBUG_INFO, __func__);
	return 0;
}

而mtk_edp_train_cr函数执行时钟恢复阶段,调整传输的电压,确保接收端可以正确地锁定时钟:

static int mtk_edp_train_cr(struct mtk_edp *mtk_edp, u8 target_lane_count)
{
	u8 lane_adjust[2] = {};
	u8 link_status[DP_LINK_STATUS_SIZE] = {};
	u8 prev_lane_adjust = 0xff;
	int train_retries = 0;
	int voltage_retries = 0;

	if (!target_lane_count)
		return -ENODEV;

	mtk_edp_pattern(mtk_edp, true);

	/* In DP spec 1.4, the retry count of CR is defined as 10. */
	do {
		train_retries++;
		if (!mtk_edp->train_info.cable_plugged_in) {
			mtk_edp_train_set_pattern(mtk_edp, 0);
			return -ENODEV;
		}

		drm_dp_dpcd_read(&mtk_edp->aux, DP_ADJUST_REQUEST_LANE0_1,
				 lane_adjust, sizeof(lane_adjust));
		mtk_edp_train_update_swing_pre(mtk_edp, target_lane_count,
					      lane_adjust);

		drm_dp_link_train_clock_recovery_delay(&mtk_edp->aux,
						       mtk_edp->rx_cap);

		/* check link status from sink device */
		drm_dp_dpcd_read_link_status(&mtk_edp->aux, link_status);
		if (drm_dp_clock_recovery_ok(link_status,
					     target_lane_count)) {
			dev_info(mtk_edp->dev, "%s CR training pass\n", EDPTX_DEBUG_INFO);
			return 0;
		}

		/*
		 * In DP spec 1.4, if current voltage level is the same
		 * with previous voltage level, we need to retry 5 times.
		 */
		if (prev_lane_adjust == link_status[4]) {
			voltage_retries++;
			/*
			 * Condition of CR fail:
			 * 1. Failed to pass CR using the same voltage
			 *    level over five times.
			 * 2. Failed to pass CR when the current voltage
			 *    level is the same with previous voltage
			 *    level and reach max voltage level (3).
			 */
			if (voltage_retries > MTK_DP_TRAIN_VOLTAGE_LEVEL_RETRY ||
			    (prev_lane_adjust & DP_ADJUST_VOLTAGE_SWING_LANE0_MASK) == 3) {
				dev_dbg(mtk_edp->dev, "Link train CR fail\n");
				break;
			}
		} else {
			/*
			 * If the voltage level is changed, we need to
			 * re-calculate this retry count.
			 */
			voltage_retries = 0;
			pr_info("%s %s %d\n", EDPTX_DEBUG_INFO, __func__, __LINE__);
		}
		prev_lane_adjust = link_status[4];
		dev_info(mtk_edp->dev, "[eDPTX] CR training retries: %d\n", voltage_retries);
	} while (train_retries < MTK_DP_TRAIN_DOWNSCALE_RETRY);

	/* Failed to train CR, and disable pattern. */
	pr_info("cr training failed %s %s %d\n", EDPTX_DEBUG_INFO, __func__, __LINE__);
	drm_dp_dpcd_writeb(&mtk_edp->aux, DP_TRAINING_PATTERN_SET,
			   DP_TRAINING_PATTERN_DISABLE);
	mtk_edp_train_set_pattern(mtk_edp, 0);

	return -ETIMEDOUT;
}

期间会不断读取 link_status 并调整 lane_adjust ,直到时钟同步。voltage_retries的定义是用于重试机制,如果失败的次数达到超过预设值,会尝试降低链路速率。

mtk_edp_video_config函数中包含有分辨率、时序等显示模式内容的的定义,在mtk_edp_set_msa定义有包括水平时序以及垂直时序的配置,这直接将设备树中的panel-timing节点的参数写入硬件。不过具体的时序值,在面板节点中定义。

static int mtk_edp_video_config(struct mtk_edp *mtk_edp)
{
	mtk_edp_config_mn_mode(mtk_edp);
	mtk_edp_set_msa(mtk_edp);
	mtk_edp_set_color_depth(mtk_edp);
	return mtk_edp_set_color_format(mtk_edp, mtk_edp->info.format);
}

(3)、panel-edp.c

不难看出这是EDP的面板驱动程序,与我们前面分析的EDP TX驱动是配对使用的,分别负责显示流水线的不同部分。这是通用的EDP面板驱动,显然是为了在DTS中配置支持多种不同的EDP屏,满足用户的需求。

老样子,核心结构体的资源定义如下:

struct panel_edp {
	struct drm_panel panel;          // 最重要的成员,将其嵌入到DRM面板框架中
	struct device *dev;              // 关联的Linux设备结构

	const char *label;               // 面板名称(可选)
	unsigned int width;              // 面板物理宽度(mm)
	unsigned int height;             // 面板物理高度(mm)
	struct videomode video_mode;     // 存储面板的时序参数(分辨率、刷新率等)
	struct backlight_device *backlight; // 背光设备(可能来自其他驱动)
	struct regulator *supply;        // 电源调节器(例如,一个主要的Panel Power Supply)

	// 用于控制面板的GPIO引脚
	struct gpio_desc *power3v3_gpio;  // 控制3.3V电源的GPIO
	struct gpio_desc *enable_gpio;    // 面板Enable/Reset引脚
	struct gpio_desc *power1v8_gpio;  // 控制1.8V电源的GPIO(可能用于逻辑电路)
	struct gpio_desc *backlight_gpio; // 直接控制背光Enable的GPIO(简单背光)
};

话分两头,通常论面板驱动的核心,在下认为是包括电、时序、复位这三个方面的配置。而在MTK的面板驱动中,drm panel相关操作函数集则是驱动的灵魂,其定义了电源时序。DRM核心会按照prepare -> enable -> disable -> unprepare的顺序来调用这些函数。

函数作用对应设备树参数代码行为
panel_edp_prepare上电序列prepare 延时

1. 打开电源 regulator

2. 拉高 power3v3_gpio (3.3V)

3. 拉高 power1v8_gpio (1.8V)

4. 等待(设备树中定义的 prepare 时间)

5. 拉高 enable_gpio (解除复位/使能面板)

6. 拉高 backlight_gpio (背光供电)

panel_edp_enable开启显示enable 延时

1. 设置背光设备状态为 FB_BLANK_UNBLANK

2. 更新背光状态,这会触发背光驱动实际打开背光

panel_edp_disable关闭显示disable 延时

1. 设置背光设备状态为 FB_BLANK_POWERDOWN

2. 更新背光状态,触发背光驱动关闭背光

panel_edp_unprepare断电序列unprepare 延时

prepare 的逆过程:

1. 拉低 enable_gpio

2. 拉低 power3v3_gpio

3. 关闭电源 regulator

面板模式的获取通过 panel_edp_get_modes 函数实现,当DRM系统需要知道面板支持什么分辨率的时候,则会调用此函数。

static int panel_edp_get_modes(struct drm_panel *panel,
					struct drm_connector *connector)
{
	struct panel_edp *edp = to_panel_edp(panel);
	struct drm_display_mode *mode;

	dev_info(edp->dev, "[eDPTX] %s+\n", __func__);

	mode = drm_mode_create(connector->dev);
	if (!mode)
		return 0;

	drm_display_mode_from_videomode(&edp->video_mode, mode);
	mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;
	drm_mode_probed_add(connector, mode);

	connector->display_info.width_mm = edp->width;
	connector->display_info.height_mm = edp->height;

	dev_info(edp->dev, "[eDPTX] %s-\n", __func__);

	return 1;
}

从设备树解析配置,则是 panel_edp_parse_dt 函数,从DTS中读取所有配置信息,是驱动与DTS之间的桥梁。

static int panel_edp_parse_dt(struct panel_edp *edp)
{
	struct device_node *np = edp->dev->of_node;
	struct display_timing timing;
	int ret;

	ret = of_get_display_timing(np, "panel-timing", &timing);
	if (ret < 0) {
		dev_info(edp->dev, "%pOF: problems parsing panel-timing (%d)\n",
			np, ret);
		return ret;
	}

	videomode_from_timing(&timing, &edp->video_mode);

	ret = of_property_read_u32(np, "width-mm", &edp->width);
	if (ret < 0) {
		dev_info(edp->dev, "%pOF: invalid or missing %s DT property\n",
			np, "width-mm");
		return -ENODEV;
	}
	ret = of_property_read_u32(np, "height-mm", &edp->height);
	if (ret < 0) {
		dev_info(edp->dev, "%pOF: invalid or missing %s DT property\n",
			np, "height-mm");
		return -ENODEV;
	}

	return 0;
}

最后,我们来看到驱动初始化的流程,panel_edp_probe

static int panel_edp_probe(struct platform_device *pdev)
{
	struct panel_edp *edp;
	int ret;

	dev_info(&pdev->dev, "[eDPTX] bincai edp panel %s+\n", __func__);

	edp = devm_kzalloc(&pdev->dev, sizeof(*edp), GFP_KERNEL);
	if (!edp)
		return -ENOMEM;

	edp->dev = &pdev->dev;

	ret = panel_edp_parse_dt(edp);
	if (ret < 0)
		return ret;

	edp->supply = devm_regulator_get_optional(edp->dev, "power");
	if (IS_ERR(edp->supply)) {
		ret = PTR_ERR(edp->supply);

		if (ret != -ENODEV) {
			if (ret != -EPROBE_DEFER)
				dev_info(edp->dev, "failed to request regulator: %d\n",
					ret);
			return ret;
		}

		edp->supply = NULL;
	}

	/* Get GPIOs and backlight controller. */
	edp->enable_gpio = devm_gpiod_get_optional(edp->dev, "enable",
						     GPIOD_OUT_HIGH);
	if (IS_ERR(edp->enable_gpio)) {
		ret = PTR_ERR(edp->enable_gpio);
		dev_info(edp->dev, "failed to request %s GPIO: %d\n",
			"enable", ret);
		return ret;
	}

	edp->power3v3_gpio = devm_gpiod_get_optional(edp->dev, "power3v3",
						     GPIOD_OUT_HIGH);
	if (IS_ERR(edp->power3v3_gpio)) {
		ret = PTR_ERR(edp->power3v3_gpio);
		dev_info(edp->dev, "failed to request %s GPIO: %d\n",
			"power3v3", ret);
		return ret;
	}

	edp->power1v8_gpio = devm_gpiod_get_optional(edp->dev, "power1v8",
							GPIOD_OUT_HIGH);
	if (IS_ERR(edp->power1v8_gpio)) {
		ret = PTR_ERR(edp->power1v8_gpio);
		dev_info(edp->dev, "failed to request %s GPIO: %d\n",
			"pwoer1v8", ret);
		return ret;
	}

	edp->backlight_gpio = devm_gpiod_get_optional(edp->dev, "backlight",
							GPIOD_OUT_HIGH);
	if (IS_ERR(edp->backlight_gpio)) {
		ret = PTR_ERR(edp->backlight_gpio);
		dev_info(edp->dev, "failed to request %s GPIO: %d\n",
			"backlight", ret);
		return ret;
	}

	/*
	 * TODO: Handle all power supplies specified in the DT node in a generic
	 * way for panels that don't care about power supply ordering. eDP
	 * panels that require a specific power sequence will need a dedicated
	 * driver.
	 */

	/* Register the panel. */
	drm_panel_init(&edp->panel, edp->dev, &panel_edp_funcs,
				DRM_MODE_CONNECTOR_eDP);

	drm_panel_add(&edp->panel);

	dev_info(&pdev->dev, "[eDPTX] %s-\n", __func__);

	dev_set_drvdata(edp->dev, edp);
	return 0;
}

在驱动加载时,probe 函数被调用:

(1)、分配 struct panel_edp 内存。

(2)、调用 panel_edp_parse_dt 解析设备树。

(3)、使用 devm_* 系列函数申请所有资源:regulator 和 GPIO (enable, power3v3, power1v8, backlight)。

(4)、GPIOD_OUT_HIGH 表示默认初始化时为高电平,但通常在 prepare 中才会按顺序设置。

(5)、最后调用 drm_panel_add,将这个面板注册到 DRM 框架,使其可供 eDP TX 驱动调用。

四、总结

以上,便是在下对MTK平台上EDP模块的一些浅薄分析,具体的配置,需要根据屏幕型号、规格参数等实际情况来实现,毕竟,实践才是检验真理的唯一标准。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值