// SPDX-License-Identifier: GPL-2.0+ /* * Meson8, Meson8b and Meson8m2 HDMI TX PHY. * * Copyright (C) 2021 Martin Blumenstingl */ #include #include #include #include #include #include #include #include #include #include /* * Unfortunately there is no detailed documentation available for the * HHI_HDMI_PHY_CNTL0 register. CTL0 and CTL1 is all we know about. * Magic register values in the driver below are taken from the vendor * BSP / kernel. */ #define HHI_HDMI_PHY_CNTL0 0x3a0 #define HHI_HDMI_PHY_CNTL0_HDMI_CTL1 GENMASK(31, 16) #define HHI_HDMI_PHY_CNTL0_HDMI_CTL0 GENMASK(15, 0) #define HHI_HDMI_PHY_CNTL1 0x3a4 #define HHI_HDMI_PHY_CNTL1_CLOCK_ENABLE BIT(1) #define HHI_HDMI_PHY_CNTL1_SOFT_RESET BIT(0) #define HHI_HDMI_PHY_CNTL2 0x3a8 struct phy_meson8_hdmi_tx_priv { struct regmap *hhi; struct clk *tmds_clk; }; static int phy_meson8_hdmi_tx_init(struct phy *phy) { struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); return clk_prepare_enable(priv->tmds_clk); } static int phy_meson8_hdmi_tx_exit(struct phy *phy) { struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); clk_disable_unprepare(priv->tmds_clk); return 0; } static int phy_meson8_hdmi_tx_power_on(struct phy *phy) { struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); unsigned int i; u16 hdmi_ctl0; if (clk_get_rate(priv->tmds_clk) >= 2970UL * 1000 * 1000) hdmi_ctl0 = 0x1e8b; else hdmi_ctl0 = 0x4d0b; regmap_write(priv->hhi, HHI_HDMI_PHY_CNTL0, FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL1, 0x08c3) | FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL0, hdmi_ctl0)); regmap_write(priv->hhi, HHI_HDMI_PHY_CNTL1, 0x0); /* Reset three times, just like the vendor driver does */ for (i = 0; i < 3; i++) { regmap_write(priv->hhi, HHI_HDMI_PHY_CNTL1, HHI_HDMI_PHY_CNTL1_CLOCK_ENABLE | HHI_HDMI_PHY_CNTL1_SOFT_RESET); usleep_range(1000, 2000); regmap_write(priv->hhi, HHI_HDMI_PHY_CNTL1, HHI_HDMI_PHY_CNTL1_CLOCK_ENABLE); usleep_range(1000, 2000); } return 0; } static int phy_meson8_hdmi_tx_power_off(struct phy *phy) { struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); regmap_write(priv->hhi, HHI_HDMI_PHY_CNTL0, FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL1, 0x0841) | FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL0, 0x8d00)); return 0; } static const struct phy_ops phy_meson8_hdmi_tx_ops = { .init = phy_meson8_hdmi_tx_init, .exit = phy_meson8_hdmi_tx_exit, .power_on = phy_meson8_hdmi_tx_power_on, .power_off = phy_meson8_hdmi_tx_power_off, .owner = THIS_MODULE, }; static int phy_meson8_hdmi_tx_probe(struct platform_device *pdev) { struct device_node *np = pdev->dev.of_node; struct phy_meson8_hdmi_tx_priv *priv; struct phy_provider *phy_provider; struct resource *res; struct phy *phy; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); if (!res) return -EINVAL; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->hhi = syscon_node_to_regmap(np->parent); if (IS_ERR(priv->hhi)) return PTR_ERR(priv->hhi); priv->tmds_clk = devm_clk_get(&pdev->dev, NULL); if (IS_ERR(priv->tmds_clk)) return PTR_ERR(priv->tmds_clk); phy = devm_phy_create(&pdev->dev, np, &phy_meson8_hdmi_tx_ops); if (IS_ERR(phy)) return PTR_ERR(phy); phy_set_drvdata(phy, priv); phy_provider = devm_of_phy_provider_register(&pdev->dev, of_phy_simple_xlate); return PTR_ERR_OR_ZERO(phy_provider); } static const struct of_device_id phy_meson8_hdmi_tx_of_match[] = { { .compatible = "amlogic,meson8-hdmi-tx-phy" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, phy_meson8_hdmi_tx_of_match); static struct platform_driver phy_meson8_hdmi_tx_driver = { .probe = phy_meson8_hdmi_tx_probe, .driver = { .name = "phy-meson8-hdmi-tx", .of_match_table = phy_meson8_hdmi_tx_of_match, }, }; module_platform_driver(phy_meson8_hdmi_tx_driver); MODULE_AUTHOR("Martin Blumenstingl "); MODULE_DESCRIPTION("Meson8, Meson8b and Meson8m2 HDMI TX PHY driver"); MODULE_LICENSE("GPL v2");