diff mbox

[RFC,3/5] drm/tinydrm/lcdreg: Add SPI support

Message ID 1458135259-31050-4-git-send-email-noralf@tronnes.org (mailing list archive)
State New, archived
Headers show

Commit Message

Noralf Trønnes March 16, 2016, 1:34 p.m. UTC
Add SPI bus support to lcdreg.
Supports the following protocols:
- MIPI DBI type C interface option 1 (3-wire) and option 3 (4-wire).
- Option 3 also fits some controllers where all registers are 16-bit.
- 8/16-bit register transfers that need to start with a special startbyte.

It also supports emulation for 16 and 9-bit words if the SPI controller
doesn't support it.

Signed-off-by: Noralf Trønnes <noralf@tronnes.org>
---
 drivers/gpu/drm/tinydrm/lcdreg/Kconfig      |   5 +
 drivers/gpu/drm/tinydrm/lcdreg/Makefile     |   2 +
 drivers/gpu/drm/tinydrm/lcdreg/lcdreg-spi.c | 720 ++++++++++++++++++++++++++++
 include/drm/tinydrm/lcdreg-spi.h            |  63 +++
 4 files changed, 790 insertions(+)
 create mode 100644 drivers/gpu/drm/tinydrm/lcdreg/lcdreg-spi.c
 create mode 100644 include/drm/tinydrm/lcdreg-spi.h
diff mbox

Patch

diff --git a/drivers/gpu/drm/tinydrm/lcdreg/Kconfig b/drivers/gpu/drm/tinydrm/lcdreg/Kconfig
index 41383b1..ade465f 100644
--- a/drivers/gpu/drm/tinydrm/lcdreg/Kconfig
+++ b/drivers/gpu/drm/tinydrm/lcdreg/Kconfig
@@ -1,3 +1,8 @@ 
 config LCDREG
 	tristate
 	depends on GPIOLIB || COMPILE_TEST
+
+config LCDREG_SPI
+	tristate
+	depends on SPI
+	select LCDREG
diff --git a/drivers/gpu/drm/tinydrm/lcdreg/Makefile b/drivers/gpu/drm/tinydrm/lcdreg/Makefile
index c9ea774..4e14571 100644
--- a/drivers/gpu/drm/tinydrm/lcdreg/Makefile
+++ b/drivers/gpu/drm/tinydrm/lcdreg/Makefile
@@ -1,3 +1,5 @@ 
 obj-$(CONFIG_LCDREG)                   += lcdreg.o
 lcdreg-y                               += lcdreg-core.o
 lcdreg-$(CONFIG_DEBUG_FS)              += lcdreg-debugfs.o
+
+obj-$(CONFIG_LCDREG_SPI)               += lcdreg-spi.o
diff --git a/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-spi.c b/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-spi.c
new file mode 100644
index 0000000..5e9d8fe1
--- /dev/null
+++ b/drivers/gpu/drm/tinydrm/lcdreg/lcdreg-spi.c
@@ -0,0 +1,720 @@ 
+//#define VERBOSE_DEBUG
+//#define DEBUG
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <asm/unaligned.h>
+#include <drm/tinydrm/lcdreg-spi.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/dma-mapping.h>
+#include <linux/gpio.h>
+#include <linux/gpio/consumer.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/spi/spi.h>
+
+static unsigned txlen;
+module_param(txlen, uint, 0);
+MODULE_PARM_DESC(txlen, "Transmit chunk length");
+
+static unsigned long bpwm;
+module_param(bpwm, ulong, 0);
+MODULE_PARM_DESC(bpwm, "Override SPI master bits_per_word_mask");
+
+struct lcdreg_spi {
+	struct lcdreg reg;
+	enum lcdreg_spi_mode mode;
+unsigned txbuflen;
+	void *txbuf_dc;
+	unsigned id;
+	u32 quirks;
+	u8 (*startbyte)(struct lcdreg *reg, struct lcdreg_transfer *tr,
+			bool read);
+	struct gpio_desc *dc;
+	struct gpio_desc *reset;
+};
+
+static inline struct lcdreg_spi *to_lcdreg_spi(struct lcdreg *reg)
+{
+	return reg ? container_of(reg, struct lcdreg_spi, reg) : NULL;
+}
+
+#ifdef VERBOSE_DEBUG
+static void lcdreg_vdbg_dump_spi(const struct device *dev, struct spi_message *m, u8 *startbyte)
+{
+	struct spi_transfer *tmp;
+	struct list_head *pos;
+	int i = 0;
+
+	if (startbyte)
+		dev_dbg(dev, "spi_message: startbyte=0x%02X\n", startbyte[0]);
+	else
+		dev_dbg(dev, "spi_message:\n");
+
+	list_for_each(pos, &m->transfers) {
+		tmp = list_entry(pos, struct spi_transfer, transfer_list);
+		if (tmp->tx_buf)
+			pr_debug("    tr%i: bpw=%i, len=%u, tx_buf(%p)=[%*ph]\n", i, tmp->bits_per_word, tmp->len, tmp->tx_buf, tmp->len > 64 ? 64 : tmp->len, tmp->tx_buf);
+		if (tmp->rx_buf)
+			pr_debug("    tr%i: bpw=%i, len=%u, rx_buf(%p)=[%*ph]\n", i, tmp->bits_per_word, tmp->len, tmp->rx_buf, tmp->len > 64 ? 64 : tmp->len, tmp->rx_buf);
+		i++;
+	}
+}
+#else
+static void lcdreg_vdbg_dump_spi(const struct device *dev, struct spi_message *m, u8 *startbyte)
+{
+}
+#endif
+
+static int lcdreg_spi_do_transfer(struct lcdreg *reg,
+				  struct lcdreg_transfer *transfer)
+{
+	struct spi_device *sdev = to_spi_device(reg->dev);
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	void *buf = transfer->buf;
+	size_t len = transfer->count * lcdreg_bytes_per_word(transfer->width);
+	size_t max = txlen ? : sdev->master->max_dma_len;
+	size_t room_left_in_page = PAGE_SIZE - offset_in_page(buf);
+	size_t chunk = min_t(size_t, len, max);
+	struct spi_message m;
+	struct spi_transfer *tr;
+	u8 *startbuf = NULL;
+	int ret, i;
+
+	dev_dbg(reg->dev, "%s: index=%u, count=%u, width=%u\n",
+		__func__, transfer->index, transfer->count, transfer->width);
+	lcdreg_dbg_transfer_buf(transfer);
+
+	tr = kzalloc(2 * sizeof(*tr), GFP_KERNEL);
+	if (!tr)
+		return -ENOMEM;
+
+	/* slow down commands? */
+	if (!transfer->index && (spi->quirks & LCDREG_SLOW_INDEX0_WRITE))
+		for (i = 0; i < 2; i++)
+			tr[i].speed_hz = min_t(u32, 2000000,
+					       sdev->max_speed_hz / 2);
+
+	if (spi->mode == LCDREG_SPI_STARTBYTE) {
+		startbuf = kmalloc(1, GFP_KERNEL);
+		if (!startbuf) {
+			ret = -ENOMEM;
+			goto out;
+		}
+		*startbuf = spi->startbyte(reg, transfer, false);
+	}
+
+	/*
+	 * transfer->buf can be unaligned to the page boundary for partial
+	 * updates when videomem is sent directly (no buffering).
+	 * Spi core can sg map the buffer for dma and relies on vmalloc'ed
+	 * memory to be page aligned.
+	 */
+//pr_debug("%s: PAGE_ALIGNED=%d, len > room_left_in_page= %d > %d = %d, chunk=%zu\n", __func__, PAGE_ALIGNED(buf), len, room_left_in_page, len > room_left_in_page, chunk);
+	if (!PAGE_ALIGNED(buf) && len > room_left_in_page) {
+//size_t chunk0 = chunk;
+
+		if (chunk >= room_left_in_page) {
+			chunk = room_left_in_page;
+//pr_debug("%s: chunk: %zu -> %zu, room_left_in_page=%zu\n\n", __func__, chunk0, chunk, room_left_in_page);
+		} else {
+			chunk = room_left_in_page % chunk ? : chunk;
+//pr_debug("%s: chunk: %zu -> %zu, room_left_in_page=%zu, room_left_in_page %% chunk=%zu\n\n", __func__, chunk0, chunk, room_left_in_page, room_left_in_page % chunk);
+		}
+	}
+
+	do {
+		i = 0;
+		spi_message_init(&m);
+
+		if (spi->mode == LCDREG_SPI_STARTBYTE) {
+			tr[i].tx_buf = startbuf;
+			tr[i].len = 1;
+			tr[i].bits_per_word = 8;
+			spi_message_add_tail(&tr[i++], &m);
+		}
+
+		tr[i].tx_buf = buf;
+		tr[i].len = chunk;
+		tr[i].bits_per_word = transfer->width;
+		buf += chunk;
+		len -= chunk;
+		spi_message_add_tail(&tr[i], &m);
+
+		lcdreg_vdbg_dump_spi(&sdev->dev, &m, startbuf);
+		ret = spi_sync(sdev, &m);
+		if (ret)
+			goto out;
+
+		chunk = min_t(size_t, len, max);
+	} while (len);
+
+out:
+	kfree(tr);
+	kfree(startbuf);
+
+	return ret;
+}
+
+static int lcdreg_spi_transfer_emulate9(struct lcdreg *reg,
+					struct lcdreg_transfer *transfer)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	struct lcdreg_transfer tr = {
+		.index = transfer->index,
+		.width = 8,
+		.count = transfer->count,
+	};
+	u16 *src = transfer->buf;
+	unsigned added = 0;
+	int i, ret;
+	u8 *dst;
+
+	if (transfer->count % 8) {
+		dev_err_once(reg->dev,
+			     "transfer->count=%u must be divisible by 8\n",
+			     transfer->count);
+		return -EINVAL;
+	}
+
+	dst = kzalloc(spi->txbuflen, GFP_KERNEL);
+	if (!dst)
+		return -ENOMEM;
+
+	tr.buf = dst;
+
+	for (i = 0; i < transfer->count; i += 8) {
+		u64 tmp = 0;
+		int j, bits = 63;
+
+		for (j = 0; j < 7; j++) {
+			u64 bit9 = (*src & 0x100) ? 1 : 0;
+			u64 val = *src++ & 0xFF;
+
+			tmp |= bit9 << bits;
+			bits -= 8;
+			tmp |= val << bits--;
+		}
+		tmp |= ((*src & 0x100) ? 1 : 0);
+		*(u64 *)dst = cpu_to_be64(tmp);
+		dst += 8;
+		*dst++ = *src++ & 0xFF;
+		added++;
+	}
+	tr.count += added;
+	ret = lcdreg_spi_do_transfer(reg, &tr);
+	kfree(tr.buf);
+
+	return ret;
+}
+
+static int lcdreg_spi_transfer_emulate16(struct lcdreg *reg,
+					 struct lcdreg_transfer *transfer)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	unsigned to_copy, remain = transfer->count;
+	struct lcdreg_transfer tr = {
+		.index = transfer->index,
+		.width = 8,
+	};
+	u16 *data16 = transfer->buf;
+	u16 *txbuf16;
+	int i, ret = 0;
+
+	txbuf16 = kzalloc(spi->txbuflen, GFP_KERNEL);
+	if (!txbuf16)
+		return -ENOMEM;
+
+	tr.buf = txbuf16;
+
+	while (remain) {
+		to_copy = min(remain, spi->txbuflen / 2);
+		dev_dbg(reg->dev, "    to_copy=%zu, remain=%zu\n",
+					to_copy, remain - to_copy);
+
+		for (i = 0; i < to_copy; i++)
+			txbuf16[i] = swab16(data16[i]);
+
+		data16 = data16 + to_copy;
+		tr.count = to_copy * 2;
+		ret = lcdreg_spi_do_transfer(reg, &tr);
+		if (ret < 0)
+			goto out;
+		remain -= to_copy;
+	}
+
+out:
+	kfree(tr.buf);
+
+	return ret;
+}
+
+static int lcdreg_spi_transfer(struct lcdreg *reg,
+			       struct lcdreg_transfer *transfer)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	bool mach_little_endian;
+
+#ifdef __LITTLE_ENDIAN
+	mach_little_endian = true;
+#endif
+	if (spi->dc)
+		gpiod_set_value_cansleep(spi->dc, transfer->index);
+
+	if (lcdreg_bpw_supported(reg, transfer->width))
+		return lcdreg_spi_do_transfer(reg, transfer);
+
+	if (transfer->width == 9)
+		return lcdreg_spi_transfer_emulate9(reg, transfer);
+
+	if ((mach_little_endian == reg->little_endian) &&
+	    (transfer->width % 8 == 0)) {
+		/* the byte order matches */
+		transfer->count *= transfer->width / 8;
+		transfer->width = 8;
+		return lcdreg_spi_do_transfer(reg, transfer);
+	}
+
+	if (mach_little_endian != reg->little_endian && transfer->width == 16)
+		return lcdreg_spi_transfer_emulate16(reg, transfer);
+
+	dev_err_once(reg->dev, "width=%u is not supported (%u:%u)\n",
+		     transfer->width, mach_little_endian, reg->little_endian);
+
+	return -EINVAL;
+}
+
+static int lcdreg_spi_write_9bit_dc(struct lcdreg *reg,
+				    struct lcdreg_transfer *transfer)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	struct lcdreg_transfer tr = {
+		.index = transfer->index,
+	};
+	u8 *data8 = transfer->buf;
+	u16 *data16 = transfer->buf;
+	unsigned width;
+	u16 *txbuf16;
+	unsigned remain;
+	unsigned tx_array_size;
+	unsigned to_copy;
+	int pad, i, ret;
+
+width = transfer->width;
+
+	if (width != 8 && width != 16) {
+		dev_err(reg->dev, "transfer width %u is not supported\n",
+								width);
+		return -EINVAL;
+	}
+
+	if (!spi->txbuf_dc) {
+		spi->txbuf_dc = devm_kzalloc(reg->dev, spi->txbuflen,
+							GFP_KERNEL);
+		if (!spi->txbuf_dc)
+			return -ENOMEM;
+		dev_info(reg->dev, "allocated %u KiB 9-bit dc buffer\n",
+						spi->txbuflen / 1024);
+	}
+
+	tr.buf = spi->txbuf_dc;
+	txbuf16 = spi->txbuf_dc;
+	remain = transfer->count;
+	if (width == 8)
+		tx_array_size = spi->txbuflen / 2;
+	else
+		tx_array_size = spi->txbuflen / 4;
+
+	/* If we're emulating 9-bit, the buffer has to be divisible by 8.
+	   Pad with no-ops if necessary (assuming here that zero is a no-op)
+	   FIX: If video buf isn't divisible by 8, it will break.
+	 */
+	if (!lcdreg_bpw_supported(reg, 9) && width == 8 &&
+						remain < tx_array_size) {
+		pad = (transfer->count % 8) ? 8 - (transfer->count % 8) : 0;
+		if (transfer->index == 0)
+			for (i = 0; i < pad; i++)
+				*txbuf16++ = 0x000;
+		for (i = 0; i < remain; i++) {
+			*txbuf16 = *data8++;
+			if (transfer->index)
+				*txbuf16++ |= 0x0100;
+		}
+		if (transfer->index == 1)
+			for (i = 0; i < pad; i++)
+				*txbuf16++ = 0x000;
+		tr.width = 9;
+		tr.count = pad + remain;
+		return lcdreg_spi_transfer(reg, &tr);
+	}
+
+	while (remain) {
+		to_copy = remain > tx_array_size ? tx_array_size : remain;
+		remain -= to_copy;
+		dev_dbg(reg->dev, "    to_copy=%zu, remain=%zu\n",
+					to_copy, remain);
+
+		if (width == 8) {
+			for (i = 0; i < to_copy; i++) {
+				txbuf16[i] = *data8++;
+				if (transfer->index)
+					txbuf16[i] |= 0x0100;
+			}
+		} else {
+			for (i = 0; i < (to_copy * 2); i += 2) {
+				txbuf16[i]     = *data16 >> 8;
+				txbuf16[i + 1] = *data16++ & 0xFF;
+				if (transfer->index) {
+					txbuf16[i]     |= 0x0100;
+					txbuf16[i + 1] |= 0x0100;
+				}
+			}
+		}
+		tr.buf = spi->txbuf_dc;
+		tr.width = 9;
+		tr.count = to_copy * 2;
+		ret = lcdreg_spi_transfer(reg, &tr);
+		if (ret < 0)
+			return ret;
+	}
+
+	return 0;
+}
+
+static int lcdreg_spi_write(struct lcdreg *reg, unsigned regnr,
+			    struct lcdreg_transfer *transfer)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	struct lcdreg_transfer tr = {
+		.width = reg->def_width,
+		.count = 1,
+	};
+	int ret;
+
+	tr.buf = kmalloc(sizeof(u32), GFP_KERNEL);
+	if (!tr.buf)
+		return -ENOMEM;
+
+	if (reg->def_width <= 8)
+		((u8 *)tr.buf)[0] = regnr;
+	else
+		((u16 *)tr.buf)[0] = regnr;
+
+	if (spi->mode == LCDREG_SPI_3WIRE)
+		ret = lcdreg_spi_write_9bit_dc(reg, &tr);
+	else
+		ret = lcdreg_spi_transfer(reg, &tr);
+	kfree(tr.buf);
+	if (ret || !transfer || !transfer->count)
+		return ret;
+
+	if (!transfer->width)
+		transfer->width = reg->def_width;
+	if (spi->mode == LCDREG_SPI_3WIRE)
+		ret = lcdreg_spi_write_9bit_dc(reg, transfer);
+	else
+		ret = lcdreg_spi_transfer(reg, transfer);
+
+	return ret;
+}
+
+/*
+   <CMD> <DM> <PA>
+   CMD = Command
+   DM = Dummy read
+   PA = Parameter or display data
+
+   ST7735R read:
+     Parallel: <CMD> <DM> <PA>
+     SPI: 8-bit plain, 24- and 32-bit needs 1 dummy clock cycle
+
+   ILI9320:
+     Parallel: no dummy read, page 51 in datasheet
+     SPI (startbyte): One byte of invalid dummy data read after the start byte.
+
+   ILI9340:
+     Parallel: no info about dummy read
+     SPI: same as ST7735R
+
+   ILI9341:
+     Parallel: no info about dummy read
+     SPI: same as ST7735R
+
+   SSD1289:
+     Parallel: 1 dummy read
+
+ */
+
+static int lcdreg_spi_read_startbyte(struct lcdreg *reg, unsigned regnr,
+				     struct lcdreg_transfer *transfer)
+{
+	struct spi_device *sdev = to_spi_device(reg->dev);
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	struct spi_message m;
+	struct spi_transfer trtx = {
+		.speed_hz = min_t(u32, 2000000, sdev->max_speed_hz / 2),
+		.bits_per_word = 8,
+		.len = 1,
+	};
+	struct spi_transfer trrx = {
+		.speed_hz = trtx.speed_hz,
+		.bits_per_word = 8,
+		.len = (transfer->count * 2) + 1,
+	};
+	u8 *txbuf, *rxbuf;
+	int i, ret;
+
+	if (!reg->readable)
+		return -EACCES;
+
+	if (!transfer || !transfer->count)
+		return -EINVAL;
+
+	transfer->width = transfer->width ? : reg->def_width;
+	if (WARN_ON(transfer->width != 16))
+		return -EINVAL;
+
+	ret = lcdreg_writereg(reg, regnr);
+	if (ret)
+		return ret;
+
+	txbuf = kzalloc(1, GFP_KERNEL);
+	if (!txbuf)
+		return -ENOMEM;
+
+	rxbuf = kzalloc(trrx.len, GFP_KERNEL);
+	if (!rxbuf) {
+		kfree(txbuf);
+		return -ENOMEM;
+	}
+
+	*txbuf = spi->startbyte(reg, transfer, true);
+
+	trtx.tx_buf = txbuf;
+	trrx.rx_buf = rxbuf;
+	spi_message_init(&m);
+	spi_message_add_tail(&trtx, &m);
+	spi_message_add_tail(&trrx, &m);
+	ret = spi_sync(sdev, &m);
+	lcdreg_vdbg_dump_spi(&sdev->dev, &m, txbuf);
+	kfree(txbuf);
+	if (ret) {
+		kfree(rxbuf);
+		return ret;
+	}
+
+	rxbuf++;
+	for (i = 0; i < transfer->count; i++) {
+		((u16 *)transfer->buf)[i] = get_unaligned_be16(rxbuf);
+		rxbuf += 2;
+	}
+	kfree(trrx.rx_buf);
+
+	return 0;
+}
+
+static int lcdreg_spi_read(struct lcdreg *reg, unsigned regnr,
+			   struct lcdreg_transfer *transfer)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+	struct spi_device *sdev = to_spi_device(reg->dev);
+	struct spi_message m;
+	struct spi_transfer trtx = {
+		.speed_hz = min_t(u32, 2000000, sdev->max_speed_hz / 2),
+		.bits_per_word = reg->def_width,
+		.len = 1,
+	};
+	struct spi_transfer trrx = {
+		.speed_hz = trtx.speed_hz,
+		.rx_buf = transfer->buf,
+		.len = transfer->count,
+	};
+	void *txbuf = NULL;
+	int i, ret;
+
+	transfer->width = transfer->width ? : reg->def_width;
+	if (WARN_ON(transfer->width != reg->def_width || !transfer->count))
+		return -EINVAL;
+
+	if (!reg->readable)
+		return -EACCES;
+
+	txbuf = kzalloc(16, GFP_KERNEL);
+	if (!txbuf)
+		return -ENOMEM;
+
+	spi_message_init(&m);
+	trtx.tx_buf = txbuf;
+	trrx.bits_per_word = transfer->width;
+
+	if (spi->mode == LCDREG_SPI_4WIRE) {
+		if (trtx.bits_per_word == 8) {
+			*(u8 *)txbuf = regnr;
+		} else if (trtx.bits_per_word == 16) {
+			if (lcdreg_bpw_supported(reg, trtx.bits_per_word)) {
+				*(u16 *)txbuf = regnr;
+			} else {
+				*(u16 *)txbuf = cpu_to_be16(regnr);
+				trtx.bits_per_word = 8;
+				trtx.len = 2;
+			}
+		} else {
+			return -EINVAL;
+		}
+		gpiod_set_value_cansleep(spi->dc, 0);
+	} else if (spi->mode == LCDREG_SPI_3WIRE) {
+		if (lcdreg_bpw_supported(reg, 9)) {
+			trtx.bits_per_word = 9;
+			*(u16 *)txbuf = regnr; /* dc=0 */
+		} else {
+			/* 8x 9-bit words, pad with leading zeroes (no-ops) */
+			((u8 *)txbuf)[8] = regnr;
+		}
+	} else {
+		kfree(txbuf);
+		return -EINVAL;
+	}
+	spi_message_add_tail(&trtx, &m);
+
+	if (spi->mode == LCDREG_SPI_4WIRE && transfer->index &&
+	    !(spi->quirks & LCDREG_INDEX0_ON_READ)) {
+		trtx.cs_change = 1; /* not always supported */
+		lcdreg_vdbg_dump_spi(&sdev->dev, &m, NULL);
+		ret = spi_sync(sdev, &m);
+		if (ret) {
+			kfree(txbuf);
+			return ret;
+		}
+		gpiod_set_value_cansleep(spi->dc, 1);
+		spi_message_init(&m);
+	}
+
+	spi_message_add_tail(&trrx, &m);
+	ret = spi_sync(sdev, &m);
+	lcdreg_vdbg_dump_spi(&sdev->dev, &m, NULL);
+	kfree(txbuf);
+	if (ret)
+		return ret;
+
+	if (!lcdreg_bpw_supported(reg, trrx.bits_per_word) &&
+						(trrx.bits_per_word == 16))
+		for (i = 0; i < transfer->count; i++)
+			((u16 *)transfer->buf)[i] = be16_to_cpu(((u16 *)transfer->buf)[i]);
+
+	return 0;
+}
+
+static void lcdreg_spi_reset(struct lcdreg *reg)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+
+	if (!spi->reset)
+		return;
+
+	dev_info(reg->dev, "%s()\n", __func__);
+	gpiod_set_value_cansleep(spi->reset, 0);
+	msleep(20);
+	gpiod_set_value_cansleep(spi->reset, 1);
+	msleep(120);
+}
+
+/* Default startbyte implementation: | 0 | 1 | 1 | 1 | 0 | ID | RS | RW | */
+static u8 lcdreg_spi_startbyte(struct lcdreg *reg, struct lcdreg_transfer *tr,
+			       bool read)
+{
+	struct lcdreg_spi *spi = to_lcdreg_spi(reg);
+
+	return 0x70 | (!!spi->id << 2) | (!!tr->index << 1) | read;
+}
+
+struct lcdreg *devm_lcdreg_spi_init(struct spi_device *sdev,
+				    const struct lcdreg_spi_config *config)
+{
+	char *dc_name = config->dc_name ? : "dc";
+	struct device *dev = &sdev->dev;
+	struct lcdreg_spi *spi;
+	struct lcdreg *reg;
+
+	if (txlen) {
+		if (txlen < PAGE_SIZE) {
+			txlen = rounddown_pow_of_two(txlen);
+			if (txlen < 64)
+				txlen = 64;
+		} else {
+			txlen &= PAGE_MASK;
+		}
+	}
+dev_info(dev, "txlen: %u\n", txlen);
+
+	spi = devm_kzalloc(dev, sizeof(*spi), GFP_KERNEL);
+	if (!spi)
+		return ERR_PTR(-ENOMEM);
+
+	reg = &spi->reg;
+	if (bpwm) {
+		reg->bits_per_word_mask = bpwm;
+	} else {
+		if (sdev->master->bits_per_word_mask)
+			reg->bits_per_word_mask = sdev->master->bits_per_word_mask;
+		else
+			reg->bits_per_word_mask = SPI_BPW_MASK(8);
+	}
+	dev_dbg(dev, "bits_per_word_mask: 0x%08x", reg->bits_per_word_mask);
+
+	reg->def_width = config->def_width;
+	reg->readable = config->readable;
+	reg->reset = lcdreg_spi_reset;
+	reg->write = lcdreg_spi_write;
+	reg->read = lcdreg_spi_read;
+
+	spi->mode = config->mode;
+	spi->quirks = config->quirks;
+	spi->id = config->id;
+	if (!spi->txbuflen)
+		spi->txbuflen = PAGE_SIZE;
+
+	switch (spi->mode) {
+	case LCDREG_SPI_4WIRE:
+		spi->dc = devm_gpiod_get(dev, dc_name, GPIOD_OUT_LOW);
+		if (IS_ERR(spi->dc)) {
+			dev_err(dev, "Failed to get gpio '%s'\n", dc_name);
+			return ERR_CAST(spi->dc);
+		}
+		break;
+	case LCDREG_SPI_3WIRE:
+		break;
+	case LCDREG_SPI_STARTBYTE:
+		reg->read = lcdreg_spi_read_startbyte;
+		spi->startbyte = config->startbyte ? : lcdreg_spi_startbyte;
+		break;
+	default:
+		dev_err(dev, "Mode is not supported: %u\n", spi->mode);
+		return ERR_PTR(-EINVAL);
+	}
+
+	spi->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(spi->reset)) {
+		dev_err(dev, "Failed to get gpio 'reset'\n");
+		return ERR_CAST(spi->reset);
+	}
+
+	pr_debug("spi->reg.def_width: %u\n", reg->def_width);
+	if (spi->reset)
+		pr_debug("spi->reset: %i\n", desc_to_gpio(spi->reset));
+	if (spi->dc)
+		pr_debug("spi->dc: %i\n", desc_to_gpio(spi->dc));
+	pr_debug("spi->mode: %u\n", spi->mode);
+
+	return devm_lcdreg_init(dev, reg);
+}
+EXPORT_SYMBOL_GPL(devm_lcdreg_spi_init);
+
+MODULE_LICENSE("GPL");
diff --git a/include/drm/tinydrm/lcdreg-spi.h b/include/drm/tinydrm/lcdreg-spi.h
new file mode 100644
index 0000000..ba5d492
--- /dev/null
+++ b/include/drm/tinydrm/lcdreg-spi.h
@@ -0,0 +1,63 @@ 
+/*
+ * Copyright (C) 2016 Noralf Trønnes
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef __LINUX_LCDREG_SPI_H
+#define __LINUX_LCDREG_SPI_H
+
+#include <drm/tinydrm/lcdreg.h>
+#include <linux/spi/spi.h>
+
+/**
+ * enum lcdreg_spi_mode - SPI interface mode
+ * @LCDREG_SPI_4WIRE: 8-bit + D/CX line, MIPI DBI Type C option 3
+ * @LCDREG_SPI_3WIRE: 9-bit inc. D/CX bit, MIPI DBI Type C option 1
+ * @LCDREG_SPI_STARTBYTE: Startbyte header on every transaction (non MIPI)
+ */
+enum lcdreg_spi_mode {
+	LCDREG_SPI_NOMODE = 0,
+	LCDREG_SPI_4WIRE,
+	LCDREG_SPI_3WIRE,
+	LCDREG_SPI_STARTBYTE,
+};
+
+/**
+ * struct lcdreg_spi_config - SPI interface configuration
+ * @mode: Register interface mode
+ * @def_width: Default register width
+ * @readable: Is the register readable, not all displays have MISO wired.
+ * @id: Display id used with LCDREG_SPI_STARTBYTE
+ * @dc_name: Index pin name, usually dc, rs or di (default is 'dc').
+ * @quirks: Deviations from the MIPI DBI standard
+ * @startbyte: Used with LCDREG_SPI_STARTBYTE to get the startbyte
+ *             (default is lcdreg_spi_startbyte).
+ */
+struct lcdreg_spi_config {
+	enum lcdreg_spi_mode mode;
+	unsigned def_width;
+	bool readable;
+	u32 id;
+	char *dc_name;
+	u32 quirks;
+/* slowdown command (index=0) */
+#define LCDREG_SLOW_INDEX0_WRITE	BIT(0)
+/*
+ * The MIPI DBI spec states that D/C should be HIGH during register reading.
+ * However, not all SPI master drivers support cs_change on last transfer and
+ * there are LCD controllers that ignore D/C on read.
+ */
+#define LCDREG_INDEX0_ON_READ		BIT(1)
+
+	u8 (*startbyte)(struct lcdreg *reg, struct lcdreg_transfer *tr,
+			bool read);
+};
+
+struct lcdreg *devm_lcdreg_spi_init(struct spi_device *sdev,
+				    const struct lcdreg_spi_config *config);
+
+#endif /* __LINUX_LCDREG_SPI_H */