summary refs log tree commit diff
path: root/mhs35.c
diff options
context:
space:
mode:
Diffstat (limited to 'mhs35.c')
-rw-r--r--mhs35.c551
1 files changed, 551 insertions, 0 deletions
diff --git a/mhs35.c b/mhs35.c
new file mode 100644
index 0000000..855f450
--- /dev/null
+++ b/mhs35.c
@@ -0,0 +1,551 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * drm driver for the goodtft mhs-3.5 raspberry pi display,
+ * a cheap clone of the waveshare 3.5" ili9486 hat!
+ *
+ * whatever the clone is using as its display controller (i doubt
+ * it's the ili9486) has a number of severe defects on memory writes,
+ * causing various corruption if the display is attempted to be used
+ * directly via drm with proper damage tracking, and not the default
+ * framebuffer full-page copy approach goodtft wants you to use.
+ *
+ * this kernel module implements a kernel drm driver for this type
+ * of panel, compensating any writes to avoid exposing any corruption
+ * inherent to the panel hardware, allowing you to use it in a simple,
+ * performant and clean manner! the differences between the genuine
+ * hardware and the clone are pointed out in the code.
+ *
+ * it's also important to note, even though i'm referring to this panel
+ * as "goodtft" hardware, it's quite likely that the actual manufacturer
+ * is someone completely different. this panel seems like it's created
+ * in large quantities at various factories, sold under a thousand
+ * different storefronts, with the one similarity being that it's
+ * documentation points you to a repository hosted under "goodtft" to
+ * set up all the driver and userspace ecosystem for this display
+ * for you, in the only official way the panel supports.
+ *
+ * Copyright 2020 Kamlesh Gurudasani <kamlesh.gurudasani@gmail.com>
+ * Copyright 2026 Mel G. <mel@rnrd.eu>
+ */
+
+#include <linux/backlight.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/spi/spi.h>
+
+#include <video/mipi_display.h>
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_damage_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fbdev_dma.h>
+#include <drm/drm_format_helper.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_atomic_helper.h>
+#include <drm/drm_gem_dma_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_mipi_dbi.h>
+#include <drm/drm_modeset_helper.h>
+#include <drm/drm_rect.h>
+
+#define MHS35_VMCTRL1		0xc5
+#define MHS35_PGAMCTRL		0xe0
+#define MHS35_NGAMCTRL		0xe1
+#define MHS35_MADCTL_BGR	BIT(3)
+#define MHS35_MADCTL_MV		BIT(5)
+#define MHS35_MADCTL_MX		BIT(6)
+#define MHS35_MADCTL_MY		BIT(7)
+
+struct mhs35 {
+	struct mipi_dbi_dev dbidev;
+	void *bounce;
+};
+
+// custom init sequence, replacing the default goodtft sequence when set.
+// encoding: 0x01000000|cmd starts command, plain values are params, 0x01000000|ms sleeps.
+static u32 mhs35_dt_init[128];
+static int mhs35_dt_init_len;
+
+// send a command to the panel.
+// copied from `waveshare_command`.
+static int mhs35_command(struct mipi_dbi *mipi, u8 *cmd, u8 *par, size_t num)
+{
+	struct spi_device *spi = mipi->spi;
+	unsigned int bpw = 8;
+	void *data = par;
+	u32 speed_hz;
+	int i, ret;
+	__be16 *buf;
+
+	buf = kmalloc(32 * sizeof(u16), GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	buf[0] = cpu_to_be16(*cmd);
+	spi_bus_lock(spi->controller);
+	gpiod_set_value_cansleep(mipi->dc, 0);
+	speed_hz = mipi_dbi_spi_cmd_max_speed(spi, 2);
+	ret = mipi_dbi_spi_transfer(spi, speed_hz, 8, buf, 2);
+	spi_bus_unlock(spi->controller);
+	if (ret || !num)
+		goto free;
+
+	// 8-bit configuration data, not 16-bit pixel data
+	if (num <= 32) {
+		for (i = 0; i < num; i++)
+			buf[i] = cpu_to_be16(par[i]);
+		num *= 2;
+		data = buf;
+	}
+
+	if (*cmd == MIPI_DCS_WRITE_MEMORY_START && !mipi->swap_bytes)
+		bpw = 16;
+
+	spi_bus_lock(spi->controller);
+	gpiod_set_value_cansleep(mipi->dc, 1);
+	speed_hz = mipi_dbi_spi_cmd_max_speed(spi, num);
+	ret = mipi_dbi_spi_transfer(spi, speed_hz, bpw, data, num);
+	spi_bus_unlock(spi->controller);
+ free:
+	kfree(buf);
+
+	return ret;
+}
+
+static void mhs35_set_window(struct mipi_dbi_dev *dbidev,
+			     unsigned int xs, unsigned int xe,
+			     unsigned int ys, unsigned int ye)
+{
+	struct mipi_dbi *dbi = &dbidev->dbi;
+
+	xs += dbidev->left_offset;
+	xe += dbidev->left_offset;
+	ys += dbidev->top_offset;
+	ye += dbidev->top_offset;
+
+	mipi_dbi_command(dbi, MIPI_DCS_SET_COLUMN_ADDRESS, (xs >> 8) & 0xff,
+			 xs & 0xff, (xe >> 8) & 0xff, xe & 0xff);
+	mipi_dbi_command(dbi, MIPI_DCS_SET_PAGE_ADDRESS, (ys >> 8) & 0xff,
+			 ys & 0xff, (ye >> 8) & 0xff, ye & 0xff);
+}
+
+static void mhs35_fb_dirty(struct iosys_map *src, struct drm_framebuffer *fb,
+			   struct drm_rect *rect,
+			   struct drm_format_conv_state *fmtcnv_state)
+{
+	struct mipi_dbi_dev *dbidev = drm_to_mipi_dbi_dev(fb->dev);
+	struct mhs35 *priv = container_of(dbidev, struct mhs35, dbidev);
+	const struct drm_format_info *dst_format;
+	struct mipi_dbi *dbi = &dbidev->dbi;
+	unsigned int width, y;
+	size_t pitch;
+	bool clip;
+	int ret = 0;
+	void *tr = dbidev->tx_buf;
+
+	// the clone can't start a window on even pages past 255, bit 8
+	// of the page address forces bit 0 high, and the write lands one
+	// row lower than requested.
+	// the internal row increment counts perfectly fine though,
+	// so we grow the window one row up and walk into the zone through it instead!
+	if (rect->y1 >= 256 && (rect->y1 & 1) == 0)
+		rect->y1--;
+	// windows starting at page 0 or touching the last column trip
+	// the byte-eating defect (see pair loop below), and our
+	// compensation for that is only calibrated for full-width
+	// windows, so widen these all the way!
+	if (rect->y1 == 0 || rect->x2 >= fb->width) {
+		rect->x1 = 0;
+		rect->x2 = fb->width;
+	}
+	// the panel assembles its 16-bit pixels from byte pairs anchored
+	// at even absolute columns & a window starting on an odd column
+	// gets all of its colors byte-swapped!
+	rect->x1 &= ~1;
+	// starting a window right next to the origin corrupts the first
+	// cell of every row for some horrible reason.
+	// just snap it it directly to the origin, it's perfectly fine there.
+	if (rect->x1 <= 2)
+		rect->x1 = 0;
+	if (rect->x2 & 1)
+		rect->x2 = min_t(int, fb->width, rect->x2 + 1);
+	width = rect->x2 - rect->x1;
+
+	DRM_DEBUG_KMS("flushing [fb:%d] " DRM_RECT_FMT "\n", fb->base.id,
+		      DRM_RECT_ARG(rect));
+
+	ret = mipi_dbi_buf_copy(tr, src, fb, rect, dbi->swap_bytes,
+				fmtcnv_state);
+	if (ret)
+		goto err_msg;
+
+	if (fb->format->format == DRM_FORMAT_XRGB8888)
+		dst_format = drm_format_info(dbidev->pixel_format);
+	else
+		dst_format = fb->format;
+	pitch = drm_format_info_min_pitch(dst_format, 0, width);
+
+	clip = (rect->x1 == 0 && rect->x2 == fb->width && width == 480);
+
+	// flush in pairs of rows! the panel eats one byte per full-width
+	// row, so both rows of a pair get clipped to 959 bytes, and the
+	// second one additionally byte-swapped, because the odd length
+	// flips the panel's byte-pair phase mid-window.
+	// a pair is 1918 bytes in total, which is conveniently parity-even too! :)
+	// windows taller than two rows just start drifting anyway, so...
+	y = rect->y1;
+	while (y < rect->y2) {
+		unsigned int i, start = y, band_h, band_end;
+		size_t off = 0;
+
+		band_end = min_t(unsigned int, y + 2, rect->y2);
+		// same even-page thing as above, but for where the next
+		// pair will start.
+		// shorten this one so its neighbor begins on an odd page.
+		if (band_end < rect->y2 && band_end >= 256 &&
+		    (band_end & 1) == 0)
+			band_end--;
+		if (band_end <= start)
+			band_end = start + 1;
+		band_h = band_end - start;
+
+		for (i = 0; i < band_h; i++) {
+			size_t n = pitch;
+			u8 *b = priv->bounce + off;
+
+			if (clip)
+				n = pitch - 1;
+			// the byte-pair phase also survives across chip-select windows.
+			// we keep every window's total byte count even,
+			// so the next flush starts out aligned.
+			// single-row tail windows carry their full final byte to make sums work!
+			if (clip && i == band_h - 1 && ((off + n) & 1))
+				n++;
+			memcpy(b, tr + (start - rect->y1 + i) * pitch, n);
+			// column 479 usually only receives the high byte
+			// of its pixel, it's basically dead. instead of
+			// sending it garbage that shows up as noise we
+			// blank it.
+			// NOTE(mel): the full-byte tail rows right above this
+			// *do* actually manage to land a real value in there,
+			// so from time to time some pixels will show up at that
+			// column anyway.
+			if (clip && n < pitch)
+				b[n - 1] = 0;
+			if (clip && (i & 1)) {
+				size_t j;
+
+				for (j = 0; j + 1 < n; j += 2)
+					swap(b[j], b[j + 1]);
+			}
+			off += n;
+		}
+
+		mhs35_set_window(dbidev, rect->x1, rect->x2 - 1,
+				 start, band_end - 1);
+		ret = mipi_dbi_command_buf(dbi, MIPI_DCS_WRITE_MEMORY_START,
+					   priv->bounce, off);
+		if (ret)
+			break;
+
+		y = band_end;
+	}
+
+ err_msg:
+	if (ret)
+		drm_err_once(fb->dev, "failed to update display %d\n", ret);
+}
+
+static void mhs35_pipe_update(struct drm_simple_display_pipe *pipe,
+			      struct drm_plane_state *old_state)
+{
+	struct drm_plane_state *state = pipe->plane.state;
+	struct drm_shadow_plane_state *shadow_plane_state =
+		to_drm_shadow_plane_state(state);
+	struct drm_framebuffer *fb = state->fb;
+	struct drm_rect rect;
+	int idx;
+
+	if (!pipe->crtc.state->active)
+		return;
+
+	if (WARN_ON(!fb))
+		return;
+
+	if (!drm_dev_enter(fb->dev, &idx))
+		return;
+
+	if (drm_atomic_helper_damage_merged(old_state, state, &rect))
+		mhs35_fb_dirty(&shadow_plane_state->data[0], fb, &rect,
+			       &shadow_plane_state->fmtcnv_state);
+
+	drm_dev_exit(idx);
+}
+
+static bool mhs35_send_dt_init(struct mipi_dbi *dbi)
+{
+	const u32 *seq = mhs35_dt_init;
+	int len = mhs35_dt_init_len, i = 0;
+	u8 par[32];
+
+	if (len <= 0)
+		return false;
+
+	while (i < len) {
+		u32 v = seq[i++];
+		u8 cmd;
+		int n = 0;
+
+		if ((v & 0xff000000) == 0x02000000) {
+			msleep(v & 0xffff);
+			continue;
+		}
+		if ((v & 0xff000000) != 0x01000000)
+			continue;
+		cmd = v & 0xff;
+		while (i < len && (seq[i] & 0xff000000) == 0 &&
+		       n < (int)sizeof(par))
+			par[n++] = seq[i++] & 0xff;
+		mipi_dbi_command_buf(dbi, cmd, par, n);
+	}
+	return true;
+}
+
+static void mhs35_enable(struct drm_simple_display_pipe *pipe,
+			 struct drm_crtc_state *crtc_state,
+			 struct drm_plane_state *plane_state)
+{
+	struct mipi_dbi_dev *dbidev = drm_to_mipi_dbi_dev(pipe->crtc.dev);
+	struct drm_shadow_plane_state *shadow_plane_state =
+		to_drm_shadow_plane_state(plane_state);
+	struct drm_framebuffer *fb = plane_state->fb;
+	struct mipi_dbi *dbi = &dbidev->dbi;
+	u8 addr_mode;
+	int ret, idx;
+
+	if (!drm_dev_enter(pipe->crtc.dev, &idx))
+		return;
+
+	DRM_DEBUG_KMS("\n");
+
+	ret = mipi_dbi_poweron_conditional_reset(dbidev);
+	if (ret < 0)
+		goto out_exit;
+	if (ret == 1)
+		goto out_enable;
+
+	if (mhs35_send_dt_init(dbi))
+		goto init_done;
+
+	// goodtft vendor init sequence, undocumented f-register pokes
+	// and all that fun stuff! the mainline ili9486 init also mostly
+	// it's likely the mainline ili9486 init also works just fine with
+	// panels like this, but since no defects come from using it, why not?
+	// extracted from devicetree files. see: https://github.com/goodtft/LCD-show
+	mipi_dbi_command(dbi, 0xf1, 0x36, 0x04, 0x00, 0x3c, 0x0f, 0x8f);
+	mipi_dbi_command(dbi, 0xf2, 0x18, 0xa3, 0x12, 0x02, 0xb2, 0x12,
+			 0xff, 0x10, 0x00);
+	mipi_dbi_command(dbi, 0xf8, 0x21, 0x04);
+	mipi_dbi_command(dbi, 0xf9, 0x00, 0x08);
+	mipi_dbi_command(dbi, 0xb4, 0x00);
+	mipi_dbi_command(dbi, 0xc1, 0x41);
+	mipi_dbi_command(dbi, MHS35_VMCTRL1, 0x00, 0x91, 0x80, 0x00);
+	mipi_dbi_command(dbi, MHS35_PGAMCTRL,
+			 0x0F, 0x1F, 0x1C, 0x0C, 0x0F, 0x08, 0x48, 0x98,
+			 0x37, 0x0A, 0x13, 0x04, 0x11, 0x0D, 0x00);
+	mipi_dbi_command(dbi, MHS35_NGAMCTRL,
+			 0x0F, 0x32, 0x2E, 0x0B, 0x0D, 0x05, 0x47, 0x75,
+			 0x37, 0x06, 0x10, 0x03, 0x24, 0x20, 0x00);
+	mipi_dbi_command(dbi, MIPI_DCS_SET_PIXEL_FORMAT, 0x55);
+	mipi_dbi_command(dbi, MIPI_DCS_EXIT_SLEEP_MODE);
+	msleep(250);
+
+	mipi_dbi_command(dbi, MIPI_DCS_SET_DISPLAY_ON);
+	msleep(100);
+
+ init_done:
+ out_enable:
+	switch (dbidev->rotation) {
+	case 90:
+		addr_mode = MHS35_MADCTL_MY;
+		break;
+	case 180:
+		addr_mode = MHS35_MADCTL_MV;
+		break;
+	case 270:
+		addr_mode = MHS35_MADCTL_MX;
+		break;
+	default:
+		addr_mode = MHS35_MADCTL_MV | MHS35_MADCTL_MY |
+			MHS35_MADCTL_MX;
+		break;
+	}
+	addr_mode |= MHS35_MADCTL_BGR;
+	mipi_dbi_command(dbi, MIPI_DCS_SET_ADDRESS_MODE, addr_mode);
+
+	// initial flush goes through our dirty path and not the in-tree one!
+	if (fb) {
+		struct drm_rect rect = {
+			.x1 = 0, .y1 = 0,
+			.x2 = fb->width, .y2 = fb->height,
+		};
+
+		mhs35_fb_dirty(&shadow_plane_state->data[0], fb, &rect,
+			       &shadow_plane_state->fmtcnv_state);
+	}
+	backlight_enable(dbidev->backlight);
+ out_exit:
+	drm_dev_exit(idx);
+}
+
+static const struct drm_simple_display_pipe_funcs mhs35_pipe_funcs = {
+	.mode_valid = mipi_dbi_pipe_mode_valid,
+	.enable = mhs35_enable,
+	.disable = mipi_dbi_pipe_disable,
+	.update = mhs35_pipe_update,
+	.begin_fb_access = mipi_dbi_pipe_begin_fb_access,
+	.end_fb_access = mipi_dbi_pipe_end_fb_access,
+	.reset_plane = mipi_dbi_pipe_reset_plane,
+	.duplicate_plane_state = mipi_dbi_pipe_duplicate_plane_state,
+	.destroy_plane_state = mipi_dbi_pipe_destroy_plane_state,
+};
+
+static const struct drm_display_mode mhs35_mode = {
+	DRM_SIMPLE_MODE(480, 320, 73, 49),
+};
+
+DEFINE_DRM_GEM_DMA_FOPS(mhs35_fops);
+
+static const struct drm_driver mhs35_driver = {
+	.driver_features	= DRIVER_GEM | DRIVER_MODESET | DRIVER_ATOMIC,
+	.fops			= &mhs35_fops,
+	DRM_GEM_DMA_DRIVER_OPS_VMAP,
+	.debugfs_init		= mipi_dbi_debugfs_init,
+	.name			= "mhs35",
+	.desc			= "goodtft MHS-3.5",
+	.date			= "20260709",
+	.major			= 1,
+	.minor			= 0,
+};
+
+static const struct of_device_id mhs35_of_match[] = {
+	{ .compatible = "goodtft,mhs35" },
+	// if you want to use this you need to blacklist the default ili9486 module!
+	{ .compatible = "waveshare,rpi-lcd-35" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, mhs35_of_match);
+
+static const struct spi_device_id mhs35_id[] = {
+	{ "mhs35", 0 },
+	{ "rpi-lcd-35", 0 },
+	{ }
+};
+MODULE_DEVICE_TABLE(spi, mhs35_id);
+
+static int mhs35_probe(struct spi_device *spi)
+{
+	struct device *dev = &spi->dev;
+	struct drm_display_mode mode = mhs35_mode;
+	struct mipi_dbi_dev *dbidev;
+	struct drm_device *drm;
+	struct mipi_dbi *dbi;
+	struct gpio_desc *dc;
+	struct mhs35 *priv;
+	u32 rotation = 0;
+	int ret, n;
+
+	priv = devm_drm_dev_alloc(dev, &mhs35_driver, struct mhs35,
+				  dbidev.drm);
+	if (IS_ERR(priv))
+		return PTR_ERR(priv);
+
+	dbidev = &priv->dbidev;
+	dbi = &dbidev->dbi;
+	drm = &dbidev->drm;
+
+	priv->bounce = devm_kmalloc(dev, 480 * 320 * 2 + 4, GFP_KERNEL);
+	if (!priv->bounce)
+		return -ENOMEM;
+
+	dbi->reset = devm_gpiod_get(dev, "reset", GPIOD_OUT_HIGH);
+	if (IS_ERR(dbi->reset))
+		return dev_err_probe(dev, PTR_ERR(dbi->reset),
+				     "failed to get gpio 'reset'\n");
+
+	dc = devm_gpiod_get(dev, "dc", GPIOD_OUT_LOW);
+	if (IS_ERR(dc))
+		return dev_err_probe(dev, PTR_ERR(dc),
+				     "failed to get gpio 'dc'\n");
+
+	dbidev->backlight = devm_of_find_backlight(dev);
+	if (IS_ERR(dbidev->backlight))
+		return PTR_ERR(dbidev->backlight);
+
+	device_property_read_u32(dev, "rotation", &rotation);
+
+	n = device_property_count_u32(dev, "init");
+	if (n > 0 && n <= (int)ARRAY_SIZE(mhs35_dt_init) &&
+	    !device_property_read_u32_array(dev, "init", mhs35_dt_init, n))
+		mhs35_dt_init_len = n;
+
+	ret = mipi_dbi_spi_init(spi, dbi, dc);
+	if (ret)
+		return ret;
+
+	dbi->command = mhs35_command;
+	dbi->read_commands = NULL;
+
+	// tinydrm has a clock=1 convention that makes some compositors
+	// like wlroots start scheduling frames at insane intervals.
+	// instead we advertise a more sane clock at roughly 30hz.
+	mode.clock = 4608; // 480*320*(30/1000)
+
+	ret = mipi_dbi_dev_init(dbidev, &mhs35_pipe_funcs, &mode, rotation);
+	if (ret)
+		return ret;
+
+	drm_mode_config_reset(drm);
+
+	ret = drm_dev_register(drm, 0);
+	if (ret)
+		return ret;
+
+	spi_set_drvdata(spi, drm);
+
+	drm_fbdev_dma_setup(drm, 0);
+
+	return 0;
+}
+
+static void mhs35_remove(struct spi_device *spi)
+{
+	struct drm_device *drm = spi_get_drvdata(spi);
+
+	drm_dev_unplug(drm);
+	drm_atomic_helper_shutdown(drm);
+}
+
+static void mhs35_shutdown(struct spi_device *spi)
+{
+	drm_atomic_helper_shutdown(spi_get_drvdata(spi));
+}
+
+static struct spi_driver mhs35_spi_driver = {
+	.driver = {
+		.name = "mhs35",
+		.of_match_table = mhs35_of_match,
+	},
+	.id_table = mhs35_id,
+	.probe = mhs35_probe,
+	.remove = mhs35_remove,
+	.shutdown = mhs35_shutdown,
+};
+module_spi_driver(mhs35_spi_driver);
+
+MODULE_DESCRIPTION("DRM driver for goodtft MHS-3.5 panels");
+MODULE_AUTHOR("Mel G. <mel@rnrd.eu>");
+MODULE_LICENSE("GPL");