概述
基于DRM框架的HDMI热插拔流程分析
1. DRM介绍
DRM 全称是Direct Rendering Manager,进行显示输出管理、buffer 分配、帧缓冲。对应userspace 库
为libdrm,,libdrm 库提供了一系列友好的控制封装,使用户可以方便的进行显示的控制和buffer 申请。
GDK8使用的是瑞芯微的RK3328芯片,而瑞芯微的显示框架有两大模块,分别是DRM与FB,其中FB框架是对应3.x内核的,而DRM框架是对应4.x内核,因此使用4.19.161内核的GDK8就是采用基于DRM显示框架的HDMI。
DRM的设备节点为"/dev/dri/cardX"
, X 为0-15 的数值,默认使用的是/dev/dri/card0
。
CRTC:显示控制器,在rockchip 平台是SOC 内部VOP(部分文档也称为LCDC)模块的抽象;
Plane:图层,在rockchip 平台是SOC 内部VOP(LCDC)模块win 图层的抽象;
Encoder:输出转换器,指RGB、LVDS、DSI、eDP、HDMI、CVBS、VGA 等显示接口;
Connector:连接器,指encoder 和panel 之间交互的接口部分;
Bridge:桥接设备,一般用于注册encoder 后面另外再接的转换芯片,如DSI2HDMI 转换芯片。
Panel:泛指屏,各种LCD、HDMI 等显示设备的抽象;
GEM:buffer 管理和分配,类似android 下的ion。
2. 热插拔介绍
热插拔是指在不关闭系统的前提下,插拔外部设备而不影响系统的正常使用。这个功能对需要许多外设都是非常重要的,毕竟你不可能希望拔出HDMI后,只能关机插入HDMI,再重新上电后,才能让HDMI再次工作。
DRM驱动处理热插拔的过程比较复杂,下面会对一些主要的函数设置断点,并分析其的作用。
3. HDMI热插拔流程分析
3.1 更新物理层状态
当内核检测到HDMI热插拔事件后,首先会通过dw_hdmi_phy_update_hpd
函数更改hdmi的phy状态。
void dw_hdmi_phy_update_hpd(struct dw_hdmi *hdmi, void *data,
bool force, bool disabled, bool rxsense)
{
u8 old_mask = hdmi->phy_mask;
if (force || disabled || !rxsense)
hdmi->phy_mask |= HDMI_PHY_RX_SENSE;
else
hdmi->phy_mask &= ~HDMI_PHY_RX_SENSE;
if (old_mask != hdmi->phy_mask)
hdmi_writeb(hdmi, hdmi->phy_mask, HDMI_PHY_MASK0);
}
EXPORT_SYMBOL_GPL(dw_hdmi_phy_update_hpd);
在用户空间中,可以通过dw-hdmi
目录下的虚文件,查看HDMI的状态信息。
cat /sys/kernel/debug/dw-hdmi/status
PHY: enabled
Mode: HDMI
Pixel Clk: 297000000Hz
TMDS Clk: 297000000Hz
Color Format: YUV444
Color Depth: 8 bit
Colorimetry: ITU.BT709
EOTF: Off
Mode:当前的输出模式
Pixel Clk:当前输出的像素时钟
TMDS Clk:当前输出的HDMI符号率
Color Format:当前输出的颜色格式
Color Depth:当前输出的颜色深度
Colorimery:当前输出的颜色标准
EOTF:HDR信息
通过栈回溯可以看到,在内核检测到HDMI插拔时,内核会检测到中断事件,并创建一个新的线程来处理该事件,这个时候内核会调用dw_hdmi_irq
,在dw_hdmi_irq
内会去更新hdmi的phy和rxsense的状态。
kn
# Child-SP
RetAddr
Call Site
00 ffffff80`0a9b3d20 ffffff80`087f17d8 lk!dw_hdmi_phy_update_hpd [drivers/gpu/drm/bridge/synopsys/dw-hdmi.c @ 1730]
01 ffffff80`0a9b3d20 ffffff80`087f1a1c lk!dw_hdmi_setup_rx_sense+0x78 [drivers/gpu/drm/bridge/synopsys/dw-hdmi.c @ 3178]
02 ffffff80`0a9b3d20 ffffff80`08129238 lk!dw_hdmi_irq+0x22c [drivers/gpu/drm/bridge/synopsys/dw-hdmi.c @ 3219]
03 ffffff80`0a9b3d20 ffffff80`08129618 lk!irq_thread_fn+0x28 [kernel/irq/manage.c @ 1010]
04 ffffff80`0a9b3d20 ffffff80`080e168c lk!irq_thread+0x118 [kernel/irq/manage.c @ 1091]
05 ffffff80`0a9b3d20 ffffff80`08085dd0 lk!kthread+0x12c [kernel/kthread.c @ 260]
06 ffffff80`0a9b3d20 00000008`00000008 lk!ret_from_fork+0x10 [arch/arm64/kernel/entry.S @ 1104]
3.2 检测连接器状态
drm驱动通过drm_helper_hpd_irq_event
函数检测每一个connector的状态。
通过下面的栈回溯可以看到当HDMI插入后,内核同样会检测到中断事件,并创建一个新的线程来处理该事件,在更新完phy和rxsense的状态后,由于是热插拔事件,所以会调用repo_hpd_event
函数(hpd:hot plug detect)。
kn
# Child-SP
RetAddr
Call Site
00 ffffff80`0abd3d60 ffffff80`087f241c lk!drm_helper_hpd_irq_event [drivers/gpu/drm/drm_probe_helper.c @ 773]
01 ffffff80`0abd3d60 ffffff80`080db1b8 lk!repo_hpd_event+0x8c [drivers/gpu/drm/bridge/synopsys/dw-hdmi.c @ 383]
02 ffffff80`0abd3d60 ffffff80`080db47c lk!process_one_work+0x1a0 [./arch/arm64/include/asm/jump_label.h @ 31]
03 ffffff80`0abd3d60 ffffff80`080e168c lk!worker_thread+0x4c [./include/linux/compiler.h @ 193]
04 ffffff80`0abd3d60 ffffff80`08085dd0 lk!kthread+0x12c [kernel/kthread.c @ 260]
05 ffffff80`0abd3d60 00000008`00000008 lk!ret_from_fork+0x10 [arch/arm64/kernel/entry.S @ 1104]
在repo_hpd_event
函数中,首先会根据phy的状态设置去设置rxsense(rxsense的状态会改变多次),之后如果发现设备存在,就会调用drm_helper_hpd_irq_event
。
static void repo_hpd_event(struct work_struct *p_work)
{
struct dw_hdmi *hdmi = container_of(p_work, struct dw_hdmi, work.work);
u8 phy_stat = hdmi_readb(hdmi, HDMI_PHY_STAT0);
mutex_lock(&hdmi->mutex);
if (!(phy_stat & HDMI_PHY_RX_SENSE))
hdmi->rxsense = false;
if (phy_stat & HDMI_PHY_HPD)
hdmi->rxsense = true;
mutex_unlock(&hdmi->mutex);
if (hdmi->bridge.dev) {
bool change;
change = drm_helper_hpd_irq_event(hdmi->bridge.dev);
#ifdef CONFIG_CEC_NOTIFIER
if (change)
cec_notifier_repo_cec_hpd(hdmi->cec_notifier,
hdmi->hpd_state,
ktime_get());
#endif
}
}
drm_helper_hpd_irq_event
函数主要做两个事情,分别是修改connector的状态和决定是否通知用户空间执行对应的操作。
bool drm_helper_hpd_irq_event(struct drm_device *dev)
{
struct drm_connector *connector;
struct drm_connector_list_iter conn_iter;
enum drm_connector_status old_status;
bool changed = false;
if (!dev->mode_config.poll_enabled)
return false;
mutex_lock(&dev->mode_config.mutex);
drm_connector_list_iter_begin(dev, &conn_iter);
drm_for_each_connector_iter(connector, &conn_iter) {
/* Only handle HPD capable connectors. */
if (!(connector->polled & DRM_CONNECTOR_POLL_HPD))
continue;
old_status = connector->status;
connector->status = drm_helper_probe_detect(connector, NULL, false);
DRM_DEBUG_KMS("[CONNECTOR:%d:%s] status updated from %s to %sn",
connector->base.id,
connector->name,
drm_get_connector_status_name(old_status),
drm_get_connector_status_name(connector->status));
if (old_status != connector->status)
changed = true;
}
drm_connector_list_iter_end(&conn_iter);
mutex_unlock(&dev->mode_config.mutex);
if (changed)
drm_kms_helper_hotplug_event(dev);
return changed;
}
3.3 触发KMS事件
如果发现状态已经改变drm_helper_hpd_irq_event
就会调用drm_kms_helper_hotplug_event
触发KMS(kernel mode setting)事件。
其中drm_sysfs_hotplug_event
通过kobject_uevent_env
函数发送uevent给用户空间的udev进程;而drm_client_dev_hotplug
会通知客户端发生了热插拔事件。
void drm_kms_helper_hotplug_event(struct drm_device *dev)
{
/* send a uevent + call fbdev */
drm_sysfs_hotplug_event(dev);
if (dev->mode_config.funcs->output_poll_changed)
dev->mode_config.funcs->output_poll_changed(dev);
drm_client_dev_hotplug(dev);
}
3.4 转移阵地->用户空间
在Ubuntu的用户空间内有两大程序为DRM驱动服务,分别是负责管理设备的udevd
守护进程和图形化程序Xorg。
3.4.1 设备管理工具-udevd
在Linux中,设备的底层支持是内核处理的,但是它们的相关事件是在用户空间中通过udevd
进程进行管理的,在上面的drm_kms_helper_hotplug_event
内也可以看到,内核会通过kobject_uevent_env
向udevd
发生uevent事件,获取uevent的事件后,udevd
会去通过用户空间的脚本文件执行相应的操作。
139 ?
00:00:00 systemd-udevd
udevd
管理的规则文件通常放在/etc/udev/rules.d/
或/usr/lib/udev/rules.d
目录下,并且以.rules
为文件的后缀名。
file /etc/udev/rules.d/60-drm.rules
/etc/udev/rules.d/60-drm.rules: ASCII text
cat /etc/udev/rules.d/60-drm.rules
SUBSYSTEM=="drm", ACTION=="change", ENV{HOTPLUG}=="1", RUN+="/usr/local/bin/drm-hotplug.sh"
通过上面的60-drm.rules
文件可以知道,热插拔事件发生后,会去运行drm-hotplug.sh
脚本文件,下面脚本会通过xrandr改变屏幕的显示状态。
cat /usr/local/bin/drm-hotplug.sh
#!/bin/sh -x
# Try to figure out XAUTHORITY and DISPLAY
for pid in $(pgrep X 2>/dev/null || ls /proc|grep -ow "[0-9]*"|sort -rn); do
PROC_DIR=/proc/$pid
# Filter out non-X processes
readlink $PROC_DIR/exe|grep -qwE "X$|Xorg$" || continue
# Parse auth file and display from cmd args
export XAUTHORITY=$(cat $PROC_DIR/cmdline|tr '