mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: ZiAo Wang <ziaowang2007@gmail.com>
To: dri-devel@lists.freedesktop.org
Cc: maarten.lankhorst@linux.intel.com, mripard@kernel.org,
	tzimmermann@suse.de, airlied@gmail.com, simona@ffwll.ch,
	jikos@kernel.org, bentiss@kernel.org,
	linux-input@vger.kernel.org, linux-kernel@vger.kernel.org,
	Ho Jie Feng <hjf3108@gmail.com>,
	ZiAo Wang <ziaowang2007@gmail.com>
Subject: [PATCH 3/3] drm/tiny: add driver for Valkyrie USB LCD displays
Date: Wed, 28 Oct 2026 21:25:36 +0800	[thread overview]
Message-ID: <20261028132536.375927-4-ziaowang2007@gmail.com> (raw)
In-Reply-To: <20261028132536.375927-1-ziaowang2007@gmail.com>

Valkyrie USB LCD displays rely on vendor-specific Windows software
to display images and do not expose a standard display interface,
preventing them from being used as regular displays on Linux.

Add a DRM/KMS driver developed from scratch by reverse-engineering
the vendor's software and USB protocol. The driver exposes these
devices as standard DRM display outputs with fixed native modes,
initially supporting the 320x240 and 360x960 variants.

The driver initializes and controls the displays through USB
control transfers, and converts and streams framebuffer contents
as UYVY data over a USB bulk OUT endpoint in chunks of up to
65536 bytes.

Use GEM shmem buffers and atomic modesetting to integrate with
the DRM subsystem, and provide fbdev emulation for compatibility
with legacy framebuffer applications.

Signed-off-by: ZiAo Wang <ziaowang2007@gmail.com>
---
 MAINTAINERS                         |    7 +
 drivers/gpu/drm/tiny/Kconfig        |   15 +
 drivers/gpu/drm/tiny/Makefile       |    1 +
 drivers/gpu/drm/tiny/valkyrie-lcd.c | 1413 +++++++++++++++++++++++++++
 4 files changed, 1436 insertions(+)
 create mode 100644 drivers/gpu/drm/tiny/valkyrie-lcd.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 365bec8eb7..1b91f813bb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8815,6 +8815,13 @@ S:	Supported
 T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
 F:	drivers/gpu/drm/udl/
 
+DRM DRIVER FOR VALKYRIE USB LCD DISPLAYS
+M:	ZiAo Wang <ziaowang2007@gmail.com>
+L:	dri-devel@lists.freedesktop.org
+S:	Maintained
+T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F:	drivers/gpu/drm/tiny/valkyrie-lcd.c
+
 DRM DRIVER FOR VIRTUAL KERNEL MODESETTING (VKMS)
 M:	Louis Chauvet <louis.chauvet@bootlin.com>
 R:	Haneen Mohammed <hamohammed.sa@gmail.com>
diff --git a/drivers/gpu/drm/tiny/Kconfig b/drivers/gpu/drm/tiny/Kconfig
index f0e72d4b6a..1e4be6d72c 100644
--- a/drivers/gpu/drm/tiny/Kconfig
+++ b/drivers/gpu/drm/tiny/Kconfig
@@ -98,6 +98,21 @@ config DRM_PIXPAPER
 
 	  If M is selected, the module will be built as pixpaper.ko.
 
+config DRM_VALKYRIE_LCD
+	tristate "DRM support for Valkyrie USB LCDs"
+	depends on DRM && USB && MMU
+	select DRM_CLIENT_SELECTION
+	select DRM_KMS_HELPER
+	select DRM_GEM_SHMEM_HELPER
+	help
+	  Enable support for Valkyrie USB LCD displays with USB ID 345f:9132.
+	  The driver provides DRM/KMS output for the 320x240 and 360x960
+	  display variants. It supports native UYVY and software-converted
+	  XRGB8888 framebuffers.
+
+	  To compile this driver as a module, choose M here. The module
+	  will be called valkyrie-lcd.
+
 config TINYDRM_HX8357D
 	tristate "DRM support for HX8357D display panels"
 	depends on DRM && SPI
diff --git a/drivers/gpu/drm/tiny/Makefile b/drivers/gpu/drm/tiny/Makefile
index 48d30bf615..e768abb4dc 100644
--- a/drivers/gpu/drm/tiny/Makefile
+++ b/drivers/gpu/drm/tiny/Makefile
@@ -7,6 +7,7 @@ obj-$(CONFIG_DRM_CIRRUS_QEMU)		+= cirrus-qemu.o
 obj-$(CONFIG_DRM_GM12U320)		+= gm12u320.o
 obj-$(CONFIG_DRM_PANEL_MIPI_DBI)	+= panel-mipi-dbi.o
 obj-$(CONFIG_DRM_PIXPAPER)              += pixpaper.o
+obj-$(CONFIG_DRM_VALKYRIE_LCD)		+= valkyrie-lcd.o
 obj-$(CONFIG_TINYDRM_HX8357D)		+= hx8357d.o
 obj-$(CONFIG_TINYDRM_ILI9163)		+= ili9163.o
 obj-$(CONFIG_TINYDRM_ILI9225)		+= ili9225.o
diff --git a/drivers/gpu/drm/tiny/valkyrie-lcd.c b/drivers/gpu/drm/tiny/valkyrie-lcd.c
new file mode 100644
index 0000000000..6478e92462
--- /dev/null
+++ b/drivers/gpu/drm/tiny/valkyrie-lcd.c
@@ -0,0 +1,1413 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/mutex.h>
+#include <linux/stddef.h>
+#include <linux/math.h>
+#include <linux/gfp_types.h>
+#include <linux/minmax.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+#include <linux/jiffies.h>
+#include <linux/unaligned.h>
+#include <linux/container_of.h>
+#include <linux/array_size.h>
+#include <linux/delay.h>
+#include <linux/completion.h>
+#include <linux/types.h>
+#include <linux/err.h>
+#include <linux/atomic.h>
+#include <linux/lockdep.h>
+
+#include <linux/usb.h>
+#include <linux/usb/ch9.h>
+#include <linux/hid.h>
+#include <linux/hiddev.h>
+
+#include <drm/drm_drv.h>
+#include <drm/drm_device.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_print.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_format_helper.h>
+#include <drm/drm_gem.h>
+#include <drm/drm_gem_atomic_helper.h>
+#include <drm/drm_gem_shmem_helper.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_fbdev_shmem.h>
+#include <drm/drm_modeset_helper_vtables.h>
+#include <drm/drm_vblank.h>
+#include <drm/drm_vblank_helper.h>
+#include <drm/clients/drm_client_setup.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_crtc.h>
+
+#define VK_VENDOR_ID 0x345f
+#define VK_PRODUCT_ID 0x9132
+
+#define VK_FRAME_EP_NUMBER 0x04
+#define VK_SERIAL_ADDR 0x001fe0
+
+#define VK_A6_POLL_INTERVAL_US 1000
+#define VK_A6_POLL_MAX 100
+
+#define VK_SEND_CHUNK_TIMEOUT 1000
+#define VK_SEND_BUF_SIZE 65536
+
+#define VK_FRAME_FORMAT DRM_FORMAT_UYVY
+
+#define VK_TIMEOUT_FLUSH_INTERVAL_MS 3000
+
+MODULE_AUTHOR("ZiAo Wang");
+MODULE_DESCRIPTION("Valkyrie USB LCD DRM driver");
+MODULE_LICENSE("GPL");
+
+struct vk_lcd_device_info {
+	/* Identification data */
+	u8 serial[32] __nonstring;
+	u8 label_0030[2];
+	u8 label_f000[3];
+
+	/* Panel parameters */
+	u32 width, height, fps;
+	u32 width_mm, height_mm;
+	u16 video_code;
+	u16 complex_switch_addr;
+	u8 complex_switch_mask;
+	bool complex_switch_active_high;
+	u8 frame_header[8], frame_trailer[8];
+};
+
+static const struct vk_lcd_device_info vk_lcd_devices[];
+
+struct vk_driver {
+	struct usb_interface *intf;
+	struct usb_device *dev;
+	struct usb_endpoint_descriptor *frame_epd;
+	const struct vk_lcd_device_info *device_info;
+
+	/* Serializes all USB I/O and protects io_blocking and device_state. */
+	struct mutex io_lock;
+	bool io_blocking;
+	bool device_state;
+
+	struct drm_device drm;
+	bool drm_suspended;
+	struct drm_plane plane;
+	struct drm_encoder encoder;
+	struct vk_crtc_driver *crtc;
+	struct vk_connector_driver *connector;
+};
+
+/* On success, return with io_lock held; on failure, release it. */
+static inline bool vk_io_start(struct vk_driver *drv)
+{
+	mutex_lock(&drv->io_lock);
+	if (drv->io_blocking) {
+		mutex_unlock(&drv->io_lock);
+		return false;
+	}
+	return true;
+}
+
+static inline void vk_io_end(struct vk_driver *drv)
+{
+	lockdep_assert_held(&drv->io_lock);
+	mutex_unlock(&drv->io_lock);
+}
+
+static int vk_probe(struct usb_interface *intf, const struct usb_device_id *id);
+static void vk_disconnect(struct usb_interface *intf);
+static int vk_suspend(struct usb_interface *intf, pm_message_t message);
+static int vk_resume(struct usb_interface *intf);
+static int vk_reset_resume(struct usb_interface *intf);
+static int vk_pre_reset(struct usb_interface *intf);
+static int vk_post_reset(struct usb_interface *intf);
+static const struct usb_device_id vk_usb_ids[] = {
+	{ USB_DEVICE_INTERFACE_NUMBER(VK_VENDOR_ID, VK_PRODUCT_ID, 3) },
+	{}
+};
+MODULE_DEVICE_TABLE(usb, vk_usb_ids);
+
+static struct usb_driver vk_usb_driver = {
+	.name = "valkyrie_lcd",
+	.id_table = vk_usb_ids,
+	.probe = vk_probe,
+	.disconnect = vk_disconnect,
+	.supports_autosuspend = 0,
+	.suspend = vk_suspend,
+	.resume = vk_resume,
+	.reset_resume = vk_reset_resume,
+	.pre_reset = vk_pre_reset,
+	.post_reset = vk_post_reset,
+};
+module_usb_driver(vk_usb_driver);
+
+DEFINE_DRM_GEM_FOPS(vk_drm_fops);
+static struct drm_driver vk_drm_driver = {
+	.name = "valkyrie_lcd",
+	.desc = "Valkyrie LCD DRM driver",
+	.major = 1,
+	.minor = 0,
+	.patchlevel = 0,
+	.driver_features = DRIVER_MODESET | DRIVER_GEM | DRIVER_ATOMIC,
+	DRM_GEM_SHMEM_DRIVER_OPS,
+	DRM_FBDEV_SHMEM_DRIVER_OPS,
+	.fops = &vk_drm_fops,
+};
+
+static const u32 vk_plane_formats[] = {
+	DRM_FORMAT_XRGB8888,
+	DRM_FORMAT_UYVY,
+};
+
+static const struct drm_mode_config_funcs vk_mode_config_funcs = {
+	.fb_create = drm_gem_fb_create_with_dirty,
+	.atomic_check = drm_atomic_helper_check,
+	.atomic_commit = drm_atomic_helper_commit
+};
+
+static const struct drm_plane_funcs vk_plane_funcs = {
+	.update_plane = drm_atomic_helper_update_plane,
+	.disable_plane = drm_atomic_helper_disable_plane,
+	DRM_GEM_SHADOW_PLANE_FUNCS,
+	.destroy = drm_plane_cleanup
+};
+
+static int vk_plane_atomic_check(struct drm_plane *plane,
+				 struct drm_atomic_commit *state);
+static void vk_plane_atomic_update(struct drm_plane *plane,
+				   struct drm_atomic_commit *state)
+{
+	// All work is done in vk_crtc_atomic_flush
+}
+
+static const struct drm_plane_helper_funcs vk_plane_helper_funcs = {
+	DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
+	.atomic_check = vk_plane_atomic_check,
+	.atomic_update = vk_plane_atomic_update
+};
+
+struct vk_send_chunk {
+	struct urb urb;
+	void *buf;
+	int errno;
+	struct completion flight;
+};
+
+static int vk_send_chunk_init(struct drm_device *drm,
+			      struct vk_send_chunk *chunk)
+{
+	init_completion(&chunk->flight);
+	usb_init_urb(&chunk->urb);
+	chunk->buf = drmm_kzalloc(drm, VK_SEND_BUF_SIZE, GFP_KERNEL);
+	if (!chunk->buf)
+		return -ENOMEM;
+	return 0;
+}
+
+struct vk_crtc_driver {
+	struct drm_crtc crtc;
+	struct delayed_work timeout_flush;
+
+	struct vk_send_chunk send_chunk[2];
+};
+
+static const struct drm_crtc_funcs vk_crtc_funcs = {
+	.set_config = drm_atomic_helper_set_config,
+	.page_flip = drm_atomic_helper_page_flip,
+	.atomic_create_state = drm_atomic_helper_crtc_create_state,
+	.atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
+	.atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
+	DRM_CRTC_VBLANK_TIMER_FUNCS
+};
+
+static enum drm_mode_status
+vk_crtc_mode_valid(struct drm_crtc *crtc, const struct drm_display_mode *mode);
+static int vk_crtc_atomic_check(struct drm_crtc *crtc,
+				struct drm_atomic_commit *state);
+static void vk_crtc_atomic_flush(struct drm_crtc *crtc,
+				 struct drm_atomic_commit *state);
+static void vk_crtc_atomic_enable(struct drm_crtc *crtc,
+				  struct drm_atomic_commit *state)
+{
+	drm_crtc_vblank_on(crtc);
+}
+
+static void vk_crtc_atomic_disable(struct drm_crtc *crtc,
+				   struct drm_atomic_commit *state)
+{
+	drm_crtc_vblank_off(crtc);
+}
+
+static const struct drm_crtc_helper_funcs vk_crtc_helper_funcs = {
+	.mode_valid = vk_crtc_mode_valid,
+	.atomic_check = vk_crtc_atomic_check,
+	.atomic_flush = vk_crtc_atomic_flush,
+	.atomic_enable = vk_crtc_atomic_enable,
+	.atomic_disable = vk_crtc_atomic_disable,
+};
+
+struct vk_connector_driver {
+	struct drm_connector connector;
+
+	struct usb_device *usb_dev;
+	unsigned int data_pipe;
+	const struct vk_lcd_device_info *device_info;
+};
+
+static const struct drm_connector_funcs vk_connector_funcs = {
+	.atomic_create_state = drm_atomic_helper_connector_create_state,
+	.fill_modes = drm_helper_probe_single_connector_modes,
+	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+	.atomic_destroy_state = drm_atomic_helper_connector_destroy_state
+};
+
+static int vk_connector_get_modes(struct drm_connector *connector);
+static const struct drm_connector_helper_funcs vk_connector_helper_funcs = {
+	.get_modes = vk_connector_get_modes,
+};
+
+static const struct drm_encoder_funcs vk_encoder_funcs = {
+	.destroy = drm_encoder_cleanup,
+};
+
+static int vk_device_info_find_or_verify(struct vk_driver *drv);
+static int vk_device_bootstrap(struct vk_driver *drv);
+static int vk_device_enable_pre(struct vk_driver *drv);
+static int vk_device_enable_post(struct vk_driver *drv);
+static int vk_device_disable(struct vk_driver *drv);
+static int vk_device_reset(struct vk_driver *drv);
+
+static int vk_plane_atomic_check(struct drm_plane *plane,
+				 struct drm_atomic_commit *state)
+{
+	struct drm_plane_state *new_plane_state =
+		drm_atomic_get_new_plane_state(state, plane);
+	struct drm_crtc_state *new_crtc_state;
+	int ret;
+
+	if (!new_plane_state->fb)
+		return 0;
+
+	if (!new_plane_state->crtc)
+		return -EINVAL;
+	new_crtc_state =
+		drm_atomic_get_new_crtc_state(state, new_plane_state->crtc);
+	if (!new_crtc_state)
+		return -EINVAL;
+
+	ret = drm_atomic_helper_check_plane_state(new_plane_state,
+						  new_crtc_state,
+						  DRM_PLANE_NO_SCALING,
+						  DRM_PLANE_NO_SCALING, false,
+						  false);
+	if (ret)
+		return ret;
+
+	if (new_plane_state->rotation != DRM_MODE_ROTATE_0) {
+		drm_dbg_kms(plane->dev, "Unsupported plane rotation %#x\n",
+			    new_plane_state->rotation);
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static enum drm_mode_status
+vk_crtc_mode_valid(struct drm_crtc *crtc, const struct drm_display_mode *mode)
+{
+	struct vk_driver *drv = container_of(crtc->dev, struct vk_driver, drm);
+	struct drm_display_mode preferred = {
+		DRM_MODE_INIT(drv->device_info->fps,
+			      drv->device_info->width,
+			      drv->device_info->height,
+			      drv->device_info->width_mm,
+			      drv->device_info->height_mm)
+	};
+
+	return drm_crtc_helper_mode_valid_fixed(crtc, mode, &preferred);
+}
+
+static int vk_crtc_atomic_check(struct drm_crtc *crtc,
+				struct drm_atomic_commit *state)
+{
+	struct vk_driver *drv = container_of(crtc->dev, struct vk_driver, drm);
+	struct drm_crtc_state *new_crtc_state =
+		drm_atomic_get_new_crtc_state(state, crtc);
+	struct drm_plane *plane = &drv->plane;
+	struct vk_connector_driver *connector_drv = drv->connector;
+	struct drm_plane_state *new_plane_state;
+
+	if (!new_crtc_state->enable || !new_crtc_state->active)
+		return 0;
+
+	if (new_crtc_state->plane_mask != drm_plane_mask(plane) ||
+	    new_crtc_state->connector_mask !=
+		    drm_connector_mask(&connector_drv->connector)) {
+		drm_dbg_kms(crtc->dev,
+			    "Active CRTC requires the primary plane and USB connector\n");
+		return -EINVAL;
+	}
+
+	new_plane_state = drm_atomic_get_new_plane_state(state, plane);
+	if (!new_plane_state)
+		new_plane_state = plane->state;
+	if (!new_plane_state || new_plane_state->crtc != crtc ||
+	    !new_plane_state->fb) {
+		drm_dbg_kms(crtc->dev,
+			    "Active CRTC has no framebuffer on its primary plane\n");
+		return -EINVAL;
+	}
+
+	if (new_plane_state->crtc_w != connector_drv->device_info->width ||
+	    new_plane_state->crtc_h != connector_drv->device_info->height) {
+		drm_dbg_kms(crtc->dev,
+			    "Unsupported scanout size %ux%u (expected %ux%u)\n",
+			    new_plane_state->crtc_w, new_plane_state->crtc_h,
+			    connector_drv->device_info->width,
+			    connector_drv->device_info->height);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int vk_connector_get_modes(struct drm_connector *connector)
+{
+	struct vk_connector_driver *drv =
+		container_of(connector, struct vk_connector_driver, connector);
+	struct drm_display_mode mode = {
+		DRM_MODE_INIT(drv->device_info->fps,
+			      drv->device_info->width,
+			      drv->device_info->height,
+			      drv->device_info->width_mm,
+			      drv->device_info->height_mm)
+	};
+	return drm_connector_helper_get_modes_fixed(connector, &mode);
+}
+
+static void vk_timeout_atomic_submit(struct work_struct *work)
+{
+	struct vk_crtc_driver *crtc_drv =
+		container_of(to_delayed_work(work), struct vk_crtc_driver,
+			     timeout_flush);
+	struct drm_crtc *crtc = &crtc_drv->crtc;
+	struct drm_device *drm = crtc->dev;
+	struct drm_modeset_acquire_ctx ctx;
+	struct drm_atomic_commit *state;
+	struct drm_crtc_state *crtc_state;
+	int ret = 0;
+
+	drm_modeset_acquire_init(&ctx, 0);
+
+	state = drm_atomic_commit_alloc(drm);
+	if (!state) {
+		ret = -ENOMEM;
+		goto fail_alloc;
+	}
+
+	state->acquire_ctx = &ctx;
+	state->allow_modeset = false;
+
+	do {
+		crtc_state = drm_atomic_get_crtc_state(state, crtc);
+		if (IS_ERR(crtc_state))
+			ret = PTR_ERR(crtc_state);
+		else if (crtc_state->active)
+			ret = drm_atomic_nonblocking_commit(state);
+		else
+			break;
+
+		if (ret != -EDEADLK)
+			break;
+		drm_atomic_commit_clear(state);
+		ret = drm_modeset_backoff(&ctx);
+	} while (!ret);
+	drm_atomic_commit_put(state);
+
+fail_alloc:
+	drm_modeset_drop_locks(&ctx);
+	drm_modeset_acquire_fini(&ctx);
+
+	if (ret == -EBUSY)
+		return;
+
+	if (ret)
+		drm_warn_ratelimited(crtc->dev, "Timeout refresh commit failed: %d\n", ret);
+}
+
+static int vk_scan_frame(struct vk_crtc_driver *crtc_drv,
+			 struct vk_connector_driver *connector_drv,
+			 struct drm_shadow_plane_state *plane);
+static void vk_crtc_atomic_flush(struct drm_crtc *crtc,
+				 struct drm_atomic_commit *state)
+{
+	struct vk_driver *drv = container_of(crtc->dev, struct vk_driver, drm);
+	struct vk_crtc_driver *crtc_drv = drv->crtc;
+	struct drm_crtc_state *old_crtc_state =
+		drm_atomic_get_old_crtc_state(state, crtc);
+	struct drm_crtc_state *new_crtc_state =
+		drm_atomic_get_new_crtc_state(state, crtc);
+	struct drm_plane *plane = &drv->plane;
+	struct vk_connector_driver *connector_drv = drv->connector;
+	struct drm_plane_state *new_plane_state =
+		drm_atomic_get_new_plane_state(state, plane);
+	int ret = 0, ret2;
+	int enter_idx;
+	bool new_enable = new_crtc_state->enable && new_crtc_state->active,
+	     old_enable = old_crtc_state->enable && old_crtc_state->active;
+	unsigned long irqflags;
+
+	if (!new_plane_state)
+		new_plane_state = plane->state;
+
+	if (!drm_dev_enter(crtc->dev, &enter_idx))
+		goto drm_fail;
+	if (!vk_io_start(drv)) {
+		drm_dbg_ratelimited(crtc->dev,
+				    "Skipping CRTC update while display I/O is blocked\n");
+		goto vkio_fail;
+	}
+
+	if (new_enable != old_enable)
+		drm_dbg_kms(crtc->dev, "CRTC active state: %d -> %d\n",
+			    old_enable, new_enable);
+
+	if (!new_enable) {
+		if (drv->device_state)
+			ret = vk_device_disable(drv);
+	} else {
+		bool enabling = !drv->device_state;
+
+		/* Testing found a ~10-second timeout; refresh every 3 seconds. */
+		mod_delayed_work(system_dfl_wq, &crtc_drv->timeout_flush,
+				 msecs_to_jiffies(VK_TIMEOUT_FLUSH_INTERVAL_MS));
+		if (enabling)
+			ret = vk_device_enable_pre(drv);
+		ret2 = vk_scan_frame(crtc_drv, connector_drv,
+				     to_drm_shadow_plane_state(new_plane_state));
+		ret = ret ? ret : ret2;
+		if (enabling) {
+			ret2 = vk_device_enable_post(drv);
+			ret = ret ? ret : ret2;
+		}
+	}
+	if (ret && ret != -ENODEV && ret != -ESHUTDOWN)
+		drm_err_ratelimited(crtc->dev,
+				    "Display update failed (active=%d): %d\n",
+				    new_enable, ret);
+	vk_io_end(drv);
+vkio_fail:
+	drm_dev_exit(enter_idx);
+drm_fail:
+	if (new_crtc_state->event) {
+		spin_lock_irqsave(&crtc->dev->event_lock, irqflags);
+		drm_crtc_send_vblank_event(crtc, new_crtc_state->event);
+		new_crtc_state->event = NULL;
+		spin_unlock_irqrestore(&crtc->dev->event_lock, irqflags);
+	}
+}
+
+static int vk_init_drm(struct vk_driver *drv)
+{
+	int ret;
+	struct drm_device *drm = &drv->drm;
+	struct drm_mode_config *mode_config = &drm->mode_config;
+	struct vk_connector_driver *connector;
+	struct vk_crtc_driver *crtc;
+
+	drm_dbg_driver(drm, "Initializing DRM resources\n");
+
+	ret = drmm_mode_config_init(drm);
+	if (ret)
+		return ret;
+
+	mode_config->funcs = &vk_mode_config_funcs;
+	mode_config->max_height = drv->device_info->height;
+	mode_config->min_height = drv->device_info->height;
+	mode_config->max_width = drv->device_info->width;
+	mode_config->min_width = drv->device_info->width;
+	mode_config->preferred_depth = 24;
+	mode_config->prefer_shadow = true;
+
+	ret = drm_universal_plane_init(drm, &drv->plane, BIT(0),
+				       &vk_plane_funcs, vk_plane_formats,
+				       ARRAY_SIZE(vk_plane_formats), NULL,
+				       DRM_PLANE_TYPE_PRIMARY, NULL);
+	if (ret)
+		return ret;
+	drm_plane_helper_add(&drv->plane, &vk_plane_helper_funcs);
+	drm_plane_enable_fb_damage_clips(&drv->plane);
+
+	crtc = drmm_crtc_alloc_with_planes(drm, struct vk_crtc_driver, crtc,
+					   &drv->plane, NULL, &vk_crtc_funcs,
+					   NULL);
+	if (IS_ERR(crtc))
+		return PTR_ERR(crtc);
+	drm_crtc_helper_add(&crtc->crtc, &vk_crtc_helper_funcs);
+	INIT_DELAYED_WORK(&crtc->timeout_flush, vk_timeout_atomic_submit);
+	drv->crtc = crtc;
+	for (size_t i = 0; i < ARRAY_SIZE(crtc->send_chunk); i++) {
+		ret = vk_send_chunk_init(drm, &crtc->send_chunk[i]);
+		if (ret)
+			return ret;
+	}
+
+	ret = drm_encoder_init(drm, &drv->encoder, &vk_encoder_funcs,
+			       DRM_MODE_ENCODER_NONE, NULL);
+	if (ret)
+		return ret;
+	drv->encoder.possible_crtcs = drm_crtc_mask(&crtc->crtc);
+
+	connector = drmm_kzalloc(drm, sizeof(struct vk_connector_driver),
+				 GFP_KERNEL);
+	if (!connector)
+		return -ENOMEM;
+	ret = drmm_connector_init(drm, &connector->connector,
+				  &vk_connector_funcs, DRM_MODE_CONNECTOR_USB,
+				  NULL);
+	if (ret)
+		return ret;
+	drm_connector_helper_add(&connector->connector,
+				 &vk_connector_helper_funcs);
+	connector->usb_dev = drv->dev;
+	connector->data_pipe =
+		usb_sndbulkpipe(drv->dev, drv->frame_epd->bEndpointAddress);
+	connector->device_info = drv->device_info;
+	ret = drm_connector_attach_encoder(&connector->connector,
+					   &drv->encoder);
+	if (ret)
+		return ret;
+	drv->connector = connector;
+
+	ret = drm_vblank_init(drm, 1);
+	if (ret)
+		return ret;
+
+	return drm_mode_config_create_initial_state(drm);
+}
+
+static int vk_init_usb(struct vk_driver *drv, struct usb_interface *intf)
+{
+	struct usb_host_interface *altsetting = intf->cur_altsetting;
+
+	drv->intf = intf;
+	usb_set_intfdata(intf, drv);
+
+	drv->dev = interface_to_usbdev(intf);
+
+	for (size_t i = 0; i < altsetting->desc.bNumEndpoints; i++) {
+		struct usb_endpoint_descriptor *epd =
+			&altsetting->endpoint[i].desc;
+		if (usb_endpoint_num(epd) == VK_FRAME_EP_NUMBER &&
+		    usb_endpoint_is_bulk_out(epd)) {
+			drv->frame_epd = epd;
+			break;
+		}
+	}
+	if (!drv->frame_epd) {
+		drm_err(&drv->drm, "Bulk OUT endpoint %u not found\n",
+			VK_FRAME_EP_NUMBER);
+		return -ENODEV;
+	}
+
+	drm_dbg_driver(&drv->drm,
+		       "Using bulk OUT endpoint %#x (max packet %u)\n",
+		       drv->frame_epd->bEndpointAddress,
+		       usb_endpoint_maxp(drv->frame_epd));
+
+	return 0;
+}
+
+static void vk_deinit_usb(struct vk_driver *drv)
+{
+	usb_set_intfdata(drv->intf, NULL);
+}
+
+static int vk_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+	struct vk_driver *drv = devm_drm_dev_alloc(&intf->dev, &vk_drm_driver,
+						   struct vk_driver, drm);
+	int ret;
+
+	if (IS_ERR(drv))
+		return dev_err_probe(&intf->dev, PTR_ERR(drv),
+				     "Failed to allocate DRM device\n");
+	mutex_init(&drv->io_lock);
+
+	ret = vk_init_usb(drv, intf);
+	if (ret)
+		goto fail;
+
+	mutex_lock(&drv->io_lock);
+
+	ret = vk_device_info_find_or_verify(drv);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to identify display: %d\n", ret);
+		goto fail_io;
+	}
+	drm_info(&drv->drm, "Detected %ux%u Valkyrie USB LCD (%u Hz)\n",
+		 drv->device_info->width, drv->device_info->height,
+		 drv->device_info->fps);
+
+	ret = vk_device_bootstrap(drv);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to initialize display: %d\n", ret);
+		goto fail_io;
+	}
+
+	drv->io_blocking = false;
+	mutex_unlock(&drv->io_lock);
+
+	ret = vk_init_drm(drv);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to initialize DRM resources: %d\n",
+			ret);
+		goto fail;
+	}
+	ret = drm_dev_register(&drv->drm, 0);
+	if (ret) {
+		drm_err(&drv->drm, "Failed to register DRM device: %d\n", ret);
+		goto fail;
+	}
+	drm_client_setup(&drv->drm, NULL);
+
+	return 0;
+fail_io:
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+fail:
+	vk_deinit_usb(drv);
+	return ret;
+}
+
+static void vk_disconnect(struct usb_interface *intf)
+{
+	struct vk_driver *drv = usb_get_intfdata(intf);
+	int ret;
+
+	drm_dbg_driver(&drv->drm, "Disconnecting display\n");
+
+	drm_dev_get(&drv->drm);
+	drm_dev_unplug(&drv->drm);
+
+	mutex_lock(&drv->io_lock);
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+
+	cancel_delayed_work_sync(&drv->crtc->timeout_flush);
+
+	if (drv->drm.mode_config.suspend_state) {
+		drm_atomic_commit_put(drv->drm.mode_config.suspend_state);
+		drv->drm.mode_config.suspend_state = NULL;
+	}
+
+	drm_dev_put(&drv->drm);
+
+	mutex_lock(&drv->io_lock);
+	drv->io_blocking = true;
+	/* Re-enable the display so its observed ~10-second timeout resets it. */
+	ret = vk_device_reset(drv);
+	if (ret)
+		drm_dbg_driver(&drv->drm,
+			       "Display reset during disconnect failed: %d\n",
+			       ret);
+	mutex_unlock(&drv->io_lock);
+	vk_deinit_usb(drv);
+}
+
+static int __vk_resume_device(struct usb_interface *intf, bool rebootstrap,
+			      bool reidentify)
+{
+	struct vk_driver *drv = usb_get_intfdata(intf);
+	int ret = 0;
+
+	drm_dbg_driver(&drv->drm,
+		       "Resuming display (reinitialize=%d, reidentify=%d, DRM suspended=%d)\n",
+		       rebootstrap, reidentify, drv->drm_suspended);
+
+	mutex_lock(&drv->io_lock);
+
+	if (reidentify) {
+		ret = vk_device_info_find_or_verify(drv);
+		if (ret) {
+			drm_err(&drv->drm,
+				"Failed to verify display after USB reset: %d\n",
+				ret);
+			goto fail;
+		}
+	}
+
+	if (rebootstrap) {
+		ret = vk_device_bootstrap(drv);
+		if (ret) {
+			drm_err(&drv->drm,
+				"Failed to initialize display after USB reset: %d\n",
+				ret);
+			goto fail;
+		}
+	}
+
+	drv->io_blocking = false;
+	mutex_unlock(&drv->io_lock);
+
+	if (drv->drm_suspended) {
+		ret = drm_mode_config_helper_resume(&drv->drm);
+		drv->drm_suspended = false;
+		if (ret)
+			drm_err(&drv->drm, "Failed to restore DRM state: %d\n",
+				ret);
+	} else {
+		drm_dbg_driver(&drv->drm,
+			       "No saved DRM state; scheduling an immediate refresh\n");
+		schedule_delayed_work(&drv->crtc->timeout_flush, 0);
+	}
+
+	if (!ret)
+		drm_dbg_driver(&drv->drm, "Display I/O resumed\n");
+
+	return ret;
+
+fail:
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+	return ret;
+}
+
+static int __vk_suspend_device(struct usb_interface *intf, bool force)
+{
+	struct vk_driver *drv = usb_get_intfdata(intf);
+	int ret = 0;
+
+	drm_dbg_driver(&drv->drm,
+		       "Suspending display (force=%d, DRM suspended=%d)\n",
+		       force, drv->drm_suspended);
+
+	if (!drv->drm_suspended) {
+		ret = drm_mode_config_helper_suspend(&drv->drm);
+		if (ret)
+			drm_err(&drv->drm, "Failed to suspend DRM state: %d\n",
+				ret);
+		if (ret && !force)
+			return ret;
+		if (!ret)
+			drv->drm_suspended = true;
+	}
+
+	mutex_lock(&drv->io_lock);
+	drv->io_blocking = true;
+	mutex_unlock(&drv->io_lock);
+	cancel_delayed_work_sync(&drv->crtc->timeout_flush);
+	drm_dbg_driver(&drv->drm, "Display I/O suspended\n");
+	return ret;
+}
+
+static int vk_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	return __vk_suspend_device(intf, true);
+}
+
+static int vk_resume(struct usb_interface *intf)
+{
+	return __vk_resume_device(intf, false, false);
+}
+
+static int vk_reset_resume(struct usb_interface *intf)
+{
+	return __vk_resume_device(intf, true, true);
+}
+
+static int vk_pre_reset(struct usb_interface *intf)
+{
+	return __vk_suspend_device(intf, false);
+}
+
+static int vk_post_reset(struct usb_interface *intf)
+{
+	return __vk_resume_device(intf, true, false);
+}
+
+static void
+__vk_conv_to_buf(struct vk_send_chunk *curr,
+		 struct drm_shadow_plane_state *plane_state, size_t *i,
+		 unsigned long *x, unsigned long *y,
+		 const struct vk_lcd_device_info *device_info,
+		 const struct drm_format_info *src_format_info,
+		 const struct drm_format_info *dst_format_info)
+{
+	struct drm_rect clip;
+	struct iosys_map dst;
+	unsigned int dst_pitch = device_info->width * dst_format_info->cpp[0];
+
+	while (*y < device_info->height && *i < VK_SEND_BUF_SIZE) {
+		size_t line_avail = device_info->width - *x,
+		       buf_avail = DIV_ROUND_DOWN_ULL(VK_SEND_BUF_SIZE - *i,
+						      dst_format_info->cpp[0]);
+		/* UYVY encodes pixels in pairs, so round down to an even count. */
+		size_t pixs_to_send = ALIGN_DOWN(min(line_avail, buf_avail),
+						 2);
+		size_t bytes_to_send = pixs_to_send * dst_format_info->cpp[0];
+
+		if (!bytes_to_send)
+			return;
+
+		clip.y1 = *y;
+		clip.y2 = *y + 1;
+		clip.x1 = *x;
+		clip.x2 = *x + pixs_to_send;
+		iosys_map_set_vaddr(&dst, curr->buf + *i);
+
+		switch (src_format_info->format) {
+		case DRM_FORMAT_XRGB8888:
+			drm_fb_xrgb8888_to_uyvy(&dst, &dst_pitch,
+						&plane_state->data[0],
+						plane_state->base.fb, &clip,
+						&plane_state->fmtcnv_state);
+			break;
+		case DRM_FORMAT_UYVY:
+			drm_fb_memcpy(&dst, &dst_pitch, &plane_state->data[0],
+				      plane_state->base.fb, &clip);
+			break;
+		}
+		*i += bytes_to_send;
+		if (clip.x2 != device_info->width) {
+			*x = clip.x2;
+		} else {
+			*x = 0;
+			(*y)++;
+		}
+	}
+}
+
+static void __vk_urb_finish_callback(struct urb *urb)
+{
+	struct vk_send_chunk *curr =
+		container_of(urb, struct vk_send_chunk, urb);
+	curr->errno = 0;
+	if (urb->transfer_buffer_length != urb->actual_length)
+		curr->errno = -EIO;
+	if (urb->status)
+		curr->errno = urb->status;
+	complete_all(&curr->flight);
+}
+
+static int __vk_wait_chunk_complete(struct vk_send_chunk *chunk,
+				    unsigned long timeout)
+{
+	int ret = 0;
+
+	if (!wait_for_completion_timeout(&chunk->flight, timeout)) {
+		ret = ret ? ret : -ETIMEDOUT;
+		usb_kill_urb(&chunk->urb);
+		complete_all(&chunk->flight);
+	}
+	if (chunk->errno)
+		ret = ret ? ret : chunk->errno;
+	return ret;
+}
+
+static int vk_scan_frame(struct vk_crtc_driver *crtc_drv,
+			 struct vk_connector_driver *connector_drv,
+			 struct drm_shadow_plane_state *plane_state)
+{
+	const struct vk_lcd_device_info *device_info =
+		connector_drv->device_info;
+	struct vk_send_chunk *curr = &crtc_drv->send_chunk[0],
+			     *next = &crtc_drv->send_chunk[1];
+	struct drm_device *drm = crtc_drv->crtc.dev;
+	struct drm_framebuffer *fb = plane_state->base.fb;
+	const struct drm_format_info *src_format_info = fb->format;
+	const struct drm_format_info *dst_format_info =
+		drm_get_format_info(drm, VK_FRAME_FORMAT,
+				    DRM_FORMAT_MOD_LINEAR);
+	unsigned long x = 0, y = 0;
+	const unsigned long wait_timeout =
+		msecs_to_jiffies(VK_SEND_CHUNK_TIMEOUT);
+	size_t i;
+	bool adding_header = true;
+	size_t trailer_i = 0;
+	int ret = 0;
+	int ret2 = 0;
+
+	lockdep_assert_held(&container_of(drm, struct vk_driver, drm)->io_lock);
+	curr->errno = 0;
+	next->errno = 0;
+	complete_all(&curr->flight);
+	complete_all(&next->flight);
+
+	ret = drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE);
+	if (ret) {
+		drm_dbg_ratelimited(drm,
+				    "Failed to begin CPU access to framebuffer %u: %d\n",
+				    fb->base.id, ret);
+		return ret;
+	}
+
+	while (trailer_i < ARRAY_SIZE(device_info->frame_trailer)) {
+		swap(curr, next);
+		i = 0;
+
+		if (adding_header) {
+			memcpy(curr->buf, device_info->frame_header,
+			       sizeof(device_info->frame_header));
+			i += sizeof(device_info->frame_header);
+			adding_header = false;
+		}
+		__vk_conv_to_buf(curr, plane_state, &i, &x, &y, device_info,
+				 src_format_info, dst_format_info);
+
+		if (y == device_info->height) {
+			size_t append_trailer_len =
+				min(ARRAY_SIZE(device_info->frame_trailer) -
+					    trailer_i,
+				    VK_SEND_BUF_SIZE - i);
+			memcpy(curr->buf + i,
+			       &device_info->frame_trailer[trailer_i],
+			       append_trailer_len);
+			i += append_trailer_len;
+			trailer_i += append_trailer_len;
+		}
+
+		usb_fill_bulk_urb(&curr->urb, connector_drv->usb_dev,
+				  connector_drv->data_pipe, curr->buf, i,
+				  __vk_urb_finish_callback, NULL);
+		ret = __vk_wait_chunk_complete(next, wait_timeout);
+		if (ret) {
+			drm_dbg_ratelimited(drm,
+					    "Bulk OUT transfer failed (%d/%d bytes): %d\n",
+					    next->urb.actual_length,
+					    next->urb.transfer_buffer_length, ret);
+			break;
+		}
+		reinit_completion(&curr->flight);
+		ret = usb_submit_urb(&curr->urb, GFP_KERNEL);
+		if (ret) {
+			drm_dbg_ratelimited(drm,
+					    "Failed to submit bulk OUT transfer (%zu bytes): %d\n",
+					    i, ret);
+			curr->errno = ret;
+			complete_all(&curr->flight);
+			break;
+		}
+	}
+	ret2 = __vk_wait_chunk_complete(curr, wait_timeout);
+	if (ret2)
+		drm_dbg_ratelimited(drm, "Last frame chunk failed (%d/%d bytes): %d\n",
+				    curr->urb.actual_length,
+				    curr->urb.transfer_buffer_length, ret2);
+	ret = ret ? ret : ret2;
+
+	/* The device expects a zero-length bulk transfer to terminate each frame. */
+	reinit_completion(&curr->flight);
+	usb_fill_bulk_urb(&curr->urb, connector_drv->usb_dev,
+			  connector_drv->data_pipe, NULL, 0,
+			  __vk_urb_finish_callback, NULL);
+	ret2 = usb_submit_urb(&curr->urb, GFP_KERNEL);
+	if (ret2) {
+		drm_dbg_ratelimited(drm, "Failed to submit frame terminator: %d\n", ret2);
+		curr->errno = ret2;
+		complete_all(&curr->flight);
+	}
+	ret2 = __vk_wait_chunk_complete(curr, wait_timeout);
+	if (ret2)
+		drm_dbg_ratelimited(drm, "Frame terminator failed: %d\n", ret2);
+	ret = ret ? ret : ret2;
+
+	drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
+	return ret;
+}
+
+/*
+ * Feature reports carry 8-byte commands. The helpers below encode observed
+ * register reads/writes and command-completion polling, including the
+ * big-endian address fields. The meaning of several command fields remains
+ * unknown.
+ */
+static int vk_feature_report(struct vk_driver *drv, const u8 request[8],
+			     u8 response[8])
+{
+	int ret;
+
+	lockdep_assert_held(&drv->io_lock);
+
+	ret = usb_control_msg_send(drv->dev, 0, HID_REQ_SET_REPORT,
+				   USB_DIR_OUT | USB_TYPE_CLASS |
+					   USB_RECIP_INTERFACE,
+				   HID_REPORT_TYPE_FEATURE << 8, 0, request, 8,
+				   USB_CTRL_SET_TIMEOUT, GFP_KERNEL);
+	if (ret) {
+		drm_dbg_ratelimited(&drv->drm, "SET_REPORT %8ph failed: %d\n",
+				    request, ret);
+		return ret;
+	}
+
+	if (response) {
+		ret = usb_control_msg_recv(drv->dev, 0, HID_REQ_GET_REPORT,
+					   USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
+					   HID_REPORT_TYPE_FEATURE << 8, 0, response, 8,
+					   USB_CTRL_GET_TIMEOUT, GFP_KERNEL);
+		if (ret)
+			drm_dbg_ratelimited(&drv->drm,
+					    "GET_REPORT for %8ph failed: %d\n",
+					    request, ret);
+	}
+
+	return ret;
+}
+
+static inline int vk_b5_read(struct vk_driver *drv, u16 addr, u8 *val)
+{
+	u8 buf[8] = { 0xB5, 0 };
+	u8 ans[8];
+	int ret;
+
+	put_unaligned_be16(addr, buf + 1);
+
+	ret = vk_feature_report(drv, buf, ans);
+	if (ret)
+		return ret;
+
+	if (memcmp(buf, ans, 3)) {
+		drm_dbg_ratelimited(&drv->drm,
+				    "Unexpected response reading register %#06x: %8ph\n",
+				    addr, ans);
+		return -EIO;
+	}
+
+	if (val)
+		*val = ans[3];
+	return 0;
+}
+
+static inline int vk_b6_write(struct vk_driver *drv, u16 addr, u8 val)
+{
+	u8 buf[8] = { 0xB6, 0 };
+
+	put_unaligned_be16(addr, buf + 1);
+	buf[3] = val;
+
+	return vk_feature_report(drv, buf, NULL);
+}
+
+static inline int vk_f5_read(struct vk_driver *drv, u32 addr, u8 val[8])
+{
+	u8 buf[8] = { 0xF5, 0 };
+
+	if (addr > 0xFFFFFF)
+		return -EINVAL;
+	put_unaligned_be32(addr, buf);
+	buf[0] = 0xF5;
+
+	return vk_feature_report(drv, buf, val);
+}
+
+static inline int vk_a6_command(struct vk_driver *drv, u8 cmd[8], u16 poll,
+				unsigned long poll_delay_us)
+{
+	int ret;
+
+	ret = vk_feature_report(drv, cmd, NULL);
+	if (ret)
+		return ret;
+
+	fsleep(poll_delay_us);
+
+	for (uint i = 0; i < VK_A6_POLL_MAX; i++) {
+		u8 val;
+
+		ret = vk_b5_read(drv, poll, &val);
+		if (ret)
+			return ret;
+		if (!val)
+			return 0;
+		fsleep(VK_A6_POLL_INTERVAL_US);
+	}
+
+	drm_dbg_ratelimited(&drv->drm,
+			    "Command %#04x timed out polling register %#06x\n",
+			    cmd[1], poll);
+	return -ETIMEDOUT;
+}
+
+static inline int vk_a601_set_screen(struct vk_driver *drv)
+{
+	u8 buf[8] = { 0xa6, 0x01, 0, 0, 0, 0, 0x22, 0x00 };
+
+	put_unaligned_be16(drv->device_info->width, buf + 2);
+	put_unaligned_be16(drv->device_info->height, buf + 4);
+
+	return vk_a6_command(drv, buf, 0xc555, 10 * 1000);
+}
+
+static inline int vk_a602_set_screen(struct vk_driver *drv)
+{
+	u8 buf[8] = { 0xa6, 0x02, 0 };
+
+	put_unaligned_be16(drv->device_info->video_code, buf + 2);
+	put_unaligned_be16(drv->device_info->width, buf + 4);
+	put_unaligned_be16(drv->device_info->height, buf + 6);
+
+	return vk_a6_command(drv, buf, 0xc557, 20 * 1000);
+}
+
+static inline int vk_a603_command(struct vk_driver *drv)
+{
+	u8 buf[8] = { 0xa6, 0x03, 0x03, 0 };
+
+	return vk_a6_command(drv, buf, 0xc558, 10 * 1000);
+}
+
+static inline int vk_a604_switch(struct vk_driver *drv, bool on)
+{
+	u8 buf[8] = { 0xa6, 0x04, on ? 1 : 0, 0 };
+	int ret;
+
+	ret = vk_a6_command(drv, buf, 0xc555, 10 * 1000);
+	if (ret)
+		return ret;
+
+	if (on) {
+		u8 f242;
+
+		ret = vk_b6_write(drv, 0xf24e, 0);
+		if (ret)
+			return ret;
+
+		ret = vk_b5_read(drv, 0xf242, &f242);
+		if (ret)
+			return ret;
+		if (f242 & BIT(3)) {
+			drm_dbg_ratelimited(&drv->drm,
+					    "Unexpected status %#04x in register 0xf242\n",
+					    f242);
+			return -EIO;
+		}
+	}
+
+	return 0;
+}
+
+static inline int vk_a605_switch(struct vk_driver *drv, bool on)
+{
+	u8 buf[8] = { 0xa6, 0x05, on ? 1 : 0, 0 };
+
+	return vk_a6_command(drv, buf, 0xc555, 10 * 1000);
+}
+
+static inline int vk_a607_switch(struct vk_driver *drv, bool on)
+{
+	u8 buf[8] = { 0xa6, 0x07, on ? 1 : 0, 0x02, 0 };
+	int ret;
+
+	if (on) {
+		ret = vk_b6_write(drv, 0xdeee, 1);
+		if (ret)
+			return ret;
+	}
+
+	return vk_a6_command(drv, buf, 0xc454, 100 * 1000);
+}
+
+static int vk_complex_switch(struct vk_driver *drv, bool on)
+{
+	int ret;
+	u8 val;
+
+	ret = vk_b5_read(drv, drv->device_info->complex_switch_addr, &val);
+	if (ret)
+		return ret;
+
+	if (drv->device_info->complex_switch_active_high ^ on)
+		val &= ~drv->device_info->complex_switch_mask;
+	else
+		val |= drv->device_info->complex_switch_mask;
+
+	return vk_b6_write(drv, drv->device_info->complex_switch_addr, val);
+}
+
+/*
+ * The sequence and ordering here follow the observed initialization and
+ * enable protocol. The purpose of some device-specific commands is unknown.
+ */
+static int vk_device_bootstrap(struct vk_driver *drv)
+{
+	int ret;
+
+	drm_dbg_driver(&drv->drm, "Initializing %ux%u display\n",
+		       drv->device_info->width, drv->device_info->height);
+
+	drv->device_state = false;
+
+	ret = vk_a603_command(drv);
+	if (ret)
+		return ret;
+
+	ret = vk_a607_switch(drv, true);
+	if (ret)
+		return ret;
+
+	ret = vk_a605_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_complex_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_a604_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_a603_command(drv);
+	if (ret)
+		return ret;
+
+	ret = vk_a601_set_screen(drv);
+	if (ret)
+		return ret;
+
+	return vk_a602_set_screen(drv);
+}
+
+static int vk_device_enable_pre(struct vk_driver *drv)
+{
+	drm_dbg_driver(&drv->drm, "Enabling display\n");
+	drv->device_state = true;
+	return vk_a604_switch(drv, true);
+}
+
+static int vk_device_enable_post(struct vk_driver *drv)
+{
+	int ret;
+
+	ret = vk_complex_switch(drv, true);
+	if (ret)
+		return ret;
+
+	return vk_a605_switch(drv, true);
+}
+
+static int vk_device_disable(struct vk_driver *drv)
+{
+	int ret;
+
+	drm_dbg_driver(&drv->drm, "Disabling display\n");
+
+	drv->device_state = false;
+
+	ret = vk_a605_switch(drv, false);
+	if (ret)
+		return ret;
+
+	ret = vk_complex_switch(drv, false);
+	if (ret)
+		return ret;
+
+	return vk_a604_switch(drv, false);
+}
+
+static int vk_device_reset(struct vk_driver *drv)
+{
+	int ret = 0;
+	int ret2;
+
+	/*
+	 * The device has no observed shutdown command. Enable it and let its
+	 * timeout reset it later; this function does not wait for that reset.
+	 */
+	ret = vk_device_bootstrap(drv);
+	ret2 = vk_device_enable_pre(drv);
+	ret = ret ? ret : ret2;
+	if (!ret2)
+		ret2 = vk_device_enable_post(drv);
+	return ret ? ret : ret2;
+}
+
+/* Known panel identification data and observed frame markers. */
+static const struct vk_lcd_device_info vk_lcd_devices[] = {
+	{ .serial = "\x5a\xa5\x1aMCVKA080320*0240S244613682\xff\xff\xff",
+	  .label_0030 = { 0x02, 0x05 },
+	  .label_f000 = { 0xb7, 0x16, 0x0a },
+
+	  .width = 320,
+	  .height = 240,
+	  .fps = 30,
+	  .width_mm = 39,
+	  .height_mm = 28,
+	  .video_code = 0x9400,
+	  .complex_switch_addr = 0xf507,
+	  .complex_switch_mask = BIT(1),
+	  .complex_switch_active_high = false,
+	  .frame_header = { 0xff, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0xf0 },
+	  .frame_trailer = { 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } },
+	{ .serial = "\x5a\xa5\x1aMCVKC030360*0960S252309025\xff\xff\xff",
+	  .label_0030 = { 0x02, 0x06 },
+	  .label_f000 = { 0xb7, 0x16, 0x0a },
+
+	  .width = 360,
+	  .height = 960,
+	  .fps = 30,
+	  .width_mm = 54,
+	  .height_mm = 144,
+	  .video_code = 0xa000,
+	  .complex_switch_addr = 0xf005,
+	  .complex_switch_mask = BIT(4),
+	  .complex_switch_active_high = true,
+	  .frame_header = { 0xff, 0x00, 0x00, 0x00, 0x00, 0x16, 0x83, 0xc0 },
+	  .frame_trailer = { 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } },
+};
+
+static int vk_device_info_find_or_verify(struct vk_driver *drv)
+{
+	u8 serial[32] __nonstring;
+	u8 label_0030[2];
+	u8 label_f000[3];
+	int ret;
+
+	for (size_t i = 0; i < 2; i++) {
+		ret = vk_b5_read(drv, 0x0030 + i, &label_0030[i]);
+		if (ret)
+			return ret;
+	}
+	for (size_t i = 0; i < 3; i++) {
+		ret = vk_b5_read(drv, 0xf000 + i, &label_f000[i]);
+		if (ret)
+			return ret;
+	}
+	for (size_t i = 0; i < 4; i++) {
+		ret = vk_f5_read(drv, VK_SERIAL_ADDR + (i * 8),
+				 serial + (i * 8));
+		if (ret)
+			return ret;
+	}
+
+	drm_dbg_driver(&drv->drm,
+		       "Panel ID: 0030=%2ph f000=%3ph identification=%32ph\n",
+		       label_0030, label_f000, serial);
+
+	for (size_t i = drv->device_info ? drv->device_info - vk_lcd_devices :
+					   0;
+	     i < ARRAY_SIZE(vk_lcd_devices); i++) {
+		if (memcmp(vk_lcd_devices[i].label_0030, label_0030,
+			   sizeof(label_0030)) ||
+		    memcmp(vk_lcd_devices[i].label_f000, label_f000,
+			   sizeof(label_f000)) ||
+		    memcmp(vk_lcd_devices[i].serial, serial, sizeof(serial))) {
+			if (drv->device_info) {
+				drm_dbg_driver(&drv->drm,
+					       "Panel identification changed after USB reset\n");
+				return -ENODEV;
+			}
+		} else {
+			drv->device_info = &vk_lcd_devices[i];
+			return 0;
+		}
+	}
+	drm_dbg_driver(&drv->drm, "Unsupported panel identification\n");
+	return -ENODEV;
+}
-- 
2.56.0


      parent reply	other threads:[~2026-10-28 13:26 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-28 13:25 [PATCH 0/3] Add support " ZiAo Wang
2026-10-28 13:25 ` [PATCH 1/3] drm/format-helper: Add XRGB8888 to UYVY conversion ZiAo Wang
2026-10-28 13:25 ` [PATCH 2/3] HID: quirks: Ignore Valkyrie USB LCD devices ZiAo Wang
2026-10-28 13:41   ` Jie Feng
2026-10-28 13:25 ` ZiAo Wang [this message]

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=20261028132536.375927-4-ziaowang2007@gmail.com \
    --to=ziaowang2007@gmail.com \
    --cc=airlied@gmail.com \
    --cc=bentiss@kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=hjf3108@gmail.com \
    --cc=jikos@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=maarten.lankhorst@linux.intel.com \
    --cc=mripard@kernel.org \
    --cc=simona@ffwll.ch \
    --cc=tzimmermann@suse.de \
    /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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®