From patchwork Sun Jul 2 07:27:10 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jonathan Liu X-Patchwork-Id: 9821057 Return-Path: Received: from mail.wl.linuxfoundation.org (pdx-wl-mail.web.codeaurora.org [172.30.200.125]) by pdx-korg-patchwork.web.codeaurora.org (Postfix) with ESMTP id AAAB460246 for ; Sun, 2 Jul 2017 07:27:29 +0000 (UTC) Received: from mail.wl.linuxfoundation.org (localhost [127.0.0.1]) by mail.wl.linuxfoundation.org (Postfix) with ESMTP id 90D652821F for ; Sun, 2 Jul 2017 07:27:29 +0000 (UTC) Received: by mail.wl.linuxfoundation.org (Postfix, from userid 486) id 8173928334; Sun, 2 Jul 2017 07:27:29 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.3.1 (2010-03-16) on pdx-wl-mail.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-1.9 required=2.0 tests=BAYES_00, DKIM_ADSP_CUSTOM_MED, DKIM_SIGNED, DKIM_VALID, FREEMAIL_FROM autolearn=unavailable version=3.3.1 Received: from bombadil.infradead.org (bombadil.infradead.org [65.50.211.133]) (using TLSv1.2 with cipher AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.wl.linuxfoundation.org (Postfix) with ESMTPS id 8B8302821F for ; Sun, 2 Jul 2017 07:27:28 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=lists.infradead.org; s=bombadil.20170209; h=Sender: Content-Transfer-Encoding:Content-Type:MIME-Version:Cc:List-Subscribe: List-Help:List-Post:List-Archive:List-Unsubscribe:List-Id:Message-Id:Date: Subject:To:From:Reply-To:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To: References:List-Owner; bh=6rTAMYBYStj5RirJ5C1lqFbtuXP/MIUwzaXBUl88ErA=; b=BFP sWJsTlOJd+kFfvMsKajtax4mtd8gnGmHqw+qh+t11zK3LenCMw02+idGy/5dbyPkH++3Q+ngazgRR ARdGYp3+25le+47zNg2JFdpClk4RrSFqOFC2iepy94uxp6osQIg3fd+tnnybt3ePOwa6Rj7jf2ATo 0JtP+xIxVzLJib7vbaGj4LkxET9JpHdCP/nUnY4vJNeglpJZpxg7XLBO+oRLMrNYIdC4kRTei8Qhf UafJg3IpS/po0EZMM6juHuS2ueYMBUfdSHXqQExtugvtEK6nIheONtV2brgcXHHXlq0XUGUDVf/ax dZ2VDaCSPDBxDluUAXKFpr1a8AY3q8Q==; Received: from localhost ([127.0.0.1] helo=bombadil.infradead.org) by bombadil.infradead.org with esmtp (Exim 4.87 #1 (Red Hat Linux)) id 1dRZHs-0003Cq-He; Sun, 02 Jul 2017 07:27:20 +0000 Received: from mail-pg0-x244.google.com ([2607:f8b0:400e:c05::244]) by bombadil.infradead.org with esmtps (Exim 4.87 #1 (Red Hat Linux)) id 1dRZHS-0003By-RT for linux-arm-kernel@lists.infradead.org; Sun, 02 Jul 2017 07:26:57 +0000 Received: by mail-pg0-x244.google.com with SMTP id d193so1487972pgc.2 for ; Sun, 02 Jul 2017 00:26:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=from:to:cc:subject:date:message-id; bh=h604vOX2H/r4/MckkVDio1J28a9jIkJLuWa/tdUygD8=; b=rocOUVZUugX9J+GrMzViKeJnsovCH3eLkOBa5qG/q1YWCkkiyzTEAFP5a+iGjpDhHr vfTpowJ37GiBjnCOSkTsQLgXdh379coXhQ52sDxxUD0Uhmj+01hPGcuOzIIg1L7CxFJD m1sS3x2gAGpeqBJXicFkyEd1g1bmvGWeMYoyUP5tTWPkJRxpoEUQQ+GlS5V0J7mVnneO ul/kdFdDS7W9r6+z1kxkG/p/vcrx588apMOhjbGRgzF8M1QkrWXC7S+ME8xsQx38yous dSaEw2e5Oth3R3KABYSvTqbJSXJirsA9/lK+swicoYezYPSCECe7x9HBySogq+s/7noY 5cGA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:to:cc:subject:date:message-id; bh=h604vOX2H/r4/MckkVDio1J28a9jIkJLuWa/tdUygD8=; b=uTo3npzNXxsyDr7+Q5b5RVK3RuI2muNrXfaFUEfhrf3B7mzFW8RzVaepWQN5e+BPv2 zFPjqcgs3SVt8mutDJfuS9DZftF4W17Zqqvz9s7h/QOVtBDaq89UJGjm7Ti7a8+94Xxg F/ogU5qLtEiurM7u8Zb+rsgGgneVcr80vKIekos6lHP0qTuSa6OmJb9lsUgo5nti+xEa 9fMeowicGkHOWQKshu+oPi4M4NldJBorLywM9vBUmws0DfAcfk1B+cLuahz4mjRwwLhO GFi1NVwPRe6uNQWzP+r16xLYOd8xI73VCdlrpR5wzt8R7vn7XupVlutZTdcJDiu8sev9 o5Fw== X-Gm-Message-State: AIVw110+OGVVg5eQmKfc3FQ5knIdW51ctax4yMGEsxXejf062r5oqHhy 9XaX9MMZqGzxmw== X-Received: by 10.84.128.9 with SMTP id 9mr4075119pla.61.1498980392160; Sun, 02 Jul 2017 00:26:32 -0700 (PDT) Received: from 60-242-179-244.static.tpgi.com.au (60-242-179-244.static.tpgi.com.au. [60.242.179.244]) by smtp.gmail.com with ESMTPSA id u17sm30917617pfa.133.2017.07.02.00.26.30 (version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256); Sun, 02 Jul 2017 00:26:31 -0700 (PDT) From: Jonathan Liu To: Maxime Ripard , David Airlie , Chen-Yu Tsai Subject: [PATCH v8] drm/sun4i: hdmi: Implement I2C adapter for A10s DDC bus Date: Sun, 2 Jul 2017 17:27:10 +1000 Message-Id: <20170702072710.29988-1-net147@gmail.com> X-Mailer: git-send-email 2.13.1 X-CRM114-Version: 20100106-BlameMichelson ( TRE 0.8.0 (BSD) ) MR-646709E3 X-CRM114-CacheID: sfid-20170702_002654_981729_46ABEA98 X-CRM114-Status: GOOD ( 29.79 ) X-BeenThere: linux-arm-kernel@lists.infradead.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: linux-arm-kernel@lists.infradead.org, linux-sunxi@googlegroups.com, Jonathan Liu , linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org MIME-Version: 1.0 Sender: "linux-arm-kernel" Errors-To: linux-arm-kernel-bounces+patchwork-linux-arm=patchwork.kernel.org@lists.infradead.org X-Virus-Scanned: ClamAV using ClamSMTP The documentation for drm_do_get_edid in drivers/gpu/drm/drm_edid.c states: "As in the general case the DDC bus is accessible by the kernel at the I2C level, drivers must make all reasonable efforts to expose it as an I2C adapter and use drm_get_edid() instead of abusing this function." Exposing the DDC bus as an I2C adapter is more beneficial as it can be used for purposes other than reading the EDID such as modifying the EDID or using the HDMI DDC pins as an I2C bus through the I2C dev interface from userspace (e.g. i2c-tools). Implement this for A10s. Signed-off-by: Jonathan Liu --- Changes for v8: - Fix clock rate in comment being 100 MHz when it is actually 100 kHz - Fix clearing of bits in interrupt status register as they are cleared by writing 1 to the bit rather than 0 - Set FIFO RX/TX thresholds so interrupt status register can be used to check if FIFO is ready instead of having to additionally check the FIFO status register - Remove unused linux/ktime.h include in sun4i_hdmi_i2c.c - Reduce timeout for clearing FIFO from 100 ms to 2 ms - Use BIT macro instead of GENMASK for SUN4I_HDMI_DDC_BYTE_COUNT_MAX to make it clearer that the maximum is derived from the field bit width rather than its position in the register Changes for v7: - Fix mixed declarations and code compiler warning for level variable Changes for v6: - Use fixed byte time of 100 us instead of dynamically calculating from DDC clock that is set to a fixed 100 MHz rate anyway - Change is_fifo_flag_unset to not read the status register as well to be more consistent with is_err_status Changes for v5: - Use devm_kzalloc instead of devm_kmemdup and remove const struct i2c_adapter - Rework to use readl_poll_timeout for checking FIFO status Changes for v4: - Carry over copyright from initial I2C code into sun4i_hdmi_i2c.c - Clean up indentation in sun4i_hdmi.h - Rename SUN4I_HDMI_DDC_MAX_TRANSFER_SIZE to SUN4I_HDMI_DDC_BYTE_COUNT_MAX and group it under the SUN4I_HDMI_DDC_BYTE_COUNT_REG define, changing the value to use the GENMASK macro to make it clear that it is derived from the width of the field in the register - Fix SUN4I_HDMI_DDC_INT_STATUS_DDC_TX_FIFO_UNDERFLOW typo which should be SUN4I_HDMI_DDC_INT_STATUS_DDC_TX_FIFO_OVERFLOW - Remove redundant rewriting of SUN4I_HDMI_DDC_INT_STATUS_REG register - Change struct i2c_adapter to be const by using devm_kmemdup on creation - Return -ETIMEDOUT instead of -EIO if there is timeout while transferring an I2C message - Instead of waiting for 1-2 bytes to transfer, wait for the time it would take for remaining bytes to transfer (limited by FIFO size) - Add additional comments Changes for v3: - Explain why drm_do_get_edid should be used and why it's better to expose it as an I2C adapter in commit message - Reorder bit defines in descending order for consistency - Keep old unused macros instead of removing them - The v2 algorithm split large transfers into 16 byte transfers but this may cause a large single write to be treated as multiple writes causing data corruption. The algorithm has been reworked to not split larger transfers and make more use of the FIFO to avoid this. - Moved the creation of the DDC clock from sun4i_hdmi_enc.c to sun4i_hdmi_i2c.c - Reformatted code - Instead of masking bits that we don't want to check for errors, explicitly check the error bits - Clear error bits at start of transfer in case of error from previous transfer - Poll for completion of FIFO clear after setting FIFO clear bit 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 | 24 ++++ drivers/gpu/drm/sun4i/sun4i_hdmi_enc.c | 101 ++------------- drivers/gpu/drm/sun4i/sun4i_hdmi_i2c.c | 220 +++++++++++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 90 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 diff --git a/drivers/gpu/drm/sun4i/sun4i_hdmi.h b/drivers/gpu/drm/sun4i/sun4i_hdmi.h index 2f2f2ff1ea63..0957ff2076ac 100644 --- a/drivers/gpu/drm/sun4i/sun4i_hdmi.h +++ b/drivers/gpu/drm/sun4i/sun4i_hdmi.h @@ -96,6 +96,7 @@ #define SUN4I_HDMI_DDC_CTRL_ENABLE BIT(31) #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_WRITE (1 << 8) #define SUN4I_HDMI_DDC_CTRL_FIFO_DIR_READ (0 << 8) #define SUN4I_HDMI_DDC_CTRL_RESET BIT(0) @@ -105,14 +106,34 @@ #define SUN4I_HDMI_DDC_ADDR_OFFSET(off) (((off) & 0xff) << 8) #define SUN4I_HDMI_DDC_ADDR_SLAVE(addr) ((addr) & 0xff) +#define SUN4I_HDMI_DDC_INT_STATUS_REG 0x50c +#define SUN4I_HDMI_DDC_INT_STATUS_ILLEGAL_FIFO_OPERATION BIT(7) +#define SUN4I_HDMI_DDC_INT_STATUS_DDC_RX_FIFO_UNDERFLOW BIT(6) +#define SUN4I_HDMI_DDC_INT_STATUS_DDC_TX_FIFO_OVERFLOW BIT(5) +#define SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST BIT(4) +#define SUN4I_HDMI_DDC_INT_STATUS_ARBITRATION_ERROR BIT(3) +#define SUN4I_HDMI_DDC_INT_STATUS_ACK_ERROR BIT(2) +#define SUN4I_HDMI_DDC_INT_STATUS_BUS_ERROR BIT(1) +#define SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE BIT(0) + #define SUN4I_HDMI_DDC_FIFO_CTRL_REG 0x510 #define SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR BIT(31) +#define SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES(n) (((n) & 0xf) << 4) +#define SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES_MASK GENMASK(7, 4) +#define SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES_MAX (BIT(4) - 1) +#define SUN4I_HDMI_DDC_FIFO_CTRL_TX_THRES(n) ((n) & 0xf) +#define SUN4I_HDMI_DDC_FIFO_CTRL_TX_THRES_MASK GENMASK(3, 0) +#define SUN4I_HDMI_DDC_FIFO_CTRL_TX_THRES_MAX (BIT(4) - 1) #define SUN4I_HDMI_DDC_FIFO_DATA_REG 0x518 + #define SUN4I_HDMI_DDC_BYTE_COUNT_REG 0x51c +#define SUN4I_HDMI_DDC_BYTE_COUNT_MAX (BIT(10) - 1) #define SUN4I_HDMI_DDC_CMD_REG 0x520 #define SUN4I_HDMI_DDC_CMD_EXPLICIT_EDDC_READ 6 +#define SUN4I_HDMI_DDC_CMD_IMPLICIT_READ 5 +#define SUN4I_HDMI_DDC_CMD_IMPLICIT_WRITE 3 #define SUN4I_HDMI_DDC_CLK_REG 0x528 #define SUN4I_HDMI_DDC_CLK_M(m) (((m) & 0x7) << 3) @@ -146,6 +167,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 +176,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 device *dev, 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..b74607feb35c 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; } @@ -407,9 +323,9 @@ static int sun4i_hdmi_bind(struct device *dev, struct device *master, SUN4I_HDMI_PLL_CTRL_PLL_EN; writel(reg, hdmi->base + SUN4I_HDMI_PLL_CTRL_REG); - ret = sun4i_ddc_create(hdmi, hdmi->tmds_clk); + ret = sun4i_hdmi_i2c_create(dev, hdmi); if (ret) { - dev_err(dev, "Couldn't create the DDC clock\n"); + dev_err(dev, "Couldn't create the HDMI I2C adapter\n"); return ret; } @@ -422,13 +338,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 +369,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 +381,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..2e42d09ab42e --- /dev/null +++ b/drivers/gpu/drm/sun4i/sun4i_hdmi_i2c.c @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2016 Maxime Ripard + * Copyright (C) 2017 Jonathan Liu + * + * 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 +#include +#include + +#include "sun4i_hdmi.h" + +#define SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK ( \ + SUN4I_HDMI_DDC_INT_STATUS_ILLEGAL_FIFO_OPERATION | \ + SUN4I_HDMI_DDC_INT_STATUS_DDC_RX_FIFO_UNDERFLOW | \ + SUN4I_HDMI_DDC_INT_STATUS_DDC_TX_FIFO_OVERFLOW | \ + SUN4I_HDMI_DDC_INT_STATUS_ARBITRATION_ERROR | \ + SUN4I_HDMI_DDC_INT_STATUS_ACK_ERROR | \ + SUN4I_HDMI_DDC_INT_STATUS_BUS_ERROR \ +) + +/* FIFO request bit is set when FIFO level is above RX_THRESHOLD during read */ +#define RX_THRESHOLD SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES_MAX +/* FIFO request bit is set when FIFO level is below TX_THRESHOLD during write */ +#define TX_THRESHOLD 1 + +static int fifo_transfer(struct sun4i_hdmi *hdmi, u8 *buf, int len, bool read) +{ + /* + * 1 byte takes 9 clock cycles (8 bits + 1 ACK) = 90 us for 100 kHz + * clock. As clock rate is fixed, just round it up to 100 us. + */ + const unsigned long byte_time_ns = 100; + const u32 mask = SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK | + SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST | + SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE; + u32 reg; + + /* Limit transfer length by FIFO threshold */ + len = min_t(int, len, read ? (RX_THRESHOLD + 1) : + (SUN4I_HDMI_DDC_FIFO_SIZE - TX_THRESHOLD + 1)); + + /* Wait until error, FIFO request bit set or transfer complete */ + if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG, reg, + reg & mask, len * byte_time_ns, 100000)) + return -ETIMEDOUT; + + if (reg & SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK) + return -EIO; + + if (read) + readsb(hdmi->base + SUN4I_HDMI_DDC_FIFO_DATA_REG, buf, len); + else + writesb(hdmi->base + SUN4I_HDMI_DDC_FIFO_DATA_REG, buf, len); + + /* Clear FIFO request bit */ + writel(SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST, + hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG); + + return len; +} + +static int xfer_msg(struct sun4i_hdmi *hdmi, struct i2c_msg *msg) +{ + int i, len; + u32 reg; + + /* Set FIFO direction */ + reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); + reg &= ~SUN4I_HDMI_DDC_CTRL_FIFO_DIR_MASK; + reg |= (msg->flags & I2C_M_RD) ? + SUN4I_HDMI_DDC_CTRL_FIFO_DIR_READ : + SUN4I_HDMI_DDC_CTRL_FIFO_DIR_WRITE; + writel(reg, hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); + + /* Set I2C address */ + writel(SUN4I_HDMI_DDC_ADDR_SLAVE(msg->addr), + hdmi->base + SUN4I_HDMI_DDC_ADDR_REG); + + /* Set FIFO RX/TX thresholds and clear FIFO */ + reg = readl(hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG); + reg |= SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR; + reg &= ~SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES_MASK; + reg |= SUN4I_HDMI_DDC_FIFO_CTRL_RX_THRES(RX_THRESHOLD); + reg &= ~SUN4I_HDMI_DDC_FIFO_CTRL_TX_THRES_MASK; + reg |= SUN4I_HDMI_DDC_FIFO_CTRL_TX_THRES(TX_THRESHOLD); + writel(reg, hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG); + if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_FIFO_CTRL_REG, + reg, + !(reg & SUN4I_HDMI_DDC_FIFO_CTRL_CLEAR), + 100, 2000)) + return -EIO; + + /* Set transfer length */ + writel(msg->len, hdmi->base + SUN4I_HDMI_DDC_BYTE_COUNT_REG); + + /* Set command */ + writel(msg->flags & I2C_M_RD ? + SUN4I_HDMI_DDC_CMD_IMPLICIT_READ : + SUN4I_HDMI_DDC_CMD_IMPLICIT_WRITE, + hdmi->base + SUN4I_HDMI_DDC_CMD_REG); + + /* Clear interrupt status bits */ + writel(SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK | + SUN4I_HDMI_DDC_INT_STATUS_FIFO_REQUEST | + SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE, + hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG); + + /* Start command */ + reg = readl(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); + writel(reg | SUN4I_HDMI_DDC_CTRL_START_CMD, + hdmi->base + SUN4I_HDMI_DDC_CTRL_REG); + + /* Transfer bytes */ + for (i = 0; i < msg->len; i += len) { + len = fifo_transfer(hdmi, msg->buf + i, msg->len - i, + msg->flags & I2C_M_RD); + if (len <= 0) + return len; + } + + /* Wait for command to finish */ + if (readl_poll_timeout(hdmi->base + SUN4I_HDMI_DDC_CTRL_REG, + reg, + !(reg & SUN4I_HDMI_DDC_CTRL_START_CMD), + 100, 100000)) + return -EIO; + + /* Check for errors */ + reg = readl(hdmi->base + SUN4I_HDMI_DDC_INT_STATUS_REG); + if ((reg & SUN4I_HDMI_DDC_INT_STATUS_ERROR_MASK) || + !(reg & SUN4I_HDMI_DDC_INT_STATUS_TRANSFER_COMPLETE)) { + return -EIO; + } + + 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, i, ret = num; + + for (i = 0; i < num; i++) { + if (!msgs[i].len) + return -EINVAL; + if (msgs[i].len > SUN4I_HDMI_DDC_BYTE_COUNT_MAX) + return -EINVAL; + } + + /* 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, +}; + +int sun4i_hdmi_i2c_create(struct device *dev, struct sun4i_hdmi *hdmi) +{ + struct i2c_adapter *adap; + int ret = 0; + + ret = sun4i_ddc_create(hdmi, hdmi->tmds_clk); + if (ret) + return ret; + + adap = devm_kzalloc(dev, sizeof(*adap), GFP_KERNEL); + if (!adap) + return -ENOMEM; + + adap->owner = THIS_MODULE; + adap->class = I2C_CLASS_DDC; + adap->algo = &sun4i_hdmi_i2c_algorithm; + strlcpy(adap->name, "sun4i_hdmi_i2c adapter", sizeof(adap->name)); + i2c_set_adapdata(adap, hdmi); + + ret = i2c_add_adapter(adap); + if (ret) + return ret; + + hdmi->i2c = adap; + + return ret; +}