diff mbox series

[02/14] phy: allwinner: phy-sun6i-mipi-dphy: Support D-PHY Rx mode for MIPI CSI-2

Message ID 20201023174546.504028-3-paul.kocialkowski@bootlin.com (mailing list archive)
State New, archived
Headers show
Series Allwinner MIPI CSI-2 support for A31/V3s/A83T | expand

Commit Message

Paul Kocialkowski Oct. 23, 2020, 5:45 p.m. UTC
The Allwinner A31 D-PHY supports both Rx and Tx modes. While the latter
is already supported and used for MIPI DSI this adds support for the
former, to be used with MIPI CSI-2.

This implementation is inspired by the Allwinner BSP implementation.

Signed-off-by: Paul Kocialkowski <paul.kocialkowski@bootlin.com>
---
 drivers/phy/allwinner/phy-sun6i-mipi-dphy.c | 164 +++++++++++++++++++-
 1 file changed, 160 insertions(+), 4 deletions(-)

Comments

Maxime Ripard Oct. 26, 2020, 3:38 p.m. UTC | #1
On Fri, Oct 23, 2020 at 07:45:34PM +0200, Paul Kocialkowski wrote:
> The Allwinner A31 D-PHY supports both Rx and Tx modes. While the latter
> is already supported and used for MIPI DSI this adds support for the
> former, to be used with MIPI CSI-2.
> 
> This implementation is inspired by the Allwinner BSP implementation.

Mentionning which BSP you took this from would be helpful

> Signed-off-by: Paul Kocialkowski <paul.kocialkowski@bootlin.com>
> ---
>  drivers/phy/allwinner/phy-sun6i-mipi-dphy.c | 164 +++++++++++++++++++-
>  1 file changed, 160 insertions(+), 4 deletions(-)
> 
> diff --git a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> index 1fa761ba6cbb..8bcd4bb79f60 100644
> --- a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> +++ b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> @@ -24,6 +24,14 @@
>  #define SUN6I_DPHY_TX_CTL_REG		0x04
>  #define SUN6I_DPHY_TX_CTL_HS_TX_CLK_CONT	BIT(28)
>  
> +#define SUN6I_DPHY_RX_CTL_REG		0x08
> +#define SUN6I_DPHY_RX_CTL_EN_DBC	BIT(31)
> +#define SUN6I_DPHY_RX_CTL_RX_CLK_FORCE	BIT(24)
> +#define SUN6I_DPHY_RX_CTL_RX_D3_FORCE	BIT(23)
> +#define SUN6I_DPHY_RX_CTL_RX_D2_FORCE	BIT(22)
> +#define SUN6I_DPHY_RX_CTL_RX_D1_FORCE	BIT(21)
> +#define SUN6I_DPHY_RX_CTL_RX_D0_FORCE	BIT(20)
> +

It's hard to tell from the diff, but it looks like you aligned the
BIT(..) with the register?

If so, you should follow the what the rest of this driver (ie, one more
indentation for register values).

>  #define SUN6I_DPHY_TX_TIME0_REG		0x10
>  #define SUN6I_DPHY_TX_TIME0_HS_TRAIL(n)		(((n) & 0xff) << 24)
>  #define SUN6I_DPHY_TX_TIME0_HS_PREPARE(n)	(((n) & 0xff) << 16)
> @@ -44,12 +52,29 @@
>  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA1(n)	(((n) & 0xff) << 8)
>  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA0(n)	((n) & 0xff)
>  
> +#define SUN6I_DPHY_RX_TIME0_REG		0x30
> +#define SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(n)	(((n) & 0xff) << 24)
> +#define SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(n)	(((n) & 0xff) << 16)
> +#define SUN6I_DPHY_RX_TIME0_LP_RX(n)		(((n) & 0xff) << 8)
> +
> +#define SUN6I_DPHY_RX_TIME1_REG		0x34
> +#define SUN6I_DPHY_RX_TIME1_RX_DLY(n)		(((n) & 0xfff) << 20)
> +#define SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(n)	((n) & 0xfffff)
> +
> +#define SUN6I_DPHY_RX_TIME2_REG		0x38
> +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA1(n)	(((n) & 0xff) << 8)
> +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(n)	((n) & 0xff)
> +
> +#define SUN6I_DPHY_RX_TIME3_REG		0x40
> +#define SUN6I_DPHY_RX_TIME3_LPRST_DLY(n)	(((n) & 0xffff) << 16)
> +
>  #define SUN6I_DPHY_ANA0_REG		0x4c
>  #define SUN6I_DPHY_ANA0_REG_PWS			BIT(31)
>  #define SUN6I_DPHY_ANA0_REG_DMPC		BIT(28)
>  #define SUN6I_DPHY_ANA0_REG_DMPD(n)		(((n) & 0xf) << 24)
>  #define SUN6I_DPHY_ANA0_REG_SLV(n)		(((n) & 7) << 12)
>  #define SUN6I_DPHY_ANA0_REG_DEN(n)		(((n) & 0xf) << 8)
> +#define SUN6I_DPHY_ANA0_REG_SFB(n)		(((n) & 3) << 2)
>  
>  #define SUN6I_DPHY_ANA1_REG		0x50
>  #define SUN6I_DPHY_ANA1_REG_VTTMODE		BIT(31)
> @@ -92,6 +117,8 @@ struct sun6i_dphy {
>  
>  	struct phy				*phy;
>  	struct phy_configure_opts_mipi_dphy	config;
> +
> +	int					submode;
>  };
>  
>  static int sun6i_dphy_init(struct phy *phy)
> @@ -105,6 +132,18 @@ static int sun6i_dphy_init(struct phy *phy)
>  	return 0;
>  }
>  
> +static int sun6i_dphy_set_mode(struct phy *phy, enum phy_mode mode, int submode)
> +{
> +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> +
> +	if (mode != PHY_MODE_MIPI_DPHY)
> +		return -EINVAL;
> +
> +	dphy->submode = submode;
> +
> +	return 0;
> +}
> +
>  static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
>  {
>  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> @@ -119,9 +158,8 @@ static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
>  	return 0;
>  }
>  
> -static int sun6i_dphy_power_on(struct phy *phy)
> +static int sun6i_dphy_tx_power_on(struct sun6i_dphy *dphy)
>  {
> -	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
>  	u8 lanes_mask = GENMASK(dphy->config.lanes - 1, 0);
>  
>  	regmap_write(dphy->regs, SUN6I_DPHY_TX_CTL_REG,
> @@ -211,12 +249,129 @@ static int sun6i_dphy_power_on(struct phy *phy)
>  	return 0;
>  }
>  
> +static int sun6i_dphy_rx_power_on(struct sun6i_dphy *dphy)
> +{
> +	/* Physical clock rate is actually half of symbol rate with DDR. */
> +	unsigned long mipi_symbol_rate = dphy->config.hs_clk_rate;
> +	unsigned long dphy_clk_rate;
> +	unsigned int rx_dly;
> +	unsigned int lprst_dly;
> +	u32 value;
> +
> +	dphy_clk_rate = clk_get_rate(dphy->mod_clk);
> +	if (!dphy_clk_rate)
> +		return -1;

Returning -1 is weird here?

> +
> +	/* Hardcoded timing parameters from the Allwinner BSP. */
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME0_REG,
> +		     SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(255) |
> +		     SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(255) |
> +		     SUN6I_DPHY_RX_TIME0_LP_RX(255));
> +
> +	/*
> +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> +	 * (probably internal divider/multiplier).
> +	 */
> +	rx_dly = 8 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 8));
> +
> +	/*
> +	 * The Allwinner BSP has an alternative formula for LP_RX_ULPS_WP:
> +	 * lp_ulps_wp_cnt = lp_ulps_wp_ms * lp_clk / 1000
> +	 * but does not use it and hardcodes 255 instead.
> +	 */
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME1_REG,
> +		     SUN6I_DPHY_RX_TIME1_RX_DLY(rx_dly) |
> +		     SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(255));
> +
> +	/* HS_RX_ANA0 value is hardcoded in the Allwinner BSP. */
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME2_REG,
> +		     SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(4));
> +
> +	/*
> +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> +	 * (probably internal divider/multiplier).
> +	 */
> +	lprst_dly = 4 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 2));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME3_REG,
> +		     SUN6I_DPHY_RX_TIME3_LPRST_DLY(lprst_dly));
> +
> +	/* Analog parameters are hardcoded in the Allwinner BSP. */
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
> +		     SUN6I_DPHY_ANA0_REG_PWS |
> +		     SUN6I_DPHY_ANA0_REG_SLV(7) |
> +		     SUN6I_DPHY_ANA0_REG_SFB(2));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG,
> +		     SUN6I_DPHY_ANA1_REG_SVTT(4));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG,
> +		     SUN6I_DPHY_ANA4_REG_DMPLVC |
> +		     SUN6I_DPHY_ANA4_REG_DMPLVD(1));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG,
> +		     SUN6I_DPHY_ANA2_REG_ENIB);
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG,
> +		     SUN6I_DPHY_ANA3_EN_LDOR |
> +		     SUN6I_DPHY_ANA3_EN_LDOC |
> +		     SUN6I_DPHY_ANA3_EN_LDOD);
> +
> +	/*
> +	 * Delay comes from the Allwinner BSP, likely for internal regulator
> +	 * ramp-up.
> +	 */
> +	udelay(3);
> +
> +	value = SUN6I_DPHY_RX_CTL_EN_DBC | SUN6I_DPHY_RX_CTL_RX_CLK_FORCE;
> +
> +	/*
> +	 * Rx data lane force-enable bits are used as regular RX enable by the
> +	 * Allwinner BSP.
> +	 */
> +	if (dphy->config.lanes >= 1)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D0_FORCE;
> +	if (dphy->config.lanes >= 2)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D1_FORCE;
> +	if (dphy->config.lanes >= 3)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D2_FORCE;
> +	if (dphy->config.lanes == 4)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D3_FORCE;
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_CTL_REG, value);
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
> +		     SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
> +		     SUN6I_DPHY_GCTL_EN);
> +
> +	return 0;
> +}
> +
> +static int sun6i_dphy_power_on(struct phy *phy)
> +{
> +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> +
> +	switch (dphy->submode) {
> +	case PHY_MIPI_DPHY_SUBMODE_TX:
> +		return sun6i_dphy_tx_power_on(dphy);
> +	case PHY_MIPI_DPHY_SUBMODE_RX:
> +		return sun6i_dphy_rx_power_on(dphy);
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +

Can one call power_on before set_mode?

>  static int sun6i_dphy_power_off(struct phy *phy)
>  {
>  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
>  
> -	regmap_update_bits(dphy->regs, SUN6I_DPHY_ANA1_REG,
> -			   SUN6I_DPHY_ANA1_REG_VTTMODE, 0);
> +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG, 0);
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG, 0);
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG, 0);

This looks like a change that should be mentioned (or in a separate
patch).

Maxime
Paul Kocialkowski Oct. 27, 2020, 9:23 a.m. UTC | #2
Hi,

On Mon 26 Oct 20, 16:38, Maxime Ripard wrote:
> On Fri, Oct 23, 2020 at 07:45:34PM +0200, Paul Kocialkowski wrote:
> > The Allwinner A31 D-PHY supports both Rx and Tx modes. While the latter
> > is already supported and used for MIPI DSI this adds support for the
> > former, to be used with MIPI CSI-2.
> > 
> > This implementation is inspired by the Allwinner BSP implementation.
> 
> Mentionning which BSP you took this from would be helpful

Sure! It's from the Github repo linked from https://linux-sunxi.org/V3s.
Would you like that I mention this URL explicitly or would it be enough to
mention "Allwinner's V3s Linux SDK" as they seem to call it?

> > Signed-off-by: Paul Kocialkowski <paul.kocialkowski@bootlin.com>
> > ---
> >  drivers/phy/allwinner/phy-sun6i-mipi-dphy.c | 164 +++++++++++++++++++-
> >  1 file changed, 160 insertions(+), 4 deletions(-)
> > 
> > diff --git a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> > index 1fa761ba6cbb..8bcd4bb79f60 100644
> > --- a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> > +++ b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> > @@ -24,6 +24,14 @@
> >  #define SUN6I_DPHY_TX_CTL_REG		0x04
> >  #define SUN6I_DPHY_TX_CTL_HS_TX_CLK_CONT	BIT(28)
> >  
> > +#define SUN6I_DPHY_RX_CTL_REG		0x08
> > +#define SUN6I_DPHY_RX_CTL_EN_DBC	BIT(31)
> > +#define SUN6I_DPHY_RX_CTL_RX_CLK_FORCE	BIT(24)
> > +#define SUN6I_DPHY_RX_CTL_RX_D3_FORCE	BIT(23)
> > +#define SUN6I_DPHY_RX_CTL_RX_D2_FORCE	BIT(22)
> > +#define SUN6I_DPHY_RX_CTL_RX_D1_FORCE	BIT(21)
> > +#define SUN6I_DPHY_RX_CTL_RX_D0_FORCE	BIT(20)
> > +
> 
> It's hard to tell from the diff, but it looks like you aligned the
> BIT(..) with the register?

That's correct, yes.

> If so, you should follow the what the rest of this driver (ie, one more
> indentation for register values).

I'll fix it in the next revision!

> >  #define SUN6I_DPHY_TX_TIME0_REG		0x10
> >  #define SUN6I_DPHY_TX_TIME0_HS_TRAIL(n)		(((n) & 0xff) << 24)
> >  #define SUN6I_DPHY_TX_TIME0_HS_PREPARE(n)	(((n) & 0xff) << 16)
> > @@ -44,12 +52,29 @@
> >  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA1(n)	(((n) & 0xff) << 8)
> >  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA0(n)	((n) & 0xff)
> >  
> > +#define SUN6I_DPHY_RX_TIME0_REG		0x30
> > +#define SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(n)	(((n) & 0xff) << 24)
> > +#define SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(n)	(((n) & 0xff) << 16)
> > +#define SUN6I_DPHY_RX_TIME0_LP_RX(n)		(((n) & 0xff) << 8)
> > +
> > +#define SUN6I_DPHY_RX_TIME1_REG		0x34
> > +#define SUN6I_DPHY_RX_TIME1_RX_DLY(n)		(((n) & 0xfff) << 20)
> > +#define SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(n)	((n) & 0xfffff)
> > +
> > +#define SUN6I_DPHY_RX_TIME2_REG		0x38
> > +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA1(n)	(((n) & 0xff) << 8)
> > +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(n)	((n) & 0xff)
> > +
> > +#define SUN6I_DPHY_RX_TIME3_REG		0x40
> > +#define SUN6I_DPHY_RX_TIME3_LPRST_DLY(n)	(((n) & 0xffff) << 16)
> > +
> >  #define SUN6I_DPHY_ANA0_REG		0x4c
> >  #define SUN6I_DPHY_ANA0_REG_PWS			BIT(31)
> >  #define SUN6I_DPHY_ANA0_REG_DMPC		BIT(28)
> >  #define SUN6I_DPHY_ANA0_REG_DMPD(n)		(((n) & 0xf) << 24)
> >  #define SUN6I_DPHY_ANA0_REG_SLV(n)		(((n) & 7) << 12)
> >  #define SUN6I_DPHY_ANA0_REG_DEN(n)		(((n) & 0xf) << 8)
> > +#define SUN6I_DPHY_ANA0_REG_SFB(n)		(((n) & 3) << 2)
> >  
> >  #define SUN6I_DPHY_ANA1_REG		0x50
> >  #define SUN6I_DPHY_ANA1_REG_VTTMODE		BIT(31)
> > @@ -92,6 +117,8 @@ struct sun6i_dphy {
> >  
> >  	struct phy				*phy;
> >  	struct phy_configure_opts_mipi_dphy	config;
> > +
> > +	int					submode;
> >  };
> >  
> >  static int sun6i_dphy_init(struct phy *phy)
> > @@ -105,6 +132,18 @@ static int sun6i_dphy_init(struct phy *phy)
> >  	return 0;
> >  }
> >  
> > +static int sun6i_dphy_set_mode(struct phy *phy, enum phy_mode mode, int submode)
> > +{
> > +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > +
> > +	if (mode != PHY_MODE_MIPI_DPHY)
> > +		return -EINVAL;
> > +
> > +	dphy->submode = submode;
> > +
> > +	return 0;
> > +}
> > +
> >  static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
> >  {
> >  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > @@ -119,9 +158,8 @@ static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
> >  	return 0;
> >  }
> >  
> > -static int sun6i_dphy_power_on(struct phy *phy)
> > +static int sun6i_dphy_tx_power_on(struct sun6i_dphy *dphy)
> >  {
> > -	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> >  	u8 lanes_mask = GENMASK(dphy->config.lanes - 1, 0);
> >  
> >  	regmap_write(dphy->regs, SUN6I_DPHY_TX_CTL_REG,
> > @@ -211,12 +249,129 @@ static int sun6i_dphy_power_on(struct phy *phy)
> >  	return 0;
> >  }
> >  
> > +static int sun6i_dphy_rx_power_on(struct sun6i_dphy *dphy)
> > +{
> > +	/* Physical clock rate is actually half of symbol rate with DDR. */
> > +	unsigned long mipi_symbol_rate = dphy->config.hs_clk_rate;
> > +	unsigned long dphy_clk_rate;
> > +	unsigned int rx_dly;
> > +	unsigned int lprst_dly;
> > +	u32 value;
> > +
> > +	dphy_clk_rate = clk_get_rate(dphy->mod_clk);
> > +	if (!dphy_clk_rate)
> > +		return -1;
> 
> Returning -1 is weird here?

What do you think would be a more appropriate error code to return?
It looks like some other drivers return -EINVAL when that happens (but many
don't do the check).

> > +
> > +	/* Hardcoded timing parameters from the Allwinner BSP. */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME0_REG,
> > +		     SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(255) |
> > +		     SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(255) |
> > +		     SUN6I_DPHY_RX_TIME0_LP_RX(255));
> > +
> > +	/*
> > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > +	 * (probably internal divider/multiplier).
> > +	 */
> > +	rx_dly = 8 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 8));
> > +
> > +	/*
> > +	 * The Allwinner BSP has an alternative formula for LP_RX_ULPS_WP:
> > +	 * lp_ulps_wp_cnt = lp_ulps_wp_ms * lp_clk / 1000
> > +	 * but does not use it and hardcodes 255 instead.
> > +	 */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME1_REG,
> > +		     SUN6I_DPHY_RX_TIME1_RX_DLY(rx_dly) |
> > +		     SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(255));
> > +
> > +	/* HS_RX_ANA0 value is hardcoded in the Allwinner BSP. */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME2_REG,
> > +		     SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(4));
> > +
> > +	/*
> > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > +	 * (probably internal divider/multiplier).
> > +	 */
> > +	lprst_dly = 4 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 2));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME3_REG,
> > +		     SUN6I_DPHY_RX_TIME3_LPRST_DLY(lprst_dly));
> > +
> > +	/* Analog parameters are hardcoded in the Allwinner BSP. */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
> > +		     SUN6I_DPHY_ANA0_REG_PWS |
> > +		     SUN6I_DPHY_ANA0_REG_SLV(7) |
> > +		     SUN6I_DPHY_ANA0_REG_SFB(2));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG,
> > +		     SUN6I_DPHY_ANA1_REG_SVTT(4));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG,
> > +		     SUN6I_DPHY_ANA4_REG_DMPLVC |
> > +		     SUN6I_DPHY_ANA4_REG_DMPLVD(1));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG,
> > +		     SUN6I_DPHY_ANA2_REG_ENIB);
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG,
> > +		     SUN6I_DPHY_ANA3_EN_LDOR |
> > +		     SUN6I_DPHY_ANA3_EN_LDOC |
> > +		     SUN6I_DPHY_ANA3_EN_LDOD);
> > +
> > +	/*
> > +	 * Delay comes from the Allwinner BSP, likely for internal regulator
> > +	 * ramp-up.
> > +	 */
> > +	udelay(3);
> > +
> > +	value = SUN6I_DPHY_RX_CTL_EN_DBC | SUN6I_DPHY_RX_CTL_RX_CLK_FORCE;
> > +
> > +	/*
> > +	 * Rx data lane force-enable bits are used as regular RX enable by the
> > +	 * Allwinner BSP.
> > +	 */
> > +	if (dphy->config.lanes >= 1)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D0_FORCE;
> > +	if (dphy->config.lanes >= 2)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D1_FORCE;
> > +	if (dphy->config.lanes >= 3)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D2_FORCE;
> > +	if (dphy->config.lanes == 4)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D3_FORCE;
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_CTL_REG, value);
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
> > +		     SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
> > +		     SUN6I_DPHY_GCTL_EN);
> > +
> > +	return 0;
> > +}
> > +
> > +static int sun6i_dphy_power_on(struct phy *phy)
> > +{
> > +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > +
> > +	switch (dphy->submode) {
> > +	case PHY_MIPI_DPHY_SUBMODE_TX:
> > +		return sun6i_dphy_tx_power_on(dphy);
> > +	case PHY_MIPI_DPHY_SUBMODE_RX:
> > +		return sun6i_dphy_rx_power_on(dphy);
> > +	default:
> > +		return -EINVAL;
> > +	}
> > +}
> > +
> 
> Can one call power_on before set_mode?

I didn't find anything indicating this is illegal. What would happen here is
that the D-PHY would be configured to PHY_MIPI_DPHY_SUBMODE_TX (submode == 0)
at power-on if set_mode is not called before.

I think it's fair to expect that it's too late to change the mode once the PHY
was powered on. Maybe we should return -EBUSY on set_mode when power on was
already requested?

> >  static int sun6i_dphy_power_off(struct phy *phy)
> >  {
> >  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> >  
> > -	regmap_update_bits(dphy->regs, SUN6I_DPHY_ANA1_REG,
> > -			   SUN6I_DPHY_ANA1_REG_VTTMODE, 0);
> > +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG, 0);
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG, 0);
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG, 0);
> 
> This looks like a change that should be mentioned (or in a separate
> patch).

Right, this is a general change that applies to both Tx and Rx to cut-off the
internal regulators. It's not directly related to this change so I think making
it a preliminary patch would make sense.

Thanks for the review!

Paul
Maxime Ripard Oct. 27, 2020, 6:28 p.m. UTC | #3
Hi,

On Tue, Oct 27, 2020 at 10:23:26AM +0100, Paul Kocialkowski wrote:
> On Mon 26 Oct 20, 16:38, Maxime Ripard wrote:
> > On Fri, Oct 23, 2020 at 07:45:34PM +0200, Paul Kocialkowski wrote:
> > > The Allwinner A31 D-PHY supports both Rx and Tx modes. While the latter
> > > is already supported and used for MIPI DSI this adds support for the
> > > former, to be used with MIPI CSI-2.
> > > 
> > > This implementation is inspired by the Allwinner BSP implementation.
> > 
> > Mentionning which BSP you took this from would be helpful
> 
> Sure! It's from the Github repo linked from https://linux-sunxi.org/V3s.
> Would you like that I mention this URL explicitly or would it be enough to
> mention "Allwinner's V3s Linux SDK" as they seem to call it?

Yeah, that would be great
> > > +static int sun6i_dphy_rx_power_on(struct sun6i_dphy *dphy)
> > > +{
> > > +	/* Physical clock rate is actually half of symbol rate with DDR. */
> > > +	unsigned long mipi_symbol_rate = dphy->config.hs_clk_rate;
> > > +	unsigned long dphy_clk_rate;
> > > +	unsigned int rx_dly;
> > > +	unsigned int lprst_dly;
> > > +	u32 value;
> > > +
> > > +	dphy_clk_rate = clk_get_rate(dphy->mod_clk);
> > > +	if (!dphy_clk_rate)
> > > +		return -1;
> > 
> > Returning -1 is weird here?
> 
> What do you think would be a more appropriate error code to return?
> It looks like some other drivers return -EINVAL when that happens (but many
> don't do the check).

Yeah, EINVAL at least is better than ENOPERM 

> > > +
> > > +	/* Hardcoded timing parameters from the Allwinner BSP. */
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME0_REG,
> > > +		     SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(255) |
> > > +		     SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(255) |
> > > +		     SUN6I_DPHY_RX_TIME0_LP_RX(255));
> > > +
> > > +	/*
> > > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > > +	 * (probably internal divider/multiplier).
> > > +	 */
> > > +	rx_dly = 8 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 8));
> > > +
> > > +	/*
> > > +	 * The Allwinner BSP has an alternative formula for LP_RX_ULPS_WP:
> > > +	 * lp_ulps_wp_cnt = lp_ulps_wp_ms * lp_clk / 1000
> > > +	 * but does not use it and hardcodes 255 instead.
> > > +	 */
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME1_REG,
> > > +		     SUN6I_DPHY_RX_TIME1_RX_DLY(rx_dly) |
> > > +		     SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(255));
> > > +
> > > +	/* HS_RX_ANA0 value is hardcoded in the Allwinner BSP. */
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME2_REG,
> > > +		     SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(4));
> > > +
> > > +	/*
> > > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > > +	 * (probably internal divider/multiplier).
> > > +	 */
> > > +	lprst_dly = 4 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 2));
> > > +
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME3_REG,
> > > +		     SUN6I_DPHY_RX_TIME3_LPRST_DLY(lprst_dly));
> > > +
> > > +	/* Analog parameters are hardcoded in the Allwinner BSP. */
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
> > > +		     SUN6I_DPHY_ANA0_REG_PWS |
> > > +		     SUN6I_DPHY_ANA0_REG_SLV(7) |
> > > +		     SUN6I_DPHY_ANA0_REG_SFB(2));
> > > +
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG,
> > > +		     SUN6I_DPHY_ANA1_REG_SVTT(4));
> > > +
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG,
> > > +		     SUN6I_DPHY_ANA4_REG_DMPLVC |
> > > +		     SUN6I_DPHY_ANA4_REG_DMPLVD(1));
> > > +
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG,
> > > +		     SUN6I_DPHY_ANA2_REG_ENIB);
> > > +
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG,
> > > +		     SUN6I_DPHY_ANA3_EN_LDOR |
> > > +		     SUN6I_DPHY_ANA3_EN_LDOC |
> > > +		     SUN6I_DPHY_ANA3_EN_LDOD);
> > > +
> > > +	/*
> > > +	 * Delay comes from the Allwinner BSP, likely for internal regulator
> > > +	 * ramp-up.
> > > +	 */
> > > +	udelay(3);
> > > +
> > > +	value = SUN6I_DPHY_RX_CTL_EN_DBC | SUN6I_DPHY_RX_CTL_RX_CLK_FORCE;
> > > +
> > > +	/*
> > > +	 * Rx data lane force-enable bits are used as regular RX enable by the
> > > +	 * Allwinner BSP.
> > > +	 */
> > > +	if (dphy->config.lanes >= 1)
> > > +		value |= SUN6I_DPHY_RX_CTL_RX_D0_FORCE;
> > > +	if (dphy->config.lanes >= 2)
> > > +		value |= SUN6I_DPHY_RX_CTL_RX_D1_FORCE;
> > > +	if (dphy->config.lanes >= 3)
> > > +		value |= SUN6I_DPHY_RX_CTL_RX_D2_FORCE;
> > > +	if (dphy->config.lanes == 4)
> > > +		value |= SUN6I_DPHY_RX_CTL_RX_D3_FORCE;
> > > +
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_CTL_REG, value);
> > > +
> > > +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
> > > +		     SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
> > > +		     SUN6I_DPHY_GCTL_EN);
> > > +
> > > +	return 0;
> > > +}
> > > +
> > > +static int sun6i_dphy_power_on(struct phy *phy)
> > > +{
> > > +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > > +
> > > +	switch (dphy->submode) {
> > > +	case PHY_MIPI_DPHY_SUBMODE_TX:
> > > +		return sun6i_dphy_tx_power_on(dphy);
> > > +	case PHY_MIPI_DPHY_SUBMODE_RX:
> > > +		return sun6i_dphy_rx_power_on(dphy);
> > > +	default:
> > > +		return -EINVAL;
> > > +	}
> > > +}
> > > +
> > 
> > Can one call power_on before set_mode?
> 
> I didn't find anything indicating this is illegal. What would happen here is
> that the D-PHY would be configured to PHY_MIPI_DPHY_SUBMODE_TX (submode == 0)
> at power-on if set_mode is not called before.
> 
> I think it's fair to expect that it's too late to change the mode once the PHY
> was powered on. Maybe we should return -EBUSY on set_mode when power on was
> already requested?

Or maybe we can just clarify it in the framework/function documentation

Maxime
Helen Koike Oct. 30, 2020, 10:44 p.m. UTC | #4
Hello,

On 10/23/20 2:45 PM, Paul Kocialkowski wrote:
> The Allwinner A31 D-PHY supports both Rx and Tx modes. While the latter
> is already supported and used for MIPI DSI this adds support for the
> former, to be used with MIPI CSI-2.
> 
> This implementation is inspired by the Allwinner BSP implementation.
> 
> Signed-off-by: Paul Kocialkowski <paul.kocialkowski@bootlin.com>
> ---
>  drivers/phy/allwinner/phy-sun6i-mipi-dphy.c | 164 +++++++++++++++++++-
>  1 file changed, 160 insertions(+), 4 deletions(-)
> 
> diff --git a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> index 1fa761ba6cbb..8bcd4bb79f60 100644
> --- a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> +++ b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> @@ -24,6 +24,14 @@
>  #define SUN6I_DPHY_TX_CTL_REG		0x04
>  #define SUN6I_DPHY_TX_CTL_HS_TX_CLK_CONT	BIT(28)
>  
> +#define SUN6I_DPHY_RX_CTL_REG		0x08
> +#define SUN6I_DPHY_RX_CTL_EN_DBC	BIT(31)
> +#define SUN6I_DPHY_RX_CTL_RX_CLK_FORCE	BIT(24)
> +#define SUN6I_DPHY_RX_CTL_RX_D3_FORCE	BIT(23)
> +#define SUN6I_DPHY_RX_CTL_RX_D2_FORCE	BIT(22)
> +#define SUN6I_DPHY_RX_CTL_RX_D1_FORCE	BIT(21)
> +#define SUN6I_DPHY_RX_CTL_RX_D0_FORCE	BIT(20)
> +
>  #define SUN6I_DPHY_TX_TIME0_REG		0x10
>  #define SUN6I_DPHY_TX_TIME0_HS_TRAIL(n)		(((n) & 0xff) << 24)
>  #define SUN6I_DPHY_TX_TIME0_HS_PREPARE(n)	(((n) & 0xff) << 16)
> @@ -44,12 +52,29 @@
>  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA1(n)	(((n) & 0xff) << 8)
>  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA0(n)	((n) & 0xff)
>  
> +#define SUN6I_DPHY_RX_TIME0_REG		0x30
> +#define SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(n)	(((n) & 0xff) << 24)
> +#define SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(n)	(((n) & 0xff) << 16)
> +#define SUN6I_DPHY_RX_TIME0_LP_RX(n)		(((n) & 0xff) << 8)
> +
> +#define SUN6I_DPHY_RX_TIME1_REG		0x34
> +#define SUN6I_DPHY_RX_TIME1_RX_DLY(n)		(((n) & 0xfff) << 20)
> +#define SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(n)	((n) & 0xfffff)
> +
> +#define SUN6I_DPHY_RX_TIME2_REG		0x38
> +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA1(n)	(((n) & 0xff) << 8)
> +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(n)	((n) & 0xff)
> +
> +#define SUN6I_DPHY_RX_TIME3_REG		0x40
> +#define SUN6I_DPHY_RX_TIME3_LPRST_DLY(n)	(((n) & 0xffff) << 16)
> +
>  #define SUN6I_DPHY_ANA0_REG		0x4c
>  #define SUN6I_DPHY_ANA0_REG_PWS			BIT(31)
>  #define SUN6I_DPHY_ANA0_REG_DMPC		BIT(28)
>  #define SUN6I_DPHY_ANA0_REG_DMPD(n)		(((n) & 0xf) << 24)
>  #define SUN6I_DPHY_ANA0_REG_SLV(n)		(((n) & 7) << 12)
>  #define SUN6I_DPHY_ANA0_REG_DEN(n)		(((n) & 0xf) << 8)
> +#define SUN6I_DPHY_ANA0_REG_SFB(n)		(((n) & 3) << 2)
>  
>  #define SUN6I_DPHY_ANA1_REG		0x50
>  #define SUN6I_DPHY_ANA1_REG_VTTMODE		BIT(31)
> @@ -92,6 +117,8 @@ struct sun6i_dphy {
>  
>  	struct phy				*phy;
>  	struct phy_configure_opts_mipi_dphy	config;
> +
> +	int					submode;
>  };
>  
>  static int sun6i_dphy_init(struct phy *phy)
> @@ -105,6 +132,18 @@ static int sun6i_dphy_init(struct phy *phy)
>  	return 0;
>  }
>  
> +static int sun6i_dphy_set_mode(struct phy *phy, enum phy_mode mode, int submode)
> +{
> +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> +
> +	if (mode != PHY_MODE_MIPI_DPHY)
> +		return -EINVAL;
> +
> +	dphy->submode = submode;

Shouldn't you check if the submode is valid here?

> +
> +	return 0;
> +}
> +
>  static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
>  {
>  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> @@ -119,9 +158,8 @@ static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
>  	return 0;
>  }
>  
> -static int sun6i_dphy_power_on(struct phy *phy)
> +static int sun6i_dphy_tx_power_on(struct sun6i_dphy *dphy)
>  {
> -	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
>  	u8 lanes_mask = GENMASK(dphy->config.lanes - 1, 0);
>  
>  	regmap_write(dphy->regs, SUN6I_DPHY_TX_CTL_REG,
> @@ -211,12 +249,129 @@ static int sun6i_dphy_power_on(struct phy *phy)
>  	return 0;
>  }
>  
> +static int sun6i_dphy_rx_power_on(struct sun6i_dphy *dphy)
> +{
> +	/* Physical clock rate is actually half of symbol rate with DDR. */
> +	unsigned long mipi_symbol_rate = dphy->config.hs_clk_rate;
> +	unsigned long dphy_clk_rate;
> +	unsigned int rx_dly;
> +	unsigned int lprst_dly;
> +	u32 value;
> +
> +	dphy_clk_rate = clk_get_rate(dphy->mod_clk);
> +	if (!dphy_clk_rate)
> +		return -1;
> +
> +	/* Hardcoded timing parameters from the Allwinner BSP. */
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME0_REG,
> +		     SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(255) |
> +		     SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(255) |
> +		     SUN6I_DPHY_RX_TIME0_LP_RX(255));
> +
> +	/*
> +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> +	 * (probably internal divider/multiplier).
> +	 */
> +	rx_dly = 8 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 8));
> +
> +	/*
> +	 * The Allwinner BSP has an alternative formula for LP_RX_ULPS_WP:
> +	 * lp_ulps_wp_cnt = lp_ulps_wp_ms * lp_clk / 1000
> +	 * but does not use it and hardcodes 255 instead.
> +	 */
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME1_REG,
> +		     SUN6I_DPHY_RX_TIME1_RX_DLY(rx_dly) |
> +		     SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(255));
> +
> +	/* HS_RX_ANA0 value is hardcoded in the Allwinner BSP. */
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME2_REG,
> +		     SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(4));
> +
> +	/*
> +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> +	 * (probably internal divider/multiplier).
> +	 */
> +	lprst_dly = 4 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 2));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME3_REG,
> +		     SUN6I_DPHY_RX_TIME3_LPRST_DLY(lprst_dly));
> +
> +	/* Analog parameters are hardcoded in the Allwinner BSP. */
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
> +		     SUN6I_DPHY_ANA0_REG_PWS |
> +		     SUN6I_DPHY_ANA0_REG_SLV(7) |
> +		     SUN6I_DPHY_ANA0_REG_SFB(2));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG,
> +		     SUN6I_DPHY_ANA1_REG_SVTT(4));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG,
> +		     SUN6I_DPHY_ANA4_REG_DMPLVC |
> +		     SUN6I_DPHY_ANA4_REG_DMPLVD(1));
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG,
> +		     SUN6I_DPHY_ANA2_REG_ENIB);
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG,
> +		     SUN6I_DPHY_ANA3_EN_LDOR |
> +		     SUN6I_DPHY_ANA3_EN_LDOC |
> +		     SUN6I_DPHY_ANA3_EN_LDOD);
> +
> +	/*
> +	 * Delay comes from the Allwinner BSP, likely for internal regulator
> +	 * ramp-up.
> +	 */
> +	udelay(3);
> +
> +	value = SUN6I_DPHY_RX_CTL_EN_DBC | SUN6I_DPHY_RX_CTL_RX_CLK_FORCE;
> +
> +	/*
> +	 * Rx data lane force-enable bits are used as regular RX enable by the
> +	 * Allwinner BSP.
> +	 */
> +	if (dphy->config.lanes >= 1)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D0_FORCE;
> +	if (dphy->config.lanes >= 2)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D1_FORCE;
> +	if (dphy->config.lanes >= 3)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D2_FORCE;
> +	if (dphy->config.lanes == 4)
> +		value |= SUN6I_DPHY_RX_CTL_RX_D3_FORCE;

I would replace this by a switch case with fallthrough to avoid too many comparisons
to the same value.

Regards,
Helen

> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_RX_CTL_REG, value);
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
> +		     SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
> +		     SUN6I_DPHY_GCTL_EN);
> +
> +	return 0;
> +}
> +
> +static int sun6i_dphy_power_on(struct phy *phy)
> +{
> +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> +
> +	switch (dphy->submode) {
> +	case PHY_MIPI_DPHY_SUBMODE_TX:
> +		return sun6i_dphy_tx_power_on(dphy);
> +	case PHY_MIPI_DPHY_SUBMODE_RX:
> +		return sun6i_dphy_rx_power_on(dphy);
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
>  static int sun6i_dphy_power_off(struct phy *phy)
>  {
>  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
>  
> -	regmap_update_bits(dphy->regs, SUN6I_DPHY_ANA1_REG,
> -			   SUN6I_DPHY_ANA1_REG_VTTMODE, 0);
> +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG, 0);
> +
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG, 0);
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG, 0);
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG, 0);
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG, 0);
> +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG, 0);
>  
>  	return 0;
>  }
> @@ -234,6 +389,7 @@ static int sun6i_dphy_exit(struct phy *phy)
>  
>  
>  static const struct phy_ops sun6i_dphy_ops = {
> +	.set_mode	= sun6i_dphy_set_mode,
>  	.configure	= sun6i_dphy_configure,
>  	.power_on	= sun6i_dphy_power_on,
>  	.power_off	= sun6i_dphy_power_off,
>
Paul Kocialkowski Nov. 4, 2020, 10:53 a.m. UTC | #5
Hi,

On Tue 27 Oct 20, 19:28, Maxime Ripard wrote:
> 
> Hi,
> 
> On Tue, Oct 27, 2020 at 10:23:26AM +0100, Paul Kocialkowski wrote:
> > On Mon 26 Oct 20, 16:38, Maxime Ripard wrote:
> > > On Fri, Oct 23, 2020 at 07:45:34PM +0200, Paul Kocialkowski wrote:
> > > > The Allwinner A31 D-PHY supports both Rx and Tx modes. While the latter
> > > > is already supported and used for MIPI DSI this adds support for the
> > > > former, to be used with MIPI CSI-2.
> > > > 
> > > > This implementation is inspired by the Allwinner BSP implementation.
> > > 
> > > Mentionning which BSP you took this from would be helpful
> > 
> > Sure! It's from the Github repo linked from https://linux-sunxi.org/V3s.
> > Would you like that I mention this URL explicitly or would it be enough to
> > mention "Allwinner's V3s Linux SDK" as they seem to call it?
> 
> Yeah, that would be great
> > > > +static int sun6i_dphy_rx_power_on(struct sun6i_dphy *dphy)
> > > > +{
> > > > +	/* Physical clock rate is actually half of symbol rate with DDR. */
> > > > +	unsigned long mipi_symbol_rate = dphy->config.hs_clk_rate;
> > > > +	unsigned long dphy_clk_rate;
> > > > +	unsigned int rx_dly;
> > > > +	unsigned int lprst_dly;
> > > > +	u32 value;
> > > > +
> > > > +	dphy_clk_rate = clk_get_rate(dphy->mod_clk);
> > > > +	if (!dphy_clk_rate)
> > > > +		return -1;
> > > 
> > > Returning -1 is weird here?
> > 
> > What do you think would be a more appropriate error code to return?
> > It looks like some other drivers return -EINVAL when that happens (but many
> > don't do the check).
> 
> Yeah, EINVAL at least is better than ENOPERM 
> 
> > > > +
> > > > +	/* Hardcoded timing parameters from the Allwinner BSP. */
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME0_REG,
> > > > +		     SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(255) |
> > > > +		     SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(255) |
> > > > +		     SUN6I_DPHY_RX_TIME0_LP_RX(255));
> > > > +
> > > > +	/*
> > > > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > > > +	 * (probably internal divider/multiplier).
> > > > +	 */
> > > > +	rx_dly = 8 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 8));
> > > > +
> > > > +	/*
> > > > +	 * The Allwinner BSP has an alternative formula for LP_RX_ULPS_WP:
> > > > +	 * lp_ulps_wp_cnt = lp_ulps_wp_ms * lp_clk / 1000
> > > > +	 * but does not use it and hardcodes 255 instead.
> > > > +	 */
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME1_REG,
> > > > +		     SUN6I_DPHY_RX_TIME1_RX_DLY(rx_dly) |
> > > > +		     SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(255));
> > > > +
> > > > +	/* HS_RX_ANA0 value is hardcoded in the Allwinner BSP. */
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME2_REG,
> > > > +		     SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(4));
> > > > +
> > > > +	/*
> > > > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > > > +	 * (probably internal divider/multiplier).
> > > > +	 */
> > > > +	lprst_dly = 4 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 2));
> > > > +
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME3_REG,
> > > > +		     SUN6I_DPHY_RX_TIME3_LPRST_DLY(lprst_dly));
> > > > +
> > > > +	/* Analog parameters are hardcoded in the Allwinner BSP. */
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
> > > > +		     SUN6I_DPHY_ANA0_REG_PWS |
> > > > +		     SUN6I_DPHY_ANA0_REG_SLV(7) |
> > > > +		     SUN6I_DPHY_ANA0_REG_SFB(2));
> > > > +
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG,
> > > > +		     SUN6I_DPHY_ANA1_REG_SVTT(4));
> > > > +
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG,
> > > > +		     SUN6I_DPHY_ANA4_REG_DMPLVC |
> > > > +		     SUN6I_DPHY_ANA4_REG_DMPLVD(1));
> > > > +
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG,
> > > > +		     SUN6I_DPHY_ANA2_REG_ENIB);
> > > > +
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG,
> > > > +		     SUN6I_DPHY_ANA3_EN_LDOR |
> > > > +		     SUN6I_DPHY_ANA3_EN_LDOC |
> > > > +		     SUN6I_DPHY_ANA3_EN_LDOD);
> > > > +
> > > > +	/*
> > > > +	 * Delay comes from the Allwinner BSP, likely for internal regulator
> > > > +	 * ramp-up.
> > > > +	 */
> > > > +	udelay(3);
> > > > +
> > > > +	value = SUN6I_DPHY_RX_CTL_EN_DBC | SUN6I_DPHY_RX_CTL_RX_CLK_FORCE;
> > > > +
> > > > +	/*
> > > > +	 * Rx data lane force-enable bits are used as regular RX enable by the
> > > > +	 * Allwinner BSP.
> > > > +	 */
> > > > +	if (dphy->config.lanes >= 1)
> > > > +		value |= SUN6I_DPHY_RX_CTL_RX_D0_FORCE;
> > > > +	if (dphy->config.lanes >= 2)
> > > > +		value |= SUN6I_DPHY_RX_CTL_RX_D1_FORCE;
> > > > +	if (dphy->config.lanes >= 3)
> > > > +		value |= SUN6I_DPHY_RX_CTL_RX_D2_FORCE;
> > > > +	if (dphy->config.lanes == 4)
> > > > +		value |= SUN6I_DPHY_RX_CTL_RX_D3_FORCE;
> > > > +
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_CTL_REG, value);
> > > > +
> > > > +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
> > > > +		     SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
> > > > +		     SUN6I_DPHY_GCTL_EN);
> > > > +
> > > > +	return 0;
> > > > +}
> > > > +
> > > > +static int sun6i_dphy_power_on(struct phy *phy)
> > > > +{
> > > > +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > > > +
> > > > +	switch (dphy->submode) {
> > > > +	case PHY_MIPI_DPHY_SUBMODE_TX:
> > > > +		return sun6i_dphy_tx_power_on(dphy);
> > > > +	case PHY_MIPI_DPHY_SUBMODE_RX:
> > > > +		return sun6i_dphy_rx_power_on(dphy);
> > > > +	default:
> > > > +		return -EINVAL;
> > > > +	}
> > > > +}
> > > > +
> > > 
> > > Can one call power_on before set_mode?
> > 
> > I didn't find anything indicating this is illegal. What would happen here is
> > that the D-PHY would be configured to PHY_MIPI_DPHY_SUBMODE_TX (submode == 0)
> > at power-on if set_mode is not called before.
> > 
> > I think it's fair to expect that it's too late to change the mode once the PHY
> > was powered on. Maybe we should return -EBUSY on set_mode when power on was
> > already requested?
> 
> Or maybe we can just clarify it in the framework/function documentation

Agreed, I'll add a patch in that direction. I would also be tempted to check on
phy->power_count to return -EBUSY in phy_set_mode_ext so that the behavior is
enforced.

What do you think?

Cheers,

Paul
Paul Kocialkowski Nov. 4, 2020, 10:54 a.m. UTC | #6
Hi Helen and thanks for the review,

On Fri 30 Oct 20, 19:44, Helen Koike wrote:
> On 10/23/20 2:45 PM, Paul Kocialkowski wrote:
> > The Allwinner A31 D-PHY supports both Rx and Tx modes. While the latter
> > is already supported and used for MIPI DSI this adds support for the
> > former, to be used with MIPI CSI-2.
> > 
> > This implementation is inspired by the Allwinner BSP implementation.
> > 
> > Signed-off-by: Paul Kocialkowski <paul.kocialkowski@bootlin.com>
> > ---
> >  drivers/phy/allwinner/phy-sun6i-mipi-dphy.c | 164 +++++++++++++++++++-
> >  1 file changed, 160 insertions(+), 4 deletions(-)
> > 
> > diff --git a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> > index 1fa761ba6cbb..8bcd4bb79f60 100644
> > --- a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> > +++ b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
> > @@ -24,6 +24,14 @@
> >  #define SUN6I_DPHY_TX_CTL_REG		0x04
> >  #define SUN6I_DPHY_TX_CTL_HS_TX_CLK_CONT	BIT(28)
> >  
> > +#define SUN6I_DPHY_RX_CTL_REG		0x08
> > +#define SUN6I_DPHY_RX_CTL_EN_DBC	BIT(31)
> > +#define SUN6I_DPHY_RX_CTL_RX_CLK_FORCE	BIT(24)
> > +#define SUN6I_DPHY_RX_CTL_RX_D3_FORCE	BIT(23)
> > +#define SUN6I_DPHY_RX_CTL_RX_D2_FORCE	BIT(22)
> > +#define SUN6I_DPHY_RX_CTL_RX_D1_FORCE	BIT(21)
> > +#define SUN6I_DPHY_RX_CTL_RX_D0_FORCE	BIT(20)
> > +
> >  #define SUN6I_DPHY_TX_TIME0_REG		0x10
> >  #define SUN6I_DPHY_TX_TIME0_HS_TRAIL(n)		(((n) & 0xff) << 24)
> >  #define SUN6I_DPHY_TX_TIME0_HS_PREPARE(n)	(((n) & 0xff) << 16)
> > @@ -44,12 +52,29 @@
> >  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA1(n)	(((n) & 0xff) << 8)
> >  #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA0(n)	((n) & 0xff)
> >  
> > +#define SUN6I_DPHY_RX_TIME0_REG		0x30
> > +#define SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(n)	(((n) & 0xff) << 24)
> > +#define SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(n)	(((n) & 0xff) << 16)
> > +#define SUN6I_DPHY_RX_TIME0_LP_RX(n)		(((n) & 0xff) << 8)
> > +
> > +#define SUN6I_DPHY_RX_TIME1_REG		0x34
> > +#define SUN6I_DPHY_RX_TIME1_RX_DLY(n)		(((n) & 0xfff) << 20)
> > +#define SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(n)	((n) & 0xfffff)
> > +
> > +#define SUN6I_DPHY_RX_TIME2_REG		0x38
> > +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA1(n)	(((n) & 0xff) << 8)
> > +#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(n)	((n) & 0xff)
> > +
> > +#define SUN6I_DPHY_RX_TIME3_REG		0x40
> > +#define SUN6I_DPHY_RX_TIME3_LPRST_DLY(n)	(((n) & 0xffff) << 16)
> > +
> >  #define SUN6I_DPHY_ANA0_REG		0x4c
> >  #define SUN6I_DPHY_ANA0_REG_PWS			BIT(31)
> >  #define SUN6I_DPHY_ANA0_REG_DMPC		BIT(28)
> >  #define SUN6I_DPHY_ANA0_REG_DMPD(n)		(((n) & 0xf) << 24)
> >  #define SUN6I_DPHY_ANA0_REG_SLV(n)		(((n) & 7) << 12)
> >  #define SUN6I_DPHY_ANA0_REG_DEN(n)		(((n) & 0xf) << 8)
> > +#define SUN6I_DPHY_ANA0_REG_SFB(n)		(((n) & 3) << 2)
> >  
> >  #define SUN6I_DPHY_ANA1_REG		0x50
> >  #define SUN6I_DPHY_ANA1_REG_VTTMODE		BIT(31)
> > @@ -92,6 +117,8 @@ struct sun6i_dphy {
> >  
> >  	struct phy				*phy;
> >  	struct phy_configure_opts_mipi_dphy	config;
> > +
> > +	int					submode;
> >  };
> >  
> >  static int sun6i_dphy_init(struct phy *phy)
> > @@ -105,6 +132,18 @@ static int sun6i_dphy_init(struct phy *phy)
> >  	return 0;
> >  }
> >  
> > +static int sun6i_dphy_set_mode(struct phy *phy, enum phy_mode mode, int submode)
> > +{
> > +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > +
> > +	if (mode != PHY_MODE_MIPI_DPHY)
> > +		return -EINVAL;
> > +
> > +	dphy->submode = submode;
> 
> Shouldn't you check if the submode is valid here?

Yes that's a good point, thanks!

> > +
> > +	return 0;
> > +}
> > +
> >  static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
> >  {
> >  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > @@ -119,9 +158,8 @@ static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
> >  	return 0;
> >  }
> >  
> > -static int sun6i_dphy_power_on(struct phy *phy)
> > +static int sun6i_dphy_tx_power_on(struct sun6i_dphy *dphy)
> >  {
> > -	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> >  	u8 lanes_mask = GENMASK(dphy->config.lanes - 1, 0);
> >  
> >  	regmap_write(dphy->regs, SUN6I_DPHY_TX_CTL_REG,
> > @@ -211,12 +249,129 @@ static int sun6i_dphy_power_on(struct phy *phy)
> >  	return 0;
> >  }
> >  
> > +static int sun6i_dphy_rx_power_on(struct sun6i_dphy *dphy)
> > +{
> > +	/* Physical clock rate is actually half of symbol rate with DDR. */
> > +	unsigned long mipi_symbol_rate = dphy->config.hs_clk_rate;
> > +	unsigned long dphy_clk_rate;
> > +	unsigned int rx_dly;
> > +	unsigned int lprst_dly;
> > +	u32 value;
> > +
> > +	dphy_clk_rate = clk_get_rate(dphy->mod_clk);
> > +	if (!dphy_clk_rate)
> > +		return -1;
> > +
> > +	/* Hardcoded timing parameters from the Allwinner BSP. */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME0_REG,
> > +		     SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(255) |
> > +		     SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(255) |
> > +		     SUN6I_DPHY_RX_TIME0_LP_RX(255));
> > +
> > +	/*
> > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > +	 * (probably internal divider/multiplier).
> > +	 */
> > +	rx_dly = 8 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 8));
> > +
> > +	/*
> > +	 * The Allwinner BSP has an alternative formula for LP_RX_ULPS_WP:
> > +	 * lp_ulps_wp_cnt = lp_ulps_wp_ms * lp_clk / 1000
> > +	 * but does not use it and hardcodes 255 instead.
> > +	 */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME1_REG,
> > +		     SUN6I_DPHY_RX_TIME1_RX_DLY(rx_dly) |
> > +		     SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(255));
> > +
> > +	/* HS_RX_ANA0 value is hardcoded in the Allwinner BSP. */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME2_REG,
> > +		     SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(4));
> > +
> > +	/*
> > +	 * Formula from the Allwinner BSP, with hardcoded coefficients
> > +	 * (probably internal divider/multiplier).
> > +	 */
> > +	lprst_dly = 4 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 2));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME3_REG,
> > +		     SUN6I_DPHY_RX_TIME3_LPRST_DLY(lprst_dly));
> > +
> > +	/* Analog parameters are hardcoded in the Allwinner BSP. */
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
> > +		     SUN6I_DPHY_ANA0_REG_PWS |
> > +		     SUN6I_DPHY_ANA0_REG_SLV(7) |
> > +		     SUN6I_DPHY_ANA0_REG_SFB(2));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG,
> > +		     SUN6I_DPHY_ANA1_REG_SVTT(4));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG,
> > +		     SUN6I_DPHY_ANA4_REG_DMPLVC |
> > +		     SUN6I_DPHY_ANA4_REG_DMPLVD(1));
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG,
> > +		     SUN6I_DPHY_ANA2_REG_ENIB);
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG,
> > +		     SUN6I_DPHY_ANA3_EN_LDOR |
> > +		     SUN6I_DPHY_ANA3_EN_LDOC |
> > +		     SUN6I_DPHY_ANA3_EN_LDOD);
> > +
> > +	/*
> > +	 * Delay comes from the Allwinner BSP, likely for internal regulator
> > +	 * ramp-up.
> > +	 */
> > +	udelay(3);
> > +
> > +	value = SUN6I_DPHY_RX_CTL_EN_DBC | SUN6I_DPHY_RX_CTL_RX_CLK_FORCE;
> > +
> > +	/*
> > +	 * Rx data lane force-enable bits are used as regular RX enable by the
> > +	 * Allwinner BSP.
> > +	 */
> > +	if (dphy->config.lanes >= 1)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D0_FORCE;
> > +	if (dphy->config.lanes >= 2)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D1_FORCE;
> > +	if (dphy->config.lanes >= 3)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D2_FORCE;
> > +	if (dphy->config.lanes == 4)
> > +		value |= SUN6I_DPHY_RX_CTL_RX_D3_FORCE;
> 
> I would replace this by a switch case with fallthrough to avoid too many comparisons
> to the same value.

Okay, why not!

Cheers,

Paul

> Regards,
> Helen
> 
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_RX_CTL_REG, value);
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
> > +		     SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
> > +		     SUN6I_DPHY_GCTL_EN);
> > +
> > +	return 0;
> > +}
> > +
> > +static int sun6i_dphy_power_on(struct phy *phy)
> > +{
> > +	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> > +
> > +	switch (dphy->submode) {
> > +	case PHY_MIPI_DPHY_SUBMODE_TX:
> > +		return sun6i_dphy_tx_power_on(dphy);
> > +	case PHY_MIPI_DPHY_SUBMODE_RX:
> > +		return sun6i_dphy_rx_power_on(dphy);
> > +	default:
> > +		return -EINVAL;
> > +	}
> > +}
> > +
> >  static int sun6i_dphy_power_off(struct phy *phy)
> >  {
> >  	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
> >  
> > -	regmap_update_bits(dphy->regs, SUN6I_DPHY_ANA1_REG,
> > -			   SUN6I_DPHY_ANA1_REG_VTTMODE, 0);
> > +	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG, 0);
> > +
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG, 0);
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG, 0);
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG, 0);
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG, 0);
> > +	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG, 0);
> >  
> >  	return 0;
> >  }
> > @@ -234,6 +389,7 @@ static int sun6i_dphy_exit(struct phy *phy)
> >  
> >  
> >  static const struct phy_ops sun6i_dphy_ops = {
> > +	.set_mode	= sun6i_dphy_set_mode,
> >  	.configure	= sun6i_dphy_configure,
> >  	.power_on	= sun6i_dphy_power_on,
> >  	.power_off	= sun6i_dphy_power_off,
> >
diff mbox series

Patch

diff --git a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
index 1fa761ba6cbb..8bcd4bb79f60 100644
--- a/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
+++ b/drivers/phy/allwinner/phy-sun6i-mipi-dphy.c
@@ -24,6 +24,14 @@ 
 #define SUN6I_DPHY_TX_CTL_REG		0x04
 #define SUN6I_DPHY_TX_CTL_HS_TX_CLK_CONT	BIT(28)
 
+#define SUN6I_DPHY_RX_CTL_REG		0x08
+#define SUN6I_DPHY_RX_CTL_EN_DBC	BIT(31)
+#define SUN6I_DPHY_RX_CTL_RX_CLK_FORCE	BIT(24)
+#define SUN6I_DPHY_RX_CTL_RX_D3_FORCE	BIT(23)
+#define SUN6I_DPHY_RX_CTL_RX_D2_FORCE	BIT(22)
+#define SUN6I_DPHY_RX_CTL_RX_D1_FORCE	BIT(21)
+#define SUN6I_DPHY_RX_CTL_RX_D0_FORCE	BIT(20)
+
 #define SUN6I_DPHY_TX_TIME0_REG		0x10
 #define SUN6I_DPHY_TX_TIME0_HS_TRAIL(n)		(((n) & 0xff) << 24)
 #define SUN6I_DPHY_TX_TIME0_HS_PREPARE(n)	(((n) & 0xff) << 16)
@@ -44,12 +52,29 @@ 
 #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA1(n)	(((n) & 0xff) << 8)
 #define SUN6I_DPHY_TX_TIME4_HS_TX_ANA0(n)	((n) & 0xff)
 
+#define SUN6I_DPHY_RX_TIME0_REG		0x30
+#define SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(n)	(((n) & 0xff) << 24)
+#define SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(n)	(((n) & 0xff) << 16)
+#define SUN6I_DPHY_RX_TIME0_LP_RX(n)		(((n) & 0xff) << 8)
+
+#define SUN6I_DPHY_RX_TIME1_REG		0x34
+#define SUN6I_DPHY_RX_TIME1_RX_DLY(n)		(((n) & 0xfff) << 20)
+#define SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(n)	((n) & 0xfffff)
+
+#define SUN6I_DPHY_RX_TIME2_REG		0x38
+#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA1(n)	(((n) & 0xff) << 8)
+#define SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(n)	((n) & 0xff)
+
+#define SUN6I_DPHY_RX_TIME3_REG		0x40
+#define SUN6I_DPHY_RX_TIME3_LPRST_DLY(n)	(((n) & 0xffff) << 16)
+
 #define SUN6I_DPHY_ANA0_REG		0x4c
 #define SUN6I_DPHY_ANA0_REG_PWS			BIT(31)
 #define SUN6I_DPHY_ANA0_REG_DMPC		BIT(28)
 #define SUN6I_DPHY_ANA0_REG_DMPD(n)		(((n) & 0xf) << 24)
 #define SUN6I_DPHY_ANA0_REG_SLV(n)		(((n) & 7) << 12)
 #define SUN6I_DPHY_ANA0_REG_DEN(n)		(((n) & 0xf) << 8)
+#define SUN6I_DPHY_ANA0_REG_SFB(n)		(((n) & 3) << 2)
 
 #define SUN6I_DPHY_ANA1_REG		0x50
 #define SUN6I_DPHY_ANA1_REG_VTTMODE		BIT(31)
@@ -92,6 +117,8 @@  struct sun6i_dphy {
 
 	struct phy				*phy;
 	struct phy_configure_opts_mipi_dphy	config;
+
+	int					submode;
 };
 
 static int sun6i_dphy_init(struct phy *phy)
@@ -105,6 +132,18 @@  static int sun6i_dphy_init(struct phy *phy)
 	return 0;
 }
 
+static int sun6i_dphy_set_mode(struct phy *phy, enum phy_mode mode, int submode)
+{
+	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
+
+	if (mode != PHY_MODE_MIPI_DPHY)
+		return -EINVAL;
+
+	dphy->submode = submode;
+
+	return 0;
+}
+
 static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
 {
 	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
@@ -119,9 +158,8 @@  static int sun6i_dphy_configure(struct phy *phy, union phy_configure_opts *opts)
 	return 0;
 }
 
-static int sun6i_dphy_power_on(struct phy *phy)
+static int sun6i_dphy_tx_power_on(struct sun6i_dphy *dphy)
 {
-	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
 	u8 lanes_mask = GENMASK(dphy->config.lanes - 1, 0);
 
 	regmap_write(dphy->regs, SUN6I_DPHY_TX_CTL_REG,
@@ -211,12 +249,129 @@  static int sun6i_dphy_power_on(struct phy *phy)
 	return 0;
 }
 
+static int sun6i_dphy_rx_power_on(struct sun6i_dphy *dphy)
+{
+	/* Physical clock rate is actually half of symbol rate with DDR. */
+	unsigned long mipi_symbol_rate = dphy->config.hs_clk_rate;
+	unsigned long dphy_clk_rate;
+	unsigned int rx_dly;
+	unsigned int lprst_dly;
+	u32 value;
+
+	dphy_clk_rate = clk_get_rate(dphy->mod_clk);
+	if (!dphy_clk_rate)
+		return -1;
+
+	/* Hardcoded timing parameters from the Allwinner BSP. */
+	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME0_REG,
+		     SUN6I_DPHY_RX_TIME0_HS_RX_SYNC(255) |
+		     SUN6I_DPHY_RX_TIME0_HS_RX_CLK_MISS(255) |
+		     SUN6I_DPHY_RX_TIME0_LP_RX(255));
+
+	/*
+	 * Formula from the Allwinner BSP, with hardcoded coefficients
+	 * (probably internal divider/multiplier).
+	 */
+	rx_dly = 8 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 8));
+
+	/*
+	 * The Allwinner BSP has an alternative formula for LP_RX_ULPS_WP:
+	 * lp_ulps_wp_cnt = lp_ulps_wp_ms * lp_clk / 1000
+	 * but does not use it and hardcodes 255 instead.
+	 */
+	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME1_REG,
+		     SUN6I_DPHY_RX_TIME1_RX_DLY(rx_dly) |
+		     SUN6I_DPHY_RX_TIME1_LP_RX_ULPS_WP(255));
+
+	/* HS_RX_ANA0 value is hardcoded in the Allwinner BSP. */
+	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME2_REG,
+		     SUN6I_DPHY_RX_TIME2_HS_RX_ANA0(4));
+
+	/*
+	 * Formula from the Allwinner BSP, with hardcoded coefficients
+	 * (probably internal divider/multiplier).
+	 */
+	lprst_dly = 4 * (unsigned int)(dphy_clk_rate / (mipi_symbol_rate / 2));
+
+	regmap_write(dphy->regs, SUN6I_DPHY_RX_TIME3_REG,
+		     SUN6I_DPHY_RX_TIME3_LPRST_DLY(lprst_dly));
+
+	/* Analog parameters are hardcoded in the Allwinner BSP. */
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG,
+		     SUN6I_DPHY_ANA0_REG_PWS |
+		     SUN6I_DPHY_ANA0_REG_SLV(7) |
+		     SUN6I_DPHY_ANA0_REG_SFB(2));
+
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG,
+		     SUN6I_DPHY_ANA1_REG_SVTT(4));
+
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG,
+		     SUN6I_DPHY_ANA4_REG_DMPLVC |
+		     SUN6I_DPHY_ANA4_REG_DMPLVD(1));
+
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG,
+		     SUN6I_DPHY_ANA2_REG_ENIB);
+
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG,
+		     SUN6I_DPHY_ANA3_EN_LDOR |
+		     SUN6I_DPHY_ANA3_EN_LDOC |
+		     SUN6I_DPHY_ANA3_EN_LDOD);
+
+	/*
+	 * Delay comes from the Allwinner BSP, likely for internal regulator
+	 * ramp-up.
+	 */
+	udelay(3);
+
+	value = SUN6I_DPHY_RX_CTL_EN_DBC | SUN6I_DPHY_RX_CTL_RX_CLK_FORCE;
+
+	/*
+	 * Rx data lane force-enable bits are used as regular RX enable by the
+	 * Allwinner BSP.
+	 */
+	if (dphy->config.lanes >= 1)
+		value |= SUN6I_DPHY_RX_CTL_RX_D0_FORCE;
+	if (dphy->config.lanes >= 2)
+		value |= SUN6I_DPHY_RX_CTL_RX_D1_FORCE;
+	if (dphy->config.lanes >= 3)
+		value |= SUN6I_DPHY_RX_CTL_RX_D2_FORCE;
+	if (dphy->config.lanes == 4)
+		value |= SUN6I_DPHY_RX_CTL_RX_D3_FORCE;
+
+	regmap_write(dphy->regs, SUN6I_DPHY_RX_CTL_REG, value);
+
+	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG,
+		     SUN6I_DPHY_GCTL_LANE_NUM(dphy->config.lanes) |
+		     SUN6I_DPHY_GCTL_EN);
+
+	return 0;
+}
+
+static int sun6i_dphy_power_on(struct phy *phy)
+{
+	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
+
+	switch (dphy->submode) {
+	case PHY_MIPI_DPHY_SUBMODE_TX:
+		return sun6i_dphy_tx_power_on(dphy);
+	case PHY_MIPI_DPHY_SUBMODE_RX:
+		return sun6i_dphy_rx_power_on(dphy);
+	default:
+		return -EINVAL;
+	}
+}
+
 static int sun6i_dphy_power_off(struct phy *phy)
 {
 	struct sun6i_dphy *dphy = phy_get_drvdata(phy);
 
-	regmap_update_bits(dphy->regs, SUN6I_DPHY_ANA1_REG,
-			   SUN6I_DPHY_ANA1_REG_VTTMODE, 0);
+	regmap_write(dphy->regs, SUN6I_DPHY_GCTL_REG, 0);
+
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA0_REG, 0);
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA1_REG, 0);
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA2_REG, 0);
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA3_REG, 0);
+	regmap_write(dphy->regs, SUN6I_DPHY_ANA4_REG, 0);
 
 	return 0;
 }
@@ -234,6 +389,7 @@  static int sun6i_dphy_exit(struct phy *phy)
 
 
 static const struct phy_ops sun6i_dphy_ops = {
+	.set_mode	= sun6i_dphy_set_mode,
 	.configure	= sun6i_dphy_configure,
 	.power_on	= sun6i_dphy_power_on,
 	.power_off	= sun6i_dphy_power_off,