| /* |
| * Copyright 2017 NXP |
| * |
| * This program is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License as published by the |
| * Free Software Foundation; either version 2 of the License, or (at your |
| * option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, but |
| * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
| * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * for more details. |
| */ |
| |
| #include <linux/delay.h> |
| #include <linux/io.h> |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_platform.h> |
| #include <linux/phy/phy-mixel-mipi-dsi.h> |
| #include <linux/phy/phy.h> |
| #include <linux/platform_device.h> |
| #include <soc/imx8/sc/sci.h> |
| |
| #define DPHY_PD_DPHY 0x00 |
| #define DPHY_M_PRG_HS_PREPARE 0x04 |
| #define DPHY_MC_PRG_HS_PREPARE 0x08 |
| #define DPHY_M_PRG_HS_ZERO 0x0c |
| #define DPHY_MC_PRG_HS_ZERO 0x10 |
| #define DPHY_M_PRG_HS_TRAIL 0x14 |
| #define DPHY_MC_PRG_HS_TRAIL 0x18 |
| #define DPHY_PD_PLL 0x1c |
| #define DPHY_TST 0x20 |
| #define DPHY_CN 0x24 |
| #define DPHY_CM 0x28 |
| #define DPHY_CO 0x2c |
| #define DPHY_LOCK 0x30 |
| #define DPHY_LOCK_BYP 0x34 |
| #define DPHY_TX_RCAL 0x38 |
| #define DPHY_AUTO_PD_EN 0x3c |
| #define DPHY_RXLPRP 0x40 |
| #define DPHY_RXCDRP 0x44 |
| |
| #define MBPS(x) ((x) * 1000000) |
| |
| #define DATA_RATE_MAX_SPEED MBPS(1500) |
| #define DATA_RATE_MIN_SPEED MBPS(80) |
| |
| #define CN_BUF 0xcb7a89c0 |
| #define CO_BUF 0x63 |
| #define CM(x) ( \ |
| ((x) < 32)?0xe0|((x)-16) : \ |
| ((x) < 64)?0xc0|((x)-32) : \ |
| ((x) < 128)?0x80|((x)-64) : \ |
| ((x) - 128)) |
| #define CN(x) (((x) == 1)?0x1f : (((CN_BUF)>>((x)-1))&0x1f)) |
| #define CO(x) ((CO_BUF)>>(8-(x))&0x3) |
| |
| /* PHY power on is LOW_ENABLE */ |
| #define PWR_ON 0 |
| #define PWR_OFF 1 |
| |
| struct pll_divider { |
| u32 cm; |
| u32 cn; |
| u32 co; |
| }; |
| |
| struct mixel_mipi_phy_priv { |
| struct device *dev; |
| void __iomem *base; |
| bool have_sc; |
| sc_rsrc_t mipi_id; |
| struct pll_divider divider; |
| struct mutex lock; |
| unsigned long data_rate; |
| }; |
| |
| struct devtype { |
| bool have_sc; |
| }; |
| |
| static inline u32 phy_read(struct phy *phy, unsigned int reg) |
| { |
| struct mixel_mipi_phy_priv *priv = phy_get_drvdata(phy); |
| |
| return readl(priv->base + reg); |
| } |
| |
| static inline void phy_write(struct phy *phy, u32 value, unsigned int reg) |
| { |
| struct mixel_mipi_phy_priv *priv = phy_get_drvdata(phy); |
| |
| writel(value, priv->base + reg); |
| } |
| |
| /* |
| * mixel_phy_mipi_set_phy_speed: |
| * Input params: |
| * bit_clk: PHY PLL needed output clock |
| * ref_clk: reference input clock for the PHY PLL |
| * |
| * Returns: |
| * 0: if the bit_clk can be achieved for the given ref_clk |
| * -EINVAL: otherwise |
| */ |
| int mixel_phy_mipi_set_phy_speed(struct phy *phy, |
| unsigned long bit_clk, |
| unsigned long ref_clk, |
| bool best_match) |
| { |
| struct mixel_mipi_phy_priv *priv = dev_get_drvdata(phy->dev.parent); |
| u32 div_rate; |
| u32 numerator = 0; |
| u32 denominator = 1; |
| |
| if (bit_clk > DATA_RATE_MAX_SPEED || bit_clk < DATA_RATE_MIN_SPEED) |
| return -EINVAL; |
| |
| /* simulated fixed point with 3 decimals */ |
| div_rate = (bit_clk * 1000) / ref_clk; |
| |
| while (denominator <= 256) { |
| if (div_rate % 1000 == 0) |
| numerator = div_rate / 1000; |
| if (numerator > 15) |
| break; |
| denominator = denominator << 1; |
| div_rate = div_rate << 1; |
| } |
| |
| /* CM ranges between 16 and 255 */ |
| /* CN ranges between 1 and 32 */ |
| /* CO is power of 2: 1, 2, 4, 8 */ |
| if (best_match && numerator < 16) |
| numerator = div_rate / 1000; |
| |
| if (best_match && numerator > 255) { |
| while (numerator > 255 && denominator > 1) { |
| numerator = DIV_ROUND_UP(numerator, 2); |
| denominator = denominator >> 1; |
| } |
| } |
| |
| if (numerator < 16 || numerator > 255) |
| return -EINVAL; |
| |
| if (best_match) |
| numerator = DIV_ROUND_UP(numerator, denominator) * denominator; |
| |
| priv->divider.cn = 1; |
| if (denominator > 8) { |
| priv->divider.cn = denominator >> 3; |
| denominator = 8; |
| } |
| priv->divider.co = denominator; |
| priv->divider.cm = numerator; |
| |
| priv->data_rate = bit_clk; |
| |
| return 0; |
| } |
| EXPORT_SYMBOL_GPL(mixel_phy_mipi_set_phy_speed); |
| |
| static int mixel_mipi_phy_enable(struct phy *phy, u32 reset) |
| { |
| struct mixel_mipi_phy_priv *priv = phy_get_drvdata(phy); |
| sc_err_t sci_err = 0; |
| sc_ipc_t ipc_handle = 0; |
| u32 mu_id; |
| |
| sci_err = sc_ipc_getMuID(&mu_id); |
| if (sci_err != SC_ERR_NONE) { |
| dev_err(&phy->dev, "Failed to get MU ID (%d)\n", sci_err); |
| return -ENODEV; |
| } |
| sci_err = sc_ipc_open(&ipc_handle, mu_id); |
| if (sci_err != SC_ERR_NONE) { |
| dev_err(&phy->dev, "Failed to open IPC (%d)\n", sci_err); |
| return -ENODEV; |
| } |
| |
| sci_err = sc_misc_set_control(ipc_handle, |
| priv->mipi_id, |
| SC_C_PHY_RESET, |
| reset); |
| if (sci_err != SC_ERR_NONE) { |
| dev_err(&phy->dev, "Failed to reset DPHY (%d)\n", sci_err); |
| sc_ipc_close(ipc_handle); |
| return -ENODEV; |
| } |
| |
| sc_ipc_close(ipc_handle); |
| |
| return 0; |
| } |
| |
| /* |
| * We tried our best here to use the values as specified in |
| * Reference Manual, but we got unstable results. So, these values |
| * are hacked from their original explanation as found in RM. |
| */ |
| static void mixel_phy_set_prg_regs(struct phy *phy) |
| { |
| struct mixel_mipi_phy_priv *priv = phy_get_drvdata(phy); |
| u32 step; |
| u32 step_num; |
| u32 step_max; |
| |
| /* MC_PRG_HS_PREPARE */ |
| if (priv->data_rate > MBPS(1000)) |
| phy_write(phy, 0x01, DPHY_MC_PRG_HS_PREPARE); |
| else |
| phy_write(phy, 0x00, DPHY_MC_PRG_HS_PREPARE); |
| |
| /* M_PRG_HS_PREPARE */ |
| if (priv->data_rate > MBPS(250)) |
| phy_write(phy, 0x00, DPHY_M_PRG_HS_PREPARE); |
| else |
| phy_write(phy, 0x01, DPHY_M_PRG_HS_PREPARE); |
| |
| /* MC_PRG_HS_ZERO */ |
| step_max = 48; |
| step = (DATA_RATE_MAX_SPEED - DATA_RATE_MIN_SPEED) / step_max; |
| step_num = ((priv->data_rate - DATA_RATE_MIN_SPEED) / step) + 1; |
| phy_write(phy, step_num, DPHY_MC_PRG_HS_ZERO); |
| |
| /* M_PRG_HS_ZERO */ |
| if (priv->data_rate < MBPS(1000)) |
| phy_write(phy, 0x09, DPHY_M_PRG_HS_ZERO); |
| else |
| phy_write(phy, 0x10, DPHY_M_PRG_HS_ZERO); |
| |
| /* MC_PRG_HS_TRAIL and M_PRG_HS_TRAIL */ |
| if (priv->data_rate < MBPS(1000)) { |
| phy_write(phy, 0x05, DPHY_MC_PRG_HS_TRAIL); |
| phy_write(phy, 0x05, DPHY_M_PRG_HS_TRAIL); |
| } else if (priv->data_rate < MBPS(1500)) { |
| phy_write(phy, 0x0C, DPHY_MC_PRG_HS_TRAIL); |
| phy_write(phy, 0x0C, DPHY_M_PRG_HS_TRAIL); |
| } else { |
| phy_write(phy, 0x0F, DPHY_MC_PRG_HS_TRAIL); |
| phy_write(phy, 0x0F, DPHY_M_PRG_HS_TRAIL); |
| } |
| } |
| |
| int mixel_mipi_phy_init(struct phy *phy) |
| { |
| struct mixel_mipi_phy_priv *priv = dev_get_drvdata(phy->dev.parent); |
| |
| mutex_lock(&priv->lock); |
| |
| mixel_phy_set_prg_regs(phy); |
| |
| phy_write(phy, 0x00, DPHY_LOCK_BYP); |
| phy_write(phy, 0x01, DPHY_TX_RCAL); |
| phy_write(phy, 0x00, DPHY_AUTO_PD_EN); |
| phy_write(phy, 0x01, DPHY_RXLPRP); |
| phy_write(phy, 0x01, DPHY_RXCDRP); |
| phy_write(phy, 0x25, DPHY_TST); |
| |
| /* VCO = REF_CLK * CM / CN * CO */ |
| if (priv->divider.cm < 16 || priv->divider.cm > 255 || |
| priv->divider.cn < 1 || priv->divider.cn > 32 || |
| priv->divider.co < 1 || priv->divider.co > 8) { |
| dev_err(&phy->dev, "Invalid CM/CN/CO values! (%u/%u/%u)\n", |
| priv->divider.cm, priv->divider.cn, priv->divider.co); |
| mutex_unlock(&priv->lock); |
| return -EINVAL; |
| } |
| dev_dbg(&phy->dev, "Using CM:%u CN:%u CO:%u\n", |
| priv->divider.cm, priv->divider.cn, priv->divider.co); |
| phy_write(phy, CM(priv->divider.cm), DPHY_CM); |
| phy_write(phy, CN(priv->divider.cn), DPHY_CN); |
| phy_write(phy, CO(priv->divider.co), DPHY_CO); |
| |
| mutex_unlock(&priv->lock); |
| |
| return 0; |
| } |
| |
| int mixel_mipi_phy_exit(struct phy *phy) |
| { |
| phy_write(phy, 0, DPHY_CM); |
| phy_write(phy, 0, DPHY_CN); |
| phy_write(phy, 0, DPHY_CO); |
| |
| return 0; |
| } |
| |
| static int mixel_mipi_phy_power_on(struct phy *phy) |
| { |
| struct mixel_mipi_phy_priv *priv = phy_get_drvdata(phy); |
| u32 lock, timeout; |
| int ret = 0; |
| |
| mutex_lock(&priv->lock); |
| |
| phy_write(phy, PWR_ON, DPHY_PD_DPHY); |
| phy_write(phy, PWR_ON, DPHY_PD_PLL); |
| |
| timeout = 100; |
| while (!(lock = phy_read(phy, DPHY_LOCK))) { |
| udelay(10); |
| if (--timeout == 0) { |
| dev_err(&phy->dev, "Could not get DPHY lock!\n"); |
| mutex_unlock(&priv->lock); |
| return -EINVAL; |
| } |
| } |
| |
| if (priv->have_sc) |
| ret = mixel_mipi_phy_enable(phy, 1); |
| |
| mutex_unlock(&priv->lock); |
| |
| return ret; |
| } |
| |
| static int mixel_mipi_phy_power_off(struct phy *phy) |
| { |
| struct mixel_mipi_phy_priv *priv = phy_get_drvdata(phy); |
| int ret = 0; |
| |
| mutex_lock(&priv->lock); |
| |
| phy_write(phy, PWR_OFF, DPHY_PD_PLL); |
| phy_write(phy, PWR_OFF, DPHY_PD_DPHY); |
| |
| if (priv->have_sc) |
| ret = mixel_mipi_phy_enable(phy, 0); |
| |
| mutex_unlock(&priv->lock); |
| |
| return ret; |
| } |
| |
| static const struct phy_ops mixel_mipi_phy_ops = { |
| .init = mixel_mipi_phy_init, |
| .exit = mixel_mipi_phy_exit, |
| .power_on = mixel_mipi_phy_power_on, |
| .power_off = mixel_mipi_phy_power_off, |
| .owner = THIS_MODULE, |
| }; |
| |
| static struct devtype imx8qm_dev = { .have_sc = true }; |
| static struct devtype imx8qxp_dev = { .have_sc = true }; |
| static struct devtype imx8mq_dev = { .have_sc = false }; |
| |
| static const struct of_device_id mixel_mipi_phy_of_match[] = { |
| { .compatible = "mixel,imx8qm-mipi-dsi-phy", .data = &imx8qm_dev }, |
| { .compatible = "mixel,imx8qxp-mipi-dsi-phy", .data = &imx8qxp_dev }, |
| { .compatible = "mixel,imx8mq-mipi-dsi-phy", .data = &imx8mq_dev }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, mixel_mipi_phy_of_match); |
| |
| static int mixel_mipi_phy_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct device_node *np = dev->of_node; |
| const struct of_device_id *of_id = |
| of_match_device(mixel_mipi_phy_of_match, dev); |
| const struct devtype *devtype = of_id->data; |
| struct phy_provider *phy_provider; |
| struct mixel_mipi_phy_priv *priv; |
| struct resource *res; |
| struct phy *phy; |
| u32 phy_id = 0; |
| |
| if (!np) |
| return -ENODEV; |
| |
| priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| if (!res) |
| return -ENODEV; |
| |
| priv->base = devm_ioremap(dev, res->start, SZ_256); |
| if (IS_ERR(priv->base)) |
| return PTR_ERR(priv->base); |
| |
| priv->have_sc = devtype->have_sc; |
| |
| phy_id = of_alias_get_id(np, "dsi_phy"); |
| if (phy_id < 0) { |
| dev_err(dev, "No dsi_phy alias found!"); |
| return phy_id; |
| } |
| |
| priv->mipi_id = phy_id?SC_R_MIPI_1:SC_R_MIPI_0; |
| |
| priv->dev = dev; |
| |
| mutex_init(&priv->lock); |
| dev_set_drvdata(dev, priv); |
| |
| phy = devm_phy_create(dev, np, &mixel_mipi_phy_ops); |
| if (IS_ERR(phy)) { |
| dev_err(dev, "Failed to create phy\n"); |
| return PTR_ERR(phy); |
| } |
| phy_set_drvdata(phy, priv); |
| |
| phy_provider = devm_of_phy_provider_register(dev, of_phy_simple_xlate); |
| |
| return PTR_ERR_OR_ZERO(phy_provider); |
| } |
| |
| static struct platform_driver mixel_mipi_phy_driver = { |
| .probe = mixel_mipi_phy_probe, |
| .driver = { |
| .name = "mixel-mipi-dsi-phy", |
| .of_match_table = mixel_mipi_phy_of_match, |
| } |
| }; |
| module_platform_driver(mixel_mipi_phy_driver); |
| |
| MODULE_AUTHOR("NXP Semiconductor"); |
| MODULE_DESCRIPTION("Mixel MIPI-DSI PHY driver"); |
| MODULE_LICENSE("GPL v2"); |