[关闭]
@wenshizhang 2020-06-12T17:48:56.000000Z 字数 7016 阅读 296

HDMI热插拔分析.md

描述

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

状态确认

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

  1. Screen 0: minimum 320 x 200, current 3840 x 1080, maximum 8192 x 8192
  2. eDP-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 382mm x 215mm
  3. 1920x1080 60.01*+ 60.01 59.97 59.96 59.93
  4. ......
  5. HDMI-1 connected 1920x1080+1920+0 (normal left inverted right x axis y axis) 527mm x 296mm
  6. 1920x1080 60.00*+ 60.00 50.00 59.94 30.00 25.00 24.00 29.97 23.98
  7. 1920x1080i 60.00 60.00 50.00 59.94
  8. ......
  9. HDMI-2 disconnected (normal left inverted right x axis y axis)

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

  1. $ cat /sys/class/drm/card0-HDMI-A-1/status
  2. connected
  3. $

这两个状态有一点区别是: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定义

从上图可以看出来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函数:

  1. irqreturn_t radeon_driver_irq_handler_kms(int irq, void *arg)
  2. {
  3. struct drm_device *dev = (struct drm_device *) arg;
  4. struct radeon_device *rdev = dev->dev_private;
  5. irqreturn_t ret;
  6. ret = radeon_irq_process(rdev);
  7. if (ret == IRQ_HANDLED)
  8. pm_runtime_mark_last_busy(dev->dev);
  9. return ret;
  10. }

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

  1. * Each IV ring entry is 128 bits:
  2. * [7:0] - interrupt source id
  3. * [31:8] - reserved
  4. * [59:32] - interrupt source data
  5. * [63:60] - reserved
  6. * [71:64] - RINGID
  7. * [79:72] - VMID
  8. * [127:80] - reserved

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

  1. restart_ih:
  2. rptr = rdev->ih.rptr;
  3. DRM_DEBUG("si_irq_process start: rptr %d, wptr %d\n", rptr, wptr);
  4. /* Order reading of wptr vs. reading of IH ring data */
  5. rmb();
  6. /* display interrupts */
  7. si_irq_ack(rdev);
  8. while (rptr != wptr) {
  9. /* wptr/rptr are in bytes! */
  10. ring_index = rptr / 4;
  11. src_id = le32_to_cpu(rdev->ih.ring[ring_index]) & 0xff;
  12. src_data = le32_to_cpu(rdev->ih.ring[ring_index + 1]) & 0xfffffff;
  13. ring_id = le32_to_cpu(rdev->ih.ring[ring_index + 2]) & 0xff;
  14. switch (src_id) {
  15. case 1: /* D1 vblank/vline */
  16. ....
  17. case 8: /* D1 page flip */
  18. .....
  19. case 42: /* HPD hotplug */
  20. ......

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

  1. case 42: /* HPD hotplug */
  2. if (src_data <= 5) {
  3. hpd_idx = src_data;
  4. mask = DC_HPD1_INTERRUPT;
  5. queue_hotplug = true;
  6. event_name = "HPD";
  7. } else if (src_data <= 11) {
  8. hpd_idx = src_data - 6;
  9. mask = DC_HPD1_RX_INTERRUPT;
  10. queue_dp = true;
  11. event_name = "HPD_RX";
  12. } else {
  13. DRM_DEBUG("Unhandled interrupt: %d %d\n",
  14. src_id, src_data);
  15. printk("Unhandled interrupt: %d %d\n",
  16. src_id, src_data);
  17. break;
  18. }
  19. if (!(disp_int[hpd_idx] & mask))
  20. DRM_DEBUG("IH: IH event w/o asserted irq bit?\n");
  21. disp_int[hpd_idx] &= ~mask;
  22. DRM_DEBUG("IH: %s%d\n", event_name, hpd_idx + 1);
  23. break;
  24. ......
  25. if (queue_hotplug)
  26. schedule_delayed_work(&rdev->hotplug_work, 0);

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

  1. int radeon_irq_kms_init(struct radeon_device *rdev)
  2. {
  3. /* enable msi */
  4. rdev->msi_enabled = 0;
  5. if (radeon_msi_ok(rdev)) {
  6. int ret = pci_enable_msi(rdev->pdev);
  7. if (!ret) {
  8. rdev->msi_enabled = 1;
  9. dev_info(rdev->dev, "radeon: using MSI.\n");
  10. }
  11. }
  12. INIT_DELAYED_WORK(&rdev->hotplug_work, radeon_hotplug_work_func);
  13. INIT_WORK(&rdev->dp_work, radeon_dp_work_func);
  14. INIT_WORK(&rdev->audio_work, r600_audio_update_hdmi);
  15. .....
  16. }

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

  1. bool drm_helper_hpd_irq_event(struct drm_device *dev)
  2. {
  3. struct drm_connector *connector;
  4. struct drm_connector_list_iter conn_iter;
  5. enum drm_connector_status old_status;
  6. bool changed = false;
  7. if (!dev->mode_config.poll_enabled)
  8. return false;
  9. mutex_lock(&dev->mode_config.mutex);
  10. drm_connector_list_iter_begin(dev, &conn_iter);
  11. drm_for_each_connector_iter(connector, &conn_iter) {
  12. /* Only handle HPD capable connectors. */
  13. if (!(connector->polled & DRM_CONNECTOR_POLL_HPD))
  14. continue;
  15. old_status = connector->status;
  16. connector->status = drm_helper_probe_detect(connector, NULL, false);
  17. if (old_status != connector->status)
  18. changed = true;
  19. }
  20. drm_connector_list_iter_end(&conn_iter);
  21. mutex_unlock(&dev->mode_config.mutex);
  22. if (changed)
  23. drm_kms_helper_hotplug_event(dev);
  24. return changed;
  25. }

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

通过DDC获取EDID

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

  1. bool radeon_ddc_probe(struct radeon_connector *radeon_connector, bool use_aux)
  2. {
  3. if (radeon_connector->router.ddc_valid)
  4. radeon_router_select_ddc_port(radeon_connector);
  5. if (use_aux) {
  6. ret = i2c_transfer(&radeon_connector->ddc_bus->aux.ddc, msgs, 2);
  7. } else {
  8. ret = i2c_transfer(&radeon_connector->ddc_bus->adapter, msgs, 2);
  9. }
  10. if (ret != 2)
  11. /* Couldn't find an accessible DDC on this connector */
  12. return false;
  13. if (drm_edid_header_is_valid(buf) < 6) {
  14. /* Couldn't find an accessible EDID on this
  15. * connector */
  16. return false;
  17. }
  18. drm_edid_header_is_valid(buf));
  19. return true;
  20. }

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

HDMI接口detect

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

  1. static enum drm_connector_status
  2. radeon_dvi_detect(struct drm_connector *connector, bool force)
  3. {
  4. if (radeon_connector->ddc_bus)
  5. dret = radeon_ddc_probe(radeon_connector, false);
  6. if (dret) {
  7. radeon_connector->detected_by_load = false;
  8. radeon_connector_free_edid(connector);
  9. radeon_connector_get_edid(connector);
  10. if (!radeon_connector->edid) {
  11. if ((rdev->family == CHIP_RS690 || rdev->family == CHIP_RS740) &&
  12. radeon_connector->base.null_edid_counter) {
  13. ret = connector_status_disconnected;
  14. radeon_connector->ddc_bus = NULL;
  15. } else {
  16. ret = connector_status_connected;
  17. broken_edid = true; /* defer use_digital to later */
  18. }
  19. } else {
  20. radeon_connector->use_digital =
  21. !!(radeon_connector->edid->input & DRM_EDID_INPUT_DIGITAL);
  22. if ((!radeon_connector->use_digital) && radeon_connector->shared_ddc) {
  23. radeon_connector_free_edid(connector);
  24. ret = connector_status_disconnected;
  25. } else {
  26. ret = connector_status_connected;
  27. }
  28. }
  29. }
  30. }

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

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注