summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Kbuild1
-rw-r--r--mhs35.c551
2 files changed, 552 insertions, 0 deletions
diff --git a/Kbuild b/Kbuild
new file mode 100644
index 0000000..98d99ac
--- /dev/null
+++ b/Kbuild
@@ -0,0 +1 @@
+obj-m := mhs35.o
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");