Rockchip RK3399 - DRM HDMI驱动程序

发布时间 2023-10-20 22:45:14作者: 大奥特曼打小怪兽

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

一、设备树配置

1.1 hdmi设备节点

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

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

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可以和VOPLVOPB连接;

因此可以得到有2条同理:

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

需要注意的是,⼀个显⽰接口在同⼀个时刻只能和⼀个VOP连接,所以在具体的板级配置中,需要设备树中把要使⽤的通路打开,把不使⽤的通路设置为disabled状态。

1.2 启用hdmi

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

&i2c7 {
        status = "okay";
};

&display_subsystem {
         status = "okay";
};

&vopb {
        status = "okay";
};

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


&hdmi_in_vopb{
        status = "okay";
};

&hdmi_in_vopl{
        status = "disabled";
};

二、platfrom driver

rockchip_drm_init函数中调用:

ADD_ROCKCHIP_SUB_DRIVER(dw_hdmi_rockchip_pltfm_driver,
            CONFIG_ROCKCHIP_DW_HDMI);

会将dw_hdmi_rockchip_pltfm_driver保存到rockchip_sub_drivers数组中。

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

2.1 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.1.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.1.2 probe

plaftrom总线设备驱动模型中,我们知道当内核中有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函数的实现,这个我们单独小节介绍。

2.2 dw_hdmi_rockchip_bind

dw_hdmi_rockchip_bind函数定义在drivers/gpu/drm/rockchip/dw_hdmi-rockchip.c,涉及到对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;

        encoder->possible_crtcs = drm_of_find_possible_crtcs(drm, dev->of_node);
        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设备节点
        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;
        }

        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;
        }

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

        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));
        }

        drm_encoder_helper_add(encoder, &dw_hdmi_rockchip_encoder_helper_funcs);
        drm_simple_encoder_init(drm, encoder, DRM_MODE_ENCODER_TMDS);

        platform_set_drvdata(pdev, 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;
}

三、相关数据结构

3.1 struct drm_encoder

linux内核使用struct drm_encoder来表示一个编码器,用于连接CRT控制器和显示设备。

3.2 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;
};

3.3 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;
};

3.4 struct rockchip_hdmi

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

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;
};
3.4.1 struct rockchip_hdmi_chip_data
/**
 * 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;
};

参考文章

[1] [Rockchip HDMI软件开发指南]