| /* |
| * Copyright 2018 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 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 devtype { |
| bool have_sc; |
| u8 reg_tx_rcal; |
| u8 reg_auto_pd_en; |
| u8 reg_rxlprp; |
| u8 reg_rxcdrp; |
| u8 reg_rxhs_settle; |
| u8 reg_bypass_pll; |
| }; |
| |
| struct mixel_mipi_phy_priv { |
| struct device *dev; |
| void __iomem *base; |
| const struct devtype *plat_data; |
| sc_rsrc_t mipi_id; |
| struct pll_divider divider; |
| struct mutex lock; |
| unsigned long data_rate; |
| }; |
| |
| |
| 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); |
| unsigned int hs_reg; |
| |
| /* MC_PRG_HS_PREPARE = 1.0 * Ttxescape if DPHY_MC_PRG_HS_PREPARE = 0 |
| * |
| * MC_PRG_HS_PREPARE = 1.5 * Ttxescape if DPHY_MC_PRG_HS_PREPARE = 1 |
| * |
| * Assume Ftxescape is 18-20 MHz with DPHY_MC_PRG_HS_PREPARE = 0, |
| * this gives 55-50 ns. |
| * The specification is 38 to 95 ns. |
| */ |
| phy_write(phy, 0x00, DPHY_MC_PRG_HS_PREPARE); |
| |
| /* PRG_HS_PREPARE |
| * for PRG_HS_PREPARE = 00, THS-PREPARE = 1 * TxClkEsc Period |
| * PRG_HS_PREPARE = 01, THS-PREPARE = 1.5 * TxClkEsc Period |
| * PRG_HS_PREPARE = 10, THS-PREPARE = 2 * TxClkEsc Period |
| * PRG_HS_PREPARE = 11, THS-PREPARE = 2.5 * TxClkEsc Period |
| * |
| * The specification for THS-PREPARE is |
| * Min (40ns + 4*UI) |
| * Max 85ns +6*UI |
| */ |
| if (priv->data_rate <= MBPS(61)) |
| phy_write(phy, 0x03, DPHY_M_PRG_HS_PREPARE); |
| else if (priv->data_rate <= MBPS(90)) |
| phy_write(phy, 0x02, DPHY_M_PRG_HS_PREPARE); |
| else if (priv->data_rate <= MBPS(500)) |
| phy_write(phy, 0x01, DPHY_M_PRG_HS_PREPARE); |
| else |
| phy_write(phy, 0x00, DPHY_M_PRG_HS_PREPARE); |
| |
| /* MC_PRG_HS_ZERO |
| * |
| * T-CLK-ZERO = ( MC_PRG_HS_ZERO + 3) * (TxByteClkHS Period) |
| * |
| * The minimum specification for THS-PREPARE is 262 ns. |
| * |
| */ |
| hs_reg = |
| /* simplified equation y = .034x - 2.5 |
| * |
| * This a linear interpolation of the values from the |
| * PHY user guide |
| */ |
| (34 * (priv->data_rate/1000000) - 2500) / 1000; |
| |
| if (hs_reg < 1) |
| hs_reg = 1; |
| phy_write(phy, hs_reg, DPHY_MC_PRG_HS_ZERO); |
| |
| /* M_PRG_HS_ZERO |
| * |
| * TT-HS-ZERO =(M_PRG_HS_ZERO + 6) * (TxByteClkHS Period) |
| * |
| * The minimum specification for THS-ZERO 105ns + 6*UI. |
| * |
| */ |
| hs_reg = |
| /* simplified equation y = .0144x - 4.75 |
| * |
| * This a linear interpolation of the values from the |
| * PHY user guide |
| */ |
| |
| (144 * (priv->data_rate/1000000) - 47500) / 10000; |
| |
| if (hs_reg < 1) |
| hs_reg = 1; |
| phy_write(phy, hs_reg, DPHY_M_PRG_HS_ZERO); |
| |
| /* MC_PRG_HS_TRAIL and M_PRG_HS_TRAIL |
| * |
| * THS-TRAIL =(PRG_HS_TRAIL) * (TxByteClkHS Period) |
| * |
| * The specification for THS-TRAIL is |
| * Min (60ns + 4*UI) |
| * Typical (82.5ns + 8*UI) |
| * Max (105ns + 12*UI) |
| * |
| */ |
| |
| hs_reg = |
| /* simplified equation y = .0103x + 1 |
| * |
| * This a linear interpolation of the values from the |
| * PHY user guide |
| */ |
| (103 * (priv->data_rate/1000000) + 10000) / 10000; |
| |
| if (hs_reg > 15) |
| hs_reg = 15; |
| if (hs_reg < 1) |
| hs_reg = 1; |
| |
| phy_write(phy, hs_reg, DPHY_MC_PRG_HS_TRAIL); |
| phy_write(phy, hs_reg, DPHY_M_PRG_HS_TRAIL); |
| |
| /* M_PRG_RXHS_SETTLE */ |
| if (priv->plat_data->reg_rxhs_settle == 0xFF) |
| return; |
| if (priv->data_rate < MBPS(80)) |
| phy_write(phy, 0x0d, priv->plat_data->reg_rxhs_settle); |
| else if (priv->data_rate < MBPS(90)) |
| phy_write(phy, 0x0c, priv->plat_data->reg_rxhs_settle); |
| else if (priv->data_rate < MBPS(125)) |
| phy_write(phy, 0x0b, priv->plat_data->reg_rxhs_settle); |
| else if (priv->data_rate < MBPS(150)) |
| phy_write(phy, 0x0a, priv->plat_data->reg_rxhs_settle); |
| else if (priv->data_rate < MBPS(225)) |
| phy_write(phy, 0x09, priv->plat_data->reg_rxhs_settle); |
| else if (priv->data_rate < MBPS(500)) |
| phy_write(phy, 0x08, priv->plat_data->reg_rxhs_settle); |
| else |
| phy_write(phy, 0x07, priv->plat_data->reg_rxhs_settle); |
| |
| } |
| |
| static int mixel_mipi_phy_init(struct phy *phy) |
| { |
| struct mixel_mipi_phy_priv *priv = dev_get_drvdata(phy->dev.parent); |
| |
| mutex_lock(&priv->lock); |
| |
| phy_write(phy, PWR_OFF, DPHY_PD_PLL); |
| phy_write(phy, PWR_OFF, DPHY_PD_DPHY); |
| |
| if (priv->plat_data->have_sc) { |
| int ret; |
| ret = mixel_mipi_phy_enable(phy, 0); |
| if (ret) |
| return ret; |
| } |
| |
| mixel_phy_set_prg_regs(phy); |
| |
| phy_write(phy, 0x00, DPHY_LOCK_BYP); |
| if (priv->plat_data->reg_tx_rcal != 0xFF) |
| phy_write(phy, 0x01, priv->plat_data->reg_tx_rcal); |
| if (priv->plat_data->reg_auto_pd_en != 0xFF) |
| phy_write(phy, 0x00, priv->plat_data->reg_auto_pd_en); |
| if (priv->plat_data->reg_rxlprp != 0xFF) |
| phy_write(phy, 0x02, priv->plat_data->reg_rxlprp); |
| if (priv->plat_data->reg_rxcdrp != 0xFF) |
| phy_write(phy, 0x02, priv->plat_data->reg_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; |
| } |
| |
| static 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_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"); |
| phy_write(phy, PWR_OFF, DPHY_PD_PLL); |
| mutex_unlock(&priv->lock); |
| return -EINVAL; |
| } |
| } |
| dev_dbg(&phy->dev, "DPHY lock acquired after %d tries\n", |
| (100 - timeout)); |
| |
| phy_write(phy, PWR_ON, DPHY_PD_DPHY); |
| |
| if (priv->plat_data->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->plat_data->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, |
| .reg_tx_rcal = 0xFF, |
| .reg_auto_pd_en = 0x38, |
| .reg_rxlprp = 0x3c, |
| .reg_rxcdrp = 0x40, |
| .reg_rxhs_settle = 0x44, |
| .reg_bypass_pll = 0xFF, |
| }; |
| static struct devtype imx8qxp_dev = { |
| .have_sc = true, |
| .reg_tx_rcal = 0xFF, |
| .reg_auto_pd_en = 0x38, |
| .reg_rxlprp = 0x3c, |
| .reg_rxcdrp = 0x40, |
| .reg_rxhs_settle = 0x44, |
| .reg_bypass_pll = 0xFF, |
| }; |
| static struct devtype imx8mq_dev = { |
| .have_sc = false, |
| .reg_tx_rcal = 0x38, |
| .reg_auto_pd_en = 0x3c, |
| .reg_rxlprp = 0x40, |
| .reg_rxcdrp = 0x44, |
| .reg_rxhs_settle = 0x48, |
| .reg_bypass_pll = 0x4c, |
| }; |
| |
| 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); |
| struct phy_provider *phy_provider; |
| struct mixel_mipi_phy_priv *priv; |
| struct resource *res; |
| struct phy *phy; |
| int 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->plat_data = of_id->data; |
| |
| 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"); |