1 简介
不同的显示屏支持的显示时序也是不同的,linux内核需要获取这些显示时序参数来适用不同的显示屏。
显示时序的具体解释请看:【RGB-时序分析】_rgb时序-CSDN博客
本文的内核代码版本时kernel-5.4.18
2 数据结构
2.1 struct drm_connector;
struct drm_connector {
......
/**
* @modes:
* Modes available on this connector (from fill_modes() + user).
* Protected by &drm_mode_config.mutex.
*/
struct list_head modes;
......
/**
* @probed_modes:
* These are modes added by probing with DDC or the BIOS, before
* filtering is applied. Used by the probe helpers. Protected by
* &drm_mode_config.mutex.
*/
struct list_head probed_modes;
.....
};
drm_mode_probed_add()函数会将struct drm_display_mode添加到链表probed_modes上。
drm_connector_list_update()会把链表 probed_modes上的struct drm_display_mode移动到链表modes上。
2.2 struct drm_display_mode;
/*
* ......
* The horizontal and vertical timings are defined per the following diagram.
*
* ::
*
*
* Active Front Sync Back
* Region Porch Porch
* <-----------------------><----------------><-------------><-------------->
* //|
* // |
* // |.................. ................
* _______________
* <----- [hv]display ----->
* <------------- [hv]sync_start ------------>
* <--------------------- [hv]sync_end --------------------->
* <-------------------------------- [hv]total ----------------------------->*
*
* ......
*/
struct drm_display_mode {
/**
* @head:
*
* struct list_head for mode lists.
*/
struct list_head head;
......
/**
* @clock:
*
* Pixel clock in kHz.
*/
int clock; /* in kHz */
int hdisplay;
int hsync_start;
int hsync_end;
int htotal;
int hskew;
int vdisplay;
int vsync_start;
int vsync_end;
int vtotal;
int vscan;
......
};
3 获取显示参数的方式
3.1 通过EDID数据获取显示参数
3.1.1 简介
常见的外接显示器中都会包含一个EDID数据,用来表示本显示器支持的显示时序。
VGA和HDMI等显示接口中都有I2C总线,CPU可以通过这个I2C总线读取外接显示器的EDID信息,通过解析EDID信息来获取显示参数。
VGA和HDMI接口中的I2C总线被称为DDC(Display Data Channel)。
通过I2C总线直接读取EDID是最常见的方式,除此之外,还有两种方式:
- 通过firmware方式指定EDID
- 通过debugfs指定EDID
3.1.2 通过I2C总线获取EDID的代码
3.1.2.1 通过I2C总线读取EDID数据
drm_get_edid();
-> drm_do_get_edid();
-> get_edid_block();
-> drm_do_probe_ddc_edid(); //get EDID information via I2C
3.1.2.2 将EDID数据解析成struct drm_display_mode
drm_get_edid()函数返回的是struct edid,可以使用 drm_add_edid_modes()函数将struct edid解析成struct drm_display_mode,并添加到struct drm_connector->probed_modes链表。
drm_add_edid_modes(); //drivers/gpu/drm/drm_edid.c
-> add_detailed_modes();
-> add_cvt_modes();
-> do_cvt_mode();
-> drm_cvt_modes();
-> drm_cvt_mode();
-> drm_mode_probed_add();
-> add_standard_modes();
-> drm_mode_std();
-> drm_cvt_mode(); / drm_mode_find_dmt(); / drm_gtf_mode();
-> drm_mode_probed_add();
-> add_established_modes();
-> drm_mode_probed_add();
-> add_cea_modes();
-> add_alternate_cea_modes();
-> drm_mode_probed_add();
3.1.2.3 整体流程
以上流程一般是在connector_funcs->get_modes()函数里完成,例如
amdgpu_connector_vga_get_modes();
-> amdgpu_connector_get_edid();
-> drm_get_edid();
-> amdgpu_connector_ddc_get_modes();
-> drm_add_edid_modes();
3.1.3 通过firmware方式指定EDID
linux-5.4.18内核强制输出图形显示信号 并通过firmware文件指定分辨率_video=lvds-1-CSDN博客
3.1.4 通过debugfs指定EDID
将EDID数据写到下面的文件里
/sys/kernel/debug/dri/0/<connector_name>/edid_override
对应的内核处理流程
edid_write();
-> connector->override_edid = XXX;
-> drm_connector_update_edid_property();
-> connector->edid_blob_ptr;
3.1.5 查看当前connector的EDID
方法一、hexdump /sys/class/drm/card0/<connector>/edid
方法二、通过/var/log/Xorg.0.log文件查看
可以在/var/log/Xorg.0.log文件中看到以下信息
[ 543.902] (II) modeset(0): EDID (in hex):
[ 543.902] (II) modeset(0): 00ffffffffffff0030e4960600000000
[ 543.902] (II) modeset(0): 001e0104a51d127807ed85a7544c9c26
[ 543.902] (II) modeset(0): 0e505400000001010101010101010101
[ 543.902] (II) modeset(0): 010101010101353c80a070b023403020
[ 543.902] (II) modeset(0): 36001eb31000001a0000000000000000
[ 543.902] (II) modeset(0): 00000000000000000000000000fe004c
[ 543.902] (II) modeset(0): 4720446973706c61790a2020000000fe
[ 543.902] (II) modeset(0): 004c503133335755312d5350423100ec
3.2 没有EDID时使用内核自带的显示参数
当内核无法获取EDID时,内核会使用drm_dmt_modes[]中的显示参数(1024x768)
内核代码中自带了一组显示参数,通过数组drm_dmt_modes[]记录。
/*
* Autogenerated from the DMT spec.
* This table is copied from xfree86/modes/xf86EdidModes.c.
*/
static const struct drm_display_mode drm_dmt_modes[] = {
/* 0x01 - 640x350@85Hz */
{ DRM_MODE("640x350", DRM_MODE_TYPE_DRIVER, 31500, 640, 672,
736, 832, 0, 350, 382, 385, 445, 0,
DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC) },
/* 0x02 - 640x400@85Hz */
{ DRM_MODE("640x400", DRM_MODE_TYPE_DRIVER, 31500, 640, 672,
736, 832, 0, 400, 401, 404, 445, 0,
DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_PVSYNC) },
/* 0x03 - 720x400@85Hz */
{ DRM_MODE("720x400", DRM_MODE_TYPE_DRIVER, 35500, 720, 756,
828, 936, 0, 400, 401, 404, 446, 0,
DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_PVSYNC) },
......
3.3 通过启动参数(video=XXX)获取显示参数
可以在内核启动参数里通过 "video=XXX" 指定显示的参数,例如:video=VGA-1:1920x1080M@60
解析 "video=XXX" 的函数是
drm_mode_parse_command_line_for_connector();
-> drm_mode_parse_cmdline_extra();
-> case 'i': mode->interlace = true;
-> case 'm': mode->margins = true;
-> case 'D': mode->force = DRM_FORCE_ON 或者 DRM_FORCE_ON_DIGITAL;
-> case 'd': mode->force = DRM_FORCE_OFF;
-> case 'e': mode->force = DRM_FORCE_ON;
-> drm_mode_parse_cmdline_res_mode();
-> xres = simple_strtol(str, &end_ptr, 10);
-> yres = simple_strtol(str, &end_ptr, 10);
-> case 'M': cvt = true;
-> case 'R': rb = true;
-> mode->xres = xres;
-> mode->yres = yres;
-> mode->cvt = cvt;
-> mode->rb = rb;
3.4 通过设备树获取显示参数
3.4.1 设备树示例
//arch/arm/boot/dts/wm8650-mid.dts
&fb {
bits-per-pixel = <16>;
display-timings {
native-mode = <&timing0>;
timing0: 800x480 {
clock-frequency = <0>; /* unused but required */
hactive = <800>;
vactive = <480>;
hfront-porch = <40>;
hback-porch = <88>;
hsync-len = <0>;
vback-porch = <32>;
vfront-porch = <11>;
vsync-len = <1>;
};
};
};
3.4.2 解析设备树的函数
of_get_drm_display_mode();
-> of_get_videomode();
-> of_get_display_timings();
-> timings_np = of_get_child_by_name(np, "display-timings");
-> entry = of_parse_phandle(timings_np, "native-mode", 0);
-> of_parse_display_timing();
-> parse_timing_property(np, "hback-porch", &dt->hback_porch);
-> parse_timing_property(np, "hfront-porch", &dt->hfront_porch);
-> parse_timing_property(np, "hactive", &dt->hactive);
-> parse_timing_property(np, "hsync-len", &dt->hsync_len);
......
4 整体代码流程
内核中经常使用drm_helper_probe_single_connector_modes()函数来获取显示参数
drm_helper_probe_single_connector_modes();
-> (*connector_funcs->get_modes)(connector); //显卡驱动通过I2C总线读取EDID
-> drm_add_override_edid_modes(); //获取debugfs中 和 firmware中的EDID
-> drm_add_modes_noedid(connector, 1024, 768); //使用内核自带的显示参数(1024x768)
-> drm_helper_probe_add_cmdline_mode(connector); //使用启动参数video=XXX指定的显示参数
-> drm_connector_list_update(connector);
5 查看connector当前使用的显示参数
方法一、/sys/class/drm/card0/<connector>/modes
方法二、执行命令:xrandr