b.liu | e958203 | 2025-04-17 19:18:16 +0800 | [diff] [blame^] | 1 | // SPDX-License-Identifier: GPL-2.0+ |
| 2 | /* |
| 3 | * Copyright: 2017-2018 Cadence Design Systems, Inc. |
| 4 | */ |
| 5 | |
| 6 | #include <linux/bitops.h> |
| 7 | #include <linux/clk.h> |
| 8 | #include <linux/io.h> |
| 9 | #include <linux/module.h> |
| 10 | #include <linux/of_address.h> |
| 11 | #include <linux/of_device.h> |
| 12 | #include <linux/platform_device.h> |
| 13 | #include <linux/reset.h> |
| 14 | |
| 15 | #include <linux/phy/phy.h> |
| 16 | #include <linux/phy/phy-mipi-dphy.h> |
| 17 | |
| 18 | #define REG_WAKEUP_TIME_NS 800 |
| 19 | #define DPHY_PLL_RATE_HZ 108000000 |
| 20 | |
| 21 | /* DPHY registers */ |
| 22 | #define DPHY_PMA_CMN(reg) (reg) |
| 23 | #define DPHY_PMA_LCLK(reg) (0x100 + (reg)) |
| 24 | #define DPHY_PMA_LDATA(lane, reg) (0x200 + ((lane) * 0x100) + (reg)) |
| 25 | #define DPHY_PMA_RCLK(reg) (0x600 + (reg)) |
| 26 | #define DPHY_PMA_RDATA(lane, reg) (0x700 + ((lane) * 0x100) + (reg)) |
| 27 | #define DPHY_PCS(reg) (0xb00 + (reg)) |
| 28 | |
| 29 | #define DPHY_CMN_SSM DPHY_PMA_CMN(0x20) |
| 30 | #define DPHY_CMN_SSM_EN BIT(0) |
| 31 | #define DPHY_CMN_TX_MODE_EN BIT(9) |
| 32 | |
| 33 | #define DPHY_CMN_PWM DPHY_PMA_CMN(0x40) |
| 34 | #define DPHY_CMN_PWM_DIV(x) ((x) << 20) |
| 35 | #define DPHY_CMN_PWM_LOW(x) ((x) << 10) |
| 36 | #define DPHY_CMN_PWM_HIGH(x) (x) |
| 37 | |
| 38 | #define DPHY_CMN_FBDIV DPHY_PMA_CMN(0x4c) |
| 39 | #define DPHY_CMN_FBDIV_VAL(low, high) (((high) << 11) | ((low) << 22)) |
| 40 | #define DPHY_CMN_FBDIV_FROM_REG (BIT(10) | BIT(21)) |
| 41 | |
| 42 | #define DPHY_CMN_OPIPDIV DPHY_PMA_CMN(0x50) |
| 43 | #define DPHY_CMN_IPDIV_FROM_REG BIT(0) |
| 44 | #define DPHY_CMN_IPDIV(x) ((x) << 1) |
| 45 | #define DPHY_CMN_OPDIV_FROM_REG BIT(6) |
| 46 | #define DPHY_CMN_OPDIV(x) ((x) << 7) |
| 47 | |
| 48 | #define DPHY_PSM_CFG DPHY_PCS(0x4) |
| 49 | #define DPHY_PSM_CFG_FROM_REG BIT(0) |
| 50 | #define DPHY_PSM_CLK_DIV(x) ((x) << 1) |
| 51 | |
| 52 | #define DSI_HBP_FRAME_OVERHEAD 12 |
| 53 | #define DSI_HSA_FRAME_OVERHEAD 14 |
| 54 | #define DSI_HFP_FRAME_OVERHEAD 6 |
| 55 | #define DSI_HSS_VSS_VSE_FRAME_OVERHEAD 4 |
| 56 | #define DSI_BLANKING_FRAME_OVERHEAD 6 |
| 57 | #define DSI_NULL_FRAME_OVERHEAD 6 |
| 58 | #define DSI_EOT_PKT_SIZE 4 |
| 59 | |
| 60 | struct cdns_dphy_cfg { |
| 61 | u8 pll_ipdiv; |
| 62 | u8 pll_opdiv; |
| 63 | u16 pll_fbdiv; |
| 64 | unsigned int nlanes; |
| 65 | }; |
| 66 | |
| 67 | enum cdns_dphy_clk_lane_cfg { |
| 68 | DPHY_CLK_CFG_LEFT_DRIVES_ALL = 0, |
| 69 | DPHY_CLK_CFG_LEFT_DRIVES_RIGHT = 1, |
| 70 | DPHY_CLK_CFG_LEFT_DRIVES_LEFT = 2, |
| 71 | DPHY_CLK_CFG_RIGHT_DRIVES_ALL = 3, |
| 72 | }; |
| 73 | |
| 74 | struct cdns_dphy; |
| 75 | struct cdns_dphy_ops { |
| 76 | int (*probe)(struct cdns_dphy *dphy); |
| 77 | void (*remove)(struct cdns_dphy *dphy); |
| 78 | void (*set_psm_div)(struct cdns_dphy *dphy, u8 div); |
| 79 | void (*set_clk_lane_cfg)(struct cdns_dphy *dphy, |
| 80 | enum cdns_dphy_clk_lane_cfg cfg); |
| 81 | void (*set_pll_cfg)(struct cdns_dphy *dphy, |
| 82 | const struct cdns_dphy_cfg *cfg); |
| 83 | unsigned long (*get_wakeup_time_ns)(struct cdns_dphy *dphy); |
| 84 | }; |
| 85 | |
| 86 | struct cdns_dphy { |
| 87 | struct cdns_dphy_cfg cfg; |
| 88 | void __iomem *regs; |
| 89 | struct clk *psm_clk; |
| 90 | struct clk *pll_ref_clk; |
| 91 | const struct cdns_dphy_ops *ops; |
| 92 | struct phy *phy; |
| 93 | }; |
| 94 | |
| 95 | static int cdns_dsi_get_dphy_pll_cfg(struct cdns_dphy *dphy, |
| 96 | struct cdns_dphy_cfg *cfg, |
| 97 | struct phy_configure_opts_mipi_dphy *opts, |
| 98 | unsigned int *dsi_hfp_ext) |
| 99 | { |
| 100 | unsigned long pll_ref_hz = clk_get_rate(dphy->pll_ref_clk); |
| 101 | u64 dlane_bps; |
| 102 | |
| 103 | memset(cfg, 0, sizeof(*cfg)); |
| 104 | |
| 105 | if (pll_ref_hz < 9600000 || pll_ref_hz >= 150000000) |
| 106 | return -EINVAL; |
| 107 | else if (pll_ref_hz < 19200000) |
| 108 | cfg->pll_ipdiv = 1; |
| 109 | else if (pll_ref_hz < 38400000) |
| 110 | cfg->pll_ipdiv = 2; |
| 111 | else if (pll_ref_hz < 76800000) |
| 112 | cfg->pll_ipdiv = 4; |
| 113 | else |
| 114 | cfg->pll_ipdiv = 8; |
| 115 | |
| 116 | dlane_bps = opts->hs_clk_rate; |
| 117 | |
| 118 | if (dlane_bps > 2500000000UL || dlane_bps < 160000000UL) |
| 119 | return -EINVAL; |
| 120 | else if (dlane_bps >= 1250000000) |
| 121 | cfg->pll_opdiv = 1; |
| 122 | else if (dlane_bps >= 630000000) |
| 123 | cfg->pll_opdiv = 2; |
| 124 | else if (dlane_bps >= 320000000) |
| 125 | cfg->pll_opdiv = 4; |
| 126 | else if (dlane_bps >= 160000000) |
| 127 | cfg->pll_opdiv = 8; |
| 128 | |
| 129 | cfg->pll_fbdiv = DIV_ROUND_UP_ULL(dlane_bps * 2 * cfg->pll_opdiv * |
| 130 | cfg->pll_ipdiv, |
| 131 | pll_ref_hz); |
| 132 | |
| 133 | return 0; |
| 134 | } |
| 135 | |
| 136 | static int cdns_dphy_setup_psm(struct cdns_dphy *dphy) |
| 137 | { |
| 138 | unsigned long psm_clk_hz = clk_get_rate(dphy->psm_clk); |
| 139 | unsigned long psm_div; |
| 140 | |
| 141 | if (!psm_clk_hz || psm_clk_hz > 100000000) |
| 142 | return -EINVAL; |
| 143 | |
| 144 | psm_div = DIV_ROUND_CLOSEST(psm_clk_hz, 1000000); |
| 145 | if (dphy->ops->set_psm_div) |
| 146 | dphy->ops->set_psm_div(dphy, psm_div); |
| 147 | |
| 148 | return 0; |
| 149 | } |
| 150 | |
| 151 | static void cdns_dphy_set_clk_lane_cfg(struct cdns_dphy *dphy, |
| 152 | enum cdns_dphy_clk_lane_cfg cfg) |
| 153 | { |
| 154 | if (dphy->ops->set_clk_lane_cfg) |
| 155 | dphy->ops->set_clk_lane_cfg(dphy, cfg); |
| 156 | } |
| 157 | |
| 158 | static void cdns_dphy_set_pll_cfg(struct cdns_dphy *dphy, |
| 159 | const struct cdns_dphy_cfg *cfg) |
| 160 | { |
| 161 | if (dphy->ops->set_pll_cfg) |
| 162 | dphy->ops->set_pll_cfg(dphy, cfg); |
| 163 | } |
| 164 | |
| 165 | static unsigned long cdns_dphy_get_wakeup_time_ns(struct cdns_dphy *dphy) |
| 166 | { |
| 167 | return dphy->ops->get_wakeup_time_ns(dphy); |
| 168 | } |
| 169 | |
| 170 | static unsigned long cdns_dphy_ref_get_wakeup_time_ns(struct cdns_dphy *dphy) |
| 171 | { |
| 172 | /* Default wakeup time is 800 ns (in a simulated environment). */ |
| 173 | return 800; |
| 174 | } |
| 175 | |
| 176 | static void cdns_dphy_ref_set_pll_cfg(struct cdns_dphy *dphy, |
| 177 | const struct cdns_dphy_cfg *cfg) |
| 178 | { |
| 179 | u32 fbdiv_low, fbdiv_high; |
| 180 | |
| 181 | fbdiv_low = (cfg->pll_fbdiv / 4) - 2; |
| 182 | fbdiv_high = cfg->pll_fbdiv - fbdiv_low - 2; |
| 183 | |
| 184 | writel(DPHY_CMN_IPDIV_FROM_REG | DPHY_CMN_OPDIV_FROM_REG | |
| 185 | DPHY_CMN_IPDIV(cfg->pll_ipdiv) | |
| 186 | DPHY_CMN_OPDIV(cfg->pll_opdiv), |
| 187 | dphy->regs + DPHY_CMN_OPIPDIV); |
| 188 | writel(DPHY_CMN_FBDIV_FROM_REG | |
| 189 | DPHY_CMN_FBDIV_VAL(fbdiv_low, fbdiv_high), |
| 190 | dphy->regs + DPHY_CMN_FBDIV); |
| 191 | writel(DPHY_CMN_PWM_HIGH(6) | DPHY_CMN_PWM_LOW(0x101) | |
| 192 | DPHY_CMN_PWM_DIV(0x8), |
| 193 | dphy->regs + DPHY_CMN_PWM); |
| 194 | } |
| 195 | |
| 196 | static void cdns_dphy_ref_set_psm_div(struct cdns_dphy *dphy, u8 div) |
| 197 | { |
| 198 | writel(DPHY_PSM_CFG_FROM_REG | DPHY_PSM_CLK_DIV(div), |
| 199 | dphy->regs + DPHY_PSM_CFG); |
| 200 | } |
| 201 | |
| 202 | /* |
| 203 | * This is the reference implementation of DPHY hooks. Specific integration of |
| 204 | * this IP may have to re-implement some of them depending on how they decided |
| 205 | * to wire things in the SoC. |
| 206 | */ |
| 207 | static const struct cdns_dphy_ops ref_dphy_ops = { |
| 208 | .get_wakeup_time_ns = cdns_dphy_ref_get_wakeup_time_ns, |
| 209 | .set_pll_cfg = cdns_dphy_ref_set_pll_cfg, |
| 210 | .set_psm_div = cdns_dphy_ref_set_psm_div, |
| 211 | }; |
| 212 | |
| 213 | static int cdns_dphy_config_from_opts(struct phy *phy, |
| 214 | struct phy_configure_opts_mipi_dphy *opts, |
| 215 | struct cdns_dphy_cfg *cfg) |
| 216 | { |
| 217 | struct cdns_dphy *dphy = phy_get_drvdata(phy); |
| 218 | unsigned int dsi_hfp_ext = 0; |
| 219 | int ret; |
| 220 | |
| 221 | ret = phy_mipi_dphy_config_validate(opts); |
| 222 | if (ret) |
| 223 | return ret; |
| 224 | |
| 225 | ret = cdns_dsi_get_dphy_pll_cfg(dphy, cfg, |
| 226 | opts, &dsi_hfp_ext); |
| 227 | if (ret) |
| 228 | return ret; |
| 229 | |
| 230 | opts->wakeup = cdns_dphy_get_wakeup_time_ns(dphy) / 1000; |
| 231 | |
| 232 | return 0; |
| 233 | } |
| 234 | |
| 235 | static int cdns_dphy_validate(struct phy *phy, enum phy_mode mode, int submode, |
| 236 | union phy_configure_opts *opts) |
| 237 | { |
| 238 | struct cdns_dphy_cfg cfg = { 0 }; |
| 239 | |
| 240 | if (mode != PHY_MODE_MIPI_DPHY) |
| 241 | return -EINVAL; |
| 242 | |
| 243 | return cdns_dphy_config_from_opts(phy, &opts->mipi_dphy, &cfg); |
| 244 | } |
| 245 | |
| 246 | static int cdns_dphy_configure(struct phy *phy, union phy_configure_opts *opts) |
| 247 | { |
| 248 | struct cdns_dphy *dphy = phy_get_drvdata(phy); |
| 249 | struct cdns_dphy_cfg cfg = { 0 }; |
| 250 | int ret; |
| 251 | |
| 252 | ret = cdns_dphy_config_from_opts(phy, &opts->mipi_dphy, &cfg); |
| 253 | if (ret) |
| 254 | return ret; |
| 255 | |
| 256 | /* |
| 257 | * Configure the internal PSM clk divider so that the DPHY has a |
| 258 | * 1MHz clk (or something close). |
| 259 | */ |
| 260 | ret = cdns_dphy_setup_psm(dphy); |
| 261 | if (ret) |
| 262 | return ret; |
| 263 | |
| 264 | /* |
| 265 | * Configure attach clk lanes to data lanes: the DPHY has 2 clk lanes |
| 266 | * and 8 data lanes, each clk lane can be attache different set of |
| 267 | * data lanes. The 2 groups are named 'left' and 'right', so here we |
| 268 | * just say that we want the 'left' clk lane to drive the 'left' data |
| 269 | * lanes. |
| 270 | */ |
| 271 | cdns_dphy_set_clk_lane_cfg(dphy, DPHY_CLK_CFG_LEFT_DRIVES_LEFT); |
| 272 | |
| 273 | /* |
| 274 | * Configure the DPHY PLL that will be used to generate the TX byte |
| 275 | * clk. |
| 276 | */ |
| 277 | cdns_dphy_set_pll_cfg(dphy, &cfg); |
| 278 | |
| 279 | return 0; |
| 280 | } |
| 281 | |
| 282 | static int cdns_dphy_power_on(struct phy *phy) |
| 283 | { |
| 284 | struct cdns_dphy *dphy = phy_get_drvdata(phy); |
| 285 | |
| 286 | clk_prepare_enable(dphy->psm_clk); |
| 287 | clk_prepare_enable(dphy->pll_ref_clk); |
| 288 | |
| 289 | /* Start TX state machine. */ |
| 290 | writel(DPHY_CMN_SSM_EN | DPHY_CMN_TX_MODE_EN, |
| 291 | dphy->regs + DPHY_CMN_SSM); |
| 292 | |
| 293 | return 0; |
| 294 | } |
| 295 | |
| 296 | static int cdns_dphy_power_off(struct phy *phy) |
| 297 | { |
| 298 | struct cdns_dphy *dphy = phy_get_drvdata(phy); |
| 299 | |
| 300 | clk_disable_unprepare(dphy->pll_ref_clk); |
| 301 | clk_disable_unprepare(dphy->psm_clk); |
| 302 | |
| 303 | return 0; |
| 304 | } |
| 305 | |
| 306 | static const struct phy_ops cdns_dphy_ops = { |
| 307 | .configure = cdns_dphy_configure, |
| 308 | .validate = cdns_dphy_validate, |
| 309 | .power_on = cdns_dphy_power_on, |
| 310 | .power_off = cdns_dphy_power_off, |
| 311 | }; |
| 312 | |
| 313 | static int cdns_dphy_probe(struct platform_device *pdev) |
| 314 | { |
| 315 | struct phy_provider *phy_provider; |
| 316 | struct cdns_dphy *dphy; |
| 317 | struct resource *res; |
| 318 | int ret; |
| 319 | |
| 320 | dphy = devm_kzalloc(&pdev->dev, sizeof(*dphy), GFP_KERNEL); |
| 321 | if (!dphy) |
| 322 | return -ENOMEM; |
| 323 | dev_set_drvdata(&pdev->dev, dphy); |
| 324 | |
| 325 | dphy->ops = of_device_get_match_data(&pdev->dev); |
| 326 | if (!dphy->ops) |
| 327 | return -EINVAL; |
| 328 | |
| 329 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| 330 | dphy->regs = devm_ioremap_resource(&pdev->dev, res); |
| 331 | if (IS_ERR(dphy->regs)) |
| 332 | return PTR_ERR(dphy->regs); |
| 333 | |
| 334 | dphy->psm_clk = devm_clk_get(&pdev->dev, "psm"); |
| 335 | if (IS_ERR(dphy->psm_clk)) |
| 336 | return PTR_ERR(dphy->psm_clk); |
| 337 | |
| 338 | dphy->pll_ref_clk = devm_clk_get(&pdev->dev, "pll_ref"); |
| 339 | if (IS_ERR(dphy->pll_ref_clk)) |
| 340 | return PTR_ERR(dphy->pll_ref_clk); |
| 341 | |
| 342 | if (dphy->ops->probe) { |
| 343 | ret = dphy->ops->probe(dphy); |
| 344 | if (ret) |
| 345 | return ret; |
| 346 | } |
| 347 | |
| 348 | dphy->phy = devm_phy_create(&pdev->dev, NULL, &cdns_dphy_ops); |
| 349 | if (IS_ERR(dphy->phy)) { |
| 350 | dev_err(&pdev->dev, "failed to create PHY\n"); |
| 351 | if (dphy->ops->remove) |
| 352 | dphy->ops->remove(dphy); |
| 353 | return PTR_ERR(dphy->phy); |
| 354 | } |
| 355 | |
| 356 | phy_set_drvdata(dphy->phy, dphy); |
| 357 | phy_provider = devm_of_phy_provider_register(&pdev->dev, |
| 358 | of_phy_simple_xlate); |
| 359 | |
| 360 | return PTR_ERR_OR_ZERO(phy_provider); |
| 361 | } |
| 362 | |
| 363 | static int cdns_dphy_remove(struct platform_device *pdev) |
| 364 | { |
| 365 | struct cdns_dphy *dphy = dev_get_drvdata(&pdev->dev); |
| 366 | |
| 367 | if (dphy->ops->remove) |
| 368 | dphy->ops->remove(dphy); |
| 369 | |
| 370 | return 0; |
| 371 | } |
| 372 | |
| 373 | static const struct of_device_id cdns_dphy_of_match[] = { |
| 374 | { .compatible = "cdns,dphy", .data = &ref_dphy_ops }, |
| 375 | { /* sentinel */ }, |
| 376 | }; |
| 377 | MODULE_DEVICE_TABLE(of, cdns_dphy_of_match); |
| 378 | |
| 379 | static struct platform_driver cdns_dphy_platform_driver = { |
| 380 | .probe = cdns_dphy_probe, |
| 381 | .remove = cdns_dphy_remove, |
| 382 | .driver = { |
| 383 | .name = "cdns-mipi-dphy", |
| 384 | .of_match_table = cdns_dphy_of_match, |
| 385 | }, |
| 386 | }; |
| 387 | module_platform_driver(cdns_dphy_platform_driver); |
| 388 | |
| 389 | MODULE_AUTHOR("Maxime Ripard <maxime.ripard@bootlin.com>"); |
| 390 | MODULE_DESCRIPTION("Cadence MIPI D-PHY Driver"); |
| 391 | MODULE_LICENSE("GPL"); |