Rockchip RK3399 - DRM HDMI驱动程序
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4
开发板
eMMC
:16GB
LPDDR3
:4GB
显示屏 :15.6
英寸HDMI
接口显示屏
u-boot
:2023.04
linux
:6.3
----------------------------------------------------------------------------------------------------------------------------
在《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
采用了Synopsys
的DesignWare HDMI IP
解决方案,因此hdmi
驱动的核心实现是位于drivers/gpu/drm/bridge/synopsys/
目录下的,而Rockchip
仅仅是对其进行一层封装。
在介绍hdmi
驱动之前,我们首先思考一个问题,假设我们自己是一个画家,现在我手里有一个笔、还有一张纸,然后我打算在纸上绘画一个卡通人物,接下来我们会怎么做呢?
-
首先我们需要对我们绘画使用的纸的尺寸有一个了解;
- 如果是
A3
上的图纸,那么我就会在大脑里构思一个A3
大小的卡通人物; - 如果是
A4
,那么我就会在大脑里构思一个A4
大小的卡通人物; - 总之我们的目的是要让卡通人物占满整张纸;
- 如果是
-
接下来我们就会使用不同颜色的画笔开始在纸上绘画了,而我们的绘画过程呢,就是将大脑中构思的卡通人物按照从左到右、从上到下一笔一笔的勾勒出来;
同样的,类比到DRM
显示子系统中;
hdmi
显示器等价于绘画的纸:LCD
驱动器会将接收到的数据在显示器上显示出来;RK3399 crtc
等价于画笔:crtc
从framebuffer
中读取待显示的图像,并按照响应的格式输出给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
;
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
,分别连接到vopl
和vopb
;也就是在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
中为以下节点新增属性:
&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
函数中调用:
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;
......
}
其中:
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
;
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"
的设备节点;
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
函数;
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_hdmi
、struct dw_hdmi_plat_data
; - 由
Rochchip hdmi
相关驱动定义:比如struct rockchip_hdmi
,struct 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
;
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
;cec
:CEC platform device
;dev
:hdmi
设备;isfr_clk
、iahb_clk
、cec_clk
:hdmi
相关的时钟;plat_data
:dw hdmi
平台数据;ddc
:存储DDC
通道使用的I2C
总线适配器;edid
:存放edid
信息;regs
:hdmi
相关寄存器基址的虚拟地址;pinctrl
、default_state
、unwedge_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
;
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
定义在如下:
struct dw_hdmi_mpll_config {
unsigned long mpixelclock;
struct {
u16 cpce;
u16 gmp;
} res[DW_HDMI_RES_MAX];
};
各项参数说明如下:
mpixelclock
:像素时钟;cpce
:OPMODE_PLLCFG
寄存器值;gmp
:PLLGMPCTRL
寄存器值;
3.1.3 struct edid
linux
使用struct edid
描述edid
主块信息;
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
edid
的Established Timings
信息在linux
中使用struct est_timings
表示;
struct est_timings {
u8 t1;
u8 t2;
u8 mfg_rsvd;
} __attribute__((packed));
3.1.5 struct std_timing
edid
的Standard Timings
信息在linux
中使用struct std_timing
表示;
/* 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
;
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=483390000Hz
,TMDS
时钟频率为483390000Hz*10=4833900KHz
所以pixel_clock
的单位为10kHZ
;
(1) struct detailed_pixel_timing
/* 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
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
设备。
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
的指针;regmap
:指向寄存器映射的struct regmap
d额指针;encoder
:指向Rockchip
平台定义的encoder
指针;chip_data
:指向Rockchip
平台定义的hdmi data
指针;ref_clk
:ref
时钟;grf_clk
:grf
时钟;avdd_0v9
:0.9V
稳压器;avdd_1v8
:1.8V
稳压器;phy
:指向HDMI PHY
的struct phy
的指针;
3.2.2 struct rockchip_hdmi_chip_data
struct rockchip_hdmi_chip_data
定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
,用于描述不同型号的Rockchip
芯片的HDMI
接口配置信息;
/**
* 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
输出;lcdsel_big
:表示在GRF
寄存器中lcdsel_grf_reg
偏移位置处设置的值,用于选择vopb
进行HDMI
输出;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
进行了扩充;
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
设备节点,涉及到clocks
、rockchip,grf
、avdd-0v9
、avdd-1v8
、以及endpoint
子节点; - 初始化
encoder
; - 构造
dw_hdmi_bind
函数需要的参数,尤其是第三个参数rk3399_hdmi_drv_data
,最后调用dw_hdmi_bind
进入到DesignWare hdmi
驱动;
具体代码如下:
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;
// 基于hdmi设备节点的信息,确定特定encoder端口可能连接的CRTC
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
;这个函数的作用是基于hdmi
设备节点中的信息,确定特定encoder
端口可能连接的CRTC
;
/**
* 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,并向上查找port设备节点,即vopb_out
remote_port = of_graph_get_remote_port(ep);
// 无效节点,进入
if (!remote_port) {
of_node_put(ep);
return 0;
}
// 下文介绍,根据remote_port设备节点,查找对应的crtc(这里即vopb)
possible_crtcs |= drm_of_crtc_port_mask(dev, remote_port);
of_node_put(remote_port);
}
return possible_crtcs;
}
以hdmi
节点为例,其有两个endpoint
子节点;
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
设备节点。
drm_of_crtc_port_mask
定义如下:
/**
* 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设备节点
{
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) // tmp为vopb时匹配
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
设备节点下port=0
、reg=0
的endpoint
设备节点,然后获取该节点remote-endpoint
属性指定的设备节点的reg
属性的值,并将其赋值给rkencoder->crtc_endpoint_id
;
/*
* 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;
// 通过遍历np设备节点的所有端点节点来查找符合指定port=0和reg=0的端点节点,这里返回的是hdmi_in_vopb设备节点
en = of_graph_get_endpoint_by_regs(np, port, reg);
if (!en)
return -ENOENT;
// 获取hdmi_in_vopb设备节点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
,其作用就是通过遍历parent
设备节点的所有端点节点来查找符合指定 port_reg
和 reg
标识符的端点节点;
/**
* 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)
首先查找reg=0
的port
,即hdmi_in
,然后查找reg=0
的端点节点,也就是hdmi_in_vopb
设备节点;
hdmi: hdmi@ff940000 {
......
ports {
hdmi_in: port { # 该节点和port_reg=0匹配, 节点属性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
,其作用就是获取与指定本地端点相关联的远程端点节点;
/**
* 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
;
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
中;
/**
* 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
设备节点为例:
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
设备节点;比如clocks
、rockchip,grf
、avdd-xxx
等属性;
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
;
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
:
/* 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(®ulator_list_mutex);
ret = (rdev->coupling_desc.n_resolved != rdev->coupling_desc.n_coupled);
mutex_unlock(®ulator_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
查找rdev
,regulator_map_list
在regulator_rdev
注册的时候初始化的;
如果找不到将返回dummy regulator
,并输入警告信息。
由于我们并没有在hdmi
设备节点中定义avdd-0v9-supply
属性,同时也没有在设备树定义regulator-name = "avdd-0v9"
的regulator_rdev
,因此 我们内核在启动时会输入如下警告信息:
[ 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
(物理层设备)的引用;
/**
* 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
输出;
/**
* 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
;
/* -----------------------------------------------------------------------------
* 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;
// 将bridge连接到encoder的链中
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_data
;rk3399_hdmi_drv_data
定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c
:
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_cfg
、rockchip_cur_ctr
、rockchip_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_config
、curr_ctrl
和phy_config
的指针。 - 然后,它通过遍历
mpll_config
、curr_ctrl
和phy_config
数组,找到与给定mpixelclock
(像素时钟频率)匹配的配置条目。一旦找到匹配的条目,就会使用dw_hdmi_phy_i2c_write
函数将对应的配置值写入HDMI PHY
寄存器中; - 在最后一部分,代码还覆盖并禁用了时钟终端,并将特定的值写入了相应的
PHY
寄存器;
/*
* 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
函数用于校验显示模式是否有效;
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
保存的是RK3399
的 HDMI-PHY-PLL
配置,用于配置HDMI_3D_TX_PHY_CPCE_CTRL
、HDMI_3D_TX_PHY_GMPCTRL
寄存器;
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
定义如下:
struct dw_hdmi_mpll_config {
unsigned long mpixelclock;
struct {
u16 cpce;
u16 gmp;
} res[DW_HDMI_RES_MAX];
};
各项参数说明如下:
mpixelclock
:像素时钟频率;cpce
:OPMODE_PLLCFG
寄存器值;gmp
:PLLGMPCTRL
寄存器值;
以rockchip_mpll_cfg
中的第一项配置为例:
{
27000000, {
{ 0x00b3, 0x0000},
{ 0x2153, 0x0000},
{ 0x40f3, 0x0000}
},
}
27000000
表示像素时钟为27000000
及以下的分辨率适用该项配置, {0x00b3, 0x0000 }
、 { 0x2153, 0x0000 }
、 { 0x40f3, 0x0000 }
三项依次对应色深为 8 BIT
、10BIT
、12 BIT
(目前Rockchip
方案实际只支持8/10 bit
两种模式) 情况下使用的配置。
由于参数的取值需要查阅PHY
的datasheet
获取,若需要新增HDMI-PHY-PLL
配置,可以向FAE
提出所需的像素时钟。然后根据上述的规则,将新增的配置添加到rockchip_mpll_cfg
中。
4.6.3 rockchip_cur_ctr
具体作用不晓得,用于配置HDMI_3D_TX_PHY_CURRCTRL
寄存器;
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_TXTERM
、HDMI_3D_TX_PHY_CKSYMTXCTRL
、HDMI_3D_TX_PHY_VLEVCTRL
寄存器;
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
#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
;
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_clk
、iahb_clk
、cec_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
; - 初始化桥接设备,设置回调
funcs
为dw_hdmi_bridge_funcs
; - 调用
drm_bridge_add
添加hdmi
桥接设备;
5.1 dw_hdmi_parse_dt
dw_hdmi_parse_dt
函数用于解析hdmi
设备节点,初始化hdmi
成员;
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
:
/**
* 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
控制器;
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
为中断处理函数;
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
相关寄存器进行读操作;
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
相关寄存器进行写操作;
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
文件;
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
函数用于将bridge
连接到encoder
的链中
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
;
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_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,
.atomic_check = dw_hdmi_connector_atomic_check,
};
get_modes
用于通过DDC
探测的所有显示模式,并将其添加到connector
的 probed_modes
链表中。
(2)connector
的控制函数func
设置为dw_hdmi_connector_funcs
;
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
函数的通用实现。基本过程如下:
-
将
connector
的modes
链表中的所有显示模式标记为过时; -
使用
drm_mode_probed_add
将新显示模式添加到connector
的probed_modes
链表中,新显示模式的状态初始值为OK
。显示模式从单个来源按照以下优先顺序添加;drm_connector_helper_funcs.get_modes
回调函数(drm_helper_probe_get_modes
);- 如果
connector
状态为connected
并且drm_helper_probe_get_modes
未获取到显示模式,则自动添加标准的VESA DMT
显示模式,最高分辨率为1024x768
(drm_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
过滤掉大于maxX
和maxY
(如果有指定)的模式;drm_mode_validate_flag
根据基本连接器能力(允许交错,允许双扫描,允许立体)检查模式;- 可选的
drm_connector_helper_funcs.mode_valid
或drm_connector_helper_funcs.mode_valid_ctx
辅助函数可以执行驱动程序和/或显示器特定的检查; - 可选的
drm_crtc_helper_funcs.mode_valid
,drm_bridge_funcs.mode_valid
(会调用dw_hdmi_bridge_mode_valid
函数进行校验)和drm_encoder_helper_funcs.mode_valid
辅助函数可以执行驱动程序和/或源特定的检查,这些辅助函数也由modeset
/atomic
辅助函数执行;
-
从
connector
的modes
链表中去除任何状态不为OK
的模式,同时输出调试消息指示模式被拒绝的原因(drm_mode_prune_invalid
)。
这里我们简单说一下drm_xxx_helper_funcs
和drm_xxx_funcs
的区别;
drm_connector_funcs
是应用层进行drm ioctl
操作是的最终入口,对于大多数的SoC
厂商来说,他们的drm_xxx_funcs
操作流程基本相同,仅仅是在寄存器配置上存在差异,因此开发者将那些通用的操作流程封装了helper
函数,而将那些厂商差异化的代码放到了drm_xxx_helper_funcs
中去,由SoC
厂商自己实现。
比如dw_hdmi_connector_funcs
中的fill_modes
、reset
、atomic_duplicate_state
等都是使用的通用的helper
函数,他们定义在drivers/gpu/drm/drm_probe_helper.c
、drivers/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
信息;
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 分析小结
经过分析我们发现无论是bridget
的funcs
中的get_edid
还是connector
的helper_private
中get_modes
都会调用dw_hdmi_get_edid
获取连接器的edid
信息;
// 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
中;
/**
* 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_funcs
的get_modes
用于通过DDC
探测到connector
的所有显示模式,并将其添加到connector
的 probed_modes
链表中,这些模式还没有经过筛选和过滤;
需要注意的是:在探测阶段,系统可能会探测到一些暂时不可用或不推荐的显示模式,这些模式会先被存储在 probed_modes
中。之后,这些模式可能会经过进一步的处理和筛选,最终加入到 modes
中成为最终可用的显示模式列表。
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
获取connector
的edid
信息; - 通过
drm_add_edid_modes
函数解析edid
信息,并将其转换为显示模式,添加到connector
的probed_modes
链表;
dw_hdmi_connector_get_modes
函数位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c
文件
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
,用于获取connector
的edid
信息;
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
;
/**
* 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
信息,并将其转换为显示模式,添加到connector
的 probed_modes
链表。
在这之前,我们需要大概了解一下CVT
/GTF
/DMT
,因此后面在分析代码讲解如何解析edid
并将其转换为显示模式的时候会有所涉及。CVT
(Coordinated Video Timing
)、GTF
(Generalized Timing Formula
)和DMT
(Display Monitor Timings
)是三种不同的显示器时序规范。它们在计算和定义显示模式参数方面有所不同;
DMT
(Display Monitor Timings
):DMT
是由VESA
(Video Electronics Standards Association
)定义的一组标准显示模式参数。DMT
规范列出了一些预定义的显示模式,包括常见的分辨率、刷新率和时序参数。这些参数在不同的显示设备上是通用的,可以提供简单的配置和兼容性。linux
内核中将标准显示模式存放在数组drm_dmt_modes
,位于drivers/gpu/drm/drm_edid.c
文件;GTF
(GeneralizedTimingFormula
):GTF
是一种更高级的算法,它可以生成更灵活的显示模式。GTF
算法基于显示器的物理特性和电信号时序的数学模型,通过计算参数来生成显示模式。GTF
算法允许更精细的控制,可以生成几乎任意分辨率、刷新率和纵横比的显示模式;内核实现函数有:drm_gtf_mode
、drm_gtf2_mode
,定义在drivers/gpu/drm/drm_modes.c
;CVT
(CoordinatedVideoTiming
):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
;
/**
* 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_edid
和 edid
;
/*
* 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
信息;
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
代码如下:
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
显示器为例分析如下代码:
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
规范中定义的优先级顺序来解析的;
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
中获取显示模式,并添加到connector
的 probed_modes
链表;
/*
* 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
信息如下:
(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 Timing
为HDMI
显示器的最佳时序: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
,创建显示模式并添加到connector
的 probed_modes
链表;
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
为例进行分析该函数;
/*
* 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
时序参数如下;
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
中表示的含义依次为name
、clock
(单位为MHz
)、hdisplay
、hsync_start
、hsync_end
、htotal
、vdisplay
、vsync_start
、vsync_end
、vtotal
、flag
,有关这些时序参数我们已经在《Rockchip RK3399 - DRM crtc
基础知识》中详细介绍了;
![img](https://gitee.com/zyly2033/blog-pic/raw/master/202311162332330.png)
![img](https://gitee.com/zyly2033/blog-pic/raw/master/202311181042673.png)
这里我们在说一下刷新率120Hz
是如何得到的:$$刷新率=\frac{像素时钟频率}{htotalvtotal}=\frac{483390000}{27201481}=120$$,也就是说每秒刷新120
帧图片。
6.4 add_cvt_modes
add_cvt_modes
函数和add_detailed_modes
类似,均是用于从Detailed Timings
中获取显示模式,并添加到connector
的 probed_modes
链表;只不过这个函数遍历Detailed Timings
,依次执行的是do_cvt_mode
,其基于CVT
算法生成显示模式;
在介绍函数之前,我们简单来说一下什么是CVT
,CVT
算法是一种用于计算显示器显示模式参数的算法,使用计算出的参数来创建Modeline
;
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
算法创建显示模式并添加到connector
的 probed_modes
链表;
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
函数定义如下;
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
,这个函数根据hdisplay
、vdisplay
和vrefresh
调用CVT
算法来生成Modeline
。它基于Graham Loveridge
于2003年4月9日编写的VESA(TM) Coordinated Video Timing Generator
。
函数定义在drivers/gpu/drm/drm_modes.c
;
/**
* 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
中获取显示模式,并添加到connector
的 probed_modes
链表;
/*
* 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
信息如下:
(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
)为例进行分析该函数;
/*
* 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
模式列表,查找与给定参数匹配的显示模式,并为之创建一个副本;
/*
* 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 RB
,1280x768@60Hz
,可以看到1280x768@60Hz RB
的htotal
、vtoatl
是远小于1280x768@60Hz
,因此其空白区域要小很多。/* 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
;
/* 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
,并添加到connector
的 probed_modes
链表,定义如下:
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
获取显示模式,并添加到connector
的 probed_modes
链表;
/*
* 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_modes
、add_alternate_cea_modes
、add_displayid_detailed_modes
都是用于从edid
扩展块中获取显示模式,并添加到connector
的 probed_modes
链表;关于具体实现就不展开说了;
6.7.1 add_cea_modes
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
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
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 好文推荐!
[11] LVDS+HDMI
输出特殊分辨率800*1280
竖屏
[12] Linux DRM
那些事-HDMI
接口EDID
获取
[13] EDID
的简介和解析