1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * Meson8, Meson8b and Meson8m2 HDMI TX PHY. |
4 | * |
5 | * Copyright (C) 2021 Martin Blumenstingl <martin.blumenstingl@googlemail.com> |
6 | */ |
7 | |
8 | #include <linux/bitfield.h> |
9 | #include <linux/bits.h> |
10 | #include <linux/clk.h> |
11 | #include <linux/mfd/syscon.h> |
12 | #include <linux/module.h> |
13 | #include <linux/of.h> |
14 | #include <linux/phy/phy.h> |
15 | #include <linux/platform_device.h> |
16 | #include <linux/property.h> |
17 | #include <linux/regmap.h> |
18 | |
19 | /* |
20 | * Unfortunately there is no detailed documentation available for the |
21 | * HHI_HDMI_PHY_CNTL0 register. CTL0 and CTL1 is all we know about. |
22 | * Magic register values in the driver below are taken from the vendor |
23 | * BSP / kernel. |
24 | */ |
25 | #define HHI_HDMI_PHY_CNTL0 0x3a0 |
26 | #define HHI_HDMI_PHY_CNTL0_HDMI_CTL1 GENMASK(31, 16) |
27 | #define HHI_HDMI_PHY_CNTL0_HDMI_CTL0 GENMASK(15, 0) |
28 | |
29 | #define HHI_HDMI_PHY_CNTL1 0x3a4 |
30 | #define HHI_HDMI_PHY_CNTL1_CLOCK_ENABLE BIT(1) |
31 | #define HHI_HDMI_PHY_CNTL1_SOFT_RESET BIT(0) |
32 | |
33 | #define HHI_HDMI_PHY_CNTL2 0x3a8 |
34 | |
35 | struct phy_meson8_hdmi_tx_priv { |
36 | struct regmap *hhi; |
37 | struct clk *tmds_clk; |
38 | }; |
39 | |
40 | static int phy_meson8_hdmi_tx_init(struct phy *phy) |
41 | { |
42 | struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); |
43 | |
44 | return clk_prepare_enable(clk: priv->tmds_clk); |
45 | } |
46 | |
47 | static int phy_meson8_hdmi_tx_exit(struct phy *phy) |
48 | { |
49 | struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); |
50 | |
51 | clk_disable_unprepare(clk: priv->tmds_clk); |
52 | |
53 | return 0; |
54 | } |
55 | |
56 | static int phy_meson8_hdmi_tx_power_on(struct phy *phy) |
57 | { |
58 | struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); |
59 | unsigned int i; |
60 | u16 hdmi_ctl0; |
61 | |
62 | if (clk_get_rate(clk: priv->tmds_clk) >= 2970UL * 1000 * 1000) |
63 | hdmi_ctl0 = 0x1e8b; |
64 | else |
65 | hdmi_ctl0 = 0x4d0b; |
66 | |
67 | regmap_write(map: priv->hhi, HHI_HDMI_PHY_CNTL0, |
68 | FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL1, 0x08c3) | |
69 | FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL0, hdmi_ctl0)); |
70 | |
71 | regmap_write(map: priv->hhi, HHI_HDMI_PHY_CNTL1, val: 0x0); |
72 | |
73 | /* Reset three times, just like the vendor driver does */ |
74 | for (i = 0; i < 3; i++) { |
75 | regmap_write(map: priv->hhi, HHI_HDMI_PHY_CNTL1, |
76 | HHI_HDMI_PHY_CNTL1_CLOCK_ENABLE | |
77 | HHI_HDMI_PHY_CNTL1_SOFT_RESET); |
78 | usleep_range(min: 1000, max: 2000); |
79 | |
80 | regmap_write(map: priv->hhi, HHI_HDMI_PHY_CNTL1, |
81 | HHI_HDMI_PHY_CNTL1_CLOCK_ENABLE); |
82 | usleep_range(min: 1000, max: 2000); |
83 | } |
84 | |
85 | return 0; |
86 | } |
87 | |
88 | static int phy_meson8_hdmi_tx_power_off(struct phy *phy) |
89 | { |
90 | struct phy_meson8_hdmi_tx_priv *priv = phy_get_drvdata(phy); |
91 | |
92 | regmap_write(map: priv->hhi, HHI_HDMI_PHY_CNTL0, |
93 | FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL1, 0x0841) | |
94 | FIELD_PREP(HHI_HDMI_PHY_CNTL0_HDMI_CTL0, 0x8d00)); |
95 | |
96 | return 0; |
97 | } |
98 | |
99 | static const struct phy_ops phy_meson8_hdmi_tx_ops = { |
100 | .init = phy_meson8_hdmi_tx_init, |
101 | .exit = phy_meson8_hdmi_tx_exit, |
102 | .power_on = phy_meson8_hdmi_tx_power_on, |
103 | .power_off = phy_meson8_hdmi_tx_power_off, |
104 | .owner = THIS_MODULE, |
105 | }; |
106 | |
107 | static int phy_meson8_hdmi_tx_probe(struct platform_device *pdev) |
108 | { |
109 | struct device_node *np = pdev->dev.of_node; |
110 | struct phy_meson8_hdmi_tx_priv *priv; |
111 | struct phy_provider *phy_provider; |
112 | struct resource *res; |
113 | struct phy *phy; |
114 | |
115 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
116 | if (!res) |
117 | return -EINVAL; |
118 | |
119 | priv = devm_kzalloc(dev: &pdev->dev, size: sizeof(*priv), GFP_KERNEL); |
120 | if (!priv) |
121 | return -ENOMEM; |
122 | |
123 | priv->hhi = syscon_node_to_regmap(np: np->parent); |
124 | if (IS_ERR(ptr: priv->hhi)) |
125 | return PTR_ERR(ptr: priv->hhi); |
126 | |
127 | priv->tmds_clk = devm_clk_get(dev: &pdev->dev, NULL); |
128 | if (IS_ERR(ptr: priv->tmds_clk)) |
129 | return PTR_ERR(ptr: priv->tmds_clk); |
130 | |
131 | phy = devm_phy_create(dev: &pdev->dev, node: np, ops: &phy_meson8_hdmi_tx_ops); |
132 | if (IS_ERR(ptr: phy)) |
133 | return PTR_ERR(ptr: phy); |
134 | |
135 | phy_set_drvdata(phy, data: priv); |
136 | |
137 | phy_provider = devm_of_phy_provider_register(&pdev->dev, |
138 | of_phy_simple_xlate); |
139 | |
140 | return PTR_ERR_OR_ZERO(ptr: phy_provider); |
141 | } |
142 | |
143 | static const struct of_device_id phy_meson8_hdmi_tx_of_match[] = { |
144 | { .compatible = "amlogic,meson8-hdmi-tx-phy" }, |
145 | { /* sentinel */ } |
146 | }; |
147 | MODULE_DEVICE_TABLE(of, phy_meson8_hdmi_tx_of_match); |
148 | |
149 | static struct platform_driver phy_meson8_hdmi_tx_driver = { |
150 | .probe = phy_meson8_hdmi_tx_probe, |
151 | .driver = { |
152 | .name = "phy-meson8-hdmi-tx" , |
153 | .of_match_table = phy_meson8_hdmi_tx_of_match, |
154 | }, |
155 | }; |
156 | module_platform_driver(phy_meson8_hdmi_tx_driver); |
157 | |
158 | MODULE_AUTHOR("Martin Blumenstingl <martin.blumenstingl@googlemail.com>" ); |
159 | MODULE_DESCRIPTION("Meson8, Meson8b and Meson8m2 HDMI TX PHY driver" ); |
160 | MODULE_LICENSE("GPL v2" ); |
161 | |