@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
。
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热插拔之后驱动做了什么事,具体定位是哪里判断出错了。
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接口包括: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数据。
主机设备上电后会检测HPD是都被上拉到2v以上,接着主机设备已经通过+5v电源输出给EDID ROM供电。通过DDC读取到显示器的EDID,解析分辨率。检测TMDS信号是都被拉上来,如果是,准备输出TMDS信号。
主机设备检检测到HPD小于0.8v,停止输出TMDS信号。
通过前面原理介绍发现radeon驱动HDMI热插拔分为3部分:HPD中断触发,DDC读取EDID,最后是HDMI接口detect。
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
重新设置当前的显示信号。
显卡驱动都需要读取显示器的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
根据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。