程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

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/bridge/synopsys/

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

对于DesignWare HDMI控制器,通常会有与之配套的PHY驱动,以确保HDMI信号能够正确地通过电缆传输,具体来说:

  • HDMI控制器负责视频和音频数据的处理、编码以及通过HDMI协议传输这些数据;
  • HDMI PHY则负责将来自控制器的数字信号转换为符合HDMI规范的电气信号,进行物理层的传输;
  • 对于当前我使用的Linux内核版本,DesignWare HDMI PHY驱动被集成在 drivers/gpu/drm/bridge/synopsys/dw_hdmi.c 驱动文件中,我们在drivers/phy/rockchip/目录下并没有找到单独的DesignWare HDMI PHY驱动;

在介绍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)是三种不同的显示器时序规范。它们在计算和定义显示模式参数方面有所不同;

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
2023-09-06*源19
2023-09-11*朝科88
2023-09-21*号5
2023-09-16*真60
2023-10-26*通9.9
2023-11-04*慎0.66
2023-11-24*恩0.01
2023-12-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
2024-09-06*强8.8
2024-09-09*波1
2024-09-10*口1
2024-09-10*波1
2024-09-12*波10
2024-09-18*明1.68
2024-09-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(1958)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示