linux-kernel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Andreas Kemnade <andreas@kemnade.info>
To: p.zabel@pengutronix.de, airlied@linux.ie, daniel@ffwll.ch,
	robh+dt@kernel.org, shawnguo@kernel.org, s.hauer@pengutronix.de,
	kernel@pengutronix.de, festevam@gmail.com, linux-imx@nxp.com,
	maarten.lankhorst@linux.intel.com, mripard@kernel.org,
	tzimmermann@suse.de, andreas@kemnade.info,
	dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org,
	linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org, alistair@alistair23.me,
	samuel@sholland.org, josua.mayer@jm0.eu,
	letux-kernel@openphoenux.org
Subject: [RFC PATCH 3/6] drm: mxc-epdc: Add display and waveform initialisation
Date: Sun,  6 Feb 2022 09:00:13 +0100	[thread overview]
Message-ID: <20220206080016.796556-4-andreas@kemnade.info> (raw)
In-Reply-To: <20220206080016.796556-1-andreas@kemnade.info>

Adds display parameter initialisation, display power up/down and
waveform loading

Signed-off-by: Andreas Kemnade <andreas@kemnade.info>
---
 drivers/gpu/drm/mxc-epdc/Makefile        |   2 +-
 drivers/gpu/drm/mxc-epdc/epdc_hw.c       | 495 +++++++++++++++++++++++
 drivers/gpu/drm/mxc-epdc/epdc_hw.h       |   8 +
 drivers/gpu/drm/mxc-epdc/epdc_waveform.c | 189 +++++++++
 drivers/gpu/drm/mxc-epdc/epdc_waveform.h |   7 +
 drivers/gpu/drm/mxc-epdc/mxc_epdc.h      |  81 ++++
 drivers/gpu/drm/mxc-epdc/mxc_epdc_drv.c  |  94 +++++
 7 files changed, 875 insertions(+), 1 deletion(-)
 create mode 100644 drivers/gpu/drm/mxc-epdc/epdc_hw.c
 create mode 100644 drivers/gpu/drm/mxc-epdc/epdc_hw.h
 create mode 100644 drivers/gpu/drm/mxc-epdc/epdc_waveform.c
 create mode 100644 drivers/gpu/drm/mxc-epdc/epdc_waveform.h

diff --git a/drivers/gpu/drm/mxc-epdc/Makefile b/drivers/gpu/drm/mxc-epdc/Makefile
index a47ced72b7f6..0263ef2bf0db 100644
--- a/drivers/gpu/drm/mxc-epdc/Makefile
+++ b/drivers/gpu/drm/mxc-epdc/Makefile
@@ -1,5 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0
-mxc_epdc_drm-y := mxc_epdc_drv.o
+mxc_epdc_drm-y := mxc_epdc_drv.o epdc_hw.o epdc_waveform.o
 
 obj-$(CONFIG_DRM_MXC_EPDC) += mxc_epdc_drm.o
 
diff --git a/drivers/gpu/drm/mxc-epdc/epdc_hw.c b/drivers/gpu/drm/mxc-epdc/epdc_hw.c
new file mode 100644
index 000000000000..a74cbd237e0d
--- /dev/null
+++ b/drivers/gpu/drm/mxc-epdc/epdc_hw.c
@@ -0,0 +1,495 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright (C) 2022 Andreas Kemnade
+//
+/*
+ * based on the EPDC framebuffer driver
+ * Copyright (C) 2010-2016 Freescale Semiconductor, Inc.
+ * Copyright 2017 NXP
+ */
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/regulator/consumer.h>
+
+#include "mxc_epdc.h"
+#include "epdc_regs.h"
+#include "epdc_hw.h"
+#include "epdc_waveform.h"
+
+void mxc_epdc_powerup(struct mxc_epdc *priv)
+{
+	int ret = 0;
+
+	mutex_lock(&priv->power_mutex);
+
+	/*
+	 * If power down request is pending, clear
+	 * powering_down to cancel the request.
+	 */
+	if (priv->powering_down)
+		priv->powering_down = false;
+
+	if (priv->powered) {
+		mutex_unlock(&priv->power_mutex);
+		return;
+	}
+
+	dev_dbg(priv->drm.dev, "EPDC Powerup\n");
+
+	priv->updates_active = true;
+
+	/* Enable the v3p3 regulator */
+	ret = regulator_enable(priv->v3p3_regulator);
+	if (IS_ERR((void *)ret)) {
+		dev_err(priv->drm.dev,
+			"Unable to enable V3P3 regulator. err = 0x%x\n",
+			ret);
+		mutex_unlock(&priv->power_mutex);
+		return;
+	}
+
+	usleep_range(1000, 2000);
+
+	pm_runtime_get_sync(priv->drm.dev);
+
+	/* Enable clocks to EPDC */
+	clk_prepare_enable(priv->epdc_clk_axi);
+	clk_prepare_enable(priv->epdc_clk_pix);
+
+	epdc_write(priv, EPDC_CTRL_CLEAR, EPDC_CTRL_CLKGATE);
+
+	/* Enable power to the EPD panel */
+	ret = regulator_enable(priv->display_regulator);
+	if (IS_ERR((void *)ret)) {
+		dev_err(priv->drm.dev,
+			"Unable to enable DISPLAY regulator. err = 0x%x\n",
+			ret);
+		mutex_unlock(&priv->power_mutex);
+		return;
+	}
+
+	ret = regulator_enable(priv->vcom_regulator);
+	if (IS_ERR((void *)ret)) {
+		dev_err(priv->drm.dev,
+			"Unable to enable VCOM regulator. err = 0x%x\n",
+			ret);
+		mutex_unlock(&priv->power_mutex);
+		return;
+	}
+
+	priv->powered = true;
+
+	mutex_unlock(&priv->power_mutex);
+}
+
+void mxc_epdc_powerdown(struct mxc_epdc *priv)
+{
+	mutex_lock(&priv->power_mutex);
+
+	/* If powering_down has been cleared, a powerup
+	 * request is pre-empting this powerdown request.
+	 */
+	if (!priv->powering_down
+		|| (!priv->powered)) {
+		mutex_unlock(&priv->power_mutex);
+		return;
+	}
+
+	dev_dbg(priv->drm.dev, "EPDC Powerdown\n");
+
+	/* Disable power to the EPD panel */
+	regulator_disable(priv->vcom_regulator);
+	regulator_disable(priv->display_regulator);
+
+	/* Disable clocks to EPDC */
+	epdc_write(priv, EPDC_CTRL_SET, EPDC_CTRL_CLKGATE);
+	clk_disable_unprepare(priv->epdc_clk_pix);
+	clk_disable_unprepare(priv->epdc_clk_axi);
+
+	pm_runtime_put_sync_suspend(priv->drm.dev);
+
+	/* turn off the V3p3 */
+	regulator_disable(priv->v3p3_regulator);
+
+	priv->powered = false;
+	priv->powering_down = false;
+
+	if (priv->wait_for_powerdown) {
+		priv->wait_for_powerdown = false;
+		complete(&priv->powerdown_compl);
+	}
+
+	mutex_unlock(&priv->power_mutex);
+}
+
+static void epdc_set_horizontal_timing(struct mxc_epdc *priv, u32 horiz_start,
+				       u32 horiz_end,
+				       u32 hsync_width, u32 hsync_line_length)
+{
+	u32 reg_val =
+	    ((hsync_width << EPDC_TCE_HSCAN1_LINE_SYNC_WIDTH_OFFSET) &
+	     EPDC_TCE_HSCAN1_LINE_SYNC_WIDTH_MASK)
+	    | ((hsync_line_length << EPDC_TCE_HSCAN1_LINE_SYNC_OFFSET) &
+	       EPDC_TCE_HSCAN1_LINE_SYNC_MASK);
+	epdc_write(priv, EPDC_TCE_HSCAN1, reg_val);
+
+	reg_val =
+	    ((horiz_start << EPDC_TCE_HSCAN2_LINE_BEGIN_OFFSET) &
+	     EPDC_TCE_HSCAN2_LINE_BEGIN_MASK)
+	    | ((horiz_end << EPDC_TCE_HSCAN2_LINE_END_OFFSET) &
+	       EPDC_TCE_HSCAN2_LINE_END_MASK);
+	epdc_write(priv, EPDC_TCE_HSCAN2, reg_val);
+}
+
+static void epdc_set_vertical_timing(struct mxc_epdc *priv,
+				     u32 vert_start,
+				     u32 vert_end,
+				     u32 vsync_width)
+{
+	u32 reg_val =
+	    ((vert_start << EPDC_TCE_VSCAN_FRAME_BEGIN_OFFSET) &
+	     EPDC_TCE_VSCAN_FRAME_BEGIN_MASK)
+	    | ((vert_end << EPDC_TCE_VSCAN_FRAME_END_OFFSET) &
+	       EPDC_TCE_VSCAN_FRAME_END_MASK)
+	    | ((vsync_width << EPDC_TCE_VSCAN_FRAME_SYNC_OFFSET) &
+	       EPDC_TCE_VSCAN_FRAME_SYNC_MASK);
+	epdc_write(priv, EPDC_TCE_VSCAN, reg_val);
+}
+
+static inline void epdc_set_screen_res(struct mxc_epdc *priv,
+				       u32 width, u32 height)
+{
+	u32 val = (height << EPDC_RES_VERTICAL_OFFSET) | width;
+
+	epdc_write(priv, EPDC_RES, val);
+}
+
+
+void epdc_init_settings(struct mxc_epdc *priv, struct drm_display_mode *m)
+{
+	u32 reg_val;
+	int num_ce;
+	int i;
+
+	/* Enable clocks to access EPDC regs */
+	clk_prepare_enable(priv->epdc_clk_axi);
+	clk_prepare_enable(priv->epdc_clk_pix);
+
+	/* Reset */
+	epdc_write(priv, EPDC_CTRL_SET, EPDC_CTRL_SFTRST);
+	while (!(epdc_read(priv, EPDC_CTRL) & EPDC_CTRL_CLKGATE))
+		;
+	epdc_write(priv, EPDC_CTRL_CLEAR, EPDC_CTRL_SFTRST);
+
+	/* Enable clock gating (clear to enable) */
+	epdc_write(priv, EPDC_CTRL_CLEAR, EPDC_CTRL_CLKGATE);
+	while (epdc_read(priv, EPDC_CTRL) & (EPDC_CTRL_SFTRST | EPDC_CTRL_CLKGATE))
+		;
+
+	/* EPDC_CTRL */
+	reg_val = epdc_read(priv, EPDC_CTRL);
+	reg_val &= ~EPDC_CTRL_UPD_DATA_SWIZZLE_MASK;
+	reg_val |= EPDC_CTRL_UPD_DATA_SWIZZLE_NO_SWAP;
+	reg_val &= ~EPDC_CTRL_LUT_DATA_SWIZZLE_MASK;
+	reg_val |= EPDC_CTRL_LUT_DATA_SWIZZLE_NO_SWAP;
+	epdc_write(priv, EPDC_CTRL_SET, reg_val);
+
+	/* EPDC_FORMAT - 2bit TFT and buf_pix_fmt Buf pixel format */
+	reg_val = EPDC_FORMAT_TFT_PIXEL_FORMAT_2BIT
+		| priv->buf_pix_fmt
+	    | ((0x0 << EPDC_FORMAT_DEFAULT_TFT_PIXEL_OFFSET) &
+	       EPDC_FORMAT_DEFAULT_TFT_PIXEL_MASK);
+	epdc_write(priv, EPDC_FORMAT, reg_val);
+	if (priv->rev >= 30) {
+		if (priv->buf_pix_fmt == EPDC_FORMAT_BUF_PIXEL_FORMAT_P5N) {
+			epdc_write(priv, EPDC_WB_FIELD2, 0xc554);
+			epdc_write(priv, EPDC_WB_FIELD1, 0xa004);
+		} else {
+			epdc_write(priv, EPDC_WB_FIELD2, 0xc443);
+			epdc_write(priv, EPDC_WB_FIELD1, 0xa003);
+		}
+	}
+
+	/* EPDC_FIFOCTRL (disabled) */
+	reg_val =
+	    ((100 << EPDC_FIFOCTRL_FIFO_INIT_LEVEL_OFFSET) &
+	     EPDC_FIFOCTRL_FIFO_INIT_LEVEL_MASK)
+	    | ((200 << EPDC_FIFOCTRL_FIFO_H_LEVEL_OFFSET) &
+	       EPDC_FIFOCTRL_FIFO_H_LEVEL_MASK)
+	    | ((100 << EPDC_FIFOCTRL_FIFO_L_LEVEL_OFFSET) &
+	       EPDC_FIFOCTRL_FIFO_L_LEVEL_MASK);
+	epdc_write(priv, EPDC_FIFOCTRL, reg_val);
+
+	/* EPDC_TEMP - Use default temp to get index */
+	epdc_write(priv, EPDC_TEMP,
+		   mxc_epdc_fb_get_temp_index(priv, TEMP_USE_AMBIENT));
+
+	/* EPDC_RES */
+	epdc_set_screen_res(priv, m->hdisplay, m->vdisplay);
+
+	/* EPDC_AUTOWV_LUT */
+	/* Initialize all auto-wavefrom look-up values to 2 - GC16 */
+	for (i = 0; i < 8; i++)
+		epdc_write(priv, EPDC_AUTOWV_LUT,
+			(2 << EPDC_AUTOWV_LUT_DATA_OFFSET) |
+			(i << EPDC_AUTOWV_LUT_ADDR_OFFSET));
+
+	/*
+	 * EPDC_TCE_CTRL
+	 * VSCAN_HOLDOFF = 4
+	 * VCOM_MODE = MANUAL
+	 * VCOM_VAL = 0
+	 * DDR_MODE = DISABLED
+	 * LVDS_MODE_CE = DISABLED
+	 * LVDS_MODE = DISABLED
+	 * DUAL_SCAN = DISABLED
+	 * SDDO_WIDTH = 8bit
+	 * PIXELS_PER_SDCLK = 4
+	 */
+	reg_val =
+	    ((priv->imx_mode.vscan_holdoff << EPDC_TCE_CTRL_VSCAN_HOLDOFF_OFFSET) &
+	     EPDC_TCE_CTRL_VSCAN_HOLDOFF_MASK)
+	    | EPDC_TCE_CTRL_PIXELS_PER_SDCLK_4;
+	epdc_write(priv, EPDC_TCE_CTRL, reg_val);
+
+	/* EPDC_TCE_HSCAN */
+	epdc_set_horizontal_timing(priv, m->hsync_start - m->hdisplay,
+				   m->htotal - m->hsync_end,
+				   m->hsync_end - m->hsync_start,
+				   m->hsync_end - m->hsync_start);
+
+	/* EPDC_TCE_VSCAN */
+	epdc_set_vertical_timing(priv, m->vsync_start - m->vdisplay,
+				 m->vtotal - m->vsync_end,
+				 m->vsync_end - m->vsync_start);
+
+	/* EPDC_TCE_OE */
+	reg_val =
+	    ((priv->imx_mode.sdoed_width << EPDC_TCE_OE_SDOED_WIDTH_OFFSET) &
+	     EPDC_TCE_OE_SDOED_WIDTH_MASK)
+	    | ((priv->imx_mode.sdoed_delay << EPDC_TCE_OE_SDOED_DLY_OFFSET) &
+	       EPDC_TCE_OE_SDOED_DLY_MASK)
+	    | ((priv->imx_mode.sdoez_width << EPDC_TCE_OE_SDOEZ_WIDTH_OFFSET) &
+	       EPDC_TCE_OE_SDOEZ_WIDTH_MASK)
+	    | ((priv->imx_mode.sdoez_delay << EPDC_TCE_OE_SDOEZ_DLY_OFFSET) &
+	       EPDC_TCE_OE_SDOEZ_DLY_MASK);
+	epdc_write(priv, EPDC_TCE_OE, reg_val);
+
+	/* EPDC_TCE_TIMING1 */
+	epdc_write(priv, EPDC_TCE_TIMING1, 0x0);
+
+	/* EPDC_TCE_TIMING2 */
+	reg_val =
+	    ((priv->imx_mode.gdclk_hp_offs << EPDC_TCE_TIMING2_GDCLK_HP_OFFSET) &
+	     EPDC_TCE_TIMING2_GDCLK_HP_MASK)
+	    | ((priv->imx_mode.gdsp_offs << EPDC_TCE_TIMING2_GDSP_OFFSET_OFFSET) &
+	       EPDC_TCE_TIMING2_GDSP_OFFSET_MASK);
+	epdc_write(priv, EPDC_TCE_TIMING2, reg_val);
+
+	/* EPDC_TCE_TIMING3 */
+	reg_val =
+	    ((priv->imx_mode.gdoe_offs << EPDC_TCE_TIMING3_GDOE_OFFSET_OFFSET) &
+	     EPDC_TCE_TIMING3_GDOE_OFFSET_MASK)
+	    | ((priv->imx_mode.gdclk_offs << EPDC_TCE_TIMING3_GDCLK_OFFSET_OFFSET) &
+	       EPDC_TCE_TIMING3_GDCLK_OFFSET_MASK);
+	epdc_write(priv, EPDC_TCE_TIMING3, reg_val);
+
+	/*
+	 * EPDC_TCE_SDCFG
+	 * SDCLK_HOLD = 1
+	 * SDSHR = 1
+	 * NUM_CE = 1
+	 * SDDO_REFORMAT = FLIP_PIXELS
+	 * SDDO_INVERT = DISABLED
+	 * PIXELS_PER_CE = display horizontal resolution
+	 */
+	num_ce = priv->imx_mode.num_ce;
+	if (num_ce == 0)
+		num_ce = 1;
+	reg_val = EPDC_TCE_SDCFG_SDCLK_HOLD | EPDC_TCE_SDCFG_SDSHR
+	    | ((num_ce << EPDC_TCE_SDCFG_NUM_CE_OFFSET) &
+	       EPDC_TCE_SDCFG_NUM_CE_MASK)
+	    | EPDC_TCE_SDCFG_SDDO_REFORMAT_FLIP_PIXELS
+	    | ((priv->epdc_mem_width/num_ce << EPDC_TCE_SDCFG_PIXELS_PER_CE_OFFSET) &
+	       EPDC_TCE_SDCFG_PIXELS_PER_CE_MASK);
+	epdc_write(priv, EPDC_TCE_SDCFG, reg_val);
+
+	/*
+	 * EPDC_TCE_GDCFG
+	 * GDRL = 1
+	 * GDOE_MODE = 0;
+	 * GDSP_MODE = 0;
+	 */
+	reg_val = EPDC_TCE_SDCFG_GDRL;
+	epdc_write(priv, EPDC_TCE_GDCFG, reg_val);
+
+	/*
+	 * EPDC_TCE_POLARITY
+	 * SDCE_POL = ACTIVE LOW
+	 * SDLE_POL = ACTIVE HIGH
+	 * SDOE_POL = ACTIVE HIGH
+	 * GDOE_POL = ACTIVE HIGH
+	 * GDSP_POL = ACTIVE LOW
+	 */
+	reg_val = EPDC_TCE_POLARITY_SDLE_POL_ACTIVE_HIGH
+	    | EPDC_TCE_POLARITY_SDOE_POL_ACTIVE_HIGH
+	    | EPDC_TCE_POLARITY_GDOE_POL_ACTIVE_HIGH;
+	epdc_write(priv, EPDC_TCE_POLARITY, reg_val);
+
+	/* EPDC_IRQ_MASK */
+	epdc_write(priv, EPDC_IRQ_MASK, EPDC_IRQ_TCE_UNDERRUN_IRQ);
+
+	/*
+	 * EPDC_GPIO
+	 * PWRCOM = ?
+	 * PWRCTRL = ?
+	 * BDR = ?
+	 */
+	reg_val = ((0 << EPDC_GPIO_PWRCTRL_OFFSET) & EPDC_GPIO_PWRCTRL_MASK)
+	    | ((0 << EPDC_GPIO_BDR_OFFSET) & EPDC_GPIO_BDR_MASK);
+	epdc_write(priv, EPDC_GPIO, reg_val);
+
+	epdc_write(priv, EPDC_WVADDR, priv->waveform_buffer_phys);
+	epdc_write(priv, EPDC_WB_ADDR, priv->working_buffer_phys);
+	if (priv->rev >= 30)
+		epdc_write(priv, EPDC_WB_ADDR_TCE_V3,
+			   priv->working_buffer_phys);
+	else
+		epdc_write(priv, EPDC_WB_ADDR_TCE,
+			   priv->working_buffer_phys);
+
+	/* Disable clock */
+	clk_disable_unprepare(priv->epdc_clk_axi);
+	clk_disable_unprepare(priv->epdc_clk_pix);
+}
+
+void mxc_epdc_init_sequence(struct mxc_epdc *priv, struct drm_display_mode *m)
+{
+	/* Initialize EPDC, passing pointer to EPDC registers */
+	struct clk *epdc_parent;
+	unsigned long rounded_parent_rate, epdc_pix_rate,
+			rounded_pix_clk, target_pix_clk;
+
+	/* Enable pix clk for EPDC */
+	clk_prepare_enable(priv->epdc_clk_axi);
+
+	target_pix_clk = m->clock * 1000;
+	rounded_pix_clk = clk_round_rate(priv->epdc_clk_pix, target_pix_clk);
+
+	if (((rounded_pix_clk >= target_pix_clk + target_pix_clk/100) ||
+		(rounded_pix_clk <= target_pix_clk - target_pix_clk/100))) {
+		/* Can't get close enough without changing parent clk */
+		epdc_parent = clk_get_parent(priv->epdc_clk_pix);
+		rounded_parent_rate = clk_round_rate(epdc_parent, target_pix_clk);
+
+		epdc_pix_rate = target_pix_clk;
+		while (epdc_pix_rate < rounded_parent_rate)
+			epdc_pix_rate *= 2;
+		clk_set_rate(epdc_parent, epdc_pix_rate);
+
+		rounded_pix_clk = clk_round_rate(priv->epdc_clk_pix, target_pix_clk);
+		if (((rounded_pix_clk >= target_pix_clk + target_pix_clk/100) ||
+			(rounded_pix_clk <= target_pix_clk - target_pix_clk/100)))
+			/* Still can't get a good clock, provide warning */
+			dev_err(priv->drm.dev,
+				"Unable to get an accurate EPDC pix clk desired = %lu, actual = %lu\n",
+				target_pix_clk,
+				rounded_pix_clk);
+	}
+
+	clk_set_rate(priv->epdc_clk_pix, rounded_pix_clk);
+	clk_prepare_enable(priv->epdc_clk_pix);
+
+	epdc_init_settings(priv, m);
+
+	priv->in_init = true;
+	mxc_epdc_powerup(priv);
+	/* Force power down event */
+	priv->powering_down = true;
+	mxc_epdc_powerdown(priv);
+	priv->updates_active = false;
+
+	/* Disable clocks */
+	clk_disable_unprepare(priv->epdc_clk_axi);
+	clk_disable_unprepare(priv->epdc_clk_pix);
+	priv->hw_ready = true;
+	priv->hw_initializing = false;
+
+}
+
+int mxc_epdc_init_hw(struct mxc_epdc *priv)
+{
+	struct pinctrl *pinctrl;
+	const char *thermal = NULL;
+	u32 val;
+
+	/* get pmic regulators */
+	priv->display_regulator = devm_regulator_get(priv->drm.dev, "DISPLAY");
+	if (IS_ERR(priv->display_regulator))
+		return dev_err_probe(priv->drm.dev, PTR_ERR(priv->display_regulator),
+				     "Unable to get display PMIC regulator\n");
+
+	priv->vcom_regulator = devm_regulator_get(priv->drm.dev, "VCOM");
+	if (IS_ERR(priv->vcom_regulator))
+		return dev_err_probe(priv->drm.dev, PTR_ERR(priv->vcom_regulator),
+				     "Unable to get VCOM regulator\n");
+
+	priv->v3p3_regulator = devm_regulator_get(priv->drm.dev, "V3P3");
+	if (IS_ERR(priv->v3p3_regulator))
+		return dev_err_probe(priv->drm.dev, PTR_ERR(priv->v3p3_regulator),
+				     "Unable to get V3P3 regulator\n");
+
+	of_property_read_string(priv->drm.dev->of_node,
+				"epd-thermal-zone", &thermal);
+	if (thermal) {
+		priv->thermal = thermal_zone_get_zone_by_name(thermal);
+		if (IS_ERR(priv->thermal))
+			return dev_err_probe(priv->drm.dev, PTR_ERR(priv->thermal),
+					     "unable to get thermal");
+	}
+	priv->iobase = devm_platform_get_and_ioremap_resource(to_platform_device(priv->drm.dev),
+							      0, NULL);
+	if (priv->iobase == NULL)
+		return -ENOMEM;
+
+	priv->epdc_clk_axi = devm_clk_get(priv->drm.dev, "axi");
+	if (IS_ERR(priv->epdc_clk_axi))
+		return dev_err_probe(priv->drm.dev, PTR_ERR(priv->epdc_clk_axi),
+				     "Unable to get EPDC AXI clk\n");
+
+	priv->epdc_clk_pix = devm_clk_get(priv->drm.dev, "pix");
+	if (IS_ERR(priv->epdc_clk_pix))
+		return dev_err_probe(priv->drm.dev, PTR_ERR(priv->epdc_clk_pix),
+				     "Unable to get EPDC pix clk\n");
+
+	clk_prepare_enable(priv->epdc_clk_axi);
+	val = epdc_read(priv, EPDC_VERSION);
+	clk_disable_unprepare(priv->epdc_clk_axi);
+	priv->rev = ((val & EPDC_VERSION_MAJOR_MASK) >>
+				EPDC_VERSION_MAJOR_OFFSET) * 10
+			+ ((val & EPDC_VERSION_MINOR_MASK) >>
+				EPDC_VERSION_MINOR_OFFSET);
+	dev_dbg(priv->drm.dev, "EPDC version = %d\n", priv->rev);
+
+	if (priv->rev <= 20) {
+		dev_err(priv->drm.dev, "Unsupported version (%d)\n", priv->rev);
+		return -ENODEV;
+	}
+
+	/* Initialize EPDC pins */
+	pinctrl = devm_pinctrl_get_select_default(priv->drm.dev);
+	if (IS_ERR(pinctrl)) {
+		dev_err(priv->drm.dev, "can't get/select pinctrl\n");
+		return PTR_ERR(pinctrl);
+	}
+
+	mutex_init(&priv->power_mutex);
+
+	return 0;
+}
diff --git a/drivers/gpu/drm/mxc-epdc/epdc_hw.h b/drivers/gpu/drm/mxc-epdc/epdc_hw.h
new file mode 100644
index 000000000000..dbf1f0d1e23e
--- /dev/null
+++ b/drivers/gpu/drm/mxc-epdc/epdc_hw.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/* Copyright (C) 2022 Andreas Kemnade */
+void mxc_epdc_init_sequence(struct mxc_epdc *priv, struct drm_display_mode *m);
+int mxc_epdc_init_hw(struct mxc_epdc *priv);
+
+void mxc_epdc_powerup(struct mxc_epdc *priv);
+void mxc_epdc_powerdown(struct mxc_epdc *priv);
+
diff --git a/drivers/gpu/drm/mxc-epdc/epdc_waveform.c b/drivers/gpu/drm/mxc-epdc/epdc_waveform.c
new file mode 100644
index 000000000000..4f2f199722d5
--- /dev/null
+++ b/drivers/gpu/drm/mxc-epdc/epdc_waveform.c
@@ -0,0 +1,189 @@
+// SPDX-License-Identifier: GPL-2.0+
+// Copyright (C) 2022 Andreas Kemnade
+//
+/*
+ * based on the EPDC framebuffer driver
+ * Copyright (C) 2010-2016 Freescale Semiconductor, Inc.
+ * Copyright 2017 NXP
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/dma-mapping.h>
+#include "mxc_epdc.h"
+
+#define DEFAULT_TEMP_INDEX      0
+#define DEFAULT_TEMP            20 /* room temp in deg Celsius */
+
+struct waveform_data_header {
+	unsigned int wi0;
+	unsigned int wi1;
+	unsigned int wi2;
+	unsigned int wi3;
+	unsigned int wi4;
+	unsigned int wi5;
+	unsigned int wi6;
+	unsigned int xwia:24;
+	unsigned int cs1:8;
+	unsigned int wmta:24;
+	unsigned int fvsn:8;
+	unsigned int luts:8;
+	unsigned int mc:8;
+	unsigned int trc:8;
+	unsigned int reserved0_0:8;
+	unsigned int eb:8;
+	unsigned int sb:8;
+	unsigned int reserved0_1:8;
+	unsigned int reserved0_2:8;
+	unsigned int reserved0_3:8;
+	unsigned int reserved0_4:8;
+	unsigned int reserved0_5:8;
+	unsigned int cs2:8;
+};
+
+struct mxcfb_waveform_data_file {
+	struct waveform_data_header wdh;
+	u32 *data;      /* Temperature Range Table + Waveform Data */
+};
+
+void mxc_epdc_set_update_waveform(struct mxc_epdc *priv,
+				  struct mxcfb_waveform_modes *wv_modes)
+{
+	u32 val;
+
+	/* Configure the auto-waveform look-up table based on waveform modes */
+
+	/* Entry 1 = DU, 2 = GC4, 3 = GC8, etc. */
+	val = (wv_modes->mode_du << EPDC_AUTOWV_LUT_DATA_OFFSET) |
+		(0 << EPDC_AUTOWV_LUT_ADDR_OFFSET);
+	epdc_write(priv, EPDC_AUTOWV_LUT, val);
+	val = (wv_modes->mode_du << EPDC_AUTOWV_LUT_DATA_OFFSET) |
+		(1 << EPDC_AUTOWV_LUT_ADDR_OFFSET);
+	epdc_write(priv, EPDC_AUTOWV_LUT, val);
+	val = (wv_modes->mode_gc4 << EPDC_AUTOWV_LUT_DATA_OFFSET) |
+		(2 << EPDC_AUTOWV_LUT_ADDR_OFFSET);
+	epdc_write(priv, EPDC_AUTOWV_LUT, val);
+	val = (wv_modes->mode_gc8 << EPDC_AUTOWV_LUT_DATA_OFFSET) |
+		(3 << EPDC_AUTOWV_LUT_ADDR_OFFSET);
+	epdc_write(priv, EPDC_AUTOWV_LUT, val);
+	val = (wv_modes->mode_gc16 << EPDC_AUTOWV_LUT_DATA_OFFSET) |
+		(4 << EPDC_AUTOWV_LUT_ADDR_OFFSET);
+	epdc_write(priv, EPDC_AUTOWV_LUT, val);
+	val = (wv_modes->mode_gc32 << EPDC_AUTOWV_LUT_DATA_OFFSET) |
+		(5 << EPDC_AUTOWV_LUT_ADDR_OFFSET);
+	epdc_write(priv, EPDC_AUTOWV_LUT, val);
+}
+
+int mxc_epdc_fb_get_temp_index(struct mxc_epdc *priv, int temp)
+{
+	int i;
+	int index = -1;
+
+	if (temp == TEMP_USE_AMBIENT) {
+		if (priv->thermal) {
+			if (thermal_zone_get_temp(priv->thermal, &temp)) {
+				dev_err(priv->drm.dev,
+					"reading temperature failed");
+				return DEFAULT_TEMP_INDEX;
+			}
+			temp /= 1000;
+		} else
+			temp = DEFAULT_TEMP;
+	}
+
+	if (priv->trt_entries == 0) {
+		dev_err(priv->drm.dev,
+			"No TRT exists...using default temp index\n");
+		return DEFAULT_TEMP_INDEX;
+	}
+
+	/* Search temperature ranges for a match */
+	for (i = 0; i < priv->trt_entries - 1; i++) {
+		if ((temp >= priv->temp_range_bounds[i])
+			&& (temp < priv->temp_range_bounds[i+1])) {
+			index = i;
+			break;
+		}
+	}
+
+	if (index < 0) {
+		dev_err(priv->drm.dev,
+			"No TRT index match...using lowest/highest\n");
+		if (temp < priv->temp_range_bounds[0]) {
+			dev_dbg(priv->drm.dev, "temperature < minimum range\n");
+			return 0;
+		}
+
+		if (temp >= priv->temp_range_bounds[priv->trt_entries-1]) {
+			dev_dbg(priv->drm.dev, "temperature >= maximum range\n");
+			return priv->trt_entries-1;
+		}
+
+		return DEFAULT_TEMP_INDEX;
+	}
+
+	dev_dbg(priv->drm.dev, "Using temperature index %d\n", index);
+
+	return index;
+}
+
+
+
+int mxc_epdc_prepare_waveform(struct mxc_epdc *priv,
+			      const u8 *data, size_t size)
+{
+	const struct mxcfb_waveform_data_file *wv_file;
+	int wv_data_offs;
+	int i;
+
+	priv->wv_modes.mode_init = 0;
+	priv->wv_modes.mode_du = 1;
+	priv->wv_modes.mode_gc4 = 3;
+	priv->wv_modes.mode_gc8 = 2;
+	priv->wv_modes.mode_gc16 = 2;
+	priv->wv_modes.mode_gc32 = 2;
+	priv->wv_modes_update = true;
+
+	wv_file = (struct mxcfb_waveform_data_file *)data;
+
+	/* Get size and allocate temperature range table */
+	priv->trt_entries = wv_file->wdh.trc + 1;
+	priv->temp_range_bounds = devm_kzalloc(priv->drm.dev, priv->trt_entries, GFP_KERNEL);
+
+	for (i = 0; i < priv->trt_entries; i++)
+		dev_dbg(priv->drm.dev, "trt entry #%d = 0x%x\n", i, *((u8 *)&wv_file->data + i));
+
+	/* Copy TRT data */
+	memcpy(priv->temp_range_bounds, &wv_file->data, priv->trt_entries);
+
+	/* Set default temperature index using TRT and room temp */
+	priv->temp_index = mxc_epdc_fb_get_temp_index(priv, DEFAULT_TEMP);
+
+	/* Get offset and size for waveform data */
+	wv_data_offs = sizeof(wv_file->wdh) + priv->trt_entries + 1;
+	priv->waveform_buffer_size = size - wv_data_offs;
+
+	/* Allocate memory for waveform data */
+	priv->waveform_buffer_virt = dmam_alloc_coherent(priv->drm.dev,
+						priv->waveform_buffer_size,
+						&priv->waveform_buffer_phys,
+						GFP_DMA | GFP_KERNEL);
+	if (priv->waveform_buffer_virt == NULL) {
+		dev_err(priv->drm.dev, "Can't allocate mem for waveform!\n");
+		return -ENOMEM;
+	}
+
+	memcpy(priv->waveform_buffer_virt, (u8 *)(data) + wv_data_offs,
+		priv->waveform_buffer_size);
+
+	/* Read field to determine if 4-bit or 5-bit mode */
+	if ((wv_file->wdh.luts & 0xC) == 0x4)
+		priv->buf_pix_fmt = EPDC_FORMAT_BUF_PIXEL_FORMAT_P5N;
+	else
+		priv->buf_pix_fmt = EPDC_FORMAT_BUF_PIXEL_FORMAT_P4N;
+
+	dev_info(priv->drm.dev, "EPDC pix format: %x\n",
+		 priv->buf_pix_fmt);
+
+	return 0;
+}
diff --git a/drivers/gpu/drm/mxc-epdc/epdc_waveform.h b/drivers/gpu/drm/mxc-epdc/epdc_waveform.h
new file mode 100644
index 000000000000..c5c461b975cb
--- /dev/null
+++ b/drivers/gpu/drm/mxc-epdc/epdc_waveform.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/* Copyright (C) 2022 Andreas Kemnade */
+int mxc_epdc_fb_get_temp_index(struct mxc_epdc *priv, int temp);
+int mxc_epdc_prepare_waveform(struct mxc_epdc *priv,
+			      const u8 *waveform, size_t size);
+void mxc_epdc_set_update_waveform(struct mxc_epdc *priv,
+				  struct mxcfb_waveform_modes *wv_modes);
diff --git a/drivers/gpu/drm/mxc-epdc/mxc_epdc.h b/drivers/gpu/drm/mxc-epdc/mxc_epdc.h
index c5f5280b574f..f7b1cbc4cc4e 100644
--- a/drivers/gpu/drm/mxc-epdc/mxc_epdc.h
+++ b/drivers/gpu/drm/mxc-epdc/mxc_epdc.h
@@ -8,6 +8,32 @@
 #include <drm/drm_drv.h>
 #include <drm/drm_connector.h>
 #include <drm/drm_simple_kms_helper.h>
+#include <linux/thermal.h>
+#include "epdc_regs.h"
+
+#define TEMP_USE_AMBIENT			0x1000
+
+struct mxcfb_waveform_modes {
+	int mode_init;
+	int mode_du;
+	int mode_gc4;
+	int mode_gc8;
+	int mode_gc16;
+	int mode_gc32;
+};
+
+struct imx_epdc_fb_mode {
+	u32 vscan_holdoff;
+	u32 sdoed_width;
+	u32 sdoed_delay;
+	u32 sdoez_width;
+	u32 sdoez_delay;
+	u32 gdclk_hp_offs;
+	u32 gdsp_offs;
+	u32 gdoe_offs;
+	u32 gdclk_offs;
+	u32 num_ce;
+};
 
 struct clk;
 struct regulator;
@@ -16,5 +42,60 @@ struct mxc_epdc {
 	struct drm_simple_display_pipe pipe;
 	struct drm_connector connector;
 	struct display_timing timing;
+	struct imx_epdc_fb_mode imx_mode;
+	void __iomem *iobase;
+	struct completion powerdown_compl;
+	struct clk *epdc_clk_axi;
+	struct clk *epdc_clk_pix;
+	struct regulator *display_regulator;
+	struct regulator *vcom_regulator;
+	struct regulator *v3p3_regulator;
+	struct thermal_zone_device *thermal;
+	int rev;
+
+	dma_addr_t epdc_mem_phys;
+	void *epdc_mem_virt;
+	int epdc_mem_width;
+	int epdc_mem_height;
+	u32 *working_buffer_virt;
+	dma_addr_t working_buffer_phys;
+	u32 working_buffer_size;
+
+	/* waveform related stuff */
+	int trt_entries;
+	int temp_index;
+	u8 *temp_range_bounds;
+	int buf_pix_fmt;
+	struct mxcfb_waveform_modes wv_modes;
+	bool wv_modes_update;
+	u32 *waveform_buffer_virt;
+	dma_addr_t waveform_buffer_phys;
+	u32 waveform_buffer_size;
+
+	struct mutex power_mutex;
+	bool powered;
+	bool powering_down;
+	bool updates_active;
+	int wait_for_powerdown;
+	int pwrdown_delay;
+
+	/* elements related to EPDC updates */
+	int num_luts;
+	int max_num_updates;
+	bool in_init;
+	bool hw_ready;
+	bool hw_initializing;
+	bool waiting_for_idle;
+
 };
 
+static inline u32 epdc_read(struct mxc_epdc *priv, u32 reg)
+{
+	return readl(priv->iobase + reg);
+}
+
+static inline void epdc_write(struct mxc_epdc *priv, u32 reg, u32 data)
+{
+	writel(data, priv->iobase + reg);
+}
+
diff --git a/drivers/gpu/drm/mxc-epdc/mxc_epdc_drv.c b/drivers/gpu/drm/mxc-epdc/mxc_epdc_drv.c
index c0b0a3bcdb57..4810e5c5bc6e 100644
--- a/drivers/gpu/drm/mxc-epdc/mxc_epdc_drv.c
+++ b/drivers/gpu/drm/mxc-epdc/mxc_epdc_drv.c
@@ -25,6 +25,8 @@
 #include <drm/drm_prime.h>
 #include <drm/drm_probe_helper.h>
 #include "mxc_epdc.h"
+#include "epdc_hw.h"
+#include "epdc_waveform.h"
 
 #define DRIVER_NAME "mxc_epdc"
 #define DRIVER_DESC "IMX EPDC"
@@ -122,6 +124,57 @@ int mxc_epdc_output(struct drm_device *drm)
 				 DRM_MODE_CONNECTOR_Unknown);
 	if (ret)
 		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "vscan-holdoff",
+				   &priv->imx_mode.vscan_holdoff);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "sdoed-width",
+				   &priv->imx_mode.sdoed_width);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "sdoed-delay",
+				   &priv->imx_mode.sdoed_delay);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "sdoez-width",
+				   &priv->imx_mode.sdoez_width);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "sdoez-delay",
+				   &priv->imx_mode.sdoez_delay);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "gdclk-hp-offs",
+				   &priv->imx_mode.gdclk_hp_offs);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "gdsp-offs",
+				   &priv->imx_mode.gdsp_offs);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "gdoe-offs",
+				   &priv->imx_mode.gdoe_offs);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "gdclk-offs",
+				   &priv->imx_mode.gdclk_offs);
+	if (ret)
+		return ret;
+
+	ret = of_property_read_u32(drm->dev->of_node, "num-ce",
+				   &priv->imx_mode.num_ce);
+	if (ret)
+		return ret;
+
 	ret = of_get_display_timing(drm->dev->of_node, "timing", &priv->timing);
 	if (ret)
 		return ret;
@@ -137,6 +190,20 @@ static void mxc_epdc_pipe_enable(struct drm_simple_display_pipe *pipe,
 	struct drm_display_mode *m = &pipe->crtc.state->adjusted_mode;
 
 	dev_info(priv->drm.dev, "Mode: %d x %d\n", m->hdisplay, m->vdisplay);
+	priv->epdc_mem_width = m->hdisplay;
+	priv->epdc_mem_height = m->vdisplay;
+	priv->epdc_mem_virt = dma_alloc_wc(priv->drm.dev,
+					   m->hdisplay * m->vdisplay,
+					   &priv->epdc_mem_phys, GFP_DMA | GFP_KERNEL);
+	priv->working_buffer_size = m->hdisplay * m->vdisplay * 2;
+	priv->working_buffer_virt =
+		dma_alloc_coherent(priv->drm.dev,
+				   priv->working_buffer_size,
+				   &priv->working_buffer_phys,
+				   GFP_DMA | GFP_KERNEL);
+
+	if (priv->working_buffer_virt && priv->epdc_mem_virt)
+		mxc_epdc_init_sequence(priv, m);
 }
 
 static void mxc_epdc_pipe_disable(struct drm_simple_display_pipe *pipe)
@@ -144,6 +211,19 @@ static void mxc_epdc_pipe_disable(struct drm_simple_display_pipe *pipe)
 	struct mxc_epdc *priv = drm_pipe_to_mxc_epdc(pipe);
 
 	dev_dbg(priv->drm.dev, "pipe disable\n");
+
+	if (priv->epdc_mem_virt) {
+		dma_free_wc(priv->drm.dev, priv->epdc_mem_width * priv->epdc_mem_height,
+			    priv->epdc_mem_virt, priv->epdc_mem_phys);
+		priv->epdc_mem_virt = NULL;
+	}
+
+	if (priv->working_buffer_virt) {
+		dma_free_wc(priv->drm.dev, priv->working_buffer_size,
+			    priv->working_buffer_virt,
+			    priv->working_buffer_phys);
+		priv->working_buffer_virt = NULL;
+	}
 }
 
 static void mxc_epdc_pipe_update(struct drm_simple_display_pipe *pipe,
@@ -187,6 +267,7 @@ static struct drm_driver mxc_epdc_driver = {
 static int mxc_epdc_probe(struct platform_device *pdev)
 {
 	struct mxc_epdc *priv;
+	const struct firmware *firmware;
 	int ret;
 
 	priv = devm_drm_dev_alloc(&pdev->dev, &mxc_epdc_driver, struct mxc_epdc, drm);
@@ -195,6 +276,19 @@ static int mxc_epdc_probe(struct platform_device *pdev)
 
 	platform_set_drvdata(pdev, priv);
 
+	ret = mxc_epdc_init_hw(priv);
+	if (ret)
+		return ret;
+
+	ret = request_firmware(&firmware, "imx/epdc/epdc.fw", priv->drm.dev);
+	if (ret)
+		return ret;
+
+	ret = mxc_epdc_prepare_waveform(priv, firmware->data, firmware->size);
+	release_firmware(firmware);
+	if (ret)
+		return ret;
+
 	mxc_epdc_setup_mode_config(&priv->drm);
 
 	ret = mxc_epdc_output(&priv->drm);
-- 
2.30.2


  parent reply	other threads:[~2022-02-06  8:20 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-02-06  8:00 [RFC PATCH 0/6] drm: EPDC driver for i.MX6 Andreas Kemnade
2022-02-06  8:00 ` [RFC PATCH 1/6] dt-bindings: display: imx: Add EPDC Andreas Kemnade
2022-02-11 15:46   ` Rob Herring
2022-02-14 22:45     ` Andreas Kemnade
2022-02-16 23:52       ` Rob Herring
2022-02-17  9:21   ` Krzysztof Kozlowski
2022-02-17 11:31     ` Andreas Kemnade
2022-02-17 11:43       ` Krzysztof Kozlowski
2022-03-12 19:23   ` Jonathan Neuschäfer
2022-03-14 22:04     ` Andreas Kemnade
2022-02-06  8:00 ` [RFC PATCH 2/6] drm: Add skeleton for EPDC driver Andreas Kemnade
2022-03-12 19:41   ` Jonathan Neuschäfer
2022-02-06  8:00 ` Andreas Kemnade [this message]
2022-03-12 20:12   ` [RFC PATCH 3/6] drm: mxc-epdc: Add display and waveform initialisation Jonathan Neuschäfer
2022-02-06  8:00 ` [RFC PATCH 4/6] drm: mxc-epdc: Add update management Andreas Kemnade
2022-03-12 20:21   ` Jonathan Neuschäfer
2022-02-06  8:00 ` [RFC PATCH 5/6] ARM: dts: imx6sll: add EPDC Andreas Kemnade
2022-02-06  8:00 ` [RFC PATCH 6/6] arm: dts: imx6sl: Add EPDC Andreas Kemnade

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20220206080016.796556-4-andreas@kemnade.info \
    --to=andreas@kemnade.info \
    --cc=airlied@linux.ie \
    --cc=alistair@alistair23.me \
    --cc=daniel@ffwll.ch \
    --cc=devicetree@vger.kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=festevam@gmail.com \
    --cc=josua.mayer@jm0.eu \
    --cc=kernel@pengutronix.de \
    --cc=letux-kernel@openphoenux.org \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-imx@nxp.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=p.zabel@pengutronix.de \
    --cc=robh+dt@kernel.org \
    --cc=s.hauer@pengutronix.de \
    --cc=samuel@sholland.org \
    --cc=shawnguo@kernel.org \
    --cc=tzimmermann@suse.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).