注:文章都是通过阅读各位前辈总结的资料 Android 11.0 && Linux(Kernel 4.19)Rockchip平台源码、加上自己的思考分析总结出来的,其中难免有理解不对的地方,欢迎大家批评指正。文章为个人学习、研究、欣赏之用,图文内容整理自互联网,如有侵权,请联系删除(◕‿◕),转载请注明出处(©Rockchip ©Android @Linux 版权所有),谢谢。

(==文章基于 Kernel-4.19==)&&(==文章基于 Android 11.0==)

【zhoujinjian.com博客原图链接】

【开发板 RockPi4bPlusV1.6】

【开发板 RockPi4bPlusV1.6 Android 11.0 && Linux(Kernel 4.19)源码链接】:(repo init -u https://github.com/radxa/manifests.git -b Android11_Radxa_rk11.1 -m rockchip-r-release.xml)

【开发板 RockPi4bPlusV1.6 Android 11.0 && Linux(Kernel 4.19)编译指南】

正是由于前人(各位大神)的分析和总结,帮助我节约了大量的时间和精力,特别感谢,由于不喜欢图片水印,去除了水印,敬请谅解!!!

本文转自Rockchip RK3399 - DRM HDMI驱动程序 ,如有侵权,请联系删除。


开发板 :ROCK Pi 4B+开发板
eMMC32GB
LPDDR44GB
显示屏 :7英寸HDMI接口显示屏
u-boot2017.09
linux4.19


Rockchip RK3399 - DRM驱动程序》我们已经介绍过了,RK3399有两个VOP,均可以支持HDMIeDPDPMIPI DSI0MIPI DSI1显示接口,本节我们选择HDMI作为分析的对象。

在《Rockchip RK3399 - DRM HDMI介绍》我们已经对HDMI协议进行了详细的介绍,本节我们选择DRM HDMI驱动程序作为分析的对象。

这里我们介绍一下Rochchip DRM驱动中与hdmi相关的实现,具体实现文件:

  • drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
  • drivers/gpu/drm/rockchip/inno_hdmi.c
  • drivers/gpu/drm/bridge/synopsys/

由于Rockchip采用了SynopsysDesignWare HDMI IP解决方案,因此hdmi驱动的核心实现是位于drivers/gpu/drm/bridge/synopsys/目录下的,而Rockchip仅仅是对其进行一层封装。

在介绍hdmi驱动之前,我们首先思考一个问题,假设我们自己是一个画家,现在我手里有一个笔、还有一张纸,然后我打算在纸上绘画一个卡通人物,接下来我们会怎么做呢?

  • 首先我们需要对我们绘画使用的纸的尺寸有一个了解;

    • 如果是A3上的图纸,那么我就会在大脑里构思一个A3大小的卡通人物;
    • 如果是A4,那么我就会在大脑里构思一个A4大小的卡通人物;
    • 总之我们的目的是要让卡通人物占满整张纸;
  • 接下来我们就会使用不同颜色的画笔开始在纸上绘画了,而我们的绘画过程呢,就是将大脑中构思的卡通人物按照从左到右、从上到下一笔一笔的勾勒出来;

同样的,类比到DRM显示子系统中;

  • hdmi显示器等价于绘画的纸:LCD驱动器会将接收到的数据在显示器上显示出来;
  • RK3399 crtc等价于画笔:crtcframebuffer中读取待显示的图像,并按照响应的格式输出给encoder
    • 对于crtc来说,其承担了各种时序参数配置的重任;
    • encoder实际上就是进行的编码工作,对于HDMI来说来用的就是TMDS协议,经过编码之后的数据就可以通过HDMI线缆输出到HDMI显示器了;这个输出的过程就类似于我们绘画的过程:从左到右、从上到下;
  • framebuffer等价于大脑中构思的卡通人物:framebuffer就是一块驱动和应用层都能访问的内存,这块内存中描述了使用的显示器的分辨率、色彩描述(RGB24 ,I420 ,YUUV等等)、以及要真正要显示的内容所在的虚拟地址(通过GEM分配物理内存);

一、设备树配置

1.1 hdmi设备节点

设备节点vopb下的子节点vopb_out_hdmi通过hdmi_in_vopb(由remote-endpoint属性指定)和hdmi显示接口组成一个连接通路;

设备节点vopl下的子节点vopl_out_hdmi通过hdmi_in_vopl(由remote-endpoint属性指定)和hdmi显示接口组成一个连接通路;

hdmi设备节点定义在arch/arm64/boot/dts/rockchip/rk3399.dtsi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
hdmi: hdmi@ff940000 {
compatible = "rockchip,rk3399-dw-hdmi";
reg = <0x0 0xff940000 0x0 0x20000>;
interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH 0>;
clocks = <&cru PCLK_HDMI_CTRL>,
<&cru SCLK_HDMI_SFR>,
<&cru SCLK_HDMI_CEC>,
<&cru PCLK_VIO_GRF>,
<&cru PLL_VPLL>;
clock-names = "iahb", "isfr", "cec", "grf", "ref";
power-domains = <&power RK3399_PD_HDCP>;
reg-io-width = <4>;
rockchip,grf = <&grf>;
#sound-dai-cells = <0>;
status = "disabled";

ports {
hdmi_in: port {
#address-cells = <1>;
#size-cells = <0>;

hdmi_in_vopb: endpoint@0 {
reg = <0>;
remote-endpoint = <&vopb_out_hdmi>;
};
hdmi_in_vopl: endpoint@1 {
reg = <1>;
remote-endpoint = <&vopl_out_hdmi>;
};
};
};
};

其中:

  • 子节点ports:包含2个input endpoint,分别连接到voplvopb;也就是在rk3399上,hdmi可以和vopl(只支持 2K)、vopb(支持 4K)连接;

因此可以得到有2条通路:

  • vopb_out_hdmi —> hdmi_in_vopb
  • vopl_out_hdmi —> hdmi_in_vopl

需要注意的是:

  • 两个vop可以分别与两个显示接口绑定(一个显示接口只能和一个vop绑定),且可以相互交换:
  • ⼀个显⽰接口在同⼀个时刻只能和⼀个vop连接,所以在具体的板级配置中,需要设备树中把要使⽤的通路打开,把不使⽤的通路设置为disabled状态。

1.2 启用hdmi

如果我们希望hdmi连接在vopb上,则需要在arch/arm64/boot/dts/rockchip/rk3399-evb.dts中为以下节点新增属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
&i2c7 {
status = "okay";
};

# 使能显示子系统
&display_subsystem {
status = "okay";
};

# 使能vopb
&vopb {
status = "okay";
};

&vopb_mmu {
status = "okay";
};

# 使能hdmi
&hdmi {
ddc-i2c-bus = <&i2c7>;
pinctrl-names = "default";
pinctrl-0 = <&hdmi_cec>;
status = "okay";
};

# hdmi绑定到vopb
&hdmi_in_vopb{
status = "okay";
};

# 禁止hdmi绑定到vopl
&hdmi_in_vopl{
status = "disabled";
};

二、Platform驱动

2.1 模块入口函数

rockchip_drm_init函数中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static int __init rockchip_drm_init(void)
{
int ret;


if (drm_firmware_drivers_only())
return -ENODEV;

// 1. 根据配置来决定是否添加xxx_xxx_driver到数组rockchip_sub_drivers
num_rockchip_sub_drivers = 0;
ADD_ROCKCHIP_SUB_DRIVER(dw_hdmi_rockchip_pltfm_driver,CONFIG_ROCKCHIP_DW_HDMI);
......

// 2. 注册多个platform driver
ret = platform_register_drivers(rockchip_sub_drivers,
num_rockchip_sub_drivers);
if (ret)
return ret;

// 3. 注册rockchip_drm_platform_driver
ret = platform_driver_register(&rockchip_drm_platform_driver);
if (ret)
goto err_unreg_drivers;

return 0;
......
}

其中:

1
ADD_ROCKCHIP_SUB_DRIVER(dw_hdmi_rockchip_pltfm_driver,CONFIG_ROCKCHIP_DW_HDMI);

会将vop_platform_driver保存到rockchip_sub_drivers数组中。

并调用platform_register_drivers遍历rockchip_sub_drivers数组,多次调用platform_driver_register注册platform driver

2.2 dw_hdmi_rockchip_pltfm_driver

dw_hdmi_rockchip_pltfm_driver定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c

1
2
3
4
5
6
7
8
9
struct platform_driver dw_hdmi_rockchip_pltfm_driver = {
.probe = dw_hdmi_rockchip_probe,
.remove = dw_hdmi_rockchip_remove,
.driver = {
.name = "dwhdmi-rockchip",
.pm = &dw_hdmi_rockchip_pm,
.of_match_table = dw_hdmi_rockchip_dt_ids, // 用于设备树匹配
},
};
2.2.1 of_match_table

其中of_match_table用于设备树匹配,匹配设备树中compatible = "rockchip,rk3399-dw-hdmi"的设备节点;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static const struct dw_hdmi_plat_data rk3399_hdmi_drv_data = {
.mode_valid = dw_hdmi_rockchip_mode_valid,
.mpll_cfg = rockchip_mpll_cfg,
.cur_ctr = rockchip_cur_ctr,
.phy_config = rockchip_phy_config,
.phy_data = &rk3399_chip_data,
.use_drm_infoframe = true,
};

static const struct of_device_id dw_hdmi_rockchip_dt_ids[] = {
{ .compatible = "rockchip,rk3228-dw-hdmi",
.data = &rk3228_hdmi_drv_data
},
{ .compatible = "rockchip,rk3288-dw-hdmi",
.data = &rk3288_hdmi_drv_data
},
{ .compatible = "rockchip,rk3328-dw-hdmi",
.data = &rk3328_hdmi_drv_data
},
{ .compatible = "rockchip,rk3399-dw-hdmi",
.data = &rk3399_hdmi_drv_data
},
{ .compatible = "rockchip,rk3568-dw-hdmi",
.data = &rk3568_hdmi_drv_data
},
{},
};
2.2.2 dw_hdmi_rockchip_probe

platform总线设备驱动模型中,我们知道当内核中有platform设备和platform驱动匹配,会调用到platform_driver里的成员.probe,在这里就是dw_hdmi_rockchip_probe函数;

1
2
3
4
5
6
7
8
9
10
static const struct component_ops dw_hdmi_rockchip_ops = {
.bind = dw_hdmi_rockchip_bind,
.unbind = dw_hdmi_rockchip_unbind,
};


static int dw_hdmi_rockchip_probe(struct platform_device *pdev)
{
return component_add(&pdev->dev, &dw_hdmi_rockchip_ops);
}

这里代码很简单,就是为设备pdev->dev向系统注册一个component,其中组件可执行的初始化操作被设置为了dw_hdmi_rockchip_ops,我们需要重点关注bind函数的实现,这个我们单独小节介绍。

三、HDMI数据结构

hdmi相关的数据结构分为两部分:

  • DesignWare hdmi相关驱动定义:比如struct dw_hdmistruct dw_hdmi_plat_data
  • Rochchip hdmi相关驱动定义:比如struct rockchip_hdmistruct rockchip_hdmi_chip_data

3.1 DesignWare hdmi

3.1.1 struct dw_hdmi

struct dw_hdmi定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
struct dw_hdmi {
struct drm_connector connector;
struct drm_bridge bridge;
struct drm_bridge *next_bridge;

unsigned int version;

struct platform_device *audio;
struct platform_device *cec;
struct device *dev;
struct clk *isfr_clk;
struct clk *iahb_clk;
struct clk *cec_clk;
struct dw_hdmi_i2c *i2c;

struct hdmi_data_info hdmi_data;
const struct dw_hdmi_plat_data *plat_data;

int vic;

u8 edid[HDMI_EDID_LEN];

struct {
const struct dw_hdmi_phy_ops *ops;
const char *name;
void *data;
bool enabled;
} phy;

struct drm_display_mode previous_mode;

struct i2c_adapter *ddc;
void __iomem *regs;
bool sink_is_hdmi;
bool sink_has_audio;

struct pinctrl *pinctrl;
struct pinctrl_state *default_state;
struct pinctrl_state *unwedge_state;

struct mutex mutex; /* for state below and previous_mode */
enum drm_connector_force force; /* mutex-protected force state */
struct drm_connector *curr_conn;/* current connector (only valid when !disabled) */
bool disabled; /* DRM has disabled our bridge */
bool bridge_is_on; /* indicates the bridge is on */
bool rxsense; /* rxsense state */
u8 phy_mask; /* desired phy int mask settings */
u8 mc_clkdis; /* clock disable register */

spinlock_t audio_lock;
struct mutex audio_mutex;
unsigned int sample_non_pcm;
unsigned int sample_width;
unsigned int sample_rate;
unsigned int channels;
unsigned int audio_cts;
unsigned int audio_n;
bool audio_enable;

unsigned int reg_shift;
struct regmap *regm;
void (*enable_audio)(struct dw_hdmi *hdmi);
void (*disable_audio)(struct dw_hdmi *hdmi);

struct mutex cec_notifier_mutex;
struct cec_notifier *cec_notifier;

hdmi_codec_plugged_cb plugged_cb;
struct device *codec_dev;
enum drm_connector_status last_connector_result;
};

其中:

  • connector:连接器;
  • bridge:桥接设备,一般用于注册encoder后面另外再接的转换芯片;
  • audio:音频platform device
  • cecCEC platform device
  • devhdmi设备;
  • isfr_clkiahb_clkcec_clkhdmi相关的时钟;
  • plat_datadw hdmi平台数据;
  • ddc:存储DDC通道使用的I2C总线适配器;
  • edid:存放edid信息;
  • regshdmi相关寄存器基址的虚拟地址;
  • pinctrldefault_stateunwedge_state:引脚状态配置信息;
  • reg_shift:寄存器地址偏移;
  • sample_width:音频采样位数;
  • sample_rate:音频采样率;
  • channels:通道数;
  • regm:寄存器映射,用于通过regmap模型访问hdmi相关寄存器;
  • enable_audio:启用音频回调函数;
  • disable_audio:禁用音频回调函数;
3.1.2 struct dw_hdmi_plat_data

struct dw_hdmi_plat_data定义在include/drm/bridge/dw_hdmi.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct dw_hdmi_plat_data {
struct regmap *regm;

unsigned int output_port;

unsigned long input_bus_encoding;
bool use_drm_infoframe;
bool ycbcr_420_allowed;

/*
* Private data passed to all the .mode_valid() and .configure_phy()
* callback functions.
*/
void *priv_data;

/* Platform-specific mode validation (optional). */
enum drm_mode_status (*mode_valid)(struct dw_hdmi *hdmi, void *data,
const struct drm_display_info *info,
const struct drm_display_mode *mode);

/* Platform-specific audio enable/disable (optional) */
void (*enable_audio)(struct dw_hdmi *hdmi, int channel,
int width, int rate, int non_pcm);
void (*disable_audio)(struct dw_hdmi *hdmi);

/* Vendor PHY support */
const struct dw_hdmi_phy_ops *phy_ops;
const char *phy_name;
void *phy_data;
unsigned int phy_force_vendor;

/* Synopsys PHY support */
const struct dw_hdmi_mpll_config *mpll_cfg;
const struct dw_hdmi_curr_ctrl *cur_ctr;
const struct dw_hdmi_phy_config *phy_config;
int (*configure_phy)(struct dw_hdmi *hdmi, void *data,
unsigned long mpixelclock);

unsigned int disable_cec : 1;
};

(1) 结构体struct dw_hdmi_mpll_config定义在如下:

1
2
3
4
5
6
7
struct dw_hdmi_mpll_config {
unsigned long mpixelclock;
struct {
u16 cpce;
u16 gmp;
} res[DW_HDMI_RES_MAX];
};

各项参数说明如下:

  • mpixelclock:像素时钟;
  • cpceOPMODE_PLLCFG寄存器值;
  • gmpPLLGMPCTRL寄存器值;
3.1.3 struct edid

linux使用struct edid描述edid主块信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
struct edid {
u8 header[8]; // 0x00~0x07
/* Vendor & product info */
u8 mfg_id[2]; // 0x08~0x09
u8 prod_code[2]; // 0x0A~0x0B
u32 serial; /* FIXME: byte order,0x0C~0X0F */
u8 mfg_week; // 0X10
u8 mfg_year; // 0x11
/* EDID version */
u8 version; // 0x12
u8 revision; // 0x13
/* Display info: */
u8 input; // 0x14
u8 width_cm; // 0x15
u8 height_cm; // 0x16
u8 gamma; // 0x17
u8 features; // 0x18
/* Color characteristics */
u8 red_green_lo; // 0x19
u8 blue_white_lo; // 0x1A
u8 red_x; // 0x1B
u8 red_y; // 0x1c
u8 green_x; // 0x1D
u8 green_y; // 0x1E
u8 blue_x; // 0x1F
u8 blue_y; // 0x20
u8 white_x; // 0x21
u8 white_y; // 0x22
/* Est. timings and mfg rsvd timings*/
struct est_timings established_timings; // 0x23~0x25
/* Standard timings 1-8*/
struct std_timing standard_timings[8]; // 0x26~0X35
/* Detailing timings 1-4 */
struct detailed_timing detailed_timings[4]; // 0X36~0X7D
/* Number of 128 byte ext. blocks */
u8 extensions; // 0x7E
/* Checksum */
u8 checksum; // 0X7F
} __attribute__((packed));

该数据结构保存edit主块128字节的信息,具体参考《Rockchip RK3399 - DRM HDMI介绍》。

3.1.4 struct est_timings

edidEstablished Timings信息在linux中使用struct est_timings表示;

1
2
3
4
5
struct est_timings {
u8 t1;
u8 t2;
u8 mfg_rsvd;
} __attribute__((packed));
3.1.5 struct std_timing

edidStandard Timings信息在linux中使用struct std_timing表示;

1
2
3
4
5
6
7
8
9
10
11
12
/* 00=16:10, 01=4:3, 10=5:4, 11=16:9 */
#define EDID_TIMING_ASPECT_SHIFT 6
#define EDID_TIMING_ASPECT_MASK (0x3 << EDID_TIMING_ASPECT_SHIFT)

/* need to add 60 */
#define EDID_TIMING_VFREQ_SHIFT 0
#define EDID_TIMING_VFREQ_MASK (0x3f << EDID_TIMING_VFREQ_SHIFT)

struct std_timing {
u8 hsize; /* need to multiply by 8 then add 248 */
u8 vfreq_aspect;
} __attribute__((packed));
3.1.6 struct detailed_timing

edid中的Detailed Timings,它分为4个块(Block),每个块占用18个字节,一共72个字节。

每个块既可以是一个时序说明(Timing Descriptor)也可以是一个显示器描述符(Monitor Descriptor)。

struct detailed_timing就是用来描述每一个Detailed Timing,定义在include/drm/drm_edid.h

1
2
3
4
5
6
7
struct detailed_timing {
__le16 pixel_clock; /* need to multiply by 10 KHz */
union {
struct detailed_pixel_timing pixel_data; // Timing Descriptor
struct detailed_non_pixel other_data; // Monitor Descriptor
} __attribute__((packed)) data;
} __attribute__((packed));

这里需要注意的是pixel_clock,如果edid信息中存放的值为0xBCD3 =48339,在drm_mode_detailed函数中pixel_clock的值会被赋值为48339*10=483390

实际像素时钟频率为 48339*10000=483390000HzTMDS时钟频率为483390000Hz*10=4833900KHz 所以pixel_clock的单位为10kHZ

(1) struct detailed_pixel_timing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* If detailed data is pixel timing */
struct detailed_pixel_timing {
u8 hactive_lo;
u8 hblank_lo;
u8 hactive_hblank_hi;
u8 vactive_lo;
u8 vblank_lo;
u8 vactive_vblank_hi;
u8 hsync_offset_lo;
u8 hsync_pulse_width_lo;
u8 vsync_offset_pulse_width_lo;
u8 hsync_vsync_offset_pulse_width_hi;
u8 width_mm_lo;
u8 height_mm_lo;
u8 width_height_mm_hi;
u8 hborder;
u8 vborder;
u8 misc;
} __attribute__((packed));

(2) struct detailed_non_pixel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct detailed_non_pixel {
u8 pad1; /* 值为0,标识该block被使用 */
u8 type; /* ff=serial, fe=string, fd=monitor range, fc=monitor name
fb=color point data, fa=standard timing data,
f9=undefined, f8=mfg. reserved */
u8 pad2;
union {
struct detailed_data_string str;
struct detailed_data_monitor_range range;
struct detailed_data_wpindex color;
struct std_timing timings[6]; // type=EDID_DETAIL_STD_MODES=0xfa时生效
struct cvt_timing cvt[4]; // type=EDID_DETAIL_CVT_3BYTE=0xf8时生效
} __attribute__((packed)) data;
} __attribute__((packed));

3.2 Rockchip hdmi

3.2.1 struct rockchip_hdmi

struct rockchip_hdmi定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c,这是Rockchip平台定义的hdmi结构体,其对struct dw_hdmi进行了扩充,用于表示Rockchip平台上的hdmi设备。

1
2
3
4
5
6
7
8
9
10
11
12
struct rockchip_hdmi {
struct device *dev;
struct regmap *regmap;
struct rockchip_encoder encoder;
const struct rockchip_hdmi_chip_data *chip_data;
struct clk *ref_clk;
struct clk *grf_clk;
struct dw_hdmi *hdmi;
struct regulator *avdd_0v9;
struct regulator *avdd_1v8;
struct phy *phy;
};

其中:

  • dev:指向设备的struct device *dev指针;
  • regmap:指向寄存器映射的struct regmap *regmap指针;
  • encoder:指向Rockchip平台定义的encoder指针;
  • chip_data:指向Rockchip平台定义的hdmi data指针;
  • ref_clkref时钟;
  • grf_clkgrf时钟;
  • avdd_0v90.9V稳压器;
  • avdd_1v81.8V稳压器;
  • phy:指向HDMI PHYstruct phy *phy指针;
3.2.2 struct rockchip_hdmi_chip_data

struct rockchip_hdmi_chip_data定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c,用于描述不同型号的Rockchip芯片的HDMI接口配置信息;

1
2
3
4
5
6
7
8
9
10
11
/**
* struct rockchip_hdmi_chip_data - splite the grf setting of kind of chips
* @lcdsel_grf_reg: grf register offset of lcdc select
* @lcdsel_big: reg value of selecting vop big for HDMI
* @lcdsel_lit: reg value of selecting vop little for HDMI
*/
struct rockchip_hdmi_chip_data {
int lcdsel_grf_reg;
u32 lcdsel_big;
u32 lcdsel_lit;
};

其中:

  • lcdsel_grf_reg:表示GRF寄存器中LCD控制器选择寄存器的偏移量,该寄存器用于选择使用哪个vop进行HDMI输出;
  • u32 lcdsel_big:表示在GRF寄存器中lcdsel_grf_reg偏移位置处设置的值,用于选择vopb进行HDMI输出;
  • u32 lcdsel_lit:表示在GRF寄存器中lcdsel_grf_reg偏移位置处设置的值,用于选择vopl进行HDMI输出。
3.2.3 struct rockchip_encoder

struct rockchip_encoder定义在drivers/gpu/drm/rockchip/rockchip_drm_drv.h,这是Rockchip平台定义的encoder结构体,用于表示Rockchip平台上的编码器设备。其对struct drm_encoder进行了扩充;

1
2
3
4
struct rockchip_encoder {
int crtc_endpoint_id;
struct drm_encoder encoder;
};

其中:

  • crtc_endpoint_id:表示crtc端点的ID,用于标识该编码器设备连接到哪个vop
  • drm_encoder encoder:表示DRM encoder的相关信息;

四、dw_hdmi_rockchip_bind

dw_hdmi_rockchip_bind函数定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c,该函数的代码虽然看着那么多,实际上主要就做了以下几件事;

  • 解析hdmi设备节点,涉及到clocksrockchip,grfavdd-0v9avdd-1v8、以及endpoint子节点;
  • 初始化encoder
  • 构造dw_hdmi_bind函数需要的参数,尤其是第三个参数rk3399_hdmi_drv_data,最后调用dw_hdmi_bind进入到DesignWare hdmi驱动;

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
static int dw_hdmi_rockchip_bind(struct device *dev, struct device *master,
void *data)
{
struct platform_device *pdev = to_platform_device(dev);
struct dw_hdmi_plat_data *plat_data;
const struct of_device_id *match;
struct drm_device *drm = data;
struct drm_encoder *encoder;
struct rockchip_hdmi *hdmi;
int ret;

if (!pdev->dev.of_node)
return -ENODEV;

// 动态分配内存,指向struct rockchip_hdmi
hdmi = devm_kzalloc(&pdev->dev, sizeof(*hdmi), GFP_KERNEL);
if (!hdmi)
return -ENOMEM;

// 根据设备的设备节点和匹配表进行匹配,并返回匹配项
match = of_match_node(dw_hdmi_rockchip_dt_ids, pdev->dev.of_node);

// 分配内存,指向一个struct dw_hdmi_plat_data,并复制rk3399_hdmi_drv_data数据
plat_data = devm_kmemdup(&pdev->dev, match->data,
sizeof(*plat_data), GFP_KERNEL);
if (!plat_data)
return -ENOMEM;

// 设置device设备
hdmi->dev = &pdev->dev;

// 设置数据
hdmi->chip_data = plat_data->phy_data;
plat_data->phy_data = hdmi;
encoder = &hdmi->encoder.encoder;

encoder->possible_crtcs = drm_of_find_possible_crtcs(drm, dev->of_node);

// 获取设备节点hdmi子节点hdmi_in_vopb的属性remote-endpoint指定vopb_out_hdmi节点的reg的值,用来初始化encoder->crtc_endpoint_id
rockchip_drm_encoder_set_crtc_endpoint_id(&hdmi->encoder,
dev->of_node, 0, 0);

/*
* If we failed to find the CRTC(s) which this encoder is
* supposed to be connected to, it's because the CRTC has
* not been registered yet. Defer probing, and hope that
* the required CRTC is added later.
*/
if (encoder->possible_crtcs == 0)
return -EPROBE_DEFER;

// 解析hdmi设备节点,比如clocks 、rockchip,grf、avdd-0v9、avdd-1v8等属性;
ret = rockchip_hdmi_parse_dt(hdmi);
if (ret) {
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(hdmi->dev, "Unable to parse OF data\n");
return ret;
}

// 查找并获取一个可选的 PHY(物理层设备)的引用
hdmi->phy = devm_phy_optional_get(dev, "hdmi");
if (IS_ERR(hdmi->phy)) {
ret = PTR_ERR(hdmi->phy);
if (ret != -EPROBE_DEFER)
DRM_DEV_ERROR(hdmi->dev, "failed to get phy\n");
return ret;
}

// 使能AVDD_0V9电源
ret = regulator_enable(hdmi->avdd_0v9);
if (ret) {
DRM_DEV_ERROR(hdmi->dev, "failed to enable avdd0v9: %d\n", ret);
goto err_avdd_0v9;
}

// 使能AVDD_1V8电源
ret = regulator_enable(hdmi->avdd_1v8);
if (ret) {
DRM_DEV_ERROR(hdmi->dev, "failed to enable avdd1v8: %d\n", ret);
goto err_avdd_1v8;
}

// 准备和使能时钟
ret = clk_prepare_enable(hdmi->ref_clk);
if (ret) {
DRM_DEV_ERROR(hdmi->dev, "Failed to enable HDMI reference clock: %d\n",
ret);
goto err_clk;
}

// 不匹配
if (hdmi->chip_data == &rk3568_chip_data) {
regmap_write(hdmi->regmap, RK3568_GRF_VO_CON1,
HIWORD_UPDATE(RK3568_HDMI_SDAIN_MSK |
RK3568_HDMI_SCLIN_MSK,
RK3568_HDMI_SDAIN_MSK |
RK3568_HDMI_SCLIN_MSK));
}

// 设置encoder的辅助函数helper_private为dw_hdmi_rockchip_encoder_helper_funcs
drm_encoder_helper_add(encoder, &dw_hdmi_rockchip_encoder_helper_funcs);

// encoder初始化
drm_simple_encoder_init(drm, encoder, DRM_MODE_ENCODER_TMDS);

// 设置驱动私有数据 pdev->dev.driver_data = hdmi
platform_set_drvdata(pdev, hdmi);

// 初始化HDMI接口
hdmi->hdmi = dw_hdmi_bind(pdev, encoder, plat_data);

/*
* If dw_hdmi_bind() fails we'll never call dw_hdmi_unbind(),
* which would have called the encoder cleanup. Do it manually.
*/
if (IS_ERR(hdmi->hdmi)) {
ret = PTR_ERR(hdmi->hdmi);
goto err_bind;
}

return 0;

err_bind:
drm_encoder_cleanup(encoder);
clk_disable_unprepare(hdmi->ref_clk);
err_clk:
regulator_disable(hdmi->avdd_1v8);
err_avdd_1v8:
regulator_disable(hdmi->avdd_0v9);
err_avdd_0v9:
return ret;
}

4.1 drm_of_find_possible_crtcs

drm_of_find_possible_crtcs定义在drivers/gpu/drm/drm_of.c;这个函数的作用是基于设备树中的信息,确定特定encoder端口可能连接的CRTCs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* drm_of_find_possible_crtcs - find the possible CRTCs for an encoder port
* @dev: DRM device
* @port: encoder port to scan for endpoints
*
* Scan all endpoints attached to a port, locate their attached CRTCs,
* and generate the DRM mask of CRTCs which may be attached to this
* encoder.
*
* See Documentation/devicetree/bindings/graph.txt for the bindings.
*/
uint32_t drm_of_find_possible_crtcs(struct drm_device *dev,
struct device_node *port) // hdmi设备节点
{
struct device_node *remote_port, *ep;
uint32_t possible_crtcs = 0;

// 遍历port结点下的每个endpoint节点,即hdmi_in_vopb、hdmi_in_vopl设备节点
for_each_endpoint_of_node(port, ep) {
// 获取hdmi_in_vopb节点remote-endpoint属性指定的设备节点vopb_out_hdmi
remote_port = of_graph_get_remote_port(ep);
// 无效节点,进入
if (!remote_port) {
of_node_put(ep);
return 0;
}

// 下文介绍
possible_crtcs |= drm_of_crtc_port_mask(dev, remote_port);

of_node_put(remote_port);
}

return possible_crtcs;
}

hdmi节点为例,其有两个endpoint子节点;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hdmi: hdmi@ff940000 {
......
ports {
hdmi_in: port {
#address-cells = <1>;
#size-cells = <0>;

hdmi_in_vopb: endpoint@0 {
reg = <0>;
remote-endpoint = <&vopb_out_hdmi>;
};
hdmi_in_vopl: endpoint@1 {
reg = <1>;
remote-endpoint = <&vopl_out_hdmi>;
};
};
};
};

第一次遍历时,drm_of_crtc_port_mask参数一传入的是drm设备,参数二传入的是vopb_out_hdmi设备节点。

drm_of_crtc_port_mask定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* DOC: overview
*
* A set of helper functions to aid DRM drivers in parsing standard DT
* properties.
*/

/**
* drm_of_crtc_port_mask - find the mask of a registered CRTC by port OF node
* @dev: DRM device
* @port: port OF node
*
* Given a port OF node, return the possible mask of the corresponding
* CRTC within a device's list of CRTCs. Returns zero if not found.
*/
uint32_t drm_of_crtc_port_mask(struct drm_device *dev,
struct device_node *port) // vopb_out_hdmi设备节点
{
unsigned int index = 0;
struct drm_crtc *tmp;

// list_for_each_entry(tmp, &(dev)->mode_config.crtc_list, head),遍历crtc链表,赋值给tmp
// 因此这里会依次遍历到vopb、vopl对应的crtc,其中vopb对应的crtc->port被设置为vopb_out设备节点
drm_for_each_crtc(tmp, dev) {
if (tmp->port == port) // 两次循环都不匹配
return 1 << index;

index++;
}

return 0;
}

4.2 rockchip_drm_encoder_set_crtc_endpoint_id

rockchip_drm_encoder_set_crtc_endpoint_id定义在drivers/gpu/drm/rockchip/rockchip_drm_drv.c;函数第二个传入的是hdmi设备节点,第三个参数port传入0,第四个参数reg同样传入0。

这段代码首先获取hdmi设备节点下reg为0的endpoint设备节点,然后获取该节点remote-endpoint属性指定的设备节点的reg属性的值,并将其赋值给rkencoder->crtc_endpoint_id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/*
* Get the endpoint id of the remote endpoint of the given encoder. This
* information is used by the VOP2 driver to identify the encoder.
*
* @rkencoder: The encoder to get the remote endpoint id from
* @np: The encoder device node
* @port: The number of the port leading to the VOP2
* @reg: The endpoint number leading to the VOP2
*/
int rockchip_drm_encoder_set_crtc_endpoint_id(struct rockchip_encoder *rkencoder,
struct device_node *np, int port, int reg)
{
struct of_endpoint ep;
struct device_node *en, *ren;
int ret;

// 通过遍历父设备节点的所有子节点(端点节点)来查找符合指定port=0和reg=0的端点节点,这里返回的是hdmi_in_vopb设备节点
en = of_graph_get_endpoint_by_regs(np, port, reg);
if (!en)
return -ENOENT;

// 获取en设备节点remote-endpoin属性指定的设备节点,即vopb_out_hdmi设备节点
ren = of_graph_get_remote_endpoint(en);
if (!ren)
return -ENOENT;

// 解析vopb_out_hdmi设备节点的属性,并将解析结果存储到ep
ret = of_graph_parse_endpoint(ren, &ep);
if (ret)
return ret;
// 由于vopb_out_hdmi设备节点的reg属性=2,所以此处赋值为2
rkencoder->crtc_endpoint_id = ep.id;

return 0;
}
4.2.1 of_graph_get_endpoint_by_regs

of_graph_get_endpoint_by_regs定义在drivers/of/property.c,其作用就是通过遍历父设备节点的所有子节点(端点节点)来查找符合指定 port_regreg 标识符的端点节点;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* of_graph_get_endpoint_by_regs() - get endpoint node of specific identifiers
* @parent: pointer to the parent device node
* @port_reg: identifier (value of reg property) of the parent port node
* @reg: identifier (value of reg property) of the endpoint node
*
* Return: An 'endpoint' node pointer which is identified by reg and at the same
* is the child of a port node identified by port_reg. reg and port_reg are
* ignored when they are -1. Use of_node_put() on the pointer when done.
*/
struct device_node *of_graph_get_endpoint_by_regs(
const struct device_node *parent, int port_reg, int reg)
{
struct of_endpoint endpoint;
struct device_node *node = NULL;
// 遍历parent结点下的每个endpoint结点
for_each_endpoint_of_node(parent, node) {
// 解析端点的信息,并将结果存储在endpoint
of_graph_parse_endpoint(node, &endpoint);
// 对比传入的port_reg和reg参数与当前端点节点的属性值,如果匹配则返回该端点节点的指针
if (((port_reg == -1) || (endpoint.port == port_reg)) &&
((reg == -1) || (endpoint.id == reg)))
return node;
}

return NULL;
}

比如我们的hdmi节点,当调用of_graph_get_endpoint_by_regs(np,0,0)返回的就是hdmi_in_vopb设备节点;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hdmi: hdmi@ff940000 {
......
ports {
hdmi_in: port { # 属性reg的值赋值给endpoint.port,不存在赋值为0
#address-cells = <1>;
#size-cells = <0>;

hdmi_in_vopb: endpoint@0 {
reg = <0>; # 属性reg的值赋值给endpoint.id
remote-endpoint = <&vopb_out_hdmi>;
};
hdmi_in_vopl: endpoint@1 {
reg = <1>;
remote-endpoint = <&vopl_out_hdmi>;
};
};
};
};
4.2.2 of_graph_get_remote_endpoint

of_graph_get_remote_endpoint定义在drivers/of/property.c,其作用就是获取与指定本地端点相关联的远程端点节点;

1
2
3
4
5
6
7
8
9
10
11
12
/**
* of_graph_get_remote_endpoint() - get remote endpoint node
* @node: pointer to a local endpoint device_node
*
* Return: Remote endpoint node associated with remote endpoint node linked
* to @node. Use of_node_put() on it when done.
*/
struct device_node *of_graph_get_remote_endpoint(const struct device_node *node)
{
/* Get remote endpoint node. */
return of_parse_phandle(node, "remote-endpoint", 0);
}

hdmi_in_vopb设备节点为例,该返回返回remote-endpoin属性指定的设备节点,即vopb_out_hdmi

1
2
3
4
hdmi_in_vopb: endpoint@0 {
reg = <0>;
remote-endpoint = <&vopb_out_hdmi>;
};
4.2.3 of_graph_parse_endpoint

of_graph_parse_endpoint定义在drivers/of/property.c,函数的作用是解析端点节点node的属性,并将解析结果存储到 endpoint中;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* of_graph_parse_endpoint() - parse common endpoint node properties
* @node: pointer to endpoint device_node
* @endpoint: pointer to the OF endpoint data structure
*
* The caller should hold a reference to @node.
*/
int of_graph_parse_endpoint(const struct device_node *node,
struct of_endpoint *endpoint)
{
// endpoint的父结点是port结点
struct device_node *port_node = of_get_parent(node);

WARN_ONCE(!port_node, "%s(): endpoint %pOF has no parent node\n",
__func__, node);

// 填充0
memset(endpoint, 0, sizeof(*endpoint));

// 设置endpoint所属的port节点
endpoint->local_node = node;
/*
* It doesn't matter whether the two calls below succeed.
* If they don't then the default value 0 is used.
* port结点下的reg属性值是endpoint->port值
* endpoint节点reg属性值是endpoint->id值
*/
of_property_read_u32(port_node, "reg", &endpoint->port);
of_property_read_u32(node, "reg", &endpoint->id);

of_node_put(port_node);

return 0;
}

vopb_out_hdmi设备节点为例:

1
2
3
4
vopb_out_hdmi: endpoint@2 {
reg = <2>;
remote-endpoint = <&hdmi_in_vopb>;
};

经过of_graph_parse_endpoint函数处理后:

  • endpoint->id = 2
  • endpoint->port= 0

4.3 rockchip_hdmi_parse_dt

rockchip_hdmi_parse_dt定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c,这段代码主要是在解析hdmi设备节点;比如clocksrockchip,grfavdd-xxx等属性;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
static int rockchip_hdmi_parse_dt(struct rockchip_hdmi *hdmi)
{
struct device_node *np = hdmi->dev->of_node;
// 根据设备树节点中的rockchip,grf属性获取与GRF相关的寄存器映射 rockchip,grf = <&grf>;
hdmi->regmap = syscon_regmap_lookup_by_phandle(np, "rockchip,grf");
if (IS_ERR(hdmi->regmap)) {
DRM_DEV_ERROR(hdmi->dev, "Unable to get rockchip,grf\n");
return PTR_ERR(hdmi->regmap);
}

// 获取ref时钟 <&cru PLL_VPLL>
hdmi->ref_clk = devm_clk_get_optional(hdmi->dev, "ref");
// 如果获取失败,则尝试获取vpll时钟
if (!hdmi->ref_clk)
hdmi->ref_clk = devm_clk_get_optional(hdmi->dev, "vpll");

// deferred error
if (PTR_ERR(hdmi->ref_clk) == -EPROBE_DEFER) {
return -EPROBE_DEFER;
} else if (IS_ERR(hdmi->ref_clk)) {
DRM_DEV_ERROR(hdmi->dev, "failed to get reference clock\n");
return PTR_ERR(hdmi->ref_clk);
}

// 获取grf相关的时钟 <&cru PCLK_VIO_GRF>
hdmi->grf_clk = devm_clk_get(hdmi->dev, "grf");
if (PTR_ERR(hdmi->grf_clk) == -ENOENT) {
hdmi->grf_clk = NULL;
} else if (PTR_ERR(hdmi->grf_clk) == -EPROBE_DEFER) {
return -EPROBE_DEFER;
} else if (IS_ERR(hdmi->grf_clk)) {
DRM_DEV_ERROR(hdmi->dev, "failed to get grf clock\n");
return PTR_ERR(hdmi->grf_clk);
}

// 获取0.9v稳压器
hdmi->avdd_0v9 = devm_regulator_get(hdmi->dev, "avdd-0v9");
if (IS_ERR(hdmi->avdd_0v9))
return PTR_ERR(hdmi->avdd_0v9);

// 获取1.8v稳压器
hdmi->avdd_1v8 = devm_regulator_get(hdmi->dev, "avdd-1v8");
if (IS_ERR(hdmi->avdd_1v8))
return PTR_ERR(hdmi->avdd_1v8);

return 0;
}
4.3.1 devm_regulator_get

devm_regulator_get定义在drivers/regulator/devres.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static struct regulator *_devm_regulator_get(struct device *dev, const char *id,
int get_type)
{
struct regulator **ptr, *regulator;

// 为设备分配资源
ptr = devres_alloc(devm_regulator_release, sizeof(*ptr), GFP_KERNEL);
if (!ptr)
return ERR_PTR(-ENOMEM);

// 查找指定名称的regulator,如果找不到对应名字的 regulator,那么就返回 dummy regulator,并且在 kernel log 中输出相关 warning 信息
regulator = _regulator_get(dev, id, get_type);
if (!IS_ERR(regulator)) {
*ptr = regulator;
// 将资源添加到设备的资源链表上。释放资源时,遍历设备资源管理链表,然后调用资源注册的释放函数
devres_add(dev, ptr);
} else {
devres_free(ptr);
}

return regulator;
}

/**
* devm_regulator_get - Resource managed regulator_get()
* @dev: device to supply
* @id: supply name or regulator ID.
*
* Managed regulator_get(). Regulators returned from this function are
* automatically regulator_put() on driver detach. See regulator_get() for more
* information.
*/
struct regulator *devm_regulator_get(struct device *dev, const char *id)
{
return _devm_regulator_get(dev, id, NORMAL_GET); // NORMAL_GET值为0
}

函数内部又调用了_regulator_get,定义在drivers/regulator/core.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/* Internal regulator request function */
struct regulator *_regulator_get(struct device *dev, const char *id, // 以avdd-0v9为例
enum regulator_get_type get_type) // 传入0
{
struct regulator_dev *rdev;
struct regulator *regulator;
struct device_link *link;
int ret;

// 0 >= 3 不会进入
if (get_type >= MAX_GET_TYPE) {
dev_err(dev, "invalid type %d in %s\n", get_type, __func__);
return ERR_PTR(-EINVAL);
}

// 不会进入
if (id == NULL) {
pr_err("get() with no identifier\n");
return ERR_PTR(-EINVAL);
}

// 首先通过设备树的方式去查找rdev,如果没有找到,在通过regulator_map_list查找rdev,regulator_map_list在regulator_rdev注册的时候初始化的
rdev = regulator_dev_lookup(dev, id);
// 找不到 进入
if (IS_ERR(rdev)) {
ret = PTR_ERR(rdev);

/*
* If regulator_dev_lookup() fails with error other
* than -ENODEV our job here is done, we simply return it.
*/
if (ret != -ENODEV)
return ERR_PTR(ret);

if (!have_full_constraints()) {
dev_warn(dev,
"incomplete constraints, dummy supplies not allowed\n");
return ERR_PTR(-ENODEV);
}

switch (get_type) {
case NORMAL_GET: // 进入,返回一个dummy regulator
/*
* Assume that a regulator is physically present and
* enabled, even if it isn't hooked up, and just
* provide a dummy.
*/
dev_warn(dev, "supply %s not found, using dummy regulator\n", id);
rdev = dummy_regulator_rdev;
get_device(&rdev->dev);
break;

case EXCLUSIVE_GET:
dev_warn(dev,
"dummy supplies not allowed for exclusive requests\n");
fallthrough;

default:
return ERR_PTR(-ENODEV);
}
}
if (rdev->exclusive) {
regulator = ERR_PTR(-EPERM);
put_device(&rdev->dev);
return regulator;
}

if (get_type == EXCLUSIVE_GET && rdev->open_count) {
regulator = ERR_PTR(-EBUSY);
put_device(&rdev->dev);
return regulator;
}

mutex_lock(&regulator_list_mutex);
ret = (rdev->coupling_desc.n_resolved != rdev->coupling_desc.n_coupled);
mutex_unlock(&regulator_list_mutex);

if (ret != 0) {
regulator = ERR_PTR(-EPROBE_DEFER);
put_device(&rdev->dev);
return regulator;
}

ret = regulator_resolve_supply(rdev);
if (ret < 0) {
regulator = ERR_PTR(ret);
put_device(&rdev->dev);
return regulator;
}

if (!try_module_get(rdev->owner)) {
regulator = ERR_PTR(-EPROBE_DEFER);
put_device(&rdev->dev);
return regulator;
}

// 如果找到则调用create_regulator创建regulator
regulator = create_regulator(rdev, dev, id);
if (regulator == NULL) {
regulator = ERR_PTR(-ENOMEM);
module_put(rdev->owner);
put_device(&rdev->dev);
return regulator;
}

rdev->open_count++;
if (get_type == EXCLUSIVE_GET) {
rdev->exclusive = 1;

ret = _regulator_is_enabled(rdev);
if (ret > 0) {
rdev->use_count = 1;
regulator->enable_count = 1;
} else {
rdev->use_count = 0;
regulator->enable_count = 0;
}
}

link = device_link_add(dev, &rdev->dev, DL_FLAG_STATELESS);
if (!IS_ERR_OR_NULL(link))
regulator->device_link = true;

return regulator;
}

devm_regulator_get(hdmi->dev, "avdd-0v9")为例,该函数会调用regulator_dev_lookup查找regulator

  • 首先通过设备树的方式去查找rdev,即在hdmi设备节点中查找avdd-0v9-supply属性指定的regulator设备节点;
  • 如果没有找到,在通过regulator_map_list查找rdevregulator_map_listregulator_rdev注册的时候初始化的;

如果找不到将返回dummy regulator,并输入警告信息。

由于我们并没有在hdmi设备节点中定义avdd-0v9-supply属性,同时也没有在设备树定义regulator-name = "avdd-0v9"regulator_rdev,因此 我们内核在启动时会输入如下警告信息:

1
2
[    1.475048] dwhdmi-rockchip ff940000.hdmi: supply avdd-0v9 not found, using dummy regulator
[ 1.484573] dwhdmi-rockchip ff940000.hdmi: supply avdd-1v8 not found, using dummy regulator

4.4 devm_phy_optional_get

devm_phy_optional_get定义在drivers/phy/phy-core.c,它用于查找并获取一个可选的PHY(物理层设备)的引用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* devm_phy_optional_get() - lookup and obtain a reference to an optional phy.
* @dev: device that requests this phy
* @string: the phy name as given in the dt data or phy device name
* for non-dt case
*
* Gets the phy using phy_get(), and associates a device with it using
* devres. On driver detach, release function is invoked on the devres
* data, then, devres data is freed. This differs to devm_phy_get() in
* that if the phy does not exist, it is not considered an error and
* -ENODEV will not be returned. Instead the NULL phy is returned,
* which can be passed to all other phy consumer calls.
*/
struct phy *devm_phy_optional_get(struct device *dev, const char *string)
{
// 获取PHY
struct phy *phy = devm_phy_get(dev, string);

if (PTR_ERR(phy) == -ENODEV)
phy = NULL;

return phy;
}

4.5 regulator_enable

regulator_enable定义在drivers/regulator/core.c,用于使能regulator输出;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* regulator_enable - enable regulator output
* @regulator: regulator source
*
* Request that the regulator be enabled with the regulator output at
* the predefined voltage or current value. Calls to regulator_enable()
* must be balanced with calls to regulator_disable().
*
* NOTE: the output value can be set by other drivers, boot loader or may be
* hardwired in the regulator.
*/
int regulator_enable(struct regulator *regulator)
{
struct regulator_dev *rdev = regulator->rdev;
struct ww_acquire_ctx ww_ctx;
int ret;

regulator_lock_dependent(rdev, &ww_ctx);
ret = _regulator_enable(regulator);
regulator_unlock_dependent(rdev, &ww_ctx);

return ret;
}

4.6 dw_hdmi_bind

dw_hdmi_bind定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* -----------------------------------------------------------------------------
* Bind/unbind API, used from platforms based on the component framework.
*/
struct dw_hdmi *dw_hdmi_bind(struct platform_device *pdev,
struct drm_encoder *encoder,
const struct dw_hdmi_plat_data *plat_data)
{
struct dw_hdmi *hdmi;
int ret;

// dw hdmi探测
hdmi = dw_hdmi_probe(pdev, plat_data);
if (IS_ERR(hdmi))
return hdmi;


ret = drm_bridge_attach(encoder, &hdmi->bridge, NULL, 0);
if (ret) {
dw_hdmi_remove(hdmi);
return ERR_PTR(ret);
}

return hdmi;
}

调用该函数时,第一个参数传入hdmi设备节点对应的platform device,第二个参数传入drm encoder,第三个参数传入rk3399_hdmi_drv_datark3399_hdmi_drv_data定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c

1
2
3
4
5
6
7
8
static const struct dw_hdmi_plat_data rk3399_hdmi_drv_data = {
.mode_valid = dw_hdmi_rockchip_mode_valid, // 用于校验显示模式是否有效
.mpll_cfg = rockchip_mpll_cfg,
.cur_ctr = rockchip_cur_ctr,
.phy_config = rockchip_phy_config,
.phy_data = &rk3399_chip_data,
.use_drm_infoframe = true,
};

rockchip_mpll_cfgrockchip_cur_ctrrockchip_phy_config中存放的都是HDMI PHY配置参数,会被hdmi_phy_configure_dwc_hdmi_3d_tx函数使用,用于配置DWC HDMI 3D TX PHY的物理层(PHY),该函数位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c

  • 首先从提供的plat_data结构中获取mpll_configcurr_ctrlphy_config的指针。
  • 然后,它通过遍历mpll_configcurr_ctrlphy_config数组,找到与给定mpixelclock(像素时钟频率)匹配的配置条目。一旦找到匹配的条目,就会使用dw_hdmi_phy_i2c_write函数将对应的配置值写入HDMI PHY寄存器中;
  • 在最后一部分,代码还覆盖并禁用了时钟终端,并将特定的值写入了相应的PHY寄存器;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/*
* PHY configuration function for the DWC HDMI 3D TX PHY. Based on the available
* information the DWC MHL PHY has the same register layout and is thus also
* supported by this function.
*/
static int hdmi_phy_configure_dwc_hdmi_3d_tx(struct dw_hdmi *hdmi,
const struct dw_hdmi_plat_data *pdata,
unsigned long mpixelclock)
{
const struct dw_hdmi_mpll_config *mpll_config = pdata->mpll_cfg;
const struct dw_hdmi_curr_ctrl *curr_ctrl = pdata->cur_ctr;
const struct dw_hdmi_phy_config *phy_config = pdata->phy_config;

/* TOFIX Will need 420 specific PHY configuration tables */

/* PLL/MPLL Cfg - always match on final entry */
for (; mpll_config->mpixelclock != ~0UL; mpll_config++)
if (mpixelclock <= mpll_config->mpixelclock)
break;

for (; curr_ctrl->mpixelclock != ~0UL; curr_ctrl++)
if (mpixelclock <= curr_ctrl->mpixelclock)
break;

for (; phy_config->mpixelclock != ~0UL; phy_config++)
if (mpixelclock <= phy_config->mpixelclock)
break;

if (mpll_config->mpixelclock == ~0UL ||
curr_ctrl->mpixelclock == ~0UL ||
phy_config->mpixelclock == ~0UL)
return -EINVAL;

dw_hdmi_phy_i2c_write(hdmi, mpll_config->res[0].cpce,
HDMI_3D_TX_PHY_CPCE_CTRL);
dw_hdmi_phy_i2c_write(hdmi, mpll_config->res[0].gmp,
HDMI_3D_TX_PHY_GMPCTRL);
dw_hdmi_phy_i2c_write(hdmi, curr_ctrl->curr[0],
HDMI_3D_TX_PHY_CURRCTRL);

dw_hdmi_phy_i2c_write(hdmi, 0, HDMI_3D_TX_PHY_PLLPHBYCTRL);
dw_hdmi_phy_i2c_write(hdmi, HDMI_3D_TX_PHY_MSM_CTRL_CKO_SEL_FB_CLK,
HDMI_3D_TX_PHY_MSM_CTRL);

dw_hdmi_phy_i2c_write(hdmi, phy_config->term, HDMI_3D_TX_PHY_TXTERM);
dw_hdmi_phy_i2c_write(hdmi, phy_config->sym_ctr,
HDMI_3D_TX_PHY_CKSYMTXCTRL);
dw_hdmi_phy_i2c_write(hdmi, phy_config->vlev_ctr,
HDMI_3D_TX_PHY_VLEVCTRL);

/* Override and disable clock termination. */
dw_hdmi_phy_i2c_write(hdmi, HDMI_3D_TX_PHY_CKCALCTRL_OVERRIDE,
HDMI_3D_TX_PHY_CKCALCTRL);

return 0;
}
4.6.1 dw_hdmi_rockchip_mode_valid

dw_hdmi_rockchip_mode_valid函数用于校验显示模式是否有效;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static enum drm_mode_status
dw_hdmi_rockchip_mode_valid(struct dw_hdmi *hdmi, void *data,
const struct drm_display_info *info,
const struct drm_display_mode *mode)
{
const struct dw_hdmi_mpll_config *mpll_cfg = rockchip_mpll_cfg;
int pclk = mode->clock * 1000; // 计算得到像素时钟频率
bool valid = false;
int i;
// 遍历mpll_cfg像素时钟,查找匹配的时钟
for (i = 0; mpll_cfg[i].mpixelclock != (~0UL); i++) {
if (pclk == mpll_cfg[i].mpixelclock) {
valid = true;
break;
}
}

return (valid) ? MODE_OK : MODE_BAD;
}
4.6.2 rockchip_mpll_cfg

rockchip_mpll_cfg保存的是RK3399HDMI-PHY-PLL配置,用于配置HDMI_3D_TX_PHY_CPCE_CTRLHDMI_3D_TX_PHY_GMPCTRL寄存器;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
static const struct dw_hdmi_mpll_config rockchip_mpll_cfg[] = {
{
27000000, {
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}, {
36000000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:800x600@56Hz 640x480@85Hz;适用于edid_est_modes中定义的显示模式:800x600@56Hz
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}, {
40000000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:800x600@60Hz;适用于edid_est_modes中定义的显示模式:800x600@60Hz
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}, {
54000000, {
{ 0x0072, 0x0001},
{ 0x2142, 0x0001},
{ 0x40a2, 0x0001},
},
}, {
65000000, { // 适用于edid_est_modes中定义的显示模式:1024x768@70Hz
{ 0x0072, 0x0001},
{ 0x2142, 0x0001},
{ 0x40a2, 0x0001},
},
}, {
66000000, {
{ 0x013e, 0x0003},
{ 0x217e, 0x0002},
{ 0x4061, 0x0002}
},
}, {
74250000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1280x720@60Hz
{ 0x0072, 0x0001},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
83500000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1280x800@60Hz
{ 0x0072, 0x0001},
},
}, {
108000000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1600x900@60Hz 1280x1024@60Hz 1152x864@75Hz 1280x960@60Hz;适用于edid_est_modes中定义的显示模式:1152x864@75Hz
{ 0x0051, 0x0002},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
106500000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1440x900@60Hz、1280x800@75Hz
{ 0x0051, 0x0002},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
146250000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1680x1050@60Hz 1280x800@120Hz RB
{ 0x0051, 0x0002},
{ 0x2145, 0x0002},
{ 0x4061, 0x0002}
},
}, {
148500000, { // 适用于drm_dmt_modes中定义的预定义的标准显示模式:1920x1080@60Hz 1280x960@85Hz
{ 0x0051, 0x0003},
{ 0x214c, 0x0003},
{ 0x4064, 0x0003}
},
}, {
~0UL, {
{ 0x00a0, 0x000a },
{ 0x2001, 0x000f },
{ 0x4002, 0x000f },
},
}
};

结构体dw_hdmi_mpll_config定义如下:

1
2
3
4
5
6
7
struct dw_hdmi_mpll_config {
unsigned long mpixelclock;
struct {
u16 cpce;
u16 gmp;
} res[DW_HDMI_RES_MAX];
};

各项参数说明如下:

  • mpixelclock :像素时钟频率;
  • cpceOPMODE_PLLCFG寄存器值;
  • gmpPLLGMPCTRL寄存器值;

rockchip_mpll_cfg中的第一项配置为例:

1
2
3
4
5
6
7
{
27000000, {
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}

27000000表示像素时钟为27000000及以下的分辨率适用该项配置, {0x00b3, 0x0000 }{ 0x2153, 0x0000 }{ 0x40f3, 0x0000 } 三项依次对应色深为 8 BIT10BIT12 BIT(目前Rockchip方案实际只支持8/10 bit两种模式) 情况下使用的配置。
由于参数的取值需要查阅PHYdatasheet获取,若需要新增HDMI-PHY-PLL配置,可以向FAE提出所需的像素时钟。然后根据上述的规则,将新增的配置添加到rockchip_mpll_cfg中。

4.6.3 rockchip_cur_ctr

具体作用不晓得,用于配置HDMI_3D_TX_PHY_CURRCTRL寄存器;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static const struct dw_hdmi_curr_ctrl rockchip_cur_ctr[] = {
/* pixelclk bpp8 bpp10 bpp12 */
{
40000000, { 0x0018, 0x0018, 0x0018 },
}, {
65000000, { 0x0028, 0x0028, 0x0028 },
}, {
66000000, { 0x0038, 0x0038, 0x0038 },
}, {
74250000, { 0x0028, 0x0038, 0x0038 },
}, {
83500000, { 0x0028, 0x0038, 0x0038 },
}, {
146250000, { 0x0038, 0x0038, 0x0038 },
}, {
148500000, { 0x0000, 0x0038, 0x0038 },
}, {
~0UL, { 0x0000, 0x0000, 0x0000},
}
};

第一个参数和rockchip_mpll_cfg类似,比如40000000表示像素时钟为40000000及以下的分辨率适用该项配置。

4.6.4 rockchip_phy_config

具体作用不晓得,用于配置HDMI_3D_TX_PHY_TXTERMHDMI_3D_TX_PHY_CKSYMTXCTRLHDMI_3D_TX_PHY_VLEVCTRL寄存器;

1
2
3
4
5
6
7
static const struct dw_hdmi_phy_config rockchip_phy_config[] = {
/*pixelclk symbol term vlev*/
{ 74250000, 0x8009, 0x0004, 0x0272},
{ 148500000, 0x802b, 0x0004, 0x028d},
{ 297000000, 0x8039, 0x0005, 0x028d},
{ ~0UL, 0x0000, 0x0000, 0x0000}
};

第一个参数和rockchip_mpll_cfg类似,比如74250000表示像素时钟为74250000及以下的分辨率适用该项配置。

4.6.5 rk3399_chip_data
1
2
3
4
5
6
7
8
9
#define RK3399_GRF_SOC_CON20            0x6250
#define RK3399_HDMI_LCDC_SEL BIT(6)
#define HIWORD_UPDATE(val, mask) (val | (mask) << 16)

static struct rockchip_hdmi_chip_data rk3399_chip_data = {
.lcdsel_grf_reg = RK3399_GRF_SOC_CON20,
.lcdsel_big = HIWORD_UPDATE(0, RK3399_HDMI_LCDC_SEL),
.lcdsel_lit = HIWORD_UPDATE(RK3399_HDMI_LCDC_SEL, RK3399_HDMI_LCDC_SEL),
}

五、dw_hdmi_probe

dw_hdmi_probe函数可以看做是DesignWare hdmi驱动的入口函数,从这里开始就告别了Rockchip hdmi驱动相关的内容,正式进入DesignWare hdmi的源码分析中;dw_hdmi_probe函数定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
struct dw_hdmi *dw_hdmi_probe(struct platform_device *pdev,  // 传入hdmi设备节点所属的platform device
const struct dw_hdmi_plat_data *plat_data) // 传入rk3399_hdmi_drv_data
{
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct platform_device_info pdevinfo;
struct device_node *ddc_node;
struct dw_hdmi_cec_data cec;
struct dw_hdmi *hdmi;
struct resource *iores = NULL;
int irq;
int ret;
u32 val = 1;
u8 prod_id0;
u8 prod_id1;
u8 config0;
u8 config3;

// 1. 动态分配内存,指向struct dw_hdmi,并进行成员的初始化
hdmi = devm_kzalloc(dev, sizeof(*hdmi), GFP_KERNEL);
if (!hdmi)
return ERR_PTR(-ENOMEM);

hdmi->plat_data = plat_data;
hdmi->dev = dev;
hdmi->sample_rate = 48000;
hdmi->channels = 2;
hdmi->disabled = true;
hdmi->rxsense = true;
hdmi->phy_mask = (u8)~(HDMI_PHY_HPD | HDMI_PHY_RX_SENSE);
hdmi->mc_clkdis = 0x7f;
hdmi->last_connector_result = connector_status_disconnected;

mutex_init(&hdmi->mutex);
mutex_init(&hdmi->audio_mutex);
mutex_init(&hdmi->cec_notifier_mutex);
spin_lock_init(&hdmi->audio_lock);

// 2. 解析hdmi设备节点,初始化hdmi成员
ret = dw_hdmi_parse_dt(hdmi);
if (ret < 0)
return ERR_PTR(ret);

// 3. 获取ddc-i2c-bus设备节点 ddc-i2c-bus = <&i2c7>
ddc_node = of_parse_phandle(np, "ddc-i2c-bus", 0);
if (ddc_node) {
// 获取i2c总线适配器
hdmi->ddc = of_get_i2c_adapter_by_node(ddc_node);
of_node_put(ddc_node);
if (!hdmi->ddc) {
dev_dbg(hdmi->dev, "failed to read ddc node\n");
return ERR_PTR(-EPROBE_DEFER);
}

} else {
dev_dbg(hdmi->dev, "no ddc property found\n");
}

// 4. 为HDMI相关寄存器注册regmap,采用regmap模型访问HDMI相关寄存器
if (!plat_data->regm) {
const struct regmap_config *reg_config;

// 获取hdmi设备节点reg-io-width属性,描述hdmi相关寄存器位宽 reg-io-width = <4>
of_property_read_u32(np, "reg-io-width", &val);
switch (val) {
case 4:
// regmap 配置信息
reg_config = &hdmi_regmap_32bit_config;
hdmi->reg_shift = 2;
break;
case 1:
reg_config = &hdmi_regmap_8bit_config;
break;
default:
dev_err(dev, "reg-io-width must be 1 or 4\n");
return ERR_PTR(-EINVAL);
}

// 获取第一个内存资源,即reg = <0x0 0xff940000 0x0 0x20000> HDMI相关寄存器基地址
iores = platform_get_resource(pdev, IORESOURCE_MEM, 0);
hdmi->regs = devm_ioremap_resource(dev, iores);
if (IS_ERR(hdmi->regs)) {
ret = PTR_ERR(hdmi->regs);
goto err_res;
}

// 为内存映射I/O注册regmap
hdmi->regm = devm_regmap_init_mmio(dev, hdmi->regs, reg_config);
if (IS_ERR(hdmi->regm)) {
dev_err(dev, "Failed to configure regmap\n");
ret = PTR_ERR(hdmi->regm);
goto err_res;
}
} else {
hdmi->regm = plat_data->regm;
}

// 根据时钟名称isfr获取时钟,设备节点属性clock-names、clocks,指定了名字为isfr对应的时钟为<&cru SCLK_HDMI_SFR>
hdmi->isfr_clk = devm_clk_get(hdmi->dev, "isfr");
if (IS_ERR(hdmi->isfr_clk)) {
ret = PTR_ERR(hdmi->isfr_clk);
dev_err(hdmi->dev, "Unable to get HDMI isfr clk: %d\n", ret);
goto err_res;
}

// 准备和使能时钟
ret = clk_prepare_enable(hdmi->isfr_clk);
if (ret) {
dev_err(hdmi->dev, "Cannot enable HDMI isfr clock: %d\n", ret);
goto err_res;
}

// 根据时钟名称iahb获取时钟,设备节点属性clock-names、clocks,指定了名字为iahb对应的时钟为<&cru PCLK_HDMI_CTRL>
hdmi->iahb_clk = devm_clk_get(hdmi->dev, "iahb");
if (IS_ERR(hdmi->iahb_clk)) {
ret = PTR_ERR(hdmi->iahb_clk);
dev_err(hdmi->dev, "Unable to get HDMI iahb clk: %d\n", ret);
goto err_isfr;
}

// 准备和使能时钟
ret = clk_prepare_enable(hdmi->iahb_clk);
if (ret) {
dev_err(hdmi->dev, "Cannot enable HDMI iahb clock: %d\n", ret);
goto err_isfr;
}

// 根据时钟名称cec获取时钟,设备节点属性clock-names、clocks,指定了名字为cec对应的时钟为<&cru SCLK_HDMI_CEC>
hdmi->cec_clk = devm_clk_get(hdmi->dev, "cec");
if (PTR_ERR(hdmi->cec_clk) == -ENOENT) {
hdmi->cec_clk = NULL;
} else if (IS_ERR(hdmi->cec_clk)) {
ret = PTR_ERR(hdmi->cec_clk);
if (ret != -EPROBE_DEFER)
dev_err(hdmi->dev, "Cannot get HDMI cec clock: %d\n",
ret);

hdmi->cec_clk = NULL;
goto err_iahb;
} else {
// 准备和使能时钟
ret = clk_prepare_enable(hdmi->cec_clk);
if (ret) {
dev_err(hdmi->dev, "Cannot enable HDMI cec clock: %d\n",
ret);
goto err_iahb;
}
}

/* Product and revision IDs, 获取产品和版本标识信息 */
hdmi->version = (hdmi_readb(hdmi, HDMI_DESIGN_ID) << 8)
| (hdmi_readb(hdmi, HDMI_REVISION_ID) << 0);
prod_id0 = hdmi_readb(hdmi, HDMI_PRODUCT_ID0);
prod_id1 = hdmi_readb(hdmi, HDMI_PRODUCT_ID1);

// 如果发现不支持的HDMI控制器类型,则会打印错误信息并返回-ENODEV错误
if (prod_id0 != HDMI_PRODUCT_ID0_HDMI_TX ||
(prod_id1 & ~HDMI_PRODUCT_ID1_HDCP) != HDMI_PRODUCT_ID1_HDMI_TX) {
dev_err(dev, "Unsupported HDMI controller (%04x:%02x:%02x)\n",
hdmi->version, prod_id0, prod_id1);
ret = -ENODEV;
goto err_iahb;
}

// 5. 检测HDMI的物理层接口
ret = dw_hdmi_detect_phy(hdmi);
if (ret < 0)
goto err_iahb;

dev_info(dev, "Detected HDMI TX controller v%x.%03x %s HDCP (%s)\n",
hdmi->version >> 12, hdmi->version & 0xfff,
prod_id1 & HDMI_PRODUCT_ID1_HDCP ? "with" : "without",
hdmi->phy.name);

// 6. HDMI硬件初始化
dw_hdmi_init_hw(hdmi);

// 7. 获取第1个IRQ编号 interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH 0>
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_iahb;
}

// 8. 申请中断,中断处理函数设置为dw_hdmi_hardirq,中断线程化的处理函数设置为dw_hdmi_irq
ret = devm_request_threaded_irq(dev, irq, dw_hdmi_hardirq,
dw_hdmi_irq, IRQF_SHARED,
dev_name(dev), hdmi);
if (ret)
goto err_iahb;

/*
* To prevent overflows in HDMI_IH_FC_STAT2, set the clk regenerator
* N and cts values before enabling phy
*/
hdmi_init_clk_regenerator(hdmi);
/* If DDC bus is not specified, try to register HDMI I2C bus,不会进入 */
if (!hdmi->ddc) {
/* Look for (optional) stuff related to unwedging */
hdmi->pinctrl = devm_pinctrl_get(dev);
if (!IS_ERR(hdmi->pinctrl)) {
hdmi->unwedge_state =
pinctrl_lookup_state(hdmi->pinctrl, "unwedge");
hdmi->default_state =
pinctrl_lookup_state(hdmi->pinctrl, "default");

if (IS_ERR(hdmi->default_state) ||
IS_ERR(hdmi->unwedge_state)) {
if (!IS_ERR(hdmi->unwedge_state))
dev_warn(dev,
"Unwedge requires default pinctrl\n");
hdmi->default_state = NULL;
hdmi->unwedge_state = NULL;
}
}

hdmi->ddc = dw_hdmi_i2c_adapter(hdmi);
if (IS_ERR(hdmi->ddc))
hdmi->ddc = NULL;
}

// 初始化桥接设备
hdmi->bridge.driver_private = hdmi;
hdmi->bridge.funcs = &dw_hdmi_bridge_funcs;
hdmi->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID
| DRM_BRIDGE_OP_HPD;
hdmi->bridge.interlace_allowed = true;
#ifdef CONFIG_OF
hdmi->bridge.of_node = pdev->dev.of_node;
#endif

memset(&pdevinfo, 0, sizeof(pdevinfo));
pdevinfo.parent = dev;
pdevinfo.id = PLATFORM_DEVID_AUTO;

// 这看起来应该是获取HDMI的配置信息,具体是啥咱也不知道
config0 = hdmi_readb(hdmi, HDMI_CONFIG0_ID);
config3 = hdmi_readb(hdmi, HDMI_CONFIG3_ID);

// AHB DMA音频?
if (iores && config3 & HDMI_CONFIG3_AHBAUDDMA) {
struct dw_hdmi_audio_data audio;

audio.phys = iores->start;
audio.base = hdmi->regs;
audio.irq = irq;
audio.hdmi = hdmi;
audio.get_eld = hdmi_audio_get_eld;
hdmi->enable_audio = dw_hdmi_ahb_audio_enable;
hdmi->disable_audio = dw_hdmi_ahb_audio_disable;

pdevinfo.name = "dw-hdmi-ahb-audio";
pdevinfo.data = &audio;
pdevinfo.size_data = sizeof(audio);
pdevinfo.dma_mask = DMA_BIT_MASK(32);
hdmi->audio = platform_device_register_full(&pdevinfo);
} else if (config0 & HDMI_CONFIG0_I2S) { // I2S音频?
struct dw_hdmi_i2s_audio_data audio;

audio.hdmi = hdmi;
audio.get_eld = hdmi_audio_get_eld;
audio.write = hdmi_writeb;
audio.read = hdmi_readb;
hdmi->enable_audio = dw_hdmi_i2s_audio_enable;
hdmi->disable_audio = dw_hdmi_i2s_audio_disable;

pdevinfo.name = "dw-hdmi-i2s-audio";
pdevinfo.data = &audio;
pdevinfo.size_data = sizeof(audio);
pdevinfo.dma_mask = DMA_BIT_MASK(32);
hdmi->audio = platform_device_register_full(&pdevinfo);
} else if (iores && config3 & HDMI_CONFIG3_GPAUD) { // GP Audiou音频
struct dw_hdmi_audio_data audio;

audio.phys = iores->start;
audio.base = hdmi->regs;
audio.irq = irq;
audio.hdmi = hdmi;
audio.get_eld = hdmi_audio_get_eld;

hdmi->enable_audio = dw_hdmi_gp_audio_enable;
hdmi->disable_audio = dw_hdmi_gp_audio_disable;

pdevinfo.name = "dw-hdmi-gp-audio";
pdevinfo.id = PLATFORM_DEVID_NONE;
pdevinfo.data = &audio;
pdevinfo.size_data = sizeof(audio);
pdevinfo.dma_mask = DMA_BIT_MASK(32);
hdmi->audio = platform_device_register_full(&pdevinfo);
}

// 如果没有禁用CEC,并且HDMI控制器支持CEC
if (!plat_data->disable_cec && (config0 & HDMI_CONFIG0_CEC)) {
cec.hdmi = hdmi;
cec.ops = &dw_hdmi_cec_ops;
cec.irq = irq;

pdevinfo.name = "dw-hdmi-cec";
pdevinfo.data = &cec;
pdevinfo.size_data = sizeof(cec);
pdevinfo.dma_mask = 0;

hdmi->cec = platform_device_register_full(&pdevinfo);
}

// 当前桥接设备到全局链表`bridge_list中
drm_bridge_add(&hdmi->bridge);

return hdmi;

err_iahb:
clk_disable_unprepare(hdmi->iahb_clk);
clk_disable_unprepare(hdmi->cec_clk);
err_isfr:
clk_disable_unprepare(hdmi->isfr_clk);
err_res:
i2c_put_adapter(hdmi->ddc);

return ERR_PTR(ret);
}

这个代码的长度一眼望过去令人窒息。我们也不用去一一解读这段代码干了什么,我们只关注我们想了解的东西,比如与edid相关的内容,以及connector初始化相关的内容;

  • 动态分配struct dw_hdmi对象,并进行hdmi成员的初始化;
  • 调用dw_hdmi_parse_dt解析hdmi设备节点,初始化hdmi成员;实际上由于没有指定hdmi->plat_data->output_port所以这个函数会直接返回;
  • 如果指定了ddc-i2c-bus属性,则 获取i2c总线适配器;
  • HDMI相关寄存器注册regmap,采用regmap模型访问hdmi相关寄存器;
  • 获取并使能时钟isfr_clkiahb_clkcec_clk
  • 调用dw_hdmi_detect_phy检测hdmi的物理层接口;
  • 调用dw_hdmi_init_hw进行HDMI硬件初始化;
  • 注册中断interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH 0>,中断处理函数设置为dw_hdmi_hardirq,中断线程化的处理函数设置为dw_hdmi_irq
  • 初始化桥接设备,设置回调funcsdw_hdmi_bridge_funcs
  • 调用drm_bridge_add添加hdmi桥接设备;

5.1 dw_hdmi_parse_dt

dw_hdmi_parse_dt函数用于解析hdmi设备节点,初始化hdmi成员;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static int dw_hdmi_parse_dt(struct dw_hdmi *hdmi)
{
struct device_node *endpoint;
struct device_node *remote;

// 直接返回
if (!hdmi->plat_data->output_port)
return 0;

// 通过遍历父设备节点的所有子节点(端点节点)来查找符合指定port_reg=0的端点节点,这里返回的是hdmi_in_vopb设备节点
endpoint = of_graph_get_endpoint_by_regs(hdmi->dev->of_node,
hdmi->plat_data->output_port,
-1);
if (!endpoint) {
/*
* On platforms whose bindings don't make the output port
* mandatory (such as Rockchip) the plat_data->output_port
* field isn't set, so it's safe to make this a fatal error.
*/
dev_err(hdmi->dev, "Missing endpoint in port@%u\n",
hdmi->plat_data->output_port);
return -ENODEV;
}

// 首先获取endpoint设备节点remote-endpoin属性指定的设备节点,即vopb_out_hdmi设备节点,并向上查找父设备节点,直至找到vopb设备节点
remote = of_graph_get_remote_port_parent(endpoint);
of_node_put(endpoint);
if (!remote) {
dev_err(hdmi->dev, "Endpoint in port@%u unconnected\n",
hdmi->plat_data->output_port);
return -ENODEV;
}

// 判断这个设备节点是否处于启用状态 即status = "ok"
if (!of_device_is_available(remote)) {
dev_err(hdmi->dev, "port@%u remote device is disabled\n",
hdmi->plat_data->output_port);
of_node_put(remote);
return -ENODEV;
}

// find the bridge corresponding to the device node in the global bridge list bridge_list
hdmi->next_bridge = of_drm_find_bridge(remote);
of_node_put(remote);
if (!hdmi->next_bridge)
return -EPROBE_DEFER;

return 0;
}

函数of_drm_find_bridge定义在drivers/gpu/drm/drm_bridge.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* of_drm_find_bridge - find the bridge corresponding to the device node in
* the global bridge list
*
* @np: device node
*
* RETURNS:
* drm_bridge control struct on success, NULL on failure
*/
struct drm_bridge *of_drm_find_bridge(struct device_node *np)
{
struct drm_bridge *bridge;

mutex_lock(&bridge_lock);

list_for_each_entry(bridge, &bridge_list, list) {
if (bridge->of_node == np) {
mutex_unlock(&bridge_lock);
return bridge;
}
}

mutex_unlock(&bridge_lock);
return NULL;
}

5.2 dw_hdmi_init_hw

dw_hdmi_init_hw函数用于初始化I2C控制器;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void dw_hdmi_init_hw(struct dw_hdmi *hdmi)
{
initialize_hdmi_ih_mutes(hdmi);

/*
* Reset HDMI DDC I2C master controller and mute I2CM interrupts.
* Even if we are using a separate i2c adapter doing this doesn't
* hurt.
*/
dw_hdmi_i2c_init(hdmi);

// 如果指定了,则执行
if (hdmi->phy.ops->setup_hpd)
hdmi->phy.ops->setup_hpd(hdmi, hdmi->phy.data);
}

5.3 dw_hdmi_hardirq

dw_hdmi_hardirq为中断处理函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static irqreturn_t dw_hdmi_hardirq(int irq, void *dev_id)
{
struct dw_hdmi *hdmi = dev_id;
u8 intr_stat;
irqreturn_t ret = IRQ_NONE;

// 不会进入
if (hdmi->i2c)
ret = dw_hdmi_i2c_irq(hdmi);

// 读取寄存器HDMI_IH_PHY_STAT0的值
intr_stat = hdmi_readb(hdmi, HDMI_IH_PHY_STAT0);
if (intr_stat) {
// 向寄存器HDMI_IH_MUTE_PHY_STAT0写入值
hdmi_writeb(hdmi, ~0, HDMI_IH_MUTE_PHY_STAT0);
return IRQ_WAKE_THREAD;
}

return ret;
}
5.3.1 hdmi_readb

hdmi_readb实际上是对regmap_read进行了又一层的包装,用于实现对RK3399 hdmi相关寄存器进行读操作;

1
2
3
4
5
6
7
8
static inline u8 hdmi_readb(struct dw_hdmi *hdmi, int offset)
{
unsigned int val = 0;

regmap_read(hdmi->regm, offset << hdmi->reg_shift, &val);

return val;
}
5.3.2 hdmi_writeb

hdmi_writeb实际上是对regmap_write进行了又一层的包装,用于实现对RK3399 hdmi相关寄存器进行写操作;

1
2
3
4
static inline void hdmi_writeb(struct dw_hdmi *hdmi, u8 val, int offset)
{
regmap_write(hdmi->regm, offset << hdmi->reg_shift, val);
}

5.4 dw_hdmi_bridge_funcs(重点)

dw_hdmi_bridge_funcs定义了bridge的控制函数,位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c文件;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static const struct drm_bridge_funcs dw_hdmi_bridge_funcs = {
.atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
.atomic_reset = drm_atomic_helper_bridge_reset,
.attach = dw_hdmi_bridge_attach, // 桥接设备连接到encoder时被调用
.detach = dw_hdmi_bridge_detach,
.atomic_check = dw_hdmi_bridge_atomic_check,
.atomic_get_output_bus_fmts = dw_hdmi_bridge_atomic_get_output_bus_fmts,
.atomic_get_input_bus_fmts = dw_hdmi_bridge_atomic_get_input_bus_fmts,
.atomic_enable = dw_hdmi_bridge_atomic_enable,
.atomic_disable = dw_hdmi_bridge_atomic_disable,
.mode_set = dw_hdmi_bridge_mode_set,
.mode_valid = dw_hdmi_bridge_mode_valid, // 用于校验显示模式是否有效,最终调用dw_hdmi_rockchip_mode_valid
.detect = dw_hdmi_bridge_detect,
.get_edid = dw_hdmi_bridge_get_edid, // 用于获取edid信息
};

其中:

  • attach:回调函数在桥接设备连接到encoder时被调用;
  • detach:回调函数在桥接设备从encoder断开时被调用;
  • mode_valid:用于校验显示模式是否有效,最终调用dw_hdmi_plat_data 的成员mode_valid ,也就是dw_hdmi_rockchip_mode_valid函数;
  • get_edid:用于读取连接显示器的edid数据的首选方法。如果桥接设备支持读取edid的话,应当实现这个回调函数,并不实现 get_modes 回调;
5.4.1 dw_hdmi_bridge_attach

对于briget而言,dw_hdmi_bridge_attach函数用于将bridgeconnector(即hdmi设备)连接起来;

1
2
3
4
5
6
7
8
9
10
11
12
static int dw_hdmi_bridge_attach(struct drm_bridge *bridge,
enum drm_bridge_attach_flags flags)
{
struct dw_hdmi *hdmi = bridge->driver_private;

// 不会进入
if (flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)
return drm_bridge_attach(bridge->encoder, hdmi->next_bridge,
bridge, flags);

return dw_hdmi_connector_create(hdmi);
}

其中dw_hdmi_connector_create函数用于初始化 connector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static int dw_hdmi_connector_create(struct dw_hdmi *hdmi)
{
struct drm_connector *connector = &hdmi->connector;
struct cec_connector_info conn_info;
struct cec_notifier *notifier;

if (hdmi->version >= 0x200a)
connector->ycbcr_420_allowed =
hdmi->plat_data->ycbcr_420_allowed;
else
connector->ycbcr_420_allowed = false;

connector->interlace_allowed = 1;
connector->polled = DRM_CONNECTOR_POLL_HPD;

// 设置connector的辅助函数helper_private为dw_hdmi_connector_helper_funcs
drm_connector_helper_add(connector, &dw_hdmi_connector_helper_funcs);

// connector初始化, connector的控制函数func设置为dw_hdmi_connector_funcs
drm_connector_init_with_ddc(hdmi->bridge.dev, connector,
&dw_hdmi_connector_funcs,
DRM_MODE_CONNECTOR_HDMIA,
hdmi->ddc);

/*
* drm_connector_attach_max_bpc_property() requires the
* connector to have a state.
*/
drm_atomic_helper_connector_reset(connector);

drm_connector_attach_max_bpc_property(connector, 8, 16);

if (hdmi->version >= 0x200a && hdmi->plat_data->use_drm_infoframe)
drm_connector_attach_hdr_output_metadata_property(connector);

// connector->possible_encoders |= drm_encoder_mask(encoder);
drm_connector_attach_encoder(connector, hdmi->bridge.encoder);

cec_fill_conn_info_from_drm(&conn_info, connector);

notifier = cec_notifier_conn_register(hdmi->dev, NULL, &conn_info);
if (!notifier)
return -ENOMEM;

mutex_lock(&hdmi->cec_notifier_mutex);
hdmi->cec_notifier = notifier;
mutex_unlock(&hdmi->cec_notifier_mutex);

return 0;
}

(1)connector的辅助函数helper_privatedw_hdmi_connector_helper_funcs

1
2
3
4
static const struct drm_connector_helper_funcs dw_hdmi_connector_helper_funcs = {
.get_modes = dw_hdmi_connector_get_modes,
.atomic_check = dw_hdmi_connector_atomic_check,
};

get_modes用于通过DDC探探测的所有显示模式,并将其添加到connectorprobed_modes 链表中。

(2)connector的控制函数func设置为dw_hdmi_connector_funcs

1
2
3
4
5
6
7
8
9
static const struct drm_connector_funcs dw_hdmi_connector_funcs = {
.fill_modes = drm_helper_probe_single_connector_modes,
.detect = dw_hdmi_connector_detect,
.destroy = drm_connector_cleanup,
.force = dw_hdmi_connector_force,
.reset = drm_atomic_helper_connector_reset,
.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
};

其中fill_modes中指定的drm_helper_probe_single_connector_modes函数用于检测并筛选出所有有效的显示模式。该函数旨在作为使用CRTC辅助函数进行输出模式过滤和检测的驱动程序的drm_connector_funcs.fill_modes函数的通用实现。基本过程如下:

  • connectormodes链表中的所有显示模式标记为过时;

  • 使用drm_mode_probed_add将新显示模式添加到connectorprobed_modes链表中,新显示模式的状态初始值为OK。显示模式从单个来源按照以下优先顺序添加;

    • drm_connector_helper_funcs.get_modes回调函数(drm_helper_probe_get_modes);
    • 如果connector状态为connected并且drm_helper_probe_get_modes未获取到显示模式,则自动添加标准的VESA DMT显示模式,最高分辨率为1024x768drm_add_modes_noedid);
  • 通过内核命令行指定的显示模式将与之前的探测结果一起添加(drm_helper_probe_add_cmdline_mode),这些模显示模式是使用VESA GTF/CVT算法生成的;

  • 将显示模式从probed_modes链表移动到modes链表中,潜在的重复模式将被合并在一起(drm_connector_list_update);此步骤完成后,probed_modes链表将再次为空;

  • modes列表中的任何非过时显示模式都要进行验证,并更新显示模式的状态;

    • drm_mode_validate_basic执行基本的合法性检查;
    • drm_mode_validate_size过滤掉大于maxXmaxY(如果有指定)的模式;
    • drm_mode_validate_flag根据基本连接器能力(允许交错,允许双扫描,允许立体)检查模式;
    • 可选的drm_connector_helper_funcs.mode_validdrm_connector_helper_funcs.mode_valid_ctx辅助函数可以执行驱动程序和/或显示器特定的检查;
    • 可选的drm_crtc_helper_funcs.mode_validdrm_bridge_funcs.mode_valid(会调用dw_hdmi_bridge_mode_valid函数进行校验)和drm_encoder_helper_funcs.mode_valid辅助函数可以执行驱动程序和/或源特定的检查,这些辅助函数也由modeset/atomic辅助函数执行;
  • connectormodes链表中去除任何状态不为OK的模式,同时输出调试消息指示模式被拒绝的原因(drm_mode_prune_invalid)。

这里我们简单说一下drm_xxx_helper_funcsdrm_xxx_funcs的区别;

drm_connector_funcs是应用层进行drm ioctl操作是的最终入口,对于大多数的SoC厂商来说,他们的drm_xxx_funcs操作流程基本相同,仅仅是在寄存器配置上存在差异,因此开发者将那些通用的操作流程封装了helper函数,而将那些厂商差异化的代码放到了drm_xxx_helper_funcs中去,由SoC厂商自己实现。

比如dw_hdmi_connector_funcs中的fill_modesresetatomic_duplicate_state等都是使用的通用的helper函数,他们定义在drivers/gpu/drm/drm_probe_helper.cdrivers/gpu/drm/drm_atomic_state_helper.c等文件中:这些helper函数内部实现一般就是回调dw_hdmi_connector_helper_funcs中的相应方法。

5.4.2 dw_hdmi_bridge_get_edid

dw_hdmi_bridge_get_edid用于获取edid信息;

1
2
3
4
5
6
7
static struct edid *dw_hdmi_bridge_get_edid(struct drm_bridge *bridge,
struct drm_connector *connector)
{
struct dw_hdmi *hdmi = bridge->driver_private;
// 获取edid信息
return dw_hdmi_get_edid(hdmi, connector);
}
5.4.3 分析小结

经过分析我们发现无论是bridgetfuncs中的get_edid还是connectorhelper_privateget_modes都会调用dw_hdmi_get_edid获取连接器的edid信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bridget的funcs被设置为dw_hdmi_bridge_funcs
static const struct drm_bridge_funcs dw_hdmi_bridge_funcs = {
.get_edid = dw_hdmi_bridge_get_edid,
......
};

dw_hdmi_bridge_get_edid(bridge,connector)
dw_hdmi_get_edid(hdmi, connector)

// connector的helper_private被设置为dw_hdmi_connector_helper_funcs
static const struct drm_connector_helper_funcs dw_hdmi_connector_helper_funcs = {
.get_modes = dw_hdmi_connector_get_modes,
......
};

dw_hdmi_connector_get_modes(connector)
dw_hdmi_get_edid(hdmi, connector)

5.5 drm_bridge_add

drm_bridge_add函数定义在drivers/gpu/drm/drm_bridge.c,用于将当前桥接设备到全局链表bridge_list中;

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* drm_bridge_add - add the given bridge to the global bridge list
*
* @bridge: bridge control structure
*/
void drm_bridge_add(struct drm_bridge *bridge)
{
mutex_init(&bridge->hpd_mutex);

mutex_lock(&bridge_lock);
list_add_tail(&bridge->list, &bridge_list);
mutex_unlock(&bridge_lock);
}

六、drm_add_edid_modes

struct drm_connector_helper_funcsget_modes用于通过DDC探测到connector的所有显示模式,并将其添加到connectorprobed_modes 链表中,这些模式还没有经过筛选和过滤;

需要注意的是:在探测阶段,系统可能会探测到一些暂时不可用或不推荐的显示模式,这些模式会先被存储在 probed_modes 中。之后,这些模式可能会经过进一步的处理和筛选,最终加入到 modes 中成为最终可用的显示模式列表。

1
2
3
4
static const struct drm_connector_helper_funcs dw_hdmi_connector_helper_funcs = {
.get_modes = dw_hdmi_connector_get_modes,
.atomic_check = dw_hdmi_connector_atomic_check,
};

dw_hdmi_connector_get_modes主要包括两步:

  • 通过dw_hdmi_get_edid获取connectoredid信息;
  • 通过drm_add_edid_modes函数解析edid信息,并将其转换为显示模式,添加到connectorprobed_modes 链表;

dw_hdmi_connector_get_modes函数位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int dw_hdmi_connector_get_modes(struct drm_connector *connector)
{
struct dw_hdmi *hdmi = container_of(connector, struct dw_hdmi,
connector);
struct edid *edid;
int ret;

// 获取connector的edid信息
edid = dw_hdmi_get_edid(hdmi, connector);
if (!edid)
return 0;

// 更新连接器edid属性
drm_connector_update_edid_property(connector, edid);
cec_notifier_set_phys_addr_from_edid(hdmi->cec_notifier, edid);

// 解析edid中的显示模式并添加到connector的probed_modes链表
ret = drm_add_edid_modes(connector, edid);
kfree(edid);

return ret;
}

6.1 dw_hdmi_get_edid

dw_hdmi_get_edid定义在drivers/gpu/drm/bridge/synopsys/dw-hdmi.c,用于获取connectoredid信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static struct edid *dw_hdmi_get_edid(struct dw_hdmi *hdmi,
struct drm_connector *connector)
{
struct edid *edid;

if (!hdmi->ddc)
return NULL;

// 通过I2C通信获取edid信息
edid = drm_get_edid(connector, hdmi->ddc);
if (!edid) {
dev_dbg(hdmi->dev, "failed to get edid\n");
return NULL;
}

dev_dbg(hdmi->dev, "got edid: width[%d] x height[%d]\n",
edid->width_cm, edid->height_cm);

hdmi->sink_is_hdmi = drm_detect_hdmi_monitor(edid);
hdmi->sink_has_audio = drm_detect_monitor_audio(edid);

return edid;
}

drm_get_edid函数位于drivers/gpu/drm/drm_edid.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* drm_get_edid - get EDID data, if available
* @connector: connector we're probing
* @adapter: I2C adapter to use for DDC
*
* Poke the given I2C channel to grab EDID data if possible. If found,
* attach it to the connector.
*
* Return: Pointer to valid EDID or NULL if we couldn't find any.
*/
struct edid *drm_get_edid(struct drm_connector *connector,
struct i2c_adapter *adapter)
{
struct edid *edid;

if (connector->force == DRM_FORCE_OFF)
return NULL;

if (connector->force == DRM_FORCE_UNSPECIFIED && !drm_probe_ddc(adapter))
return NULL;

// 调用drm_do_probe_ddc_edid函数实现通过I2C总线读取edid信息,有兴趣可以看一下i2c_transfer,I2C从设备地址为0x50
edid = _drm_do_get_edid(connector, drm_do_probe_ddc_edid, adapter, NULL);
drm_connector_update_edid_property(connector, edid);
return edid;
}

6.2 drm_add_edid_modes

drm_add_edid_modes函数用于解析edid信息,并将其转换为显示模式,添加到connectorprobed_modes 链表。

在这之前,我们需要大概了解一下CVT/GTF/DMT,因此后面在分析代码讲解如何解析edid并将其转换为显示模式的时候会有所涉及。CVTCoordinated Video Timing)、GTFGeneralized Timing Formula)和DMTDisplay Monitor Timings)是三种不同的显示器时序规范。它们在计算和定义显示模式参数方面有所不同;

  • DMTDisplay Monitor Timings):DMT是由VESAVideo Electronics Standards Association)定义的一组标准显示模式参数。DMT规范列出了一些预定义的显示模式,包括常见的分辨率、刷新率和时序参数。这些参数在不同的显示设备上是通用的,可以提供简单的配置和兼容性。linux内核中将标准显示模式存放在数组drm_dmt_modes,位于drivers/gpu/drm/drm_edid.c文件;
  • GTFGeneralizedTimingFormula):GTF是一种更高级的算法,它可以生成更灵活的显示模式。GTF算法基于显示器的物理特性和电信号时序的数学模型,通过计算参数来生成显示模式。GTF算法允许更精细的控制,可以生成几乎任意分辨率、刷新率和纵横比的显示模式;内核实现函数有:drm_gtf_modedrm_gtf2_mode,定义在drivers/gpu/drm/drm_modes.c
  • CVTCoordinatedVideoTiming):CVT是一种改进的显示模式计算方法,用于在不同的显示设备上创建更准确的显示模式。CVT算法考虑了显示器的特性、带宽和可见性要求,以生成更准确的时序参数。CVT模式具有更高的精度和可调节性,可以提供更好的显示效果;内核实现函数drm_cvt_mode定义在drivers/gpu/drm/drm_modes.c

总结来说,DMT是一组预定义的标准模式,GTF允许生成更灵活的自定义模式,而CVT通过考虑更多因素生成更准确的显示模式。选择使用哪种时序规范取决于具体的需求和显示设备的兼容性。

drm_add_edid_modes其实现位于drivers/gpu/drm/drm_edid.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* drm_add_edid_modes - add modes from EDID data, if available
* @connector: connector we're probing
* @edid: EDID data
*
* Add the specified modes to the connector's mode list. Also fills out the
* &drm_display_info structure and ELD in @connector with any information which
* can be derived from the edid.
*
* This function is deprecated. Use drm_edid_connector_add_modes() instead.
*
* Return: The number of modes added or 0 if we couldn't find any.
*/
int drm_add_edid_modes(struct drm_connector *connector, struct edid *edid)
{
struct drm_edid _drm_edid;
const struct drm_edid *drm_edid;

// edid有效性校验
if (edid && !drm_edid_is_valid(edid)) {
drm_warn(connector->dev, "[CONNECTOR:%d:%s] EDID invalid.\n",
connector->base.id, connector->name);
edid = NULL;
}

drm_edid = drm_edid_legacy_init(&_drm_edid, edid);

update_display_info(connector, drm_edid);

return _drm_edid_connector_add_modes(connector, drm_edid);
}

函数首先检查edid数据的有效性,然后使用提供的edid数据初始化 drm_edid 结构,接着更新显示信息,最后调用 _drm_edid_connector_add_modes 函数来添加模式。

6.2.1 drm_edid_legacy_init

drm_edid_legacy_init函数仅仅是用来初始化 drm_edid 结构体。它接受两个参数:drm_edidedid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* Initializer helper for legacy interfaces, where we have no choice but to
* trust edid size. Not for general purpose use.
*/
static const struct drm_edid *drm_edid_legacy_init(struct drm_edid *drm_edid,
const struct edid *edid)
{
if (!edid)
return NULL;

memset(drm_edid, 0, sizeof(*drm_edid));

drm_edid->edid = edid;
// 计算edid数据的大小
drm_edid->size = edid_size(edid);

return drm_edid;
}
6.2.2 update_display_info

update_display_info用于更新HDMI显示设备的显示信息(即connector->display_info),该函数接收两个参数,第一个是connector,第二个参数为edid信息;下面是我使用的一款HDMI显示器的edid信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
                0   1   2   3   4   5   6   7   8   9   
000 | 00 FF FF FF FF FF FF 00 35 34
010 | 00 00 01 01 01 01 00 20 01 03
020 | 81 00 00 78 EE 8C 75 A9 54 45
030 | 98 22 1E 50 54 2F CF 00 71 40
040 | 81 C0 81 80 95 00 A9 C0 B3 00
050 | D1 C0 D1 00 D3 BC 00 A0 A0 A0
060 | 29 50 30 20 35 00 B9 88 21 00
070 | 00 1A 56 5E 00 A0 A0 A0 29 50
080 | 30 20 35 00 B9 88 21 00 00 1A
090 | 67 E2 00 A0 A0 A0 29 50 30 20
100 | 35 00 B9 88 21 00 00 1A 00 00
110 | 00 FC 00 4D 45 49 54 49 41 4E
120 | 48 41 4F 0A 20 20 01 0B

(8-9) ID Manufacture Name : MIT
(10-11) ID Product Code : 0000
(12-15) ID Serial Number : N/A
(16) Week of Manufacture : 0
(17) Year of Manufacture : 2022

(18) EDID Version Number : 1
(19) EDID Revision Number: 3

(20) Video Input Definition : Digital
DFP 1.x Compatible

(21) Maximum Horizontal Image Size: 0 cm
(22) Maximum Vertical Image Size : 0 cm
(23) Display Gamma : 2.20
(24) Power Management and Supported Feature(s):
Standby, Suspend, Active Off/Very Low Power, RGB Color, sRGB, Preferred Timing Mode

(25-34) Color Characteristics
Red Chromaticity : Rx = 0.658 Ry = 0.328
Green Chromaticity : Gx = 0.269 Gy = 0.594
Blue Chromaticity : Bx = 0.134 By = 0.120
Default White Point: Wx = 0.313 Wy = 0.329
......

(126-127) Extension Flag and Checksum

Extension Block(s) : 1 # 表明有扩展块
Checksum Value : 11
___________________________________________________________________

Block 1 ( CEA-861 Extension Block), Bytes 128 - 255, 128 BYTES OF EDID CODE:

0 1 2 3 4 5 6 7 8 9
128 | 02 03 3A F2 4F 04 05 10 13 14
138 | 1F 6C 6C 6C 27 6C 6C 6C 4B 4C
148 | E2 00 D5 E3 05 C0 00 23 09 7F
158 | 07 83 01 00 00 67 03 0C 00 10
168 | 00 38 78 E6 06 05 01 69 69 4F
178 | 67 D8 5D C4 01 76 C0 00 02 3A
188 | 80 18 71 38 2D 40 58 2C 25 00
198 | 58 C3 10 00 00 1E D4 BC 00 A0
208 | A0 A0 29 50 30 20 35 00 B9 88
218 | 21 00 00 1E 98 E2 00 A0 A0 A0
228 | 29 50 30 20 35 00 B9 88 21 00
238 | 00 1E 00 00 00 00 00 00 00 00
248 | 00 00 00 00 00 00 00 C4

(128-130) Extension Header

Revision Number : 3
DTD Starting Offset: 58

(131) Display Support

DTV Underscan, Basic Audio, YCbCr 4:4:4, YCbCr 4:2:2
Number of Native Formats: 2

(132-147) Video Data Block

1280x720p @ 59.94/60Hz - HDTV (16:9, 1:1)
1920x1080i @ 59.94/60Hz - HDTV (16:9, 1:1)
1920x1080p @ 59.94/60Hz - HDTV (16:9, 1:1)
1280x720p @ 50Hz - HDTV (16:9, 1:1)
1920x1080i @ 50Hz - HDTV (16:9, 1:1)
1920x1080p @ 50Hz - HDTV (16:9, 1:1)
Reserved for the Future
Reserved for the Future
Reserved for the Future
1920x1080i (1250 total) - HDTV 50Hz (16:9, 1:1)
Reserved for the Future
Reserved for the Future
Reserved for the Future
Reserved for the Future
Reserved for the Future

(148-150) Video Capability Data Block (VCDB)


(151-154) Colorimetry Data Block

update_display_info代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
static void update_display_info(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
struct drm_display_info *info = &connector->display_info;
const struct edid *edid;

// 复位info各个成员的值
drm_reset_display_info(connector);
clear_eld(connector);

if (!drm_edid)
return;

edid = drm_edid->edid;

// 从edit中获取面板ID(由制造商名称、产品代码构成的一个u32类型),然后根据面板ID到edid_quirk_list列表匹配来获取相应的quirk标志;比如我使用的一款HDMI显示器的面板ID就在edid_quirk_list中不到匹配项,所以quirks设置为0
info->quirks = edid_get_quirks(drm_edid);

// 从edid中获取最大水平图像尺寸 0x15H
info->width_mm = edid->width_cm * 10;
// 从edid中获取最大垂直图像尺寸 0x16H
info->height_mm = edid->height_cm * 10;

// 对于edit revision版本号小于4,该函数直接返回
drm_get_monitor_range(connector, drm_edid);

if (edid->revision < 3)
goto out;

// 如果是模拟信号,进入
if (!(edid->input & DRM_EDID_INPUT_DIGITAL))
goto out;

// 设置HDMI的颜色格式
info->color_formats |= DRM_COLOR_FORMAT_RGB444;
// 解析EDID扩展块(CEA-861D)
drm_parse_cea_ext(connector, drm_edid);

/*
* Digital sink with "DFP 1.x compliant TMDS" according to EDID 1.3?
*
* For such displays, the DFP spec 1.0, section 3.10 "EDID support"
* tells us to assume 8 bpc color depth if the EDID doesn't have
* extensions which tell otherwise.
*/
if (info->bpc == 0 && edid->revision == 3 &&
edid->input & DRM_EDID_DIGITAL_DFP_1_X) { // 进入 DRM_EDID_DIGITAL_DFP_1_X = 1<<0
info->bpc = 8;
drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] Assigning DFP sink color depth as %d bpc.\n",
connector->base.id, connector->name, info->bpc);
}

/* Only defined for 1.4 with digital displays,对于EDID 1.4以下版本跳转到out */
if (edid->revision < 4)
goto out;

switch (edid->input & DRM_EDID_DIGITAL_DEPTH_MASK) {
case DRM_EDID_DIGITAL_DEPTH_6:
info->bpc = 6;
break;
case DRM_EDID_DIGITAL_DEPTH_8:
info->bpc = 8;
break;
case DRM_EDID_DIGITAL_DEPTH_10:
info->bpc = 10;
break;
case DRM_EDID_DIGITAL_DEPTH_12:
info->bpc = 12;
break;
case DRM_EDID_DIGITAL_DEPTH_14:
info->bpc = 14;
break;
case DRM_EDID_DIGITAL_DEPTH_16:
info->bpc = 16;
break;
case DRM_EDID_DIGITAL_DEPTH_UNDEF:
default:
info->bpc = 0;
break;
}

drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] Assigning EDID-1.4 digital sink color depth as %d bpc.\n",
connector->base.id, connector->name, info->bpc);

if (edid->features & DRM_EDID_FEATURE_RGB_YCRCB444)
info->color_formats |= DRM_COLOR_FORMAT_YCBCR444;
if (edid->features & DRM_EDID_FEATURE_RGB_YCRCB422)
info->color_formats |= DRM_COLOR_FORMAT_YCBCR422;

drm_update_mso(connector, drm_edid);

out:
// 跳过
if (info->quirks & EDID_QUIRK_NON_DESKTOP) { // 1<<12
drm_dbg_kms(connector->dev, "[CONNECTOR:%d:%s] Non-desktop display%s\n",
connector->base.id, connector->name,
info->non_desktop ? " (redundant quirk)" : "");
info->non_desktop = true;
}

// 跳过
if (info->quirks & EDID_QUIRK_CAP_DSC_15BPP) // 1<<13
info->max_dsc_bpp = 15;

// 跳过
if (info->quirks & EDID_QUIRK_FORCE_6BPC) // 1<<10
info->bpc = 6;

// 跳过
if (info->quirks & EDID_QUIRK_FORCE_8BPC) // 1<<8
info->bpc = 8;

// 跳过
if (info->quirks & EDID_QUIRK_FORCE_10BPC) // 1<<11
info->bpc = 10;

// 跳过
if (info->quirks & EDID_QUIRK_FORCE_12BPC) // 1<<9
info->bpc = 12;

/* Depends on info->cea_rev set by drm_parse_cea_ext() above */
drm_edid_to_eld(connector, drm_edid);
}

在该函数的执行流程中同时会解析edid扩展块,由drm_parse_cea_ext函数实现,以我是用的HDMI显示器为例分析如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
static void drm_parse_cea_ext(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
struct drm_display_info *info = &connector->display_info;
struct drm_edid_iter edid_iter;
const struct cea_db *db;
struct cea_db_iter iter;
const u8 *edid_ext;
u64 y420cmdb_map = 0;

drm_edid_iter_begin(drm_edid, &edid_iter);
// edid信息由edid主块和edid扩展块组成,这里是一个遍历操作,edid_ext依次指向主块的首地址、edid扩展块的首地址
drm_edid_iter_for_each(edid_ext, &edid_iter) {
// 通过标记位判定是不是扩展块,如果不是跳过
if (edid_ext[0] != CEA_EXT)
continue;

// 设置版本号,未设置则进入,cea_rev设置为03H,
if (!info->cea_rev)
info->cea_rev = edid_ext[1];

if (info->cea_rev != edid_ext[1])
drm_dbg_kms(connector->dev,
"[CONNECTOR:%d:%s] CEA extension version mismatch %u != %u\n",
connector->base.id, connector->name,
info->cea_rev, edid_ext[1]);

/* The existence of a CTA extension should imply RGB support */
info->color_formats = DRM_COLOR_FORMAT_RGB444;
// 是否支持YCbCr 4:4:4 支持
if (edid_ext[3] & EDID_CEA_YCRCB444)
info->color_formats |= DRM_COLOR_FORMAT_YCBCR444;
// 是否支持YCbCr 4:2:2 支持
if (edid_ext[3] & EDID_CEA_YCRCB422)
info->color_formats |= DRM_COLOR_FORMAT_YCBCR422;
}
drm_edid_iter_end(&edid_iter);

cea_db_iter_edid_begin(drm_edid, &iter);
// 遍历edid扩展块的Data Blocks
cea_db_iter_for_each(db, &iter) {
/* FIXME: convert parsers to use struct cea_db */
const u8 *data = (const u8 *)db;
// Vendor Specific类型的Data Block 不会进入
if (cea_db_is_hdmi_vsdb(db))
drm_parse_hdmi_vsdb_video(connector, data);
// Forum Vendor Specific类型的Data Block 不会进入
else if (cea_db_is_hdmi_forum_vsdb(db) ||
cea_db_is_hdmi_forum_scdb(db))
drm_parse_hdmi_forum_scds(connector, data);
// Microsoft Vendor Specific类型的Data Block 不会进入
else if (cea_db_is_microsoft_vsdb(db))
drm_parse_microsoft_vsdb(connector, data);
else if (cea_db_is_y420cmdb(db))
parse_cta_y420cmdb(connector, db, &y420cmdb_map);
else if (cea_db_is_y420vdb(db))
parse_cta_y420vdb(connector, db);
else if (cea_db_is_vcdb(db))
drm_parse_vcdb(connector, data);
else if (cea_db_is_hdmi_hdr_metadata_block(db))
drm_parse_hdr_metadata_block(connector, data);
else if (cea_db_tag(db) == CTA_DB_VIDEO)
parse_cta_vdb(connector, db);
}
cea_db_iter_end(&iter);

if (y420cmdb_map)
update_cta_y420cmdb(connector, y420cmdb_map);
}
6.2.3 _drm_edid_connector_add_modes

_drm_edid_connector_add_modes函数是今天我们学习的重点,这个函数会从edid中解析HDMI显示设备支持的所有显示模式,在解析edid时,是按照edid规范中定义的优先级顺序来解析的;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int _drm_edid_connector_add_modes(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
const struct drm_display_info *info = &connector->display_info;
int num_modes = 0;

if (!drm_edid)
return 0;

/*
* EDID spec says modes should be preferred in this order:
* - preferred detailed mode
* - other detailed modes from base block
* - detailed modes from extension blocks
* - CVT 3-byte code modes
* - standard timing codes
* - established timing codes
* - modes inferred from GTF or CVT range information
*
* We get this pretty much right.
*
* XXX order for additional mode types in extension blocks?
*/
num_modes += add_detailed_modes(connector, drm_edid);
num_modes += add_cvt_modes(connector, drm_edid);
num_modes += add_standard_modes(connector, drm_edid);
num_modes += add_established_modes(connector, drm_edid);
num_modes += add_cea_modes(connector, drm_edid);
num_modes += add_alternate_cea_modes(connector, drm_edid);
num_modes += add_displayid_detailed_modes(connector, drm_edid);
if (drm_edid->edid->features & DRM_EDID_FEATURE_CONTINUOUS_FREQ)
num_modes += add_inferred_modes(connector, drm_edid);

if (info->quirks & (EDID_QUIRK_PREFER_LARGE_60 | EDID_QUIRK_PREFER_LARGE_75))
edid_fixup_preferred(connector);

return num_modes;
}

6.3 add_detailed_modes

add_detailed_modes函数用于从Detailed Timings中获取显示模式,并添加到connectorprobed_modes 链表;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* add_detailed_modes - Add modes from detailed timings
* @connector: attached connector
* @drm_edid: EDID block to scan
*/
static int add_detailed_modes(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
struct detailed_mode_closure closure = {
.connector = connector,
.drm_edid = drm_edid,
};

// 我使用的HDMI显示器revision为3,因此不会进入
if (drm_edid->edid->revision >= 4)
closure.preferred = true; /* first detailed timing is always preferred */
else
// 0x18 位[1]:如果置1,推荐分辨率为第一个Detailed Timing
closure.preferred =
drm_edid->edid->features & DRM_EDID_FEATURE_PREFERRED_TIMING; // 1<<1

// 遍历Detailed Timings,依次执行do_detailed_mode
drm_for_each_detailed_block(drm_edid, do_detailed_mode, &closure);

return closure.modes;
}
6.3.1 Detailed Timings信息

以我使用的HDMI显示器为例,Detailed Timings信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
(54-71)      Detailed Descriptor #1: Preferred Detailed Timing (2560x1440 @ 120Hz)

Pixel Clock : 483.39 MHz
Horizontal Image Size : 697 mm
Vertical Image Size : 392 mm
Refresh Mode : Non-interlaced
Normal Display, No Stereo

Horizontal:
Active Time : 2560 Pixels
Blanking Time : 160 Pixels
Sync Offset : 48 Pixels
Sync Pulse Width: 32 Pixels
Border : 0 Pixels
Frequency : 177 kHz

Vertical:
Active Time : 1440 Lines
Blanking Time : 41 Lines
Sync Offset : 3 Lines
Sync Pulse Width: 5 Lines
Border : 0 Lines

Digital Separate, Horizontal Polarity (+), Vertical Polarity (-)

Modeline: "2560x1440" 483.390 2560 2608 2640 2720 1440 1443 1448 1481 +hsync -vsync

(72-89) Detailed Descriptor #2: Detailed Timing (2560x1440 @ 60Hz)

Pixel Clock : 241.5 MHz
Horizontal Image Size : 697 mm
Vertical Image Size : 392 mm
Refresh Mode : Non-interlaced
Normal Display, No Stereo

Horizontal:
Active Time : 2560 Pixels
Blanking Time : 160 Pixels
Sync Offset : 48 Pixels
Sync Pulse Width: 32 Pixels
Border : 0 Pixels
Frequency : 88 kHz

Vertical:
Active Time : 1440 Lines
Blanking Time : 41 Lines
Sync Offset : 3 Lines
Sync Pulse Width: 5 Lines
Border : 0 Lines

Digital Separate, Horizontal Polarity (+), Vertical Polarity (-)

Modeline: "2560x1440" 241.500 2560 2608 2640 2720 1440 1443 1448 1481 +hsync -vsync

(90-107) Detailed Descriptor #3: Detailed Timing (2560x1440 @ 144Hz)

Pixel Clock : 579.59 MHz
Horizontal Image Size : 697 mm
Vertical Image Size : 392 mm
Refresh Mode : Non-interlaced
Normal Display, No Stereo

Horizontal:
Active Time : 2560 Pixels
Blanking Time : 160 Pixels
Sync Offset : 48 Pixels
Sync Pulse Width: 32 Pixels
Border : 0 Pixels
Frequency : 213 kHz

Vertical:
Active Time : 1440 Lines
Blanking Time : 41 Lines
Sync Offset : 3 Lines
Sync Pulse Width: 5 Lines
Border : 0 Lines

Digital Separate, Horizontal Polarity (+), Vertical Polarity (-)

Modeline: "2560x1440" 579.590 2560 2608 2640 2720 1440 1443 1448 1481 +hsync -vsync

(108-125) Detailed Descriptor #4: Monitor Name

Monitor Name: MEITIANHAO

其中第一个Detailed TimingHDMI显示器的最佳时序:2560x1440 @ 120Hz

这里一共有4个块,只有前3个块描述的是Timing Descriptor,因此add_detailed_modes函数会遍历这三个Timing Descriptor,依次执行do_detailed_mode

6.3.2 do_detailed_mode

do_detailed_mode函数用于解析Timing Descriptor,创建显示模式并添加到connectorprobed_modes 链表;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void
do_detailed_mode(const struct detailed_timing *timing, void *c)
{
struct detailed_mode_closure *closure = c;
struct drm_display_mode *newmode;

// 如果不是Timing Descriptor,将跳过
if (!is_detailed_timing_descriptor(timing))
return;

// 解析Timing Descriptor 此对于我使用的HDMI显示器,只有前3个块可以走到这一步
newmode = drm_mode_detailed(closure->connector,
closure->drm_edid, timing);
if (!newmode)
return;

// 第一个Timing Descriptor会进入,其他的不会进入
if (closure->preferred)
newmode->type |= DRM_MODE_TYPE_PREFERRED;

/*
* Detailed modes are limited to 10kHz pixel clock resolution,
* so fix up anything that looks like CEA/HDMI mode, but the clock
* is just slightly off.
*/
fixup_detailed_cea_mode_clock(closure->connector, newmode);

// list_add_tail(&mode->head, &connector->probed_modes);
drm_mode_probed_add(closure->connector, newmode);
// 计数
closure->modes++;
// 清除标志位
closure->preferred = false;
}

整个函数的核心为drm_mode_detailed

6.3.3 drm_mode_detailed

drm_mode_detailed函数定义如下,下面我们以第一个Timing Descriptor为例进行分析该函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/*
* Create a new mode from an EDID detailed timing section. An EDID detailed
* timing block contains enough info for us to create and return a new struct
* drm_display_mode.
*/
static struct drm_display_mode *drm_mode_detailed(struct drm_connector *connector,
const struct drm_edid *drm_edid,
const struct detailed_timing *timing)
{
// 获取显示信息
const struct drm_display_info *info = &connector->display_info;
// 获取drm设备
struct drm_device *dev = connector->dev;
struct drm_display_mode *mode;
// 时序参数
const struct detailed_pixel_timing *pt = &timing->data.pixel_data;
// 水平活动像素高4位 | 水平活动像素低8位 = 2560 Pixels
unsigned hactive = (pt->hactive_hblank_hi & 0xf0) << 4 | pt->hactive_lo;
// 垂直活动像素高4位 | 水平活动像素低8位 = 1440 Lines
unsigned vactive = (pt->vactive_vblank_hi & 0xf0) << 4 | pt->vactive_lo;
// 水平blanking高4位 | 水平blanking低8位 = 160 Pixels
unsigned hblank = (pt->hactive_hblank_hi & 0xf) << 8 | pt->hblank_lo;
// 垂直blanking高4位 | 水平blanking低8位 = 41 Lines
unsigned vblank = (pt->vactive_vblank_hi & 0xf) << 8 | pt->vblank_lo;
// 水平同步信号偏移量 = 48 Pixels
unsigned hsync_offset = (pt->hsync_vsync_offset_pulse_width_hi & 0xc0) << 2 | pt->hsync_offset_lo;
// 水平同步信号脉冲宽度 = 32 Pixels
unsigned hsync_pulse_width = (pt->hsync_vsync_offset_pulse_width_hi & 0x30) << 4 | pt->hsync_pulse_width_lo;
// 垂直同步信号偏移量 = 3 Lines
unsigned vsync_offset = (pt->hsync_vsync_offset_pulse_width_hi & 0xc) << 2 | pt->vsync_offset_pulse_width_lo >> 4;
// 垂直同步信号脉冲宽度 = 5 Lines
unsigned vsync_pulse_width = (pt->hsync_vsync_offset_pulse_width_hi & 0x3) << 4 | (pt->vsync_offset_pulse_width_lo & 0xf);

/* ignore tiny modes,跳过 */
if (hactive < 64 || vactive < 64)
return NULL;

// 标志位 配置了立体 0x1A&1<<5=0, 所以跳过
if (pt->misc & DRM_EDID_PT_STEREO) { // 1<<5
drm_dbg_kms(dev, "[CONNECTOR:%d:%s] Stereo mode not supported\n",
connector->base.id, connector->name);
return NULL;
}

// 标志位 配置了数字分离信号 0x1A&0x3<<3=0x3<<3, 所以进入
if (!(pt->misc & DRM_EDID_PT_SEPARATE_SYNC)) { // 3<<3
drm_dbg_kms(dev, "[CONNECTOR:%d:%s] Composite sync not supported\n",
connector->base.id, connector->name);
}

/* it is incorrect if hsync/vsync width is zero,跳过 */
if (!hsync_pulse_width || !vsync_pulse_width) {
drm_dbg_kms(dev, "[CONNECTOR:%d:%s] Incorrect Detailed timing. Wrong Hsync/Vsync pulse width\n",
connector->base.id, connector->name);
return NULL;
}

// 跳过
if (info->quirks & EDID_QUIRK_FORCE_REDUCED_BLANKING) { // 1<<7
mode = drm_cvt_mode(dev, hactive, vactive, 60, true, false, false);
if (!mode)
return NULL;

goto set_size;
}

// 创建显示模式,动态分配内存
mode = drm_mode_create(dev);
if (!mode)
return NULL;

// 跳过
if (info->quirks & EDID_QUIRK_135_CLOCK_TOO_HIGH) // 1<<1
mode->clock = 1088 * 10;
else
// 为TMDS时钟频率:串行数据速率是实际像素时钟速率的10倍 单位10KHz
mode->clock = le16_to_cpu(timing->pixel_clock) * 10;

// 行有效像素 2560 Pixels
mode->hdisplay = hactive;
// 行同步起始像素 2560 + 48 = 2608 Pixels
mode->hsync_start = mode->hdisplay + hsync_offset;
// 水平同步结束 2608 + 32 = 2640 Pixels
mode->hsync_end = mode->hsync_start + hsync_pulse_width;
// 水平总大小 2560 + 160 = 2720 Pixels
mode->htotal = mode->hdisplay + hblank;

// 垂直显示大小 1440 Lines
mode->vdisplay = vactive;
// 垂直同步起始 1440 + 3 = 1443 Lines
mode->vsync_start = mode->vdisplay + vsync_offset;
// 帧同步结束行 1443 + 5 = 1448 Lines
mode->vsync_end = mode->vsync_start + vsync_pulse_width;
// 一帧总行数 1440 + 41 = 1481 Lines
mode->vtotal = mode->vdisplay + vblank;

/* Some EDIDs have bogus h/vtotal values,跳过 */
if (mode->hsync_end > mode->htotal)
mode->htotal = mode->hsync_end + 1;
/* 跳过 */
if (mode->vsync_end > mode->vtotal)
mode->vtotal = mode->vsync_end + 1;

// 处理交叉模式,对于当前Timing Descriptor 0x47位[7]为0,该函数直接返回
drm_mode_do_interlace_quirk(mode, pt);

// 跳过
if (info->quirks & EDID_QUIRK_DETAILED_SYNC_PP) { // 1<<6
mode->flags |= DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC;
} else {
mode->flags |= (pt->misc & DRM_EDID_PT_HSYNC_POSITIVE) ? // 1<<1 DRM_MODE_FLAG_PHSYNC
DRM_MODE_FLAG_PHSYNC : DRM_MODE_FLAG_NHSYNC;
mode->flags |= (pt->misc & DRM_EDID_PT_VSYNC_POSITIVE) ? // 1<<2 DRM_MODE_FLAG_NVSYNC
DRM_MODE_FLAG_PVSYNC : DRM_MODE_FLAG_NVSYNC;
}

set_size:
// 水平图像尺寸 697mm
mode->width_mm = pt->width_mm_lo | (pt->width_height_mm_hi & 0xf0) << 4;
// 垂直图像尺寸 392mm
mode->height_mm = pt->height_mm_lo | (pt->width_height_mm_hi & 0xf) << 8;

// 跳过
if (info->quirks & EDID_QUIRK_DETAILED_IN_CM) { // 1<<3
mode->width_mm *= 10;
mode->height_mm *= 10;
}

// 跳过
if (info->quirks & EDID_QUIRK_DETAILED_USE_MAXIMUM_SIZE) { // 1<<4
mode->width_mm = drm_edid->edid->width_cm * 10;
mode->height_mm = drm_edid->edid->height_cm * 10;
}

// 一个标志位的位掩码,主要用于表示显示模式的来源;DRM_MODE_TYPE_DRIVER标识驱动程序创建的模式
mode->type = DRM_MODE_TYPE_DRIVER;
// 设置显示模式的名称 %dx%d%s 第一个参数为:mode->hdisplay 第二个参数为:mode->vdisplay 第三个参数为:i/''(取决于mode->flags是否设置了DRM_MODE_FLAG_INTERLACE)
drm_mode_set_name(mode);

return mode;
}

经过分析,第一个Timing Descriptor时序参数如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
36  37  38  39  3A  3B  3C  3D  3E  3F  40  41  41  43  44  45  46  47 
D3 BC 00 A0 A0 A0 29 50 30 20 35 00 B9 88 21 00 00 1A

(54-71) Detailed Descriptor #1: Preferred Detailed Timing (2560x1440 @ 120Hz)

#像素时钟/10000 ${0x36}|${0x37}<<8=0xBCD3 ==> 48339*10000/10^6 MHz
Pixel Clock : 483.39 MHz

# 水平图像尺寸(width_mm) (${0x44}&0xf0)<<4|${0x42}=(0 x21&0xf0)<<4|0xB9=0x2B9=697
Horizontal Image Size : 697 mm
# 垂直图像尺寸(height_mm) (${0x44}&0x0f)<<8|${0x43}=(0x21&0x0f)<<8|0x88=0x188=392
Vertical Image Size : 392 mm
Refresh Mode : Non-interlaced
Normal Display, No Stereo

Horizontal:
# 水平活动像素数(hactive) (${0x3A}&0xf0)<<4|${0x38}=(0xA0&0xf0)<<4|0x00=0xA00=2560
Active Time : 2560 Pixels
# 水平blanking(hblank) (${0x3A}&0x0f)<<4|${0x39}=(0xA0&0x0f)<<8|0xA0=160
Blanking Time : 160 Pixels
# 水平同步信号偏移量(hsync_offset) (${0x41}&0xc0)<<2|${0x3E}=(0x00&0xc0)<<2|0x30=48
Sync Offset : 48 Pixels
# 水平同步信号脉冲宽度(hsync_pulse_width) (${0x41}&0x30)<<4 |${0x3F}=(0x00&0x30)<<4|0x20=32
Sync Pulse Width: 32 Pixels
# $(0x45)=0
Border : 0 Pixels
Frequency : 177 kHz

Vertical:
# 垂直活动像素数(vactive) (${0x3D}&0xf0)<<4|${0x3B}=(0x50&0xf0)<<4|0xA0=0x5A0=1440
Active Time : 1440 Lines
# 垂直blanking(vblank) (${0x3D}&0x0f)<<4|${0x3C}=(0x50&0x0f)<<8|0x29=0x29=41
Blanking Time : 41 Lines
# 垂直同步信号偏移量(vsync_offset) (${0x41}&0xc)<<2|${0x40}>> 4=(0x00&0xc)<<2|0x35>>4=3
Sync Offset : 3 Lines
# 垂直同步信号脉冲宽度(vsync_pulse_width) (${0x41}&0x03)<<4 |(${0x40}&0x0f)=(0x00&0x03)<<4|(0x35&0x0f)=5
Sync Pulse Width: 5 Lines
# $(0x46)=0
Border : 0 Lines

Digital Separate, Horizontal Polarity (+), Vertical Polarity (-)

Modeline: "2560x1440" 483.390 2560 2608 2640 2720 1440 1443 1448 1481 +hsync -vsync

Modeline中表示的含义依次为nameclock (单位为MHz)、hdisplayhsync_starthsync_endhtotalvdisplayvsync_startvsync_endvtotalflag,有关这些时序参数我们已经在《Rockchip RK3399 - DRM crtc基础知识》中详细介绍了;

img0

img1

这里我们在说一下刷新率120Hz是如何得到的:$$刷新率=\frac{像素时钟频率}{htotal_vtotal}=\frac{483390000}{2720_1481}=120$$,也就是说每秒刷新120帧图片。

6.4 add_cvt_modes

add_cvt_modes函数和add_detailed_modes类似,均是用于从Detailed Timings中获取显示模式,并添加到connectorprobed_modes 链表;只不过这个函数遍历Detailed Timings,依次执行的是do_cvt_mode,其基于CVT算法生成显示模式;

在介绍函数之前,我们简单来说一下什么是CVTCVT算法是一种用于计算显示器显示模式参数的算法,使用计算出的参数来创建Modeline

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int
add_cvt_modes(struct drm_connector *connector, const struct drm_edid *drm_edid)
{
struct detailed_mode_closure closure = {
.connector = connector,
.drm_edid = drm_edid,
};

// 进入
if (drm_edid->edid->revision >= 3)
// 遍历Detailed Timings,依次执行do_cvt_mode,对于我使用的HDMI显示器,实际上这里什么也不会做
drm_for_each_detailed_block(drm_edid, do_cvt_mode, &closure);

/* XXX should also look for CVT codes in VTB blocks */

return closure.modes;
}
6.4.1 do_cvt_mode

do_cvt_mode函数用于解析EDID_DETAIL_CVT_3BYTE类型的Monitor Descriptor,基于CVT算法创建显示模式并添加到connectorprobed_modes 链表;

1
2
3
4
5
6
7
8
9
10
11
static void
do_cvt_mode(const struct detailed_timing *timing, void *c)
{
struct detailed_mode_closure *closure = c;

// 如果不是EDID_DETAIL_CVT_3BYTE类型的Monitor Descriptor,对于我使用的HDMI显示器,直接return
if (!is_display_descriptor(timing, EDID_DETAIL_CVT_3BYTE))
return;

closure->modes += drm_cvt_modes(closure->connector, timing);
}

对于我使用的HDMI显示器,只有最后一个块是Monitor Descriptor,但是其类型为EDID_DETAIL_MONITOR_NAME,因此不会执行drm_cvt_modes函数。但是不妨我们可以了解一下drm_cvt_modes

6.4.2 drm_cvt_modes

drm_cvt_modes函数定义如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static int drm_cvt_modes(struct drm_connector *connector,
const struct detailed_timing *timing)
{
int i, j, modes = 0;
struct drm_display_mode *newmode;
struct drm_device *dev = connector->dev;
const struct cvt_timing *cvt;
const int rates[] = { 60, 85, 75, 60, 50 };
const u8 empty[3] = { 0, 0, 0 };

// 遍历每一个cvt_timing
for (i = 0; i < 4; i++) {
int width, height;

// 获取第i个cvt_timing
cvt = &(timing->data.other_data.data.cvt[i]);

// 如果相等,continue
if (!memcmp(cvt->code, empty, 3))
continue;

// 计算visplay
height = (cvt->code[0] + ((cvt->code[1] & 0xf0) << 4) + 1) * 2;
// 计算hdisplay
switch (cvt->code[1] & 0x0c) {
/* default - because compiler doesn't see that we've enumerated all cases */
default:
case 0x00:
width = height * 4 / 3;
break;
case 0x04:
width = height * 16 / 9;
break;
case 0x08:
width = height * 16 / 10;
break;
case 0x0c:
width = height * 15 / 9;
break;
}

for (j = 1; j < 5; j++) {
if (cvt->code[2] & (1 << j)) {
// 创建显示模式
newmode = drm_cvt_mode(dev, width, height,
rates[j], j == 0,
false, false);
if (newmode) {
// list_add_tail(&mode->head, &connector->probed_modes);
drm_mode_probed_add(connector, newmode);
modes++;
}
}
}
}

return modes;
}
6.4.3 drm_cvt_mode

drm_cvt_mode函数用于create a modeline based on the CVT algorithm,这个函数根据hdisplayvdisplayvrefresh调用CVT算法来生成Modeline。它基于Graham Loveridge于2003年4月9日编写的VESA(TM) Coordinated Video Timing Generator

函数定义在drivers/gpu/drm/drm_modes.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**
* drm_cvt_mode -create a modeline based on the CVT algorithm
* @dev: drm device
* @hdisplay: hdisplay size
* @vdisplay: vdisplay size
* @vrefresh: vrefresh rate
* @reduced: whether to use reduced blanking
* @interlaced: whether to compute an interlaced mode
* @margins: whether to add margins (borders)
*
* This function is called to generate the modeline based on CVT algorithm
* according to the hdisplay, vdisplay, vrefresh.
* It is based from the VESA(TM) Coordinated Video Timing Generator by
* Graham Loveridge April 9, 2003 available at
* http://www.elo.utfsm.cl/~elo212/docs/CVTd6r1.xls
*
* And it is copied from xf86CVTmode in xserver/hw/xfree86/modes/xf86cvt.c.
* What I have done is to translate it by using integer calculation.
*
* Returns:
* The modeline based on the CVT algorithm stored in a drm_display_mode object.
* The display mode object is allocated with drm_mode_create(). Returns NULL
* when no mode could be allocated.
*/
struct drm_display_mode *drm_cvt_mode(struct drm_device *dev, int hdisplay,
int vdisplay, int vrefresh,
bool reduced, bool interlaced, bool margins)
{
#define HV_FACTOR 1000
/* 1) top/bottom margin size (% of height) - default: 1.8, */
#define CVT_MARGIN_PERCENTAGE 18
/* 2) character cell horizontal granularity (pixels) - default 8 */
#define CVT_H_GRANULARITY 8
/* 3) Minimum vertical porch (lines) - default 3 */
#define CVT_MIN_V_PORCH 3
/* 4) Minimum number of vertical back porch lines - default 6 */
#define CVT_MIN_V_BPORCH 6
/* Pixel Clock step (kHz) */
#define CVT_CLOCK_STEP 250
struct drm_display_mode *drm_mode;
unsigned int vfieldrate, hperiod;
int hdisplay_rnd, hmargin, vdisplay_rnd, vmargin, vsync;
int interlace;
u64 tmp;

if (!hdisplay || !vdisplay)
return NULL;

/* allocate the drm_display_mode structure. If failure, we will
* return directly
drm_mode = drm_mode_create(dev);
if (!drm_mode)
return NULL;

/* the CVT default refresh rate is 60Hz */
if (!vrefresh)
vrefresh = 60;

/* the required field fresh rate */
if (interlaced)
vfieldrate = vrefresh * 2;
else
vfieldrate = vrefresh;

/* horizontal pixels */
hdisplay_rnd = hdisplay - (hdisplay % CVT_H_GRANULARITY);

/* determine the left&right borders */
hmargin = 0;
if (margins) {
hmargin = hdisplay_rnd * CVT_MARGIN_PERCENTAGE / 1000;
hmargin -= hmargin % CVT_H_GRANULARITY;
}
/* find the total active pixels */
drm_mode->hdisplay = hdisplay_rnd + 2 * hmargin;

/* find the number of lines per field */
if (interlaced)
vdisplay_rnd = vdisplay / 2;
else
vdisplay_rnd = vdisplay;

/* find the top & bottom borders */
vmargin = 0;
if (margins)
vmargin = vdisplay_rnd * CVT_MARGIN_PERCENTAGE / 1000;

drm_mode->vdisplay = vdisplay + 2 * vmargin;

/* Interlaced */
if (interlaced)
interlace = 1;
else
interlace = 0;

/* Determine VSync Width from aspect ratio */
if (!(vdisplay % 3) && ((vdisplay * 4 / 3) == hdisplay))
vsync = 4;
else if (!(vdisplay % 9) && ((vdisplay * 16 / 9) == hdisplay))
vsync = 5;
else if (!(vdisplay % 10) && ((vdisplay * 16 / 10) == hdisplay))
vsync = 6;
else if (!(vdisplay % 4) && ((vdisplay * 5 / 4) == hdisplay))
vsync = 7;
else if (!(vdisplay % 9) && ((vdisplay * 15 / 9) == hdisplay))
vsync = 7;
else /* custom */
vsync = 10;

if (!reduced) {
/* simplify the GTF calculation */
/* 4) Minimum time of vertical sync + back porch interval (µs)
* default 550.0
*/
int tmp1, tmp2;
#define CVT_MIN_VSYNC_BP 550
/* 3) Nominal HSync width (% of line period) - default 8 */
#define CVT_HSYNC_PERCENTAGE 8
unsigned int hblank_percentage;
int vsyncandback_porch, __maybe_unused vback_porch, hblank;

/* estimated the horizontal period */
tmp1 = HV_FACTOR * 1000000 -
CVT_MIN_VSYNC_BP * HV_FACTOR * vfieldrate;
tmp2 = (vdisplay_rnd + 2 * vmargin + CVT_MIN_V_PORCH) * 2 +
interlace;
hperiod = tmp1 * 2 / (tmp2 * vfieldrate);

tmp1 = CVT_MIN_VSYNC_BP * HV_FACTOR / hperiod + 1;
/* 9. Find number of lines in sync + backporch */
if (tmp1 < (vsync + CVT_MIN_V_PORCH))
vsyncandback_porch = vsync + CVT_MIN_V_PORCH;
else
vsyncandback_porch = tmp1;
/* 10. Find number of lines in back porch */
vback_porch = vsyncandback_porch - vsync;
drm_mode->vtotal = vdisplay_rnd + 2 * vmargin +
vsyncandback_porch + CVT_MIN_V_PORCH;
/* 5) Definition of Horizontal blanking time limitation */
/* Gradient (%/kHz) - default 600 */
#define CVT_M_FACTOR 600
/* Offset (%) - default 40 */
#define CVT_C_FACTOR 40
/* Blanking time scaling factor - default 128 */
#define CVT_K_FACTOR 128
/* Scaling factor weighting - default 20 */
#define CVT_J_FACTOR 20
#define CVT_M_PRIME (CVT_M_FACTOR * CVT_K_FACTOR / 256)
#define CVT_C_PRIME ((CVT_C_FACTOR - CVT_J_FACTOR) * CVT_K_FACTOR / 256 + \
CVT_J_FACTOR)
/* 12. Find ideal blanking duty cycle from formula */
hblank_percentage = CVT_C_PRIME * HV_FACTOR - CVT_M_PRIME *
hperiod / 1000;
/* 13. Blanking time */
if (hblank_percentage < 20 * HV_FACTOR)
hblank_percentage = 20 * HV_FACTOR;
hblank = drm_mode->hdisplay * hblank_percentage /
(100 * HV_FACTOR - hblank_percentage);
hblank -= hblank % (2 * CVT_H_GRANULARITY);
/* 14. find the total pixels per line */
drm_mode->htotal = drm_mode->hdisplay + hblank;
drm_mode->hsync_end = drm_mode->hdisplay + hblank / 2;
drm_mode->hsync_start = drm_mode->hsync_end -
(drm_mode->htotal * CVT_HSYNC_PERCENTAGE) / 100;
drm_mode->hsync_start += CVT_H_GRANULARITY -
drm_mode->hsync_start % CVT_H_GRANULARITY;
/* fill the Vsync values */
drm_mode->vsync_start = drm_mode->vdisplay + CVT_MIN_V_PORCH;
drm_mode->vsync_end = drm_mode->vsync_start + vsync;
} else {
/* Reduced blanking */
/* Minimum vertical blanking interval time (µs)- default 460 */
#define CVT_RB_MIN_VBLANK 460
/* Fixed number of clocks for horizontal sync */
#define CVT_RB_H_SYNC 32
/* Fixed number of clocks for horizontal blanking */
#define CVT_RB_H_BLANK 160
/* Fixed number of lines for vertical front porch - default 3*/
#define CVT_RB_VFPORCH 3
int vbilines;
int tmp1, tmp2;
/* 8. Estimate Horizontal period. */
tmp1 = HV_FACTOR * 1000000 -
CVT_RB_MIN_VBLANK * HV_FACTOR * vfieldrate;
tmp2 = vdisplay_rnd + 2 * vmargin;
hperiod = tmp1 / (tmp2 * vfieldrate);
/* 9. Find number of lines in vertical blanking */
vbilines = CVT_RB_MIN_VBLANK * HV_FACTOR / hperiod + 1;
/* 10. Check if vertical blanking is sufficient */
if (vbilines < (CVT_RB_VFPORCH + vsync + CVT_MIN_V_BPORCH))
vbilines = CVT_RB_VFPORCH + vsync + CVT_MIN_V_BPORCH;
/* 11. Find total number of lines in vertical field */
drm_mode->vtotal = vdisplay_rnd + 2 * vmargin + vbilines;
/* 12. Find total number of pixels in a line */
drm_mode->htotal = drm_mode->hdisplay + CVT_RB_H_BLANK;
/* Fill in HSync values */
drm_mode->hsync_end = drm_mode->hdisplay + CVT_RB_H_BLANK / 2;
drm_mode->hsync_start = drm_mode->hsync_end - CVT_RB_H_SYNC;
/* Fill in VSync values */
drm_mode->vsync_start = drm_mode->vdisplay + CVT_RB_VFPORCH;
drm_mode->vsync_end = drm_mode->vsync_start + vsync;
}
/* 15/13. Find pixel clock frequency (kHz for xf86) */
tmp = drm_mode->htotal; /* perform intermediate calcs in u64 */
tmp *= HV_FACTOR * 1000;
do_div(tmp, hperiod);
tmp -= drm_mode->clock % CVT_CLOCK_STEP;
drm_mode->clock = tmp;
/* 18/16. Find actual vertical frame frequency */
/* ignore - just set the mode flag for interlaced */
if (interlaced) {
drm_mode->vtotal *= 2;
drm_mode->flags |= DRM_MODE_FLAG_INTERLACE;
}
/* Fill the mode line name */
drm_mode_set_name(drm_mode);
if (reduced)
drm_mode->flags |= (DRM_MODE_FLAG_PHSYNC |
DRM_MODE_FLAG_NVSYNC);
else
drm_mode->flags |= (DRM_MODE_FLAG_PVSYNC |
DRM_MODE_FLAG_NHSYNC);

return drm_mode;
}

6.5 add_standard_modes

add_standard_modes函数用于从Standard Timings中获取显示模式,并添加到connectorprobed_modes 链表;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* Get standard modes from EDID and add them. Standard modes can be calculated
* using the appropriate standard (DMT, GTF, or CVT). Grab them from EDID and
* add them to the list.
*/
static int add_standard_modes(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
int i, modes = 0;
struct detailed_mode_closure closure = {
.connector = connector,
.drm_edid = drm_edid,
};

// i<8 循环8次,遍历每一个Standard Timing
for (i = 0; i < EDID_STD_TIMINGS; i++) {
struct drm_display_mode *newmode;
// 解析每一个Standard Timing
newmode = drm_mode_std(connector, drm_edid,
&drm_edid->edid->standard_timings[i]);
if (newmode) {
drm_mode_probed_add(connector, newmode);
modes++;
}
}

// 进入
if (drm_edid->edid->revision >= 1)
// 遍历Detailed Timings,依次执行do_standard_modes,对于我使用的HDMI显示器,实际上这里什么也不会做
drm_for_each_detailed_block(drm_edid, do_standard_modes,
&closure);

/* XXX should also look for standard codes in VTB blocks */

return modes + closure.modes;
}
6.5.1 Standard Timings信息

以我使用的HDMI显示器为例,Standard Timings信息如下:

1
2
3
4
5
6
7
8
9
10
(38-53)      Standard Timings

1152x864 @ 60 Hz (4:3 Aspect Ratio)
1280x720 @ 60 Hz (16:9 Aspect Ratio)
1280x1024 @ 60 Hz (5:4 Aspect Ratio)
1440x900 @ 60 Hz (16:10 Aspect Ratio)
1600x900 @ 60 Hz (16:9 Aspect Ratio)
1680x1050 @ 60 Hz (16:10 Aspect Ratio)
1920x1080 @ 60 Hz (16:9 Aspect Ratio)
1920x1200 @ 60 Hz (16:10 Aspect Ratio)
6.5.2 drm_mode_std

drm_mode_std函数定义如下,该函数使用CVT/GTF/DMT算法将Standard Timing(在这种情况下是宽度、Aspect Ratio和刷新率)转换为实际的模式。

下面我们以第一个Standard Timing(两个字节为0x71 0x40)为例进行分析该函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/*
* Take the standard timing params (in this case width, aspect, and refresh)
* and convert them into a real mode using CVT/GTF/DMT.
*/
static struct drm_display_mode *drm_mode_std(struct drm_connector *connector,
const struct drm_edid *drm_edid,
const struct std_timing *t)
{
struct drm_device *dev = connector->dev;
struct drm_display_mode *m, *mode = NULL;
int hsize, vsize;
int vrefresh_rate;
// (0x40 & 3<<6)>>6=1
unsigned aspect_ratio = (t->vfreq_aspect & EDID_TIMING_ASPECT_MASK)
>> EDID_TIMING_ASPECT_SHIFT;
// (0x40 & 3f<<0)>>0=0
unsigned vfreq = (t->vfreq_aspect & EDID_TIMING_VFREQ_MASK)
>> EDID_TIMING_VFREQ_SHIFT;
// 根据edid->revision来判定,应该返回LEVEL_GTF,其值为1
int timing_level = standard_timing_level(drm_edid);

// 跳过
if (bad_std_timing(t->hsize, t->vfreq_aspect))
return NULL;

/* According to the EDID spec, the hdisplay = hsize * 8 + 248 = 0x71*8+248=1152 */
hsize = t->hsize * 8 + 248;
/* vrefresh_rate = vfreq + 60 = 0+60=60 */
vrefresh_rate = vfreq + 60;
/* the vdisplay is calculated based on the aspect ratio */
if (aspect_ratio == 0) {
if (drm_edid->edid->revision < 3)
vsize = hsize;
else
vsize = (hsize * 10) / 16;
} else if (aspect_ratio == 1) // 走这里 1152*3/4=864
vsize = (hsize * 3) / 4;
else if (aspect_ratio == 2)
vsize = (hsize * 4) / 5;
else
vsize = (hsize * 9) / 16;

/* HDTV hack, part 1,跳过 */
if (vrefresh_rate == 60 &&
((hsize == 1360 && vsize == 765) ||
(hsize == 1368 && vsize == 769))) {
hsize = 1366;
vsize = 768;
}

/*
* If this connector already has a mode for this size and refresh
* rate (because it came from detailed or CVT info), use that
* instead. This way we don't have to guess at interlace or
* reduced blanking. 如果此连接器已经具有适合该尺寸和刷新率的模式,则使用该模式
*/
list_for_each_entry(m, &connector->probed_modes, head)
if (m->hdisplay == hsize && m->vdisplay == vsize &&
drm_mode_vrefresh(m) == vrefresh_rate)
return NULL;
/* HDTV hack, part 2,跳过 */
if (hsize == 1366 && vsize == 768 && vrefresh_rate == 60) {
mode = drm_cvt_mode(dev, 1366, 768, vrefresh_rate, 0, 0,
false);
if (!mode)
return NULL;
mode->hdisplay = 1366;
mode->hsync_start = mode->hsync_start - 1;
mode->hsync_end = mode->hsync_end - 1;
return mode;
}

/* check whether it can be found in default mode table,支持rb,进入 */
if (drm_monitor_supports_rb(drm_edid)) {
// 遍历DMT模式列表,查找与给定参数匹配的显示模式(最后一个参数指明需要减少空白)
mode = drm_mode_find_dmt(dev, hsize, vsize, vrefresh_rate,
true);
// NULL
if (mode)
return mode;
}
// 遍历DMT模式列表,查找与给定参数匹配的显示模式(最后一个参数指明不需要减少空白)
mode = drm_mode_find_dmt(dev, hsize, vsize, vrefresh_rate, false);
// NULL
if (mode)
return mode;

/* okay, generate it */
switch (timing_level) {
case LEVEL_DMT:
break;
case LEVEL_GTF: // 进入 create the modeline based on the GTF algorithm,有关GTF算法咱们就深究了
mode = drm_gtf_mode(dev, hsize, vsize, vrefresh_rate, 0, 0);
break;
case LEVEL_GTF2:
mode = drm_gtf2_mode(dev, drm_edid, hsize, vsize, vrefresh_rate);
break;
case LEVEL_CVT: // create a modeline based on the CVT algorithm
mode = drm_cvt_mode(dev, hsize, vsize, vrefresh_rate, 0, 0,
false);
break;
}
return mode;
}

drm_mode_find_dmt函数会遍历DMT模式列表,查找与给定参数匹配的显示模式,并为之创建一个副本;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* drm_mode_find_dmt - Create a copy of a mode if present in DMT
* @dev: Device to duplicate against
* @hsize: Mode width
* @vsize: Mode height
* @fresh: Mode refresh rate
* @rb: Mode reduced-blanking-ness
*
* Walk the DMT mode list looking for a match for the given parameters.
*
* Return: A newly allocated copy of the mode, or NULL if not found.
*/
struct drm_display_mode *drm_mode_find_dmt(struct drm_device *dev,
int hsize, int vsize, int fresh,
bool rb)
{
int i;

// 遍历drm_dmt_modes数组,该数组保存了一组标准显示模式参数,比如640x350@60Hz、800x600@60Hz、1280x800@60Hz等
for (i = 0; i < ARRAY_SIZE(drm_dmt_modes); i++) {
const struct drm_display_mode *ptr = &drm_dmt_modes[i];

// 行有效像素匹配 1152
if (hsize != ptr->hdisplay)
continue;
// 帧有效行匹配 864
if (vsize != ptr->vdisplay)
continue;
// 刷新率匹配
if (fresh != drm_mode_vrefresh(ptr))
continue;
// rb匹配
if (rb != mode_is_rb(ptr))
continue;
// 拷贝副本
return drm_mode_duplicate(dev, ptr);
}

return NULL;
}

这里我们简单介绍一下最后一个参数:

  • Mode reduced-blanking-ness:指的是显示模式的减少空白程度。在视频显示中,每个帧都由可见区域和空白区域组成。可见区域是实际显示图像的部分,而空白区域是没有显示图像的部分。减少空白指的是减少空白区域的时间,从而提高帧率或提供更多的可见图像信息。通过减少空白时间,可以增加视频的连续性和流畅性。

    比如drm_dmt_modes中的项1280x768@60Hz RB1280x768@60Hz,可以看到1280x768@60Hz RBhtotalvtoatl是远小于1280x768@60Hz,因此其空白区域要小很多。

    1
    2
    3
    4
    5
    6
    7
    8
    /* 0x16 - 1280x768@60Hz RB */
    { DRM_MODE("1280x768", DRM_MODE_TYPE_DRIVER, 68250, 1280, 1328,
    1360, 1440, 0, 768, 771, 778, 790, 0, // 1280+160=1440、768+22=790
    DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC) },
    /* 0x17 - 1280x768@60Hz */
    { DRM_MODE("1280x768", DRM_MODE_TYPE_DRIVER, 79500, 1280, 1344,
    1472, 1664, 0, 768, 771, 778, 798, 0, // 1280 + 384 = 1664、768 + 30 = 798
    DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_PVSYNC) },

函数执行完,实际上我们在drm_dmt_modes中无法找到匹配项,不过可以找到一个接近的项1152x864@75Hz

1
2
3
4
/* 0x15 - 1152x864@75Hz */
{ DRM_MODE("1152x864", DRM_MODE_TYPE_DRIVER, 108000, 1152, 1216,
1344, 1600, 0, 864, 865, 868, 900, 0,
DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC) },

我们在下一篇文章会介绍如何新增分辨率时序1152x864@60Hz

6.5.3 do_standard_modes

do_standard_modes函数用于解析EDID_DETAIL_STD_MODES类型的Monitor Descriptor,并添加到connectorprobed_modes 链表,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void
do_standard_modes(const struct detailed_timing *timing, void *c)
{
struct detailed_mode_closure *closure = c;
const struct detailed_non_pixel *data = &timing->data.other_data;
struct drm_connector *connector = closure->connector;
int i;

// 如果不是EDID_DETAIL_STD_MODES类型的Monitor Descriptor,对于我使用的HDMI显示器,直接return
if (!is_display_descriptor(timing, EDID_DETAIL_STD_MODES))
return;

// 遍历每一个std_timing
for (i = 0; i < 6; i++) {
// 获取第i个std_timing
const struct std_timing *std = &data->data.timings[i];
struct drm_display_mode *newmode;

// 创建显示模式,这个函数上面已经介绍过了
newmode = drm_mode_std(connector, closure->drm_edid, std);
if (newmode) {
drm_mode_probed_add(connector, newmode);
closure->modes++;
}
}
}

对于我使用的HDMI显示器,只有最后一个块是Monitor Descriptor,但是其类型为EDID_DETAIL_MONITOR_NAME,因此不会执行drm_mode_std函数。

6.6 add_established_modes

add_standard_modes函数用于从Established Timing获取显示模式,并添加到connectorprobed_modes链表;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
* Get established modes from EDID and add them. Each EDID block contains a
* bitmap of the supported "established modes" list (defined above). Tease them
* out and add them to the global modes list.
*/
static int add_established_modes(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
struct drm_device *dev = connector->dev;
const struct edid *edid = drm_edid->edid;
// 转换为unsigned long类型,描述17个通用时序。若支持,则相应的位为1。
unsigned long est_bits = edid->established_timings.t1 |
(edid->established_timings.t2 << 8) |
((edid->established_timings.mfg_rsvd & 0x80) << 9);
int i, modes = 0;
struct detailed_mode_closure closure = {
.connector = connector,
.drm_edid = drm_edid,
};

// i<=16 循环17次,遍历每一个Established Timing
for (i = 0; i <= EDID_EST_TIMINGS; i++) {
// 位为1,表示支持当前Established Timing
if (est_bits & (1<<i)) {
struct drm_display_mode *newmode;
// edid_est_modes数组中定义了这17个Established Timings对应的显示模式,因此这里实际上就是进行拷贝副本
newmode = drm_mode_duplicate(dev, &edid_est_modes[i]);
if (newmode) {
// 添加到connector的probed_modes链表
drm_mode_probed_add(connector, newmode);
modes++;
}
}
}

// 进入
if (edid->revision >= 1)
// 遍历Detailed Timings,依次执行do_established_modes,对于我使用的HDMI显示器,实际上这里什么也不会做
drm_for_each_detailed_block(drm_edid, do_established_modes,
&closure);

return modes + closure.modes;
}

6.7 edid扩展块(CEA-861D)解析

add_cea_modesadd_alternate_cea_modesadd_displayid_detailed_modes都是用于从edid扩展块中获取显示模式,并添加到connectorprobed_modes 链表;关于具体实现就不展开说了;

6.7.1 add_cea_modes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static int add_cea_modes(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
const struct cea_db *db;
struct cea_db_iter iter;
int modes;

/* CTA VDB block VICs parsed earlier */
modes = add_cta_vdb_modes(connector);

cea_db_iter_edid_begin(drm_edid, &iter);
// 遍历edid扩展块 Data Blocks
cea_db_iter_for_each(db, &iter) {
// Vendor Specific data block?
if (cea_db_is_hdmi_vsdb(db)) {
// Parse the HDMI Vendor Specific data block
modes += do_hdmi_vsdb_modes(connector, (const u8 *)db,
cea_db_payload_len(db));
} else if (cea_db_is_y420vdb(db)) {
const u8 *vdb420 = cea_db_data(db) + 1;

/* Add 4:2:0(only) modes present in EDID */
// Parse the CEA-861-F YCBCR 420 Video Data Block (Y420VDB)
modes += do_y420vdb_modes(connector, vdb420,
cea_db_payload_len(db) - 1);
}
}
cea_db_iter_end(&iter);

return modes;
}
6.7.2 add_alternate_cea_modes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
static int add_alternate_cea_modes(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
struct drm_device *dev = connector->dev;
struct drm_display_mode *mode, *tmp;
LIST_HEAD(list);
int modes = 0;

/* Don't add CTA modes if the CTA extension block is missing */
if (!drm_edid_has_cta_extension(drm_edid))
return 0;

/*
* Go through all probed modes and create a new mode
* with the alternate clock for certain CEA modes.
*/
list_for_each_entry(mode, &connector->probed_modes, head) {
const struct drm_display_mode *cea_mode = NULL;
struct drm_display_mode *newmode;
u8 vic = drm_match_cea_mode(mode);
unsigned int clock1, clock2;

if (drm_valid_cea_vic(vic)) {
cea_mode = cea_mode_for_vic(vic);
clock2 = cea_mode_alternate_clock(cea_mode);
} else {
vic = drm_match_hdmi_mode(mode);
if (drm_valid_hdmi_vic(vic)) {
cea_mode = &edid_4k_modes[vic];
clock2 = hdmi_mode_alternate_clock(cea_mode);
}
}

if (!cea_mode)
continue;

clock1 = cea_mode->clock;

if (clock1 == clock2)
continue;

if (mode->clock != clock1 && mode->clock != clock2)
continue;

newmode = drm_mode_duplicate(dev, cea_mode);
if (!newmode)
continue;

/* Carry over the stereo flags */
newmode->flags |= mode->flags & DRM_MODE_FLAG_3D_MASK;

/*
* The current mode could be either variant. Make
* sure to pick the "other" clock for the new mode.
*/
if (mode->clock != clock1)
newmode->clock = clock1;
else
newmode->clock = clock2;

list_add_tail(&newmode->head, &list);
}

list_for_each_entry_safe(mode, tmp, &list, head) {
list_del(&mode->head);
drm_mode_probed_add(connector, mode);
modes++;
}

return modes;
}
6.7.3 add_displayid_detailed_modes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int add_displayid_detailed_modes(struct drm_connector *connector,
const struct drm_edid *drm_edid)
{
const struct displayid_block *block;
struct displayid_iter iter;
int num_modes = 0;

displayid_iter_edid_begin(drm_edid, &iter);
displayid_iter_for_each(block, &iter) {
if (block->tag == DATA_BLOCK_TYPE_1_DETAILED_TIMING ||
block->tag == DATA_BLOCK_2_TYPE_7_DETAILED_TIMING)
num_modes += add_displayid_detailed_1_modes(connector, block);
}
displayid_iter_end(&iter);

return num_modes;
}

参考文章

[1] [Rockchip_Developer_Guide_HDMI_CN]

[2] Linux驱动学习–HDMI开发(一) 相关协议及传输原理的介绍

[3] LINUX驱动学习–HDMI开发(二)HDMI驱动源码分析(RK平台)

[4] HDMI协议介绍

[5] HDMI接口电路设计

[7] HDMI接口协议

[8] HDMI协议1.4 好文推荐!

[9] DRM框架介绍及基于DRM框架的HDMI开发

[10] DesignWare HDMI IP 解决方案

[11] LVDS+HDMI输出特殊分辨率800*1280竖屏

[12] Linux DRM那些事-HDMI接口EDID获取

[13] EDID的简介和解析

[14] 什么是EDIDEDID能做什么,EDID基本介绍