From: Sam Ravnborg <sam@ravnborg.org>
To: dri-devel@lists.freedesktop.org
Subject: [RFC PATCH] drm: add Atmel LCDC display controller support
Date: Fri, 26 Jul 2019 22:44:13 +0200 [thread overview]
Message-ID: <20190726204413.GA32705@ravnborg.org> (raw)
In-Reply-To: <20190726203626.GA31474@ravnborg.org>
This is the current state of the driver for the at91sam* LCDC IP.
Notice that it needs some other companion drivers and I posted this
code-drop only to support the request for help on the BGR versus RGB
issue.
That said - any review feedback will be appreciated!
Sam
From c4c2274313dbb23f38626920834819735f70d899 Mon Sep 17 00:00:00 2001
From: Sam Ravnborg <sam@ravnborg.org>
Date: Thu, 27 Dec 2018 02:23:31 +0100
Subject: [PATCH 22/22] drm: add Atmel LCDC display controller support
This is a DRM based driver for the Atmel LCDC IP.
There exist today a framebuffer based driver and
this is a re-implmentation of the same on top of DRM.
The rewrite was based on the original fbdev driver
but the driver has also seen inspiration from
the atmel-hlcdc_dc driver and others.
The driver is not a full replacement:
- STN displays are not supported
STN displays are not considered relevant for
current kernels, and there is no plan to add STN support
The atmel-lcdc driver is divided into a few files:
atmel_lcdc.h - definitions shared by the driver implmentation
../linux/atmel-lcdc.h - definitions shared with the pwm subdriver
atmel_lcdc_drv.c - all the driver details such as DT support etc.
atmel_lcdc_pipe.c - the display pipeline, including the outputs
v3:
- reworked most of the driver after feedback and more testing
Signed-off-by: Sam Ravnborg <sam@ravnborg.org>
Cc: Nicolas Ferre <nicolas.ferre@atmel.com>
Cc: Boris Brezillon <boris.brezillon@free-electrons.com>
Cc: Alexandre Belloni <alexandre.belloni@bootlin.com>
---
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/atmel/Kconfig | 12 +
drivers/gpu/drm/atmel/Makefile | 3 +
drivers/gpu/drm/atmel/atmel_lcdc.h | 81 +++
drivers/gpu/drm/atmel/atmel_lcdc_drv.c | 408 ++++++++++++++
drivers/gpu/drm/atmel/atmel_lcdc_pipe.c | 675 ++++++++++++++++++++++++
6 files changed, 1180 insertions(+)
create mode 100644 drivers/gpu/drm/atmel/atmel_lcdc.h
create mode 100644 drivers/gpu/drm/atmel/atmel_lcdc_drv.c
create mode 100644 drivers/gpu/drm/atmel/atmel_lcdc_pipe.c
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index d7f5328918ab..386fc6166de1 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -87,6 +87,7 @@ obj-$(CONFIG_DRM_UDL) += udl/
obj-$(CONFIG_DRM_AST) += ast/
obj-$(CONFIG_DRM_ARMADA) += armada/
obj-$(CONFIG_DRM_ATMEL_HLCDC) += atmel/
+obj-$(CONFIG_DRM_ATMEL_LCDC) += atmel/
obj-y += rcar-du/
obj-$(CONFIG_DRM_SHMOBILE) +=shmobile/
obj-y += omapdrm/
diff --git a/drivers/gpu/drm/atmel/Kconfig b/drivers/gpu/drm/atmel/Kconfig
index 5f67f001553b..1405ae2802a3 100644
--- a/drivers/gpu/drm/atmel/Kconfig
+++ b/drivers/gpu/drm/atmel/Kconfig
@@ -9,3 +9,15 @@ config DRM_ATMEL_HLCDC
help
Choose this option if you have an ATMEL SoC with an HLCDC display
controller (i.e. at91sam9n12, at91sam9x5 family or sama5d3 family).
+
+config DRM_ATMEL_LCDC
+ tristate "DRM Support for ATMEL LCDC Display Controller"
+ depends on DRM && OF && COMMON_CLK && MFD_ATMEL_LCDC
+ depends on ARM || COMPILE_TEST
+ select DRM_GEM_CMA_HELPER
+ select DRM_KMS_HELPER
+ select DRM_KMS_CMA_HELPER
+ select DRM_PANEL
+ help
+ Choose this option if you have an ATMEL SoC with an LCDC display
+ controller (found on many SoC's in the at91sam9 family).
diff --git a/drivers/gpu/drm/atmel/Makefile b/drivers/gpu/drm/atmel/Makefile
index 49dc89f36b73..9d95543f39a3 100644
--- a/drivers/gpu/drm/atmel/Makefile
+++ b/drivers/gpu/drm/atmel/Makefile
@@ -5,3 +5,6 @@ atmel-hlcdc-dc-y := atmel_hlcdc_crtc.o \
atmel_hlcdc_plane.o
obj-$(CONFIG_DRM_ATMEL_HLCDC) += atmel-hlcdc-dc.o
+
+atmel-lcdc-y := atmel_lcdc_drv.o atmel_lcdc_pipe.o
+obj-$(CONFIG_DRM_ATMEL_LCDC) += atmel-lcdc.o
diff --git a/drivers/gpu/drm/atmel/atmel_lcdc.h b/drivers/gpu/drm/atmel/atmel_lcdc.h
new file mode 100644
index 000000000000..e1b3290c3c81
--- /dev/null
+++ b/drivers/gpu/drm/atmel/atmel_lcdc.h
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2018 Sam Ravnborg
+ *
+ * Author: Sam Ravnborg <sam@ravnborg.org>
+ */
+
+#ifndef __DRM_ATMEL_LCDC_H
+#define __DRM_ATMEL_LCDC_H
+
+#include <linux/workqueue.h>
+
+#include <drm/drm_connector.h>
+#include <drm/drm_simple_kms_helper.h>
+
+#include <linux/mfd/atmel-lcdc.h>
+
+struct clk;
+struct regmap;
+struct regulator;
+
+#define ATMEL_LCDC_DMA_BURST_LEN 8 /* words */
+
+/*
+ * struct atmel_lcdc_desc - CPU specific configuration properties
+ */
+struct atmel_lcdc_desc {
+ /* Default is 1, maybe we need to change this later */
+ int guard_time;
+
+ /* 512 or 2018 */
+ int fifo_size;
+
+ /* Maximum resolution */
+ int max_width;
+ int max_height;
+
+ /* false if "Pixel_clock = system_clock / (CLKVAL + 1) x 2" */
+ /* true if "Pixel_clock = system_clock / (CLKVAL + 1)" */
+ bool have_alt_pixclock;
+};
+
+/*
+ * Private data for the lcdc driver
+ */
+struct lcdc {
+ struct drm_device drm;
+ struct device *dev;
+
+ const struct atmel_lcdc_desc *desc;
+
+ struct atmel_mfd_lcdc *mfd;
+
+ bool wiring_reversed; /* Select between BGR or RGB */
+
+ struct drm_simple_display_pipe pipe;
+ struct work_struct reset_lcdc_work;
+
+ struct drm_panel *panel;
+ struct drm_connector *connector;
+
+ struct mutex enable_lock;
+ bool enabled;
+
+ /* Saved state during suspend */
+ struct {
+ u32 imr;
+ /* true if suspended */
+ bool state;
+ } suspend;
+};
+
+/* atmel_lcdc_pipe.c */
+int atmel_lcdc_vblank_init(struct lcdc *lcdc);
+void atmel_lcdc_vblank(struct lcdc *ldcd);
+
+void atmel_lcdc_start(struct lcdc *lcdc);
+void atmel_lcdc_stop(struct lcdc *lcdc);
+int atmel_lcdc_modeset_init(struct lcdc *lcdc);
+
+#endif /* __DRM_ATMEL_LCDC_H */
diff --git a/drivers/gpu/drm/atmel/atmel_lcdc_drv.c b/drivers/gpu/drm/atmel/atmel_lcdc_drv.c
new file mode 100644
index 000000000000..2c94761ea648
--- /dev/null
+++ b/drivers/gpu/drm/atmel/atmel_lcdc_drv.c
@@ -0,0 +1,408 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2018 Sam Ravnborg
+ *
+ * The driver is based on atmel_lcdfb which is:
+ * Copyright (C) 2007 Atmel Corporation
+ *
+ */
+
+/**
+ * DOC: Atmel LCD Controller Display Controller.
+ *
+ * The Atmel LCD Controller supports in the following configuration:
+ * - TFT only, 24 bits/pixel
+ * - Resolution up to 2048x2048
+ * - Single plane, crtc, one fixed output
+ *
+ * Features not (yet) ported from atmel_lcdfb:
+ * - set color / palette handling
+ */
+
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/platform_device.h>
+#include <linux/regulator/consumer.h>
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_crtc_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fb_helper.h>
+#include <drm/drm_gem_cma_helper.h>
+#include <drm/drm_irq.h>
+#include <drm/drm_print.h>
+#include <drm/drm_probe_helper.h>
+
+#include "atmel_lcdc.h"
+
+/* Configuration of individual CPU's */
+static const struct atmel_lcdc_desc lcdc_at91sam9261 = {
+ .guard_time = 1,
+ .fifo_size = 512,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = false,
+};
+
+static const struct atmel_lcdc_desc lcdc_at91sam9263 = {
+ .guard_time = 1,
+ .fifo_size = 2048,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = false,
+};
+
+static const struct atmel_lcdc_desc lcdc_at91sam9g10 = {
+ .guard_time = 1,
+ .fifo_size = 512,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = false,
+};
+
+static const struct atmel_lcdc_desc lcdc_at91sam9g45 = {
+ .guard_time = 1,
+ .fifo_size = 512,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = true,
+};
+
+static const struct atmel_lcdc_desc lcdc_at91sam9g46 = {
+ .guard_time = 1,
+ .fifo_size = 512,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = true,
+};
+
+static const struct atmel_lcdc_desc lcdc_at91sam9m10 = {
+ .guard_time = 1,
+ .fifo_size = 512,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = true,
+};
+
+static const struct atmel_lcdc_desc lcdc_at91sam9m11 = {
+ .guard_time = 1,
+ .fifo_size = 512,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = true,
+};
+
+static const struct atmel_lcdc_desc lcdc_at91sam9rl = {
+ .guard_time = 1,
+ .fifo_size = 512,
+ .max_width = 2048,
+ .max_height = 2048,
+ .have_alt_pixclock = false,
+};
+
+static const struct of_device_id lcdc_of_match[] = {
+ { .compatible = "atmel,at91sam9261-lcdc", .data = &lcdc_at91sam9261 },
+ { .compatible = "atmel,at91sam9263-lcdc", .data = &lcdc_at91sam9263 },
+ { .compatible = "atmel,at91sam9g10-lcdc", .data = &lcdc_at91sam9g10 },
+ { .compatible = "atmel,at91sam9g45-lcdc", .data = &lcdc_at91sam9g45 },
+ { .compatible = "atmel,at91sam9g45es-lcdc", .data = &lcdc_at91sam9g45 },
+ { .compatible = "atmel,at91sam9g46-lcdc", .data = &lcdc_at91sam9g46 },
+ { .compatible = "atmel,at91sam9m10-lcdc", .data = &lcdc_at91sam9m10 },
+ { .compatible = "atmel,at91sam9m11-lcdc", .data = &lcdc_at91sam9m11 },
+ { .compatible = "atmel,at91sam9rl-lcdc", .data = &lcdc_at91sam9rl },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, lcdc_of_match);
+
+
+/* scheduled worker to reset LCD */
+static void reset_lcdc_work(struct work_struct *work)
+{
+ struct lcdc *lcdc;
+
+ lcdc = container_of(work, struct lcdc, reset_lcdc_work);
+
+ mutex_lock(&lcdc->enable_lock);
+
+ /* Check that we are enabled and not suspended */
+ if (lcdc->enabled && !lcdc->suspend.state) {
+ atmel_lcdc_stop(lcdc);
+ atmel_lcdc_start(lcdc);
+ }
+ mutex_unlock(&lcdc->enable_lock);
+}
+
+static irqreturn_t lcdc_irq_handler(int irq, void *arg)
+{
+ struct drm_device *drm;
+ unsigned int status;
+ struct lcdc *lcdc;
+ unsigned int imr;
+ unsigned int isr;
+
+ drm = arg;
+ lcdc = drm->dev_private;
+
+ regmap_read(lcdc->mfd->regmap, ATMEL_LCDC_IMR, &imr);
+ regmap_read(lcdc->mfd->regmap, ATMEL_LCDC_ISR, &isr);
+ status = imr & isr;
+ if (!status)
+ return IRQ_NONE;
+
+ if (status & ATMEL_LCDC_LSTLNI)
+ atmel_lcdc_vblank(lcdc);
+
+ if (status & ATMEL_LCDC_UFLWI) {
+ DRM_DEV_INFO(lcdc->dev, "FIFO underflow %#x\n", status);
+ /* reset DMA and FIFO to avoid screen shifting */
+ schedule_work(&lcdc->reset_lcdc_work);
+ }
+
+ if (status & ATMEL_LCDC_OWRI)
+ DRM_DEV_INFO(lcdc->dev, "FIFO overwrite interrupt\n");
+
+ if (status & ATMEL_LCDC_MERI)
+ DRM_DEV_INFO(lcdc->dev, "DMA memory error\n");
+
+ /* Clear all reported (from ISR) interrupts */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_ICR, isr);
+
+ return IRQ_HANDLED;
+}
+
+static int lcdc_irq_postinstall(struct drm_device *dev)
+{
+ struct lcdc *lcdc;
+ unsigned int ier;
+
+ lcdc = dev->dev_private;
+
+ ier = 0;
+ /* FIFO underflow interrupt enable */
+ ier |= ATMEL_LCDC_UFLWI;
+ /* FIFO overwrite interrupt enable */
+ ier |= ATMEL_LCDC_OWRI;
+ /* DMA memory error interrupt enable */
+ ier |= ATMEL_LCDC_MERI;
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_IER, ier);
+
+ return 0;
+}
+
+static void lcdc_irq_uninstall(struct drm_device *dev)
+{
+ struct lcdc *lcdc;
+ unsigned int isr;
+
+ lcdc = dev->dev_private;
+
+ /* disable all interrupts */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_IDR, ~0);
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_ICR, ~0);
+
+ /* Clear any pending interrupts */
+ regmap_read(lcdc->mfd->regmap, ATMEL_LCDC_ISR, &isr);
+}
+
+static void lcdc_release(struct drm_device *drm)
+{
+ struct lcdc *lcdc;
+
+ lcdc = drm->dev_private;
+
+ flush_work(&lcdc->reset_lcdc_work);
+ drm_kms_helper_poll_fini(drm);
+
+ drm_mode_config_cleanup(drm);
+
+ pm_runtime_get_sync(drm->dev);
+ drm_irq_uninstall(drm);
+ pm_runtime_put_sync(drm->dev);
+ cancel_work_sync(&lcdc->reset_lcdc_work);
+ pm_runtime_disable(drm->dev);
+
+ drm_dev_fini(drm);
+ kfree(lcdc);
+}
+
+DEFINE_DRM_GEM_CMA_FOPS(lcdc_drm_fops);
+
+static struct drm_driver lcdc_drm_driver = {
+ .driver_features = DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
+ .name = ATMEL_LCDC_DRM_DRV_NAME,
+ .desc = "Atmel LCD Display Controller DRM",
+ .date = "20180808",
+ .major = 1,
+ .minor = 0,
+ .patchlevel = 0,
+
+ .irq_handler = lcdc_irq_handler,
+ .irq_preinstall = lcdc_irq_uninstall,
+ .irq_postinstall = lcdc_irq_postinstall,
+ .irq_uninstall = lcdc_irq_uninstall,
+
+ .fops = &lcdc_drm_fops,
+ .release = lcdc_release,
+
+ DRM_GEM_CMA_VMAP_DRIVER_OPS,
+};
+
+static int lcdc_probe(struct platform_device *pdev)
+{
+ const struct of_device_id *match;
+ struct atmel_mfd_lcdc *mfd;
+ struct drm_device *drm;
+ struct device *dev;
+ struct lcdc *lcdc;
+ int ret;
+
+ dev = pdev->dev.parent;
+ mfd = dev_get_drvdata(dev);
+ /* the MFD device driver prepares clocks etc. */
+ if (!mfd)
+ return -EPROBE_DEFER;
+
+
+ lcdc = kzalloc(sizeof(*lcdc), GFP_KERNEL);
+ if (!lcdc)
+ return -ENOMEM;
+
+ drm = &lcdc->drm;
+ ret = devm_drm_dev_init(dev, drm, &lcdc_drm_driver);
+ if (ret) {
+ kfree(lcdc);
+ return ret;
+ }
+
+ match = of_match_node(lcdc_of_match, dev->of_node);
+ if (!match || !match->data) {
+ DRM_DEV_ERROR(dev, "failed to find compatible match\n");
+ return -EINVAL;
+ }
+
+ lcdc->desc = match->data;
+
+ drm->dev_private = lcdc;
+ lcdc->dev = dev;
+ lcdc->mfd = mfd;
+ platform_set_drvdata(pdev, drm);
+
+ mutex_init(&lcdc->enable_lock);
+
+ /* reset of lcdc might sleep and require a preemptible task context */
+ INIT_WORK(&lcdc->reset_lcdc_work, reset_lcdc_work);
+
+ pm_runtime_enable(dev);
+
+ ret = atmel_lcdc_vblank_init(lcdc);
+ if (ret)
+ goto err_pm_runtime_disable;
+
+ ret = atmel_lcdc_modeset_init(lcdc);
+ if (ret)
+ goto err_pm_runtime_disable;
+
+ pm_runtime_get_sync(dev);
+
+ ret = drm_irq_install(drm, mfd->irq);
+ pm_runtime_put_sync(dev);
+ if (ret < 0) {
+ DRM_DEV_ERROR(dev, "failed to install IRQ: %d\n", ret);
+ goto err_pm_runtime_disable;
+ }
+
+ drm_kms_helper_poll_init(drm);
+
+ ret = drm_dev_register(drm, 0);
+ if (ret) {
+ DRM_DEV_ERROR(dev, "failed to register drm: %d\n", ret);
+ goto err_pm_runtime_disable;
+ }
+
+ ret = drm_fbdev_generic_setup(drm, 24);
+ if (ret < 0)
+ DRM_DEV_ERROR(dev, "failed to initialize fbdev: %d\n", ret);
+
+ return 0;
+
+err_pm_runtime_disable:
+ cancel_work_sync(&lcdc->reset_lcdc_work);
+ pm_runtime_disable(dev);
+
+ return ret;
+}
+
+static int lcdc_remove(struct platform_device *pdev)
+{
+ struct drm_device *drm;
+
+ drm = platform_get_drvdata(pdev);
+ drm_dev_unregister(drm);
+ drm_atomic_helper_shutdown(drm);
+
+ return 0;
+}
+
+static void lcdc_shutdown(struct platform_device *pdev)
+{
+ drm_atomic_helper_shutdown(platform_get_drvdata(pdev));
+}
+
+static int __maybe_unused lcdc_drm_suspend(struct device *dev)
+{
+ struct drm_device *drm;
+ struct lcdc *lcdc;
+ int ret;
+
+ drm = dev_get_drvdata(dev);
+ lcdc = drm->dev_private;
+
+ ret = drm_mode_config_helper_suspend(drm);
+ if (ret < 0)
+ return ret;
+
+ lcdc->suspend.state = true;
+
+ /* Disable interrupts */
+ regmap_read(lcdc->mfd->regmap, ATMEL_LCDC_IMR, &lcdc->suspend.imr);
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_IDR, lcdc->suspend.imr);
+
+ return 0;
+}
+
+static int __maybe_unused lcdc_drm_resume(struct device *dev)
+{
+ struct drm_device *drm;
+ struct lcdc *lcdc;
+
+ drm = dev_get_drvdata(dev);
+ lcdc = drm->dev_private;
+
+ /* Enable interrupts */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_IER, lcdc->suspend.imr);
+
+ lcdc->suspend.state = false;
+ return drm_mode_config_helper_resume(drm);
+}
+
+static SIMPLE_DEV_PM_OPS(lcdc_pm_ops,
+ lcdc_drm_suspend, lcdc_drm_resume);
+
+static struct platform_driver lcdc_driver = {
+ .driver = {
+ .name = ATMEL_LCDC_DRM_DRV_NAME,
+ .of_match_table = lcdc_of_match,
+ .pm = &lcdc_pm_ops,
+ },
+ .probe = lcdc_probe,
+ .remove = lcdc_remove,
+ .shutdown = lcdc_shutdown,
+};
+
+module_platform_driver(lcdc_driver);
+
+MODULE_AUTHOR("Sam Ravnborg <sam@ravnborg.org>");
+MODULE_DESCRIPTION("Atmel LCDC Display Controller DRM Driver");
+MODULE_LICENSE("GPL v2");
+MODULE_ALIAS("platform:atmel-lcdc");
diff --git a/drivers/gpu/drm/atmel/atmel_lcdc_pipe.c b/drivers/gpu/drm/atmel/atmel_lcdc_pipe.c
new file mode 100644
index 000000000000..c40175cd1e1f
--- /dev/null
+++ b/drivers/gpu/drm/atmel/atmel_lcdc_pipe.c
@@ -0,0 +1,675 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2018 Sam Ravnborg
+ *
+ */
+
+#include <linux/regulator/consumer.h>
+#include <linux/pm_runtime.h>
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_device.h>
+#include <drm/drm_fb_cma_helper.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_of.h>
+#include <drm/drm_panel.h>
+#include <drm/drm_print.h>
+#include <drm/drm_vblank.h>
+
+#include "atmel_lcdc.h"
+
+/**
+ * DOC: Display pipe for Atmel LCDC
+ *
+ * The LCDC display pipe utilize the drm_simple_kms_helper infrastructure
+ * to provide the necessary DRM functionality.
+ *
+ * The LCDC IP core is always used in setups with only a single
+ * output with direct connection to a panel.
+ */
+
+/*
+ * Atmel LCD controller 24 bit formats.
+ * Formats for reversed wiring included.
+ */
+static const u32 bgr_formats[] = {
+ DRM_FORMAT_XBGR8888,
+ DRM_FORMAT_BGR888,
+ DRM_FORMAT_BGR565,
+};
+
+static const u32 rgb_formats[] = {
+ DRM_FORMAT_XRGB8888,
+ DRM_FORMAT_RGB888,
+ DRM_FORMAT_RGB565,
+};
+
+static const u32* get_formats(bool reversed, size_t *nformats)
+{
+ if (reversed) {
+ /* B and R are swapped in HW */
+ *nformats = ARRAY_SIZE(rgb_formats);
+ return rgb_formats;
+ } else {
+ /* Normal wiring, use BGR formats */
+ *nformats = ARRAY_SIZE(bgr_formats);
+ return bgr_formats;
+ }
+}
+
+static void set_lcdcon2(struct lcdc *lcdc,
+ const struct drm_format_info *format)
+{
+ struct drm_format_name_buf format_buf;
+ struct drm_display_mode *dmode;
+ unsigned int lcdcon2;
+ u32 bus_flags;
+
+ dmode = &lcdc->pipe.crtc.state->adjusted_mode;
+
+ /* Control register 2 */
+ /* Only TFT supported (Controller supports STN too) */
+ lcdcon2 = ATMEL_LCDC_DISTYPE_TFT;
+
+ /* scan mode */
+ if (dmode->flags & DRM_MODE_FLAG_DBLSCAN)
+ lcdcon2 |= ATMEL_LCDC_SCANMOD_DUAL;
+ else
+ lcdcon2 |= ATMEL_LCDC_SCANMOD_SINGLE;
+
+ /* Interface width 4 bits (STN only) */
+ lcdcon2 |= ATMEL_LCDC_IFWIDTH_4;
+
+ switch (format->format) {
+ case DRM_FORMAT_XBGR8888:
+ case DRM_FORMAT_XRGB8888:
+ /* 32 bit layout */
+ lcdcon2 |= ATMEL_LCDC_PIXELSIZE_24;
+ break;
+ case DRM_FORMAT_BGR888:
+ case DRM_FORMAT_RGB888:
+ /* 24 bit layout */
+ lcdcon2 |= ATMEL_LCDC_PIXELSIZE_24_PACKED;
+ break;
+ case DRM_FORMAT_BGR565:
+ case DRM_FORMAT_RGB565:
+ /* 16 bit layout */
+ lcdcon2 |= ATMEL_LCDC_PIXELSIZE_16;
+ break;
+ default:
+ lcdcon2 |= ATMEL_LCDC_PIXELSIZE_24;
+ DRM_DEV_INFO(lcdc->dev, "Unsupported format %s\n",
+ drm_get_format_name(format->format, &format_buf));
+ }
+
+ /* LCDD Polarity normal */
+ lcdcon2 |= ATMEL_LCDC_INVVD_NORMAL;
+
+ /* vsync polarity - TODO: Check ! again */
+ if (!(dmode->flags & DRM_MODE_FLAG_PVSYNC))
+ lcdcon2 |= ATMEL_LCDC_INVFRAME_INVERTED;
+ else
+ lcdcon2 |= ATMEL_LCDC_INVFRAME_NORMAL;
+
+ /* hsync polarity - TODO: Check ! again*/
+ if (!(dmode->flags & DRM_MODE_FLAG_PHSYNC))
+ lcdcon2 |= ATMEL_LCDC_INVLINE_INVERTED;
+ else
+ lcdcon2 |= ATMEL_LCDC_INVLINE_NORMAL;
+
+ bus_flags = lcdc->connector->display_info.bus_flags;
+
+ /* dot clock (pix clock) polarity */
+ if (bus_flags & DRM_BUS_FLAG_PIXDATA_NEGEDGE)
+ lcdcon2 |= ATMEL_LCDC_INVCLK_INVERTED;
+ else
+ lcdcon2 |= ATMEL_LCDC_INVCLK_NORMAL;
+
+ /* Date Enable polarity */
+ if (bus_flags & DRM_BUS_FLAG_DE_LOW)
+ lcdcon2 |= ATMEL_LCDC_INVDVAL_INVERTED;
+ else
+ lcdcon2 |= ATMEL_LCDC_INVDVAL_NORMAL;
+
+ /* Clock is always active */
+ lcdcon2 |= ATMEL_LCDC_CLKMOD_ALWAYSACTIVE;
+
+ /* Memory layout */
+ if (format->format & DRM_FORMAT_BIG_ENDIAN)
+ lcdcon2 |= ATMEL_LCDC_MEMOR_BIG;
+ else
+ lcdcon2 |= ATMEL_LCDC_MEMOR_LITTLE;
+
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_LCDCON2, lcdcon2);
+}
+
+/* Check that the configuation is supported */
+static int lcdc_pipe_check(struct drm_simple_display_pipe *pipe,
+ struct drm_plane_state *pstate,
+ struct drm_crtc_state *cstate)
+{
+ const struct drm_display_mode *dmode;
+ struct drm_framebuffer *fb;
+ struct lcdc *lcdc;
+ long clk_rate;
+
+ lcdc = pipe->crtc.dev->dev_private;
+ dmode = &cstate->mode;
+ fb = pstate->fb;
+
+ if (!fb)
+ return 0;
+
+ /* Check if we can support the requested clock rate */
+ clk_rate = dmode->clock * 1000;
+ if (clk_rate > clk_get_rate(lcdc->mfd->lcdc_clk))
+ return -EINVAL;
+
+ /* Only one plane supported */
+ if (fb->format->num_planes != 1)
+ return -EINVAL;
+
+ return 0;
+}
+
+/*
+ * DMA config. Set frame size and burst length
+ * Frame_size equals size of visible area * bits / 32
+ * (size in 32 bit words)
+ */
+static void lcdc_pipe_enable_dma(struct lcdc *lcdc)
+{
+ const struct drm_format_info *format_info;
+ struct drm_plane_state *plane_state;
+ unsigned int width, height;
+ unsigned int frame_size;
+ unsigned int dmafrmcfg;
+
+ plane_state = lcdc->pipe.plane.state;
+ format_info = drm_format_info(plane_state->fb->format->format);
+ width = plane_state->crtc->state->adjusted_mode.hdisplay;
+ height = plane_state->crtc->state->adjusted_mode.vdisplay;
+
+ frame_size = width * height * format_info->depth;
+ dmafrmcfg = frame_size / 32;
+
+ dmafrmcfg |= (ATMEL_LCDC_DMA_BURST_LEN - 1) << ATMEL_LCDC_BLENGTH_OFFSET;
+
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_DMAFRMCFG, dmafrmcfg);
+}
+
+static void set_vertical_timing(struct lcdc *lcdc,
+ struct drm_display_mode *dmode)
+{
+ unsigned int tim1;
+ unsigned int vfp;
+ unsigned int vbp;
+ unsigned int vpw;
+ unsigned int vhdly;
+
+ /* VFP: Vertical Front Porch */
+ vfp = dmode->vsync_start - dmode->vdisplay;
+
+ /* VBP: Vertical Back Porch */
+ vbp = dmode->vtotal - dmode->vsync_end;
+
+ /* VPW: Vertical Synchronization pulse width */
+ vpw = dmode->vsync_end - dmode->vsync_start - 1;
+
+ /* VHDLY: Vertical to horizontal delay */
+ vhdly = 0;
+
+ tim1 = vfp << ATMEL_LCDC_VFP_OFFSET |
+ vbp << ATMEL_LCDC_VBP_OFFSET |
+ vpw << ATMEL_LCDC_VPW_OFFSET |
+ vhdly << ATMEL_LCDC_VHDLY_OFFSET;
+
+ DRM_DEV_DEBUG(lcdc->dev, " TIM1 = %08x\n", tim1);
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_TIM1, tim1);
+}
+
+static void set_horizontal_timing(struct lcdc *lcdc,
+ struct drm_display_mode *dmode)
+{
+ unsigned int tim2;
+ unsigned int hbp;
+ unsigned int hpw;
+ unsigned int hfp;
+
+ /* HBP: Horizontal Back Porch */
+ hbp = dmode->htotal - dmode->hsync_end - 1;
+
+ /* HPW: Horizontal synchronization pulse width */
+ hpw = dmode->hsync_end - dmode->hsync_start - 1;
+
+ /* HFP: Horizontal Front Porch */
+ hfp = dmode->hsync_start - dmode->hdisplay - 2;
+
+ tim2 = hbp << ATMEL_LCDC_HBP_OFFSET |
+ hpw << ATMEL_LCDC_HPW_OFFSET |
+ hfp << ATMEL_LCDC_HFP_OFFSET;
+
+ DRM_DEV_DEBUG(lcdc->dev, " TIM2 = %08x\n", tim2);
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_TIM2, tim2);
+}
+
+static void lcdc_pipe_enable_timing(struct lcdc *lcdc)
+{
+ struct drm_display_mode *dmode;
+ unsigned int value;
+
+ dmode = &lcdc->pipe.crtc.state->adjusted_mode;
+
+ /* Vertical & horizontal timing */
+ set_vertical_timing(lcdc, dmode);
+ set_horizontal_timing(lcdc, dmode);
+
+ /* Display size */
+ value = (dmode->crtc_hdisplay - 1) << ATMEL_LCDC_HOZVAL_OFFSET;
+ value |= dmode->crtc_vdisplay - 1;
+ DRM_DEV_DEBUG(lcdc->dev, " LCDFRMCFG = %08x\n", value);
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_LCDFRMCFG, value);
+
+ /* FIFO Threshold: Use formula from data sheet */
+ value = lcdc->desc->fifo_size - (2 * ATMEL_LCDC_DMA_BURST_LEN + 3);
+ DRM_DEV_DEBUG(lcdc->dev, " FIFO = %08x\n", value);
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_FIFO, value);
+
+ /*
+ * Toggle LCD_MODE every frame
+ * Note: register not documented, this is from atmel_lcdfb
+ */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_MVAL, 0);
+}
+
+static void lcdc_pipe_enable_ctrl(struct lcdc *lcdc)
+{
+ const struct drm_format_info *format;
+ struct drm_display_mode *dmode;
+ unsigned long clk_value_khz;
+ unsigned int pix_factor;
+ unsigned int lcdcon1;
+
+ format = lcdc->pipe.crtc.primary->state->fb->format;
+ dmode = &lcdc->pipe.crtc.state->adjusted_mode;
+
+ /* LCDC Control register 1 */
+ /* Set pixel clock */
+ if (lcdc->desc->have_alt_pixclock)
+ pix_factor = 1;
+ else
+ pix_factor = 2;
+
+ clk_value_khz = clk_get_rate(lcdc->mfd->lcdc_clk) / 1000;
+ lcdcon1 = DIV_ROUND_UP(clk_value_khz, dmode->clock);
+
+ if (lcdcon1 < pix_factor) {
+ DRM_DEV_INFO(lcdc->dev, "Bypassing pixel clock divider\n");
+ regmap_write(lcdc->mfd->regmap,
+ ATMEL_LCDC_LCDCON1, ATMEL_LCDC_BYPASS);
+ } else {
+
+ lcdcon1 = (lcdcon1 / pix_factor) - 1;
+ DRM_DEV_DEBUG(lcdc->dev, "CLKVAL = 0x%08x\n", lcdcon1);
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_LCDCON1,
+ lcdcon1 << ATMEL_LCDC_CLKVAL_OFFSET);
+ dmode->clock = clk_value_khz / (pix_factor * (lcdcon1 + 1));
+ DRM_DEV_DEBUG(lcdc->dev, "updated pixclk: %u KHz\n",
+ dmode->clock);
+ }
+
+ /* LCDC Control register 2 */
+ set_lcdcon2(lcdc, format);
+}
+
+static void lcdc_pipe_enable(struct drm_simple_display_pipe *pipe,
+ struct drm_crtc_state *cstate,
+ struct drm_plane_state *plane_state)
+{
+ struct drm_device *drm;
+ struct drm_crtc *crtc;
+ struct lcdc *lcdc;
+
+ crtc = &pipe->crtc;
+ drm = crtc->dev;
+ lcdc = drm->dev_private;
+
+ mutex_lock(&lcdc->enable_lock);
+ pm_runtime_get_sync(drm->dev);
+ pm_runtime_forbid(drm->dev);
+
+ lcdc_pipe_enable_timing(lcdc);
+ lcdc_pipe_enable_dma(lcdc);
+ lcdc_pipe_enable_ctrl(lcdc);
+ atmel_lcdc_start(lcdc);
+ drm_crtc_vblank_on(&lcdc->pipe.crtc);
+
+ pm_runtime_put_sync(drm->dev);
+
+ lcdc->enabled = true;
+ mutex_unlock(&lcdc->enable_lock);
+}
+
+static void lcdc_pipe_disable(struct drm_simple_display_pipe *pipe)
+{
+ struct drm_device *drm;
+ struct drm_crtc *crtc;
+ struct lcdc *lcdc;
+
+ crtc = &pipe->crtc;
+ drm = crtc->dev;
+ lcdc = drm->dev_private;
+
+ mutex_lock(&lcdc->enable_lock);
+ pm_runtime_get_sync(drm->dev);
+
+ drm_crtc_vblank_off(crtc);
+ atmel_lcdc_stop(lcdc);
+
+ pm_runtime_allow(drm->dev);
+ pm_runtime_put_sync(drm->dev);
+
+ lcdc->enabled = false;
+ mutex_unlock(&lcdc->enable_lock);
+}
+
+
+static void lcdc_pipe_update_format(struct lcdc *lcdc,
+ struct drm_simple_display_pipe *pipe)
+{
+ struct drm_plane_state *plane_state;
+ struct drm_framebuffer *fb;
+
+ plane_state = pipe->plane.state;
+ fb = plane_state->fb;
+
+ if (fb)
+ set_lcdcon2(lcdc, fb->format);
+}
+
+/* Update DMA addr */
+static void lcdc_pipe_update_dma(struct lcdc *lcdc,
+ struct drm_simple_display_pipe *pipe)
+{
+ struct drm_plane_state *plane_state;
+ struct drm_framebuffer *fb;
+ dma_addr_t dma_addr;
+
+ plane_state = pipe->plane.state;
+ fb = plane_state->fb;
+
+ if (fb) {
+ dma_addr = drm_fb_cma_get_gem_addr(fb, pipe->plane.state, 0);
+
+ /* Set frame buffer DMA base address */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_DMABADDR1, dma_addr);
+ }
+}
+
+static void lcdc_pipe_update_event(struct drm_simple_display_pipe *pipe)
+{
+ struct drm_pending_vblank_event *event;
+ struct drm_crtc *crtc;
+
+ crtc = &pipe->crtc;
+ if (!crtc)
+ return;
+
+ spin_lock_irq(&crtc->dev->event_lock);
+ event = crtc->state->event;
+ if (event) {
+ crtc->state->event = NULL;
+
+ if (drm_crtc_vblank_get(crtc) == 0)
+ drm_crtc_arm_vblank_event(crtc, event);
+ else
+ drm_crtc_send_vblank_event(crtc, event);
+ }
+ spin_unlock_irq(&crtc->dev->event_lock);
+}
+
+static void lcdc_pipe_update(struct drm_simple_display_pipe *pipe,
+ struct drm_plane_state *old_pstate)
+{
+ struct lcdc *lcdc;
+
+ lcdc = pipe->crtc.dev->dev_private;
+
+ /* Format management */
+ lcdc_pipe_update_format(lcdc, pipe);
+
+ /* DMA engine... */
+ lcdc_pipe_update_dma(lcdc, pipe);
+
+ /* vblank event handling */
+ lcdc_pipe_update_event(pipe);
+}
+
+static int lcdc_pipe_enable_vblank(struct drm_simple_display_pipe *pipe)
+{
+ struct drm_crtc *crtc;
+ struct lcdc *lcdc;
+
+ crtc = &pipe->crtc;
+ lcdc = crtc->dev->dev_private;
+
+ /* Last line interrupt enable */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_IER, ATMEL_LCDC_LSTLNI);
+
+ return 0;
+}
+
+static void lcdc_pipe_disable_vblank(struct drm_simple_display_pipe *pipe)
+{
+ struct drm_crtc *crtc;
+ struct lcdc *lcdc;
+
+ crtc = &pipe->crtc;
+ lcdc = crtc->dev->dev_private;
+
+ /* Last line interrupt disable */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_IDR, ATMEL_LCDC_LSTLNI);
+}
+
+const struct drm_simple_display_pipe_funcs lcdc_display_funcs = {
+ .check = lcdc_pipe_check,
+ .enable = lcdc_pipe_enable,
+ .disable = lcdc_pipe_disable,
+ .update = lcdc_pipe_update,
+ .prepare_fb = drm_gem_fb_simple_display_pipe_prepare_fb,
+ .enable_vblank = lcdc_pipe_enable_vblank,
+ .disable_vblank = lcdc_pipe_disable_vblank,
+};
+
+static int lcdc_get_of_wiring(struct lcdc *lcdc,
+ const struct device_node *ep)
+{
+ const char *str;
+ int ret;
+
+ // HACK
+ lcdc->wiring_reversed = true;
+
+ ret = of_property_read_string(ep, "wiring", &str);
+ if (ret)
+ return ret;
+
+ if (strcmp(str, "red-green-reversed") == 0) {
+ lcdc->wiring_reversed = true;
+ } else if (strcmp(str, "straight") == 0) {
+ /* Use default format */
+ } else {
+ DRM_DEV_ERROR(lcdc->dev, "unknown \"wiring\" property: %s\n",
+ str);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int lcdc_display_init(struct lcdc *lcdc)
+{
+ const u32 *formats;
+ size_t nformats;
+ int ret;
+
+ formats = get_formats(lcdc->wiring_reversed, &nformats);
+
+ ret = drm_simple_display_pipe_init(&lcdc->drm, &lcdc->pipe,
+ &lcdc_display_funcs,
+ formats, nformats,
+ NULL, NULL);
+ if (ret < 0)
+ DRM_DEV_ERROR(lcdc->dev, "failed to init display pipe: %d\n",
+ ret);
+
+ return ret;
+}
+
+static int lcdc_attach_panel(struct lcdc *lcdc)
+{
+ struct drm_bridge *bridge;
+ struct drm_panel *panel;
+ struct device_node *np;
+ struct device *dev;
+ int ret;
+
+ dev = lcdc->dev;
+ np = dev->of_node;
+ ret = drm_of_find_panel_or_bridge(np, 0, 0, &panel, NULL);
+ if (ret < 0)
+ return ret;
+
+ if (!panel) {
+ DRM_DEV_ERROR(dev, "no panel found\n");
+ return -ENODEV;
+ }
+
+ lcdc->panel = panel;
+ bridge = devm_drm_panel_bridge_add(dev, panel, DRM_MODE_CONNECTOR_DPI);
+ ret = PTR_ERR_OR_ZERO(bridge);
+ if (ret < 0) {
+ DRM_DEV_ERROR(dev, "failed to add bridge: %d\n", ret);
+ return ret;
+ }
+
+ ret = lcdc_display_init(lcdc);
+ if (ret < 0)
+ return ret;
+
+ ret = drm_simple_display_pipe_attach_bridge(&lcdc->pipe, bridge);
+ if (ret < 0)
+ DRM_DEV_ERROR(dev, "failed to attach bridge: %d\n", ret);
+
+ lcdc->connector = panel->connector;
+ return ret;
+}
+
+static int lcdc_create_output(struct lcdc *lcdc)
+{
+ struct device_node *endpoint;
+ struct device_node *np;
+ int ret;
+
+ /* port@0/endpoint@0 is the only port/endpoint */
+ np = lcdc->dev->of_node;
+ endpoint = of_graph_get_endpoint_by_regs(np, 0, 0);
+ if (!endpoint) {
+ DRM_DEV_ERROR(lcdc->dev, "failed to find endpoint node\n");
+ return -ENODEV;
+ }
+
+ lcdc_get_of_wiring(lcdc, endpoint);
+ of_node_put(endpoint);
+
+ ret = lcdc_attach_panel(lcdc);
+
+ return ret;
+}
+
+static const struct drm_mode_config_funcs mode_config_funcs = {
+ .fb_create = drm_gem_fb_create,
+ .atomic_check = drm_atomic_helper_check,
+ .atomic_commit = drm_atomic_helper_commit,
+};
+
+int atmel_lcdc_modeset_init(struct lcdc *lcdc)
+{
+ struct drm_device *drm;
+ struct device *dev;
+ int ret;
+
+ drm = &lcdc->drm;
+ dev = drm->dev;
+
+ drm_mode_config_init(drm);
+ ret = lcdc_create_output(lcdc);
+
+ if (ret) {
+ drm_mode_config_cleanup(drm);
+ return ret;
+ }
+
+ drm->mode_config.min_width = 0;
+ drm->mode_config.min_height = 0;
+ drm->mode_config.max_width = lcdc->desc->max_width;
+ drm->mode_config.max_height = lcdc->desc->max_height;
+ drm->mode_config.funcs = &mode_config_funcs;
+ drm->mode_config.quirk_addfb_rgb_to_bgr = !lcdc->wiring_reversed;
+
+ drm_mode_config_reset(drm);
+
+ return 0;
+}
+
+int atmel_lcdc_vblank_init(struct lcdc *lcdc)
+{
+ int ret;
+
+ ret = drm_vblank_init(&lcdc->drm, 1);
+ if (ret)
+ DRM_DEV_ERROR(lcdc->dev, "vblank init failed: %d\n", ret);
+
+ return ret;
+}
+
+void atmel_lcdc_vblank(struct lcdc *lcdc)
+{
+ drm_crtc_handle_vblank(&lcdc->pipe.crtc);
+}
+
+/*
+ * Start LCD Controller (DMA + PWR)
+ * Caller must hold enable_lock
+ */
+void atmel_lcdc_start(struct lcdc *lcdc)
+{
+ /* Enable DMA */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_DMACON, ATMEL_LCDC_DMAEN);
+ /* Enable LCD */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_PWRCON,
+ (lcdc->desc->guard_time << ATMEL_LCDC_GUARDT_OFFSET)
+ | ATMEL_LCDC_PWR);
+}
+
+/*
+ * Stop LCD Controller (PWR + DMA)
+ * Caller must hold enable_lock
+ */
+void atmel_lcdc_stop(struct lcdc *lcdc)
+{
+ unsigned int pwrcon;
+
+ might_sleep();
+
+ /* Turn off the LCD controller and the DMA controller */
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_PWRCON,
+ lcdc->desc->guard_time << ATMEL_LCDC_GUARDT_OFFSET);
+
+ regmap_write(lcdc->mfd->regmap, ATMEL_LCDC_DMACON, !(ATMEL_LCDC_DMAEN));
+
+ /* Wait for the LCDC core to become idle */
+ regmap_read_poll_timeout(lcdc->mfd->regmap, ATMEL_LCDC_PWRCON, pwrcon,
+ !(pwrcon & ATMEL_LCDC_BUSY), 100, 10000);
+}
--
2.20.1
_______________________________________________
dri-devel mailing list
dri-devel@lists.freedesktop.org
https://lists.freedesktop.org/mailman/listinfo/dri-devel
next prev parent reply other threads:[~2019-07-26 20:44 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2019-07-26 20:36 My penguin has blue feets (aka: RGB versus BGR troubles) Sam Ravnborg
2019-07-26 20:44 ` Sam Ravnborg [this message]
2019-07-26 21:35 ` Ilia Mirkin
2019-07-26 22:22 ` Daniel Vetter
2019-07-27 10:12 ` Sam Ravnborg
2019-07-27 10:50 ` Sam Ravnborg
2019-07-27 9:56 ` Sam Ravnborg
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=20190726204413.GA32705@ravnborg.org \
--to=sam@ravnborg.org \
--cc=dri-devel@lists.freedesktop.org \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.