All of lore.kernel.org
 help / color / mirror / Atom feed
From: Samuel Holland <samuel@sholland.org>
To: "Heiko Stübner" <heiko@sntech.de>,
	"Sandy Huang" <hjc@rock-chips.com>,
	dri-devel@lists.freedesktop.org
Cc: linux-rockchip@lists.infradead.org,
	"Alistair Francis" <alistair@alistair23.me>,
	"Ondřej Jirman" <x@xff.cz>,
	"Andreas Kemnade" <andreas@kemnade.info>,
	"Daniel Vetter" <daniel@ffwll.ch>,
	"David Airlie" <airlied@linux.ie>,
	"Geert Uytterhoeven" <geert@linux-m68k.org>,
	"Samuel Holland" <samuel@sholland.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Liang Chen" <cl@rock-chips.com>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Michael Riesch" <michael.riesch@wolfvision.net>,
	"Nicolas Frattaroli" <frattaroli.nicolas@gmail.com>,
	"Peter Geis" <pgwipeout@gmail.com>,
	"Rob Herring" <robh+dt@kernel.org>,
	"Sam Ravnborg" <sam@ravnborg.org>,
	"Thierry Reding" <thierry.reding@gmail.com>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	devicetree@vger.kernel.org, linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org
Subject: [RFC PATCH 01/16] drm: Add a helper library for EPD drivers
Date: Wed, 13 Apr 2022 17:19:01 -0500	[thread overview]
Message-ID: <20220413221916.50995-2-samuel@sholland.org> (raw)
In-Reply-To: <20220413221916.50995-1-samuel@sholland.org>

Currently, this library is focused on LUTs and LUT files, specifically
the PVI wbf-format files shipped with E-Ink displays. It provides
helpers to load and validate LUT files, and extract LUTs from them.

Since EPD controllers need the LUTs in various formats, this library
allows drivers to declare their desired format. It will then convert
LUTs to that format before returning them.

Signed-off-by: Samuel Holland <samuel@sholland.org>
---

 drivers/gpu/drm/Kconfig          |   6 +
 drivers/gpu/drm/Makefile         |   2 +
 drivers/gpu/drm/drm_epd_helper.c | 663 +++++++++++++++++++++++++++++++
 include/drm/drm_epd_helper.h     | 104 +++++
 4 files changed, 775 insertions(+)
 create mode 100644 drivers/gpu/drm/drm_epd_helper.c
 create mode 100644 include/drm/drm_epd_helper.h

diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index f1422bee3dcc..ad96cf605444 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -198,6 +198,12 @@ config DRM_DP_CEC
 	  Note: not all adapters support this feature, and even for those
 	  that do support this they often do not hook up the CEC pin.
 
+config DRM_EPD_HELPER
+	tristate
+	depends on DRM
+	help
+	  Choose this if you need the EPD (LUT, etc.) helper functions
+
 config DRM_TTM
 	tristate
 	depends on DRM && MMU
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index c2ef5f9fce54..49380ccfe9d6 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -33,6 +33,8 @@ drm-$(CONFIG_DRM_PRIVACY_SCREEN) += drm_privacy_screen.o drm_privacy_screen_x86.
 
 obj-$(CONFIG_DRM_NOMODESET) += drm_nomodeset.o
 
+obj-$(CONFIG_DRM_EPD_HELPER) += drm_epd_helper.o
+
 drm_cma_helper-y := drm_gem_cma_helper.o
 drm_cma_helper-$(CONFIG_DRM_KMS_HELPER) += drm_fb_cma_helper.o
 obj-$(CONFIG_DRM_GEM_CMA_HELPER) += drm_cma_helper.o
diff --git a/drivers/gpu/drm/drm_epd_helper.c b/drivers/gpu/drm/drm_epd_helper.c
new file mode 100644
index 000000000000..433a6728ef3e
--- /dev/null
+++ b/drivers/gpu/drm/drm_epd_helper.c
@@ -0,0 +1,663 @@
+// SPDX-License-Identifier: GPL-2.0 OR MIT
+/*
+ * Electrophoretic Display Helper Library
+ *
+ * Copyright (C) 2022 Samuel Holland <samuel@sholland.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sub license, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the
+ * next paragraph) shall be included in all copies or substantial portions
+ * of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDERS, AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <linux/firmware.h>
+#include <linux/module.h>
+#include <linux/vmalloc.h>
+
+#include <drm/drm_device.h>
+#include <drm/drm_epd_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_print.h>
+
+/**
+ * DOC: overview
+ *
+ * This library provides functions for working with the lookup tables (LUTs)
+ * used by electrophoretic displays (EPDs). It fills in a LUT buffer based on
+ * the selected waveform, the panel temperature, and the buffer format needed
+ * by the driver.
+ */
+
+struct pvi_wbf_offset {
+	u8			b[3];
+};
+
+struct pvi_wbf_pointer {
+	struct pvi_wbf_offset	offset;
+	u8			checksum;
+};
+
+struct pvi_wbf_file_header {
+	__le32			checksum;		// 0x00
+	__le32			file_size;		// 0x04
+	__le32			serial;			// 0x08
+	u8			run_type;		// 0x0c
+	u8			fpl_platform;		// 0x0d
+	__le16			fpl_lot;		// 0x0e
+	u8			mode_version;		// 0x10
+	u8			wf_version;		// 0x11
+	u8			wf_subversion;		// 0x12
+	u8			wf_type;		// 0x13
+	u8			panel_size;		// 0x14
+	u8			amepd_part_number;	// 0x15
+	u8			wf_rev;			// 0x16
+	u8			frame_rate_bcd;		// 0x17
+	u8			frame_rate_hex;		// 0x18
+	u8			vcom_offset;		// 0x19
+	u8			unknown[2];		// 0x1a
+	struct pvi_wbf_offset	xwia;			// 0x1c
+	u8			cs1;			// 0x1f
+	struct pvi_wbf_offset	wmta;			// 0x20
+	u8			fvsn;			// 0x23
+	u8			luts;			// 0x24
+	u8			mode_count;		// 0x25
+	u8			temp_range_count;	// 0x26
+	u8			advanced_wf_flags;	// 0x27
+	u8			eb;			// 0x28
+	u8			sb;			// 0x29
+	u8			reserved[5];		// 0x2a
+	u8			cs2;			// 0x2f
+	u8			temp_range_table[];	// 0x30
+};
+static_assert(sizeof(struct pvi_wbf_file_header) == 0x30);
+
+struct pvi_wbf_mode_info {
+	u8			versions[2];
+	u8			format;
+	u8			modes[DRM_EPD_WF_MAX];
+};
+
+static const struct pvi_wbf_mode_info pvi_wbf_mode_info_table[] = {
+	{
+		.versions = {
+			0x09,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x12,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 3,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 5,
+			[DRM_EPD_WF_GLD16]	= 6,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x16,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x18,
+			0x20,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x19,
+			0x43,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x23,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 5,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x54,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 5,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+};
+
+static const void *pvi_wbf_apply_offset(const struct drm_epd_lut_file *file,
+					const struct pvi_wbf_offset *offset)
+{
+	u32 bytes = offset->b[0] | offset->b[1] << 8 | offset->b[2] << 16;
+
+	if (bytes >= file->fw->size)
+		return NULL;
+
+	return (const void *)file->header + bytes;
+}
+
+static const void *pvi_wbf_dereference(const struct drm_epd_lut_file *file,
+				       const struct pvi_wbf_pointer *ptr)
+{
+	u8 sum = ptr->offset.b[0] + ptr->offset.b[1] + ptr->offset.b[2];
+
+	if (ptr->checksum != sum)
+		return NULL;
+
+	return pvi_wbf_apply_offset(file, &ptr->offset);
+}
+
+static int pvi_wbf_get_mode_index(const struct drm_epd_lut_file *file,
+				  enum drm_epd_waveform waveform)
+{
+	if (waveform >= DRM_EPD_WF_MAX)
+		return -EINVAL;
+
+	return file->mode_info->modes[waveform];
+}
+
+static int pvi_wbf_get_mode_info(struct drm_epd_lut_file *file)
+{
+	u8 mode_version = file->header->mode_version;
+	const struct pvi_wbf_mode_info *mode_info;
+	int i, v;
+
+	for (i = 0; i < ARRAY_SIZE(pvi_wbf_mode_info_table); i++) {
+		mode_info = &pvi_wbf_mode_info_table[i];
+
+		for (v = 0; v < ARRAY_SIZE(mode_info->versions); v++) {
+			if (mode_info->versions[v] == mode_version) {
+				file->mode_info = mode_info;
+				return 0;
+			}
+		}
+	}
+
+	drm_err(file->dev, "Unknown PVI waveform version 0x%02x\n",
+		mode_version);
+
+	return -EOPNOTSUPP;
+}
+
+static int pvi_wbf_get_temp_index(const struct drm_epd_lut_file *file,
+				  int temperature)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int i;
+
+	for (i = 0; i < header->temp_range_count; i++)
+		if (temperature < header->temp_range_table[i])
+			return i - 1;
+
+	return header->temp_range_count - 1;
+}
+
+static int pvi_wbf_validate_header(struct drm_epd_lut_file *file)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int ret;
+
+	if (le32_to_cpu(header->file_size) > file->fw->size)
+		return -EINVAL;
+
+	ret = pvi_wbf_get_mode_info(file);
+	if (ret)
+		return ret;
+
+	drm_info(file->dev, "Loaded %d-bit PVI waveform version 0x%02x\n",
+		 file->mode_info->format == DRM_EPD_LUT_5BIT_PACKED ? 5 : 4,
+		 header->mode_version);
+
+	return 0;
+}
+
+static void drm_epd_lut_file_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut_file *file = res;
+
+	release_firmware(file->fw);
+}
+
+/**
+ * drmm_epd_lut_file_init - Initialize a managed EPD LUT file
+ *
+ * @dev: The DRM device owning this LUT file
+ * @file: The LUT file to initialize
+ * @file_name: The filesystem name of the LUT file
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name)
+{
+	int ret;
+
+	ret = request_firmware(&file->fw, file_name, dev->dev);
+	if (ret)
+		return ret;
+
+	ret = drmm_add_action_or_reset(dev, drm_epd_lut_file_free, file);
+	if (ret)
+		return ret;
+
+	file->dev = dev;
+	file->header = (const void *)file->fw->data;
+
+	ret = pvi_wbf_validate_header(file);
+	if (ret)
+		return ret;
+
+	/* Only 5-bit waveform files are supported by drm_epd_lut_convert. */
+	if (file->mode_info->format != DRM_EPD_LUT_5BIT_PACKED)
+		return -EOPNOTSUPP;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_file_init);
+
+/**
+ * drm_epd_lut_size_shift - Get the size of a LUT phase in power-of-2 bytes
+ *
+ * @format: One of the LUT buffer formats
+ *
+ * Return: buffer size shift amount
+ */
+static int drm_epd_lut_size_shift(enum drm_epd_lut_format format)
+{
+	switch (format) {
+	case DRM_EPD_LUT_4BIT:
+		/* (4 bits/pixel)^2 / 1 pixel/byte */
+		return 4 + 4;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		/* (4 bits/pixel)^2 / 4 pixels/byte */
+		return 4 + 4 - 2;
+	case DRM_EPD_LUT_5BIT:
+		/* (5 bits/pixel)^2 / 1 pixel/byte */
+		return 5 + 5;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* (5 bits/pixel)^2 / 4 pixels/byte */
+		return 5 + 5 - 2;
+	}
+
+	unreachable();
+}
+
+static int pvi_wbf_decode_lut(struct drm_epd_lut *lut, const u8 *lut_data)
+{
+
+	unsigned int copies, max_bytes, size_shift, state, total_bytes;
+	struct drm_device *dev = lut->file->dev;
+	const u8 *in = lut_data;
+	u8 *out = lut->buf;
+	u8 token;
+
+	size_shift = drm_epd_lut_size_shift(lut->file->mode_info->format);
+	max_bytes = lut->max_phases << size_shift;
+	total_bytes = 0;
+	state = 1;
+
+	/* Read tokens until reaching the end-of-input marker. */
+	while ((token = *in++) != 0xff) {
+		/* Special handling for the state switch token. */
+		if (token == 0xfc) {
+			state = !state;
+			token = *in++;
+		}
+
+		/*
+		 * State 0 is a sequence of data bytes.
+		 * State 1 is a sequence of [data byte, extra copies] pairs.
+		 */
+		copies = 1 + (state ? *in++ : 0);
+
+		total_bytes += copies;
+		if (total_bytes > max_bytes) {
+			drm_err(dev, "LUT contains too many phases\n");
+			lut->num_phases = 0;
+			return -EILSEQ;
+		}
+
+		while (copies--)
+			*out++ = token;
+	}
+
+	lut->num_phases = total_bytes >> size_shift;
+	if (total_bytes != lut->num_phases << size_shift) {
+		drm_err(dev, "LUT contains a partial phase\n");
+		lut->num_phases = 0;
+		return -EILSEQ;
+	}
+
+	drm_dbg_core(dev, "LUT contains %d phases (%ld => %ld bytes)\n",
+		     lut->num_phases, in - lut_data, out - lut->buf);
+
+	return 0;
+}
+
+static int pvi_wbf_get_lut(struct drm_epd_lut *lut,
+			   int mode_index, int temp_index)
+{
+	const struct pvi_wbf_pointer *mode_table, *temp_table;
+	const struct drm_epd_lut_file *file = lut->file;
+	const u8 *lut_data;
+	int ret;
+
+	mode_table = pvi_wbf_apply_offset(file, &file->header->wmta);
+	if (!mode_table)
+		return -EFAULT;
+
+	temp_table = pvi_wbf_dereference(file, &mode_table[mode_index]);
+	if (!temp_table)
+		return -EFAULT;
+
+	lut_data = pvi_wbf_dereference(file, &temp_table[temp_index]);
+	if (!lut_data)
+		return -EFAULT;
+
+	ret = pvi_wbf_decode_lut(lut, lut_data);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void drm_epd_lut_convert(const struct drm_epd_lut *lut)
+{
+	enum drm_epd_lut_format from = lut->file->mode_info->format;
+	enum drm_epd_lut_format to = lut->format;
+	u8 *buf = lut->buf;
+	size_t x, y;
+
+	if (to == from)
+		return;
+
+	switch (to) {
+	case DRM_EPD_LUT_4BIT:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 8; x--;) {
+				u8 byte = buf[16 * y + x];
+
+				buf[16 * y + 2 * x + 0] = (byte >> 0) & 0x03;
+				buf[16 * y + 2 * x + 1] = (byte >> 4) & 0x03;
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 8; x--;) {
+				buf[16 * y + 2 * x + 0] = 0;
+				buf[16 * y + 2 * x + 1] = 0;
+			}
+		}
+		break;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 4; x--;) {
+				u8 lo_byte = buf[16 * y + 2 * x + 0] & 0x33;
+				u8 hi_byte = buf[16 * y + 2 * x + 1] & 0x33;
+
+				/* Copy bits 4:5 => bits 2:3. */
+				lo_byte |= lo_byte >> 2;
+				hi_byte |= hi_byte >> 2;
+
+				buf[4 * y + x] = (lo_byte & 0xf) |
+						 (hi_byte << 4);
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 4; x--;)
+				buf[4 * y + x] = 0;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT:
+		memset(buf + 256 * lut->num_phases, 0,
+		       256 * (lut->max_phases - lut->num_phases));
+		for (x = 256 * lut->num_phases; x--;) {
+			u8 byte = buf[x];
+
+			buf[4 * x + 0] = (byte >> 0) & 0x03;
+			buf[4 * x + 1] = (byte >> 2) & 0x03;
+			buf[4 * x + 2] = (byte >> 4) & 0x03;
+			buf[4 * x + 3] = (byte >> 6) & 0x03;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* Nothing to do. */
+		break;
+	}
+}
+
+static int drm_epd_lut_update(struct drm_epd_lut *lut,
+			      int mode_index, int temp_index)
+{
+	int ret;
+
+	ret = pvi_wbf_get_lut(lut, mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	drm_epd_lut_convert(lut);
+
+	return 0;
+}
+
+/**
+ * drm_epd_lut_set_temperature - Update the LUT due to panel temperature change
+ *
+ * @lut: The LUT structure
+ * @temperateure: The current panel temperature in degrees Celsius
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature)
+{
+	int temp_index;
+	int ret;
+
+	temp_index = pvi_wbf_get_temp_index(lut->file, temperature);
+	if (temp_index < 0)
+		return -ENOENT;
+
+	if (temp_index == lut->temp_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT temperature changed (%d)\n",
+		     temperature);
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	lut->temp_index = temp_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_temperature);
+
+/**
+ * drm_epd_lut_set_waveform - Update the LUT due to waveform selection change
+ *
+ * @lut: The LUT structure
+ * @waveform: The desired waveform
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform)
+{
+	int mode_index;
+	int ret;
+
+	mode_index = pvi_wbf_get_mode_index(lut->file, waveform);
+	if (mode_index < 0)
+		return -ENOENT;
+
+	if (mode_index == lut->mode_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT waveform changed (%d)\n",
+		     waveform);
+
+	ret = drm_epd_lut_update(lut, mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	lut->mode_index = mode_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_waveform);
+
+static void drm_epd_lut_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut *lut = res;
+
+	vfree(lut->buf);
+}
+
+/**
+ * drmm_epd_lut_init - Initialize a managed EPD LUT from a LUT file
+ *
+ * @dev: The DRM device owning this LUT
+ * @lut: The LUT to initialize
+ * @file_name: The file name of the waveform firmware
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases)
+{
+	size_t max_order;
+	int ret;
+
+	/* Allocate a buffer large enough to convert the LUT in place. */
+	max_order = max(drm_epd_lut_size_shift(file->mode_info->format),
+			drm_epd_lut_size_shift(format));
+	lut->buf = vmalloc(max_phases << max_order);
+	if (!lut->buf)
+		return -ENOMEM;
+
+	ret = drmm_add_action_or_reset(file->dev, drm_epd_lut_free, lut);
+	if (ret)
+		return ret;
+
+	lut->file	= file;
+	lut->format	= format;
+	lut->max_phases	= max_phases;
+	lut->num_phases	= 0;
+
+	/* Set sane initial values for the waveform and temperature. */
+	lut->mode_index = pvi_wbf_get_mode_index(file, DRM_EPD_WF_RESET);
+	if (lut->mode_index < 0)
+		return -ENOENT;
+
+	lut->temp_index = pvi_wbf_get_temp_index(file, 25);
+	if (lut->temp_index < 0)
+		return -ENOENT;
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_init);
+
+MODULE_AUTHOR("Samuel Holland <samuel@sholland.org>");
+MODULE_DESCRIPTION("DRM EPD waveform LUT library");
+MODULE_LICENSE("Dual MIT/GPL");
diff --git a/include/drm/drm_epd_helper.h b/include/drm/drm_epd_helper.h
new file mode 100644
index 000000000000..290f5d48c0cb
--- /dev/null
+++ b/include/drm/drm_epd_helper.h
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: GPL-2.0 OR MIT */
+
+#ifndef __DRM_EPD_HELPER_H__
+#define __DRM_EPD_HELPER_H__
+
+#define DRM_EPD_DEFAULT_TEMPERATURE	25
+
+struct drm_device;
+struct firmware;
+struct pvi_wbf_file_header;
+struct pvi_wbf_mode_info;
+
+/**
+ * enum drm_epd_waveform - Identifiers for waveforms used to drive EPD pixels
+ *
+ * @DRM_EPD_WF_RESET: Used to initialize the panel, ends with white
+ * @DRM_EPD_WF_A2: Fast transitions between black and white only
+ * @DRM_EPD_WF_DU: Transitions 16-level grayscale to monochrome
+ * @DRM_EPD_WF_DU4: Transitions 16-level grayscale to 4-level grayscale
+ * @DRM_EPD_WF_GC16: High-quality but flashy 16-level grayscale
+ * @DRM_EPD_WF_GCC16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GL16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GLR16: Less flashy 16-level grayscale, plus anti-ghosting
+ * @DRM_EPD_WF_GLD16: Less flashy 16-level grayscale, plus anti-ghosting
+ */
+enum drm_epd_waveform {
+	DRM_EPD_WF_RESET,
+	DRM_EPD_WF_A2,
+	DRM_EPD_WF_DU,
+	DRM_EPD_WF_DU4,
+	DRM_EPD_WF_GC16,
+	DRM_EPD_WF_GCC16,
+	DRM_EPD_WF_GL16,
+	DRM_EPD_WF_GLR16,
+	DRM_EPD_WF_GLD16,
+	DRM_EPD_WF_MAX
+};
+
+/**
+ * enum drm_epd_lut_format - EPD LUT buffer format
+ *
+ * @DRM_EPD_LUT_4BIT: 4-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_4BIT_PACKED: 4-bit grayscale indexes, 2 bits per element
+ * @DRM_EPD_LUT_5BIT: 5-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_5BIT_PACKED: 5-bit grayscale indexes, 2 bits per element
+ */
+enum drm_epd_lut_format {
+	DRM_EPD_LUT_4BIT,
+	DRM_EPD_LUT_4BIT_PACKED,
+	DRM_EPD_LUT_5BIT,
+	DRM_EPD_LUT_5BIT_PACKED,
+};
+
+/**
+ * struct drm_epd_lut_file - Describes a file containing EPD LUTs
+ *
+ * @dev: The DRM device owning this LUT file
+ * @fw: The firmware object holding the raw file contents
+ * @header: Vendor-specific LUT file header
+ * @mode_info: Vendor-specific information about available waveforms
+ */
+struct drm_epd_lut_file {
+	struct drm_device			*dev;
+	const struct firmware			*fw;
+	const struct pvi_wbf_file_header	*header;
+	const struct pvi_wbf_mode_info		*mode_info;
+};
+
+/**
+ * struct drm_epd_lut - Describes a particular LUT buffer
+ *
+ * @buf: The LUT, in the format requested by the driver
+ * @file: The file where this LUT was loaded from
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ * @num_phases: The number of waveform phases in the current LUT
+ * @mode_index: Private identifier for the current waveform
+ * @temp_index: Private identifier for the current temperature
+ */
+struct drm_epd_lut {
+	u8				*buf;
+	const struct drm_epd_lut_file	*file;
+	enum drm_epd_lut_format		format;
+	unsigned int			max_phases;
+	unsigned int			num_phases;
+	int				mode_index;
+	int				temp_index;
+};
+
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name);
+
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases);
+
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature);
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform);
+
+#endif /* __DRM_EPD_HELPER_H__ */
-- 
2.35.1


WARNING: multiple messages have this Message-ID (diff)
From: Samuel Holland <samuel@sholland.org>
To: "Heiko Stübner" <heiko@sntech.de>,
	"Sandy Huang" <hjc@rock-chips.com>,
	dri-devel@lists.freedesktop.org
Cc: "David Airlie" <airlied@linux.ie>, "Ondřej Jirman" <x@xff.cz>,
	"Thierry Reding" <thierry.reding@gmail.com>,
	"Michael Riesch" <michael.riesch@wolfvision.net>,
	"Sam Ravnborg" <sam@ravnborg.org>,
	"Samuel Holland" <samuel@sholland.org>,
	"Nicolas Frattaroli" <frattaroli.nicolas@gmail.com>,
	linux-rockchip@lists.infradead.org,
	"Andreas Kemnade" <andreas@kemnade.info>,
	"Geert Uytterhoeven" <geert@linux-m68k.org>,
	"Liang Chen" <cl@rock-chips.com>,
	devicetree@vger.kernel.org,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	"Alistair Francis" <alistair@alistair23.me>,
	"Rob Herring" <robh+dt@kernel.org>,
	linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org, "Peter Geis" <pgwipeout@gmail.com>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>
Subject: [RFC PATCH 01/16] drm: Add a helper library for EPD drivers
Date: Wed, 13 Apr 2022 17:19:01 -0500	[thread overview]
Message-ID: <20220413221916.50995-2-samuel@sholland.org> (raw)
In-Reply-To: <20220413221916.50995-1-samuel@sholland.org>

Currently, this library is focused on LUTs and LUT files, specifically
the PVI wbf-format files shipped with E-Ink displays. It provides
helpers to load and validate LUT files, and extract LUTs from them.

Since EPD controllers need the LUTs in various formats, this library
allows drivers to declare their desired format. It will then convert
LUTs to that format before returning them.

Signed-off-by: Samuel Holland <samuel@sholland.org>
---

 drivers/gpu/drm/Kconfig          |   6 +
 drivers/gpu/drm/Makefile         |   2 +
 drivers/gpu/drm/drm_epd_helper.c | 663 +++++++++++++++++++++++++++++++
 include/drm/drm_epd_helper.h     | 104 +++++
 4 files changed, 775 insertions(+)
 create mode 100644 drivers/gpu/drm/drm_epd_helper.c
 create mode 100644 include/drm/drm_epd_helper.h

diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index f1422bee3dcc..ad96cf605444 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -198,6 +198,12 @@ config DRM_DP_CEC
 	  Note: not all adapters support this feature, and even for those
 	  that do support this they often do not hook up the CEC pin.
 
+config DRM_EPD_HELPER
+	tristate
+	depends on DRM
+	help
+	  Choose this if you need the EPD (LUT, etc.) helper functions
+
 config DRM_TTM
 	tristate
 	depends on DRM && MMU
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index c2ef5f9fce54..49380ccfe9d6 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -33,6 +33,8 @@ drm-$(CONFIG_DRM_PRIVACY_SCREEN) += drm_privacy_screen.o drm_privacy_screen_x86.
 
 obj-$(CONFIG_DRM_NOMODESET) += drm_nomodeset.o
 
+obj-$(CONFIG_DRM_EPD_HELPER) += drm_epd_helper.o
+
 drm_cma_helper-y := drm_gem_cma_helper.o
 drm_cma_helper-$(CONFIG_DRM_KMS_HELPER) += drm_fb_cma_helper.o
 obj-$(CONFIG_DRM_GEM_CMA_HELPER) += drm_cma_helper.o
diff --git a/drivers/gpu/drm/drm_epd_helper.c b/drivers/gpu/drm/drm_epd_helper.c
new file mode 100644
index 000000000000..433a6728ef3e
--- /dev/null
+++ b/drivers/gpu/drm/drm_epd_helper.c
@@ -0,0 +1,663 @@
+// SPDX-License-Identifier: GPL-2.0 OR MIT
+/*
+ * Electrophoretic Display Helper Library
+ *
+ * Copyright (C) 2022 Samuel Holland <samuel@sholland.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sub license, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the
+ * next paragraph) shall be included in all copies or substantial portions
+ * of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDERS, AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <linux/firmware.h>
+#include <linux/module.h>
+#include <linux/vmalloc.h>
+
+#include <drm/drm_device.h>
+#include <drm/drm_epd_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_print.h>
+
+/**
+ * DOC: overview
+ *
+ * This library provides functions for working with the lookup tables (LUTs)
+ * used by electrophoretic displays (EPDs). It fills in a LUT buffer based on
+ * the selected waveform, the panel temperature, and the buffer format needed
+ * by the driver.
+ */
+
+struct pvi_wbf_offset {
+	u8			b[3];
+};
+
+struct pvi_wbf_pointer {
+	struct pvi_wbf_offset	offset;
+	u8			checksum;
+};
+
+struct pvi_wbf_file_header {
+	__le32			checksum;		// 0x00
+	__le32			file_size;		// 0x04
+	__le32			serial;			// 0x08
+	u8			run_type;		// 0x0c
+	u8			fpl_platform;		// 0x0d
+	__le16			fpl_lot;		// 0x0e
+	u8			mode_version;		// 0x10
+	u8			wf_version;		// 0x11
+	u8			wf_subversion;		// 0x12
+	u8			wf_type;		// 0x13
+	u8			panel_size;		// 0x14
+	u8			amepd_part_number;	// 0x15
+	u8			wf_rev;			// 0x16
+	u8			frame_rate_bcd;		// 0x17
+	u8			frame_rate_hex;		// 0x18
+	u8			vcom_offset;		// 0x19
+	u8			unknown[2];		// 0x1a
+	struct pvi_wbf_offset	xwia;			// 0x1c
+	u8			cs1;			// 0x1f
+	struct pvi_wbf_offset	wmta;			// 0x20
+	u8			fvsn;			// 0x23
+	u8			luts;			// 0x24
+	u8			mode_count;		// 0x25
+	u8			temp_range_count;	// 0x26
+	u8			advanced_wf_flags;	// 0x27
+	u8			eb;			// 0x28
+	u8			sb;			// 0x29
+	u8			reserved[5];		// 0x2a
+	u8			cs2;			// 0x2f
+	u8			temp_range_table[];	// 0x30
+};
+static_assert(sizeof(struct pvi_wbf_file_header) == 0x30);
+
+struct pvi_wbf_mode_info {
+	u8			versions[2];
+	u8			format;
+	u8			modes[DRM_EPD_WF_MAX];
+};
+
+static const struct pvi_wbf_mode_info pvi_wbf_mode_info_table[] = {
+	{
+		.versions = {
+			0x09,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x12,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 3,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 5,
+			[DRM_EPD_WF_GLD16]	= 6,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x16,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x18,
+			0x20,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x19,
+			0x43,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x23,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 5,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x54,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 5,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+};
+
+static const void *pvi_wbf_apply_offset(const struct drm_epd_lut_file *file,
+					const struct pvi_wbf_offset *offset)
+{
+	u32 bytes = offset->b[0] | offset->b[1] << 8 | offset->b[2] << 16;
+
+	if (bytes >= file->fw->size)
+		return NULL;
+
+	return (const void *)file->header + bytes;
+}
+
+static const void *pvi_wbf_dereference(const struct drm_epd_lut_file *file,
+				       const struct pvi_wbf_pointer *ptr)
+{
+	u8 sum = ptr->offset.b[0] + ptr->offset.b[1] + ptr->offset.b[2];
+
+	if (ptr->checksum != sum)
+		return NULL;
+
+	return pvi_wbf_apply_offset(file, &ptr->offset);
+}
+
+static int pvi_wbf_get_mode_index(const struct drm_epd_lut_file *file,
+				  enum drm_epd_waveform waveform)
+{
+	if (waveform >= DRM_EPD_WF_MAX)
+		return -EINVAL;
+
+	return file->mode_info->modes[waveform];
+}
+
+static int pvi_wbf_get_mode_info(struct drm_epd_lut_file *file)
+{
+	u8 mode_version = file->header->mode_version;
+	const struct pvi_wbf_mode_info *mode_info;
+	int i, v;
+
+	for (i = 0; i < ARRAY_SIZE(pvi_wbf_mode_info_table); i++) {
+		mode_info = &pvi_wbf_mode_info_table[i];
+
+		for (v = 0; v < ARRAY_SIZE(mode_info->versions); v++) {
+			if (mode_info->versions[v] == mode_version) {
+				file->mode_info = mode_info;
+				return 0;
+			}
+		}
+	}
+
+	drm_err(file->dev, "Unknown PVI waveform version 0x%02x\n",
+		mode_version);
+
+	return -EOPNOTSUPP;
+}
+
+static int pvi_wbf_get_temp_index(const struct drm_epd_lut_file *file,
+				  int temperature)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int i;
+
+	for (i = 0; i < header->temp_range_count; i++)
+		if (temperature < header->temp_range_table[i])
+			return i - 1;
+
+	return header->temp_range_count - 1;
+}
+
+static int pvi_wbf_validate_header(struct drm_epd_lut_file *file)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int ret;
+
+	if (le32_to_cpu(header->file_size) > file->fw->size)
+		return -EINVAL;
+
+	ret = pvi_wbf_get_mode_info(file);
+	if (ret)
+		return ret;
+
+	drm_info(file->dev, "Loaded %d-bit PVI waveform version 0x%02x\n",
+		 file->mode_info->format == DRM_EPD_LUT_5BIT_PACKED ? 5 : 4,
+		 header->mode_version);
+
+	return 0;
+}
+
+static void drm_epd_lut_file_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut_file *file = res;
+
+	release_firmware(file->fw);
+}
+
+/**
+ * drmm_epd_lut_file_init - Initialize a managed EPD LUT file
+ *
+ * @dev: The DRM device owning this LUT file
+ * @file: The LUT file to initialize
+ * @file_name: The filesystem name of the LUT file
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name)
+{
+	int ret;
+
+	ret = request_firmware(&file->fw, file_name, dev->dev);
+	if (ret)
+		return ret;
+
+	ret = drmm_add_action_or_reset(dev, drm_epd_lut_file_free, file);
+	if (ret)
+		return ret;
+
+	file->dev = dev;
+	file->header = (const void *)file->fw->data;
+
+	ret = pvi_wbf_validate_header(file);
+	if (ret)
+		return ret;
+
+	/* Only 5-bit waveform files are supported by drm_epd_lut_convert. */
+	if (file->mode_info->format != DRM_EPD_LUT_5BIT_PACKED)
+		return -EOPNOTSUPP;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_file_init);
+
+/**
+ * drm_epd_lut_size_shift - Get the size of a LUT phase in power-of-2 bytes
+ *
+ * @format: One of the LUT buffer formats
+ *
+ * Return: buffer size shift amount
+ */
+static int drm_epd_lut_size_shift(enum drm_epd_lut_format format)
+{
+	switch (format) {
+	case DRM_EPD_LUT_4BIT:
+		/* (4 bits/pixel)^2 / 1 pixel/byte */
+		return 4 + 4;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		/* (4 bits/pixel)^2 / 4 pixels/byte */
+		return 4 + 4 - 2;
+	case DRM_EPD_LUT_5BIT:
+		/* (5 bits/pixel)^2 / 1 pixel/byte */
+		return 5 + 5;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* (5 bits/pixel)^2 / 4 pixels/byte */
+		return 5 + 5 - 2;
+	}
+
+	unreachable();
+}
+
+static int pvi_wbf_decode_lut(struct drm_epd_lut *lut, const u8 *lut_data)
+{
+
+	unsigned int copies, max_bytes, size_shift, state, total_bytes;
+	struct drm_device *dev = lut->file->dev;
+	const u8 *in = lut_data;
+	u8 *out = lut->buf;
+	u8 token;
+
+	size_shift = drm_epd_lut_size_shift(lut->file->mode_info->format);
+	max_bytes = lut->max_phases << size_shift;
+	total_bytes = 0;
+	state = 1;
+
+	/* Read tokens until reaching the end-of-input marker. */
+	while ((token = *in++) != 0xff) {
+		/* Special handling for the state switch token. */
+		if (token == 0xfc) {
+			state = !state;
+			token = *in++;
+		}
+
+		/*
+		 * State 0 is a sequence of data bytes.
+		 * State 1 is a sequence of [data byte, extra copies] pairs.
+		 */
+		copies = 1 + (state ? *in++ : 0);
+
+		total_bytes += copies;
+		if (total_bytes > max_bytes) {
+			drm_err(dev, "LUT contains too many phases\n");
+			lut->num_phases = 0;
+			return -EILSEQ;
+		}
+
+		while (copies--)
+			*out++ = token;
+	}
+
+	lut->num_phases = total_bytes >> size_shift;
+	if (total_bytes != lut->num_phases << size_shift) {
+		drm_err(dev, "LUT contains a partial phase\n");
+		lut->num_phases = 0;
+		return -EILSEQ;
+	}
+
+	drm_dbg_core(dev, "LUT contains %d phases (%ld => %ld bytes)\n",
+		     lut->num_phases, in - lut_data, out - lut->buf);
+
+	return 0;
+}
+
+static int pvi_wbf_get_lut(struct drm_epd_lut *lut,
+			   int mode_index, int temp_index)
+{
+	const struct pvi_wbf_pointer *mode_table, *temp_table;
+	const struct drm_epd_lut_file *file = lut->file;
+	const u8 *lut_data;
+	int ret;
+
+	mode_table = pvi_wbf_apply_offset(file, &file->header->wmta);
+	if (!mode_table)
+		return -EFAULT;
+
+	temp_table = pvi_wbf_dereference(file, &mode_table[mode_index]);
+	if (!temp_table)
+		return -EFAULT;
+
+	lut_data = pvi_wbf_dereference(file, &temp_table[temp_index]);
+	if (!lut_data)
+		return -EFAULT;
+
+	ret = pvi_wbf_decode_lut(lut, lut_data);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void drm_epd_lut_convert(const struct drm_epd_lut *lut)
+{
+	enum drm_epd_lut_format from = lut->file->mode_info->format;
+	enum drm_epd_lut_format to = lut->format;
+	u8 *buf = lut->buf;
+	size_t x, y;
+
+	if (to == from)
+		return;
+
+	switch (to) {
+	case DRM_EPD_LUT_4BIT:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 8; x--;) {
+				u8 byte = buf[16 * y + x];
+
+				buf[16 * y + 2 * x + 0] = (byte >> 0) & 0x03;
+				buf[16 * y + 2 * x + 1] = (byte >> 4) & 0x03;
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 8; x--;) {
+				buf[16 * y + 2 * x + 0] = 0;
+				buf[16 * y + 2 * x + 1] = 0;
+			}
+		}
+		break;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 4; x--;) {
+				u8 lo_byte = buf[16 * y + 2 * x + 0] & 0x33;
+				u8 hi_byte = buf[16 * y + 2 * x + 1] & 0x33;
+
+				/* Copy bits 4:5 => bits 2:3. */
+				lo_byte |= lo_byte >> 2;
+				hi_byte |= hi_byte >> 2;
+
+				buf[4 * y + x] = (lo_byte & 0xf) |
+						 (hi_byte << 4);
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 4; x--;)
+				buf[4 * y + x] = 0;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT:
+		memset(buf + 256 * lut->num_phases, 0,
+		       256 * (lut->max_phases - lut->num_phases));
+		for (x = 256 * lut->num_phases; x--;) {
+			u8 byte = buf[x];
+
+			buf[4 * x + 0] = (byte >> 0) & 0x03;
+			buf[4 * x + 1] = (byte >> 2) & 0x03;
+			buf[4 * x + 2] = (byte >> 4) & 0x03;
+			buf[4 * x + 3] = (byte >> 6) & 0x03;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* Nothing to do. */
+		break;
+	}
+}
+
+static int drm_epd_lut_update(struct drm_epd_lut *lut,
+			      int mode_index, int temp_index)
+{
+	int ret;
+
+	ret = pvi_wbf_get_lut(lut, mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	drm_epd_lut_convert(lut);
+
+	return 0;
+}
+
+/**
+ * drm_epd_lut_set_temperature - Update the LUT due to panel temperature change
+ *
+ * @lut: The LUT structure
+ * @temperateure: The current panel temperature in degrees Celsius
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature)
+{
+	int temp_index;
+	int ret;
+
+	temp_index = pvi_wbf_get_temp_index(lut->file, temperature);
+	if (temp_index < 0)
+		return -ENOENT;
+
+	if (temp_index == lut->temp_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT temperature changed (%d)\n",
+		     temperature);
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	lut->temp_index = temp_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_temperature);
+
+/**
+ * drm_epd_lut_set_waveform - Update the LUT due to waveform selection change
+ *
+ * @lut: The LUT structure
+ * @waveform: The desired waveform
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform)
+{
+	int mode_index;
+	int ret;
+
+	mode_index = pvi_wbf_get_mode_index(lut->file, waveform);
+	if (mode_index < 0)
+		return -ENOENT;
+
+	if (mode_index == lut->mode_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT waveform changed (%d)\n",
+		     waveform);
+
+	ret = drm_epd_lut_update(lut, mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	lut->mode_index = mode_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_waveform);
+
+static void drm_epd_lut_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut *lut = res;
+
+	vfree(lut->buf);
+}
+
+/**
+ * drmm_epd_lut_init - Initialize a managed EPD LUT from a LUT file
+ *
+ * @dev: The DRM device owning this LUT
+ * @lut: The LUT to initialize
+ * @file_name: The file name of the waveform firmware
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases)
+{
+	size_t max_order;
+	int ret;
+
+	/* Allocate a buffer large enough to convert the LUT in place. */
+	max_order = max(drm_epd_lut_size_shift(file->mode_info->format),
+			drm_epd_lut_size_shift(format));
+	lut->buf = vmalloc(max_phases << max_order);
+	if (!lut->buf)
+		return -ENOMEM;
+
+	ret = drmm_add_action_or_reset(file->dev, drm_epd_lut_free, lut);
+	if (ret)
+		return ret;
+
+	lut->file	= file;
+	lut->format	= format;
+	lut->max_phases	= max_phases;
+	lut->num_phases	= 0;
+
+	/* Set sane initial values for the waveform and temperature. */
+	lut->mode_index = pvi_wbf_get_mode_index(file, DRM_EPD_WF_RESET);
+	if (lut->mode_index < 0)
+		return -ENOENT;
+
+	lut->temp_index = pvi_wbf_get_temp_index(file, 25);
+	if (lut->temp_index < 0)
+		return -ENOENT;
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_init);
+
+MODULE_AUTHOR("Samuel Holland <samuel@sholland.org>");
+MODULE_DESCRIPTION("DRM EPD waveform LUT library");
+MODULE_LICENSE("Dual MIT/GPL");
diff --git a/include/drm/drm_epd_helper.h b/include/drm/drm_epd_helper.h
new file mode 100644
index 000000000000..290f5d48c0cb
--- /dev/null
+++ b/include/drm/drm_epd_helper.h
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: GPL-2.0 OR MIT */
+
+#ifndef __DRM_EPD_HELPER_H__
+#define __DRM_EPD_HELPER_H__
+
+#define DRM_EPD_DEFAULT_TEMPERATURE	25
+
+struct drm_device;
+struct firmware;
+struct pvi_wbf_file_header;
+struct pvi_wbf_mode_info;
+
+/**
+ * enum drm_epd_waveform - Identifiers for waveforms used to drive EPD pixels
+ *
+ * @DRM_EPD_WF_RESET: Used to initialize the panel, ends with white
+ * @DRM_EPD_WF_A2: Fast transitions between black and white only
+ * @DRM_EPD_WF_DU: Transitions 16-level grayscale to monochrome
+ * @DRM_EPD_WF_DU4: Transitions 16-level grayscale to 4-level grayscale
+ * @DRM_EPD_WF_GC16: High-quality but flashy 16-level grayscale
+ * @DRM_EPD_WF_GCC16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GL16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GLR16: Less flashy 16-level grayscale, plus anti-ghosting
+ * @DRM_EPD_WF_GLD16: Less flashy 16-level grayscale, plus anti-ghosting
+ */
+enum drm_epd_waveform {
+	DRM_EPD_WF_RESET,
+	DRM_EPD_WF_A2,
+	DRM_EPD_WF_DU,
+	DRM_EPD_WF_DU4,
+	DRM_EPD_WF_GC16,
+	DRM_EPD_WF_GCC16,
+	DRM_EPD_WF_GL16,
+	DRM_EPD_WF_GLR16,
+	DRM_EPD_WF_GLD16,
+	DRM_EPD_WF_MAX
+};
+
+/**
+ * enum drm_epd_lut_format - EPD LUT buffer format
+ *
+ * @DRM_EPD_LUT_4BIT: 4-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_4BIT_PACKED: 4-bit grayscale indexes, 2 bits per element
+ * @DRM_EPD_LUT_5BIT: 5-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_5BIT_PACKED: 5-bit grayscale indexes, 2 bits per element
+ */
+enum drm_epd_lut_format {
+	DRM_EPD_LUT_4BIT,
+	DRM_EPD_LUT_4BIT_PACKED,
+	DRM_EPD_LUT_5BIT,
+	DRM_EPD_LUT_5BIT_PACKED,
+};
+
+/**
+ * struct drm_epd_lut_file - Describes a file containing EPD LUTs
+ *
+ * @dev: The DRM device owning this LUT file
+ * @fw: The firmware object holding the raw file contents
+ * @header: Vendor-specific LUT file header
+ * @mode_info: Vendor-specific information about available waveforms
+ */
+struct drm_epd_lut_file {
+	struct drm_device			*dev;
+	const struct firmware			*fw;
+	const struct pvi_wbf_file_header	*header;
+	const struct pvi_wbf_mode_info		*mode_info;
+};
+
+/**
+ * struct drm_epd_lut - Describes a particular LUT buffer
+ *
+ * @buf: The LUT, in the format requested by the driver
+ * @file: The file where this LUT was loaded from
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ * @num_phases: The number of waveform phases in the current LUT
+ * @mode_index: Private identifier for the current waveform
+ * @temp_index: Private identifier for the current temperature
+ */
+struct drm_epd_lut {
+	u8				*buf;
+	const struct drm_epd_lut_file	*file;
+	enum drm_epd_lut_format		format;
+	unsigned int			max_phases;
+	unsigned int			num_phases;
+	int				mode_index;
+	int				temp_index;
+};
+
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name);
+
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases);
+
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature);
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform);
+
+#endif /* __DRM_EPD_HELPER_H__ */
-- 
2.35.1


WARNING: multiple messages have this Message-ID (diff)
From: Samuel Holland <samuel@sholland.org>
To: "Heiko Stübner" <heiko@sntech.de>,
	"Sandy Huang" <hjc@rock-chips.com>,
	dri-devel@lists.freedesktop.org
Cc: linux-rockchip@lists.infradead.org,
	"Alistair Francis" <alistair@alistair23.me>,
	"Ondřej Jirman" <x@xff.cz>,
	"Andreas Kemnade" <andreas@kemnade.info>,
	"Daniel Vetter" <daniel@ffwll.ch>,
	"David Airlie" <airlied@linux.ie>,
	"Geert Uytterhoeven" <geert@linux-m68k.org>,
	"Samuel Holland" <samuel@sholland.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Liang Chen" <cl@rock-chips.com>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Michael Riesch" <michael.riesch@wolfvision.net>,
	"Nicolas Frattaroli" <frattaroli.nicolas@gmail.com>,
	"Peter Geis" <pgwipeout@gmail.com>,
	"Rob Herring" <robh+dt@kernel.org>,
	"Sam Ravnborg" <sam@ravnborg.org>,
	"Thierry Reding" <thierry.reding@gmail.com>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	devicetree@vger.kernel.org, linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org
Subject: [RFC PATCH 01/16] drm: Add a helper library for EPD drivers
Date: Wed, 13 Apr 2022 17:19:01 -0500	[thread overview]
Message-ID: <20220413221916.50995-2-samuel@sholland.org> (raw)
In-Reply-To: <20220413221916.50995-1-samuel@sholland.org>

Currently, this library is focused on LUTs and LUT files, specifically
the PVI wbf-format files shipped with E-Ink displays. It provides
helpers to load and validate LUT files, and extract LUTs from them.

Since EPD controllers need the LUTs in various formats, this library
allows drivers to declare their desired format. It will then convert
LUTs to that format before returning them.

Signed-off-by: Samuel Holland <samuel@sholland.org>
---

 drivers/gpu/drm/Kconfig          |   6 +
 drivers/gpu/drm/Makefile         |   2 +
 drivers/gpu/drm/drm_epd_helper.c | 663 +++++++++++++++++++++++++++++++
 include/drm/drm_epd_helper.h     | 104 +++++
 4 files changed, 775 insertions(+)
 create mode 100644 drivers/gpu/drm/drm_epd_helper.c
 create mode 100644 include/drm/drm_epd_helper.h

diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index f1422bee3dcc..ad96cf605444 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -198,6 +198,12 @@ config DRM_DP_CEC
 	  Note: not all adapters support this feature, and even for those
 	  that do support this they often do not hook up the CEC pin.
 
+config DRM_EPD_HELPER
+	tristate
+	depends on DRM
+	help
+	  Choose this if you need the EPD (LUT, etc.) helper functions
+
 config DRM_TTM
 	tristate
 	depends on DRM && MMU
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index c2ef5f9fce54..49380ccfe9d6 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -33,6 +33,8 @@ drm-$(CONFIG_DRM_PRIVACY_SCREEN) += drm_privacy_screen.o drm_privacy_screen_x86.
 
 obj-$(CONFIG_DRM_NOMODESET) += drm_nomodeset.o
 
+obj-$(CONFIG_DRM_EPD_HELPER) += drm_epd_helper.o
+
 drm_cma_helper-y := drm_gem_cma_helper.o
 drm_cma_helper-$(CONFIG_DRM_KMS_HELPER) += drm_fb_cma_helper.o
 obj-$(CONFIG_DRM_GEM_CMA_HELPER) += drm_cma_helper.o
diff --git a/drivers/gpu/drm/drm_epd_helper.c b/drivers/gpu/drm/drm_epd_helper.c
new file mode 100644
index 000000000000..433a6728ef3e
--- /dev/null
+++ b/drivers/gpu/drm/drm_epd_helper.c
@@ -0,0 +1,663 @@
+// SPDX-License-Identifier: GPL-2.0 OR MIT
+/*
+ * Electrophoretic Display Helper Library
+ *
+ * Copyright (C) 2022 Samuel Holland <samuel@sholland.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sub license, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the
+ * next paragraph) shall be included in all copies or substantial portions
+ * of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDERS, AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <linux/firmware.h>
+#include <linux/module.h>
+#include <linux/vmalloc.h>
+
+#include <drm/drm_device.h>
+#include <drm/drm_epd_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_print.h>
+
+/**
+ * DOC: overview
+ *
+ * This library provides functions for working with the lookup tables (LUTs)
+ * used by electrophoretic displays (EPDs). It fills in a LUT buffer based on
+ * the selected waveform, the panel temperature, and the buffer format needed
+ * by the driver.
+ */
+
+struct pvi_wbf_offset {
+	u8			b[3];
+};
+
+struct pvi_wbf_pointer {
+	struct pvi_wbf_offset	offset;
+	u8			checksum;
+};
+
+struct pvi_wbf_file_header {
+	__le32			checksum;		// 0x00
+	__le32			file_size;		// 0x04
+	__le32			serial;			// 0x08
+	u8			run_type;		// 0x0c
+	u8			fpl_platform;		// 0x0d
+	__le16			fpl_lot;		// 0x0e
+	u8			mode_version;		// 0x10
+	u8			wf_version;		// 0x11
+	u8			wf_subversion;		// 0x12
+	u8			wf_type;		// 0x13
+	u8			panel_size;		// 0x14
+	u8			amepd_part_number;	// 0x15
+	u8			wf_rev;			// 0x16
+	u8			frame_rate_bcd;		// 0x17
+	u8			frame_rate_hex;		// 0x18
+	u8			vcom_offset;		// 0x19
+	u8			unknown[2];		// 0x1a
+	struct pvi_wbf_offset	xwia;			// 0x1c
+	u8			cs1;			// 0x1f
+	struct pvi_wbf_offset	wmta;			// 0x20
+	u8			fvsn;			// 0x23
+	u8			luts;			// 0x24
+	u8			mode_count;		// 0x25
+	u8			temp_range_count;	// 0x26
+	u8			advanced_wf_flags;	// 0x27
+	u8			eb;			// 0x28
+	u8			sb;			// 0x29
+	u8			reserved[5];		// 0x2a
+	u8			cs2;			// 0x2f
+	u8			temp_range_table[];	// 0x30
+};
+static_assert(sizeof(struct pvi_wbf_file_header) == 0x30);
+
+struct pvi_wbf_mode_info {
+	u8			versions[2];
+	u8			format;
+	u8			modes[DRM_EPD_WF_MAX];
+};
+
+static const struct pvi_wbf_mode_info pvi_wbf_mode_info_table[] = {
+	{
+		.versions = {
+			0x09,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x12,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 3,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 5,
+			[DRM_EPD_WF_GLD16]	= 6,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x16,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x18,
+			0x20,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x19,
+			0x43,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x23,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 5,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x54,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 5,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+};
+
+static const void *pvi_wbf_apply_offset(const struct drm_epd_lut_file *file,
+					const struct pvi_wbf_offset *offset)
+{
+	u32 bytes = offset->b[0] | offset->b[1] << 8 | offset->b[2] << 16;
+
+	if (bytes >= file->fw->size)
+		return NULL;
+
+	return (const void *)file->header + bytes;
+}
+
+static const void *pvi_wbf_dereference(const struct drm_epd_lut_file *file,
+				       const struct pvi_wbf_pointer *ptr)
+{
+	u8 sum = ptr->offset.b[0] + ptr->offset.b[1] + ptr->offset.b[2];
+
+	if (ptr->checksum != sum)
+		return NULL;
+
+	return pvi_wbf_apply_offset(file, &ptr->offset);
+}
+
+static int pvi_wbf_get_mode_index(const struct drm_epd_lut_file *file,
+				  enum drm_epd_waveform waveform)
+{
+	if (waveform >= DRM_EPD_WF_MAX)
+		return -EINVAL;
+
+	return file->mode_info->modes[waveform];
+}
+
+static int pvi_wbf_get_mode_info(struct drm_epd_lut_file *file)
+{
+	u8 mode_version = file->header->mode_version;
+	const struct pvi_wbf_mode_info *mode_info;
+	int i, v;
+
+	for (i = 0; i < ARRAY_SIZE(pvi_wbf_mode_info_table); i++) {
+		mode_info = &pvi_wbf_mode_info_table[i];
+
+		for (v = 0; v < ARRAY_SIZE(mode_info->versions); v++) {
+			if (mode_info->versions[v] == mode_version) {
+				file->mode_info = mode_info;
+				return 0;
+			}
+		}
+	}
+
+	drm_err(file->dev, "Unknown PVI waveform version 0x%02x\n",
+		mode_version);
+
+	return -EOPNOTSUPP;
+}
+
+static int pvi_wbf_get_temp_index(const struct drm_epd_lut_file *file,
+				  int temperature)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int i;
+
+	for (i = 0; i < header->temp_range_count; i++)
+		if (temperature < header->temp_range_table[i])
+			return i - 1;
+
+	return header->temp_range_count - 1;
+}
+
+static int pvi_wbf_validate_header(struct drm_epd_lut_file *file)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int ret;
+
+	if (le32_to_cpu(header->file_size) > file->fw->size)
+		return -EINVAL;
+
+	ret = pvi_wbf_get_mode_info(file);
+	if (ret)
+		return ret;
+
+	drm_info(file->dev, "Loaded %d-bit PVI waveform version 0x%02x\n",
+		 file->mode_info->format == DRM_EPD_LUT_5BIT_PACKED ? 5 : 4,
+		 header->mode_version);
+
+	return 0;
+}
+
+static void drm_epd_lut_file_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut_file *file = res;
+
+	release_firmware(file->fw);
+}
+
+/**
+ * drmm_epd_lut_file_init - Initialize a managed EPD LUT file
+ *
+ * @dev: The DRM device owning this LUT file
+ * @file: The LUT file to initialize
+ * @file_name: The filesystem name of the LUT file
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name)
+{
+	int ret;
+
+	ret = request_firmware(&file->fw, file_name, dev->dev);
+	if (ret)
+		return ret;
+
+	ret = drmm_add_action_or_reset(dev, drm_epd_lut_file_free, file);
+	if (ret)
+		return ret;
+
+	file->dev = dev;
+	file->header = (const void *)file->fw->data;
+
+	ret = pvi_wbf_validate_header(file);
+	if (ret)
+		return ret;
+
+	/* Only 5-bit waveform files are supported by drm_epd_lut_convert. */
+	if (file->mode_info->format != DRM_EPD_LUT_5BIT_PACKED)
+		return -EOPNOTSUPP;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_file_init);
+
+/**
+ * drm_epd_lut_size_shift - Get the size of a LUT phase in power-of-2 bytes
+ *
+ * @format: One of the LUT buffer formats
+ *
+ * Return: buffer size shift amount
+ */
+static int drm_epd_lut_size_shift(enum drm_epd_lut_format format)
+{
+	switch (format) {
+	case DRM_EPD_LUT_4BIT:
+		/* (4 bits/pixel)^2 / 1 pixel/byte */
+		return 4 + 4;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		/* (4 bits/pixel)^2 / 4 pixels/byte */
+		return 4 + 4 - 2;
+	case DRM_EPD_LUT_5BIT:
+		/* (5 bits/pixel)^2 / 1 pixel/byte */
+		return 5 + 5;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* (5 bits/pixel)^2 / 4 pixels/byte */
+		return 5 + 5 - 2;
+	}
+
+	unreachable();
+}
+
+static int pvi_wbf_decode_lut(struct drm_epd_lut *lut, const u8 *lut_data)
+{
+
+	unsigned int copies, max_bytes, size_shift, state, total_bytes;
+	struct drm_device *dev = lut->file->dev;
+	const u8 *in = lut_data;
+	u8 *out = lut->buf;
+	u8 token;
+
+	size_shift = drm_epd_lut_size_shift(lut->file->mode_info->format);
+	max_bytes = lut->max_phases << size_shift;
+	total_bytes = 0;
+	state = 1;
+
+	/* Read tokens until reaching the end-of-input marker. */
+	while ((token = *in++) != 0xff) {
+		/* Special handling for the state switch token. */
+		if (token == 0xfc) {
+			state = !state;
+			token = *in++;
+		}
+
+		/*
+		 * State 0 is a sequence of data bytes.
+		 * State 1 is a sequence of [data byte, extra copies] pairs.
+		 */
+		copies = 1 + (state ? *in++ : 0);
+
+		total_bytes += copies;
+		if (total_bytes > max_bytes) {
+			drm_err(dev, "LUT contains too many phases\n");
+			lut->num_phases = 0;
+			return -EILSEQ;
+		}
+
+		while (copies--)
+			*out++ = token;
+	}
+
+	lut->num_phases = total_bytes >> size_shift;
+	if (total_bytes != lut->num_phases << size_shift) {
+		drm_err(dev, "LUT contains a partial phase\n");
+		lut->num_phases = 0;
+		return -EILSEQ;
+	}
+
+	drm_dbg_core(dev, "LUT contains %d phases (%ld => %ld bytes)\n",
+		     lut->num_phases, in - lut_data, out - lut->buf);
+
+	return 0;
+}
+
+static int pvi_wbf_get_lut(struct drm_epd_lut *lut,
+			   int mode_index, int temp_index)
+{
+	const struct pvi_wbf_pointer *mode_table, *temp_table;
+	const struct drm_epd_lut_file *file = lut->file;
+	const u8 *lut_data;
+	int ret;
+
+	mode_table = pvi_wbf_apply_offset(file, &file->header->wmta);
+	if (!mode_table)
+		return -EFAULT;
+
+	temp_table = pvi_wbf_dereference(file, &mode_table[mode_index]);
+	if (!temp_table)
+		return -EFAULT;
+
+	lut_data = pvi_wbf_dereference(file, &temp_table[temp_index]);
+	if (!lut_data)
+		return -EFAULT;
+
+	ret = pvi_wbf_decode_lut(lut, lut_data);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void drm_epd_lut_convert(const struct drm_epd_lut *lut)
+{
+	enum drm_epd_lut_format from = lut->file->mode_info->format;
+	enum drm_epd_lut_format to = lut->format;
+	u8 *buf = lut->buf;
+	size_t x, y;
+
+	if (to == from)
+		return;
+
+	switch (to) {
+	case DRM_EPD_LUT_4BIT:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 8; x--;) {
+				u8 byte = buf[16 * y + x];
+
+				buf[16 * y + 2 * x + 0] = (byte >> 0) & 0x03;
+				buf[16 * y + 2 * x + 1] = (byte >> 4) & 0x03;
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 8; x--;) {
+				buf[16 * y + 2 * x + 0] = 0;
+				buf[16 * y + 2 * x + 1] = 0;
+			}
+		}
+		break;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 4; x--;) {
+				u8 lo_byte = buf[16 * y + 2 * x + 0] & 0x33;
+				u8 hi_byte = buf[16 * y + 2 * x + 1] & 0x33;
+
+				/* Copy bits 4:5 => bits 2:3. */
+				lo_byte |= lo_byte >> 2;
+				hi_byte |= hi_byte >> 2;
+
+				buf[4 * y + x] = (lo_byte & 0xf) |
+						 (hi_byte << 4);
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 4; x--;)
+				buf[4 * y + x] = 0;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT:
+		memset(buf + 256 * lut->num_phases, 0,
+		       256 * (lut->max_phases - lut->num_phases));
+		for (x = 256 * lut->num_phases; x--;) {
+			u8 byte = buf[x];
+
+			buf[4 * x + 0] = (byte >> 0) & 0x03;
+			buf[4 * x + 1] = (byte >> 2) & 0x03;
+			buf[4 * x + 2] = (byte >> 4) & 0x03;
+			buf[4 * x + 3] = (byte >> 6) & 0x03;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* Nothing to do. */
+		break;
+	}
+}
+
+static int drm_epd_lut_update(struct drm_epd_lut *lut,
+			      int mode_index, int temp_index)
+{
+	int ret;
+
+	ret = pvi_wbf_get_lut(lut, mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	drm_epd_lut_convert(lut);
+
+	return 0;
+}
+
+/**
+ * drm_epd_lut_set_temperature - Update the LUT due to panel temperature change
+ *
+ * @lut: The LUT structure
+ * @temperateure: The current panel temperature in degrees Celsius
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature)
+{
+	int temp_index;
+	int ret;
+
+	temp_index = pvi_wbf_get_temp_index(lut->file, temperature);
+	if (temp_index < 0)
+		return -ENOENT;
+
+	if (temp_index == lut->temp_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT temperature changed (%d)\n",
+		     temperature);
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	lut->temp_index = temp_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_temperature);
+
+/**
+ * drm_epd_lut_set_waveform - Update the LUT due to waveform selection change
+ *
+ * @lut: The LUT structure
+ * @waveform: The desired waveform
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform)
+{
+	int mode_index;
+	int ret;
+
+	mode_index = pvi_wbf_get_mode_index(lut->file, waveform);
+	if (mode_index < 0)
+		return -ENOENT;
+
+	if (mode_index == lut->mode_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT waveform changed (%d)\n",
+		     waveform);
+
+	ret = drm_epd_lut_update(lut, mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	lut->mode_index = mode_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_waveform);
+
+static void drm_epd_lut_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut *lut = res;
+
+	vfree(lut->buf);
+}
+
+/**
+ * drmm_epd_lut_init - Initialize a managed EPD LUT from a LUT file
+ *
+ * @dev: The DRM device owning this LUT
+ * @lut: The LUT to initialize
+ * @file_name: The file name of the waveform firmware
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases)
+{
+	size_t max_order;
+	int ret;
+
+	/* Allocate a buffer large enough to convert the LUT in place. */
+	max_order = max(drm_epd_lut_size_shift(file->mode_info->format),
+			drm_epd_lut_size_shift(format));
+	lut->buf = vmalloc(max_phases << max_order);
+	if (!lut->buf)
+		return -ENOMEM;
+
+	ret = drmm_add_action_or_reset(file->dev, drm_epd_lut_free, lut);
+	if (ret)
+		return ret;
+
+	lut->file	= file;
+	lut->format	= format;
+	lut->max_phases	= max_phases;
+	lut->num_phases	= 0;
+
+	/* Set sane initial values for the waveform and temperature. */
+	lut->mode_index = pvi_wbf_get_mode_index(file, DRM_EPD_WF_RESET);
+	if (lut->mode_index < 0)
+		return -ENOENT;
+
+	lut->temp_index = pvi_wbf_get_temp_index(file, 25);
+	if (lut->temp_index < 0)
+		return -ENOENT;
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_init);
+
+MODULE_AUTHOR("Samuel Holland <samuel@sholland.org>");
+MODULE_DESCRIPTION("DRM EPD waveform LUT library");
+MODULE_LICENSE("Dual MIT/GPL");
diff --git a/include/drm/drm_epd_helper.h b/include/drm/drm_epd_helper.h
new file mode 100644
index 000000000000..290f5d48c0cb
--- /dev/null
+++ b/include/drm/drm_epd_helper.h
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: GPL-2.0 OR MIT */
+
+#ifndef __DRM_EPD_HELPER_H__
+#define __DRM_EPD_HELPER_H__
+
+#define DRM_EPD_DEFAULT_TEMPERATURE	25
+
+struct drm_device;
+struct firmware;
+struct pvi_wbf_file_header;
+struct pvi_wbf_mode_info;
+
+/**
+ * enum drm_epd_waveform - Identifiers for waveforms used to drive EPD pixels
+ *
+ * @DRM_EPD_WF_RESET: Used to initialize the panel, ends with white
+ * @DRM_EPD_WF_A2: Fast transitions between black and white only
+ * @DRM_EPD_WF_DU: Transitions 16-level grayscale to monochrome
+ * @DRM_EPD_WF_DU4: Transitions 16-level grayscale to 4-level grayscale
+ * @DRM_EPD_WF_GC16: High-quality but flashy 16-level grayscale
+ * @DRM_EPD_WF_GCC16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GL16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GLR16: Less flashy 16-level grayscale, plus anti-ghosting
+ * @DRM_EPD_WF_GLD16: Less flashy 16-level grayscale, plus anti-ghosting
+ */
+enum drm_epd_waveform {
+	DRM_EPD_WF_RESET,
+	DRM_EPD_WF_A2,
+	DRM_EPD_WF_DU,
+	DRM_EPD_WF_DU4,
+	DRM_EPD_WF_GC16,
+	DRM_EPD_WF_GCC16,
+	DRM_EPD_WF_GL16,
+	DRM_EPD_WF_GLR16,
+	DRM_EPD_WF_GLD16,
+	DRM_EPD_WF_MAX
+};
+
+/**
+ * enum drm_epd_lut_format - EPD LUT buffer format
+ *
+ * @DRM_EPD_LUT_4BIT: 4-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_4BIT_PACKED: 4-bit grayscale indexes, 2 bits per element
+ * @DRM_EPD_LUT_5BIT: 5-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_5BIT_PACKED: 5-bit grayscale indexes, 2 bits per element
+ */
+enum drm_epd_lut_format {
+	DRM_EPD_LUT_4BIT,
+	DRM_EPD_LUT_4BIT_PACKED,
+	DRM_EPD_LUT_5BIT,
+	DRM_EPD_LUT_5BIT_PACKED,
+};
+
+/**
+ * struct drm_epd_lut_file - Describes a file containing EPD LUTs
+ *
+ * @dev: The DRM device owning this LUT file
+ * @fw: The firmware object holding the raw file contents
+ * @header: Vendor-specific LUT file header
+ * @mode_info: Vendor-specific information about available waveforms
+ */
+struct drm_epd_lut_file {
+	struct drm_device			*dev;
+	const struct firmware			*fw;
+	const struct pvi_wbf_file_header	*header;
+	const struct pvi_wbf_mode_info		*mode_info;
+};
+
+/**
+ * struct drm_epd_lut - Describes a particular LUT buffer
+ *
+ * @buf: The LUT, in the format requested by the driver
+ * @file: The file where this LUT was loaded from
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ * @num_phases: The number of waveform phases in the current LUT
+ * @mode_index: Private identifier for the current waveform
+ * @temp_index: Private identifier for the current temperature
+ */
+struct drm_epd_lut {
+	u8				*buf;
+	const struct drm_epd_lut_file	*file;
+	enum drm_epd_lut_format		format;
+	unsigned int			max_phases;
+	unsigned int			num_phases;
+	int				mode_index;
+	int				temp_index;
+};
+
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name);
+
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases);
+
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature);
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform);
+
+#endif /* __DRM_EPD_HELPER_H__ */
-- 
2.35.1


_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip

WARNING: multiple messages have this Message-ID (diff)
From: Samuel Holland <samuel@sholland.org>
To: "Heiko Stübner" <heiko@sntech.de>,
	"Sandy Huang" <hjc@rock-chips.com>,
	dri-devel@lists.freedesktop.org
Cc: linux-rockchip@lists.infradead.org,
	"Alistair Francis" <alistair@alistair23.me>,
	"Ondřej Jirman" <x@xff.cz>,
	"Andreas Kemnade" <andreas@kemnade.info>,
	"Daniel Vetter" <daniel@ffwll.ch>,
	"David Airlie" <airlied@linux.ie>,
	"Geert Uytterhoeven" <geert@linux-m68k.org>,
	"Samuel Holland" <samuel@sholland.org>,
	"Krzysztof Kozlowski" <krzk+dt@kernel.org>,
	"Liang Chen" <cl@rock-chips.com>,
	"Maarten Lankhorst" <maarten.lankhorst@linux.intel.com>,
	"Maxime Ripard" <mripard@kernel.org>,
	"Michael Riesch" <michael.riesch@wolfvision.net>,
	"Nicolas Frattaroli" <frattaroli.nicolas@gmail.com>,
	"Peter Geis" <pgwipeout@gmail.com>,
	"Rob Herring" <robh+dt@kernel.org>,
	"Sam Ravnborg" <sam@ravnborg.org>,
	"Thierry Reding" <thierry.reding@gmail.com>,
	"Thomas Zimmermann" <tzimmermann@suse.de>,
	devicetree@vger.kernel.org, linux-arm-kernel@lists.infradead.org,
	linux-kernel@vger.kernel.org
Subject: [RFC PATCH 01/16] drm: Add a helper library for EPD drivers
Date: Wed, 13 Apr 2022 17:19:01 -0500	[thread overview]
Message-ID: <20220413221916.50995-2-samuel@sholland.org> (raw)
In-Reply-To: <20220413221916.50995-1-samuel@sholland.org>

Currently, this library is focused on LUTs and LUT files, specifically
the PVI wbf-format files shipped with E-Ink displays. It provides
helpers to load and validate LUT files, and extract LUTs from them.

Since EPD controllers need the LUTs in various formats, this library
allows drivers to declare their desired format. It will then convert
LUTs to that format before returning them.

Signed-off-by: Samuel Holland <samuel@sholland.org>
---

 drivers/gpu/drm/Kconfig          |   6 +
 drivers/gpu/drm/Makefile         |   2 +
 drivers/gpu/drm/drm_epd_helper.c | 663 +++++++++++++++++++++++++++++++
 include/drm/drm_epd_helper.h     | 104 +++++
 4 files changed, 775 insertions(+)
 create mode 100644 drivers/gpu/drm/drm_epd_helper.c
 create mode 100644 include/drm/drm_epd_helper.h

diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index f1422bee3dcc..ad96cf605444 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -198,6 +198,12 @@ config DRM_DP_CEC
 	  Note: not all adapters support this feature, and even for those
 	  that do support this they often do not hook up the CEC pin.
 
+config DRM_EPD_HELPER
+	tristate
+	depends on DRM
+	help
+	  Choose this if you need the EPD (LUT, etc.) helper functions
+
 config DRM_TTM
 	tristate
 	depends on DRM && MMU
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index c2ef5f9fce54..49380ccfe9d6 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -33,6 +33,8 @@ drm-$(CONFIG_DRM_PRIVACY_SCREEN) += drm_privacy_screen.o drm_privacy_screen_x86.
 
 obj-$(CONFIG_DRM_NOMODESET) += drm_nomodeset.o
 
+obj-$(CONFIG_DRM_EPD_HELPER) += drm_epd_helper.o
+
 drm_cma_helper-y := drm_gem_cma_helper.o
 drm_cma_helper-$(CONFIG_DRM_KMS_HELPER) += drm_fb_cma_helper.o
 obj-$(CONFIG_DRM_GEM_CMA_HELPER) += drm_cma_helper.o
diff --git a/drivers/gpu/drm/drm_epd_helper.c b/drivers/gpu/drm/drm_epd_helper.c
new file mode 100644
index 000000000000..433a6728ef3e
--- /dev/null
+++ b/drivers/gpu/drm/drm_epd_helper.c
@@ -0,0 +1,663 @@
+// SPDX-License-Identifier: GPL-2.0 OR MIT
+/*
+ * Electrophoretic Display Helper Library
+ *
+ * Copyright (C) 2022 Samuel Holland <samuel@sholland.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sub license, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the
+ * next paragraph) shall be included in all copies or substantial portions
+ * of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDERS, AUTHORS AND/OR ITS SUPPLIERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+ * USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include <linux/firmware.h>
+#include <linux/module.h>
+#include <linux/vmalloc.h>
+
+#include <drm/drm_device.h>
+#include <drm/drm_epd_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_print.h>
+
+/**
+ * DOC: overview
+ *
+ * This library provides functions for working with the lookup tables (LUTs)
+ * used by electrophoretic displays (EPDs). It fills in a LUT buffer based on
+ * the selected waveform, the panel temperature, and the buffer format needed
+ * by the driver.
+ */
+
+struct pvi_wbf_offset {
+	u8			b[3];
+};
+
+struct pvi_wbf_pointer {
+	struct pvi_wbf_offset	offset;
+	u8			checksum;
+};
+
+struct pvi_wbf_file_header {
+	__le32			checksum;		// 0x00
+	__le32			file_size;		// 0x04
+	__le32			serial;			// 0x08
+	u8			run_type;		// 0x0c
+	u8			fpl_platform;		// 0x0d
+	__le16			fpl_lot;		// 0x0e
+	u8			mode_version;		// 0x10
+	u8			wf_version;		// 0x11
+	u8			wf_subversion;		// 0x12
+	u8			wf_type;		// 0x13
+	u8			panel_size;		// 0x14
+	u8			amepd_part_number;	// 0x15
+	u8			wf_rev;			// 0x16
+	u8			frame_rate_bcd;		// 0x17
+	u8			frame_rate_hex;		// 0x18
+	u8			vcom_offset;		// 0x19
+	u8			unknown[2];		// 0x1a
+	struct pvi_wbf_offset	xwia;			// 0x1c
+	u8			cs1;			// 0x1f
+	struct pvi_wbf_offset	wmta;			// 0x20
+	u8			fvsn;			// 0x23
+	u8			luts;			// 0x24
+	u8			mode_count;		// 0x25
+	u8			temp_range_count;	// 0x26
+	u8			advanced_wf_flags;	// 0x27
+	u8			eb;			// 0x28
+	u8			sb;			// 0x29
+	u8			reserved[5];		// 0x2a
+	u8			cs2;			// 0x2f
+	u8			temp_range_table[];	// 0x30
+};
+static_assert(sizeof(struct pvi_wbf_file_header) == 0x30);
+
+struct pvi_wbf_mode_info {
+	u8			versions[2];
+	u8			format;
+	u8			modes[DRM_EPD_WF_MAX];
+};
+
+static const struct pvi_wbf_mode_info pvi_wbf_mode_info_table[] = {
+	{
+		.versions = {
+			0x09,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x12,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 3,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 5,
+			[DRM_EPD_WF_GLD16]	= 6,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x16,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 5,
+		},
+	},
+	{
+		.versions = {
+			0x18,
+			0x20,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x19,
+			0x43,
+		},
+		.format = DRM_EPD_LUT_5BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 7,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 5,
+			[DRM_EPD_WF_A2]		= 6,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+	{
+		.versions = {
+			0x23,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 5,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 3,
+			[DRM_EPD_WF_GLD16]	= 3,
+			[DRM_EPD_WF_A2]		= 4,
+			[DRM_EPD_WF_GCC16]	= 3,
+		},
+	},
+	{
+		.versions = {
+			0x54,
+		},
+		.format = DRM_EPD_LUT_4BIT_PACKED,
+		.modes = {
+			[DRM_EPD_WF_RESET]	= 0,
+			[DRM_EPD_WF_DU]		= 1,
+			[DRM_EPD_WF_DU4]	= 1,
+			[DRM_EPD_WF_GC16]	= 2,
+			[DRM_EPD_WF_GL16]	= 3,
+			[DRM_EPD_WF_GLR16]	= 4,
+			[DRM_EPD_WF_GLD16]	= 4,
+			[DRM_EPD_WF_A2]		= 5,
+			[DRM_EPD_WF_GCC16]	= 4,
+		},
+	},
+};
+
+static const void *pvi_wbf_apply_offset(const struct drm_epd_lut_file *file,
+					const struct pvi_wbf_offset *offset)
+{
+	u32 bytes = offset->b[0] | offset->b[1] << 8 | offset->b[2] << 16;
+
+	if (bytes >= file->fw->size)
+		return NULL;
+
+	return (const void *)file->header + bytes;
+}
+
+static const void *pvi_wbf_dereference(const struct drm_epd_lut_file *file,
+				       const struct pvi_wbf_pointer *ptr)
+{
+	u8 sum = ptr->offset.b[0] + ptr->offset.b[1] + ptr->offset.b[2];
+
+	if (ptr->checksum != sum)
+		return NULL;
+
+	return pvi_wbf_apply_offset(file, &ptr->offset);
+}
+
+static int pvi_wbf_get_mode_index(const struct drm_epd_lut_file *file,
+				  enum drm_epd_waveform waveform)
+{
+	if (waveform >= DRM_EPD_WF_MAX)
+		return -EINVAL;
+
+	return file->mode_info->modes[waveform];
+}
+
+static int pvi_wbf_get_mode_info(struct drm_epd_lut_file *file)
+{
+	u8 mode_version = file->header->mode_version;
+	const struct pvi_wbf_mode_info *mode_info;
+	int i, v;
+
+	for (i = 0; i < ARRAY_SIZE(pvi_wbf_mode_info_table); i++) {
+		mode_info = &pvi_wbf_mode_info_table[i];
+
+		for (v = 0; v < ARRAY_SIZE(mode_info->versions); v++) {
+			if (mode_info->versions[v] == mode_version) {
+				file->mode_info = mode_info;
+				return 0;
+			}
+		}
+	}
+
+	drm_err(file->dev, "Unknown PVI waveform version 0x%02x\n",
+		mode_version);
+
+	return -EOPNOTSUPP;
+}
+
+static int pvi_wbf_get_temp_index(const struct drm_epd_lut_file *file,
+				  int temperature)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int i;
+
+	for (i = 0; i < header->temp_range_count; i++)
+		if (temperature < header->temp_range_table[i])
+			return i - 1;
+
+	return header->temp_range_count - 1;
+}
+
+static int pvi_wbf_validate_header(struct drm_epd_lut_file *file)
+{
+	const struct pvi_wbf_file_header *header = file->header;
+	int ret;
+
+	if (le32_to_cpu(header->file_size) > file->fw->size)
+		return -EINVAL;
+
+	ret = pvi_wbf_get_mode_info(file);
+	if (ret)
+		return ret;
+
+	drm_info(file->dev, "Loaded %d-bit PVI waveform version 0x%02x\n",
+		 file->mode_info->format == DRM_EPD_LUT_5BIT_PACKED ? 5 : 4,
+		 header->mode_version);
+
+	return 0;
+}
+
+static void drm_epd_lut_file_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut_file *file = res;
+
+	release_firmware(file->fw);
+}
+
+/**
+ * drmm_epd_lut_file_init - Initialize a managed EPD LUT file
+ *
+ * @dev: The DRM device owning this LUT file
+ * @file: The LUT file to initialize
+ * @file_name: The filesystem name of the LUT file
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name)
+{
+	int ret;
+
+	ret = request_firmware(&file->fw, file_name, dev->dev);
+	if (ret)
+		return ret;
+
+	ret = drmm_add_action_or_reset(dev, drm_epd_lut_file_free, file);
+	if (ret)
+		return ret;
+
+	file->dev = dev;
+	file->header = (const void *)file->fw->data;
+
+	ret = pvi_wbf_validate_header(file);
+	if (ret)
+		return ret;
+
+	/* Only 5-bit waveform files are supported by drm_epd_lut_convert. */
+	if (file->mode_info->format != DRM_EPD_LUT_5BIT_PACKED)
+		return -EOPNOTSUPP;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_file_init);
+
+/**
+ * drm_epd_lut_size_shift - Get the size of a LUT phase in power-of-2 bytes
+ *
+ * @format: One of the LUT buffer formats
+ *
+ * Return: buffer size shift amount
+ */
+static int drm_epd_lut_size_shift(enum drm_epd_lut_format format)
+{
+	switch (format) {
+	case DRM_EPD_LUT_4BIT:
+		/* (4 bits/pixel)^2 / 1 pixel/byte */
+		return 4 + 4;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		/* (4 bits/pixel)^2 / 4 pixels/byte */
+		return 4 + 4 - 2;
+	case DRM_EPD_LUT_5BIT:
+		/* (5 bits/pixel)^2 / 1 pixel/byte */
+		return 5 + 5;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* (5 bits/pixel)^2 / 4 pixels/byte */
+		return 5 + 5 - 2;
+	}
+
+	unreachable();
+}
+
+static int pvi_wbf_decode_lut(struct drm_epd_lut *lut, const u8 *lut_data)
+{
+
+	unsigned int copies, max_bytes, size_shift, state, total_bytes;
+	struct drm_device *dev = lut->file->dev;
+	const u8 *in = lut_data;
+	u8 *out = lut->buf;
+	u8 token;
+
+	size_shift = drm_epd_lut_size_shift(lut->file->mode_info->format);
+	max_bytes = lut->max_phases << size_shift;
+	total_bytes = 0;
+	state = 1;
+
+	/* Read tokens until reaching the end-of-input marker. */
+	while ((token = *in++) != 0xff) {
+		/* Special handling for the state switch token. */
+		if (token == 0xfc) {
+			state = !state;
+			token = *in++;
+		}
+
+		/*
+		 * State 0 is a sequence of data bytes.
+		 * State 1 is a sequence of [data byte, extra copies] pairs.
+		 */
+		copies = 1 + (state ? *in++ : 0);
+
+		total_bytes += copies;
+		if (total_bytes > max_bytes) {
+			drm_err(dev, "LUT contains too many phases\n");
+			lut->num_phases = 0;
+			return -EILSEQ;
+		}
+
+		while (copies--)
+			*out++ = token;
+	}
+
+	lut->num_phases = total_bytes >> size_shift;
+	if (total_bytes != lut->num_phases << size_shift) {
+		drm_err(dev, "LUT contains a partial phase\n");
+		lut->num_phases = 0;
+		return -EILSEQ;
+	}
+
+	drm_dbg_core(dev, "LUT contains %d phases (%ld => %ld bytes)\n",
+		     lut->num_phases, in - lut_data, out - lut->buf);
+
+	return 0;
+}
+
+static int pvi_wbf_get_lut(struct drm_epd_lut *lut,
+			   int mode_index, int temp_index)
+{
+	const struct pvi_wbf_pointer *mode_table, *temp_table;
+	const struct drm_epd_lut_file *file = lut->file;
+	const u8 *lut_data;
+	int ret;
+
+	mode_table = pvi_wbf_apply_offset(file, &file->header->wmta);
+	if (!mode_table)
+		return -EFAULT;
+
+	temp_table = pvi_wbf_dereference(file, &mode_table[mode_index]);
+	if (!temp_table)
+		return -EFAULT;
+
+	lut_data = pvi_wbf_dereference(file, &temp_table[temp_index]);
+	if (!lut_data)
+		return -EFAULT;
+
+	ret = pvi_wbf_decode_lut(lut, lut_data);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void drm_epd_lut_convert(const struct drm_epd_lut *lut)
+{
+	enum drm_epd_lut_format from = lut->file->mode_info->format;
+	enum drm_epd_lut_format to = lut->format;
+	u8 *buf = lut->buf;
+	size_t x, y;
+
+	if (to == from)
+		return;
+
+	switch (to) {
+	case DRM_EPD_LUT_4BIT:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 8; x--;) {
+				u8 byte = buf[16 * y + x];
+
+				buf[16 * y + 2 * x + 0] = (byte >> 0) & 0x03;
+				buf[16 * y + 2 * x + 1] = (byte >> 4) & 0x03;
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 8; x--;) {
+				buf[16 * y + 2 * x + 0] = 0;
+				buf[16 * y + 2 * x + 1] = 0;
+			}
+		}
+		break;
+	case DRM_EPD_LUT_4BIT_PACKED:
+		for (y = 0; y < 16 * lut->num_phases; ++y) {
+			for (x = 4; x--;) {
+				u8 lo_byte = buf[16 * y + 2 * x + 0] & 0x33;
+				u8 hi_byte = buf[16 * y + 2 * x + 1] & 0x33;
+
+				/* Copy bits 4:5 => bits 2:3. */
+				lo_byte |= lo_byte >> 2;
+				hi_byte |= hi_byte >> 2;
+
+				buf[4 * y + x] = (lo_byte & 0xf) |
+						 (hi_byte << 4);
+			}
+		}
+		for (; y < 16 * lut->max_phases; ++y) {
+			for (x = 4; x--;)
+				buf[4 * y + x] = 0;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT:
+		memset(buf + 256 * lut->num_phases, 0,
+		       256 * (lut->max_phases - lut->num_phases));
+		for (x = 256 * lut->num_phases; x--;) {
+			u8 byte = buf[x];
+
+			buf[4 * x + 0] = (byte >> 0) & 0x03;
+			buf[4 * x + 1] = (byte >> 2) & 0x03;
+			buf[4 * x + 2] = (byte >> 4) & 0x03;
+			buf[4 * x + 3] = (byte >> 6) & 0x03;
+		}
+		break;
+	case DRM_EPD_LUT_5BIT_PACKED:
+		/* Nothing to do. */
+		break;
+	}
+}
+
+static int drm_epd_lut_update(struct drm_epd_lut *lut,
+			      int mode_index, int temp_index)
+{
+	int ret;
+
+	ret = pvi_wbf_get_lut(lut, mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	drm_epd_lut_convert(lut);
+
+	return 0;
+}
+
+/**
+ * drm_epd_lut_set_temperature - Update the LUT due to panel temperature change
+ *
+ * @lut: The LUT structure
+ * @temperateure: The current panel temperature in degrees Celsius
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature)
+{
+	int temp_index;
+	int ret;
+
+	temp_index = pvi_wbf_get_temp_index(lut->file, temperature);
+	if (temp_index < 0)
+		return -ENOENT;
+
+	if (temp_index == lut->temp_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT temperature changed (%d)\n",
+		     temperature);
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, temp_index);
+	if (ret)
+		return ret;
+
+	lut->temp_index = temp_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_temperature);
+
+/**
+ * drm_epd_lut_set_waveform - Update the LUT due to waveform selection change
+ *
+ * @lut: The LUT structure
+ * @waveform: The desired waveform
+ *
+ * Return: negative errno on failure, 1 if LUT was changed, 0 otherwise
+ */
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform)
+{
+	int mode_index;
+	int ret;
+
+	mode_index = pvi_wbf_get_mode_index(lut->file, waveform);
+	if (mode_index < 0)
+		return -ENOENT;
+
+	if (mode_index == lut->mode_index)
+		return 0;
+
+	drm_dbg_core(lut->file->dev, "LUT waveform changed (%d)\n",
+		     waveform);
+
+	ret = drm_epd_lut_update(lut, mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	lut->mode_index = mode_index;
+
+	return 1;
+}
+EXPORT_SYMBOL(drm_epd_lut_set_waveform);
+
+static void drm_epd_lut_free(struct drm_device *dev, void *res)
+{
+	struct drm_epd_lut *lut = res;
+
+	vfree(lut->buf);
+}
+
+/**
+ * drmm_epd_lut_init - Initialize a managed EPD LUT from a LUT file
+ *
+ * @dev: The DRM device owning this LUT
+ * @lut: The LUT to initialize
+ * @file_name: The file name of the waveform firmware
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ *
+ * Return: negative errno on failure, 0 otherwise
+ */
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases)
+{
+	size_t max_order;
+	int ret;
+
+	/* Allocate a buffer large enough to convert the LUT in place. */
+	max_order = max(drm_epd_lut_size_shift(file->mode_info->format),
+			drm_epd_lut_size_shift(format));
+	lut->buf = vmalloc(max_phases << max_order);
+	if (!lut->buf)
+		return -ENOMEM;
+
+	ret = drmm_add_action_or_reset(file->dev, drm_epd_lut_free, lut);
+	if (ret)
+		return ret;
+
+	lut->file	= file;
+	lut->format	= format;
+	lut->max_phases	= max_phases;
+	lut->num_phases	= 0;
+
+	/* Set sane initial values for the waveform and temperature. */
+	lut->mode_index = pvi_wbf_get_mode_index(file, DRM_EPD_WF_RESET);
+	if (lut->mode_index < 0)
+		return -ENOENT;
+
+	lut->temp_index = pvi_wbf_get_temp_index(file, 25);
+	if (lut->temp_index < 0)
+		return -ENOENT;
+
+	ret = drm_epd_lut_update(lut, lut->mode_index, lut->temp_index);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+EXPORT_SYMBOL(drmm_epd_lut_init);
+
+MODULE_AUTHOR("Samuel Holland <samuel@sholland.org>");
+MODULE_DESCRIPTION("DRM EPD waveform LUT library");
+MODULE_LICENSE("Dual MIT/GPL");
diff --git a/include/drm/drm_epd_helper.h b/include/drm/drm_epd_helper.h
new file mode 100644
index 000000000000..290f5d48c0cb
--- /dev/null
+++ b/include/drm/drm_epd_helper.h
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: GPL-2.0 OR MIT */
+
+#ifndef __DRM_EPD_HELPER_H__
+#define __DRM_EPD_HELPER_H__
+
+#define DRM_EPD_DEFAULT_TEMPERATURE	25
+
+struct drm_device;
+struct firmware;
+struct pvi_wbf_file_header;
+struct pvi_wbf_mode_info;
+
+/**
+ * enum drm_epd_waveform - Identifiers for waveforms used to drive EPD pixels
+ *
+ * @DRM_EPD_WF_RESET: Used to initialize the panel, ends with white
+ * @DRM_EPD_WF_A2: Fast transitions between black and white only
+ * @DRM_EPD_WF_DU: Transitions 16-level grayscale to monochrome
+ * @DRM_EPD_WF_DU4: Transitions 16-level grayscale to 4-level grayscale
+ * @DRM_EPD_WF_GC16: High-quality but flashy 16-level grayscale
+ * @DRM_EPD_WF_GCC16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GL16: Less flashy 16-level grayscale
+ * @DRM_EPD_WF_GLR16: Less flashy 16-level grayscale, plus anti-ghosting
+ * @DRM_EPD_WF_GLD16: Less flashy 16-level grayscale, plus anti-ghosting
+ */
+enum drm_epd_waveform {
+	DRM_EPD_WF_RESET,
+	DRM_EPD_WF_A2,
+	DRM_EPD_WF_DU,
+	DRM_EPD_WF_DU4,
+	DRM_EPD_WF_GC16,
+	DRM_EPD_WF_GCC16,
+	DRM_EPD_WF_GL16,
+	DRM_EPD_WF_GLR16,
+	DRM_EPD_WF_GLD16,
+	DRM_EPD_WF_MAX
+};
+
+/**
+ * enum drm_epd_lut_format - EPD LUT buffer format
+ *
+ * @DRM_EPD_LUT_4BIT: 4-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_4BIT_PACKED: 4-bit grayscale indexes, 2 bits per element
+ * @DRM_EPD_LUT_5BIT: 5-bit grayscale indexes, 1 byte per element
+ * @DRM_EPD_LUT_5BIT_PACKED: 5-bit grayscale indexes, 2 bits per element
+ */
+enum drm_epd_lut_format {
+	DRM_EPD_LUT_4BIT,
+	DRM_EPD_LUT_4BIT_PACKED,
+	DRM_EPD_LUT_5BIT,
+	DRM_EPD_LUT_5BIT_PACKED,
+};
+
+/**
+ * struct drm_epd_lut_file - Describes a file containing EPD LUTs
+ *
+ * @dev: The DRM device owning this LUT file
+ * @fw: The firmware object holding the raw file contents
+ * @header: Vendor-specific LUT file header
+ * @mode_info: Vendor-specific information about available waveforms
+ */
+struct drm_epd_lut_file {
+	struct drm_device			*dev;
+	const struct firmware			*fw;
+	const struct pvi_wbf_file_header	*header;
+	const struct pvi_wbf_mode_info		*mode_info;
+};
+
+/**
+ * struct drm_epd_lut - Describes a particular LUT buffer
+ *
+ * @buf: The LUT, in the format requested by the driver
+ * @file: The file where this LUT was loaded from
+ * @format: The LUT buffer format needed by the driver
+ * @max_phases: The maximum number of waveform phases supported by the driver
+ * @num_phases: The number of waveform phases in the current LUT
+ * @mode_index: Private identifier for the current waveform
+ * @temp_index: Private identifier for the current temperature
+ */
+struct drm_epd_lut {
+	u8				*buf;
+	const struct drm_epd_lut_file	*file;
+	enum drm_epd_lut_format		format;
+	unsigned int			max_phases;
+	unsigned int			num_phases;
+	int				mode_index;
+	int				temp_index;
+};
+
+int drmm_epd_lut_file_init(struct drm_device *dev,
+			   struct drm_epd_lut_file *file,
+			   const char *file_name);
+
+int drmm_epd_lut_init(struct drm_epd_lut_file *file,
+		      struct drm_epd_lut *lut,
+		      enum drm_epd_lut_format format,
+		      unsigned int max_phases);
+
+int drm_epd_lut_set_temperature(struct drm_epd_lut *lut,
+				int temperature);
+int drm_epd_lut_set_waveform(struct drm_epd_lut *lut,
+			     enum drm_epd_waveform waveform);
+
+#endif /* __DRM_EPD_HELPER_H__ */
-- 
2.35.1


_______________________________________________
linux-arm-kernel mailing list
linux-arm-kernel@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-arm-kernel

  reply	other threads:[~2022-04-13 22:19 UTC|newest]

Thread overview: 110+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-04-13 22:19 [RFC PATCH 00/16] drm/rockchip: Rockchip EBC ("E-Book Controller") display driver Samuel Holland
2022-04-13 22:19 ` Samuel Holland
2022-04-13 22:19 ` Samuel Holland
2022-04-13 22:19 ` Samuel Holland
2022-04-13 22:19 ` Samuel Holland [this message]
2022-04-13 22:19   ` [RFC PATCH 01/16] drm: Add a helper library for EPD drivers Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 02/16] dt-bindings: display: rockchip: Add EBC binding Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-14  8:15   ` Andreas Kemnade
2022-04-14  8:15     ` Andreas Kemnade
2022-04-14  8:15     ` Andreas Kemnade
2022-04-14  8:15     ` Andreas Kemnade
2022-04-15  3:00     ` Samuel Holland
2022-04-15  3:00       ` Samuel Holland
2022-04-15  3:00       ` Samuel Holland
2022-04-15  3:00       ` Samuel Holland
2022-04-15 11:45       ` Andreas Kemnade
2022-04-15 11:45         ` Andreas Kemnade
2022-04-15 11:45         ` Andreas Kemnade
2022-04-15 11:45         ` Andreas Kemnade
2022-04-13 22:19 ` [RFC PATCH 03/16] drm/rockchip: Add EBC platform driver skeleton Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 04/16] drm/rockchip: ebc: Add DRM " Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 05/16] drm/rockchip: ebc: Add CRTC mode setting Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 06/16] drm/rockchip: ebc: Add CRTC refresh thread Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 07/16] drm/rockchip: ebc: Add CRTC buffer management Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 08/16] drm/rockchip: ebc: Add LUT loading Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 09/16] drm/rockchip: ebc: Implement global refreshes Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 10/16] drm/rockchip: ebc: Implement partial refreshes Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 11/16] drm/rockchip: ebc: Enable diff mode for " Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 12/16] drm/rockchip: ebc: Add support for direct mode Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 13/16] drm/rockchip: ebc: Add a panel reflection option Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 14/16] drm/panel-simple: Add eInk ED103TC2 Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 15/16] arm64: dts: rockchip: rk356x: Add EBC node Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19 ` [RFC PATCH 16/16] [DO NOT MERGE] arm64: dts: rockchip: pinenote: Enable EBC display pipeline Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-13 22:19   ` Samuel Holland
2022-04-14  8:50 ` [RFC PATCH 00/16] drm/rockchip: Rockchip EBC ("E-Book Controller") display driver Maxime Ripard
2022-04-14  8:50   ` Maxime Ripard
2022-04-14  8:50   ` Maxime Ripard
2022-04-14  8:50   ` Maxime Ripard
2022-05-25 17:18   ` Daniel Vetter
2022-05-25 17:18     ` Daniel Vetter
2022-05-25 17:18     ` Daniel Vetter
2022-05-25 17:18     ` Daniel Vetter
2022-05-31  8:58     ` Maxime Ripard
2022-05-31  8:58       ` Maxime Ripard
2022-05-31  8:58       ` Maxime Ripard
2022-06-01 12:35       ` Daniel Vetter
2022-06-01 12:35         ` Daniel Vetter
2022-06-01 12:35         ` Daniel Vetter
2022-06-01 12:35         ` Daniel Vetter
2022-06-08 14:48         ` Maxime Ripard
2022-06-08 14:48           ` Maxime Ripard
2022-06-08 14:48           ` Maxime Ripard
2022-06-08 15:34           ` Daniel Vetter
2022-06-08 15:34             ` Daniel Vetter
2022-06-08 15:34             ` Daniel Vetter
2022-06-08 15:34             ` Daniel Vetter
2022-04-21  6:43 ` Andreas Kemnade
2022-04-21  6:43   ` Andreas Kemnade
2022-04-21  6:43   ` Andreas Kemnade
2022-04-21  6:43   ` Andreas Kemnade
2022-04-21  7:10   ` Nicolas Frattaroli
2022-04-21  7:10     ` Nicolas Frattaroli
2022-04-21  7:10     ` Nicolas Frattaroli
2022-04-21  7:10     ` Nicolas Frattaroli

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20220413221916.50995-2-samuel@sholland.org \
    --to=samuel@sholland.org \
    --cc=airlied@linux.ie \
    --cc=alistair@alistair23.me \
    --cc=andreas@kemnade.info \
    --cc=cl@rock-chips.com \
    --cc=daniel@ffwll.ch \
    --cc=devicetree@vger.kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=frattaroli.nicolas@gmail.com \
    --cc=geert@linux-m68k.org \
    --cc=heiko@sntech.de \
    --cc=hjc@rock-chips.com \
    --cc=krzk+dt@kernel.org \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-rockchip@lists.infradead.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=michael.riesch@wolfvision.net \
    --cc=mripard@kernel.org \
    --cc=pgwipeout@gmail.com \
    --cc=robh+dt@kernel.org \
    --cc=sam@ravnborg.org \
    --cc=thierry.reding@gmail.com \
    --cc=tzimmermann@suse.de \
    --cc=x@xff.cz \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.