Benjamin Gaignard | ec17f03 | 2017-12-06 12:29:46 +0100 | [diff] [blame] | 1 | // SPDX-License-Identifier: GPL-2.0 |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 2 | /* |
| 3 | * Copyright (C) STMicroelectronics SA 2017 |
| 4 | * |
| 5 | * Authors: Philippe Cornu <philippe.cornu@st.com> |
| 6 | * Yannick Fertre <yannick.fertre@st.com> |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 7 | */ |
| 8 | |
| 9 | #include <linux/clk.h> |
| 10 | #include <linux/iopoll.h> |
| 11 | #include <linux/module.h> |
| 12 | #include <drm/drmP.h> |
| 13 | #include <drm/drm_mipi_dsi.h> |
| 14 | #include <drm/bridge/dw_mipi_dsi.h> |
| 15 | #include <video/mipi_display.h> |
| 16 | |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 17 | #define HWVER_130 0x31333000 /* IP version 1.30 */ |
| 18 | #define HWVER_131 0x31333100 /* IP version 1.31 */ |
| 19 | |
| 20 | /* DSI digital registers & bit definitions */ |
| 21 | #define DSI_VERSION 0x00 |
| 22 | #define VERSION GENMASK(31, 8) |
| 23 | |
| 24 | /* DSI wrapper registers & bit definitions */ |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 25 | /* Note: registers are named as in the Reference Manual */ |
| 26 | #define DSI_WCFGR 0x0400 /* Wrapper ConFiGuration Reg */ |
| 27 | #define WCFGR_DSIM BIT(0) /* DSI Mode */ |
| 28 | #define WCFGR_COLMUX GENMASK(3, 1) /* COLor MUltipleXing */ |
| 29 | |
| 30 | #define DSI_WCR 0x0404 /* Wrapper Control Reg */ |
| 31 | #define WCR_DSIEN BIT(3) /* DSI ENable */ |
| 32 | |
| 33 | #define DSI_WISR 0x040C /* Wrapper Interrupt and Status Reg */ |
| 34 | #define WISR_PLLLS BIT(8) /* PLL Lock Status */ |
| 35 | #define WISR_RRS BIT(12) /* Regulator Ready Status */ |
| 36 | |
| 37 | #define DSI_WPCR0 0x0418 /* Wrapper Phy Conf Reg 0 */ |
| 38 | #define WPCR0_UIX4 GENMASK(5, 0) /* Unit Interval X 4 */ |
| 39 | #define WPCR0_TDDL BIT(16) /* Turn Disable Data Lanes */ |
| 40 | |
| 41 | #define DSI_WRPCR 0x0430 /* Wrapper Regulator & Pll Ctrl Reg */ |
| 42 | #define WRPCR_PLLEN BIT(0) /* PLL ENable */ |
| 43 | #define WRPCR_NDIV GENMASK(8, 2) /* pll loop DIVision Factor */ |
| 44 | #define WRPCR_IDF GENMASK(14, 11) /* pll Input Division Factor */ |
| 45 | #define WRPCR_ODF GENMASK(17, 16) /* pll Output Division Factor */ |
| 46 | #define WRPCR_REGEN BIT(24) /* REGulator ENable */ |
| 47 | #define WRPCR_BGREN BIT(28) /* BandGap Reference ENable */ |
| 48 | #define IDF_MIN 1 |
| 49 | #define IDF_MAX 7 |
| 50 | #define NDIV_MIN 10 |
| 51 | #define NDIV_MAX 125 |
| 52 | #define ODF_MIN 1 |
| 53 | #define ODF_MAX 8 |
| 54 | |
| 55 | /* dsi color format coding according to the datasheet */ |
| 56 | enum dsi_color { |
| 57 | DSI_RGB565_CONF1, |
| 58 | DSI_RGB565_CONF2, |
| 59 | DSI_RGB565_CONF3, |
| 60 | DSI_RGB666_CONF1, |
| 61 | DSI_RGB666_CONF2, |
| 62 | DSI_RGB888, |
| 63 | }; |
| 64 | |
| 65 | #define LANE_MIN_KBPS 31250 |
| 66 | #define LANE_MAX_KBPS 500000 |
| 67 | |
| 68 | /* Sleep & timeout for regulator on/off, pll lock/unlock & fifo empty */ |
| 69 | #define SLEEP_US 1000 |
| 70 | #define TIMEOUT_US 200000 |
| 71 | |
| 72 | struct dw_mipi_dsi_stm { |
| 73 | void __iomem *base; |
| 74 | struct clk *pllref_clk; |
Brian Norris | 8242ecb | 2017-11-27 17:05:38 -0800 | [diff] [blame] | 75 | struct dw_mipi_dsi *dsi; |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 76 | u32 hw_version; |
| 77 | int lane_min_kbps; |
| 78 | int lane_max_kbps; |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 79 | }; |
| 80 | |
| 81 | static inline void dsi_write(struct dw_mipi_dsi_stm *dsi, u32 reg, u32 val) |
| 82 | { |
| 83 | writel(val, dsi->base + reg); |
| 84 | } |
| 85 | |
| 86 | static inline u32 dsi_read(struct dw_mipi_dsi_stm *dsi, u32 reg) |
| 87 | { |
| 88 | return readl(dsi->base + reg); |
| 89 | } |
| 90 | |
| 91 | static inline void dsi_set(struct dw_mipi_dsi_stm *dsi, u32 reg, u32 mask) |
| 92 | { |
| 93 | dsi_write(dsi, reg, dsi_read(dsi, reg) | mask); |
| 94 | } |
| 95 | |
| 96 | static inline void dsi_clear(struct dw_mipi_dsi_stm *dsi, u32 reg, u32 mask) |
| 97 | { |
| 98 | dsi_write(dsi, reg, dsi_read(dsi, reg) & ~mask); |
| 99 | } |
| 100 | |
| 101 | static inline void dsi_update_bits(struct dw_mipi_dsi_stm *dsi, u32 reg, |
| 102 | u32 mask, u32 val) |
| 103 | { |
| 104 | dsi_write(dsi, reg, (dsi_read(dsi, reg) & ~mask) | val); |
| 105 | } |
| 106 | |
| 107 | static enum dsi_color dsi_color_from_mipi(enum mipi_dsi_pixel_format fmt) |
| 108 | { |
| 109 | switch (fmt) { |
| 110 | case MIPI_DSI_FMT_RGB888: |
| 111 | return DSI_RGB888; |
| 112 | case MIPI_DSI_FMT_RGB666: |
| 113 | return DSI_RGB666_CONF2; |
| 114 | case MIPI_DSI_FMT_RGB666_PACKED: |
| 115 | return DSI_RGB666_CONF1; |
| 116 | case MIPI_DSI_FMT_RGB565: |
| 117 | return DSI_RGB565_CONF1; |
| 118 | default: |
| 119 | DRM_DEBUG_DRIVER("MIPI color invalid, so we use rgb888\n"); |
| 120 | } |
| 121 | return DSI_RGB888; |
| 122 | } |
| 123 | |
| 124 | static int dsi_pll_get_clkout_khz(int clkin_khz, int idf, int ndiv, int odf) |
| 125 | { |
Arnd Bergmann | 3ff558e | 2017-09-06 15:13:07 +0200 | [diff] [blame] | 126 | int divisor = idf * odf; |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 127 | |
Arnd Bergmann | 3ff558e | 2017-09-06 15:13:07 +0200 | [diff] [blame] | 128 | /* prevent from division by 0 */ |
| 129 | if (!divisor) |
| 130 | return 0; |
| 131 | |
| 132 | return DIV_ROUND_CLOSEST(clkin_khz * ndiv, divisor); |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 133 | } |
| 134 | |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 135 | static int dsi_pll_get_params(struct dw_mipi_dsi_stm *dsi, |
| 136 | int clkin_khz, int clkout_khz, |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 137 | int *idf, int *ndiv, int *odf) |
| 138 | { |
| 139 | int i, o, n, n_min, n_max; |
| 140 | int fvco_min, fvco_max, delta, best_delta; /* all in khz */ |
| 141 | |
| 142 | /* Early checks preventing division by 0 & odd results */ |
Philippe CORNU | 0163d1f | 2017-10-26 12:24:33 +0200 | [diff] [blame] | 143 | if (clkin_khz <= 0 || clkout_khz <= 0) |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 144 | return -EINVAL; |
| 145 | |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 146 | fvco_min = dsi->lane_min_kbps * 2 * ODF_MAX; |
| 147 | fvco_max = dsi->lane_max_kbps * 2 * ODF_MIN; |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 148 | |
| 149 | best_delta = 1000000; /* big started value (1000000khz) */ |
| 150 | |
| 151 | for (i = IDF_MIN; i <= IDF_MAX; i++) { |
| 152 | /* Compute ndiv range according to Fvco */ |
| 153 | n_min = ((fvco_min * i) / (2 * clkin_khz)) + 1; |
| 154 | n_max = (fvco_max * i) / (2 * clkin_khz); |
| 155 | |
| 156 | /* No need to continue idf loop if we reach ndiv max */ |
| 157 | if (n_min >= NDIV_MAX) |
| 158 | break; |
| 159 | |
| 160 | /* Clamp ndiv to valid values */ |
| 161 | if (n_min < NDIV_MIN) |
| 162 | n_min = NDIV_MIN; |
| 163 | if (n_max > NDIV_MAX) |
| 164 | n_max = NDIV_MAX; |
| 165 | |
| 166 | for (o = ODF_MIN; o <= ODF_MAX; o *= 2) { |
| 167 | n = DIV_ROUND_CLOSEST(i * o * clkout_khz, clkin_khz); |
| 168 | /* Check ndiv according to vco range */ |
Philippe CORNU | 0163d1f | 2017-10-26 12:24:33 +0200 | [diff] [blame] | 169 | if (n < n_min || n > n_max) |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 170 | continue; |
| 171 | /* Check if new delta is better & saves parameters */ |
| 172 | delta = dsi_pll_get_clkout_khz(clkin_khz, i, n, o) - |
| 173 | clkout_khz; |
| 174 | if (delta < 0) |
| 175 | delta = -delta; |
| 176 | if (delta < best_delta) { |
| 177 | *idf = i; |
| 178 | *ndiv = n; |
| 179 | *odf = o; |
| 180 | best_delta = delta; |
| 181 | } |
| 182 | /* fast return in case of "perfect result" */ |
| 183 | if (!delta) |
| 184 | return 0; |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | return 0; |
| 189 | } |
| 190 | |
| 191 | static int dw_mipi_dsi_phy_init(void *priv_data) |
| 192 | { |
| 193 | struct dw_mipi_dsi_stm *dsi = priv_data; |
| 194 | u32 val; |
| 195 | int ret; |
| 196 | |
| 197 | /* Enable the regulator */ |
| 198 | dsi_set(dsi, DSI_WRPCR, WRPCR_REGEN | WRPCR_BGREN); |
| 199 | ret = readl_poll_timeout(dsi->base + DSI_WISR, val, val & WISR_RRS, |
| 200 | SLEEP_US, TIMEOUT_US); |
| 201 | if (ret) |
| 202 | DRM_DEBUG_DRIVER("!TIMEOUT! waiting REGU, let's continue\n"); |
| 203 | |
| 204 | /* Enable the DSI PLL & wait for its lock */ |
| 205 | dsi_set(dsi, DSI_WRPCR, WRPCR_PLLEN); |
| 206 | ret = readl_poll_timeout(dsi->base + DSI_WISR, val, val & WISR_PLLLS, |
| 207 | SLEEP_US, TIMEOUT_US); |
| 208 | if (ret) |
| 209 | DRM_DEBUG_DRIVER("!TIMEOUT! waiting PLL, let's continue\n"); |
| 210 | |
| 211 | /* Enable the DSI wrapper */ |
| 212 | dsi_set(dsi, DSI_WCR, WCR_DSIEN); |
| 213 | |
| 214 | return 0; |
| 215 | } |
| 216 | |
| 217 | static int |
| 218 | dw_mipi_dsi_get_lane_mbps(void *priv_data, struct drm_display_mode *mode, |
| 219 | unsigned long mode_flags, u32 lanes, u32 format, |
| 220 | unsigned int *lane_mbps) |
| 221 | { |
| 222 | struct dw_mipi_dsi_stm *dsi = priv_data; |
| 223 | unsigned int idf, ndiv, odf, pll_in_khz, pll_out_khz; |
| 224 | int ret, bpp; |
| 225 | u32 val; |
| 226 | |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 227 | /* Update lane capabilities according to hw version */ |
| 228 | dsi->hw_version = dsi_read(dsi, DSI_VERSION) & VERSION; |
| 229 | dsi->lane_min_kbps = LANE_MIN_KBPS; |
| 230 | dsi->lane_max_kbps = LANE_MAX_KBPS; |
| 231 | if (dsi->hw_version == HWVER_131) { |
| 232 | dsi->lane_min_kbps *= 2; |
| 233 | dsi->lane_max_kbps *= 2; |
| 234 | } |
| 235 | |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 236 | pll_in_khz = (unsigned int)(clk_get_rate(dsi->pllref_clk) / 1000); |
| 237 | |
| 238 | /* Compute requested pll out */ |
| 239 | bpp = mipi_dsi_pixel_format_to_bpp(format); |
| 240 | pll_out_khz = mode->clock * bpp / lanes; |
| 241 | /* Add 20% to pll out to be higher than pixel bw (burst mode only) */ |
| 242 | pll_out_khz = (pll_out_khz * 12) / 10; |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 243 | if (pll_out_khz > dsi->lane_max_kbps) { |
| 244 | pll_out_khz = dsi->lane_max_kbps; |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 245 | DRM_WARN("Warning max phy mbps is used\n"); |
| 246 | } |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 247 | if (pll_out_khz < dsi->lane_min_kbps) { |
| 248 | pll_out_khz = dsi->lane_min_kbps; |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 249 | DRM_WARN("Warning min phy mbps is used\n"); |
| 250 | } |
| 251 | |
| 252 | /* Compute best pll parameters */ |
| 253 | idf = 0; |
| 254 | ndiv = 0; |
| 255 | odf = 0; |
Philippe CORNU | 023f348 | 2018-01-22 17:46:28 +0100 | [diff] [blame] | 256 | ret = dsi_pll_get_params(dsi, pll_in_khz, pll_out_khz, |
| 257 | &idf, &ndiv, &odf); |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 258 | if (ret) |
| 259 | DRM_WARN("Warning dsi_pll_get_params(): bad params\n"); |
| 260 | |
| 261 | /* Get the adjusted pll out value */ |
| 262 | pll_out_khz = dsi_pll_get_clkout_khz(pll_in_khz, idf, ndiv, odf); |
| 263 | |
| 264 | /* Set the PLL division factors */ |
| 265 | dsi_update_bits(dsi, DSI_WRPCR, WRPCR_NDIV | WRPCR_IDF | WRPCR_ODF, |
| 266 | (ndiv << 2) | (idf << 11) | ((ffs(odf) - 1) << 16)); |
| 267 | |
| 268 | /* Compute uix4 & set the bit period in high-speed mode */ |
| 269 | val = 4000000 / pll_out_khz; |
| 270 | dsi_update_bits(dsi, DSI_WPCR0, WPCR0_UIX4, val); |
| 271 | |
| 272 | /* Select video mode by resetting DSIM bit */ |
| 273 | dsi_clear(dsi, DSI_WCFGR, WCFGR_DSIM); |
| 274 | |
| 275 | /* Select the color coding */ |
| 276 | dsi_update_bits(dsi, DSI_WCFGR, WCFGR_COLMUX, |
| 277 | dsi_color_from_mipi(format) << 1); |
| 278 | |
| 279 | *lane_mbps = pll_out_khz / 1000; |
| 280 | |
| 281 | DRM_DEBUG_DRIVER("pll_in %ukHz pll_out %ukHz lane_mbps %uMHz\n", |
| 282 | pll_in_khz, pll_out_khz, *lane_mbps); |
| 283 | |
| 284 | return 0; |
| 285 | } |
| 286 | |
Philippe CORNU | 89a15e6 | 2017-07-20 14:05:57 +0200 | [diff] [blame] | 287 | static const struct dw_mipi_dsi_phy_ops dw_mipi_dsi_stm_phy_ops = { |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 288 | .init = dw_mipi_dsi_phy_init, |
| 289 | .get_lane_mbps = dw_mipi_dsi_get_lane_mbps, |
| 290 | }; |
| 291 | |
| 292 | static struct dw_mipi_dsi_plat_data dw_mipi_dsi_stm_plat_data = { |
| 293 | .max_data_lanes = 2, |
| 294 | .phy_ops = &dw_mipi_dsi_stm_phy_ops, |
| 295 | }; |
| 296 | |
| 297 | static const struct of_device_id dw_mipi_dsi_stm_dt_ids[] = { |
| 298 | { .compatible = "st,stm32-dsi", .data = &dw_mipi_dsi_stm_plat_data, }, |
| 299 | { }, |
| 300 | }; |
| 301 | MODULE_DEVICE_TABLE(of, dw_mipi_dsi_stm_dt_ids); |
| 302 | |
| 303 | static int dw_mipi_dsi_stm_probe(struct platform_device *pdev) |
| 304 | { |
| 305 | struct device *dev = &pdev->dev; |
| 306 | struct dw_mipi_dsi_stm *dsi; |
| 307 | struct resource *res; |
| 308 | int ret; |
| 309 | |
| 310 | dsi = devm_kzalloc(dev, sizeof(*dsi), GFP_KERNEL); |
| 311 | if (!dsi) |
| 312 | return -ENOMEM; |
| 313 | |
| 314 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 315 | dsi->base = devm_ioremap_resource(dev, res); |
| 316 | if (IS_ERR(dsi->base)) { |
| 317 | DRM_ERROR("Unable to get dsi registers\n"); |
| 318 | return PTR_ERR(dsi->base); |
| 319 | } |
| 320 | |
| 321 | dsi->pllref_clk = devm_clk_get(dev, "ref"); |
| 322 | if (IS_ERR(dsi->pllref_clk)) { |
| 323 | ret = PTR_ERR(dsi->pllref_clk); |
| 324 | dev_err(dev, "Unable to get pll reference clock: %d\n", ret); |
| 325 | return ret; |
| 326 | } |
| 327 | |
| 328 | ret = clk_prepare_enable(dsi->pllref_clk); |
| 329 | if (ret) { |
| 330 | dev_err(dev, "%s: Failed to enable pllref_clk\n", __func__); |
| 331 | return ret; |
| 332 | } |
| 333 | |
| 334 | dw_mipi_dsi_stm_plat_data.base = dsi->base; |
| 335 | dw_mipi_dsi_stm_plat_data.priv_data = dsi; |
| 336 | |
Brian Norris | 8242ecb | 2017-11-27 17:05:38 -0800 | [diff] [blame] | 337 | platform_set_drvdata(pdev, dsi); |
| 338 | |
| 339 | dsi->dsi = dw_mipi_dsi_probe(pdev, &dw_mipi_dsi_stm_plat_data); |
| 340 | if (IS_ERR(dsi->dsi)) { |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 341 | DRM_ERROR("Failed to initialize mipi dsi host\n"); |
| 342 | clk_disable_unprepare(dsi->pllref_clk); |
Brian Norris | 8242ecb | 2017-11-27 17:05:38 -0800 | [diff] [blame] | 343 | return PTR_ERR(dsi->dsi); |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 344 | } |
| 345 | |
Brian Norris | 8242ecb | 2017-11-27 17:05:38 -0800 | [diff] [blame] | 346 | return 0; |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 347 | } |
| 348 | |
| 349 | static int dw_mipi_dsi_stm_remove(struct platform_device *pdev) |
| 350 | { |
Brian Norris | 8242ecb | 2017-11-27 17:05:38 -0800 | [diff] [blame] | 351 | struct dw_mipi_dsi_stm *dsi = platform_get_drvdata(pdev); |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 352 | |
| 353 | clk_disable_unprepare(dsi->pllref_clk); |
Brian Norris | 8242ecb | 2017-11-27 17:05:38 -0800 | [diff] [blame] | 354 | dw_mipi_dsi_remove(dsi->dsi); |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 355 | |
| 356 | return 0; |
| 357 | } |
| 358 | |
| 359 | static struct platform_driver dw_mipi_dsi_stm_driver = { |
| 360 | .probe = dw_mipi_dsi_stm_probe, |
| 361 | .remove = dw_mipi_dsi_stm_remove, |
| 362 | .driver = { |
| 363 | .of_match_table = dw_mipi_dsi_stm_dt_ids, |
Philippe CORNU | bb3fb5e | 2017-10-26 12:04:50 +0200 | [diff] [blame] | 364 | .name = "stm32-display-dsi", |
Philippe CORNU | c1c026d | 2017-07-17 09:40:23 +0200 | [diff] [blame] | 365 | }, |
| 366 | }; |
| 367 | |
| 368 | module_platform_driver(dw_mipi_dsi_stm_driver); |
| 369 | |
| 370 | MODULE_AUTHOR("Philippe Cornu <philippe.cornu@st.com>"); |
| 371 | MODULE_AUTHOR("Yannick Fertre <yannick.fertre@st.com>"); |
| 372 | MODULE_DESCRIPTION("STMicroelectronics DW MIPI DSI host controller driver"); |
| 373 | MODULE_LICENSE("GPL v2"); |