目录
一、引子
继前面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_inSoC内部的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. 拉高 3. 拉高 4. 等待(设备树中定义的 5. 拉高 6. 拉高 |
panel_edp_enable | 开启显示 | enable 延时 | 1. 设置背光设备状态为 2. 更新背光状态,这会触发背光驱动实际打开背光 |
panel_edp_disable | 关闭显示 | disable 延时 | 1. 设置背光设备状态为 2. 更新背光状态,触发背光驱动关闭背光 |
panel_edp_unprepare | 断电序列 | unprepare 延时 |
1. 拉低 2. 拉低 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模块的一些浅薄分析,具体的配置,需要根据屏幕型号、规格参数等实际情况来实现,毕竟,实践才是检验真理的唯一标准。

被折叠的 条评论
为什么被折叠?



