程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

Rockchip RK3399 - DRM HDMI驱动程序

----------------------------------------------------------------------------------------------------------------------------

开发板 :NanoPC-T4开发板
eMMC16GB
LPDDR34GB
显示屏 :15.6英寸HDMI接口显示屏
u-boot2023.04
linux6.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采用了SynopsysDesignWare HDMI IP解决方案,因此hdmi驱动的核心实现是位于drivers/gpu/drm/bridge/synopsys/目录下的,而Rockchip仅仅是对其进行一层封装。

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

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

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

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

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

一、设备树配置

1.1 hdmi设备节点

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

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

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

hdmi: hdmi@ff940000 {
		compatible = "rockchip,rk3399-dw-hdmi";
		reg = <0x0 0xff940000 0x0 0x20000>;
		interrupts = <GIC_SPI 23 IRQ_TYPE_LEVEL_HIGH 0>;
		clocks = <&cru PCLK_HDMI_CTRL>,
				 <&cru SCLK_HDMI_SFR>,
				 <&cru SCLK_HDMI_CEC>,
				 <&cru PCLK_VIO_GRF>,
				 <&cru PLL_VPLL>;
		clock-names = "iahb", "isfr", "cec", "grf", "ref";
		power-domains = <&power RK3399_PD_HDCP>;
		reg-io-width = <4>;
		rockchip,grf = <&grf>;
		#sound-dai-cells = <0>;
		status = "disabled";

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

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

其中:

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

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

  • vopb_out_hdmi ---> hdmi_in_vopb
  • vopl_out_hdmi ---> hdmi_in_vopl

需要注意的是:

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

1.2 启用hdmi

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

&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_hdmistruct dw_hdmi_plat_data
  • Rochchip hdmi相关驱动定义:比如struct rockchip_hdmistruct rockchip_hdmi_chip_data

3.1 DesignWare hdmi

3.1.1 struct dw_hdmi

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

struct dw_hdmi {
        struct drm_connector connector;
        struct drm_bridge bridge;
        struct drm_bridge *next_bridge;

        unsigned int version;

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

        struct hdmi_data_info hdmi_data;
        const struct dw_hdmi_plat_data *plat_data;

        int vic;

        u8 edid[HDMI_EDID_LEN];

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

        struct drm_display_mode previous_mode;

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

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

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

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

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

        struct mutex cec_notifier_mutex;
        struct cec_notifier *cec_notifier;

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

其中:

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

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

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:像素时钟;
  • cpceOPMODE_PLLCFG寄存器值;
  • gmpPLLGMPCTRL寄存器值;
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

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

struct est_timings {
        u8 t1;
        u8 t2;
        u8 mfg_rsvd;
} __attribute__((packed));
3.1.5 struct std_timing

edidStandard 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=483390000HzTMDS时钟频率为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 regmapd额指针;
  • encoder:指向Rockchip平台定义的encoder指针;
  • chip_data:指向Rockchip平台定义的hdmi data指针;
  • ref_clkref时钟;
  • grf_clkgrf时钟;
  • avdd_0v90.9V稳压器;
  • avdd_1v81.8V稳压器;
  • phy:指向HDMI PHYstruct 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设备节点,涉及到clocksrockchip,grfavdd-0v9avdd-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=0reg=0endpoint设备节点,然后获取该节点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_regreg 标识符的端点节点;

/**
 * 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=0port,即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设备节点;比如clocksrockchip,grfavdd-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(&regulator_list_mutex);
        ret = (rdev->coupling_desc.n_resolved != rdev->coupling_desc.n_coupled);
        mutex_unlock(&regulator_list_mutex);

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

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

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

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

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

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

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

        return regulator;
}

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

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

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

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

[    1.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_datark3399_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_cfgrockchip_cur_ctrrockchip_phy_config中存放的都是HDMI PHY配置参数,会被hdmi_phy_configure_dwc_hdmi_3d_tx函数使用,用于配置DWC HDMI 3D TX PHY的物理层(PHY),该函数位于drivers/gpu/drm/bridge/synopsys/dw-hdmi.c

  • 首先从提供的plat_data结构中获取mpll_configcurr_ctrlphy_config的指针。
  • 然后,它通过遍历mpll_configcurr_ctrlphy_config数组,找到与给定mpixelclock(像素时钟频率)匹配的配置条目。一旦找到匹配的条目,就会使用dw_hdmi_phy_i2c_write函数将对应的配置值写入HDMI PHY寄存器中;
  • 在最后一部分,代码还覆盖并禁用了时钟终端,并将特定的值写入了相应的PHY寄存器;
/*
 * 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保存的是RK3399HDMI-PHY-PLL配置,用于配置HDMI_3D_TX_PHY_CPCE_CTRLHDMI_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 :像素时钟频率;
  • cpceOPMODE_PLLCFG寄存器值;
  • gmpPLLGMPCTRL寄存器值;

rockchip_mpll_cfg中的第一项配置为例:

{
	27000000, {
		{ 0x00b3, 0x0000},
		{ 0x2153, 0x0000},
		{ 0x40f3, 0x0000}
    },
}

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

4.6.3 rockchip_cur_ctr

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

static const struct dw_hdmi_curr_ctrl rockchip_cur_ctr[] = {
        /*      pixelclk    bpp8    bpp10   bpp12 */
        {
                40000000,  { 0x0018, 0x0018, 0x0018 },
        }, {
                65000000,  { 0x0028, 0x0028, 0x0028 },
        }, {
                66000000,  { 0x0038, 0x0038, 0x0038 },
        }, {
                74250000,  { 0x0028, 0x0038, 0x0038 },
        }, {
                83500000,  { 0x0028, 0x0038, 0x0038 },
        }, {
                146250000, { 0x0038, 0x0038, 0x0038 },
        }, {
                148500000, { 0x0000, 0x0038, 0x0038 },
        }, {
                ~0UL,      { 0x0000, 0x0000, 0x0000},
        }
};

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

4.6.4 rockchip_phy_config

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

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
  • 初始化桥接设备,设置回调funcsdw_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_privatedw_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探测的所有显示模式,并将其添加到connectorprobed_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函数的通用实现。基本过程如下:

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

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

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

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

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

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

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

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

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

5.4.2 dw_hdmi_bridge_get_edid

dw_hdmi_bridge_get_edid用于获取edid信息;

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

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

// 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_funcsget_modes用于通过DDC探测到connector的所有显示模式,并将其添加到connectorprobed_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获取connectoredid信息;
  • 通过drm_add_edid_modes函数解析edid信息,并将其转换为显示模式,添加到connectorprobed_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,用于获取connectoredid信息;

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信息,并将其转换为显示模式,添加到connectorprobed_modes 链表。

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

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

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

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

/**
 * drm_add_edid_modes - add modes from EDID data, if available
 * @connector: connector we're probing
 * @edid: EDID data
 *
 * Add the specified modes to the connector's mode list. Also fills out the
 * &drm_display_info structure and ELD in @connector with any information which
 * can be derived from the edid.
 *
 * This function is deprecated. Use drm_edid_connector_add_modes() instead.
 *
 * Return: The number of modes added or 0 if we couldn't find any.
 */
int drm_add_edid_modes(struct drm_connector *connector, struct edid *edid)
{
        struct drm_edid _drm_edid;
        const struct drm_edid *drm_edid;

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

        drm_edid = drm_edid_legacy_init(&_drm_edid, edid);

        update_display_info(connector, drm_edid);

        return _drm_edid_connector_add_modes(connector, drm_edid);
}

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

6.2.1 drm_edid_legacy_init

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

/*
 * 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中获取显示模式,并添加到connectorprobed_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 TimingHDMI显示器的最佳时序:2560x1440 @ 120Hz

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

6.3.2 do_detailed_mode

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

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

img img

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

6.4 add_cvt_modes

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

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

static int
add_cvt_modes(struct drm_connector *connector, const struct drm_edid *drm_edid)
{
        struct detailed_mode_closure closure = {
                .connector = connector,
                .drm_edid = drm_edid,
        };

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

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

        return closure.modes;
}
6.4.1 do_cvt_mode

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

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,这个函数根据hdisplayvdisplayvrefresh调用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中获取显示模式,并添加到connectorprobed_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 RB1280x768@60Hz,可以看到1280x768@60Hz RBhtotalvtoatl是远小于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,并添加到connectorprobed_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获取显示模式,并添加到connectorprobed_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_modesadd_alternate_cea_modesadd_displayid_detailed_modes都是用于从edid扩展块中获取显示模式,并添加到connectorprobed_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 好文推荐!

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

[10] DesignWare HDMI IP 解决方案

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

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

[13] EDID的简介和解析

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

posted @ 2023-11-06 19:12  大奥特曼打小怪兽  阅读(811)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步