From 1a8de735c0b99b2d9aa538603431e63bf121bf0a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20Jirman?= <megi@xff.cz>
Date: Tue, 24 Sep 2019 17:53:02 +0200
Subject: video: fbdev: eInk display driver for A13 based PocketBooks

This driver uses DEBE and TCON0 to bitbang the ED060XD4 eInk panel
protocol on LCD data pins. There's no other sane way to do it,
as PocketBook e-book reader was designed this way.

Signed-off-by: Ondrej Jirman <megi@xff.cz>
---
 drivers/video/fbdev/Kconfig           |   12 +
 drivers/video/fbdev/Makefile          |    4 +
 drivers/video/fbdev/sun5i-eink-neon.c |   49 ++
 drivers/video/fbdev/sun5i-eink.c      | 1156 +++++++++++++++++++++++++
 4 files changed, 1221 insertions(+)
 create mode 100644 drivers/video/fbdev/sun5i-eink-neon.c
 create mode 100644 drivers/video/fbdev/sun5i-eink.c

diff --git a/drivers/video/fbdev/Kconfig b/drivers/video/fbdev/Kconfig
index 55c6686f091e..5685c65da340 100644
--- a/drivers/video/fbdev/Kconfig
+++ b/drivers/video/fbdev/Kconfig
@@ -658,6 +658,18 @@ config FB_ATMEL
 	help
 	  This enables support for the AT91 LCD Controller.
 
+config FB_SUN5I_EINK
+	tristate "eInk display Framebuffer Support (A13 based eBook readers)"
+	depends on FB
+	select FB_MODE_HELPERS
+	select FB_CFB_FILLRECT
+	select FB_CFB_COPYAREA
+	select FB_CFB_IMAGEBLIT
+	select BITREVERSE
+	help
+	  This driver supports PocketBook Touch Lux 3 e-paper display.
+
+
 config FB_NVIDIA
 	tristate "nVidia Framebuffer Support"
 	depends on FB && PCI
diff --git a/drivers/video/fbdev/Makefile b/drivers/video/fbdev/Makefile
index b3d12f977c06..36026f5adf6e 100644
--- a/drivers/video/fbdev/Makefile
+++ b/drivers/video/fbdev/Makefile
@@ -126,3 +126,7 @@ obj-$(CONFIG_FB_SIMPLE)           += simplefb.o
 
 # the test framebuffer is last
 obj-$(CONFIG_FB_VIRTUAL)          += vfb.o
+
+obj-$(CONFIG_FB_SUN5I_EINK)       += sun5ieink.o
+sun5ieink-objs = sun5i-eink.o sun5i-eink-neon.o
+CFLAGS_sun5i-eink-neon.o	  += -march=armv7-a -mfloat-abi=softfp -mfpu=neon -ffreestanding -isystem $(shell $(CC) -print-file-name=include)
diff --git a/drivers/video/fbdev/sun5i-eink-neon.c b/drivers/video/fbdev/sun5i-eink-neon.c
new file mode 100644
index 000000000000..9e2386fd91ea
--- /dev/null
+++ b/drivers/video/fbdev/sun5i-eink-neon.c
@@ -0,0 +1,49 @@
+#include <linux/export.h>
+#include <linux/compiler.h>
+#include <linux/types.h>
+#include <arm_neon.h>
+
+void eink_ctlstream_fill_data_neon(u32* cmd, u8* fb, int stride,
+				   int sources, int gates, u32 masks[4])
+{
+	uint32x4_t mask;
+	int g, s;
+	int cutoff = sources / 4 / 8 / 3 * 2;
+
+	mask = vdupq_n_u32(0);
+
+	for (g = 0; g < gates; g++) {
+		if (unlikely(g == 0))
+			mask = vdupq_n_u32(masks[0]);
+		else
+			mask = vdupq_n_u32(masks[2]);
+
+		for (s = 0; s < sources / 4 / 8; s++) {
+			uint8x8_t src;
+			uint16x8_t dst;
+			uint32x4_t out;
+
+			if (unlikely(s == cutoff))
+				mask = vdupq_n_u32(g == 0 ? masks[1] : masks[3]);
+
+			src = vld1_u8(fb);
+			fb += 8;
+
+			dst = vshll_n_u8(src, 3);
+			dst = vsliq_n_u16(dst, vmovl_u8(vshr_n_u8(src, 3)), 8);
+
+			out = vmovl_u16(vget_low_u16(dst));
+			out = vorrq_u32(out, mask);
+			vst1q_u32(cmd, out);
+			cmd += 4;
+
+			out = vmovl_u16(vget_high_u16(dst));
+			out = vorrq_u32(out, mask);
+			vst1q_u32(cmd, out);
+			cmd += 4;
+		}
+
+		cmd += stride - sources / 4;
+	}
+}
+EXPORT_SYMBOL_GPL(eink_ctlstream_fill_data_neon);
diff --git a/drivers/video/fbdev/sun5i-eink.c b/drivers/video/fbdev/sun5i-eink.c
new file mode 100644
index 000000000000..fbc2cbaedb22
--- /dev/null
+++ b/drivers/video/fbdev/sun5i-eink.c
@@ -0,0 +1,1156 @@
+/*
+ * Copyright Ondrej Jirman <megi@xff.cz>
+ */
+#include <linux/cdev.h>
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dmaengine.h>
+#include <linux/dma-mapping.h>
+#include <linux/gpio/consumer.h>
+#include <linux/interrupt.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/poll.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/iio/consumer.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+#include <asm/neon.h>
+
+// {{{ Registry defines
+
+#define SUN4I_TCON_GCTL_REG			0x0
+#define SUN4I_TCON_GCTL_TCON_ENABLE			BIT(31)
+#define SUN4I_TCON_GCTL_IOMAP_MASK			BIT(0)
+#define SUN4I_TCON_GCTL_IOMAP_TCON1			(1 << 0)
+#define SUN4I_TCON_GCTL_IOMAP_TCON0			(0 << 0)
+
+#define SUN4I_TCON_GINT0_REG			0x4
+#define SUN4I_TCON_GINT0_VBLANK_ENABLE(pipe)		BIT(31 - (pipe))
+#define SUN4I_TCON_GINT0_VBLANK_INT(pipe)		BIT(15 - (pipe))
+#define SUN4I_TCON_GINT0_LINE_ENABLE(pipe)		BIT(29 - (pipe))
+#define SUN4I_TCON_GINT0_LINE_INT(pipe)			BIT(13 - (pipe))
+
+#define SUN4I_TCON_GINT1_REG			0x8
+
+#define SUN4I_TCON0_CTL_REG			0x40
+#define SUN4I_TCON0_CTL_TCON_ENABLE			BIT(31)
+#define SUN4I_TCON0_CTL_IF_MASK				GENMASK(25, 24)
+#define SUN4I_TCON0_CTL_IF_8080				(1 << 24)
+#define SUN4I_TCON0_CTL_CLK_DELAY_MASK			GENMASK(8, 4)
+#define SUN4I_TCON0_CTL_CLK_DELAY(delay)		((delay << 4) & SUN4I_TCON0_CTL_CLK_DELAY_MASK)
+#define SUN4I_TCON0_CTL_SRC_SEL_MASK			GENMASK(2, 0)
+
+#define SUN4I_TCON0_DCLK_REG			0x44
+#define SUN4I_TCON0_DCLK_GATE_BIT			(31)
+#define SUN4I_TCON0_DCLK_DIV_SHIFT			(0)
+#define SUN4I_TCON0_DCLK_DIV_WIDTH			(7)
+
+#define SUN4I_TCON0_BASIC0_REG			0x48
+#define SUN4I_TCON0_BASIC0_X(width)			((((width) - 1) & 0xfff) << 16)
+#define SUN4I_TCON0_BASIC0_Y(height)			(((height) - 1) & 0xfff)
+
+#define SUN4I_TCON0_BASIC1_REG			0x4c
+#define SUN4I_TCON0_BASIC1_H_TOTAL(total)		((((total) - 1) & 0x1fff) << 16)
+#define SUN4I_TCON0_BASIC1_H_BACKPORCH(bp)		(((bp) - 1) & 0xfff)
+
+#define SUN4I_TCON0_BASIC2_REG			0x50
+#define SUN4I_TCON0_BASIC2_V_TOTAL(total)		(((total) & 0x1fff) << 16)
+#define SUN4I_TCON0_BASIC2_V_BACKPORCH(bp)		(((bp) - 1) & 0xfff)
+
+#define SUN4I_TCON0_BASIC3_REG			0x54
+#define SUN4I_TCON0_BASIC3_H_SYNC(width)		((((width) - 1) & 0x7ff) << 16)
+#define SUN4I_TCON0_BASIC3_V_SYNC(height)		(((height) - 1) & 0x7ff)
+
+#define SUN4I_TCON0_HV_IF_REG			0x58
+
+#define SUN4I_TCON0_IO_POL_REG			0x88
+#define SUN4I_TCON0_IO_POL_DCLK_PHASE(phase)		((phase & 3) << 28)
+#define SUN4I_TCON0_IO_POL_DE_NEGATIVE			BIT(27)
+#define SUN4I_TCON0_IO_POL_DCLK_NEGATIVE		BIT(26)
+#define SUN4I_TCON0_IO_POL_HSYNC_POSITIVE		BIT(25)
+#define SUN4I_TCON0_IO_POL_VSYNC_POSITIVE		BIT(24)
+
+#define SUN4I_TCON0_IO_TRI_REG			0x8c
+#define SUN4I_TCON0_IO_TRI_HSYNC_DISABLE		BIT(25)
+#define SUN4I_TCON0_IO_TRI_VSYNC_DISABLE		BIT(24)
+#define SUN4I_TCON0_IO_TRI_DATA_PINS_DISABLE(pins)	GENMASK(pins, 0)
+
+#define SUN4I_BACKEND_MODCTL_REG		0x800
+#define SUN4I_BACKEND_MODCTL_LINE_SEL			BIT(29)
+#define SUN4I_BACKEND_MODCTL_ITLMOD_EN			BIT(28)
+#define SUN4I_BACKEND_MODCTL_OUT_SEL			GENMASK(22, 20)
+#define SUN4I_BACKEND_MODCTL_OUT_LCD0				(0 << 20)
+#define SUN4I_BACKEND_MODCTL_OUT_LCD1				(1 << 20)
+#define SUN4I_BACKEND_MODCTL_OUT_FE0				(6 << 20)
+#define SUN4I_BACKEND_MODCTL_OUT_FE1				(7 << 20)
+#define SUN4I_BACKEND_MODCTL_HWC_EN			BIT(16)
+#define SUN4I_BACKEND_MODCTL_LAY_EN(l)			BIT(8 + l)
+#define SUN4I_BACKEND_MODCTL_OCSC_EN			BIT(5)
+#define SUN4I_BACKEND_MODCTL_DFLK_EN			BIT(4)
+#define SUN4I_BACKEND_MODCTL_DLP_START_CTL		BIT(2)
+#define SUN4I_BACKEND_MODCTL_START_CTL			BIT(1)
+#define SUN4I_BACKEND_MODCTL_DEBE_EN			BIT(0)
+
+#define SUN4I_BACKEND_BACKCOLOR_REG		0x804
+#define SUN4I_BACKEND_BACKCOLOR(r, g, b)		(((r) << 16) | ((g) << 8) | (b))
+
+#define SUN4I_BACKEND_DISSIZE_REG		0x808
+#define SUN4I_BACKEND_DISSIZE(w, h)			(((((h) - 1) & 0xffff) << 16) | \
+							 (((w) - 1) & 0xffff))
+
+#define SUN4I_BACKEND_LAYSIZE_REG(l)		(0x810 + (0x4 * (l)))
+#define SUN4I_BACKEND_LAYSIZE(w, h)			(((((h) - 1) & 0x1fff) << 16) | \
+							 (((w) - 1) & 0x1fff))
+
+#define SUN4I_BACKEND_LAYCOOR_REG(l)		(0x820 + (0x4 * (l)))
+#define SUN4I_BACKEND_LAYCOOR(x, y)			((((u32)(y) & 0xffff) << 16) | \
+							 ((u32)(x) & 0xffff))
+
+#define SUN4I_BACKEND_LAYLINEWIDTH_REG(l)	(0x840 + (0x4 * (l)))
+
+#define SUN4I_BACKEND_LAYFB_L32ADD_REG(l)	(0x850 + (0x4 * (l)))
+
+#define SUN4I_BACKEND_LAYFB_H4ADD_REG		0x860
+#define SUN4I_BACKEND_LAYFB_H4ADD_MSK(l)		GENMASK(3 + ((l) * 8), (l) * 8)
+#define SUN4I_BACKEND_LAYFB_H4ADD(l, val)		((val) << ((l) * 8))
+
+#define SUN4I_BACKEND_REGBUFFCTL_REG		0x870
+#define SUN4I_BACKEND_REGBUFFCTL_AUTOLOAD_DIS		BIT(1)
+#define SUN4I_BACKEND_REGBUFFCTL_LOADCTL		BIT(0)
+
+#define SUN4I_BACKEND_CKMAX_REG			0x880
+#define SUN4I_BACKEND_CKMIN_REG			0x884
+#define SUN4I_BACKEND_CKCFG_REG			0x888
+#define SUN4I_BACKEND_ATTCTL_REG0(l)		(0x890 + (0x4 * (l)))
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_GLBALPHA_MASK	GENMASK(31, 24)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_GLBALPHA(x)		((x) << 24)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_PIPESEL_MASK	BIT(15)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_PIPESEL(x)		((x) << 15)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_PRISEL_MASK	GENMASK(11, 10)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_PRISEL(x)			((x) << 10)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_WORKMOD_MASK	GENMASK(23, 22)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_WORKMOD(x)		((x) << 22)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_YUVEN		BIT(2)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_VDOEN		BIT(1)
+#define SUN4I_BACKEND_ATTCTL_REG0_LAY_GLBALPHA_EN	BIT(0)
+
+#define SUN4I_BACKEND_ATTCTL_REG1(l)		(0x8a0 + (0x4 * (l)))
+#define SUN4I_BACKEND_ATTCTL_REG1_LAY_HSCAFCT		GENMASK(15, 14)
+#define SUN4I_BACKEND_ATTCTL_REG1_LAY_WSCAFCT		GENMASK(13, 12)
+#define SUN4I_BACKEND_ATTCTL_REG1_LAY_FBFMT		GENMASK(11, 8)
+#define SUN4I_BACKEND_LAY_FBFMT_1BPP				(0 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_2BPP				(1 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_4BPP				(2 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_8BPP				(3 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_RGB655				(4 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_RGB565				(5 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_RGB556				(6 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_ARGB1555			(7 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_RGBA5551			(8 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_XRGB8888			(9 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_ARGB8888			(10 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_RGB888				(11 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_ARGB4444			(12 << 8)
+#define SUN4I_BACKEND_LAY_FBFMT_RGBA4444			(13 << 8)
+
+// }}}
+
+#define EINK_IOCTL_UPDATE _IO('A', 0)
+//#define EINK_IOCTL_STATUS _IOR('A', 2, int)
+
+enum {
+	EINK_REQ_NONE = 0,
+	EINK_REQ_UPDATE,
+	EINK_REQ_POWERDOWN,
+};
+
+enum {
+	EINK_WORKING,
+	EINK_OPEN,
+	EINK_WAITING_INT,
+	EINK_BE_SETUP,
+};
+
+struct eink_dev {
+	struct device *dev;
+
+	// io
+	struct pinctrl* pinctrl;
+	struct pinctrl_state *pinctrl_active;
+	struct pinctrl_state *pinctrl_idle;
+	struct gpio_descs* gpios;
+
+	// power
+	struct regulator *panel_supply;
+	struct timer_list powerdown_timer;
+	bool powered;
+
+	// tcon0
+	struct clk_bulk_data *clks;
+	int num_clks;
+
+        struct reset_control *rstc;
+	struct regmap *tcon_regs;
+	struct regmap *be_regs;
+	int irq;
+
+	// framebuffers
+	u8* framebuffers[2];
+	u32* ctlstream;
+	dma_addr_t ctlstream_paddr;
+	size_t ctlstream_size; // byte size
+
+	// control device
+	struct cdev cdev;
+	dev_t major;
+	bool is_open; // device can be used by only one user
+
+	// rendering work
+	spinlock_t lock;
+	wait_queue_head_t waitqueue;
+	struct workqueue_struct *wq;
+	struct work_struct work;
+	unsigned long flags[1];
+	int last_request;
+
+	const struct eink_panel_config* panel;
+};
+
+struct eink_panel_config {
+	int sources;
+	int gates;
+	int h_lead;
+	int h_trail;
+	int v_extra;
+};
+
+static const struct eink_panel_config eink_ed060xd4_panel = {
+	.sources = 1024,
+	.gates = 758,
+	.h_lead = 40, // 1000ns (should be multiple of 8 for 256-bit alignment)
+	.h_trail = 8, // should be multiple of 8 for 256-bit alignment
+	.v_extra = 9,
+};
+
+// buf format is 1 byte per pixel, len needs to be multiple of 4 bytes
+
+/* PocketBook 626(2) (Touch Lux 3) port D <-> Panel pin map */
+
+#define	D0        BIT(3)
+#define	D1        BIT(4)
+#define	D2        BIT(5)
+#define	D3        BIT(6)
+#define	D4        BIT(7)
+
+#define	D5       BIT(10)
+#define	D6       BIT(11)
+#define	D7       BIT(12)
+#define	S_LE     BIT(13)
+#define	G_MODE1  BIT(15)
+
+#define	S_OE     BIT(20)
+#define	S_SP     BIT(21)
+#define	G_CLK    BIT(22)
+#define	G_SP     BIT(23)
+
+/*
+ * we need to encode stream of control signals for various operations
+ * into separate rows:
+ *
+ * - sequence for starting a frame
+ * - three leading G_CLK pulses
+ * - sequence for all data rows
+ *   - wait time with OE for previous row
+ * - wait time for the last row without shifting in more data
+ * - trailing row with a sequence for ending a frame
+ *
+ * So we have to have:
+ * - 6 special rows with no data + all the data rows
+ * - all of these have to have the same width
+ * - we want to use TCON to manage timing of the OE via HT/HBP settings
+ *
+ * Timings:
+ * - we use 20MHz clock (50ns period)
+ * - we have to spend 256 clocks per row, pushing data
+ * - we want to have 85Hz frame refresh rate
+ *   - 11765us budget per frame
+ *   - minimum G_CLK low time is 500ns, minimum G_CLK cycle time is 5us
+ *     - G_SP 100ns setup (to G_CLK H->L)/hold time(after G_CLK H->L)
+ *   - LE pulse width >150ns
+ *     - LE delay before more data 200ns
+ *   - 12us time the output reacts to LE (this should be hsync time/hbp)
+ *   - at 20MHz, time to load data is 13us (about 100 FPS)
+ */
+static void eink_ctlstream_prepare(struct eink_dev* eink)
+{
+	const struct eink_panel_config* pc = eink->panel;
+	u32* p = eink->ctlstream;
+	int row_width = pc->sources / 4 + pc->h_lead + pc->h_trail; // ~262
+	int i, j, t, r;
+	u32 base;
+
+#define SET_PINS(t, v) { \
+		for (i = 0; i < t; i++) \
+			*p++ = v; \
+		r -= t; \
+	}
+
+	// prepare a startup row
+	t = row_width / 3;
+
+	r = row_width;
+	SET_PINS(20, G_MODE1 | G_SP | S_SP            );
+	SET_PINS(2*t,G_MODE1 | G_SP | S_SP | G_CLK    );
+	SET_PINS(r,  G_MODE1 | G_SP | S_SP            );
+
+	r = row_width;
+	SET_PINS(t,  G_MODE1 | G_SP | S_SP | G_CLK    );
+	SET_PINS(t,  G_MODE1 |        S_SP | G_CLK    );
+	SET_PINS(r,  G_MODE1 |        S_SP            );
+
+	r = row_width;
+	SET_PINS(t,  G_MODE1 |        S_SP | G_CLK    );
+	SET_PINS(t,  G_MODE1 | G_SP | S_SP | G_CLK    );
+	SET_PINS(r,  G_MODE1 | G_SP | S_SP            );
+
+	// three empty rows
+	for (j = 0; j < 3; j++) {
+		r = row_width;
+
+		SET_PINS(2*t,  G_MODE1 | G_SP | S_SP | G_CLK    );
+		SET_PINS(r,    G_MODE1 | G_SP | S_SP            );
+	}
+
+	// extra row is there to keep S_OE high for the previous last data
+	// row that was shifted in
+	for (j = 0; j < pc->gates + 1; j++) {
+		base = G_MODE1 | G_SP;
+
+                // We have 40 periods here to work with here (h_lead).
+		r = pc->h_lead;
+
+		// S_SP goes high after the last data and there's 2us
+		// delay (it may even go high during the last data period?)
+		SET_PINS(20, base | S_SP); // 1000ns for tLEdly
+		base &= ~S_OE;
+		SET_PINS(2,  base | S_SP);
+		SET_PINS(6,  base | S_SP | (j > 0 ? S_LE : 0)); // 300ns for tLEw
+		if (j > 0)
+			base |= S_OE;
+		SET_PINS(2,  base | S_SP | (j > 0 ? S_LE : 0));  // S_OE goes high
+		SET_PINS(r-2,base | S_SP); // 400ns for tLEoff
+		SET_PINS(r,  base | S_SP | G_CLK); // 400ns for tLEoff
+
+		if (j >= pc->gates)
+			base |= S_SP;
+
+		// start loading data immediately at S_SP going low
+		r = pc->sources / 4;
+		t = r / 3;
+		SET_PINS(t,  base | G_CLK);
+		SET_PINS(r,  base);
+
+		r = pc->h_trail;
+		SET_PINS(r,  base | S_SP);
+	}
+
+	r = row_width;
+	SET_PINS(r,               G_SP | S_SP                   );
+
+	// this last row will be stopped by an interrupt before finishing
+	// completely, so it's a dummy one
+	r = row_width;
+	SET_PINS(r,               G_SP | S_SP                   );
+}
+
+void eink_ctlstream_fill_data_neon(u32* cmd, u8* fb, int stride,
+				   int sources, int gates, u32 masks[4]);
+
+static void eink_ctlstream_fill_data(struct eink_dev* eink, u8* fb)
+{
+	const struct eink_panel_config* pc = eink->panel;
+	int row_width = pc->sources / 4 + pc->h_lead + pc->h_trail;
+	u32 masks[4] = {
+		G_MODE1 | G_SP | G_CLK,
+		G_MODE1 | G_SP,
+		G_MODE1 | G_SP | S_OE | G_CLK,
+		G_MODE1 | G_SP | S_OE,
+	};
+
+	kernel_neon_begin();
+	eink_ctlstream_fill_data_neon(eink->ctlstream + 6 * row_width + pc->h_lead,
+				      fb, row_width, pc->sources,
+				      pc->gates, masks);
+	kernel_neon_end();
+}
+
+static int eink_set_power(struct eink_dev* eink, bool en)
+{
+	int ret;
+
+	if (!en != !eink->powered) {
+		dev_warn(eink->dev, "%sable power supply", en ? "en" : "dis");
+
+		if (en)
+			ret = regulator_enable(eink->panel_supply);
+		else
+			ret = regulator_disable(eink->panel_supply);
+		if (ret) {
+			dev_err(eink->dev, "can't %sable power supply (%d)",
+				en ? "en" : "dis", ret);
+			return ret;
+		}
+	}
+
+	if (en)
+		mod_timer(&eink->powerdown_timer,
+			  jiffies + msecs_to_jiffies(5000));
+	else
+		del_timer_sync(&eink->powerdown_timer);
+
+	eink->powered = en;
+	return 0;
+}
+
+__maybe_unused
+static void print_time(struct device* dev, u64 ts, char *buf)
+{
+	do_div(ts, 1000);
+	dev_info(dev, "[%s] %6lu us", buf, (unsigned long)ts);
+}
+
+// DCLK = 20MHz
+static int eink_update(struct eink_dev* eink)
+{
+	struct regmap *tcon = eink->tcon_regs;
+	struct regmap *be = eink->be_regs;
+	unsigned long sclk_rate;
+	u32 ddiv = 6, w, h;
+	u32 lo_paddr, hi_paddr;
+	u32 vbp = 1, hbp = 40;
+	int ret;
+
+	ret = eink_set_power(eink, 1);
+	if (ret)
+		return ret;
+
+	sclk_rate = clk_get_rate(eink->clks[1].clk);
+	if (sclk_rate == 0)
+		return -EINVAL;
+
+	spin_lock(&eink->lock);
+	eink_ctlstream_fill_data(eink, eink->framebuffers[0]);
+	spin_unlock(&eink->lock);
+
+	w = eink->panel->sources / 4 + eink->panel->h_lead + eink->panel->h_trail;
+	h = eink->panel->gates + eink->panel->v_extra;
+
+	// Setup DEBE
+
+	if (!test_bit(EINK_BE_SETUP, eink->flags)) {
+	regmap_write(be, SUN4I_BACKEND_DISSIZE_REG,
+		     SUN4I_BACKEND_DISSIZE(w, h));
+	regmap_write(be, SUN4I_BACKEND_LAYSIZE_REG(0),
+		     SUN4I_BACKEND_LAYSIZE(w, h));
+	regmap_write(be, SUN4I_BACKEND_LAYCOOR_REG(0),
+		     SUN4I_BACKEND_LAYCOOR(0, 0));
+	regmap_write(be, SUN4I_BACKEND_LAYLINEWIDTH_REG(0),
+		     w * 4 * 8); /* input format has 4 bytes per pixel */
+
+	lo_paddr = eink->ctlstream_paddr << 3;
+	hi_paddr = eink->ctlstream_paddr >> 29;
+	regmap_write(be, SUN4I_BACKEND_LAYFB_L32ADD_REG(0), lo_paddr);
+	regmap_update_bits(be, SUN4I_BACKEND_LAYFB_H4ADD_REG,
+			   SUN4I_BACKEND_LAYFB_H4ADD_MSK(0),
+			   SUN4I_BACKEND_LAYFB_H4ADD(0, hi_paddr));
+
+	regmap_write(be, SUN4I_BACKEND_ATTCTL_REG0(0),
+		     SUN4I_BACKEND_ATTCTL_REG0_LAY_WORKMOD(0) |
+		     SUN4I_BACKEND_ATTCTL_REG0_LAY_PIPESEL(0) |
+		     SUN4I_BACKEND_ATTCTL_REG0_LAY_PRISEL(3));
+
+	regmap_write(be, SUN4I_BACKEND_ATTCTL_REG1(0),
+		     SUN4I_BACKEND_LAY_FBFMT_XRGB8888);
+
+	/* load BE buffers */
+	regmap_write(be, SUN4I_BACKEND_REGBUFFCTL_REG,
+		     SUN4I_BACKEND_REGBUFFCTL_AUTOLOAD_DIS |
+		     SUN4I_BACKEND_REGBUFFCTL_LOADCTL);
+	set_bit(EINK_BE_SETUP, eink->flags);
+	}
+
+	// Setup TCON/TCON0
+	//
+	// TCON0 timings, allwinner style:
+	//
+	// ht - horizontal total time
+	//   - total number of clock cycles in a row
+	// hbp - horizontal back porch
+	//   - number of dclk cycles between start of hsync and data
+	// vt - vertical total time
+	//   - number of rows in two frames
+	// vbp - vertical back porch
+	//   - number of rows between vsync start and a valid data line
+	// hspw
+	//   - width of hsync signal (in # of dclk cycles)
+	// vspw
+	//   - width of vsync signal (in # of rows)
+	//
+	// Notes:
+	// hbp(aw)=hbp(panel)+hspw(panel) (same for vbp)
+
+	/* Enable tcon module */
+	regmap_write(tcon, SUN4I_TCON_GCTL_REG,
+		     SUN4I_TCON_GCTL_TCON_ENABLE |
+		     SUN4I_TCON_GCTL_IOMAP_TCON0);
+
+	/* Set the resolution */
+	regmap_write(tcon, SUN4I_TCON0_BASIC0_REG,
+		     SUN4I_TCON0_BASIC0_X(w) |
+		     SUN4I_TCON0_BASIC0_Y(h));
+
+	/* Set horizontal display timings */
+	regmap_write(tcon, SUN4I_TCON0_BASIC1_REG,
+		     SUN4I_TCON0_BASIC1_H_TOTAL(w + hbp + 2) |
+		     SUN4I_TCON0_BASIC1_H_BACKPORCH(hbp));
+
+	/* Set vertical display timings */
+	regmap_write(tcon, SUN4I_TCON0_BASIC2_REG,
+		     SUN4I_TCON0_BASIC2_V_TOTAL((h + vbp + 2) * 2) |
+		     SUN4I_TCON0_BASIC2_V_BACKPORCH(vbp));
+
+	/* Set Hsync and Vsync length */
+	regmap_write(tcon, SUN4I_TCON0_BASIC3_REG,
+		     SUN4I_TCON0_BASIC3_V_SYNC(1) |
+		     SUN4I_TCON0_BASIC3_H_SYNC(1));
+
+        /* Setup output mode/pins */
+	regmap_write(tcon, SUN4I_TCON0_HV_IF_REG, 0); // 24bit parallel mode
+        regmap_write(tcon, SUN4I_TCON0_IO_POL_REG, SUN4I_TCON0_IO_POL_DCLK_NEGATIVE);
+        regmap_write(tcon, SUN4I_TCON0_IO_TRI_REG, 0);
+
+	ret = pinctrl_select_state(eink->pinctrl, eink->pinctrl_active);
+	if (ret) {
+		dev_err(eink->dev, "can't switch to active pinctrl state (%d)",
+			ret);
+	}
+
+	set_bit(EINK_WAITING_INT, eink->flags);
+
+	/* Interrupt after h lines */
+	regmap_write(tcon, SUN4I_TCON_GINT0_REG, 0);
+        regmap_write(tcon, SUN4I_TCON_GINT1_REG, (h + vbp + 2) << 16);
+	//regmap_write(tcon, SUN4I_TCON_GINT0_REG,
+		     //SUN4I_TCON_GINT0_VBLANK_ENABLE(0));
+	regmap_write(tcon, SUN4I_TCON_GINT0_REG,
+		     SUN4I_TCON_GINT0_LINE_ENABLE(0));
+
+	/* Configure and enable the dot clock */
+	regmap_write(tcon, SUN4I_TCON0_DCLK_REG,
+		     BIT(SUN4I_TCON0_DCLK_GATE_BIT) |
+		     (ddiv << SUN4I_TCON0_DCLK_DIV_SHIFT));
+
+	/* Enable tcon 0, set clk delay */
+	regmap_write(tcon, SUN4I_TCON0_CTL_REG,
+		     SUN4I_TCON0_CTL_CLK_DELAY(3)); // up to 30
+
+	regmap_update_bits(tcon, SUN4I_TCON0_CTL_REG,
+			   SUN4I_TCON0_CTL_TCON_ENABLE,
+			   SUN4I_TCON0_CTL_TCON_ENABLE);
+
+	ret = wait_event_timeout(eink->waitqueue,
+				 !test_bit(EINK_WAITING_INT, eink->flags),
+				 msecs_to_jiffies(100));
+	if (ret == 0)
+		dev_warn(eink->dev, "Timeout waiting for TCON transfer\n");
+
+	// stop everything, cleanup
+        //regmap_write(tcon, SUN4I_TCON0_DCLK_REG, 0);
+	regmap_update_bits(tcon, SUN4I_TCON0_CTL_REG,
+			   SUN4I_TCON0_CTL_TCON_ENABLE,
+			   0);
+        //regmap_write(tcon, SUN4I_TCON_GCTL_REG, 0);
+        regmap_write(tcon, SUN4I_TCON_GINT0_REG, 0);
+        regmap_write(tcon, SUN4I_TCON_GINT1_REG, 0);
+
+	ret = pinctrl_select_state(eink->pinctrl, eink->pinctrl_idle);
+	if (ret) {
+		dev_err(eink->dev, "can't switch to idle pinctrl state (%d)",
+			ret);
+	}
+
+        regmap_write(tcon, SUN4I_TCON0_IO_TRI_REG, ~0);
+
+	return ret;
+}
+
+static void eink_supply_powerdown_timer(struct timer_list *t)
+{
+	struct eink_dev *eink = from_timer(eink, t, powerdown_timer);
+
+	//dev_info(eink->dev, "powering down");
+
+	spin_lock(&eink->lock);
+	eink->last_request = EINK_REQ_POWERDOWN;
+	spin_unlock(&eink->lock);
+
+	queue_work(eink->wq, &eink->work);
+}
+
+static void eink_work_handler(struct work_struct *work)
+{
+	struct eink_dev *eink = container_of(work, struct eink_dev, work);
+	int last_request;
+
+	spin_lock(&eink->lock);
+	last_request = eink->last_request;
+	eink->last_request = 0;
+	spin_unlock(&eink->lock);
+
+	if (last_request == EINK_REQ_UPDATE) {
+		eink_update(eink);
+	} else if (last_request == EINK_REQ_POWERDOWN) {
+		eink_set_power(eink, 0);
+	}
+
+	clear_bit(EINK_WORKING, eink->flags);
+	wake_up(&eink->waitqueue);
+}
+
+static ssize_t eink_read(struct file *fp, char __user *buf, size_t len,
+			 loff_t *off)
+{
+	struct eink_dev* eink = fp->private_data;
+	int ret, fb_size;
+
+	fb_size = eink->panel->sources / 4 * eink->panel->gates;
+	if (len != fb_size)
+		return -EINVAL;
+
+	spin_lock(&eink->lock);
+	if (copy_to_user(buf, eink->framebuffers[0], fb_size))
+		ret = -EFAULT;
+	else
+		ret = fb_size;
+	spin_unlock(&eink->lock);
+
+	return ret;
+}
+
+static ssize_t eink_write(struct file *fp, const char __user *buf,
+			  size_t len, loff_t *off)
+{
+	struct eink_dev* eink = fp->private_data;
+	int non_blocking = fp->f_flags & O_NONBLOCK;
+	int ret, fb_size;
+
+	fb_size = eink->panel->sources / 4 * eink->panel->gates;
+	if (len != fb_size)
+		return -EINVAL;
+
+	if (test_and_set_bit(EINK_WORKING, eink->flags))
+		return -EBUSY;
+
+	spin_lock(&eink->lock);
+	ret = copy_from_user(eink->framebuffers[0], buf, len);
+	if (ret) {
+		clear_bit(EINK_WORKING, eink->flags);
+		spin_unlock(&eink->lock);
+		return -EFAULT;
+	}
+
+	eink->last_request = EINK_REQ_UPDATE;
+	spin_unlock(&eink->lock);
+
+	queue_work(eink->wq, &eink->work);
+
+	// first handle non-blocking path
+	if (non_blocking && test_bit(EINK_WORKING, eink->flags))
+		return -EWOULDBLOCK;
+
+	// wait for availability of wakeup
+	wait_event(eink->waitqueue,
+		   !test_bit(EINK_WORKING, eink->flags));
+
+	//u64 ts = ktime_get_boottime_ns();
+	//print_time(eink->dev, ktime_get_boottime() - ts, "one frame");
+
+	return len;
+}
+
+static unsigned int eink_poll(struct file *fp, poll_table *wait)
+{
+	struct eink_dev* eink = fp->private_data;
+	int ret = 0;
+
+	poll_wait(fp, &eink->waitqueue, wait);
+
+	if (!test_bit(EINK_WORKING, eink->flags))
+		ret |= POLLIN | POLLRDNORM;
+
+	return ret;
+}
+
+static long eink_ioctl(struct file *fp, unsigned int cmd, unsigned long arg)
+{
+	struct eink_dev* eink = fp->private_data;
+	unsigned long flags;
+	int ret = -ENOSYS;
+	//void __user *parg = (void __user *)arg;
+
+	if (!capable(CAP_SYS_ADMIN))
+		return -EACCES;
+
+	if (test_and_set_bit(EINK_WORKING, eink->flags))
+		return -EBUSY;
+
+	spin_lock_irqsave(&eink->lock, flags);
+
+	switch (cmd) {
+	case EINK_IOCTL_UPDATE:
+		eink->last_request = EINK_REQ_UPDATE;
+		ret = 0;
+		break;
+		/*
+	case EINK_IOCTL_BLANK:
+		eink->last_request = EINK_REQ_BLANK;
+		ret = 0;
+		break;
+	case EINK_IOCTL_STATUS:
+		if (copy_to_user(parg, &powered, sizeof powered))
+			return -EFAULT;
+
+		return 0;
+		*/
+	}
+
+	spin_unlock_irqrestore(&eink->lock, flags);
+
+	if (ret == 0)
+		queue_work(eink->wq, &eink->work);
+	else
+		clear_bit(EINK_WORKING, eink->flags);
+
+	return ret;
+}
+
+static int eink_release(struct inode *ip, struct file *fp)
+{
+	struct eink_dev* eink = fp->private_data;
+
+	clear_bit(EINK_OPEN, eink->flags);
+	return 0;
+}
+
+static int eink_open(struct inode *ip, struct file *fp)
+{
+	struct eink_dev* eink = container_of(ip->i_cdev, struct eink_dev, cdev);
+
+	if (test_and_set_bit(EINK_OPEN, eink->flags))
+		return -EBUSY;
+
+	fp->private_data = eink;
+
+	nonseekable_open(ip, fp);
+	return 0;
+}
+
+static const struct file_operations eink_fops = {
+	.owner		= THIS_MODULE,
+	.read		= eink_read,
+	.write		= eink_write,
+	.poll		= eink_poll,
+	.unlocked_ioctl	= eink_ioctl,
+	.open		= eink_open,
+	.release	= eink_release,
+	.llseek		= noop_llseek,
+};
+
+static struct regmap_config eink_tcon_regmap_config = {
+	.name		= "tcon",
+        .reg_bits       = 32,
+        .val_bits       = 32,
+        .reg_stride     = 4,
+        .max_register   = 0x7ff,
+	.cache_type     = REGCACHE_NONE,
+};
+
+static struct regmap_config eink_be_regmap_config = {
+	.name		= "be",
+        .reg_bits       = 32,
+        .val_bits       = 32,
+        .reg_stride     = 4,
+        .max_register   = 0x57ff,
+	.cache_type     = REGCACHE_NONE,
+};
+
+static struct class* eink_class;
+
+static irqreturn_t eink_tcon0_irq_handler(int irq, void *private)
+{
+        struct eink_dev *eink = private;
+	struct regmap *tcon = eink->tcon_regs;
+        unsigned int status;
+
+        regmap_read(eink->tcon_regs, SUN4I_TCON_GINT0_REG, &status);
+
+        if (!(status & (SUN4I_TCON_GINT0_VBLANK_INT(0) |
+			SUN4I_TCON_GINT0_LINE_INT(0))))
+                return IRQ_NONE;
+
+	// stop everything
+        //regmap_write(tcon, SUN4I_TCON_GCTL_REG, 0);
+
+	regmap_update_bits(tcon, SUN4I_TCON0_CTL_REG,
+			   SUN4I_TCON0_CTL_TCON_ENABLE,
+			   0);
+
+        regmap_write(tcon, SUN4I_TCON_GINT0_REG, 0);
+        regmap_write(tcon, SUN4I_TCON_GINT1_REG, 0);
+        //regmap_write(tcon, SUN4I_TCON0_DCLK_REG, 0);
+        //regmap_write(tcon, SUN4I_TCON0_IO_TRI_REG, ~0);
+
+	clear_bit(EINK_WAITING_INT, eink->flags);
+	wake_up(&eink->waitqueue);
+
+	/*
+        regmap_update_bits(eink->tcon_regs, SUN4I_TCON_GINT0_REG,
+                           SUN4I_TCON_GINT0_VBLANK_INT(0) |
+                           SUN4I_TCON_GINT0_LINE_INT(0),
+                           0);
+          */
+
+        return IRQ_HANDLED;
+}
+
+static const struct clk_bulk_data eink_clocks[] = {
+	{ .id = "tcon_bus" },
+	{ .id = "tcon_mod" },
+	{ .id = "be_bus" },
+	{ .id = "be_mod" },
+	{ .id = "be_ram" },
+};
+
+static int eink_probe(struct platform_device *pdev)
+{
+	struct eink_dev *eink;
+	struct device *dev = &pdev->dev;
+	struct device_node *np = dev->of_node;
+	struct device *sdev;
+	const char* cdev_name = NULL;
+        void __iomem *tcon_regs, *be_regs;
+	int ret, i, fb_size, cs_size;
+
+	eink = devm_kzalloc(dev, sizeof(*eink), GFP_KERNEL);
+	if (!eink)
+		return -ENOMEM;
+
+	eink->panel = &eink_ed060xd4_panel;
+	fb_size = eink->panel->sources / 4 * eink->panel->gates;
+
+	// we shift four 2-bit source values per clock cycle
+	cs_size = ((eink->panel->sources / 4 + eink->panel->h_lead + eink->panel->h_trail) *
+		   (eink->panel->gates + eink->panel->v_extra)) * 4;
+
+	eink->framebuffers[0] = devm_kzalloc(dev, fb_size, GFP_KERNEL);
+	if (!eink->framebuffers[0])
+		return -ENOMEM;
+
+	eink->framebuffers[1] = devm_kzalloc(dev, fb_size, GFP_KERNEL);
+	if (!eink->framebuffers[1])
+		return -ENOMEM;
+
+	eink->dev = dev;
+	platform_set_drvdata(pdev, eink);
+	init_waitqueue_head(&eink->waitqueue);
+	spin_lock_init(&eink->lock);
+	INIT_WORK(&eink->work, &eink_work_handler);
+	timer_setup(&eink->powerdown_timer, eink_supply_powerdown_timer, 0);
+
+	ret = of_dma_configure(dev, dev->of_node, true);
+	if (ret) {
+		dev_err(dev, "failed to configure dma (%d)\n", ret);
+		return ret;
+	}
+
+	tcon_regs = devm_platform_ioremap_resource(pdev, 0);
+        if (IS_ERR(tcon_regs))
+                return PTR_ERR(tcon_regs);
+
+	be_regs = devm_platform_ioremap_resource(pdev, 1);
+        if (IS_ERR(be_regs))
+                return PTR_ERR(be_regs);
+
+	eink->pinctrl = devm_pinctrl_get(dev);
+	if (IS_ERR(eink->pinctrl)) {
+		ret = PTR_ERR(eink->pinctrl);
+		dev_err(dev, "can't get pinctrl (%d)\n", ret);
+		return ret;
+	}
+
+	eink->pinctrl_active = pinctrl_lookup_state(eink->pinctrl, "active");
+	if (!eink->pinctrl_active) {
+		dev_err(dev, "missing active pinctrl state");
+		return -EINVAL;
+	}
+
+	eink->pinctrl_idle = pinctrl_lookup_state(eink->pinctrl, "idle");
+	if (!eink->pinctrl_idle) {
+		dev_err(dev, "missing idle pinctrl state");
+		return -EINVAL;
+	}
+
+	ret = pinctrl_select_state(eink->pinctrl, eink->pinctrl_idle);
+	if (ret) {
+		dev_err(eink->dev, "can't switch to idle pinctrl state (%d)",
+			ret);
+		return ret;
+	}
+
+	eink->gpios = devm_gpiod_get_array(dev, "all", GPIOD_OUT_LOW);
+	if (IS_ERR(eink->gpios)) {
+		ret = PTR_ERR(eink->gpios);
+		dev_err(dev, "can't get all gpios (%d)\n", ret);
+		return ret;
+	}
+
+	eink->irq = platform_get_irq(pdev, 0);
+	if (eink->irq < 0) {
+                dev_err(dev, "Couldn't retrieve the TCON interrupt\n");
+		return eink->irq;
+	}
+
+	ret = of_property_read_string(np, "control-device-name", &cdev_name);
+	if (ret) {
+		dev_err(dev, "char-device-name is not configured (%d)\n", ret);
+		return ret;
+	}
+
+	eink->panel_supply = devm_regulator_get(dev, "panel");
+	if (IS_ERR(eink->panel_supply)) {
+		ret = PTR_ERR(eink->panel_supply);
+		dev_err(dev, "can't get panel supply (%d)\n", ret);
+		return ret;
+	}
+
+	eink->clks = devm_kmemdup(dev, eink_clocks, sizeof(eink_clocks),
+				  GFP_KERNEL);
+	if (!eink->clks)
+		return -ENOMEM;
+
+	eink->num_clks = ARRAY_SIZE(eink_clocks);
+	ret = devm_clk_bulk_get(dev, eink->num_clks, eink->clks);
+	if (ret)
+		return ret;
+
+        eink->rstc = devm_reset_control_array_get(dev, 0);
+        if (IS_ERR(eink->rstc)) {
+		ret = PTR_ERR(eink->rstc);
+                dev_err(dev, "Couldn't get our reset line (%d)\n", ret);
+                return ret;
+        }
+
+        eink->tcon_regs = devm_regmap_init_mmio(dev, tcon_regs, &eink_tcon_regmap_config);
+        if (IS_ERR(eink->tcon_regs)) {
+		ret = PTR_ERR(eink->tcon_regs);
+                dev_err(dev, "Couldn't create the TCON regmap (%d)\n", ret);
+                return ret;
+        }
+
+        eink->be_regs = devm_regmap_init_mmio(dev, be_regs, &eink_be_regmap_config);
+        if (IS_ERR(eink->be_regs)) {
+		ret = PTR_ERR(eink->be_regs);
+                dev_err(dev, "Couldn't create the BE regmap (%d)\n", ret);
+                return ret;
+        }
+
+	// init the actual hardware
+        ret = reset_control_deassert(eink->rstc);
+        if (ret) {
+                dev_err(dev, "Couldn't deassert our reset line (%d)\n", ret);
+                return ret;
+        }
+
+	// possible to set 27-381MHz in 3MHz steps
+	ret = clk_set_rate(eink->clks[1].clk, 120000000);
+	if (ret) {
+                dev_err(dev, "Couldn't set tcon0 rate to  (%d)\n", ret);
+                goto err_reset;
+	}
+
+	ret = clk_bulk_prepare_enable(eink->num_clks, eink->clks);
+	if (ret) {
+                dev_err(dev, "Couldn't enable clocks (%d)\n", ret);
+                goto err_reset;
+	}
+
+	/* Clear DEBE registers */
+	for (i = 0x800; i < 0x1000; i += 4)
+		regmap_write(eink->be_regs, i, 0);
+
+	/* Disable registers autoloading */
+	regmap_write(eink->be_regs,
+		     SUN4I_BACKEND_REGBUFFCTL_REG,
+		     SUN4I_BACKEND_REGBUFFCTL_AUTOLOAD_DIS);
+
+	regmap_write(eink->be_regs,
+		     SUN4I_BACKEND_MODCTL_REG,
+		     SUN4I_BACKEND_MODCTL_START_CTL);
+
+	/* Enable the backend */
+	regmap_write(eink->be_regs,
+		     SUN4I_BACKEND_MODCTL_REG,
+		     SUN4I_BACKEND_MODCTL_DEBE_EN |
+		     SUN4I_BACKEND_MODCTL_START_CTL |
+		     SUN4I_BACKEND_MODCTL_OUT_LCD0 |
+		     SUN4I_BACKEND_MODCTL_LAY_EN(0));
+
+        /* Make sure the TCON is disabled and all IRQs are off */
+        regmap_write(eink->tcon_regs, SUN4I_TCON_GCTL_REG, 0);
+        regmap_write(eink->tcon_regs, SUN4I_TCON_GINT0_REG, 0);
+        regmap_write(eink->tcon_regs, SUN4I_TCON_GINT1_REG, 0);
+
+        /* Disable IO lines and set them to tristate */
+        regmap_write(eink->tcon_regs, SUN4I_TCON0_IO_TRI_REG, ~0);
+
+        ret = devm_request_irq(dev, eink->irq, eink_tcon0_irq_handler, 0,
+                               dev_name(dev), eink);
+        if (ret) {
+                dev_err(dev, "Couldn't request the IRQ\n");
+                goto err_disable_hw;
+        }
+
+	// create char device
+	ret = alloc_chrdev_region(&eink->major, 0, 1, "eink");
+	if (ret) {
+		dev_err(dev, "can't allocate chrdev region");
+		goto err_disable_hw;
+	}
+
+	cdev_init(&eink->cdev, &eink_fops);
+	eink->cdev.owner = THIS_MODULE;
+	ret = cdev_add(&eink->cdev, eink->major, 1);
+	if (ret) {
+		dev_err(dev, "can't add cdev");
+		goto err_unreg_chrev_region;
+	}
+
+	sdev = device_create(eink_class, dev, eink->major, eink, cdev_name);
+	if (IS_ERR(sdev)) {
+		ret = PTR_ERR(sdev);
+		goto err_del_cdev;
+	}
+
+	eink->ctlstream_size = round_up(cs_size, PAGE_SIZE);
+	eink->ctlstream = dma_alloc_wc(dev, eink->ctlstream_size,
+				       &eink->ctlstream_paddr,
+				       GFP_KERNEL | __GFP_NOWARN);
+	if (!eink->ctlstream) {
+		ret = -ENOMEM;
+		dev_err(dev, "failed to allocate buffer with size %zu\n",
+			eink->ctlstream_size);
+		goto err_destroy_device;
+	}
+
+	eink_ctlstream_prepare(eink);
+
+	eink->wq = alloc_workqueue("eink", WQ_SYSFS | WQ_HIGHPRI, 0);
+	if (!eink->wq) {
+		ret = -ENOMEM;
+		dev_err(dev, "failed to allocate workqueue\n");
+		goto err_free_dma;
+	}
+
+	dev_info(dev, "eink-panel driver ready!\n");
+	return 0;
+
+err_free_dma:
+	dma_free_wc(eink->dev, eink->ctlstream_size, eink->ctlstream,
+		    eink->ctlstream_paddr);
+err_destroy_device:
+	device_destroy(eink_class, eink->major);
+err_del_cdev:
+	cdev_del(&eink->cdev);
+err_unreg_chrev_region:
+	unregister_chrdev(eink->major, "eink");
+err_disable_hw:
+	clk_bulk_disable_unprepare(eink->num_clks, eink->clks);
+err_reset:
+        reset_control_assert(eink->rstc);
+	cancel_work_sync(&eink->work);
+
+	return ret;
+}
+
+static void eink_remove(struct platform_device *pdev)
+{
+	struct eink_dev *eink = platform_get_drvdata(pdev);
+
+	eink_set_power(eink, 0);
+
+	del_timer_sync(&eink->powerdown_timer);
+	cancel_work_sync(&eink->work);
+	destroy_workqueue(eink->wq);
+
+	dma_free_wc(eink->dev, eink->ctlstream_size, eink->ctlstream,
+		    eink->ctlstream_paddr);
+
+	clk_bulk_disable_unprepare(eink->num_clks, eink->clks);
+        reset_control_assert(eink->rstc);
+
+	device_destroy(eink_class, eink->major);
+	cdev_del(&eink->cdev);
+	unregister_chrdev(eink->major, "eink");
+}
+
+static const struct of_device_id eink_of_match[] = {
+	{ .compatible = "custom,pocketbook-touch-lux-3-tcon0-ed060xd4-display" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, eink_of_match);
+
+static struct platform_driver eink_platform_driver = {
+	.probe = eink_probe,
+	.remove = eink_remove,
+	.driver = {
+		.name = "eink_tcon0",
+		.of_match_table = eink_of_match,
+	},
+};
+
+static int __init eink_driver_init(void)
+{
+	int ret;
+
+	eink_class = class_create("eink-panel");
+
+	ret = platform_driver_register(&eink_platform_driver);
+	if (ret)
+		class_destroy(eink_class);
+
+	return ret;
+}
+
+static void __exit eink_driver_exit(void)
+{
+	platform_driver_unregister(&eink_platform_driver);
+	class_destroy(eink_class);
+}
+
+module_init(eink_driver_init);
+module_exit(eink_driver_exit);
+
+MODULE_VERSION("1.0.0");
+MODULE_DESCRIPTION("eInk display Allwinner TCON0 based bit-banging driver");
+MODULE_AUTHOR("Ondrej Jirman <megi@xff.cz>");
+MODULE_LICENSE("GPL v2");
-- 
2.35.3

