1278 lines
37 KiB
Diff
1278 lines
37 KiB
Diff
|
From 2574a07834efde66e75ddf6db928f568ed65237e 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: [PATCH 074/469] 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 | 1158 +++++++++++++++++++++++++
|
||
|
4 files changed, 1223 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 0fdf5f46802c..7a7e80b82d76 100644
|
||
|
--- a/drivers/video/fbdev/Kconfig
|
||
|
+++ b/drivers/video/fbdev/Kconfig
|
||
|
@@ -879,6 +879,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 e6b0ae094b8b..f4ce3358ddec 100644
|
||
|
--- a/drivers/video/fbdev/Makefile
|
||
|
+++ b/drivers/video/fbdev/Makefile
|
||
|
@@ -129,3 +129,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..daceb0613922
|
||
|
--- /dev/null
|
||
|
+++ b/drivers/video/fbdev/sun5i-eink.c
|
||
|
@@ -0,0 +1,1158 @@
|
||
|
+/*
|
||
|
+ * 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, false, false);
|
||
|
+ 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 int 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");
|
||
|
+
|
||
|
+ return 0;
|
||
|
+}
|
||
|
+
|
||
|
+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.34.1
|
||
|
|