[PATCH v2] drm/sun4i: hdmi: Implement I2C adapter for A10s DDC bus

Maxime Ripard maxime.ripard at free-electrons.com
Tue Jun 13 04:15:05 PDT 2017


On Mon, Jun 12, 2017 at 03:52:35PM +1000, Jonathan Liu wrote:
> The drm_get_edid function should be used instead of drm_do_get_edid by
> exposing the DDC bus as an I2C adapter. Implement this for A10s.
> 
> Signed-off-by: Jonathan Liu <net147 at gmail.com>

Your commit log should explain *why* that function should be used
instead, and why it's better to expose it as an i2c adadpter.

> ---
> Changes for v2:
>  - Rebased against Maxime's sunxi-drm/for-next branch
>  - Fix up error paths in sun4i_hdmi_bind so that the I2C adapter is deleted if
>    any of the calls after the I2C adapter is created fails
>  - Remove unnecessary includes in sun4i_hdmi_i2c.c
> 
>  drivers/gpu/drm/sun4i/Makefile         |   1 +
>  drivers/gpu/drm/sun4i/sun4i_hdmi.h     |  11 ++-
>  drivers/gpu/drm/sun4i/sun4i_hdmi_enc.c | 103 +++------------------
>  drivers/gpu/drm/sun4i/sun4i_hdmi_i2c.c | 163 +++++++++++++++++++++++++++++++++
>  4 files changed, 189 insertions(+), 89 deletions(-)
>  create mode 100644 drivers/gpu/drm/sun4i/sun4i_hdmi_i2c.c
> 
> diff --git a/drivers/gpu/drm/sun4i/Makefile b/drivers/gpu/drm/sun4i/Makefile
> index e29fd3a2ba9c..43c753cafc88 100644
> --- a/drivers/gpu/drm/sun4i/Makefile
> +++ b/drivers/gpu/drm/sun4i/Makefile
> @@ -2,6 +2,7 @@ sun4i-drm-y += sun4i_drv.o
>  sun4i-drm-y += sun4i_framebuffer.o
>  
>  sun4i-drm-hdmi-y += sun4i_hdmi_enc.o
> +sun4i-drm-hdmi-y += sun4i_hdmi_i2c.o
>  sun4i-drm-hdmi-y += sun4i_hdmi_ddc_clk.o
>  sun4i-drm-hdmi-y += sun4i_hdmi_tmds_clk.o

You probably also need to depend on the I2C framework in order to
work, right?

>  
> diff --git a/drivers/gpu/drm/sun4i/sun4i_hdmi.h b/drivers/gpu/drm/sun4i/sun4i_hdmi.h
> index 2f2f2ff1ea63..4c01dbe89cd9 100644
> --- a/drivers/gpu/drm/sun4i/sun4i_hdmi.h
> +++ b/drivers/gpu/drm/sun4i/sun4i_hdmi.h
> @@ -97,6 +97,7 @@
>  #define SUN4I_HDMI_DDC_CTRL_START_CMD		BIT(30)
>  #define SUN4I_HDMI_DDC_CTRL_FIFO_DIR_MASK	BIT(8)
>  #define SUN4I_HDMI_DDC_CTRL_FIFO_DIR_READ	(0 << 8)
> +#define SUN4I_HDMI_DDC_CTRL_FIFO_DIR_WRITE	(1 << 8)

All the other bit defines are ordered by descending orders, please
keep it consistent.

>  #define SUN4I_HDMI_DDC_CTRL_RESET		BIT(0)
>  
>  #define SUN4I_HDMI_DDC_ADDR_REG		0x504
> @@ -105,6 +106,10 @@
>  #define SUN4I_HDMI_DDC_ADDR_OFFSET(off)		(((off) & 0xff) << 8)
>  #define SUN4I_HDMI_DDC_ADDR_SLAVE(addr)		((addr) & 0xff)
>  
> +#define SUN4I_HDMI_DDC_INTERRUPT_STATUS_REG	0x50c

It is called INT_STATUS in the datasheet

> +#define SUN4I_HDMI_DDC_INTERRUPT_STATUS_FIFO_REQUEST	BIT(4)
> +#define SUN4I_HDMI_DDC_INTERRUPT_STATUS_TRANSFER_COMPLETE	BIT(0)
> +
>  #define SUN4I_HDMI_DDC_FIFO_CTRL_REG	0x510
>  #define SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR		BIT(31)
>  
> @@ -112,7 +117,8 @@
>  #define SUN4I_HDMI_DDC_BYTE_COUNT_REG	0x51c
>  
>  #define SUN4I_HDMI_DDC_CMD_REG		0x520
> -#define SUN4I_HDMI_DDC_CMD_EXPLICIT_EDDC_READ	6
> +#define SUN4I_HDMI_DDC_CMD_IMPLICIT_WRITE	3
> +#define SUN4I_HDMI_DDC_CMD_IMPLICIT_READ	5

Same remark here, and why do you remove the old one?

>  
>  #define SUN4I_HDMI_DDC_CLK_REG		0x528
>  #define SUN4I_HDMI_DDC_CLK_M(m)			(((m) & 0x7) << 3)
> @@ -146,6 +152,8 @@ struct sun4i_hdmi {
>  	struct clk		*ddc_clk;
>  	struct clk		*tmds_clk;
>  
> +	struct i2c_adapter	*i2c;
> +
>  	struct sun4i_drv	*drv;
>  
>  	bool			hdmi_monitor;
> @@ -153,5 +161,6 @@ struct sun4i_hdmi {
>  
>  int sun4i_ddc_create(struct sun4i_hdmi *hdmi, struct clk *clk);
>  int sun4i_tmds_create(struct sun4i_hdmi *hdmi);
> +int sun4i_hdmi_i2c_create(struct sun4i_hdmi *hdmi);
>  
>  #endif /* _SUN4I_HDMI_H_ */
> diff --git a/drivers/gpu/drm/sun4i/sun4i_hdmi_enc.c b/drivers/gpu/drm/sun4i/sun4i_hdmi_enc.c
> index d3398f6250ef..2a8c0b14eabc 100644
> --- a/drivers/gpu/drm/sun4i/sun4i_hdmi_enc.c
> +++ b/drivers/gpu/drm/sun4i/sun4i_hdmi_enc.c
> @@ -29,8 +29,6 @@
>  #include "sun4i_hdmi.h"
>  #include "sun4i_tcon.h"
>  
> -#define DDC_SEGMENT_ADDR	0x30
> -
>  static inline struct sun4i_hdmi *
>  drm_encoder_to_sun4i_hdmi(struct drm_encoder *encoder)
>  {
> @@ -184,93 +182,13 @@ static const struct drm_encoder_funcs sun4i_hdmi_funcs = {
>  	.destroy	= drm_encoder_cleanup,
>  };
>  
> -static int sun4i_hdmi_read_sub_block(struct sun4i_hdmi *hdmi,
> -				     unsigned int blk, unsigned int offset,
> -				     u8 *buf, unsigned int count)
> -{
> -	unsigned long reg;
> -	int i;
> -
> -	reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> -	reg &= ~SUN4I_HDMI_DDC_CTRL_FIFO_DIR_MASK;
> -	writel(reg | SUN4I_HDMI_DDC_CTRL_FIFO_DIR_READ,
> -	       hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> -
> -	writel(SUN4I_HDMI_DDC_ADDR_SEGMENT(offset >> 8) |
> -	       SUN4I_HDMI_DDC_ADDR_EDDC(DDC_SEGMENT_ADDR << 1) |
> -	       SUN4I_HDMI_DDC_ADDR_OFFSET(offset) |
> -	       SUN4I_HDMI_DDC_ADDR_SLAVE(DDC_ADDR),
> -	       hdmi->base + SUN4I_HDMI_DDC_ADDR_REG);
> -
> -	reg = readl(hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG);
> -	writel(reg | SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR,
> -	       hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG);
> -
> -	writel(count, hdmi->base + SUN4I_HDMI_DDC_BYTE_COUNT_REG);
> -	writel(SUN4I_HDMI_DDC_CMD_EXPLICIT_EDDC_READ,
> -	       hdmi->base + SUN4I_HDMI_DDC_CMD_REG);
> -
> -	reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> -	writel(reg | SUN4I_HDMI_DDC_CTRL_START_CMD,
> -	       hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> -
> -	if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG, reg,
> -			       !(reg & SUN4I_HDMI_DDC_CTRL_START_CMD),
> -			       100, 100000))
> -		return -EIO;
> -
> -	for (i = 0; i < count; i++)
> -		buf[i] = readb(hdmi->base + SUN4I_HDMI_DDC_FIFO_DATA_REG);
> -
> -	return 0;
> -}
> -
> -static int sun4i_hdmi_read_edid_block(void *data, u8 *buf, unsigned int blk,
> -				      size_t length)
> -{
> -	struct sun4i_hdmi *hdmi = data;
> -	int retry = 2, i;
> -
> -	do {
> -		for (i = 0; i < length; i += SUN4I_HDMI_DDC_FIFO_SIZE) {
> -			unsigned char offset = blk * EDID_LENGTH + i;
> -			unsigned int count = min((unsigned int)SUN4I_HDMI_DDC_FIFO_SIZE,
> -						 length - i);
> -			int ret;
> -
> -			ret = sun4i_hdmi_read_sub_block(hdmi, blk, offset,
> -							buf + i, count);
> -			if (ret)
> -				return ret;
> -		}
> -	} while (!drm_edid_block_valid(buf, blk, true, NULL) && (retry--));
> -
> -	return 0;
> -}
> -
>  static int sun4i_hdmi_get_modes(struct drm_connector *connector)
>  {
>  	struct sun4i_hdmi *hdmi = drm_connector_to_sun4i_hdmi(connector);
> -	unsigned long reg;
>  	struct edid *edid;
>  	int ret;
>  
> -	/* Reset i2c controller */
> -	writel(SUN4I_HDMI_DDC_CTRL_ENABLE | SUN4I_HDMI_DDC_CTRL_RESET,
> -	       hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> -	if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG, reg,
> -			       !(reg & SUN4I_HDMI_DDC_CTRL_RESET),
> -			       100, 2000))
> -		return -EIO;
> -
> -	writel(SUN4I_HDMI_DDC_LINE_CTRL_SDA_ENABLE |
> -	       SUN4I_HDMI_DDC_LINE_CTRL_SCL_ENABLE,
> -	       hdmi->base + SUN4I_HDMI_DDC_LINE_CTRL_REG);
> -
> -	clk_prepare_enable(hdmi->ddc_clk);
> -	clk_set_rate(hdmi->ddc_clk, 100000);
> -
> -	edid = drm_do_get_edid(connector, sun4i_hdmi_read_edid_block, hdmi);
> +	edid = drm_get_edid(connector, hdmi->i2c);
>  	if (!edid)
>  		return 0;
>  
> @@ -282,8 +200,6 @@ static int sun4i_hdmi_get_modes(struct drm_connector *connector)
>  	ret = drm_add_edid_modes(connector, edid);
>  	kfree(edid);
>  
> -	clk_disable_unprepare(hdmi->ddc_clk);
> -
>  	return ret;
>  }
>  
> @@ -413,6 +329,12 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
>  		return ret;
>  	}
>  
> +	ret = sun4i_hdmi_i2c_create(hdmi);
> +	if (ret) {
> +		dev_err(dev, "Couldn't create the HDMI I2C adapter\n");
> +		return ret;
> +	}
> +
>  	drm_encoder_helper_add(&hdmi->encoder,
>  			       &sun4i_hdmi_helper_funcs);
>  	ret = drm_encoder_init(drm,
> @@ -422,13 +344,15 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
>  			       NULL);
>  	if (ret) {
>  		dev_err(dev, "Couldn't initialise the HDMI encoder\n");
> -		return ret;
> +		goto err_del_i2c_adapter;
>  	}
>  
>  	hdmi->encoder.possible_crtcs = drm_of_find_possible_crtcs(drm,
>  								  dev->of_node);
> -	if (!hdmi->encoder.possible_crtcs)
> -		return -EPROBE_DEFER;
> +	if (!hdmi->encoder.possible_crtcs) {
> +		ret = -EPROBE_DEFER;
> +		goto err_del_i2c_adapter;
> +	}
>  
>  	drm_connector_helper_add(&hdmi->connector,
>  				 &sun4i_hdmi_connector_helper_funcs);
> @@ -451,6 +375,8 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master,
>  
>  err_cleanup_connector:
>  	drm_encoder_cleanup(&hdmi->encoder);
> +err_del_i2c_adapter:
> +	i2c_del_adapter(hdmi->i2c);
>  	return ret;
>  }
>  
> @@ -461,6 +387,7 @@ static void sun4i_hdmi_unbind(struct device *dev, struct device *master,
>  
>  	drm_connector_cleanup(&hdmi->connector);
>  	drm_encoder_cleanup(&hdmi->encoder);
> +	i2c_del_adapter(hdmi->i2c);
>  }
>  
>  static const struct component_ops sun4i_hdmi_ops = {
> diff --git a/drivers/gpu/drm/sun4i/sun4i_hdmi_i2c.c b/drivers/gpu/drm/sun4i/sun4i_hdmi_i2c.c
> new file mode 100644
> index 000000000000..389b770be09b
> --- /dev/null
> +++ b/drivers/gpu/drm/sun4i/sun4i_hdmi_i2c.c
> @@ -0,0 +1,163 @@
> +/*
> + * Copyright (C) 2017 Jonathan Liu
> + *
> + * Jonathan Liu <net147 at gmail.com>

Is it?

> + *
> + * 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.
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/i2c.h>
> +#include <linux/iopoll.h>
> +
> +#include "sun4i_hdmi.h"
> +
> +static int xfer_msg_chunk(struct sun4i_hdmi *hdmi, struct i2c_msg *msg)
> +{
> +	u32 count = min_t(u32, msg->len, SUN4I_HDMI_DDC_FIFO_SIZE);
> +	u32 reg;
> +	int i;
> +
> +	reg = readl(hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG);
> +	writel(reg | SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR,
> +	       hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG);
> +
> +	writel(count, hdmi->base
> +	       + SUN4I_HDMI_DDC_BYTE_COUNT_REG);

It's pretty much everywhere in that file, but the operator should be
at the end of the first line, not the beginning of the second.

(and you don't really need to wrap that one do you?)

> +	writel(msg->flags & I2C_M_RD

Parenthesis around the condition would make it easier to read.

> +	       ? SUN4I_HDMI_DDC_CMD_IMPLICIT_READ
> +	       : SUN4I_HDMI_DDC_CMD_IMPLICIT_WRITE,
> +	       hdmi->base + SUN4I_HDMI_DDC_CMD_REG);
> +
> +	reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> +	writel(reg | SUN4I_HDMI_DDC_CTRL_START_CMD,
> +	       hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> +
> +	if (!(msg->flags & I2C_M_RD)) {
> +		for (i = 0; i < count; i++) {
> +			writeb(*msg->buf++, hdmi->base
> +			       + SUN4I_HDMI_DDC_FIFO_DATA_REG);

writesb?

> +			--msg->len;
> +		}
> +	}
> +
> +	if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG,
> +			       reg,
> +			       !(reg & SUN4I_HDMI_DDC_CTRL_START_CMD),
> +			       100, 100000))
> +		return -EIO;
> +
> +	reg = readl(hdmi->base + SUN4I_HDMI_DDC_INTERRUPT_STATUS_REG);
> +	reg &= ~SUN4I_HDMI_DDC_INTERRUPT_STATUS_FIFO_REQUEST;

You don't need to use that mask.

> +	if (reg != SUN4I_HDMI_DDC_INTERRUPT_STATUS_TRANSFER_COMPLETE)
> +		return -EIO;

!(reg & SUN4I_HDMI_DDC_INTERRUPT_STATUS_TRANSFER_COMPLETE) would be enough.

> +
> +	if (msg->flags & I2C_M_RD) {
> +		for (i = 0; i < count; i++) {
> +			*msg->buf++ = readb(hdmi->base
> +					    + SUN4I_HDMI_DDC_FIFO_DATA_REG);

readsb ?

> +			--msg->len;

You shouldn't modify that length.

> +		}
> +	}
> +
> +	return 0;
> +}
> +
> +static int xfer_msg(struct sun4i_hdmi *hdmi, struct i2c_msg *msg)
> +{
> +	u32 reg;
> +	int ret;
> +
> +	if (!msg->len)
> +		return -EIO;
> +
> +	while (msg->len) {
> +		reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> +		reg &= ~SUN4I_HDMI_DDC_CTRL_FIFO_DIR_MASK;
> +
> +		writel(reg | (msg->flags & I2C_M_RD
> +			      ? SUN4I_HDMI_DDC_CTRL_FIFO_DIR_READ
> +			      : SUN4I_HDMI_DDC_CTRL_FIFO_DIR_WRITE),

You can put that in a separate assignment after the mask has been
applied, it will be easier to read.

> +		       hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> +
> +		writel(SUN4I_HDMI_DDC_ADDR_SLAVE(msg->addr),
> +		       hdmi->base + SUN4I_HDMI_DDC_ADDR_REG);

What about the segment, offset and eddc addresses?

> +
> +		ret = xfer_msg_chunk(hdmi, msg);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int sun4i_hdmi_i2c_xfer(struct i2c_adapter *adap,
> +			       struct i2c_msg *msgs, int num)
> +{
> +	struct sun4i_hdmi *hdmi = i2c_get_adapdata(adap);
> +	u32 reg;
> +	int err, ret = num;
> +	int i;
> +
> +	/* Reset i2c controller */
> +	writel(SUN4I_HDMI_DDC_CTRL_ENABLE | SUN4I_HDMI_DDC_CTRL_RESET,
> +	       hdmi->base + SUN4I_HDMI_DDC_CTRL_REG);
> +	if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG, reg,
> +			       !(reg & SUN4I_HDMI_DDC_CTRL_RESET),
> +			       100, 2000))
> +		return -EIO;
> +
> +	writel(SUN4I_HDMI_DDC_LINE_CTRL_SDA_ENABLE |
> +	       SUN4I_HDMI_DDC_LINE_CTRL_SCL_ENABLE,
> +	       hdmi->base + SUN4I_HDMI_DDC_LINE_CTRL_REG);
> +
> +	clk_prepare_enable(hdmi->ddc_clk);
> +	clk_set_rate(hdmi->ddc_clk, 100000);
> +
> +	for (i = 0; i < num; i++) {
> +		err = xfer_msg(hdmi, &msgs[i]);
> +		if (err) {
> +			ret = err;
> +			break;
> +		}
> +	}
> +
> +	clk_disable_unprepare(hdmi->ddc_clk);
> +	return ret;
> +}
> +
> +static u32 sun4i_hdmi_i2c_func(struct i2c_adapter *adap)
> +{
> +	return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
> +}
> +
> +static const struct i2c_algorithm sun4i_hdmi_i2c_algorithm = {
> +	.master_xfer	= sun4i_hdmi_i2c_xfer,
> +	.functionality	= sun4i_hdmi_i2c_func,
> +};
> +
> +static struct i2c_adapter sun4i_hdmi_i2c_adapter = {

const?

> +	.owner	= THIS_MODULE,
> +	.class	= I2C_CLASS_DDC,
> +	.algo	= &sun4i_hdmi_i2c_algorithm,
> +	.name	= "sun4i_hdmi_i2c adapter",
> +};
> +
> +int sun4i_hdmi_i2c_create(struct sun4i_hdmi *hdmi)
> +{
> +	int ret = 0;
> +
> +	i2c_set_adapdata(&sun4i_hdmi_i2c_adapter, hdmi);
> +
> +	ret = i2c_add_adapter(&sun4i_hdmi_i2c_adapter);
> +	if (ret)
> +		return ret;

It should probably create the DDC clock too.

I'd also like to have this patch split. First converting the current
code to a i2c (read-only) dirver, and then adding write support.

Thanks!
Maxime

-- 
Maxime Ripard, Free Electrons
Embedded Linux and Kernel engineering
http://free-electrons.com
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 801 bytes
Desc: not available
URL: <http://lists.infradead.org/pipermail/linux-arm-kernel/attachments/20170613/fdc8fb5f/attachment-0001.sig>


More information about the linux-arm-kernel mailing list