oland显卡HDMI热插拔问题分析

描述

机缘巧合之下,在4.19内核里发现了radeon驱动一个很神奇的问题,插拔hdmi线时候,先拔出一半等10s左右再全部拔出。这时候,在sys下读到的hdmi连接状态还是connected。这个感觉还是很神奇的。切到amdgpu之后,也有这个问题,研究看看。

状态确认

显示器的连接状态,可以通过两个位置看xrandr/sys/class/drm/card0-HDMI-A-1/status


Screen 0: minimum 320 x 200, current 3840 x 1080, maximum 8192 x 8192

eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 382mm x 215mm

   1920x1080 60.01*+ 60.01 59.97 59.96 59.93  

 ......

HDMI-1 connected 1920x1080+1920+0 (normal left inverted right x axis y axis) 527mm x 296mm

   1920x1080 60.00*+ 60.00 50.00 59.94 30.00 25.00 24.00 29.97 23.98  

   1920x1080i 60.00 60.00 50.00 59.94  

   ......

HDMI-2 disconnected (normal left inverted right x axis y axis)



xrandr会打印出来所有的显示器信息的全部信息,包括主屏位置,显示模式(复制或者扩展),支持的分辨率等等。或者是可以看/sys/class/drm/card0-HDMI-A-1/status


$ cat /sys/class/drm/card0-HDMI-A-1/status 

connected

$ 

这两个状态有一点区别是:xrandr是调用drm提供的接口读取显示器状态,而sys下的状态是drm每次更新状态之后填进去的。

因此在这个问题的上下文中,拔出hdmi线,在sys下看到状态是connected,但是执行xrandr之后,状态就更新为disconnect。不难看出,第一次驱动判断hdmi状态出错了,第二次判断是对的。接下来要分析的事,hdmi热插拔之后驱动做了什么事,具体定位是哪里判断出错了。

HDMI原理介绍

名词解释

DDC(Display Data Channel):DDC是显示器与电脑主机进行通信的一个总线标准,他的基本功能就是将显示器的基本信息发送给主机。例如:可显示频率范围、生产厂商、生产日期、产品序列号、产品型号、标准显示模式和参数、亮度、对比度、色温参数等等。

EDID(Extended Display Identification Data Standard):是显示器通过DDC传输给电脑主机的标准数据信息格式。每次启动在drm debug信息或者X的启动日志中都能看到对应显示器的EDID信息。

HPD(Hot Plug Detection):热插拔探测,为热插拔设计的。

TMDS:最小传输差分信号传输技术。

CEC(Consumer Electronics Control):消费电子控制通道,电子设备可以借助cec信号控制hdmi接口上的连接的装置,比如单键播放,系统待机等。

HDMI定义

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dmQXbEfm-1613982945349)(https://github.com/wenshizhang/photo/blob/master/hdmi定义.jpg)]

从上图可以看出来HDMI接口包括:3个TMDS数据通道、1个TMDS时钟通道、CEC控制信号,DDC信号,+5v电源输出和HPD信号。TMDS是用来传输HDMI数据的,CEC是用了该控制试听设备的,和本文关系不大暂不介绍。DDC信号是显示器与主机电脑进行统信的一个总线,基本功能是将显示器的基本信息发送给主机(如EDID信息);HPD信号是显示器向主机发送的检测信号,用来检测显示器连接或断开。当HDMI主机检测到HPD引脚大于2v表示显示器与主机之间连接,当HDMI主机检测到HPD引脚小于0.8v表示显示器与主机之间断开了。当计算机通过HDMI接口与计算机相连时,主机通过+5v电源输出给显示器的DDC存储器供电,确保及时显示器不开机,计算机主机也能通过HDMI接口读到显示器的EDID数据。

HDMI接口识别过程

主机设备上电后会检测HPD是都被上拉到2v以上,接着主机设备已经通过+5v电源输出给EDID ROM供电。通过DDC读取到显示器的EDID,解析分辨率。检测TMDS信号是都被拉上来,如果是,准备输出TMDS信号。

主机设备检检测到HPD小于0.8v,停止输出TMDS信号。

内核radeon驱动代码分析

通过前面原理介绍发现radeon驱动HDMI热插拔分为3部分:HPD中断触发,DDC读取EDID,最后是HDMI接口detect。

HPD中断触发

HPD电平变化之后触发中断,cpu探测到中断上来,经过解析、分发、映射等等操作,最终调到驱动的中断处理函数来。radeon驱动的中断处理函数入口是radeon_driver_irq_handler_kms函数:


irqreturn_t radeon_driver_irq_handler_kms(int irq, void *arg)

{

        struct drm_device *dev = (struct drm_device *) arg;

        struct radeon_device *rdev = dev->dev_private;

        irqreturn_t ret;



        ret = radeon_irq_process(rdev);

        if (ret == IRQ_HANDLED)

                pm_runtime_mark_last_busy(dev->dev);

        return ret;

}

radeon_irq_process是定义好的一个宏,通过钩子函数,分别调用到si、cik或者是evergreen中真正的中断处理函数中来,这几个处理都比较类似,以si_irq_process为例。radedon中断是共享中断,诸如显示、uvd硬解、gui_idle等等事件都是通过这个中断触发的。处理函数去radeon读取wptr寄存器的值,根据寄存器内容来判断是哪类事件触发中断。这是内核对寄存器内容的定义:


 * Each IV ring entry is 128 bits:

 * [7:0] - interrupt source id

 * [31:8] - reserved

 * [59:32] - interrupt source data

 * [63:60] - reserved

 * [71:64] - RINGID

 * [79:72] - VMID

 * [127:80] - reserved

下面是radeon驱动根据寄存器内容判断触发中断事件类型的代码段:


restart_ih:

        rptr = rdev->ih.rptr;

        DRM_DEBUG("si_irq_process start: rptr %d, wptr %d\n", rptr, wptr);



        /* Order reading of wptr vs. reading of IH ring data */

        rmb();



        /* display interrupts */

        si_irq_ack(rdev);



        while (rptr != wptr) {

                /* wptr/rptr are in bytes! */

                ring_index = rptr / 4;

                src_id = le32_to_cpu(rdev->ih.ring[ring_index]) & 0xff;

                src_data = le32_to_cpu(rdev->ih.ring[ring_index + 1]) & 0xfffffff;

                ring_id = le32_to_cpu(rdev->ih.ring[ring_index + 2]) & 0xff;



                switch (src_id) {

                case 1: /* D1 vblank/vline */

                    ....

                case 8: /* D1 page flip */

                    .....

                case 42: /* HPD hotplug */

                ......



在本文的上下文场景下,寄存器判断得出中断源是HPD,执行下面的操作:


case 42: /* HPD hotplug */

        if (src_data <= 5) {

                hpd_idx = src_data;

                mask = DC_HPD1_INTERRUPT;

                queue_hotplug = true;

                event_name = "HPD";



        } else if (src_data <= 11) {

                hpd_idx = src_data - 6;

                mask = DC_HPD1_RX_INTERRUPT;

                queue_dp = true;

                event_name = "HPD_RX";



        } else {

                DRM_DEBUG("Unhandled interrupt: %d %d\n",

                          src_id, src_data);

                printk("Unhandled interrupt: %d %d\n",

                          src_id, src_data);

                break;

        }



        if (!(disp_int[hpd_idx] & mask))

                DRM_DEBUG("IH: IH event w/o asserted irq bit?\n");



        disp_int[hpd_idx] &= ~mask;

        DRM_DEBUG("IH: %s%d\n", event_name, hpd_idx + 1);

        break;

......

if (queue_hotplug)

        schedule_delayed_work(&rdev->hotplug_work, 0);



设置hotplug标志,最后,根据标志延迟调用hotplug_work函数。延迟调用采用了linux内核工作队列机制,先调用INIT_DELAYED_WORK把处理函数插入到工作队列中。hpd处理函数在radeon_irq_kms_init函数中插入工作队列:


int radeon_irq_kms_init(struct radeon_device *rdev)

{

        /* enable msi */

        rdev->msi_enabled = 0;



        if (radeon_msi_ok(rdev)) {

                int ret = pci_enable_msi(rdev->pdev);

                if (!ret) {

                        rdev->msi_enabled = 1;

                        dev_info(rdev->dev, "radeon: using MSI.\n");

                }

        }



        INIT_DELAYED_WORK(&rdev->hotplug_work, radeon_hotplug_work_func);

        INIT_WORK(&rdev->dp_work, radeon_dp_work_func);

        INIT_WORK(&rdev->audio_work, r600_audio_update_hdmi);

        .....   

}



从中断初始化中看,任务在这时候已经被插入在工作队列里,等到真的事件上来时候才会被调用。

hotplug_work->radeon_hotplug_work_func,最终到drm_helper_hpd_irq_event中。


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);

                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;

}



循环遍历每一个显示器,标记当前的设备连接状态,调用drm_helper_probe_detect得到当前状态,判断当前状态和刚刚标记的状态,如果相同则什么都不执行直接退出。如果不同,说明状态发生了变化,调用drm_kms_helper_hotplug_event重新设置当前的显示信号。

通过DDC获取EDID

显卡驱动都需要读取显示器的EDID,通过解析EDID回去显示器支持的分辨率,频率等等。radeon驱动也是读取EDID帮助判断显示器连接状态。


bool radeon_ddc_probe(struct radeon_connector *radeon_connector, bool use_aux)

{

        if (radeon_connector->router.ddc_valid)

                radeon_router_select_ddc_port(radeon_connector);



        if (use_aux) {

                ret = i2c_transfer(&radeon_connector->ddc_bus->aux.ddc, msgs, 2);

        } else {

                ret = i2c_transfer(&radeon_connector->ddc_bus->adapter, msgs, 2);

        }



        if (ret != 2)

                /* Couldn't find an accessible DDC on this connector */

                return false;



        if (drm_edid_header_is_valid(buf) < 6) {

                /* Couldn't find an accessible EDID on this

                 * connector */

                return false;

        }

        drm_edid_header_is_valid(buf));

        return true;

}

i2c读取显示器EDID,判断i2c读取结果和EDID合法性,都为成功的情况下,认为显示器状态正常。否则,返回失败。虽然原理中讲hpd读到电压在0.8v

HDMI接口detect

根据DDC返回值,detect函数根据显卡芯片类型、DDC类型等等因素设置显示器连接状态。


static enum drm_connector_status

radeon_dvi_detect(struct drm_connector *connector, bool force)

{

        if (radeon_connector->ddc_bus) 

                dret = radeon_ddc_probe(radeon_connector, false);



        if (dret) {

                radeon_connector->detected_by_load = false;

                radeon_connector_free_edid(connector);

                radeon_connector_get_edid(connector);



                if (!radeon_connector->edid) {

                        if ((rdev->family == CHIP_RS690 || rdev->family == CHIP_RS740) &&

                            radeon_connector->base.null_edid_counter) {

                                ret = connector_status_disconnected;

                                radeon_connector->ddc_bus = NULL;

                        } else {

                                ret = connector_status_connected;

                                broken_edid = true; /* defer use_digital to later */

                        }

                } else {

                        radeon_connector->use_digital =

                                !!(radeon_connector->edid->input & DRM_EDID_INPUT_DIGITAL);



                        if ((!radeon_connector->use_digital) && radeon_connector->shared_ddc) {

                                radeon_connector_free_edid(connector);

                                ret = connector_status_disconnected;

                        } else {

                                ret = connector_status_connected;

                        }

    

                }

        }

}

总结一下,HDMI拔出正常逻辑应该是:HPD探测到电压变化触发中断,接下来DDC读取显示器EDID返回失败,最终到dvi_detect函数中,通过DDC返回的失败,设置显示器连接状态为disconnected。

在这个问题中,HDMI拔出,HPD探测到电压变化中断触发,DDC读取显示器EDID返回成功,detect函数设置显示器连接状态是connected。那真正出错的位置是DDC不应该读取到EDID。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值