基于DRM框架的HDMI热插拔流程分析

2e1e9c29e6fe41c1f648a866fcdd52e0.png

D

R

M

基于DRM框架的

HDMI热插拔流程分析

866f20c3278e58b33c8fcb6008da3b56.jpeg

PART.01

D

R

M

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。

5b262a8dbcd0b9b3ce36ff50c691d01c.png

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

PART.02

热插拔是指在不关闭系统的前提下,插拔外部设备而不影响系统的正常使用。这个功能对需要许多外设都是非常重要的,毕竟你不可能希望拔出HDMI后,只能关机插入HDMI,再重新上电后,才能让HDMI再次工作。

DRM驱动处理热插拔的过程比较复杂,下面会对一些主要的函数设置断点,并分析其的作用。

6ae5b8a2e861f71e8a95e9d93d9d9133.png

PART.03

#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 %s\n",
            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 '\0' '\n'| \
        grep -w "\-auth" -A 1|tail -1)
    export DISPLAY=$(cat $PROC_DIR/cmdline|tr '\0' '\n'| \
        grep -w "^:.*" || echo ":0")


    echo Found auth: $XAUTHORITY for dpy: $DISPLAY
    break
done


export DISPLAY=${DISPLAY:-:0}


# Find an authorized user
unset USER
for user in root $(users);do
    sudo -u $user xdpyinfo &>/dev/null && \
        { USER=$user; break; }
done
[ $USER ] || exit 0


# Find disconnected monitors
MONITORS=$(sudo -u $user xrandr|grep -w disconnected|cut -d' ' -f1)


# Make sure every disconnected monitors been disabled especially DP-1.
for monitor in $MONITORS;do
    sudo -u $user xrandr --output $monitor --off
done


# Find connected monitors
MONITORS=$(sudo -u $user xrandr|grep -w connected|cut -d' ' -f1)


# Make sure every connected monitors been enabled with a valid mode.
for monitor in $MONITORS;do
    # Unlike the drm driver, X11 modesetting drv uses HDMI for HDMI-A
    CRTC=$(echo $monitor|sed "s/HDMI\(-[^B]\)/HDMI-A\1/")


    SYS="/sys/class/drm/card*-$CRTC/"


    # Already got a valid mode
    grep -w "$(cat $SYS/mode)" $SYS/modes && continue


    # Ether disabled or wrongly configured
    sudo -u $user xrandr --output $monitor --auto
done


exit 0

3.4.2

图形化程序-Xorg

当drm_client_dev_hotplug通过客户端Xorg热插拔事件发生后,Xorg会更新显示信息(比如合成图层,充填缓冲区),并通过libdrm库中的drmIoctl,帮助drm驱动协助处理显示信息。
首先Xorg会通过RROutputSetCrtcs获取显示设备的信息。

Bool
RROutputSetCrtcs(RROutputPtr output, RRCrtcPtr * crtcs, int numCrtcs)
{
    RRCrtcPtr *newCrtcs;
    int i;


    if (numCrtcs == output->numCrtcs) {
        for (i = 0; i < numCrtcs; i++)
            if (output->crtcs[i] != crtcs[i])
                break;
        if (i == numCrtcs)
            return TRUE;
    }
    if (numCrtcs) {
        newCrtcs = xallocarray(numCrtcs, sizeof(RRCrtcPtr));
        if (!newCrtcs)
            return FALSE;
    }
    else
        newCrtcs = NULL;
    free(output->crtcs);
    memcpy(newCrtcs, crtcs, numCrtcs * sizeof(RRCrtcPtr));
    output->crtcs = newCrtcs;
    output->numCrtcs = numCrtcs;
    RROutputChanged(output, TRUE);
    return TRUE;
}

之后会通过RRCrtcSet函数决定是否需要显示图形化界面。

Bool
RRCrtcSet(RRCrtcPtr crtc,
          RRModePtr mode,
          int x,
          int y, Rotation rotation, int numOutputs, RROutputPtr * outputs)
{
    ScreenPtr pScreen = crtc->pScreen;
    Bool ret = FALSE;
    Bool recompute = TRUE;
    Bool crtcChanged;
    int  o;


    rrScrPriv(pScreen);


    crtcChanged = FALSE;
    for (o = 0; o < numOutputs; o++) {
        if (outputs[o] && outputs[o]->crtc != crtc) {
            crtcChanged = TRUE;
            break;
        }
    }


    /* See if nothing changed */
    if (crtc->mode == mode &&
        crtc->x == x &&
        crtc->y == y &&
        crtc->rotation == rotation &&
        crtc->numOutputs == numOutputs &&
        !memcmp(crtc->outputs, outputs, numOutputs * sizeof(RROutputPtr)) &&
        !RRCrtcPendingProperties(crtc) && !RRCrtcPendingTransform(crtc) &&
        !crtcChanged) {
        recompute = FALSE;
        ret = TRUE;
    }
    else {
        if (pScreen->isGPU) {
            ScreenPtr master = pScreen->current_master;
            int width = 0, height = 0;


            if (mode) {
                width = mode->mode.width;
                height = mode->mode.height;
            }
            ret = rrCheckPixmapBounding(master, crtc,
                                        rotation, x, y, width, height);
            if (!ret)
                return FALSE;


            if (pScreen->current_master) {
                Bool sync = rrGetPixmapSharingSyncProp(numOutputs, outputs);
                ret = rrSetupPixmapSharing(crtc, width, height,
                                           x, y, rotation, sync,
                                           numOutputs, outputs);
            }
        }
#if RANDR_12_INTERFACE
        if (pScrPriv->rrCrtcSet) {
            ret = (*pScrPriv->rrCrtcSet) (pScreen, crtc, mode, x, y,
                                          rotation, numOutputs, outputs);
        }
        else
#endif
        {
#if RANDR_10_INTERFACE
            if (pScrPriv->rrSetConfig) {
                RRScreenSize size;
                RRScreenRate rate;


                if (!mode) {
                    RRCrtcNotify(crtc, NULL, x, y, rotation, NULL, 0, NULL);
                    ret = TRUE;
                }
                else {
                    size.width = mode->mode.width;
                    size.height = mode->mode.height;
                    if (outputs[0]->mmWidth && outputs[0]->mmHeight) {
                        size.mmWidth = outputs[0]->mmWidth;
                        size.mmHeight = outputs[0]->mmHeight;
                    }
                    else {
                        size.mmWidth = pScreen->mmWidth;
                        size.mmHeight = pScreen->mmHeight;
                    }
                    size.nRates = 1;
                    rate.rate = RRVerticalRefresh(&mode->mode);
                    size.pRates = &rate;
                    ret =
                        (*pScrPriv->rrSetConfig) (pScreen, rotation, rate.rate,
                                                  &size);
                    /*
                     * Old 1.0 interface tied screen size to mode size
                     */
                    if (ret) {
                        RRCrtcNotify(crtc, mode, x, y, rotation, NULL, 1,
                                     outputs);
                        RRScreenSizeNotify(pScreen);
                    }
                }
            }
#endif
        }
        if (ret) {


            RRTellChanged(pScreen);


            for (o = 0; o < numOutputs; o++)
                RRPostPendingProperties(outputs[o]);
        }
    }


    if (recompute)
        RRComputeContiguity(pScreen);


    return ret;
}

PART.04

硬件设备

GDK8 + 挥码枪

软件

Nano Code

GDK8(GEDU Development Kit 8)是格蠹科技针对ARMv8平台研发的开发和调试平台,预装了GNU的开发工具链,可以在ARM系统本机开发各种ARM应用软件和驱动程序, 彻底改变了传统的交叉编译方式,大大提高了开发和调试效率。GDK8可以与格蠹科技的Nano Debugger(NDB)一起工作,使用WinDBG的各种调试命令来调试Linux程序,将Linux平台的调试技术推上一个新的台阶, 是学习和研究ARMv8架构、LINUX操作系统和嵌入式软件技术的有力助手。

520cdae46a8fb62958347792aade656a.png

挥码枪(Nano Target Probe,NTP)是基于ARM CoreSight技术的硬件调试器, 其核心功能是通过CoreSight协议访问目标系统,实现系统调试和调优等功能。

9b340fba973416c8d93a25d16523d278.png

挥码枪上手指南:

https://www.nanocode.cn/wiki/docs/gedu_ntp_wiki

Nano Code下载链接:

https://www.nanocode.cn/#/download

PART.05

9fcc21c76051ed64d563f5dc506bc006.png

盛格塾是格蠹科技旗下的知识分享平台,是以“格物致知”为教育理念的现代私塾。

本着为先圣继绝学的思想,盛格塾努力将传统文化中的精华与现代科技密切结合,以传统文化和人文情怀阐释现代科技,用现代科技传播传统文化。

访问方式

手机端:微信小程序搜索“盛格塾”

电脑端:下载Nano Code社区版客户端

https://www.nanocode.cn/#/download

cd735afdd63b7c59fc6301a48ccd1a48.png

格友公众号

d50d7da0493cb5e043de7eb9b761d6c2.png

盛格塾小程序

 ✦ 

往期推荐

这个机制让人服

从头再来问题多

M核的第一条指令

哈佛架构的历史和今天

6e926006f5b4f614bc4063eadecadbd2.png

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值