diff mbox series

[RFC] drm: add Atmel LCDC display controller support

Message ID 20190726204413.GA32705@ravnborg.org (mailing list archive)
State New, archived
Headers show
Series [RFC] drm: add Atmel LCDC display controller support | expand

Commit Message

Sam Ravnborg July 26, 2019, 8:44 p.m. UTC
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 mbox series

Patch

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);
+}