* [PATCH v2 1/4] drm/fl2000: Add USB driver with register and I2C access
2026-11-12 3:34 [PATCH v2 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-12 3:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-12 3:34 ` [PATCH v2 2/4] drm/fl2000: Add ITE IT66121 HDMI transmitter support Angel Gabriel Crispin Valdivia via B4 Relay
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-12 3:34 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Angel Gabriel Crispin Valdivia
Cc: linux-kernel, dri-devel
From: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
The Fresco Logic FL2000DX is a USB 3.0 display controller used in
inexpensive USB to VGA and USB to HDMI adapters (USB ID 1d5c:2000). The
FL2000 itself outputs parallel RGB/VGA; HDMI adapters pair it with an
ITE IT66121 HDMI transmitter that sits on the FL2000's I2C bus.
This first patch adds the USB side of the driver: it matches interface
0 and claims the other two FL2000 interfaces from its probe, selects the
altsetting that has the bulk endpoint, describes the FL2000 registers,
provides a regmap on top of the vendor control transfers that access
them, and registers an I2C adapter backed by the FL2000 I2C engine. The
engine only transfers aligned 32-bit words, so byte accesses are done as
read-modify-write of the containing word and the adapter advertises the
matching quirks.
Everything hangs off the binding of interface 0 and goes away with it;
unbinding any of the interfaces takes the others along.
The driver is based on the out-of-tree fl2000_drm driver by Artem
Mygaiev, which carries code from the Fresco Logic reference driver.
Signed-off-by: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
---
MAINTAINERS | 7 +
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/fl2000/Kconfig | 19 ++
drivers/gpu/drm/fl2000/Makefile | 8 +
drivers/gpu/drm/fl2000/fl2000.h | 89 +++++++++
drivers/gpu/drm/fl2000/fl2000_drv.c | 163 ++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_i2c.c | 179 +++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_registers.c | 315 ++++++++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_registers.h | 276 ++++++++++++++++++++++++++
10 files changed, 1058 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index c10342aa3..6e74cb478 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8324,6 +8324,13 @@ F: include/linux/aperture.h
F: include/linux/sysfb.h
F: include/video/nomodeset.h
+DRM DRIVER FOR FRESCO LOGIC FL2000 USB DISPLAY ADAPTERS
+M: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
+L: dri-devel@lists.freedesktop.org
+S: Maintained
+T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F: drivers/gpu/drm/fl2000/
+
DRM DRIVER FOR GENERIC EDP PANELS
R: Douglas Anderson <dianders@chromium.org>
F: Documentation/devicetree/bindings/display/panel/panel-edp.yaml
diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index 61111d50b..6ff093830 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -313,6 +313,7 @@ source "drivers/gpu/drm/atmel-hlcdc/Kconfig"
source "drivers/gpu/drm/bridge/Kconfig"
source "drivers/gpu/drm/etnaviv/Kconfig"
source "drivers/gpu/drm/exynos/Kconfig"
+source "drivers/gpu/drm/fl2000/Kconfig"
source "drivers/gpu/drm/fsl-dcu/Kconfig"
source "drivers/gpu/drm/gma500/Kconfig"
source "drivers/gpu/drm/gud/Kconfig"
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index 2c6bc093a..a4a8a958a 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -190,6 +190,7 @@ obj-$(CONFIG_DRM_NOUVEAU) +=nouveau/
obj-$(CONFIG_DRM_EXYNOS) +=exynos/
obj-$(CONFIG_DRM_ROCKCHIP) +=rockchip/
obj-$(CONFIG_DRM_GMA500) += gma500/
+obj-$(CONFIG_DRM_FL2000) += fl2000/
obj-$(CONFIG_DRM_UDL) += udl/
obj-$(CONFIG_DRM_AST) += ast/
obj-$(CONFIG_DRM_ARMADA) += armada/
diff --git a/drivers/gpu/drm/fl2000/Kconfig b/drivers/gpu/drm/fl2000/Kconfig
new file mode 100644
index 000000000..bf1badc3e
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/Kconfig
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config DRM_FL2000
+ tristate "Fresco Logic FL2000 USB display adapters"
+ depends on DRM && USB && MMU
+ depends on USB_ARCH_HAS_HCD
+ select DRM_BRIDGE_CONNECTOR
+ select DRM_CLIENT_SELECTION
+ select DRM_DISPLAY_HELPER
+ select DRM_GEM_SHMEM_HELPER
+ select DRM_KMS_HELPER
+ select I2C
+ select REGMAP
+ select REGMAP_I2C
+ help
+ This is a KMS driver for USB display adapters based on the
+ Fresco Logic FL2000DX USB 3.0 to VGA controller, paired with an
+ ITE IT66121 HDMI transmitter (USB ID 1d5c:2000).
+
+ If M is selected the module will be called fl2000.
diff --git a/drivers/gpu/drm/fl2000/Makefile b/drivers/gpu/drm/fl2000/Makefile
new file mode 100644
index 000000000..2b3a2f151
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+fl2000-y := \
+ fl2000_drv.o \
+ fl2000_i2c.o \
+ fl2000_registers.o
+
+obj-$(CONFIG_DRM_FL2000) += fl2000.o
diff --git a/drivers/gpu/drm/fl2000/fl2000.h b/drivers/gpu/drm/fl2000/fl2000.h
new file mode 100644
index 000000000..9b903e68c
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000.h
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#ifndef FL2000_H
+#define FL2000_H
+
+#include <linux/device.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+#include <linux/types.h>
+#include <linux/usb.h>
+
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_crtc.h>
+#include <drm/drm_damage_helper.h>
+#include <drm/drm_device.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_gem_framebuffer_helper.h>
+#include <drm/drm_gem_shmem_helper.h>
+#include <drm/drm_print.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_crtc_helper.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_encoder.h>
+#include <drm/drm_modeset_helper_vtables.h>
+
+#include "fl2000_registers.h"
+
+/* Known USB interfaces of FL2000 */
+enum fl2000_interface {
+ FL2000_USBIF_AVCONTROL = 0,
+ FL2000_USBIF_STREAMING = 1,
+ FL2000_USBIF_INTERRUPT = 2,
+ FL2000_USBIF_NUM,
+};
+
+struct fl2000_timings {
+ u32 hactive;
+ u32 htotal;
+ u32 hsync_width;
+ u32 hstart;
+ u32 vactive;
+ u32 vtotal;
+ u32 vsync_width;
+ u32 vstart;
+};
+
+struct fl2000_pll {
+ u32 prescaler;
+ u32 multiplier;
+ u32 divisor;
+ u32 function;
+};
+
+/* Polling interval and timeout in us for I2C read/write operations */
+#define I2C_RDWR_INTERVAL 200
+#define I2C_RDWR_TIMEOUT (256 * 1000)
+
+/* I2C adapter interface creation */
+struct i2c_adapter *fl2000_i2c_init(struct usb_interface *interface, struct regmap *regmap);
+int fl2000_ddc_read_block(struct i2c_adapter *adapter, u8 *buf, unsigned int block, size_t len);
+
+/* Register map creation */
+struct regmap *fl2000_regmap_init(struct device *dev, struct usb_device *usb_dev);
+void fl2000_regmap_drop_cache(struct regmap *regmap);
+
+/* Registers interface */
+int fl2000_reset(struct regmap *regmap);
+int fl2000_usb_magic(struct regmap *regmap);
+int fl2000_afe_magic(struct regmap *regmap);
+int fl2000_set_transfers(struct regmap *regmap);
+int fl2000_set_pixfmt(struct regmap *regmap, u32 bytes_pix);
+int fl2000_set_timings(struct regmap *regmap, struct fl2000_timings *timings);
+int fl2000_set_pll(struct regmap *regmap, struct fl2000_pll *pll);
+int fl2000_force_connect(struct regmap *regmap, bool force);
+bool fl2000_force_connect_active(struct regmap *regmap);
+int fl2000_enable_sink_interrupts(struct regmap *regmap);
+int fl2000_stream_interrupts(struct regmap *regmap, bool enable);
+int fl2000_read_status(struct regmap *regmap, u32 *status);
+int fl2000_i2c_dword(struct regmap *regmap, bool read, u16 addr, u8 offset, u32 *data);
+
+#endif /* FL2000_H */
diff --git a/drivers/gpu/drm/fl2000/fl2000_drv.c b/drivers/gpu/drm/fl2000/fl2000_drv.c
new file mode 100644
index 000000000..8e41de8f5
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_drv.c
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#include "fl2000.h"
+
+#define USB_VENDOR_FRESCO_LOGIC 0x1D5C
+#define USB_PRODUCT_FL2000 0x2000
+
+static struct usb_driver fl2000_driver;
+
+/*
+ * The three interfaces of the display function only work together, and the driver binds to the
+ * device through the first one: it matches interface 0 and claims the others from its probe, as
+ * other multi-interface USB drivers do. Everything else hangs off the binding of interface 0 and
+ * goes away with it, whatever the reason.
+ */
+static const struct usb_device_id fl2000_id_table[] = {
+ {
+ .match_flags = USB_DEVICE_ID_MATCH_DEVICE | USB_DEVICE_ID_MATCH_INT_CLASS |
+ USB_DEVICE_ID_MATCH_INT_NUMBER,
+ .idVendor = USB_VENDOR_FRESCO_LOGIC,
+ .idProduct = USB_PRODUCT_FL2000,
+ .bInterfaceClass = USB_CLASS_AUDIO_VIDEO,
+ .bInterfaceNumber = FL2000_USBIF_AVCONTROL,
+ },
+ {},
+};
+MODULE_DEVICE_TABLE(usb, fl2000_id_table);
+
+struct fl2000_devs {
+ struct regmap *regmap;
+ struct i2c_adapter *adapter;
+};
+
+/* Interface 0 is what the driver is probing: claim the others */
+static int fl2000_claim_interfaces(struct usb_device *usb_dev, struct fl2000_devs *devs)
+{
+ int ret;
+
+ for (int i = FL2000_USBIF_AVCONTROL + 1; i < FL2000_USBIF_NUM; i++) {
+ ret = usb_driver_claim_interface(&fl2000_driver, usb_ifnum_to_if(usb_dev, i), devs);
+ if (ret) {
+ dev_err(&usb_dev->dev, "Cannot claim interface %d (%d)\n", i, ret);
+ while (--i > FL2000_USBIF_AVCONTROL) {
+ struct usb_interface *other = usb_ifnum_to_if(usb_dev, i);
+
+ usb_set_intfdata(other, NULL);
+ usb_driver_release_interface(&fl2000_driver, other);
+ }
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+/* Let go of all interfaces but @interface, which is being unbound if it is one of them */
+static void fl2000_release_interfaces(struct usb_device *usb_dev, struct usb_interface *interface)
+{
+ for (int i = 0; i < FL2000_USBIF_NUM; i++) {
+ struct usb_interface *other = usb_ifnum_to_if(usb_dev, i);
+
+ if (other != interface)
+ usb_driver_release_interface(&fl2000_driver, other);
+ }
+}
+
+static int fl2000_probe(struct usb_interface *interface, const struct usb_device_id *usb_dev_id)
+{
+ int ret;
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+ struct fl2000_devs *devs;
+
+ if (usb_dev->speed < USB_SPEED_HIGH) {
+ dev_err(&interface->dev, "USB 1.1 is not supported\n");
+ return -ENODEV;
+ }
+
+ /* Frames go to the bulk endpoint of altsetting 1, and the chip has always been used so */
+ ret = usb_set_interface(usb_dev, FL2000_USBIF_AVCONTROL, 1);
+ if (ret) {
+ dev_err(&interface->dev, "Cannot set streaming interface for bulk transfers\n");
+ return ret;
+ }
+
+ devs = devm_kzalloc(&interface->dev, sizeof(*devs), GFP_KERNEL);
+ if (!devs)
+ return -ENOMEM;
+
+ devs->regmap = fl2000_regmap_init(&interface->dev, usb_dev);
+ if (IS_ERR(devs->regmap)) {
+ dev_err(&interface->dev, "Cannot initialize regmap (%pe)\n", devs->regmap);
+ return PTR_ERR(devs->regmap);
+ }
+
+ devs->adapter = fl2000_i2c_init(interface, devs->regmap);
+ if (IS_ERR(devs->adapter)) {
+ dev_err(&interface->dev, "Cannot initialize I2C adapter (%pe)\n", devs->adapter);
+ return PTR_ERR(devs->adapter);
+ }
+
+ ret = fl2000_claim_interfaces(usb_dev, devs);
+ if (ret)
+ return ret;
+
+ usb_set_intfdata(interface, devs);
+
+ return 0;
+}
+
+static void fl2000_disconnect(struct usb_interface *interface)
+{
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+ struct fl2000_devs *devs = usb_get_intfdata(interface);
+
+ /* This interface was only let go of, by the disconnect of another one */
+ if (!devs)
+ return;
+
+ for (int i = 0; i < FL2000_USBIF_NUM; i++)
+ usb_set_intfdata(usb_ifnum_to_if(usb_dev, i), NULL);
+
+ /* Last, as devs goes with interface 0, which may be one of these */
+ fl2000_release_interfaces(usb_dev, interface);
+}
+
+static int fl2000_suspend(struct usb_interface *interface, pm_message_t message)
+{
+ /*
+ * Nothing to save yet. Providing the callback keeps the USB core from
+ * unbinding the driver across system sleep.
+ */
+ dev_dbg(&interface->dev, "suspend\n");
+
+ return 0;
+}
+
+static int fl2000_resume(struct usb_interface *interface)
+{
+ dev_dbg(&interface->dev, "resume\n");
+
+ return 0;
+}
+
+static struct usb_driver fl2000_driver = {
+ .name = KBUILD_MODNAME,
+ .probe = fl2000_probe,
+ .disconnect = fl2000_disconnect,
+ .suspend = fl2000_suspend,
+ .resume = fl2000_resume,
+ .id_table = fl2000_id_table,
+ .supports_autosuspend = false,
+ .disable_hub_initiated_lpm = true,
+};
+
+module_usb_driver(fl2000_driver);
+
+MODULE_AUTHOR("Artem Mygaiev");
+MODULE_DESCRIPTION("FL2000 USB display driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/gpu/drm/fl2000/fl2000_i2c.c b/drivers/gpu/drm/fl2000/fl2000_i2c.c
new file mode 100644
index 000000000..97c8cf573
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_i2c.c
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2019, Artem Mygaiev
+ */
+
+#include <linux/unaligned.h>
+
+#include <drm/drm_edid.h>
+
+#include "fl2000.h"
+
+/*
+ * The I2C controller requires a mandatory 8-bit sub-address for any read/write operation. Each
+ * read or write operates on 8-bit data. Every exchange shall consist of 2 messages (sub-address +
+ * data) combined. The USB transfer always aligns the address to a 4-byte boundary.
+ */
+#define I2C_CMESSAGES_NUM 2
+#define I2C_REG_ADDR_SIZE (sizeof(u8))
+#define I2C_REG_DATA_SIZE (sizeof(u8))
+#define I2C_XFER_ADDR_MASK (~0x3ul)
+
+static inline int fl2000_i2c_read_dword(struct regmap *regmap, u16 addr, u8 offset, u32 *data)
+{
+ return fl2000_i2c_dword(regmap, true, addr, offset, data);
+}
+
+static inline int fl2000_i2c_write_dword(struct regmap *regmap, u16 addr, u8 offset, u32 *data)
+{
+ return fl2000_i2c_dword(regmap, false, addr, offset, data);
+}
+
+static int fl2000_i2c_xfer(struct i2c_adapter *adapter, struct i2c_msg *msgs, int num)
+{
+ int ret;
+ bool read;
+ u16 addr;
+ u8 idx;
+ u8 offset;
+ u32 data;
+
+ /*
+ * The adapter quirks only let through:
+ * - 2 messages of 1 byte each, first write then read
+ * - 1 message of at most 2 bytes
+ */
+ if (num == 2)
+ read = true;
+ else if (msgs[0].len == 2 && !(msgs[0].flags & I2C_M_RD))
+ read = false;
+ else
+ return -EOPNOTSUPP;
+
+ addr = msgs[0].addr;
+ offset = msgs[0].buf[0] & I2C_XFER_ADDR_MASK;
+ idx = msgs[0].buf[0] - offset;
+
+ /*
+ * The FL2000 I2C engine only transfers aligned dwords, so the offset is aligned down to a
+ * 4-byte boundary and the requested byte is picked out of (or merged into) the dword.
+ */
+ if (read) {
+ ret = fl2000_i2c_read_dword(adapter->algo_data, addr, offset, &data);
+ if (ret)
+ return ret;
+
+ msgs[1].buf[0] = data >> (idx * 8);
+ } else {
+ /*
+ * Since the FL2000 I2C engine always operates on 4-byte messages, we need to read
+ * before write in order not to corrupt the unrelated registers of the same dword.
+ */
+ ret = fl2000_i2c_read_dword(adapter->algo_data, addr, offset, &data);
+ if (ret)
+ return ret;
+
+ data &= ~(0xffU << (idx * 8));
+ data |= (u32)msgs[0].buf[1] << (idx * 8);
+
+ ret = fl2000_i2c_write_dword(adapter->algo_data, addr, offset, &data);
+ if (ret)
+ return ret;
+ }
+
+ return num;
+}
+
+/*
+ * Monitor DDC on the FL2000 bus. The bridge's own EDID command does not work behind this engine,
+ * and the VGA connector has no bridge at all, so EDID is read with the FL2000 I2C engine at address
+ * 0x50, 4 bytes per operation with an 8-bit offset, which covers EDID blocks 0 and 1.
+ */
+#define FL2000_DDC_ADDR 0x50
+
+/**
+ * fl2000_ddc_read_block() - read one EDID block through the FL2000 I2C engine
+ * @adapter: the FL2000 I2C adapter
+ * @buf: destination, @len bytes
+ * @block: EDID block number (0 or 1)
+ * @len: bytes to read, at most EDID_LENGTH
+ *
+ * The signature matches what drm_edid_read_custom() expects, with the adapter as context.
+ *
+ * Return: 0 on success, a negative error code otherwise
+ */
+int fl2000_ddc_read_block(struct i2c_adapter *adapter, u8 *buf, unsigned int block, size_t len)
+{
+ struct regmap *regmap = adapter->algo_data;
+ unsigned int start = block * EDID_LENGTH;
+ int ret = 0;
+
+ if (block > 1 || len > EDID_LENGTH)
+ return -EOPNOTSUPP;
+
+ /* One operation of the engine is a sequence of register accesses: keep others out */
+ i2c_lock_bus(adapter, I2C_LOCK_SEGMENT);
+ for (unsigned int i = 0; i < len; i += 4) {
+ u32 data;
+
+ ret = fl2000_i2c_dword(regmap, true, FL2000_DDC_ADDR, start + i, &data);
+ if (ret)
+ break;
+
+ put_unaligned_le32(data, buf + i);
+ }
+ i2c_unlock_bus(adapter, I2C_LOCK_SEGMENT);
+
+ return ret;
+}
+
+static u32 fl2000_i2c_func(struct i2c_adapter *adap)
+{
+ return I2C_FUNC_I2C;
+}
+
+static const struct i2c_algorithm fl2000_i2c_algorithm = {
+ .xfer = fl2000_i2c_xfer,
+ .functionality = fl2000_i2c_func,
+};
+
+static const struct i2c_adapter_quirks fl2000_i2c_quirks = {
+ /* Combined transfers only: an address write, then a read, on the same address */
+ .flags = I2C_AQ_COMB_WRITE_THEN_READ | I2C_AQ_NO_ZERO_LEN,
+ .max_num_msgs = I2C_CMESSAGES_NUM,
+ .max_write_len = 2 * I2C_REG_DATA_SIZE,
+ .max_read_len = I2C_REG_DATA_SIZE,
+ .max_comb_1st_msg_len = I2C_REG_ADDR_SIZE,
+ .max_comb_2nd_msg_len = I2C_REG_DATA_SIZE,
+};
+
+struct i2c_adapter *fl2000_i2c_init(struct usb_interface *interface, struct regmap *regmap)
+{
+ int ret;
+ struct i2c_adapter *adapter;
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+ u8 usb_path[32];
+
+ /* Adapter must be allocated before anything else */
+ adapter = devm_kzalloc(&interface->dev, sizeof(*adapter), GFP_KERNEL);
+ if (!adapter)
+ return ERR_PTR(-ENOMEM);
+
+ adapter->owner = THIS_MODULE;
+ adapter->algo = &fl2000_i2c_algorithm;
+ adapter->quirks = &fl2000_i2c_quirks;
+ adapter->algo_data = regmap;
+ adapter->dev.parent = &interface->dev;
+ strscpy(adapter->name, "FL2000 bridge I2C bus", sizeof(adapter->name));
+
+ ret = devm_i2c_add_adapter(&interface->dev, adapter);
+ if (ret)
+ return ERR_PTR(ret);
+
+ usb_make_path(usb_dev, usb_path, sizeof(usb_path));
+ dev_dbg(&interface->dev, "Created FL2000 bridge I2C bus %d at interface %s\n",
+ i2c_adapter_id(adapter), usb_path);
+
+ return adapter;
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_registers.c b/drivers/gpu/drm/fl2000/fl2000_registers.c
new file mode 100644
index 000000000..f1cd28434
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_registers.c
@@ -0,0 +1,315 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2019, Artem Mygaiev
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+
+#include "fl2000.h"
+
+#define CONTROL_MSG_READ 64
+#define CONTROL_MSG_WRITE 65
+
+#define FL2000_HW_RST_MDELAY 10
+
+static bool fl2000_reg_precious(struct device *dev, unsigned int reg)
+{
+ return FL2000_REG_PRECIOUS(reg);
+}
+
+static bool fl2000_reg_volatile(struct device *dev, unsigned int reg)
+{
+ return FL2000_REG_VOLATILE(reg);
+}
+
+/* Protected by internal regmap mutex */
+static int fl2000_reg_read(void *context, unsigned int reg, unsigned int *val)
+{
+ struct usb_device *usb_dev = context;
+ __le32 data;
+ int ret;
+
+ ret = usb_control_msg_recv(usb_dev, 0, CONTROL_MSG_READ, USB_DIR_IN | USB_TYPE_VENDOR, 0,
+ reg, &data, sizeof(data), USB_CTRL_GET_TIMEOUT, GFP_KERNEL);
+ if (ret)
+ return ret;
+
+ *val = le32_to_cpu(data);
+
+ return 0;
+}
+
+/* Protected by internal regmap mutex */
+static int fl2000_reg_write(void *context, unsigned int reg, unsigned int val)
+{
+ struct usb_device *usb_dev = context;
+ __le32 data = cpu_to_le32(val);
+
+ return usb_control_msg_send(usb_dev, 0, CONTROL_MSG_WRITE, USB_DIR_OUT | USB_TYPE_VENDOR, 0,
+ reg, &data, sizeof(data), USB_CTRL_SET_TIMEOUT, GFP_KERNEL);
+}
+
+/*
+ * We do not use default values as per documentation because:
+ * a) somehow they differ from real HW
+ * b) on SW reset not all of them are cleared
+ */
+static const struct regmap_config fl2000_regmap_config = {
+ .val_bits = 32,
+ .reg_bits = 16,
+ .reg_stride = 4,
+ .max_register = 0xFFFF,
+
+ .cache_type = REGCACHE_MAPLE,
+
+ .precious_reg = fl2000_reg_precious,
+ .volatile_reg = fl2000_reg_volatile,
+
+ .reg_format_endian = REGMAP_ENDIAN_BIG,
+ .val_format_endian = REGMAP_ENDIAN_BIG,
+
+ .reg_read = fl2000_reg_read,
+ .reg_write = fl2000_reg_write,
+
+ .use_single_read = true,
+ .use_single_write = true,
+};
+
+int fl2000_set_pll(struct regmap *regmap, struct fl2000_pll *pll)
+{
+ u32 val;
+
+ val = FIELD_PREP(FL2000_VGA_PLL_PRESCALER, pll->prescaler) |
+ FIELD_PREP(FL2000_VGA_PLL_MULTIPLIER, pll->multiplier) |
+ FIELD_PREP(FL2000_VGA_PLL_DIVISOR, pll->divisor) |
+ FIELD_PREP(FL2000_VGA_PLL_FUNCTION, pll->function);
+ regmap_write(regmap, FL2000_VGA_PLL_REG, val);
+
+ val = FL2000_VGA_CTRL_ACLK_FORCE_PLL_UP;
+ regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_ACLK, val, val);
+
+ return 0;
+}
+
+/*
+ * Make the chip believe a monitor is connected whatever its own sense says. The HDMI path has
+ * always run with this set, so it stays set while HDMI is driven. It must be clear while only VGA
+ * is driven: it hides the cable being unplugged, and with it clear the chip powers its DAC and PLL
+ * by itself whenever it senses a monitor.
+ */
+int fl2000_force_connect(struct regmap *regmap, bool force)
+{
+ return regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_ACLK,
+ FL2000_VGA_CTRL_ACLK_FORCE_VGA_CONNECT,
+ force ? FL2000_VGA_CTRL_ACLK_FORCE_VGA_CONNECT : 0);
+}
+
+/* Is the chip's monitor sense forced on: then it does not follow any cable */
+bool fl2000_force_connect_active(struct regmap *regmap)
+{
+ return regmap_test_bits(regmap, FL2000_VGA_CTRL_REG_ACLK,
+ FL2000_VGA_CTRL_ACLK_FORCE_VGA_CONNECT) > 0;
+}
+
+int fl2000_set_timings(struct regmap *regmap, struct fl2000_timings *timings)
+{
+ regmap_write(regmap, FL2000_VGA_HSYNC_REG1,
+ FIELD_PREP(FL2000_VGA_HSYNC1_HACTIVE, timings->hactive) |
+ FIELD_PREP(FL2000_VGA_HSYNC1_HTOTAL, timings->htotal));
+
+ regmap_write(regmap, FL2000_VGA_HSYNC_REG2,
+ FIELD_PREP(FL2000_VGA_HSYNC2_HSYNC_WIDTH, timings->hsync_width) |
+ FIELD_PREP(FL2000_VGA_HSYNC2_HSTART, timings->hstart));
+
+ regmap_write(regmap, FL2000_VGA_VSYNC_REG1,
+ FIELD_PREP(FL2000_VGA_VSYNC1_VACTIVE, timings->vactive) |
+ FIELD_PREP(FL2000_VGA_VSYNC1_VTOTAL, timings->vtotal));
+
+ regmap_write(regmap, FL2000_VGA_VSYNC_REG2,
+ FIELD_PREP(FL2000_VGA_VSYNC2_VSYNC_WIDTH, timings->vsync_width) |
+ FIELD_PREP(FL2000_VGA_VSYNC2_VSTART, timings->vstart) |
+ FIELD_PREP(FL2000_VGA_VSYNC2_START_LATENCY, timings->vstart));
+
+ return 0;
+}
+
+int fl2000_set_pixfmt(struct regmap *regmap, u32 bytes_pix)
+{
+ u32 mask, val;
+
+ mask = FL2000_VGA_CTRL_PXCLK_DAC_OUTPUT_EN | FL2000_VGA_CTRL_PXCLK_DROP_CNT |
+ FL2000_VGA_CTRL_PXCLK_VGA565_MODE | FL2000_VGA_CTRL_PXCLK_VGA332_MODE |
+ FL2000_VGA_CTRL_PXCLK_VGA555_MODE | FL2000_VGA_CTRL_PXCLK_VGA_COMPRESS |
+ FL2000_VGA_CTRL_PXCLK_CLEAR_WATERMARK;
+ val = FL2000_VGA_CTRL_PXCLK_DAC_OUTPUT_EN | FL2000_VGA_CTRL_PXCLK_CLEAR_WATERMARK;
+ if (bytes_pix == 2)
+ val |= FL2000_VGA_CTRL_PXCLK_VGA565_MODE;
+ regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_PXCLK, mask, val);
+
+ return 0;
+}
+
+int fl2000_set_transfers(struct regmap *regmap)
+{
+ u32 mask;
+
+ mask = FL2000_VGA_CTRL_ACLK_USE_PKT_PENDING | FL2000_VGA_CTRL_ACLK_USE_ZERO_TD |
+ FL2000_VGA_CTRL_ACLK_USE_ZERO_PKT_LEN;
+ regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_ACLK, mask,
+ FL2000_VGA_CTRL_ACLK_USE_ZERO_PKT_LEN);
+
+ regmap_write_bits(regmap, FL2000_VGA_ISOCH_REG, FL2000_VGA_ISOCH_MFRAME_CNT, 0);
+
+ return 0;
+}
+
+int fl2000_reset(struct regmap *regmap)
+{
+ regmap_write_bits(regmap, FL2000_RST_CTRL_REG, FL2000_RST_CTRL_SW_RESET,
+ FL2000_RST_CTRL_SW_RESET);
+
+ msleep(FL2000_HW_RST_MDELAY);
+
+ return 0;
+}
+
+int fl2000_afe_magic(struct regmap *regmap)
+{
+ regmap_write_bits(regmap, FL2000_USB_LPM_REG, FL2000_USB_LPM_MAGIC, FL2000_USB_LPM_MAGIC);
+
+ return 0;
+}
+
+int fl2000_usb_magic(struct regmap *regmap)
+{
+ u32 val;
+
+ /* Writing DONE as 0 would start an I2C operation, see fl2000_i2c_dword() */
+ val = FL2000_VGA_I2C_SC_MONITOR_DETECT | FL2000_VGA_I2C_SC_EDID_DETECT |
+ FL2000_VGA_I2C_SC_DONE;
+ regmap_write_bits(regmap, FL2000_VGA_I2C_SC_REG, val, val);
+
+ regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_3, FL2000_VGA_CTRL3_WAKEUP_CLR_EN, 0);
+
+ val = FL2000_USB_LPM_U1_REJECT | FL2000_USB_LPM_U2_REJECT;
+ regmap_write_bits(regmap, FL2000_USB_LPM_REG, val, val);
+
+ regmap_write_bits(regmap, FL2000_USB_CTRL_REG, FL2000_USB_CTRL_WAKE_NRDY, 0);
+
+ return 0;
+}
+
+/* The interrupts about monitors: they matter from the moment the device is up, mode or no mode */
+int fl2000_enable_sink_interrupts(struct regmap *regmap)
+{
+ regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_ACLK, FL2000_VGA_CTRL_ACLK_EDID_MON_INT_EN,
+ FL2000_VGA_CTRL_ACLK_EDID_MON_INT_EN);
+
+ regmap_write_bits(regmap, FL2000_VGA_CTRL2_REG_ACLK, FL2000_VGA_CTRL2_ACLK_HDMI_INT_EN,
+ FL2000_VGA_CTRL2_ACLK_HDMI_INT_EN);
+
+ return 0;
+}
+
+/* The interrupts about the frames being sent */
+int fl2000_stream_interrupts(struct regmap *regmap, bool enable)
+{
+ u32 mask, val;
+
+ val = FL2000_VGA_CTRL_ACLK_VGA_ERR_INT_EN | FL2000_VGA_CTRL_ACLK_LBUF_ERR_INT_EN;
+ mask = val | FL2000_VGA_CTRL_ACLK_FEEDBACK_INT_EN;
+
+ return regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_ACLK, mask, enable ? val : 0);
+}
+
+/*
+ * Read the status register. Its event bits clear on read, so whoever reads it owns the events it
+ * returns: callers must not drop them.
+ */
+int fl2000_read_status(struct regmap *regmap, u32 *status)
+{
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(regmap, FL2000_VGA_STATUS_REG, &val);
+ if (ret)
+ return ret;
+
+ /*
+ * LBUF issues are recoverable: write their bits back. regmap_write_bits() would read the
+ * register once more and lose any event raised since the read above.
+ */
+ if (val & (FL2000_VGA_STATUS_LBUF_OVERFLOW | FL2000_VGA_STATUS_LBUF_UNDERFLOW))
+ regmap_write(regmap, FL2000_VGA_STATUS_REG, val & ~FL2000_VGA_STATUS_SELF_CLEAR);
+
+ /* lbuf_halt and vga_error are not handled: no recovery sequence is known */
+
+ *status = val;
+
+ return 0;
+}
+
+int fl2000_i2c_dword(struct regmap *regmap, bool read, u16 addr, u8 offset, u32 *data)
+{
+ int ret;
+ unsigned int val;
+ u32 mask;
+
+ if (!read) {
+ ret = regmap_write(regmap, FL2000_VGA_I2C_WR_REG, *data);
+ if (ret)
+ return -EIO;
+ }
+
+ /*
+ * The detection bits always read back as 0, so they need to be set again on every write.
+ * It is not clear whether the monitor detection circuit is needed at all for the HDMI use
+ * case.
+ */
+ mask = FL2000_VGA_I2C_SC_MONITOR_DETECT | FL2000_VGA_I2C_SC_EDID_DETECT |
+ FL2000_VGA_I2C_SC_STATUS | FL2000_VGA_I2C_SC_ADDR | FL2000_VGA_I2C_SC_CMD |
+ FL2000_VGA_I2C_SC_OFFSET | FL2000_VGA_I2C_SC_DONE;
+ val = FL2000_VGA_I2C_SC_MONITOR_DETECT | FL2000_VGA_I2C_SC_EDID_DETECT |
+ FIELD_PREP(FL2000_VGA_I2C_SC_ADDR, addr) |
+ FIELD_PREP(FL2000_VGA_I2C_SC_CMD, read) |
+ FIELD_PREP(FL2000_VGA_I2C_SC_OFFSET, offset);
+
+ ret = regmap_write_bits(regmap, FL2000_VGA_I2C_SC_REG, mask, val);
+ if (ret)
+ return -EIO;
+
+ ret = regmap_read_poll_timeout(regmap, FL2000_VGA_I2C_SC_REG, val,
+ val & FL2000_VGA_I2C_SC_DONE, I2C_RDWR_INTERVAL,
+ I2C_RDWR_TIMEOUT);
+ /*
+ * This shouldn't normally happen: there's internal 256ms HW timeout on I2C operations and
+ * USB must be always available so no I/O errors. But if it happens we are probably facing
+ * an irreversible HW issue.
+ */
+ if (ret || FIELD_GET(FL2000_VGA_I2C_SC_STATUS, val))
+ return -EIO;
+
+ if (read) {
+ ret = regmap_read(regmap, FL2000_VGA_I2C_RD_REG, data);
+ if (ret)
+ return -EIO;
+ }
+
+ return 0;
+}
+
+/*
+ * The chip may have lost power (system sleep): forget the cached registers, so that the
+ * read-modify-write updates that follow read the chip instead of skipping writes it still needs.
+ */
+void fl2000_regmap_drop_cache(struct regmap *regmap)
+{
+ regcache_drop_region(regmap, 0, fl2000_regmap_config.max_register);
+}
+
+struct regmap *fl2000_regmap_init(struct device *dev, struct usb_device *usb_dev)
+{
+ return devm_regmap_init(dev, NULL, usb_dev, &fl2000_regmap_config);
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_registers.h b/drivers/gpu/drm/fl2000/fl2000_registers.h
new file mode 100644
index 000000000..f17431e38
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_registers.h
@@ -0,0 +1,276 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Based on the official registers description from Fresco Logic
+ *
+ * (C) Copyright 2019, Artem Mygaiev
+ */
+
+#ifndef FL2000_REGISTERS_H
+#define FL2000_REGISTERS_H
+
+#include <linux/bits.h>
+#include <linux/types.h>
+
+/* #### USB Control Registers Bank #### */
+/* Some unknown & undocumented FL2000 USB settings */
+
+#define FL2000_USB_CONTROL_OFFSET 0x0000
+
+#define FL2000_USB_LPM_REG (FL2000_USB_CONTROL_OFFSET + 0x70)
+#define FL2000_USB_LPM_MAGIC BIT(13)
+#define FL2000_USB_LPM_U2_REJECT BIT(19)
+#define FL2000_USB_LPM_U1_REJECT BIT(20)
+
+#define FL2000_USB_CTRL_REG (FL2000_USB_CONTROL_OFFSET + 0x78)
+#define FL2000_USB_CTRL_WAKE_NRDY BIT(17)
+
+/* #### VGA Control Registers Bank #### */
+/* Taken from the 'FL200DX Memory Mapped Address Space Registers' spec */
+
+#define FL2000_VGA_CONTROL_OFFSET 0x8000
+
+#define FL2000_VGA_STATUS_REG (FL2000_VGA_CONTROL_OFFSET + 0x00)
+#define FL2000_VGA_STATUS_VGA_STATUS BIT(0)
+#define FL2000_VGA_STATUS_VGA_ERROR BIT(1) /* read self clear */
+#define FL2000_VGA_STATUS_LBUF_HALT BIT(2)
+#define FL2000_VGA_STATUS_ISO_ACK BIT(3) /* read self clear */
+#define FL2000_VGA_STATUS_TD_DROP BIT(4) /* read self clear */
+#define FL2000_VGA_STATUS_IRQ_PENDING BIT(5) /* read self clear */
+#define FL2000_VGA_STATUS_PLL_STATUS BIT(6)
+#define FL2000_VGA_STATUS_DAC_STATUS BIT(7)
+#define FL2000_VGA_STATUS_LBUF_OVERFLOW BIT(8)
+#define FL2000_VGA_STATUS_LBUF_UNDERFLOW BIT(9)
+#define FL2000_VGA_STATUS_FRAME_CNT GENMASK(25, 10)
+#define FL2000_VGA_STATUS_HDMI_EVENT BIT(26) /* read self clear */
+#define FL2000_VGA_STATUS_HDMI_STATUS BIT(27)
+#define FL2000_VGA_STATUS_EDID_STATUS BIT(28)
+#define FL2000_VGA_STATUS_MONITOR_STATUS BIT(29)
+#define FL2000_VGA_STATUS_MONITOR_EVENT BIT(30) /* read self clear */
+#define FL2000_VGA_STATUS_EDID_EVENT BIT(31) /* read self clear */
+#define FL2000_VGA_STATUS_SELF_CLEAR \
+ (FL2000_VGA_STATUS_VGA_ERROR | FL2000_VGA_STATUS_ISO_ACK | FL2000_VGA_STATUS_TD_DROP | \
+ FL2000_VGA_STATUS_IRQ_PENDING | FL2000_VGA_STATUS_HDMI_EVENT | \
+ FL2000_VGA_STATUS_MONITOR_EVENT | FL2000_VGA_STATUS_EDID_EVENT)
+
+#define FL2000_VGA_CTRL_REG_PXCLK (FL2000_VGA_CONTROL_OFFSET + 0x04)
+#define FL2000_VGA_CTRL_PXCLK_CLEAR_WATERMARK BIT(0)
+#define FL2000_VGA_CTRL_PXCLK_FRAME_SYNC BIT(1)
+#define FL2000_VGA_CTRL_PXCLK_HSYNC_POLARITY BIT(2)
+#define FL2000_VGA_CTRL_PXCLK_VSYNC_POLARITY BIT(3)
+#define FL2000_VGA_CTRL_PXCLK_DE_POLARITY BIT(4)
+#define FL2000_VGA_CTRL_PXCLK_MIRROR_MODE BIT(5)
+#define FL2000_VGA_CTRL_PXCLK_VGA565_MODE BIT(6)
+#define FL2000_VGA_CTRL_PXCLK_DAC_OUTPUT_EN BIT(7)
+#define FL2000_VGA_CTRL_PXCLK_VGA_TIMING_EN BIT(8)
+#define FL2000_VGA_CTRL_PXCLK_USE_NEW_PKT_RETRY BIT(9)
+#define FL2000_VGA_CTRL_PXCLK_REF_SELECT BIT(10)
+#define FL2000_VGA_CTRL_PXCLK_DAC_PX_CLK_INVERT BIT(11)
+#define FL2000_VGA_CTRL_PXCLK_CLEAR_LBUF_STATUS BIT(12)
+#define FL2000_VGA_CTRL_PXCLK_DROP_CNT BIT(13)
+#define FL2000_VGA_CTRL_PXCLK_USE_VDI_ITP_CNT BIT(14)
+#define FL2000_VGA_CTRL_PXCLK_VGA_COMPRESS BIT(24)
+#define FL2000_VGA_CTRL_PXCLK_VGA332_MODE BIT(25)
+#define FL2000_VGA_CTRL_PXCLK_VGA_COLOR_PALETTE_EN BIT(26)
+#define FL2000_VGA_CTRL_PXCLK_VGA_FIRST_BT_ENC_EN BIT(27)
+#define FL2000_VGA_CTRL_PXCLK_CLEAR_125US_CNT BIT(28)
+#define FL2000_VGA_CTRL_PXCLK_DISABLE_HALT BIT(29)
+#define FL2000_VGA_CTRL_PXCLK_FORCE_DE_EN BIT(30)
+#define FL2000_VGA_CTRL_PXCLK_VGA555_MODE BIT(31)
+
+#define FL2000_VGA_HSYNC_REG1 (FL2000_VGA_CONTROL_OFFSET + 0x08)
+#define FL2000_VGA_HSYNC1_HTOTAL GENMASK(11, 0)
+#define FL2000_VGA_HSYNC1_HACTIVE GENMASK(27, 16)
+
+#define FL2000_VGA_HSYNC_REG2 (FL2000_VGA_CONTROL_OFFSET + 0x0C)
+#define FL2000_VGA_HSYNC2_HSTART GENMASK(11, 0)
+#define FL2000_VGA_HSYNC2_HSYNC_WIDTH GENMASK(23, 16)
+
+#define FL2000_VGA_VSYNC_REG1 (FL2000_VGA_CONTROL_OFFSET + 0x10)
+#define FL2000_VGA_VSYNC1_VTOTAL GENMASK(11, 0)
+#define FL2000_VGA_VSYNC1_VACTIVE GENMASK(27, 16)
+
+#define FL2000_VGA_VSYNC_REG2 (FL2000_VGA_CONTROL_OFFSET + 0x14)
+#define FL2000_VGA_VSYNC2_VSTART GENMASK(11, 0)
+#define FL2000_VGA_VSYNC2_VSYNC_WIDTH GENMASK(18, 16)
+#define FL2000_VGA_VSYNC2_START_LATENCY GENMASK(29, 20)
+#define FL2000_VGA_VSYNC2_BUF_ERROR_EN BIT(31)
+
+#define FL2000_VGA_TEST_REG (FL2000_VGA_CONTROL_OFFSET + 0x18)
+#define FL2000_VGA_ISOCH_REG (FL2000_VGA_CONTROL_OFFSET + 0x1C)
+#define FL2000_VGA_ISOCH_START_MFRAME_CNT GENMASK(13, 0)
+#define FL2000_VGA_ISOCH_USE_MFRAME_MATCH BIT(14)
+#define FL2000_VGA_ISOCH_USE_ZERO_LEN_FRAME BIT(15)
+#define FL2000_VGA_ISOCH_MFRAME_CNT GENMASK(29, 16)
+#define FL2000_VGA_ISOCH_MFRAME_CNT_UPDATE BIT(30)
+
+#define FL2000_VGA_I2C_SC_REG (FL2000_VGA_CONTROL_OFFSET + 0x20)
+#define FL2000_VGA_I2C_SC_ADDR GENMASK(6, 0)
+#define FL2000_VGA_I2C_SC_CMD BIT(7)
+#define FL2000_VGA_I2C_SC_OFFSET GENMASK(15, 8)
+#define FL2000_VGA_I2C_SC_VGA_STATUS GENMASK(23, 16)
+#define FL2000_VGA_I2C_SC_STATUS GENMASK(27, 24)
+#define FL2000_VGA_I2C_SC_MONITOR_DETECT BIT(28)
+#define FL2000_VGA_I2C_SC_READY BIT(29)
+#define FL2000_VGA_I2C_SC_EDID_DETECT BIT(30)
+#define FL2000_VGA_I2C_SC_DONE BIT(31)
+
+#define FL2000_VGA_I2C_RD_REG (FL2000_VGA_CONTROL_OFFSET + 0x24)
+#define FL2000_VGA_I2C_WR_REG (FL2000_VGA_CONTROL_OFFSET + 0x28)
+
+#define FL2000_VGA_PLL_REG (FL2000_VGA_CONTROL_OFFSET + 0x2C)
+#define FL2000_VGA_PLL_DIVISOR GENMASK(7, 0)
+#define FL2000_VGA_PLL_PRESCALER GENMASK(9, 8)
+#define FL2000_VGA_PLL_FUNCTION GENMASK(14, 13)
+#define FL2000_VGA_PLL_MULTIPLIER GENMASK(23, 16)
+#define FL2000_VGA_PLL_TEST_IO BIT(24)
+#define FL2000_VGA_PLL_CFG_DAC_PWRDOWN BIT(25)
+#define FL2000_VGA_PLL_FORCE_DAC_PWRUP BIT(26)
+
+#define FL2000_VGA_LBUF_REG (FL2000_VGA_CONTROL_OFFSET + 0x30)
+#define FL2000_VGA_LBUF_WATERMARK_ASSERT_RDY GENMASK(14, 0)
+
+#define FL2000_VGA_HI_MARK (FL2000_VGA_CONTROL_OFFSET + 0x34)
+#define FL2000_VGA_HI_MARK_HIGH_WATERMARK GENMASK(16, 0)
+
+#define FL2000_VGA_LO_MARK (FL2000_VGA_CONTROL_OFFSET + 0x38)
+#define FL2000_VGA_LO_MARK_LOW_WATERMARK GENMASK(16, 0)
+
+#define FL2000_VGA_CTRL_REG_ACLK (FL2000_VGA_CONTROL_OFFSET + 0x3C)
+#define FL2000_VGA_CTRL_ACLK_CFG_TIMING_RESET_N BIT(0)
+#define FL2000_VGA_CTRL_ACLK_PLH_BLOCK_EN BIT(1)
+#define FL2000_VGA_CTRL_ACLK_EDID_MON_INT_EN BIT(2)
+#define FL2000_VGA_CTRL_ACLK_EXT_MON_INT_EN BIT(3)
+#define FL2000_VGA_CTRL_ACLK_VGA_STATUS_SELF_CLEAR BIT(4)
+#define FL2000_VGA_CTRL_ACLK_PLL_LOCK_TIME GENMASK(9, 5)
+#define FL2000_VGA_CTRL_ACLK_PLL_FAST_TIMEOUT_EN BIT(10)
+#define FL2000_VGA_CTRL_ACLK_PPE_BLOCK_EM BIT(11)
+#define FL2000_VGA_CTRL_ACLK_PLL_TIMER_EN BIT(12)
+#define FL2000_VGA_CTRL_ACLK_FEEDBACK_INT_EN BIT(13)
+#define FL2000_VGA_CTRL_ACLK_CLR_125US_COUNTER BIT(14)
+#define FL2000_VGA_CTRL_ACLK_CCS_PD_DIS BIT(15)
+#define FL2000_VGA_CTRL_ACLK_STANDBY_EN BIT(16)
+#define FL2000_VGA_CTRL_ACLK_FORCE_LOOPBACK BIT(17)
+#define FL2000_VGA_CTRL_ACLK_LBUF_DROP_FRAME_EN BIT(18)
+#define FL2000_VGA_CTRL_ACLK_LBUF_VDE_RST_EN BIT(19)
+#define FL2000_VGA_CTRL_ACLK_LBUF_SW_RST BIT(20)
+#define FL2000_VGA_CTRL_ACLK_LBUF_ERR_INT_EN BIT(21)
+#define FL2000_VGA_CTRL_ACLK_BIAC_EN BIT(22)
+#define FL2000_VGA_CTRL_ACLK_PXCLK_IN_EN BIT(23)
+#define FL2000_VGA_CTRL_ACLK_VGA_ERR_INT_EN BIT(24)
+#define FL2000_VGA_CTRL_ACLK_FORCE_VGA_CONNECT BIT(25)
+#define FL2000_VGA_CTRL_ACLK_FORCE_PLL_UP BIT(26)
+#define FL2000_VGA_CTRL_ACLK_USE_ZERO_TD BIT(27)
+#define FL2000_VGA_CTRL_ACLK_USE_ZERO_PKT_LEN BIT(28)
+#define FL2000_VGA_CTRL_ACLK_USE_PKT_PENDING BIT(29)
+#define FL2000_VGA_CTRL_ACLK_PLL_DAC_PD_USBP3_EN BIT(30)
+#define FL2000_VGA_CTRL_ACLK_PLL_DAC_PD_NOVGA_EN BIT(31)
+
+#define FL2000_VGA_PXCLK_CNT_REG (FL2000_VGA_CONTROL_OFFSET + 0x40)
+#define FL2000_VGA_PXCLK_CNT_PIX_CLOCK_COUNT GENMASK(27, 0)
+
+#define FL2000_VGA_VCNT_REG (FL2000_VGA_CONTROL_OFFSET + 0x44)
+#define FL2000_VGA_VCNT_MAX_ACLK_COUNT GENMASK(14, 0)
+#define FL2000_VGA_VCNT_MAX_ACLK_COUNT_HIT BIT(15)
+#define FL2000_VGA_VCNT_MAX_LBUF_ACCUMULATE GENMASK(31, 16)
+
+#define FL2000_RST_CTRL_REG (FL2000_VGA_CONTROL_OFFSET + 0x48)
+#define FL2000_RST_CTRL_DIS_HOT_RST2_PORT BIT(0)
+#define FL2000_RST_CTRL_DIS_WARM_RST2_PORT BIT(1)
+#define FL2000_RST_CTRL_DIS_HOT_RESET_PIPE BIT(2)
+#define FL2000_RST_CTRL_DIS_WARM_RESET_PIPE BIT(3)
+#define FL2000_RST_CTRL_DIS_HOT_RESET_PIX BIT(4)
+#define FL2000_RST_CTRL_DIS_WARM_RESET_PIX BIT(5)
+#define FL2000_RST_CTRL_DIS_USB2_RESET_PIX BIT(6)
+#define FL2000_RST_CTRL_DIS_PLL_RESET_PIX BIT(7)
+#define FL2000_RST_CTRL_DIS_SW_RESET_PIX BIT(8)
+#define FL2000_RST_CTRL_DIS_USB2_RESET_BUF BIT(9)
+#define FL2000_RST_CTRL_DIS_SW_RESET_BUF BIT(10)
+#define FL2000_RST_CTRL_DIS_LBUF_RESET_PIX BIT(11)
+#define FL2000_RST_CTRL_DIS_HOT_RESET_PORT BIT(12)
+#define FL2000_RST_CTRL_DIS_WARM_RESET_PORT BIT(13)
+#define FL2000_RST_CTRL_SET_SLOW_CLK_PREDFT BIT(14)
+#define FL2000_RST_CTRL_SW_RESET BIT(15)
+#define FL2000_RST_CTRL_FRAME_FIRST_ITP_WL GENMASK(31, 16)
+
+#define FL2000_BIAC_CTRL1_REG (FL2000_VGA_CONTROL_OFFSET + 0x4C)
+#define FL2000_BIAC_CTRL1_CTRL_LO8 GENMASK(7, 0)
+#define FL2000_BIAC_CTRL1_FRAME_MULT GENMASK(15, 8)
+#define FL2000_BIAC_CTRL1_125US_MULT GENMASK(31, 16)
+
+#define FL2000_BIAC_CTRL2_REG (FL2000_VGA_CONTROL_OFFSET + 0x50)
+#define FL2000_BIAC_CTRL2_CTRL_HI16 GENMASK(15, 0)
+
+#define FL2000_BIAC_STATUS_REG (FL2000_VGA_CONTROL_OFFSET + 0x54)
+#define FL2000_BIAC_STATUS_CURRENT_STATUS GENMASK(15, 0)
+#define FL2000_BIAC_STATUS_CURRENT_VALUE GENMASK(31, 16)
+
+/* undefined (FL2000_VGA_CONTROL_OFFSET + 0x58) */
+
+#define FL2000_VGA_PLT_REG_PXCLK (FL2000_VGA_CONTROL_OFFSET + 0x5C)
+#define FL2000_VGA_PLT_PXCLK_PALETTE_RAM_WR_DATA GENMASK(23, 0)
+
+#define FL2000_VGA_PLT_RADDR_REG_PXCLK (FL2000_VGA_CONTROL_OFFSET + 0x60)
+#define FL2000_VGA_PLT_RADDR_PALETTE_RAM_RD_ADDR GENMASK(7, 0)
+#define FL2000_VGA_PLT_RADDR_LAST_FRAME_LBUF_WATERMARK GENMASK(23, 8)
+
+#define FL2000_VGA_CTRL2_REG_ACLK (FL2000_VGA_CONTROL_OFFSET + 0x64)
+#define FL2000_VGA_CTRL2_ACLK_PLL_POWERDOWN_DETECT_EN BIT(0)
+#define FL2000_VGA_CTRL2_ACLK_MSTOR_BLKSIZE_PTR_WIDTH GENMASK(3, 1)
+#define FL2000_VGA_CTRL2_ACLK_MSTOR_BLK_COUNT GENMASK(15, 4)
+#define FL2000_VGA_CTRL2_ACLK_SPI_WR_EN BIT(16)
+#define FL2000_VGA_CTRL2_ACLK_DETECT_PINS_DEBOUNCE_EN BIT(17)
+#define FL2000_VGA_CTRL2_ACLK_HDMI_INT_EN BIT(18)
+#define FL2000_VGA_CTRL2_ACLK_HDMI_INT_ACTIVE_HIGH BIT(19)
+#define FL2000_VGA_CTRL2_ACLK_SPI_EN BIT(20)
+#define FL2000_VGA_CTRL2_ACLK_SW_PROD_REV GENMASK(30, 23)
+
+/* undefined (FL2000_VGA_CONTROL_OFFSET + 0x68) */
+/* undefined (FL2000_VGA_CONTROL_OFFSET + 0x6C) */
+
+#define FL2000_TEST_CNTL_REG1 (FL2000_VGA_CONTROL_OFFSET + 0x70)
+#define FL2000_TEST_CNTL_REG2 (FL2000_VGA_CONTROL_OFFSET + 0x74)
+#define FL2000_TEST_CNTL_REG3 (FL2000_VGA_CONTROL_OFFSET + 0x78)
+#define FL2000_TEST_STAT1 (FL2000_VGA_CONTROL_OFFSET + 0x7C)
+#define FL2000_TEST_STAT2 (FL2000_VGA_CONTROL_OFFSET + 0x80)
+#define FL2000_TEST_STAT3 (FL2000_VGA_CONTROL_OFFSET + 0x84)
+
+#define FL2000_VGA_CTRL_REG_3 (FL2000_VGA_CONTROL_OFFSET + 0x88)
+#define FL2000_VGA_CTRL3_WAKEUP_CLR_EN BIT(10)
+
+/* undefined (FL2000_VGA_CONTROL_OFFSET + 0x8C) */
+
+/*
+ * List of registers that shall not be read automatically (e.g. for cache update) due to presence
+ * of self-clear bits
+ */
+static inline bool FL2000_REG_PRECIOUS(u32 reg)
+{
+ return reg == FL2000_VGA_STATUS_REG;
+}
+
+/* List of volatile registers that shall not be cached */
+static inline bool FL2000_REG_VOLATILE(u32 reg)
+{
+ switch (reg) {
+ case FL2000_VGA_STATUS_REG:
+ case FL2000_VGA_CTRL_REG_PXCLK:
+ case FL2000_VGA_ISOCH_REG:
+ case FL2000_VGA_I2C_SC_REG:
+ case FL2000_VGA_I2C_RD_REG:
+ case FL2000_VGA_PXCLK_CNT_REG:
+ case FL2000_VGA_VCNT_REG:
+ case FL2000_RST_CTRL_REG:
+ case FL2000_BIAC_STATUS_REG:
+ case FL2000_VGA_PLT_RADDR_REG_PXCLK:
+ case FL2000_TEST_CNTL_REG1:
+ case FL2000_TEST_CNTL_REG2:
+ case FL2000_TEST_STAT1:
+ case FL2000_TEST_STAT2:
+ case FL2000_TEST_STAT3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#endif /* FL2000_REGISTERS_H */
--
2.56.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH v2 2/4] drm/fl2000: Add ITE IT66121 HDMI transmitter support
2026-11-12 3:34 [PATCH v2 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-12 3:34 ` [PATCH v2 1/4] drm/fl2000: Add USB driver with register and I2C access Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-12 3:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-12 3:34 ` [PATCH v2 3/4] drm/fl2000: Add frame streaming Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-12 3:34 ` [PATCH v2 4/4] drm/fl2000: Add KMS display pipeline Angel Gabriel Crispin Valdivia via B4 Relay
3 siblings, 0 replies; 5+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-12 3:34 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Angel Gabriel Crispin Valdivia
Cc: linux-kernel, dri-devel
From: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
Probe the IT66121 on the FL2000 I2C bus and expose it as a DRM bridge.
It attaches with DRM_BRIDGE_ATTACH_NO_CONNECTOR and reports the hot plug
line and the EDID (DRM_BRIDGE_OP_DETECT and DRM_BRIDGE_OP_EDID), so the
connector is the display driver's. The transmitter (input bus, AFE, AVI
infoframe, HDMI/DVI mode) is programmed in atomic_enable, where the
connector that tells HDMI from DVI is known. The EDID is kept while the
monitor stays, since reading it over USB takes about a second.
The EDID is read at address 0x50 with the FL2000 I2C engine. On the
tested adapter, as on the Fresco Logic reference design, the monitor's
DDC lines are wired to the FL2000 I2C bus, and the IT66121 DDC engine
never completes. It could not work behind this I2C engine anyway, which
transfers aligned dwords: every read of the DDC status register also
pops a byte from the DDC FIFO, which is in the same dword. The HDCP
engine holds that bus after a chip reset, so the driver releases it
once before reading.
The bridge is created and destroyed with the binding of the USB device.
It is guarded with drm_bridge_enter(), since a DRM device that userspace
keeps open after an unplug still has it in its encoder's chain.
Signed-off-by: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
---
drivers/gpu/drm/fl2000/Makefile | 1 +
drivers/gpu/drm/fl2000/fl2000_drv.c | 19 +-
drivers/gpu/drm/fl2000/fl2000_it66121.c | 799 +++++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_it66121.h | 18 +
drivers/gpu/drm/fl2000/fl2000_it66121_regs.h | 284 ++++++++++
5 files changed, 1120 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/fl2000/Makefile b/drivers/gpu/drm/fl2000/Makefile
index 2b3a2f151..f64057634 100644
--- a/drivers/gpu/drm/fl2000/Makefile
+++ b/drivers/gpu/drm/fl2000/Makefile
@@ -3,6 +3,7 @@
fl2000-y := \
fl2000_drv.o \
fl2000_i2c.o \
+ fl2000_it66121.o \
fl2000_registers.o
obj-$(CONFIG_DRM_FL2000) += fl2000.o
diff --git a/drivers/gpu/drm/fl2000/fl2000_drv.c b/drivers/gpu/drm/fl2000/fl2000_drv.c
index 8e41de8f5..31d153553 100644
--- a/drivers/gpu/drm/fl2000/fl2000_drv.c
+++ b/drivers/gpu/drm/fl2000/fl2000_drv.c
@@ -5,6 +5,7 @@
*/
#include "fl2000.h"
+#include "fl2000_it66121.h"
#define USB_VENDOR_FRESCO_LOGIC 0x1D5C
#define USB_PRODUCT_FL2000 0x2000
@@ -33,6 +34,7 @@ MODULE_DEVICE_TABLE(usb, fl2000_id_table);
struct fl2000_devs {
struct regmap *regmap;
struct i2c_adapter *adapter;
+ struct drm_bridge *bridge;
};
/* Interface 0 is what the driver is probing: claim the others */
@@ -102,13 +104,26 @@ static int fl2000_probe(struct usb_interface *interface, const struct usb_device
return PTR_ERR(devs->adapter);
}
+ /* The IT66121 is looked for with the I2C engine, ready as for reading the EDID */
+ fl2000_reset(devs->regmap);
+ fl2000_usb_magic(devs->regmap);
+
+ devs->bridge = it66121_create(devs->adapter);
+ if (IS_ERR(devs->bridge))
+ return PTR_ERR(devs->bridge);
+
ret = fl2000_claim_interfaces(usb_dev, devs);
if (ret)
- return ret;
+ goto err_destroy_bridge;
usb_set_intfdata(interface, devs);
return 0;
+
+err_destroy_bridge:
+ it66121_destroy(devs->bridge);
+
+ return ret;
}
static void fl2000_disconnect(struct usb_interface *interface)
@@ -123,6 +138,8 @@ static void fl2000_disconnect(struct usb_interface *interface)
for (int i = 0; i < FL2000_USBIF_NUM; i++)
usb_set_intfdata(usb_ifnum_to_if(usb_dev, i), NULL);
+ it66121_destroy(devs->bridge);
+
/* Last, as devs goes with interface 0, which may be one of these */
fl2000_release_interfaces(usb_dev, interface);
}
diff --git a/drivers/gpu/drm/fl2000/fl2000_it66121.c b/drivers/gpu/drm/fl2000/fl2000_it66121.c
new file mode 100644
index 000000000..7235317b0
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_it66121.c
@@ -0,0 +1,799 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2019, Artem Mygaiev
+ */
+
+#include <linux/delay.h>
+#include <linux/hdmi.h>
+#include <linux/unaligned.h>
+
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/drm_edid.h>
+
+#include "fl2000.h"
+#include "fl2000_it66121.h"
+#include "fl2000_it66121_regs.h"
+
+#define VENDOR_ID 0x4954
+#define DEVICE_ID 0x0612
+#define REVISION_MASK 0xF000
+#define REVISION_SHIFT 12
+
+/* How often, and how far apart, to look for the chip on the bus */
+#define IT66121_DETECT_TRIES 5
+#define IT66121_DETECT_DELAY_MS 50
+
+struct it66121_priv {
+ struct i2c_adapter *adapter;
+ struct i2c_client *client;
+ struct regmap *regmap;
+ struct drm_bridge bridge;
+
+ /* The chip was reset and its HDCP engine may be holding the DDC bus */
+ bool ddc_held;
+
+ struct regmap_field *irq_pending;
+ struct regmap_field *hpd;
+ struct regmap_field *clr_irq;
+};
+
+static const struct regmap_range_cfg it66121_regmap_banks[] = {
+ /*
+ * Do not put common registers to any range, this will lead to skipping "bank" configuration
+ * when accessing those at addresses 0x00-0x2F.
+ */
+ {
+ .name = "Banks",
+ .range_min = IT66121_BANK_START,
+ .range_max = IT66121_BANK_END,
+ .selector_reg = IT66121_SYS_CONTROL,
+ .selector_mask = IT66121_SYS_BANK_MASK,
+ .selector_shift = 0,
+ .window_start = IT66121_BANK_START,
+ .window_len = IT66121_BANK_SIZE,
+ },
+};
+
+static bool it66121_reg_volatile(struct device *dev, unsigned int reg)
+{
+ return IT66121_REG_VOLATILE(reg);
+}
+
+static const struct regmap_config it66121_regmap_config = {
+ .val_bits = 8, /* 8-bit register size */
+ .reg_bits = 8, /* 8-bit register address space */
+ .reg_stride = 1,
+ .max_register = 2 * IT66121_BANK_SIZE - 1, /* 2 banks of registers */
+
+ .cache_type = REGCACHE_MAPLE,
+
+ .volatile_reg = it66121_reg_volatile,
+
+ .ranges = it66121_regmap_banks,
+ .num_ranges = ARRAY_SIZE(it66121_regmap_banks),
+
+ .reg_format_endian = REGMAP_ENDIAN_BIG,
+ .val_format_endian = REGMAP_ENDIAN_BIG,
+
+ .use_single_read = true,
+ .use_single_write = true,
+};
+
+/*
+ * Poll sparsely: every status read is a transaction on the shared bus that
+ * competes with the DDC engine's own transfer.
+ */
+#define DDC_POLL_SLEEP_US 5000
+#define DDC_POLL_TIMEOUT_US 500000
+
+#define DDC_CMD_ABORT 0xF
+
+static int it66121_configure_input(struct it66121_priv *priv)
+{
+ int ret;
+
+ /*
+ * This configures the bridge IC connection to the encoder IC, i.e. the
+ * interface between IT66121 and FL2000. It is fixed by the board design,
+ * so it is hardcoded:
+ * - input mode RGB
+ * - IO latch clock = TXCLK
+ * - CCIR656 disabled
+ * - embedded sync disabled
+ * - DDR mode disabled
+ * - 1 cycle input PCLK delay
+ */
+ ret = regmap_write(priv->regmap, IT66121_INPUT_MODE,
+ IT66121_INPUT_MODE_RGB | IT66121_INPUT_PCLKDELAY1);
+ if (ret)
+ return ret;
+
+ /*
+ * No color space conversion: the input is RGB and so is the output, so the
+ * conversion matrix and blank levels are left untouched.
+ */
+ ret = regmap_write(priv->regmap, IT66121_INPUT_COLOR_CONV, IT66121_INPUT_NO_CONV);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int it66121_configure_afe(struct it66121_priv *priv, int clock_khz)
+{
+ int ret;
+
+ ret = regmap_write(priv->regmap, IT66121_AFE_DRV_CONTROL, IT66121_AFE_RST);
+ if (ret)
+ return ret;
+
+ /* Values from the vendor reference driver, the bits are undocumented */
+ if (clock_khz > 80000) {
+ ret = regmap_write_bits(priv->regmap, IT66121_AFE_XP_CONTROL, 0x90, 0x80);
+ if (ret)
+ return ret;
+ ret = regmap_write_bits(priv->regmap, IT66121_AFE_IP_CONTROL_1, 0x89, 0x80);
+ if (ret)
+ return ret;
+ ret = regmap_write_bits(priv->regmap, IT66121_AFE_IP_CONTROL_3, 0x10, 0);
+ if (ret)
+ return ret;
+ } else {
+ ret = regmap_write_bits(priv->regmap, IT66121_AFE_XP_CONTROL, 0x90, 0x10);
+ if (ret)
+ return ret;
+ ret = regmap_write_bits(priv->regmap, IT66121_AFE_IP_CONTROL_1, 0x89, 0x09);
+ if (ret)
+ return ret;
+ ret = regmap_write_bits(priv->regmap, IT66121_AFE_IP_CONTROL_3, 0x10, 0x10);
+ if (ret)
+ return ret;
+ }
+
+ /* Fire AFE */
+ ret = regmap_write(priv->regmap, IT66121_AFE_DRV_CONTROL, 0);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+/* DDC status bits: done (7), active (6), noack/wait bus/arbitration lose (5:3) */
+#define DDC_STATUS_TX_DONE BIT(7)
+
+static int it66121_wait_ddc_ready(struct it66121_priv *priv)
+{
+ unsigned int val = 0;
+ int ret;
+
+ /*
+ * Wait for completion only. On this hardware the FL2000 I2C master and
+ * the IT66121 DDC master share the physical bus, so 'wait bus' and
+ * 'arbitration lose' flags are live/latched transients of the engine
+ * retrying on a contended bus, not terminal errors - the only reliable
+ * terminal signal is TX_DONE (or the timeout).
+ */
+ ret = regmap_read_poll_timeout(priv->regmap, IT66121_DDC_STATUS, val,
+ val & DDC_STATUS_TX_DONE, DDC_POLL_SLEEP_US,
+ DDC_POLL_TIMEOUT_US);
+ if (ret)
+ dev_dbg(&priv->client->dev, "DDC not done (%d), status 0x%02X\n", ret, val);
+
+ return ret;
+}
+
+/*
+ * The HDCP engine shares the DDC master and holds the bus after chip reset (DDC status reports
+ * 'wait bus' + 'arbitration lose', 0x1A), and that bus is the one the monitor's DDC is on. Follow
+ * the ITE reference sequence, as the ite-it66121 driver does: clear HDCP CP_DESIRE, reset the HDCP
+ * engine and re-select the host as DDC master before issuing the abort command.
+ */
+static int it66121_release_ddc(struct it66121_priv *priv)
+{
+ int ret;
+
+ ret = regmap_write_bits(priv->regmap, IT66121_HDCP_CONTROL, IT66121_HDCP_CP_DESIRE, 0);
+ if (ret)
+ return ret;
+
+ ret = regmap_write_bits(priv->regmap, IT66121_SW_RST, IT66121_SW_HDCP_RST,
+ IT66121_SW_HDCP_RST);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(priv->regmap, IT66121_DDC_CONTROL,
+ IT66121_DDC_MASTER_DDC | IT66121_DDC_MASTER_HOST);
+ if (ret)
+ return ret;
+
+ /* The ITE reference driver issues the abort command twice */
+ for (int i = 0; i < 2; i++) {
+ ret = regmap_write(priv->regmap, IT66121_DDC_COMMAND, DDC_CMD_ABORT);
+ if (ret)
+ return ret;
+
+ ret = it66121_wait_ddc_ready(priv);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/**
+ * it66121_hpd_connected() - live state of the HDMI hot plug detect line
+ * @bridge: the IT66121 bridge
+ *
+ * Return: 1 if an HDMI monitor is connected, 0 if not, a negative error code if the line cannot
+ * be read
+ */
+int it66121_hpd_connected(struct drm_bridge *bridge)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+ unsigned int val;
+ int idx, ret;
+
+ if (!drm_bridge_enter(bridge, &idx))
+ return -ENODEV;
+
+ ret = regmap_field_read(priv->hpd, &val);
+ drm_bridge_exit(idx);
+ if (ret)
+ return ret;
+
+ return !!val;
+}
+
+/**
+ * it66121_ack_interrupts() - acknowledge what is pending in the IT66121
+ * @bridge: the IT66121 bridge
+ *
+ * Its interrupt line is routed to the FL2000, which reports it on its own interrupt endpoint (see
+ * fl2000_interrupt.c): called from there, and hotplug is handled by probing the connector. The
+ * interrupt sources themselves are not decoded here. An interrupt left pending would keep the line
+ * from changing again.
+ *
+ * Return: 0 on success, a negative error code otherwise
+ */
+int it66121_ack_interrupts(struct drm_bridge *bridge)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+ unsigned int val;
+ int ret;
+
+ ret = regmap_field_read(priv->irq_pending, &val);
+ if (ret || !val)
+ return ret;
+
+ ret = regmap_read(priv->regmap, IT66121_INT_STATUS_1, &val);
+ if (ret)
+ return ret;
+
+ /* The only two sources with the same bit in both registers, and the only ones unmasked */
+ val &= IT66121_INT_STATUS_1_HPD | IT66121_INT_STATUS_1_RX_SENSE;
+ if (val) {
+ ret = regmap_write(priv->regmap, IT66121_INT_CLEAR_1, val);
+ if (ret)
+ return ret;
+ }
+
+ /* Forced: when the bit happens to read as set, a plain update would not write it */
+ return regmap_field_force_write(priv->clr_irq, 1);
+}
+
+/*
+ * The EDID is read with the FL2000's own I2C engine. On the Fresco reference designs the monitor's
+ * DDC lines hang off the FL2000 I2C bus and the vendor driver reads the EDID at address 0x50 this
+ * way. The IT66121's own EDID command cannot be used: it never completes on these boards, and
+ * could not work behind this I2C engine anyway, which transfers aligned dwords: every read of the
+ * DDC status register (0x16) also reads, and so pops, the DDC FIFO (0x17).
+ */
+static int it66121_get_edid_block(void *context, u8 *buf, unsigned int block, size_t len)
+{
+ struct it66121_priv *priv = context;
+ int ret;
+
+ ret = fl2000_ddc_read_block(priv->adapter, buf, block, len);
+ if (ret)
+ dev_dbg(&priv->client->dev, "EDID block %u read failed (%d)\n", block, ret);
+
+ return ret;
+}
+
+static enum drm_connector_status it66121_bridge_detect(struct drm_bridge *bridge,
+ struct drm_connector *connector)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+ int ret = it66121_hpd_connected(bridge);
+
+ if (ret > 0)
+ return connector_status_connected;
+
+ /* Gone with the USB device */
+ if (ret == -ENODEV)
+ return connector_status_disconnected;
+
+ if (ret < 0) {
+ /* Not bridge->dev: the chip, not the DRM device, failed to answer */
+ dev_err_ratelimited(&priv->client->dev, "Cannot get monitor status (%d)\n", ret);
+ return connector_status_unknown;
+ }
+
+ return connector_status_disconnected;
+}
+
+static const struct drm_edid *it66121_bridge_edid_read(struct drm_bridge *bridge,
+ struct drm_connector *connector)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+ const struct drm_edid *drm_edid;
+ int idx, ret;
+
+ if (!drm_bridge_enter(bridge, &idx))
+ return NULL;
+
+ if (priv->ddc_held) {
+ /* Not fatal: the read tells whether the bus is free */
+ ret = it66121_release_ddc(priv);
+ if (ret)
+ dev_dbg(&priv->client->dev, "Cannot release the DDC bus (%d)\n", ret);
+ priv->ddc_held = false;
+ }
+
+ drm_edid = drm_edid_read_custom(connector, it66121_get_edid_block, priv);
+ if (!drm_edid)
+ dev_warn(&priv->client->dev, "EDID read failed, using fallback modes\n");
+
+ drm_bridge_exit(idx);
+
+ return drm_edid;
+}
+
+/*
+ * Bring the transmitter out of reset into a known state, as the IT66121 manual and the vendor
+ * driver do: at attach, and again after system sleep, which may have cut the power.
+ */
+static void it66121_init_chip(struct it66121_priv *priv)
+{
+ /* Reset according to IT66121 manual */
+ regmap_write_bits(priv->regmap, IT66121_SW_RST, IT66121_SW_REF_RST_HDMITX,
+ IT66121_SW_REF_RST_HDMITX);
+ msleep(50);
+
+ /* Power up GRCLK & power down IACLK, TxCLK, CRCLK */
+ regmap_write_bits(priv->regmap, IT66121_SYS_CONTROL, (0xf << 3), (7 << 3));
+
+ /* Continue according to IT66121 manual */
+ regmap_write_bits(priv->regmap, IT66121_INT_CONTROL, (1 << 0), (0 << 0));
+ regmap_write_bits(priv->regmap, IT66121_AFE_DRV_CONTROL, (1 << 5), (0 << 5));
+ regmap_write_bits(priv->regmap, IT66121_AFE_XP_CONTROL, (1 << 2) | (1 << 6),
+ (0 << 2) | (0 << 6));
+ regmap_write_bits(priv->regmap, IT66121_AFE_IP_CONTROL_1, (1 << 6), (0 << 6));
+ regmap_write_bits(priv->regmap, IT66121_AFE_DRV_CONTROL, (1 << 4), (0 << 4));
+ regmap_write_bits(priv->regmap, IT66121_AFE_XP_CONTROL, (1 << 3), (1 << 3));
+ regmap_write_bits(priv->regmap, IT66121_AFE_IP_CONTROL_1, (1 << 2), (1 << 2));
+
+ /* Extra AFE steps from the vendor driver */
+ /* Whole register is defined as XP_TEST, values are undisclosed */
+ regmap_write_bits(priv->regmap, IT66121_AFE_XP_TEST, 0xFF, 0x70);
+ /* Lower 5 bits are undisclosed in manual */
+ regmap_write_bits(priv->regmap, IT66121_AFE_DRV_HS, 0xFF, 0x1F);
+ /*
+ * DRV_ISW[5:3] value '011' is a default output current level swing, with change to '111' we
+ * set output current level swing to maximum.
+ */
+ regmap_write_bits(priv->regmap, IT66121_AFE_IP_CONTROL_2, 0xFF, 0x38);
+
+ /* Power up IACLK, TxCLK */
+ regmap_write_bits(priv->regmap, IT66121_SYS_CONTROL, (3 << 4), (0 << 4));
+
+ /* Reset according to IT66121 manual */
+ regmap_write_bits(priv->regmap, IT66121_SW_RST, IT66121_SW_REF_RST_HDMITX,
+ IT66121_SW_REF_RST_HDMITX);
+ msleep(50);
+
+ /* We do not support HDCP so its ok to statically set host controls */
+ regmap_write(priv->regmap, IT66121_DDC_CONTROL,
+ IT66121_DDC_MASTER_DDC | IT66121_DDC_MASTER_HOST);
+
+ priv->ddc_held = true;
+}
+
+/*
+ * Unmask the hot plug interrupt, the one that means something here. DDC errors have nobody to read
+ * them: the DDC engine is not used.
+ */
+static void it66121_start_interrupts(struct it66121_priv *priv)
+{
+ regmap_write_bits(priv->regmap, IT66121_INT_MASK_1, IT66121_MASK_DDC | IT66121_MASK_HPD,
+ IT66121_MASK_DDC);
+ it66121_ack_interrupts(&priv->bridge);
+}
+
+static int it66121_bridge_attach(struct drm_bridge *bridge, struct drm_encoder *encoder,
+ enum drm_bridge_attach_flags flags)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+
+ /* The connector is the display driver's (drm_bridge_connector_init()) */
+ if (!(flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)) {
+ drm_err(bridge->dev, "The IT66121 bridge does not create a connector\n");
+ return -EINVAL;
+ }
+
+ if (!bridge->encoder) {
+ drm_err(bridge->dev, "Parent encoder object not found\n");
+ return -ENODEV;
+ }
+
+ it66121_init_chip(priv);
+
+ it66121_start_interrupts(priv);
+
+ drm_dbg(bridge->dev, "Bridge attached\n");
+
+ return 0;
+}
+
+/**
+ * it66121_resume() - set the IT66121 up again after system sleep
+ * @bridge: the IT66121 bridge
+ *
+ * The dongle is bus powered, and the test laptop resets its USB devices when it wakes from
+ * suspend to RAM: the chip may have lost every register. The register cache would then be wrong,
+ * and the read-modify-write updates of the initialisation would skip writes the chip still needs:
+ * drop it, so that values are read from the chip again. The mode is set again by the DRM core.
+ */
+void it66121_resume(struct drm_bridge *bridge)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+
+ regcache_drop_region(priv->regmap, 0, it66121_regmap_config.max_register);
+ it66121_init_chip(priv);
+ it66121_start_interrupts(priv);
+}
+
+/*
+ * Program the transmitter for @mode, as the monitor behind @connector wants it (HDMI, or DVI when
+ * its EDID says so or there is none). The FL2000 already clocks the input at this point.
+ */
+static void it66121_set_mode(struct it66121_priv *priv, struct drm_connector *connector,
+ const struct drm_display_mode *mode)
+{
+ struct drm_bridge *bridge = &priv->bridge;
+ bool dvi = !connector->display_info.is_hdmi;
+ struct hdmi_avi_infoframe frame;
+ ssize_t frame_size;
+ u8 buf[HDMI_INFOFRAME_SIZE(AVI)];
+ int ret;
+ const u16 aviinfo_reg[HDMI_AVI_INFOFRAME_SIZE] = {
+ IT66121_HDMI_AVIINFO_DB1, IT66121_HDMI_AVIINFO_DB2, IT66121_HDMI_AVIINFO_DB3,
+ IT66121_HDMI_AVIINFO_DB4, IT66121_HDMI_AVIINFO_DB5, IT66121_HDMI_AVIINFO_DB6,
+ IT66121_HDMI_AVIINFO_DB7, IT66121_HDMI_AVIINFO_DB8, IT66121_HDMI_AVIINFO_DB9,
+ IT66121_HDMI_AVIINFO_DB10, IT66121_HDMI_AVIINFO_DB11, IT66121_HDMI_AVIINFO_DB12,
+ IT66121_HDMI_AVIINFO_DB13
+ };
+
+ drm_dbg(bridge->dev, "Setting %s mode " DRM_MODE_FMT "\n", dvi ? "DVI" : "HDMI",
+ DRM_MODE_ARG(mode));
+
+ ret = drm_hdmi_avi_infoframe_from_display_mode(&frame, connector, mode);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot create AVI infoframe (%d)\n", ret);
+ return;
+ }
+
+ frame_size = hdmi_avi_infoframe_pack(&frame, buf, sizeof(buf));
+ if (frame_size < 0) {
+ drm_err(bridge->dev, "Cannot pack AVI infoframe (%zd)\n", frame_size);
+ return;
+ }
+
+ /* Set TX mode */
+ ret = regmap_write(priv->regmap, IT66121_HDMI_MODE,
+ dvi ? IT66121_HDMI_MODE_DVI : IT66121_HDMI_MODE_HDMI);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot set TX mode (%d)\n", ret);
+ return;
+ }
+
+ /* Enable HDMI packets, repeat for every data frame as recommended */
+ ret = regmap_write(priv->regmap, IT66121_HDMI_GEN_CTRL_PKT,
+ IT66121_HDMI_GEN_CTRL_PKT_ON | IT66121_HDMI_GEN_CTRL_PKT_RPT);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot enable HDMI packets\n");
+ return;
+ }
+
+ /* Mute AV */
+ ret = regmap_write(priv->regmap, IT66121_HDMI_AV_MUTE,
+ IT66121_HDMI_AV_MUTE_ON | IT66121_HDMI_AV_MUTE_BLUE);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot mute AV\n");
+ return;
+ }
+
+ /* Write new AVI infoframe packet */
+ for (int i = 0; i < HDMI_AVI_INFOFRAME_SIZE; i++) {
+ ret = regmap_write(priv->regmap, aviinfo_reg[i],
+ buf[i + HDMI_INFOFRAME_HEADER_SIZE]);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot write AVI infoframe byte %d (%d)\n", i, ret);
+ return;
+ }
+ }
+ ret = regmap_write(priv->regmap, IT66121_HDMI_AVIINFO_CSUM, buf[3]);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot write AVI infoframe checksum (%d)\n", ret);
+ return;
+ }
+
+ /* Enable AVI infoframe */
+ ret = regmap_write(priv->regmap, IT66121_HDMI_AVI_INFO_PKT,
+ IT66121_HDMI_AVI_INFO_PKT_ON | IT66121_HDMI_AVI_INFO_RPT);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot enable AVI infoframe (%d)\n", ret);
+ return;
+ }
+
+ /* Set reset flags */
+ ret = regmap_write_bits(priv->regmap, IT66121_SW_RST,
+ IT66121_SW_REF_RST_HDMITX | IT66121_SW_HDMI_VID_RST,
+ IT66121_SW_REF_RST_HDMITX | IT66121_SW_HDMI_VID_RST);
+ if (ret)
+ return;
+
+ /* Disable TXCLK prior to configuration */
+ ret = regmap_write_bits(priv->regmap, IT66121_SYS_CONTROL, IT66121_SYS_TXCLK_OFF,
+ IT66121_SYS_TXCLK_OFF);
+ if (ret)
+ return;
+
+ /* Configure connection, conversions, etc. */
+ ret = it66121_configure_input(priv);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot configure input bus (%d)\n", ret);
+ return;
+ }
+
+ /* Configure AFE */
+ ret = it66121_configure_afe(priv, mode->clock);
+ if (ret) {
+ drm_err(bridge->dev, "Cannot configure AFE (%d)\n", ret);
+ return;
+ }
+
+ /* Clear reset flags */
+ ret = regmap_write_bits(priv->regmap, IT66121_SW_RST,
+ IT66121_SW_REF_RST_HDMITX | IT66121_SW_HDMI_VID_RST, 0);
+ if (ret)
+ return;
+
+ /* Enable TXCLK */
+ ret = regmap_write_bits(priv->regmap, IT66121_SYS_CONTROL, IT66121_SYS_TXCLK_OFF, 0);
+ if (ret)
+ return;
+}
+
+/*
+ * The mode is programmed here rather than in .mode_set, which has no atomic state: the connector,
+ * which tells HDMI from DVI and goes into the AVI infoframe, is only known from the state.
+ */
+static void it66121_bridge_atomic_enable(struct drm_bridge *bridge,
+ struct drm_atomic_commit *state)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+ const struct drm_connector_state *conn_state;
+ const struct drm_crtc_state *crtc_state;
+ struct drm_connector *connector;
+ int idx;
+
+ connector = drm_atomic_get_new_connector_for_encoder(state, bridge->encoder);
+ if (WARN_ON(!connector))
+ return;
+
+ conn_state = drm_atomic_get_new_connector_state(state, connector);
+ if (WARN_ON(!conn_state || !conn_state->crtc))
+ return;
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+ if (WARN_ON(!crtc_state))
+ return;
+
+ if (!drm_bridge_enter(bridge, &idx))
+ return;
+
+ it66121_set_mode(priv, connector, &crtc_state->mode);
+
+ /* Unmute AV */
+ regmap_write(priv->regmap, IT66121_HDMI_AV_MUTE, 0);
+
+ drm_bridge_exit(idx);
+}
+
+static void it66121_bridge_atomic_disable(struct drm_bridge *bridge,
+ struct drm_atomic_commit *state)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+ int idx;
+
+ if (!drm_bridge_enter(bridge, &idx))
+ return;
+
+ /* Mute AV */
+ regmap_write_bits(priv->regmap, IT66121_HDMI_AV_MUTE, IT66121_HDMI_AV_MUTE_ON,
+ IT66121_HDMI_AV_MUTE_ON);
+
+ drm_bridge_exit(idx);
+}
+
+static const struct drm_bridge_funcs it66121_bridge_funcs = {
+ .attach = it66121_bridge_attach,
+ .atomic_create_state = drm_atomic_helper_bridge_create_state,
+ .atomic_duplicate_state = drm_atomic_helper_bridge_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_bridge_destroy_state,
+ .atomic_enable = it66121_bridge_atomic_enable,
+ .atomic_disable = it66121_bridge_atomic_disable,
+ .detect = it66121_bridge_detect,
+ .edid_read = it66121_bridge_edid_read,
+};
+
+static int it66121_regs_init(struct it66121_priv *priv, struct i2c_client *client)
+{
+ priv->regmap = devm_regmap_init_i2c(client, &it66121_regmap_config);
+ if (IS_ERR(priv->regmap))
+ return PTR_ERR(priv->regmap);
+
+ priv->irq_pending =
+ devm_regmap_field_alloc(&client->dev, priv->regmap, IT66121_SYS_STATUS_irq_pending);
+ priv->hpd = devm_regmap_field_alloc(&client->dev, priv->regmap, IT66121_SYS_STATUS_hpd);
+ priv->clr_irq =
+ devm_regmap_field_alloc(&client->dev, priv->regmap, IT66121_SYS_STATUS_clr_irq);
+ if (IS_ERR(priv->irq_pending) || IS_ERR(priv->hpd) || IS_ERR(priv->clr_irq))
+ return -ENOMEM;
+
+ return 0;
+}
+
+/*
+ * The callback of i2c_new_scanned_device(): nonzero if the device is there. It reads the IDs with
+ * the same transfers as the regmap, which is all this adapter can do (see its quirks).
+ */
+static int it66121_i2c_probe(struct i2c_adapter *adapter, unsigned short address)
+{
+ int ret;
+ u8 id_regs[] = { IT66121_VENDOR_ID_1, IT66121_VENDOR_ID_2, IT66121_DEVICE_ID_1,
+ IT66121_DEVICE_ID_2 };
+ u8 id[ARRAY_SIZE(id_regs)];
+ u16 vendor, device;
+
+ dev_dbg(&adapter->dev, "Detecting IT66121 at address 0x%X on %s\n", address,
+ adapter->name);
+
+ for (int i = 0; i < ARRAY_SIZE(id_regs); i++) {
+ struct i2c_msg msgs[] = {
+ { .addr = address, .flags = 0, .len = 1, .buf = &id_regs[i] },
+ { .addr = address, .flags = I2C_M_RD, .len = 1, .buf = &id[i] }
+ };
+
+ ret = i2c_transfer(adapter, msgs, ARRAY_SIZE(msgs));
+ if (ret != ARRAY_SIZE(msgs)) {
+ dev_dbg(&adapter->dev, "No answer at address 0x%X (%d)\n", address, ret);
+ return 0;
+ }
+ }
+
+ vendor = get_unaligned_le16(&id[0]);
+ device = get_unaligned_le16(&id[2]);
+ if (vendor != VENDOR_ID || (device & ~REVISION_MASK) != DEVICE_ID) {
+ dev_warn(&adapter->dev, "Not an IT66121 at address 0x%X (0x%04X-0x%04X)\n", address,
+ vendor, device);
+ return 0;
+ }
+
+ dev_dbg(&adapter->dev, "IT66121 found, revision %d\n",
+ (device & REVISION_MASK) >> REVISION_SHIFT);
+
+ return 1;
+}
+
+static struct i2c_client *it66121_i2c_init(struct i2c_adapter *adapter)
+{
+ /*
+ * Not "it66121": that name would make the ite-it66121 driver, if built,
+ * try to bind this client and release our devres when its probe fails.
+ */
+ struct i2c_board_info board_info = { I2C_BOARD_INFO("fl2000-it66121", 0) };
+
+ /* According to datasheet IT66121 addresses are 0x98 or 0x9A including cmd */
+ const unsigned short it66121_addr[] = { (0x98 >> 1), (0x9A >> 1), I2C_CLIENT_END };
+ struct i2c_client *client;
+
+ /* The chip may still be waking up when the USB device has just enumerated */
+ for (int i = 0; i < IT66121_DETECT_TRIES; i++) {
+ if (i)
+ msleep(IT66121_DETECT_DELAY_MS);
+
+ client = i2c_new_scanned_device(adapter, &board_info, it66121_addr,
+ it66121_i2c_probe);
+ if (!IS_ERR(client))
+ break;
+ }
+
+ return client;
+}
+
+/**
+ * it66121_destroy() - remove the bridge
+ * @bridge: the IT66121 bridge
+ *
+ * A DRM device that userspace keeps open still has the bridge in its encoder's chain, and frees it
+ * with the last reference, but nothing gets past drm_bridge_enter() any more.
+ */
+void it66121_destroy(struct drm_bridge *bridge)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+
+ drm_bridge_unplug(bridge);
+
+ /*
+ * Releases what is managed on the client, its regmap included, and drops the devm reference
+ * to the bridge.
+ */
+ i2c_unregister_device(priv->client);
+}
+
+/**
+ * it66121_create() - find the IT66121 on the FL2000 I2C bus and make a bridge of it
+ * @adapter: the FL2000 I2C adapter
+ *
+ * Return: the bridge, to attach to the encoder, or an ERR_PTR() on failure
+ */
+struct drm_bridge *it66121_create(struct i2c_adapter *adapter)
+{
+ int ret;
+ struct it66121_priv *priv;
+ struct i2c_client *client;
+
+ client = it66121_i2c_init(adapter);
+ if (IS_ERR(client)) {
+ dev_err(&adapter->dev, "No IT66121 found (%pe): adapters without one are not supported\n",
+ client);
+ return ERR_CAST(client);
+ }
+
+ /*
+ * DRM core takes references on the bridge, so it must be allocated with
+ * devm_drm_bridge_alloc(). Freeing happens automatically when the last
+ * reference is dropped after i2c_unregister_device().
+ */
+ priv = devm_drm_bridge_alloc(&client->dev, struct it66121_priv, bridge,
+ &it66121_bridge_funcs);
+ if (IS_ERR(priv)) {
+ i2c_unregister_device(client);
+ return ERR_CAST(priv);
+ }
+
+ priv->client = client;
+ priv->adapter = adapter;
+
+ ret = it66121_regs_init(priv, priv->client);
+ if (ret) {
+ dev_err(&client->dev, "Cannot initialize registers (%d)\n", ret);
+ i2c_unregister_device(client);
+ return ERR_PTR(ret);
+ }
+
+ /*
+ * The chip's interrupt reaches the host only through the FL2000's, which sends a hotplug
+ * event for the whole device (fl2000_drm_hotplug()). The bridge therefore reports no hot
+ * plug of its own (DRM_BRIDGE_OP_HPD), and the connector also polls the line.
+ */
+ priv->bridge.type = DRM_MODE_CONNECTOR_HDMIA;
+ priv->bridge.ops = DRM_BRIDGE_OP_DETECT | DRM_BRIDGE_OP_EDID;
+
+ drm_bridge_add(&priv->bridge);
+
+ return &priv->bridge;
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_it66121.h b/drivers/gpu/drm/fl2000/fl2000_it66121.h
new file mode 100644
index 000000000..568d3cd60
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_it66121.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * (C) Copyright 2019, Artem Mygaiev
+ */
+
+#ifndef FL2000_IT66121_H
+#define FL2000_IT66121_H
+
+struct drm_bridge;
+struct i2c_adapter;
+
+struct drm_bridge *it66121_create(struct i2c_adapter *adapter);
+void it66121_destroy(struct drm_bridge *bridge);
+int it66121_hpd_connected(struct drm_bridge *bridge);
+int it66121_ack_interrupts(struct drm_bridge *bridge);
+void it66121_resume(struct drm_bridge *bridge);
+
+#endif /* FL2000_IT66121_H */
diff --git a/drivers/gpu/drm/fl2000/fl2000_it66121_regs.h b/drivers/gpu/drm/fl2000/fl2000_it66121_regs.h
new file mode 100644
index 000000000..de784cd71
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_it66121_regs.h
@@ -0,0 +1,284 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Based on the un-official documentation found on the net and registers
+ * description from source code:
+ * - FL2000DX Linux driver on GitHub
+ * - RK3188 Android driver on GitHub
+ *
+ * (C) Copyright 2019, Artem Mygaiev
+ */
+
+#ifndef FL2000_IT66121_REGS_H
+#define FL2000_IT66121_REGS_H
+
+#define IT66121_BANK_SIZE 0x100
+
+/* 000 - 02F Common registers, same as 100 - 12F */
+#define IT66121_VENDOR_ID_1 0x00
+#define IT66121_VENDOR_ID_2 0x01
+#define IT66121_DEVICE_ID_1 0x02
+#define IT66121_DEVICE_ID_2 0x03
+#define IT66121_SW_RST 0x04
+#define IT66121_SW_ENTEST BIT(7)
+#define IT66121_SW_REF_RST_HDMITX BIT(5)
+#define IT66121_SW_AREF_RST BIT(4)
+#define IT66121_SW_HDMI_VID_RST BIT(3)
+#define IT66121_SW_HDMI_AUD_RST BIT(2)
+#define IT66121_SW_HDMI_RST BIT(1)
+#define IT66121_SW_HDCP_RST BIT(0)
+
+#define IT66121_INT_CONTROL 0x05
+#define IT66121_INT_STATUS_1 0x06
+#define IT66121_INT_STATUS_1_HPD BIT(0)
+#define IT66121_INT_STATUS_1_RX_SENSE BIT(1)
+#define IT66121_INT_STATUS_1_DDC_BUS_HANG BIT(2)
+#define IT66121_INT_STATUS_1_ROMACQ_BUS_HANG BIT(3)
+#define IT66121_INT_STATUS_1_DDC_FIFO_ERR BIT(4)
+#define IT66121_INT_STATUS_1_DDC_NOACK BIT(5)
+#define IT66121_INT_STATUS_1_ROMACQ_NOACK BIT(6)
+#define IT66121_INT_STATUS_1_AUD_OVERFLOW BIT(7)
+
+#define IT66121_INT_STATUS_2 0x07
+#define IT66121_INT_STATUS_3 0x08
+#define IT66121_INT_MASK_1 0x09
+#define IT66121_MASK_DDC_NOACK BIT(5)
+#define IT66121_MASK_DDC_FIFOERR BIT(4)
+#define IT66121_MASK_DDC_BUSHANG BIT(2)
+#define IT66121_MASK_DDC GENMASK(5, 2)
+#define IT66121_MASK_HPD BIT(0)
+
+#define IT66121_INT_MASK_2 0x0A
+#define IT66121_INT_MASK_3 0x0B
+#define IT66121_INT_CLEAR_1 0x0C
+#define IT66121_INT_CLEAR_2 0x0D
+#define IT66121_SYS_STATUS 0x0E
+static const struct reg_field IT66121_SYS_STATUS_irq_pending = REG_FIELD(IT66121_SYS_STATUS, 7, 7);
+static const struct reg_field IT66121_SYS_STATUS_hpd = REG_FIELD(IT66121_SYS_STATUS, 6, 6);
+static const struct reg_field IT66121_SYS_STATUS_clr_irq = REG_FIELD(IT66121_SYS_STATUS, 0, 0);
+
+#define IT66121_SYS_CONTROL 0x0F
+#define IT66121_SYS_RCLK_OFF BIT(6)
+#define IT66121_SYS_IACLK_OFF BIT(5)
+#define IT66121_SYS_TXCLK_OFF BIT(4)
+#define IT66121_SYS_CRCLK_OFF BIT(3)
+#define IT66121_SYS_BANK_MASK 0x03
+
+#define IT66121_DDC_CONTROL 0x10
+#define IT66121_DDC_MASTER_ROM BIT(1)
+#define IT66121_DDC_MASTER_DDC 0
+#define IT66121_DDC_MASTER_HOST BIT(0)
+#define IT66121_DDC_MASTER_HDCP 0
+
+#define IT66121_DDC_ADDRESS 0x11
+#define IT66121_DDC_OFFSET 0x12
+#define IT66121_DDC_SIZE 0x13
+#define IT66121_DDC_SEGMENT 0x14
+#define IT66121_DDC_COMMAND 0x15
+#define IT66121_DDC_STATUS 0x16
+
+#define IT66121_DDC_RD_FIFO 0x17
+/* reserved */
+#define IT66121_HDCP_ADDRESS 0x19
+
+#define IT66121_HDCP_CONTROL 0x20
+#define IT66121_HDCP_CP_DESIRE BIT(0)
+/* reserved */
+#define IT66121_DDC_BUS_HOLD_TIME 0x1B
+#define IT66121_ROM_STATUS 0x1C
+/* reserved */
+/* reserved */
+/* 01F - 2F HDCP registers, ignored */
+
+#define IT66121_BANK_START 0x30
+
+/* 030 - 0FF Bank 0 */
+/* 030 - 057 HDCP registers, ignored */
+#define IT66121_MCLK_CONTROL 0x058
+#define IT66121_PLL_CONTROL 0x059
+#define IT66121_CLK_POWER_CONTROL 0x05A
+#define IT66121_OS_FREQ_NUM_2 0x05B
+#define IT66121_OS_FREQ_NUM_1 0x05C
+/* reserved */
+#define IT66121_TX_CLK_COUNT 0x05E
+#define IT66121_PLL_LOCK_STATUS 0x05F
+#define IT66121_AUDIO_FREQ_COUNT 0x060
+#define IT66121_AFE_DRV_CONTROL 0x061
+#define IT66121_AFE_DRV_PWD BIT(5)
+#define IT66121_AFE_RST BIT(4)
+
+#define IT66121_AFE_XP_CONTROL 0x062
+#define IT66121_AFE_IP_CONTROL_2 0x063
+#define IT66121_AFE_IP_CONTROL_1 0x064
+#define IT66121_AFE_RING_CONTROL 0x065
+#define IT66121_AFE_DRV_HS 0x066
+/* reserved */
+#define IT66121_AFE_IP_CONTROL_3 0x068
+#define IT66121_AFE_PAT_RSTB 0x069
+#define IT66121_AFE_XP_TEST 0x06A
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+#define IT66121_INPUT_MODE 0x070
+#define IT66121_INPUT_MODE_RGB 0
+#define IT66121_INPUT_MODE_YUV444 BIT(7)
+#define IT66121_INPUT_MODE_YUV422 BIT(6)
+#define IT66121_INPUT_MODE_TXCLKDIV2 BIT(5)
+#define IT66121_INPUT_MODE_CCIR656 BIT(4)
+#define IT66121_INPUT_MODE_SYNCEMB BIT(3)
+#define IT66121_INPUT_MODE_DDR BIT(2)
+#define IT66121_INPUT_PCLKDELAY1 1
+#define IT66121_INPUT_PCLKDELAY2 2
+#define IT66121_INPUT_PCLKDELAY3 3
+
+#define IT66121_INPUT_IO_CONTROL 0x071
+#define IT66121_INPUT_COLOR_CONV 0x072
+#define IT66121_INPUT_DITHER BIT(7)
+#define IT66121_INPUT_UDFILTER BIT(6)
+#define IT66121_INPUT_DNFREE_GO BIT(5)
+#define IT66121_INPUT_BTAT1004 BIT(2)
+#define IT66121_INPUT_RGB_TO_YUV 0x2
+#define IT66121_INPUT_YUV_TO_RGB 0x3
+#define IT66121_INPUT_NO_CONV 0x0
+
+#define IT66121_Y_BLANK_LEVEL 0x073
+#define IT66121_C_BLANK_LEVEL 0x074
+#define IT66121_RGB_BLANK_LEVEL 0x075
+#define IT66121_MATRIX_11V_1 0x076
+#define IT66121_MATRIX_11V_2 0x077
+#define IT66121_MATRIX_12V_1 0x078
+#define IT66121_MATRIX_12V_2 0x079
+#define IT66121_MATRIX_13V_1 0x07A
+#define IT66121_MATRIX_13V_2 0x07B
+#define IT66121_MATRIX_21V_1 0x07C
+#define IT66121_MATRIX_21V_2 0x07D
+#define IT66121_MATRIX_22V_1 0x07E
+#define IT66121_MATRIX_22V_2 0x07F
+#define IT66121_MATRIX_23V_1 0x080
+#define IT66121_MATRIX_23V_2 0x081
+#define IT66121_MATRIX_31V_1 0x082
+#define IT66121_MATRIX_31V_2 0x083
+#define IT66121_MATRIX_32V_1 0x084
+#define IT66121_MATRIX_32V_2 0x085
+#define IT66121_MATRIX_33V_1 0x086
+#define IT66121_MATRIX_33V_2 0x087
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+#define IT66121_PCI2C_CEC_ADDRESS 0x08D
+/* reserved */
+/* reserved */
+/* 090 - 0B2 Pattern generation registers, ignored */
+/* reserved */
+#define IT66121_HDMI_DATA_SWAP 0x0BF
+
+#define IT66121_HDMI_MODE 0x0C0
+#define IT66121_HDMI_MODE_HDMI BIT(0)
+#define IT66121_HDMI_MODE_DVI 0
+
+#define IT66121_HDMI_AV_MUTE 0x0C1
+#define IT66121_HDMI_AV_MUTE_ON BIT(0)
+#define IT66121_HDMI_AV_MUTE_BLUE BIT(1)
+
+#define IT66121_HDMI_BLACK_SRC 0x0C2
+#define IT66121_HDMI_OESS_PREIOD 0x0C3
+/* reserved */
+#define IT66121_HDMI_AUDIO_CTS 0x0C5
+#define IT66121_HDMI_GEN_CTRL_PKT 0x0C6
+#define IT66121_HDMI_GEN_CTRL_PKT_ON BIT(0)
+#define IT66121_HDMI_GEN_CTRL_PKT_RPT BIT(1)
+
+/* reserved */
+/* reserved */
+#define IT66121_HDMI_NULL_PKT 0x0C9
+#define IT66121_HDMI_ACP_PKT 0x0CA
+/* reserved */
+/* reserved */
+#define IT66121_HDMI_AVI_INFO_PKT 0x0CD
+#define IT66121_HDMI_AVI_INFO_PKT_ON BIT(0)
+#define IT66121_HDMI_AVI_INFO_RPT BIT(1)
+
+#define IT66121_HDMI_AUD_INFO_PKT 0x0CE
+/* reserved */
+#define IT66121_HDMI_MPEG_INFO_PKT 0x0D0
+#define IT66121_HDMI_VIDEO_PARAM_STATUS 0x0D1
+#define IT66121_HDMI_3D_INFO_PKT 0x0D2
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+#define IT66121_HDMI_PCLK_CONTROL 0x0D7
+#define IT66121_HDMI_PCLK_COUNT 0x0D8
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+/* reserved */
+/* E0 - E7 Audio channel registers, ignored */
+#define IT66121_EXT_INT_CONTROL 0x0E8
+/* reserved */
+/* reserved */
+/* reserved */
+#define IT66121_EXT_INT_MASK 0x0EC
+/* reserved */
+#define IT66121_EXT_INT_STATUS_1 0x0EE
+/* reserved */
+#define IT66121_EXT_INT_STATUS_2 0x0F0
+/* reserved */
+/* reserved */
+/* 0F3 - 0F8 Test registers, ignored */
+/* 0F9 - 0FF undefined*/
+
+/* 130 - 1BF Bank 1 */
+/* 130 - 18F HDMI packet content registers, ignored except AVI InfoFrame */
+#define IT66121_HDMI_AVIINFO_DB1 0x158
+#define IT66121_HDMI_AVIINFO_DB2 0x159
+#define IT66121_HDMI_AVIINFO_DB3 0x15A
+#define IT66121_HDMI_AVIINFO_DB4 0x15B
+#define IT66121_HDMI_AVIINFO_DB5 0x15C
+#define IT66121_HDMI_AVIINFO_CSUM 0x15D
+#define IT66121_HDMI_AVIINFO_DB6 0x15E
+#define IT66121_HDMI_AVIINFO_DB7 0x15F
+#define IT66121_HDMI_AVIINFO_DB8 0x160
+#define IT66121_HDMI_AVIINFO_DB9 0x161
+#define IT66121_HDMI_AVIINFO_DB10 0x162
+#define IT66121_HDMI_AVIINFO_DB11 0x163
+#define IT66121_HDMI_AVIINFO_DB12 0x164
+#define IT66121_HDMI_AVIINFO_DB13 0x165
+
+/* 190 - 1BF Audio channel status registers, ignored */
+
+#define IT66121_BANK_END 0x1FF
+
+/* CEC registers ignored (must be configured as a separate regmap / device) */
+
+/* List of volatile registers that shall not be cached */
+static inline bool IT66121_REG_VOLATILE(u32 reg)
+{
+ switch (reg) {
+ case IT66121_INT_STATUS_1:
+ case IT66121_INT_STATUS_2:
+ case IT66121_INT_STATUS_3:
+ case IT66121_SYS_STATUS:
+ case IT66121_DDC_STATUS:
+ case IT66121_DDC_RD_FIFO:
+ case IT66121_ROM_STATUS:
+ case IT66121_OS_FREQ_NUM_2:
+ case IT66121_OS_FREQ_NUM_1:
+ case IT66121_TX_CLK_COUNT:
+ case IT66121_PLL_LOCK_STATUS:
+ case IT66121_AUDIO_FREQ_COUNT:
+ case IT66121_HDMI_PCLK_CONTROL:
+ case IT66121_HDMI_PCLK_COUNT:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#endif /* FL2000_IT66121_REGS_H */
--
2.56.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH v2 3/4] drm/fl2000: Add frame streaming
2026-11-12 3:34 [PATCH v2 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-12 3:34 ` [PATCH v2 1/4] drm/fl2000: Add USB driver with register and I2C access Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-12 3:34 ` [PATCH v2 2/4] drm/fl2000: Add ITE IT66121 HDMI transmitter support Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-12 3:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-12 3:34 ` [PATCH v2 4/4] drm/fl2000: Add KMS display pipeline Angel Gabriel Crispin Valdivia via B4 Relay
3 siblings, 0 replies; 5+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-12 3:34 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Angel Gabriel Crispin Valdivia
Cc: linux-kernel, dri-devel
From: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
Frames are sent as one bulk transfer per frame on endpoint 1 OUT, of
altsetting 1 of the AV control interface. Three URBs are kept
in flight from a pool of six scatter-gather buffers, and when no new
frame is ready the current one is sent again, so the FL2000 always has
data to scan out.
The current frame is kept in a shadow buffer in the device's pixel
format. Only the damaged rectangle of the part of the framebuffer that
the plane selects is converted, into the shadow, and each outgoing
buffer is brought up to date from it just before it is submitted, by
replaying the damage rectangles it missed or with a full copy when it is
too far behind. The send path never waits for a conversion in progress:
it resends the newest complete frame instead.
The framebuffer is often a buffer imported from the primary GPU and
lives in its VRAM, mapped write-combined. It is read a scanline at a
time with drm_memcpy_from_wc(), which uses streaming loads: a 1080p
frame then converts in about 8 ms instead of the ~88 ms of
memcpy_fromio(). The FL2000 wants the two 32-bit halves of every 64-bit
word swapped, which the converters do as they write. Copies run with the
buffer taken off the lists, so list_lock, which is also taken from URB
completion, is never held across multi-megabyte work.
The stream is wired up to KMS in the next patch.
Signed-off-by: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
---
drivers/gpu/drm/fl2000/Makefile | 3 +-
drivers/gpu/drm/fl2000/fl2000.h | 37 ++
drivers/gpu/drm/fl2000/fl2000_streaming.c | 807 ++++++++++++++++++++++++++++++
3 files changed, 846 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/fl2000/Makefile b/drivers/gpu/drm/fl2000/Makefile
index f64057634..0671c867e 100644
--- a/drivers/gpu/drm/fl2000/Makefile
+++ b/drivers/gpu/drm/fl2000/Makefile
@@ -4,6 +4,7 @@ fl2000-y := \
fl2000_drv.o \
fl2000_i2c.o \
fl2000_it66121.o \
- fl2000_registers.o
+ fl2000_registers.o \
+ fl2000_streaming.o
obj-$(CONFIG_DRM_FL2000) += fl2000.o
diff --git a/drivers/gpu/drm/fl2000/fl2000.h b/drivers/gpu/drm/fl2000/fl2000.h
index 9b903e68c..e6a7de0f7 100644
--- a/drivers/gpu/drm/fl2000/fl2000.h
+++ b/drivers/gpu/drm/fl2000/fl2000.h
@@ -41,6 +41,24 @@ enum fl2000_interface {
FL2000_USBIF_NUM,
};
+/* Called from URB completion handlers, i.e. in atomic context */
+static inline int fl2000_urb_status(struct usb_device *usb_dev, int status)
+{
+ switch (status) {
+ case 0:
+ case -ECONNRESET:
+ case -ENOENT:
+ case -ESHUTDOWN:
+ /* Success, or the URB was unlinked / the device is going away */
+ break;
+ default:
+ dev_err_ratelimited(&usb_dev->dev, "Nonzero urb status, %d\n", status);
+ break;
+ }
+
+ return status;
+}
+
struct fl2000_timings {
u32 hactive;
u32 htotal;
@@ -63,6 +81,25 @@ struct fl2000_pll {
#define I2C_RDWR_INTERVAL 200
#define I2C_RDWR_TIMEOUT (256 * 1000)
+/* Streaming transfer task */
+struct fl2000_stream;
+struct fl2000_stream *fl2000_stream_create(struct usb_device *usb_dev);
+void fl2000_stream_destroy(struct fl2000_stream *stream);
+
+/* Streaming interface */
+int fl2000_stream_mode_set(struct fl2000_stream *stream, unsigned int width,
+ unsigned int height, u32 bytes_pix);
+struct drm_rect;
+struct iosys_map;
+void fl2000_stream_compress(struct fl2000_stream *stream, const struct iosys_map *src_map,
+ bool imported, unsigned int pitch,
+ const struct drm_rect *src, const struct drm_rect *clip);
+void fl2000_stream_note_render(struct fl2000_stream *stream, unsigned int conv_us,
+ unsigned int damage_px);
+
+int fl2000_stream_enable(struct fl2000_stream *stream);
+void fl2000_stream_disable(struct fl2000_stream *stream);
+
/* I2C adapter interface creation */
struct i2c_adapter *fl2000_i2c_init(struct usb_interface *interface, struct regmap *regmap);
int fl2000_ddc_read_block(struct i2c_adapter *adapter, u8 *buf, unsigned int block, size_t len);
diff --git a/drivers/gpu/drm/fl2000/fl2000_streaming.c b/drivers/gpu/drm/fl2000/fl2000_streaming.c
new file mode 100644
index 000000000..dad640de5
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_streaming.c
@@ -0,0 +1,807 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Frame streaming. The pixel data is sent as one bulk transfer per frame on
+ * endpoint 1 OUT, followed by a zero-length packet that ends the frame, after
+ * switching the AV control interface (#0) to altsetting 1. The hardware
+ * exposes endpoint #1 on more than one interface, which the USB specification
+ * only allows across alternate settings.
+ *
+ * The current frame lives in a shadow buffer, already in the device's pixel
+ * format. Damage is converted into the shadow and nowhere else, and every
+ * outgoing URB buffer is brought up to date from it just before it is sent.
+ *
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#include <linux/highmem.h>
+#include <linux/iosys-map.h>
+#include <linux/scatterlist.h>
+#include <linux/vmalloc.h>
+
+#include <drm/drm_cache.h>
+#include <drm/drm_rect.h>
+
+#include "fl2000.h"
+
+/*
+ * FL2000_SB_MIN URBs are kept in flight; the rest are spares the send path
+ * draws from. Damage does not depend on this pool - it lands in the shadow
+ * frame - so an empty pool only delays a send, it can never lose an update.
+ */
+#define FL2000_SB_MIN 3
+#define FL2000_SB_NUM (FL2000_SB_MIN * 2)
+
+#define FL2000_SUBMIT_ATTEMPTS 10
+
+/*
+ * How many shadow generations of damage history are kept. A buffer further
+ * behind than this is refreshed with a full copy.
+ */
+#define FL2000_DAMAGE_LOG 16
+
+/* Must cover FL2000_MAX_WIDTH from fl2000_drm.c */
+#define FL2000_MAX_LINE_PIXELS 4096
+
+struct fl2000_stream;
+
+struct fl2000_stream_buf {
+ struct fl2000_stream *stream;
+ struct list_head list;
+ /* Generation of the content it was built from: if current, it is resent as it is */
+ u32 seq;
+ struct sg_table sgt;
+ struct page **pages;
+ unsigned int nr_pages;
+ void *vaddr;
+};
+
+struct fl2000_stream {
+ struct usb_device *usb_dev;
+ /* Each buffer journey: render (free) -> wait (in flight) -> render */
+ struct list_head render_list;
+ struct list_head wait_list;
+ spinlock_t list_lock; /* Protects the lists, taken from URB completion */
+ size_t buf_size;
+ u32 bytes_pix;
+ unsigned int width;
+ unsigned int height;
+ /*
+ * The authoritative copy of the current frame, in the device's pixel
+ * format. Damage lands here and nowhere else, and every outgoing URB
+ * buffer is a copy of it.
+ */
+ void *shadow;
+ struct mutex shadow_lock; /* Serialises the shadow against the send path */
+ u32 shadow_seq;
+ /*
+ * What changed at each shadow generation. A buffer that is only a few
+ * generations behind is caught up by replaying these rectangles
+ * instead of copying the whole frame.
+ */
+ struct drm_rect damage_log[FL2000_DAMAGE_LOG];
+ /*
+ * The buffer that carries the most recent content. Only the send path
+ * writes a buffer, and never one that is in flight, so it can be
+ * copied from without a lock.
+ */
+ struct fl2000_stream_buf *newest_sb;
+ struct work_struct work;
+ struct workqueue_struct *work_queue;
+ /* URBs the work may still submit: limits the frames in flight */
+ atomic_t credits;
+ wait_queue_head_t credits_wq;
+ bool enabled;
+ struct usb_anchor anchor;
+ /* Throughput accounting, see fl2000_stream_report() */
+ atomic_t frames;
+ atomic_t rendered;
+ atomic_t stalled;
+ atomic_t conv_us_total;
+ atomic_t conv_us_max;
+ atomic_t damage_px_total;
+ ktime_t last_report;
+ /* Cached staging buffer for one scanline of source pixels */
+ u32 *line_buf;
+};
+
+/*
+ * Periodic throughput line:
+ *
+ * sent - URBs completed, new frames and resends alike: the link rate.
+ * rendered - frames the compositor actually handed over: what the eye sees.
+ * stalled - sends that could not use the shadow because no buffer was free
+ * or a conversion held it. No update is lost, the frame that goes
+ * out is one generation older.
+ * conv - time converting pixels, and the average damaged area it covered.
+ */
+#define FL2000_REPORT_INTERVAL_MS 5000
+
+static void fl2000_stream_report(struct fl2000_stream *stream)
+{
+ ktime_t now = ktime_get();
+ s64 elapsed_ms = ktime_ms_delta(now, stream->last_report);
+ unsigned int frames, rendered, stalled;
+ unsigned int conv_us, conv_max, damage_px;
+
+ if (elapsed_ms < FL2000_REPORT_INTERVAL_MS)
+ return;
+
+ stream->last_report = now;
+ frames = atomic_xchg(&stream->frames, 0);
+ rendered = atomic_xchg(&stream->rendered, 0);
+ stalled = atomic_xchg(&stream->stalled, 0);
+ conv_us = atomic_xchg(&stream->conv_us_total, 0);
+ conv_max = atomic_xchg(&stream->conv_us_max, 0);
+ damage_px = atomic_xchg(&stream->damage_px_total, 0);
+
+ /* div64_u64(): the MB/s divisor does not fit in 32 bits */
+ dev_dbg(&stream->usb_dev->dev,
+ "Stream: %llu sent/s, %llu rendered/s, %llu MB/s, %u stalled, conv %u us (max %u) over %u px\n",
+ div64_u64((u64)frames * 1000, elapsed_ms),
+ div64_u64((u64)rendered * 1000, elapsed_ms),
+ div64_u64((u64)frames * stream->buf_size * 1000, (u64)elapsed_ms * 1024 * 1024),
+ stalled, rendered ? conv_us / rendered : 0, conv_max,
+ rendered ? damage_px / rendered : 0);
+}
+
+/* Called by the primary plane update with the cost of one damage update */
+void fl2000_stream_note_render(struct fl2000_stream *stream, unsigned int conv_us,
+ unsigned int damage_px)
+{
+ atomic_inc(&stream->rendered);
+ atomic_add(conv_us, &stream->conv_us_total);
+ atomic_add(damage_px, &stream->damage_px_total);
+ if (conv_us > (unsigned int)atomic_read(&stream->conv_us_max))
+ atomic_set(&stream->conv_us_max, conv_us);
+}
+
+static void fl2000_free_sb(struct fl2000_stream_buf *sb)
+{
+ vunmap(sb->vaddr);
+
+ sg_free_table(&sb->sgt);
+
+ for (int i = 0; i < sb->nr_pages && sb->pages[i]; i++)
+ __free_page(sb->pages[i]);
+
+ kfree(sb->pages);
+
+ kfree(sb);
+}
+
+static struct fl2000_stream_buf *fl2000_alloc_sb(struct fl2000_stream *stream, unsigned int size)
+{
+ int ret;
+ struct fl2000_stream_buf *sb;
+ unsigned int nr_pages = PAGE_ALIGN(size) >> PAGE_SHIFT;
+
+ sb = kzalloc_obj(*sb);
+ if (!sb)
+ return NULL;
+
+ sb->stream = stream;
+
+ sb->pages = kzalloc_objs(*sb->pages, nr_pages);
+ if (!sb->pages)
+ goto error;
+
+ sb->nr_pages = nr_pages;
+
+ for (int i = 0; i < nr_pages; i++) {
+ sb->pages[i] = alloc_page(GFP_KERNEL);
+ if (!sb->pages[i])
+ goto error;
+ }
+
+ ret = sg_alloc_table_from_pages(&sb->sgt, sb->pages, nr_pages, 0, size, GFP_KERNEL);
+ if (ret != 0)
+ goto error;
+
+ sb->vaddr = vmap(sb->pages, nr_pages, VM_MAP, PAGE_KERNEL);
+ if (!sb->vaddr)
+ goto error;
+
+ INIT_LIST_HEAD(&sb->list);
+ memset(sb->vaddr, 0, nr_pages << PAGE_SHIFT);
+
+ return sb;
+
+error:
+ fl2000_free_sb(sb);
+
+ return NULL;
+}
+
+static void fl2000_stream_put_buffers(struct fl2000_stream *stream)
+{
+ struct fl2000_stream_buf *cur_sb;
+ struct fl2000_stream_buf *temp_sb;
+
+ list_for_each_entry_safe(cur_sb, temp_sb, &stream->render_list, list) {
+ list_del(&cur_sb->list);
+ fl2000_free_sb(cur_sb);
+ }
+
+ /* These are gone; the send path must not copy from one of them */
+ stream->newest_sb = NULL;
+}
+
+static int fl2000_stream_get_buffers(struct fl2000_stream *stream, unsigned int size)
+{
+ int ret;
+ struct fl2000_stream_buf *cur_sb;
+
+ if (WARN_ON(!list_empty(&stream->render_list)))
+ return -EBUSY;
+
+ for (int i = 0; i < FL2000_SB_NUM; i++) {
+ cur_sb = fl2000_alloc_sb(stream, size);
+ if (!cur_sb) {
+ ret = -ENOMEM;
+ goto error;
+ }
+
+ list_add(&cur_sb->list, &stream->render_list);
+ }
+
+ return 0;
+
+error:
+ fl2000_stream_put_buffers(stream);
+ return ret;
+}
+
+static void fl2000_stream_data_completion(struct urb *urb)
+{
+ struct fl2000_stream_buf *cur_sb = urb->context;
+ struct fl2000_stream *stream = cur_sb->stream;
+ unsigned long flags;
+
+ spin_lock_irqsave(&stream->list_lock, flags);
+ list_move_tail(&cur_sb->list, &stream->render_list);
+ spin_unlock_irqrestore(&stream->list_lock, flags);
+
+ atomic_inc(&stream->frames);
+
+ /* Return the credit and kick the transmit work */
+ atomic_inc(&stream->credits);
+ wake_up(&stream->credits_wq);
+
+ fl2000_urb_status(urb->dev, urb->status);
+
+ usb_free_urb(urb);
+}
+
+/* The end-of-frame packet carries no buffer: only its errors matter */
+static void fl2000_stream_eof_completion(struct urb *urb)
+{
+ fl2000_urb_status(urb->dev, urb->status);
+
+ usb_free_urb(urb);
+}
+
+/*
+ * Anchor and submit one URB. Its completion frees it; one that could not be
+ * submitted is freed here. Process context only: it sleeps between retries of
+ * what can go away by itself, no memory (-ENOMEM) or no DMA mapping (-EAGAIN).
+ */
+static int fl2000_stream_submit(struct fl2000_stream *stream, struct urb *urb)
+{
+ int attempts = FL2000_SUBMIT_ATTEMPTS;
+ int ret;
+
+ usb_anchor_urb(urb, &stream->anchor);
+ for (;;) {
+ ret = usb_submit_urb(urb, GFP_KERNEL);
+ if ((ret != -ENOMEM && ret != -EAGAIN) || !--attempts)
+ break;
+ usleep_range(1000, 2000);
+ }
+ if (ret) {
+ /*
+ * A URB that was never submitted stays on the anchor. Freeing
+ * it there leaves a dangling entry that
+ * usb_kill_anchored_urbs() would spin on forever, which hangs
+ * the machine when the adapter is unplugged while streaming.
+ */
+ usb_unanchor_urb(urb);
+ usb_free_urb(urb);
+ }
+
+ return ret;
+}
+
+/*
+ * Frame layout. The FL2000 expects the two 32-bit halves of every 64-bit word
+ * of the frame swapped, and the frame is one flat stream: a line does not start
+ * on a word boundary unless its length is a multiple of 8 bytes (1366 pixels at
+ * 3 bytes per pixel is 4098). Every writer below therefore applies the swizzle
+ * ('^ 4' on bytes for 24bpp, '^ 2' on 16-bit words for 16bpp) to the offset
+ * from the start of the FRAME, which is why they take the frame base and the
+ * offset of the line in it, never a pointer to the line.
+ */
+
+/*
+ * Copy one rectangle of the shadow into an outgoing buffer. The pixels of a
+ * line sit in 64-bit words whose halves are swapped, so a pixel's bytes can lie
+ * up to 4 bytes outside the plain byte range of the pixel: copy whole words.
+ */
+static void fl2000_copy_rect(struct fl2000_stream *stream, void *dst, const struct drm_rect *r)
+{
+ u32 line_len = stream->width * stream->bytes_pix;
+ size_t off, span;
+ int x1, x2, y1, y2;
+
+ /*
+ * A rectangle can outlive the geometry it was recorded under: a mode
+ * change that keeps the buffer size keeps the buffers too.
+ */
+ x1 = clamp(r->x1, 0, (int)stream->width);
+ x2 = clamp(r->x2, 0, (int)stream->width);
+ y1 = clamp(r->y1, 0, (int)stream->height);
+ y2 = clamp(r->y2, 0, (int)stream->height);
+ if (x1 >= x2 || y1 >= y2)
+ return;
+
+ off = (size_t)x1 * stream->bytes_pix;
+ span = (size_t)(x2 - x1) * stream->bytes_pix;
+
+ for (int y = y1; y < y2; y++) {
+ size_t start = round_down((size_t)y * line_len + off, 8);
+ size_t end = min_t(size_t, round_up((size_t)y * line_len + off + span, 8),
+ stream->buf_size);
+
+ memcpy(dst + start, stream->shadow + start, end - start);
+ }
+}
+
+/*
+ * Fill one URB buffer with what should be on screen now, the shadow frame. A
+ * buffer that is only a few generations behind replays the damage rectangles
+ * since, rather than copying the whole frame.
+ */
+static void fl2000_stream_build(struct fl2000_stream *stream, struct fl2000_stream_buf *sb)
+{
+ /*
+ * Never block here. fl2000_stream_compress() holds the mutex for as
+ * long as one conversion takes, and waiting for it stops URB
+ * submission for that whole time: the device runs out of data and the
+ * monitor blinks. Resend instead, but never a frame older than the
+ * newest one already sent, or the picture would step back in time.
+ */
+ if (!mutex_trylock(&stream->shadow_lock)) {
+ struct fl2000_stream_buf *newest = stream->newest_sb;
+
+ atomic_inc(&stream->stalled);
+
+ if (newest && newest != sb && sb->seq != newest->seq) {
+ memcpy(sb->vaddr, newest->vaddr, stream->buf_size);
+ sb->seq = newest->seq;
+ }
+
+ return;
+ }
+
+ if (!stream->shadow) {
+ mutex_unlock(&stream->shadow_lock);
+ return;
+ }
+
+ if (sb->seq != stream->shadow_seq) {
+ u32 behind = stream->shadow_seq - sb->seq;
+
+ if (!sb->seq || behind >= FL2000_DAMAGE_LOG) {
+ memcpy(sb->vaddr, stream->shadow, stream->buf_size);
+ } else {
+ /*
+ * Replay the rectangles one by one rather than their
+ * bounding box: damage in two opposite corners would
+ * otherwise cost a full-frame copy.
+ */
+ for (u32 seq = sb->seq + 1; seq != stream->shadow_seq + 1; seq++)
+ fl2000_copy_rect(stream, sb->vaddr,
+ &stream->damage_log[seq % FL2000_DAMAGE_LOG]);
+ }
+
+ sb->seq = stream->shadow_seq;
+ }
+
+ stream->newest_sb = sb;
+ mutex_unlock(&stream->shadow_lock);
+}
+
+static void fl2000_stream_work(struct work_struct *work)
+{
+ int ret;
+ struct fl2000_stream *stream = container_of(work, struct fl2000_stream, work);
+ struct usb_device *usb_dev = stream->usb_dev;
+ struct fl2000_stream_buf *cur_sb;
+ struct urb *data_urb, *eof_urb;
+
+ while (READ_ONCE(stream->enabled)) {
+ /* Interruptible sleep: an idle stream may wait here indefinitely */
+ ret = wait_event_interruptible(stream->credits_wq, !READ_ONCE(stream->enabled) ||
+ atomic_add_unless(&stream->credits, -1, 0));
+ if (ret) {
+ dev_err(&usb_dev->dev, "Work interrupt error %d\n", ret);
+ WRITE_ONCE(stream->enabled, false);
+ return;
+ }
+ if (!READ_ONCE(stream->enabled))
+ break;
+
+ fl2000_stream_report(stream);
+
+ spin_lock_irq(&stream->list_lock);
+ if (list_empty(&stream->render_list)) {
+ /*
+ * No buffer available - should not happen. Give the
+ * credit back: only completions replenish them, so one
+ * taken without submitting a URB is lost for good.
+ */
+ spin_unlock_irq(&stream->list_lock);
+ atomic_inc(&stream->stalled);
+ atomic_inc(&stream->credits);
+ usleep_range(1000, 2000);
+ continue;
+ }
+ cur_sb = list_first_entry(&stream->render_list, struct fl2000_stream_buf, list);
+ list_del_init(&cur_sb->list);
+ spin_unlock_irq(&stream->list_lock);
+
+ /*
+ * Building the frame copies up to several MB and must run with
+ * interrupts enabled, so cur_sb is owned (off all lists) for
+ * the duration. Holding list_lock (which disables IRQs) across
+ * it starves the whole system.
+ */
+ fl2000_stream_build(stream, cur_sb);
+
+ spin_lock_irq(&stream->list_lock);
+ list_add_tail(&cur_sb->list, &stream->wait_list);
+ spin_unlock_irq(&stream->list_lock);
+
+ data_urb = usb_alloc_urb(0, GFP_KERNEL);
+ eof_urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!data_urb || !eof_urb) {
+ usb_free_urb(data_urb);
+ usb_free_urb(eof_urb);
+ dev_err(&usb_dev->dev, "Data URB allocation error\n");
+ WRITE_ONCE(stream->enabled, false);
+ return;
+ }
+
+ /*
+ * The frame was written through the vmap of the pages the URB sends: where
+ * caches can alias, DMA mapping the pages does not see those writes.
+ */
+ flush_kernel_vmap_range(cur_sb->vaddr, stream->buf_size);
+
+ /* Endpoint 1 bulk out, the data is in the scatterlist */
+ usb_fill_bulk_urb(data_urb, usb_dev, usb_sndbulkpipe(usb_dev, 1), NULL,
+ (int)stream->buf_size, fl2000_stream_data_completion, cur_sb);
+ data_urb->interval = 0;
+ data_urb->sg = cur_sb->sgt.sgl;
+ data_urb->num_sgs = cur_sb->sgt.nents;
+
+ /*
+ * The device ends a frame at a zero-length packet
+ * (FL2000_VGA_CTRL_ACLK_USE_ZERO_PKT_LEN), so send one of its
+ * own after every frame, as the vendor driver does.
+ * URB_ZERO_PACKET is not enough: it only adds the packet when
+ * the frame is a whole number of packets. Any other frame
+ * (1366x768 at 3 bytes per pixel is 3073.5 packets of 1024
+ * bytes) ends in a short packet, the device never sees the end
+ * of the frame, and the screen stays black while the device
+ * drains data at one pixel per clock, blanking included.
+ */
+ usb_fill_bulk_urb(eof_urb, usb_dev, usb_sndbulkpipe(usb_dev, 1), NULL, 0,
+ fl2000_stream_eof_completion, NULL);
+
+ ret = fl2000_stream_submit(stream, data_urb);
+ if (ret)
+ usb_free_urb(eof_urb);
+ else
+ ret = fl2000_stream_submit(stream, eof_urb);
+ if (ret) {
+ /* The USB core disables the endpoint before the disconnect callback */
+ if (ret != -ENODEV && ret != -ENOENT && ret != -ESHUTDOWN)
+ dev_err(&usb_dev->dev, "Data URB error %d\n", ret);
+ WRITE_ONCE(stream->enabled, false);
+ }
+ }
+}
+
+/*
+ * The converters write through the frame swizzle described above. 'frame' is
+ * the start of the destination frame, 'line' the offset of the destination line
+ * in it (in bytes for 24bpp, in 16-bit words for 16bpp) and the x range is
+ * absolute. 'sbuf' points at the first pixel of the span in the staging line.
+ */
+static void fl2000_xrgb8888_to_rgb888_line(u8 *frame, size_t line, const u32 *sbuf, u32 x_start,
+ u32 x_end)
+{
+ size_t xx = line + (size_t)x_start * 3;
+
+ for (unsigned int x = x_start; x < x_end; x++) {
+ u32 pix = *sbuf++;
+
+ frame[xx++ ^ 4] = (pix & 0x000000FF) >> 0;
+ frame[xx++ ^ 4] = (pix & 0x0000FF00) >> 8;
+ frame[xx++ ^ 4] = (pix & 0x00FF0000) >> 16;
+ }
+}
+
+static void fl2000_rgb565_to_frame_line(u16 *frame, size_t line, const u16 *sbuf, u32 x_start,
+ u32 x_end)
+{
+ for (unsigned int x = x_start; x < x_end; x++)
+ frame[(line + x) ^ 2] = *sbuf++;
+}
+
+/*
+ * Pull one span of source pixels into the cached line buffer.
+ *
+ * A framebuffer imported from the primary GPU is mapped write-combining,
+ * whether it sits in VRAM (I/O memory) or in system memory pinned for us,
+ * and a plain memcpy reads it at ~100 MB/s: every access is a partial
+ * cache-line transaction. drm_memcpy_from_wc() uses streaming loads to fetch
+ * whole lines, about ten times faster. On our own cached shmem buffers a
+ * non-temporal load is slower than a plain memcpy, so choose per source.
+ *
+ * Streaming loads move whole 16 bytes and fall back to the slow plain read for
+ * any other length, and a line of 1366 pixels is 5464 bytes (78 ms instead of
+ * 8 ms per frame). From a 16-byte aligned address the length can be rounded
+ * up without leaving the buffer, whose size is whole pages; the pixels past
+ * the span land in the line buffer but are not converted.
+ */
+static void fl2000_read_span(struct fl2000_stream *stream, const struct iosys_map *src_map,
+ bool wc, size_t offset, size_t len)
+{
+ struct iosys_map dst = IOSYS_MAP_INIT_VADDR(stream->line_buf);
+ struct iosys_map src = *src_map;
+ uintptr_t addr;
+
+ if (!wc) {
+ iosys_map_memcpy_from(stream->line_buf, src_map, offset, len);
+ return;
+ }
+
+ iosys_map_incr(&src, offset);
+ addr = src.is_iomem ? (uintptr_t)src.vaddr_iomem : (uintptr_t)src.vaddr;
+ if (IS_ALIGNED(addr, 16))
+ len = ALIGN(len, 16);
+ drm_memcpy_from_wc(&dst, &src, len);
+}
+
+/*
+ * Convert one damaged rectangle of the source framebuffer into the shadow
+ * frame, from RGB565 if the device is sent that and from XRGB8888 otherwise.
+ * The shadow is the only thing written here, so it is always the
+ * complete current frame: an update can never be lost because no URB buffer
+ * happened to be free.
+ *
+ * @src is the part of the framebuffer that the mode shows, as the framebuffer
+ * can be larger than the mode, and @clip the damage. Both are in whole pixels of
+ * the framebuffer.
+ */
+void fl2000_stream_compress(struct fl2000_stream *stream, const struct iosys_map *src_map,
+ bool imported, unsigned int pitch,
+ const struct drm_rect *src, const struct drm_rect *clip)
+{
+ unsigned int src_cpp = stream->bytes_pix == 2 ? 2 : sizeof(u32);
+ unsigned int line_px = 16 / src_cpp;
+ bool wc = imported || src_map->is_iomem;
+ struct drm_rect rect = *clip;
+ u32 dst_line_len;
+ u32 x_start, x_end, y_start, y_end;
+ size_t span;
+ int x1, x2;
+
+ if (!drm_rect_intersect(&rect, src))
+ return;
+
+ guard(mutex)(&stream->shadow_lock);
+
+ if (!stream->shadow || drm_rect_width(src) != stream->width ||
+ drm_rect_height(src) != stream->height)
+ return;
+
+ /*
+ * Streaming loads need 16-byte alignment, so the read may start and end
+ * a few pixels out of the damage. Those pixels are not converted: only
+ * the damage is known to be current.
+ */
+ x1 = rect.x1 & ~(line_px - 1);
+ if (x1 < src->x1)
+ x1 = rect.x1;
+ x2 = min_t(int, ALIGN(rect.x2, line_px), src->x2);
+
+ /* From here on the coordinates are those of the frame */
+ x_start = rect.x1 - src->x1;
+ x_end = rect.x2 - src->x1;
+ y_start = rect.y1 - src->y1;
+ y_end = rect.y2 - src->y1;
+
+ dst_line_len = stream->width * stream->bytes_pix;
+ span = (x2 - x1) * src_cpp;
+
+ for (unsigned int y = y_start; y < y_end; y++) {
+ /*
+ * Pull the span into a cached line buffer in one bulk transfer
+ * and convert from there: converting straight from an uncached
+ * source issues one read per pixel.
+ */
+ fl2000_read_span(stream, src_map, wc,
+ (size_t)(y + src->y1) * pitch + (size_t)x1 * src_cpp, span);
+
+ switch (stream->bytes_pix) {
+ case 2:
+ fl2000_rgb565_to_frame_line(stream->shadow, (size_t)y * stream->width,
+ (const u16 *)stream->line_buf + rect.x1 - x1,
+ x_start, x_end);
+ break;
+ case 3:
+ fl2000_xrgb8888_to_rgb888_line(stream->shadow, (size_t)y * dst_line_len,
+ stream->line_buf + (rect.x1 - x1),
+ x_start, x_end);
+ break;
+ default:
+ break;
+ }
+ }
+
+ stream->shadow_seq++;
+ stream->damage_log[stream->shadow_seq % FL2000_DAMAGE_LOG] =
+ (struct drm_rect){ x_start, y_start, x_end, y_end };
+}
+
+int fl2000_stream_mode_set(struct fl2000_stream *stream, unsigned int width,
+ unsigned int height, u32 bytes_pix)
+{
+ int ret;
+ unsigned int size;
+
+ /* Round buffer size up to multiple of 8 to meet HW expectations */
+ size = ALIGN(width * height * bytes_pix, 8);
+
+ guard(mutex)(&stream->shadow_lock);
+
+ /* If there are buffers with same size - keep them */
+ if (stream->buf_size != size) {
+ /*
+ * Without a shadow nothing writes a frame, and an allocation that fails below
+ * must not leave the old, smaller one to be written with the new geometry.
+ */
+ fl2000_stream_put_buffers(stream);
+ vfree(stream->shadow);
+ stream->shadow = NULL;
+ stream->buf_size = 0;
+
+ ret = fl2000_stream_get_buffers(stream, size);
+ if (ret)
+ return ret;
+
+ /* One full frame in the device format; only the CPU ever reads it */
+ stream->shadow = vzalloc(size);
+ if (!stream->shadow) {
+ fl2000_stream_put_buffers(stream);
+ return -ENOMEM;
+ }
+
+ /* A buffer carrying zero has never been built and takes the full-copy path */
+ stream->shadow_seq++;
+
+ stream->buf_size = size;
+ }
+
+ stream->bytes_pix = bytes_pix;
+ stream->width = width;
+ stream->height = height;
+
+ return 0;
+}
+
+int fl2000_stream_enable(struct fl2000_stream *stream)
+{
+ /* The mode set could not allocate the buffers, and has said so */
+ if (list_empty(&stream->render_list))
+ return -ENOMEM;
+
+ stream->last_report = ktime_get();
+ atomic_set(&stream->frames, 0);
+ atomic_set(&stream->rendered, 0);
+ atomic_set(&stream->stalled, 0);
+ atomic_set(&stream->conv_us_total, 0);
+ atomic_set(&stream->conv_us_max, 0);
+ atomic_set(&stream->damage_px_total, 0);
+
+ /* Start the transmit work with the minimum number of buffers in flight */
+ atomic_set(&stream->credits, FL2000_SB_MIN);
+ WRITE_ONCE(stream->enabled, true);
+ queue_work(stream->work_queue, &stream->work);
+
+ return 0;
+}
+
+void fl2000_stream_disable(struct fl2000_stream *stream)
+{
+ struct fl2000_stream_buf *cur_sb;
+
+ /* Wake the work up even when no URB is in flight to complete */
+ WRITE_ONCE(stream->enabled, false);
+ wake_up(&stream->credits_wq);
+
+ drain_workqueue(stream->work_queue);
+
+ if (!usb_wait_anchor_empty_timeout(&stream->anchor, 1000))
+ usb_kill_anchored_urbs(&stream->anchor);
+
+ spin_lock_irq(&stream->list_lock);
+ while (!list_empty(&stream->wait_list)) {
+ cur_sb = list_first_entry(&stream->wait_list, struct fl2000_stream_buf, list);
+ list_move_tail(&cur_sb->list, &stream->render_list);
+ }
+ spin_unlock_irq(&stream->list_lock);
+}
+
+/**
+ * fl2000_stream_create() - streaming processing context creation
+ * @usb_dev: FL2000 USB device
+ *
+ * Called when the DRM device is set up. It does not initiate any USB transfers:
+ * buffers and URBs are allocated later, once the mode (and thus the frame size) is
+ * known.
+ *
+ * Return: stream context, or an ERR_PTR() on failure
+ */
+struct fl2000_stream *fl2000_stream_create(struct usb_device *usb_dev)
+{
+ int ret;
+ struct fl2000_stream *stream;
+
+ /* The probe selected altsetting 1 on interface 0 */
+ ret = usb_pipe_type_check(usb_dev, usb_sndbulkpipe(usb_dev, 1));
+ if (ret) {
+ dev_err(&usb_dev->dev, "No bulk endpoint to send frames to\n");
+ return ERR_PTR(ret);
+ }
+
+ stream = kzalloc_obj(*stream);
+ if (!stream)
+ return ERR_PTR(-ENOMEM);
+
+ INIT_WORK(&stream->work, &fl2000_stream_work);
+ INIT_LIST_HEAD(&stream->render_list);
+ INIT_LIST_HEAD(&stream->wait_list);
+ spin_lock_init(&stream->list_lock);
+ mutex_init(&stream->shadow_lock);
+ init_usb_anchor(&stream->anchor);
+ atomic_set(&stream->credits, 0);
+ init_waitqueue_head(&stream->credits_wq);
+ stream->usb_dev = usb_dev;
+
+ /* One scanline of source pixels, for the widest mode we advertise */
+ stream->line_buf = kmalloc_objs(*stream->line_buf, FL2000_MAX_LINE_PIXELS);
+ stream->work_queue = alloc_ordered_workqueue("fl2000_stream", 0);
+ if (!stream->line_buf || !stream->work_queue) {
+ fl2000_stream_destroy(stream);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ return stream;
+}
+
+void fl2000_stream_destroy(struct fl2000_stream *stream)
+{
+ if (stream->work_queue) {
+ fl2000_stream_disable(stream);
+ destroy_workqueue(stream->work_queue);
+ }
+ fl2000_stream_put_buffers(stream);
+ vfree(stream->shadow);
+ kfree(stream->line_buf);
+ mutex_destroy(&stream->shadow_lock);
+ kfree(stream);
+}
--
2.56.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH v2 4/4] drm/fl2000: Add KMS display pipeline
2026-11-12 3:34 [PATCH v2 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
` (2 preceding siblings ...)
2026-11-12 3:34 ` [PATCH v2 3/4] drm/fl2000: Add frame streaming Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-12 3:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
3 siblings, 0 replies; 5+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-12 3:34 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Angel Gabriel Crispin Valdivia
Cc: linux-kernel, dri-devel
From: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
Register the DRM device from the probe, and connect the pieces:
- a primary plane, a CRTC and an encoder, with GEM SHMEM framebuffers
accessed through the shadow-plane helpers. The device takes RGB888 or
RGB565: XRGB8888 is converted to the former and RGB565 goes as it is,
and the plane rejects what the USB link cannot carry at the mode. The
framebuffer is only read by the CPU, so it does not need to be
physically contiguous. The primary plane converts each damaged
rectangle into the stream's shadow frame
- mode validation and PLL search in mode_valid and mode_fixup (which
adjusts the clock), and FL2000 programming in mode_set_nofb, before the
bridge is enabled. The PLL cannot make most pixel clocks exactly:
digital sinks are only offered modes it hits within 500 ppm, while the
VGA output accepts 5000 ppm, since an analog monitor does not notice
that but does notice a changed htotal
- the HDMI connector from drm_bridge_connector_init() on the encoder the
IT66121 bridge is attached to. It is polled, as the bridge has no hot
plug interrupt of its own
- the analog VGA output of the chip, which adapters with a VGA
connector expose next to the HDMI one: a DAC encoder and a VGA
connector on the same CRTC (the two encoders are clones of each
other). The chip has one monitor sense input, which follows
whichever monitor is plugged in, so the VGA connector reports a
monitor when the chip senses one and the IT66121 does not; the EDID
is read with the FL2000 I2C engine, and without one the standard
timings are offered
- fbdev emulation
- the FL2000 interrupt endpoint, which reports monitor and IT66121
events and triggers a hotplug event
- system sleep and USB resets, which stop the display and start it again
without unbinding: the compositor does not see an unplug. The chips
may have lost power, so they are set up from scratch
Tested on x86_64 with a UGREEN USB 3.0 adapter with HDMI and VGA
connectors (FL2000DX + IT66121FN): a 1920x1080 monitor on HDMI and an
old 1366x768 monitor without a usable EDID on VGA. EDID, mode setting
in XRGB8888 and RGB565, plugging and unplugging monitors, system sleep,
USB resets and a GNOME Wayland desktop work, with one frame per refresh
on the link. Adapters with a VGA connector only (no IT66121) are not
supported.
Signed-off-by: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
---
drivers/gpu/drm/fl2000/Makefile | 5 +-
drivers/gpu/drm/fl2000/fl2000.h | 33 ++
drivers/gpu/drm/fl2000/fl2000_drm.c | 902 ++++++++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_drv.c | 56 +-
drivers/gpu/drm/fl2000/fl2000_interrupt.c | 226 ++++++++
drivers/gpu/drm/fl2000/fl2000_vga.c | 223 ++++++++
6 files changed, 1437 insertions(+), 8 deletions(-)
diff --git a/drivers/gpu/drm/fl2000/Makefile b/drivers/gpu/drm/fl2000/Makefile
index 0671c867e..e3e9cb72b 100644
--- a/drivers/gpu/drm/fl2000/Makefile
+++ b/drivers/gpu/drm/fl2000/Makefile
@@ -1,10 +1,13 @@
# SPDX-License-Identifier: GPL-2.0-only
fl2000-y := \
+ fl2000_drm.o \
fl2000_drv.o \
fl2000_i2c.o \
+ fl2000_interrupt.o \
fl2000_it66121.o \
fl2000_registers.o \
- fl2000_streaming.o
+ fl2000_streaming.o \
+ fl2000_vga.o
obj-$(CONFIG_DRM_FL2000) += fl2000.o
diff --git a/drivers/gpu/drm/fl2000/fl2000.h b/drivers/gpu/drm/fl2000/fl2000.h
index e6a7de0f7..9a8ee7927 100644
--- a/drivers/gpu/drm/fl2000/fl2000.h
+++ b/drivers/gpu/drm/fl2000/fl2000.h
@@ -100,6 +100,30 @@ void fl2000_stream_note_render(struct fl2000_stream *stream, unsigned int conv_u
int fl2000_stream_enable(struct fl2000_stream *stream);
void fl2000_stream_disable(struct fl2000_stream *stream);
+/* Interrupt polling task */
+struct fl2000_intr;
+struct fl2000_intr *fl2000_intr_create(struct usb_device *usb_dev, struct regmap *regmap,
+ struct drm_device *drm);
+void fl2000_intr_quiesce(struct fl2000_intr *intr);
+void fl2000_intr_suspend(struct fl2000_intr *intr);
+int fl2000_intr_start(struct fl2000_intr *intr);
+int fl2000_intr_monitor_connected(struct fl2000_intr *intr);
+void fl2000_intr_destroy(struct fl2000_intr *intr);
+
+/* VGA output: a DAC encoder and connector next to the HDMI ones, on the same CRTC */
+struct fl2000_vga {
+ struct drm_encoder encoder;
+ struct drm_connector connector;
+ struct i2c_adapter *adapter;
+ struct fl2000_intr *intr;
+ /* The HDMI transmitter shares the chip's monitor sense with this output */
+ struct drm_bridge *hdmi_bridge;
+};
+
+int fl2000_vga_init(struct drm_device *drm, struct fl2000_vga *vga, struct drm_crtc *crtc,
+ struct drm_encoder *hdmi_encoder, struct i2c_adapter *adapter,
+ struct fl2000_intr *intr);
+
/* I2C adapter interface creation */
struct i2c_adapter *fl2000_i2c_init(struct usb_interface *interface, struct regmap *regmap);
int fl2000_ddc_read_block(struct i2c_adapter *adapter, u8 *buf, unsigned int block, size_t len);
@@ -123,4 +147,13 @@ int fl2000_stream_interrupts(struct regmap *regmap, bool enable);
int fl2000_read_status(struct regmap *regmap, u32 *status);
int fl2000_i2c_dword(struct regmap *regmap, bool read, u16 addr, u8 offset, u32 *data);
+/* DRM device creation */
+struct fl2000_device;
+struct fl2000_device *fl2000_drm_create(struct usb_interface *interface, struct regmap *regmap,
+ struct i2c_adapter *adapter, struct drm_bridge *bridge);
+void fl2000_drm_destroy(struct fl2000_device *fdev);
+void fl2000_drm_hotplug(struct drm_device *drm, u32 events);
+int fl2000_drm_suspend(struct fl2000_device *fdev);
+int fl2000_drm_resume(struct fl2000_device *fdev);
+
#endif /* FL2000_H */
diff --git a/drivers/gpu/drm/fl2000/fl2000_drm.c b/drivers/gpu/drm/fl2000/fl2000_drm.c
new file mode 100644
index 000000000..04efcf583
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_drm.c
@@ -0,0 +1,902 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2012, Red Hat
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#include <linux/bitfield.h>
+#include <linux/math64.h>
+
+#include <drm/clients/drm_client_setup.h>
+#include <drm/drm_bridge_connector.h>
+#include <drm/drm_connector.h>
+#include <drm/drm_fbdev_shmem.h>
+#include <drm/drm_gem_atomic_helper.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_modeset_helper.h>
+#include <drm/drm_probe_helper.h>
+
+#include "fl2000.h"
+#include "fl2000_it66121.h"
+
+#define DRM_DRIVER_NAME "fl2000"
+#define DRM_DRIVER_DESC "Fresco Logic FL2000 USB display"
+
+#define DRM_DRIVER_MAJOR 0
+#define DRM_DRIVER_MINOR 0
+#define DRM_DRIVER_PATCHLEVEL 1
+
+/* What the timing registers can hold */
+#define FL2000_MAX_WIDTH FIELD_MAX(FL2000_VGA_HSYNC1_HACTIVE)
+#define FL2000_MAX_HEIGHT FIELD_MAX(FL2000_VGA_VSYNC1_VACTIVE)
+#define FL2000_MAX_HTOTAL FIELD_MAX(FL2000_VGA_HSYNC1_HTOTAL)
+#define FL2000_MAX_VTOTAL FIELD_MAX(FL2000_VGA_VSYNC1_VTOTAL)
+
+/*
+ * RGB565 goes to the device as it is. RGB888 is what it takes otherwise, and XRGB8888, which all of
+ * userspace expects, is converted to it.
+ */
+static const u32 fl2000_pixel_formats[] = {
+ DRM_FORMAT_XRGB8888,
+ DRM_FORMAT_RGB565,
+};
+
+/*
+ * Maximum pixel clock set to 500MHz. It is hard to get more or less precise PLL configuration for
+ * higher clock.
+ */
+#define FL2000_MAX_PIXCLOCK 500000000
+
+/* PLL computing precision is 6 digits after the decimal point */
+#define FL2000_PLL_PRECISION 1000000
+
+/* Input xtal clock, Hz */
+#define FL2000_XTAL 10000000 /* 10 MHz */
+
+/* Internal vco clock min/max, Hz */
+#define FL2000_VCOCLOCK_MIN 62500000 /* 62.5 MHz */
+#define FL2000_VCOCLOCK_MAX 1000000000 /* 1 GHz */
+
+/*
+ * Maximum acceptable ppm error of the pixel clock. Digital sinks get the exact clock or nothing;
+ * an analog monitor locks to the line rate of whatever it is sent, so a clock off by half a
+ * percent goes unnoticed, while changing the length of a line (htotal) throws its sampling off.
+ */
+#define FL2000_PPM_ERR_MAX 500
+#define FL2000_PPM_ERR_MAX_VGA 5000
+
+/*
+ * Bulk payload the link sustains, bytes per second. High speed carries at most 13 packets of 512
+ * bytes per 125 us microframe (53 MB/s); SuperSpeed 500 MB/s after its 8b/10b encoding. Packet and
+ * link overhead are taken as 10 %. On SuperSpeed 1920x1080 at 60 Hz in RGB888 (373 MB/s) streams
+ * fine, and the vendor driver offers 1920x1200 at 60 Hz in RGB888 (415 MB/s). The FL2000 is a
+ * 5 Gb/s device: it never runs at SuperSpeed Plus.
+ */
+#define FL2000_BULK_BW_HIGH_SPEED (53248000ull * 90 / 100)
+#define FL2000_BULK_BW_SUPER_SPEED (500000000ull * 90 / 100)
+
+/* Bytes per pixel that the device is sent for a framebuffer */
+static unsigned int fl2000_fb_bytes_pix(const struct drm_framebuffer *fb)
+{
+ return fb->format->format == DRM_FORMAT_RGB565 ? 2 : 3;
+}
+
+/*
+ * Can the USB link carry @mode at @bytes_pix bytes per pixel? The FL2000 has a line buffer and no
+ * frame buffer: it takes the active pixels of exactly one frame per refresh, so the stream needs
+ * the rate of the active area, not the pixel clock.
+ */
+static bool fl2000_link_fits(enum usb_device_speed speed, const struct drm_display_mode *mode,
+ unsigned int bytes_pix)
+{
+ u32 total = (u32)mode->htotal * mode->vtotal;
+ u64 max_bw, rate;
+
+ switch (speed) {
+ case USB_SPEED_HIGH:
+ max_bw = FL2000_BULK_BW_HIGH_SPEED;
+ break;
+ case USB_SPEED_SUPER:
+ case USB_SPEED_SUPER_PLUS:
+ max_bw = FL2000_BULK_BW_SUPER_SPEED;
+ break;
+ default:
+ return false;
+ }
+
+ if (!total)
+ return false;
+
+ /* Active pixels per second; at most 500 MHz x 4095 x 4095 (see fl2000_mode_check()) */
+ rate = div_u64((u64)mode->clock * 1000 * mode->hdisplay * mode->vdisplay, total);
+
+ return rate * bytes_pix <= max_bw;
+}
+
+struct fl2000_device {
+ struct usb_device *usb_dev;
+ struct drm_device drm;
+ struct drm_plane plane;
+ struct drm_crtc crtc;
+ struct drm_encoder encoder;
+ struct fl2000_vga vga;
+ /*
+ * Lives as long as the DRM device, not the USB one: a commit that was on its way when the
+ * adapter was unplugged can still reach it, with userspace holding the device open.
+ */
+ struct fl2000_stream *stream;
+ struct fl2000_intr *intr;
+ struct regmap *regmap;
+ struct i2c_adapter *adapter;
+ struct drm_bridge *bridge;
+};
+
+static inline struct fl2000_device *to_fl2000_device(struct drm_device *drm)
+{
+ return container_of(drm, struct fl2000_device, drm);
+}
+
+DEFINE_DRM_GEM_FOPS(fl2000_drm_driver_fops);
+
+/*
+ * Mode config cleanup is managed by drmm_mode_config_init(), and the display
+ * pipeline is shut down on unbind, so no .release callback is needed.
+ */
+static struct drm_driver fl2000_drm_driver = {
+ .driver_features = DRIVER_ATOMIC | DRIVER_GEM | DRIVER_MODESET,
+ .fops = &fl2000_drm_driver_fops,
+
+ .dumb_create = drm_gem_shmem_dumb_create,
+ .gem_prime_import = drm_gem_prime_import,
+ .gem_prime_import_sg_table = drm_gem_shmem_prime_import_sg_table,
+ DRM_FBDEV_SHMEM_DRIVER_OPS,
+
+ .name = DRM_DRIVER_NAME,
+ .desc = DRM_DRIVER_DESC,
+ .major = DRM_DRIVER_MAJOR,
+ .minor = DRM_DRIVER_MINOR,
+ .patchlevel = DRM_DRIVER_PATCHLEVEL,
+};
+
+/*
+ * What the device is sent is set up along with the mode, and the stream sizes its buffers from it:
+ * a framebuffer that changes it takes a full mode set.
+ */
+static int fl2000_atomic_check(struct drm_device *drm, struct drm_atomic_commit *state)
+{
+ struct drm_plane *plane = &to_fl2000_device(drm)->plane;
+ const struct drm_plane_state *new_state = drm_atomic_get_new_plane_state(state, plane);
+ const struct drm_plane_state *old_state = drm_atomic_get_old_plane_state(state, plane);
+
+ if (new_state && new_state->crtc && new_state->fb &&
+ (!old_state->fb ||
+ fl2000_fb_bytes_pix(old_state->fb) != fl2000_fb_bytes_pix(new_state->fb))) {
+ struct drm_crtc_state *crtc_state = drm_atomic_get_crtc_state(state,
+ new_state->crtc);
+
+ if (IS_ERR(crtc_state))
+ return PTR_ERR(crtc_state);
+
+ crtc_state->mode_changed = true;
+ }
+
+ return drm_atomic_helper_check(drm, state);
+}
+
+static const struct drm_mode_config_funcs fl2000_mode_config_funcs = {
+ .fb_create = drm_gem_fb_create_with_dirty,
+ .atomic_check = fl2000_atomic_check,
+ .atomic_commit = drm_atomic_helper_commit,
+};
+
+/* Integer division compute of ppm error */
+static u64 fl2000_pll_ppm_err(u64 clock_mil, u32 vco_clk, u32 divisor)
+{
+ u64 pll_clk_mil = div_u64((u64)vco_clk * FL2000_PLL_PRECISION, divisor);
+ u64 pll_clk_err;
+
+ /* Not using abs() here to avoid a possible overflow */
+ if (pll_clk_mil > clock_mil)
+ pll_clk_err = pll_clk_mil - clock_mil;
+ else
+ pll_clk_err = clock_mil - pll_clk_mil;
+
+ return div64_u64(pll_clk_err, div_u64(clock_mil, FL2000_PLL_PRECISION));
+}
+
+static inline u32 fl2000_pll_get_divisor(u64 clock_mil, u32 vco_clk, u64 *min_ppm_err)
+{
+ static const u32 divisor_arr[] = {
+ 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
+ 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
+ 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
+ 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
+ 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115,
+ 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128
+ };
+ u32 best_divisor = 0;
+
+ for (int i = 0; i < ARRAY_SIZE(divisor_arr); i++) {
+ u32 divisor = divisor_arr[i];
+ u64 ppm_err = fl2000_pll_ppm_err(clock_mil, vco_clk, divisor);
+
+ if (ppm_err < *min_ppm_err) {
+ *min_ppm_err = ppm_err;
+ best_divisor = divisor;
+ }
+ }
+
+ return best_divisor;
+}
+
+/* Try to match pixel clock - find parameters with minimal PLL error */
+static u64 fl2000_pll_calc(u64 clock_mil, struct fl2000_pll *pll, u32 *clock_calculated)
+{
+ static const u32 prescaler_max = 2;
+ static const u32 multiplier_max = 128;
+ u64 min_ppm_err = (u64)(-1);
+
+ for (u32 prescaler = 1; prescaler <= prescaler_max; prescaler++)
+ for (u32 multiplier = 1; multiplier <= multiplier_max; multiplier++) {
+ /* Do not need precision here yet, no 10^6 multiply */
+ u32 vco_clk = FL2000_XTAL / prescaler * multiplier;
+ u32 divisor;
+
+ if (vco_clk < FL2000_VCOCLOCK_MIN || vco_clk > FL2000_VCOCLOCK_MAX)
+ continue;
+
+ divisor = fl2000_pll_get_divisor(clock_mil, vco_clk, &min_ppm_err);
+ if (divisor == 0)
+ continue;
+
+ pll->prescaler = prescaler;
+ pll->multiplier = multiplier;
+ pll->divisor = divisor;
+ if (vco_clk < 125000000)
+ pll->function = 0;
+ else if (vco_clk < 250000000)
+ pll->function = 1;
+ else if (vco_clk < 500000000)
+ pll->function = 2;
+ else
+ pll->function = 3;
+ *clock_calculated = vco_clk / divisor;
+ }
+
+ return min_ppm_err;
+}
+
+/*
+ * Check a mode against the limits of the chip and find its PLL setting, accepting a pixel clock off
+ * by up to @max_ppm parts per million. @clock is what the PLL really makes, in Hz. The timings of
+ * the mode are never touched.
+ */
+static enum drm_mode_status fl2000_mode_check(const struct drm_display_mode *mode,
+ struct fl2000_pll *pll, u32 *clock, u64 max_ppm)
+{
+ /*
+ * The core checks only the modes it probes against mode_config's limits. A mode set by
+ * userspace gets here as it is, and the stream sizes its buffers from it: a mode the
+ * primary plane does not even cover could make that size overflow.
+ */
+ if (mode->hdisplay > FL2000_MAX_WIDTH || mode->htotal > FL2000_MAX_HTOTAL)
+ return MODE_BAD_HVALUE;
+
+ if (mode->vdisplay > FL2000_MAX_HEIGHT || mode->vtotal > FL2000_MAX_VTOTAL)
+ return MODE_BAD_VVALUE;
+
+ if (mode->clock > FL2000_MAX_PIXCLOCK / 1000)
+ return MODE_CLOCK_HIGH;
+
+ /* To keep precision use clock multiplied by 10^6 */
+ if (fl2000_pll_calc((u64)mode->clock * 1000 * FL2000_PLL_PRECISION, pll, clock) >= max_ppm)
+ return MODE_NOCLOCK;
+
+ return MODE_OK;
+}
+
+static enum drm_mode_status fl2000_crtc_mode_valid(struct drm_crtc *crtc,
+ const struct drm_display_mode *mode)
+{
+ struct fl2000_pll pll;
+ enum drm_mode_status status;
+ u32 clock;
+
+ status = fl2000_mode_check(mode, &pll, &clock, FL2000_PPM_ERR_MAX_VGA);
+ if (status != MODE_OK)
+ return status;
+
+ /* Whatever the framebuffer: the plane checks the format it has */
+ if (!fl2000_link_fits(to_fl2000_device(crtc->dev)->usb_dev->speed, mode, 2))
+ return MODE_CLOCK_HIGH;
+
+ return MODE_OK;
+}
+
+/* The clock of the mode becomes the one the PLL makes for it */
+static bool fl2000_crtc_mode_fixup(struct drm_crtc *crtc, const struct drm_display_mode *mode,
+ struct drm_display_mode *adjusted_mode)
+{
+ struct fl2000_pll pll;
+ u32 clock;
+
+ if (fl2000_mode_check(mode, &pll, &clock, FL2000_PPM_ERR_MAX_VGA) != MODE_OK)
+ return false;
+
+ adjusted_mode->clock = clock / 1000;
+
+ return true;
+}
+
+/*
+ * Primary plane
+ */
+
+static const u64 fl2000_plane_format_modifiers[] = {
+ DRM_FORMAT_MOD_LINEAR,
+ DRM_FORMAT_MOD_INVALID,
+};
+
+static int fl2000_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 = NULL;
+ int ret;
+
+ if (new_plane_state->crtc)
+ new_crtc_state = drm_atomic_get_new_crtc_state(state, new_plane_state->crtc);
+
+ 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->visible &&
+ !fl2000_link_fits(to_fl2000_device(plane->dev)->usb_dev->speed,
+ &new_crtc_state->adjusted_mode,
+ fl2000_fb_bytes_pix(new_plane_state->fb)))
+ return -EINVAL;
+
+ return 0;
+}
+
+/*
+ * Only the damaged rectangles are converted, into the stream's shadow frame,
+ * which is the one persistent copy every outgoing buffer is built from. One by
+ * one rather than their bounding box: damage in two opposite corners would cost
+ * a whole frame, and a compositor copying into a rotation of buffers (Mutter
+ * 51.1) only keeps the damage current.
+ */
+static void fl2000_plane_atomic_update(struct drm_plane *plane, struct drm_atomic_commit *state)
+{
+ struct drm_plane_state *old_plane_state = drm_atomic_get_old_plane_state(state, plane);
+ struct drm_plane_state *plane_state = drm_atomic_get_new_plane_state(state, plane);
+ struct drm_shadow_plane_state *shadow_plane_state = to_drm_shadow_plane_state(plane_state);
+ struct fl2000_device *fdev = to_fl2000_device(plane->dev);
+ struct drm_framebuffer *fb = plane_state->fb;
+ struct drm_atomic_helper_damage_iter iter;
+ struct drm_rect damage, src;
+ unsigned int damage_px = 0;
+ ktime_t start;
+ int idx;
+
+ if (!fb)
+ return; /* plane is disabled */
+
+ if (drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE))
+ return;
+
+ if (drm_dev_enter(plane->dev, &idx)) {
+ start = ktime_get();
+ drm_rect_fp_to_int(&src, &plane_state->src);
+ drm_atomic_helper_damage_iter_init(&iter, old_plane_state, plane_state);
+ drm_atomic_for_each_plane_damage(&iter, &damage) {
+ fl2000_stream_compress(fdev->stream, &shadow_plane_state->data[0],
+ drm_gem_is_imported(fb->obj[0]), fb->pitches[0],
+ &src, &damage);
+ damage_px += drm_rect_width(&damage) * drm_rect_height(&damage);
+ }
+ if (damage_px)
+ fl2000_stream_note_render(fdev->stream,
+ (unsigned int)ktime_us_delta(ktime_get(), start),
+ damage_px);
+ drm_dev_exit(idx);
+ }
+
+ drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
+}
+
+static const struct drm_plane_helper_funcs fl2000_plane_helper_funcs = {
+ DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
+ .atomic_check = fl2000_plane_atomic_check,
+ .atomic_update = fl2000_plane_atomic_update,
+};
+
+static const struct drm_plane_funcs fl2000_plane_funcs = {
+ .update_plane = drm_atomic_helper_update_plane,
+ .disable_plane = drm_atomic_helper_disable_plane,
+ .destroy = drm_plane_cleanup,
+ DRM_GEM_SHADOW_PLANE_FUNCS,
+};
+
+/*
+ * CRTC
+ */
+
+/*
+ * Reset the FL2000 with its I2C engine idle. An operation of the engine is a sequence of register
+ * accesses, by the EDID read or the IT66121 regmap at any moment, and it would not survive a reset
+ * or a write to its control register in the middle.
+ */
+static void fl2000_reset_locked(struct fl2000_device *fdev)
+{
+ i2c_lock_bus(fdev->adapter, I2C_LOCK_SEGMENT);
+ fl2000_reset(fdev->regmap);
+ i2c_unlock_bus(fdev->adapter, I2C_LOCK_SEGMENT);
+}
+
+/* Bring the chip to where it can be used, at bind time or when it may have lost power */
+static void fl2000_chip_setup(struct fl2000_device *fdev)
+{
+ i2c_lock_bus(fdev->adapter, I2C_LOCK_SEGMENT);
+ fl2000_reset(fdev->regmap);
+ fl2000_usb_magic(fdev->regmap);
+ i2c_unlock_bus(fdev->adapter, I2C_LOCK_SEGMENT);
+
+ fl2000_enable_sink_interrupts(fdev->regmap);
+}
+
+/* Runs before the bridge is enabled: the IT66121 is programmed with the FL2000 clocking it */
+static void fl2000_crtc_mode_set_nofb(struct drm_crtc *crtc)
+{
+ struct drm_device *drm = crtc->dev;
+ struct fl2000_device *fdev = to_fl2000_device(drm);
+ struct regmap *regmap = fdev->regmap;
+ const struct drm_display_mode *mode = &crtc->state->mode;
+ const struct drm_display_mode *adjusted_mode = &crtc->state->adjusted_mode;
+ struct fl2000_timings timings;
+ struct fl2000_pll pll;
+ unsigned int bytes_pix;
+ u32 clock;
+ int idx, ret;
+
+ /* The same as in mode_fixup, which found one */
+ if (WARN_ON(fl2000_mode_check(mode, &pll, &clock, FL2000_PPM_ERR_MAX_VGA) != MODE_OK))
+ return;
+ /* The CRTC needs a primary plane to be enabled, and this state is the one being applied */
+ bytes_pix = fl2000_fb_bytes_pix(fdev->plane.state->fb);
+
+ /* The registers go with the USB device */
+ if (!drm_dev_enter(drm, &idx))
+ return;
+
+ drm_dbg(drm, "Mode requested: " DRM_MODE_FMT "\n", DRM_MODE_ARG(mode));
+ drm_dbg(drm, "Mode configured: " DRM_MODE_FMT "\n", DRM_MODE_ARG(adjusted_mode));
+
+ /* Prepare timing configuration */
+ timings.hactive = adjusted_mode->hdisplay;
+ timings.htotal = adjusted_mode->htotal;
+ timings.hsync_width = adjusted_mode->hsync_end - adjusted_mode->hsync_start;
+ timings.hstart = adjusted_mode->htotal - adjusted_mode->hsync_start + 1;
+ timings.vactive = adjusted_mode->vdisplay;
+ timings.vtotal = adjusted_mode->vtotal;
+ timings.vsync_width = adjusted_mode->vsync_end - adjusted_mode->vsync_start;
+ timings.vstart = adjusted_mode->vtotal - adjusted_mode->vsync_start + 1;
+
+ /* Set PLL settings */
+ fl2000_set_pll(regmap, &pll);
+
+ /* Reset FL2000 and confirm PLL settings */
+ fl2000_reset_locked(fdev);
+
+ /* Set timings settings */
+ fl2000_set_timings(regmap, &timings);
+
+ /* Pixel format according to number of bytes per pixel */
+ fl2000_set_pixfmt(regmap, bytes_pix);
+
+ /* Configure frame transfers */
+ fl2000_set_transfers(regmap);
+
+ /* The reset may have cleared them, and monitors are to be heard of at all times */
+ fl2000_enable_sink_interrupts(regmap);
+
+ fl2000_afe_magic(regmap);
+
+ /* Too late to fail the commit: without buffers the stream just does not start */
+ ret = fl2000_stream_mode_set(fdev->stream, mode->hdisplay, mode->vdisplay, bytes_pix);
+ if (ret)
+ drm_err(drm, "Cannot allocate the stream buffers (%d)\n", ret);
+
+ drm_dev_exit(idx);
+}
+
+static void fl2000_crtc_atomic_enable(struct drm_crtc *crtc, struct drm_atomic_commit *state)
+{
+ struct fl2000_device *fdev = to_fl2000_device(crtc->dev);
+ int idx;
+
+ if (drm_dev_enter(crtc->dev, &idx)) {
+ fl2000_stream_interrupts(fdev->regmap, true);
+
+ fl2000_stream_enable(fdev->stream);
+
+ drm_dev_exit(idx);
+ }
+}
+
+static void fl2000_crtc_atomic_disable(struct drm_crtc *crtc, struct drm_atomic_commit *state)
+{
+ struct fl2000_device *fdev = to_fl2000_device(crtc->dev);
+ int idx;
+
+ fl2000_stream_disable(fdev->stream);
+
+ if (drm_dev_enter(crtc->dev, &idx)) {
+ fl2000_stream_interrupts(fdev->regmap, false);
+ drm_dev_exit(idx);
+ }
+}
+
+static const struct drm_crtc_helper_funcs fl2000_crtc_helper_funcs = {
+ .mode_valid = fl2000_crtc_mode_valid,
+ .mode_fixup = fl2000_crtc_mode_fixup,
+ .atomic_check = drm_crtc_helper_atomic_check,
+ .mode_set_nofb = fl2000_crtc_mode_set_nofb,
+ .atomic_enable = fl2000_crtc_atomic_enable,
+ .atomic_disable = fl2000_crtc_atomic_disable,
+};
+
+static const struct drm_crtc_funcs fl2000_crtc_funcs = {
+ .atomic_create_state = drm_atomic_helper_crtc_create_state,
+ .set_config = drm_atomic_helper_set_config,
+ .page_flip = drm_atomic_helper_page_flip,
+ .atomic_duplicate_state = drm_atomic_helper_crtc_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_crtc_destroy_state,
+};
+
+/*
+ * Digital sinks are only offered the modes whose pixel clock the PLL hits closely: they expect the
+ * exact clock of the mode they advertised. The VGA output accepts a larger error.
+ */
+static enum drm_mode_status fl2000_hdmi_encoder_mode_valid(struct drm_encoder *encoder,
+ const struct drm_display_mode *mode)
+{
+ struct fl2000_pll pll;
+ u32 clock;
+
+ return fl2000_mode_check(mode, &pll, &clock, FL2000_PPM_ERR_MAX);
+}
+
+/*
+ * HDMI has always run with the monitor sense forced, so it stays so whenever HDMI is driven.
+ * Driving VGA alone leaves it to the chip, which then powers the DAC by itself and notices the
+ * cable being unplugged.
+ */
+static void fl2000_hdmi_encoder_force_connect(struct drm_encoder *encoder, bool force)
+{
+ int idx;
+
+ if (drm_dev_enter(encoder->dev, &idx)) {
+ fl2000_force_connect(to_fl2000_device(encoder->dev)->regmap, force);
+ drm_dev_exit(idx);
+ }
+}
+
+static void fl2000_hdmi_encoder_atomic_enable(struct drm_encoder *encoder,
+ struct drm_atomic_commit *state)
+{
+ fl2000_hdmi_encoder_force_connect(encoder, true);
+}
+
+static void fl2000_hdmi_encoder_atomic_disable(struct drm_encoder *encoder,
+ struct drm_atomic_commit *state)
+{
+ fl2000_hdmi_encoder_force_connect(encoder, false);
+}
+
+static const struct drm_encoder_helper_funcs fl2000_hdmi_encoder_helper_funcs = {
+ .mode_valid = fl2000_hdmi_encoder_mode_valid,
+ .atomic_enable = fl2000_hdmi_encoder_atomic_enable,
+ .atomic_disable = fl2000_hdmi_encoder_atomic_disable,
+};
+
+/**
+ * fl2000_drm_hotplug() - tell userspace that a monitor came, went or changed
+ * @drm: DRM device
+ * @events: the sink event bits of the status register that were seen
+ *
+ * Called from the interrupt work, which is stopped before the bridge goes away.
+ */
+void fl2000_drm_hotplug(struct drm_device *drm, u32 events)
+{
+ struct fl2000_device *fdev = to_fl2000_device(drm);
+ int idx;
+
+ if (events & FL2000_VGA_STATUS_HDMI_EVENT) {
+ int ret = it66121_ack_interrupts(fdev->bridge);
+
+ if (ret)
+ drm_dbg(drm, "Cannot acknowledge IT66121 interrupts (%d)\n", ret);
+ }
+
+ if (!drm_dev_enter(drm, &idx))
+ return;
+
+ drm_kms_helper_hotplug_event(drm);
+
+ drm_dev_exit(idx);
+}
+
+/**
+ * fl2000_drm_destroy() - stop the display and take the DRM device away from userspace
+ * @fdev: the DRM device
+ *
+ * Called when the USB interfaces are unbound.
+ */
+void fl2000_drm_destroy(struct fl2000_device *fdev)
+{
+ struct drm_device *drm = &fdev->drm;
+
+ /* No hotplug events for a node that is about to disappear */
+ fl2000_intr_quiesce(fdev->intr);
+ drm_kms_helper_poll_fini(drm);
+
+ /* Block userspace access and shut down the display pipeline */
+ drm_dev_unplug(drm);
+ drm_atomic_helper_shutdown(drm);
+
+ /* Already stopped, unless it was left running by a shutdown that failed */
+ fl2000_stream_disable(fdev->stream);
+
+ fl2000_intr_destroy(fdev->intr);
+}
+
+/**
+ * fl2000_drm_suspend() - stop the display for system sleep or a USB reset
+ * @fdev: the DRM device
+ *
+ * Called once for the device, before any of its interfaces is suspended. The USB core goes on with
+ * the sleep even if this fails, so the stream is stopped anyway, and fl2000_drm_resume() sets the
+ * chips up again in any case.
+ *
+ * Return: 0 on success, a negative error code otherwise
+ */
+int fl2000_drm_suspend(struct fl2000_device *fdev)
+{
+ int ret;
+
+ /* No hotplug events while going to sleep */
+ fl2000_intr_suspend(fdev->intr);
+
+ /* Saves the state, turns the outputs off and stops the stream */
+ ret = drm_mode_config_helper_suspend(&fdev->drm);
+ if (ret) {
+ drm_err(&fdev->drm, "Cannot save the display state (%d)\n", ret);
+ fl2000_stream_disable(fdev->stream);
+ }
+
+ return ret;
+}
+
+/**
+ * fl2000_drm_resume() - bring the display back after system sleep or a USB reset
+ * @fdev: the DRM device
+ *
+ * Called once for the device, when all of it is awake again, also after a reset-resume. The chips
+ * may have lost power: they are set up as at bind time before the saved state is restored.
+ *
+ * Return: 0 on success, a negative error code otherwise
+ */
+int fl2000_drm_resume(struct fl2000_device *fdev)
+{
+ struct regmap *regmap = fdev->regmap;
+ int ret = 0;
+
+ /* First, as the IT66121 is reached through the FL2000 I2C engine */
+ fl2000_regmap_drop_cache(regmap);
+
+ it66121_resume(fdev->bridge);
+
+ /* The same order as in fl2000_drm_create() */
+ fl2000_chip_setup(fdev);
+
+ /* There is nothing to restore if saving the state failed */
+ if (fdev->drm.mode_config.suspend_state) {
+ ret = drm_mode_config_helper_resume(&fdev->drm);
+ if (ret)
+ drm_err(&fdev->drm, "Cannot restore the display after sleep (%d)\n", ret);
+ }
+
+ /* Without interrupts the connectors are still polled */
+ fl2000_intr_start(fdev->intr);
+
+ drm_kms_helper_hotplug_event(&fdev->drm);
+
+ return ret;
+}
+
+static void fl2000_stream_release(struct drm_device *drm, void *res)
+{
+ fl2000_stream_destroy(res);
+}
+
+/**
+ * fl2000_drm_create() - set up and register the DRM device
+ * @interface: USB interface the device belongs to
+ * @regmap: FL2000 registers
+ * @adapter: FL2000 I2C adapter
+ * @bridge: IT66121 bridge
+ *
+ * Return: the DRM device, or an ERR_PTR() on failure
+ */
+struct fl2000_device *fl2000_drm_create(struct usb_interface *interface, struct regmap *regmap,
+ struct i2c_adapter *adapter, struct drm_bridge *bridge)
+{
+ int ret = 0;
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+ struct fl2000_device *fdev;
+ struct drm_device *drm;
+ struct device *dma_dev;
+ struct drm_mode_config *mode_config;
+ struct drm_connector_list_iter conn_iter;
+ struct drm_connector *connector;
+
+ dev_dbg(&interface->dev, "Creating the FL2000 DRM device\n");
+
+ fdev = devm_drm_dev_alloc(&interface->dev, &fl2000_drm_driver, struct fl2000_device, drm);
+ if (IS_ERR(fdev)) {
+ dev_err(&interface->dev, "Cannot allocate DRM structure (%pe)\n", fdev);
+ return fdev;
+ }
+ drm = &fdev->drm;
+ fdev->usb_dev = usb_dev;
+ fdev->regmap = regmap;
+ fdev->adapter = adapter;
+ fdev->bridge = bridge;
+
+ /* A USB device cannot DMA by itself: dma-buf imports go through its host controller */
+ dma_dev = usb_intf_get_dma_device(interface);
+ if (dma_dev) {
+ drm_dev_set_dma_dev(drm, dma_dev);
+ put_device(dma_dev);
+ } else {
+ drm_warn(drm, "No DMA device: framebuffers from another GPU cannot be used\n");
+ }
+
+ ret = drmm_mode_config_init(drm);
+ if (ret) {
+ drm_err(drm, "Cannot initialize DRM mode (%d)\n", ret);
+ return ERR_PTR(ret);
+ }
+
+ mode_config = &drm->mode_config;
+ mode_config->funcs = &fl2000_mode_config_funcs;
+ mode_config->max_width = FL2000_MAX_WIDTH;
+ mode_config->max_height = FL2000_MAX_HEIGHT;
+
+ ret = drm_universal_plane_init(drm, &fdev->plane, 0, &fl2000_plane_funcs,
+ fl2000_pixel_formats, ARRAY_SIZE(fl2000_pixel_formats),
+ fl2000_plane_format_modifiers, DRM_PLANE_TYPE_PRIMARY, NULL);
+ if (ret) {
+ drm_err(drm, "Cannot initialize plane (%d)\n", ret);
+ return ERR_PTR(ret);
+ }
+ drm_plane_helper_add(&fdev->plane, &fl2000_plane_helper_funcs);
+
+ /*
+ * Enable the damage-clips property the update path relies on
+ * (drm_atomic_helper_damage_merged()).
+ */
+ drm_plane_enable_fb_damage_clips(&fdev->plane);
+
+ ret = drmm_crtc_init_with_planes(drm, &fdev->crtc, &fdev->plane, NULL,
+ &fl2000_crtc_funcs, NULL);
+ if (ret) {
+ drm_err(drm, "Cannot initialize CRTC (%d)\n", ret);
+ return ERR_PTR(ret);
+ }
+ drm_crtc_helper_add(&fdev->crtc, &fl2000_crtc_helper_funcs);
+ fdev->plane.possible_crtcs = drm_crtc_mask(&fdev->crtc);
+
+ /* The FL2000 is programmed from the CRTC; the IT66121 bridge hangs off this encoder */
+ ret = drmm_encoder_init(drm, &fdev->encoder, NULL, DRM_MODE_ENCODER_NONE, NULL);
+ if (ret) {
+ drm_err(drm, "Cannot initialize encoder (%d)\n", ret);
+ return ERR_PTR(ret);
+ }
+ fdev->encoder.possible_crtcs = drm_crtc_mask(&fdev->crtc);
+ drm_encoder_helper_add(&fdev->encoder, &fl2000_hdmi_encoder_helper_funcs);
+
+ /* Start streaming interface */
+ fdev->stream = fl2000_stream_create(usb_dev);
+ if (IS_ERR(fdev->stream)) {
+ ret = PTR_ERR(fdev->stream);
+ drm_err(drm, "Cannot create stream (%d)\n", ret);
+ return ERR_PTR(ret);
+ }
+
+ ret = drmm_add_action_or_reset(drm, fl2000_stream_release, fdev->stream);
+ if (ret)
+ return ERR_PTR(ret);
+
+ fdev->intr = fl2000_intr_create(usb_dev, regmap, drm);
+ if (IS_ERR(fdev->intr)) {
+ ret = PTR_ERR(fdev->intr);
+ drm_err(drm, "Cannot set up interrupts (%d)\n", ret);
+ return ERR_PTR(ret);
+ }
+
+ /* The display driver makes the connector, from what the bridge reports */
+ ret = drm_bridge_attach(&fdev->encoder, bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+ if (ret) {
+ drm_err(drm, "Cannot attach bridge (%d)\n", ret);
+ goto err_destroy_intr;
+ }
+
+ /*
+ * The HDMI connector is the generic one of the bridge chain: the IT66121 attached without
+ * a connector of its own and reports the hot plug line and the EDID.
+ */
+ connector = drm_bridge_connector_init(drm, &fdev->encoder);
+ if (IS_ERR(connector)) {
+ ret = PTR_ERR(connector);
+ drm_err(drm, "Cannot initialize HDMI connector (%d)\n", ret);
+ goto err_destroy_intr;
+ }
+
+ /* The VGA output looks at the bridge the HDMI encoder has by now */
+ ret = fl2000_vga_init(drm, &fdev->vga, &fdev->crtc, &fdev->encoder, adapter,
+ fdev->intr);
+ if (ret) {
+ drm_err(drm, "Cannot initialize VGA output (%d)\n", ret);
+ goto err_destroy_intr;
+ }
+
+ drm_mode_config_reset(drm);
+
+ fl2000_chip_setup(fdev);
+
+ /*
+ * Probe the connector before userspace can see the device. A compositor
+ * that finds a new GPU reads the cached connector state without probing
+ * it, and does not look again when the hotplug event follows; with an
+ * unprobed connector it never enables the output until something else
+ * makes it reload.
+ */
+ mutex_lock(&drm->mode_config.mutex);
+ drm_connector_list_iter_begin(drm, &conn_iter);
+ drm_for_each_connector_iter(connector, &conn_iter)
+ drm_helper_probe_single_connector_modes(connector, FL2000_MAX_WIDTH,
+ FL2000_MAX_HEIGHT);
+ drm_connector_list_iter_end(&conn_iter);
+ mutex_unlock(&drm->mode_config.mutex);
+
+ ret = drm_dev_register(drm, 0);
+ if (ret) {
+ drm_err(drm, "Cannot register DRM device (%d)\n", ret);
+ goto err_destroy_intr;
+ }
+
+ /* Without interrupts the connectors are still polled */
+ fl2000_intr_start(fdev->intr);
+
+ /* Polls both connectors: events can be missed, and HDMI has no hot plug of its own */
+ drm_kms_helper_poll_init(drm);
+
+ /*
+ * fbdev emulation, so the console can use the display. Last, because it
+ * may set a mode right away and the reset above would undo it.
+ */
+ drm_client_setup(drm, NULL);
+
+ return fdev;
+
+err_destroy_intr:
+ fl2000_intr_destroy(fdev->intr);
+
+ return ERR_PTR(ret);
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_drv.c b/drivers/gpu/drm/fl2000/fl2000_drv.c
index 31d153553..663fb5daa 100644
--- a/drivers/gpu/drm/fl2000/fl2000_drv.c
+++ b/drivers/gpu/drm/fl2000/fl2000_drv.c
@@ -35,6 +35,9 @@ struct fl2000_devs {
struct regmap *regmap;
struct i2c_adapter *adapter;
struct drm_bridge *bridge;
+ struct fl2000_device *fdev;
+ /* The display was stopped for system sleep or a reset and has not been restarted yet */
+ bool suspended;
};
/* Interface 0 is what the driver is probing: claim the others */
@@ -116,10 +119,20 @@ static int fl2000_probe(struct usb_interface *interface, const struct usb_device
if (ret)
goto err_destroy_bridge;
+ devs->fdev = fl2000_drm_create(interface, devs->regmap, devs->adapter, devs->bridge);
+ if (IS_ERR(devs->fdev)) {
+ ret = PTR_ERR(devs->fdev);
+ goto err_release_interfaces;
+ }
+
usb_set_intfdata(interface, devs);
return 0;
+err_release_interfaces:
+ for (int i = FL2000_USBIF_AVCONTROL + 1; i < FL2000_USBIF_NUM; i++)
+ usb_set_intfdata(usb_ifnum_to_if(usb_dev, i), NULL);
+ fl2000_release_interfaces(usb_dev, interface);
err_destroy_bridge:
it66121_destroy(devs->bridge);
@@ -138,26 +151,52 @@ static void fl2000_disconnect(struct usb_interface *interface)
for (int i = 0; i < FL2000_USBIF_NUM; i++)
usb_set_intfdata(usb_ifnum_to_if(usb_dev, i), NULL);
+ fl2000_drm_destroy(devs->fdev);
it66121_destroy(devs->bridge);
/* Last, as devs goes with interface 0, which may be one of these */
fl2000_release_interfaces(usb_dev, interface);
}
+/*
+ * System sleep. The USB core suspends the interfaces one by one, the last first, and resumes them
+ * in order once the device is awake. The display is one thing for all three: stop it at the first
+ * suspend, before any interface sleeps, and start it again at the first resume. The same callback
+ * serves a reset-resume, after which the chips are set up from scratch: without one the USB core
+ * would unbind and bind the driver again, which a running compositor sees as an unplug.
+ */
static int fl2000_suspend(struct usb_interface *interface, pm_message_t message)
{
- /*
- * Nothing to save yet. Providing the callback keeps the USB core from
- * unbinding the driver across system sleep.
- */
- dev_dbg(&interface->dev, "suspend\n");
+ struct fl2000_devs *devs = usb_get_intfdata(interface);
- return 0;
+ if (!devs || devs->suspended)
+ return 0;
+
+ /* Even if this fails: the USB core ignores the error during system sleep and goes on */
+ devs->suspended = true;
+
+ return fl2000_drm_suspend(devs->fdev);
}
static int fl2000_resume(struct usb_interface *interface)
{
- dev_dbg(&interface->dev, "resume\n");
+ struct fl2000_devs *devs = usb_get_intfdata(interface);
+
+ if (!devs || !devs->suspended)
+ return 0;
+
+ devs->suspended = false;
+
+ return fl2000_drm_resume(devs->fdev);
+}
+
+/*
+ * A reset of the USB device (the storage function of the adapter asks for one when it times out)
+ * is the same as far as the display goes. Nonzero would make the USB core unbind the driver.
+ */
+static int fl2000_pre_reset(struct usb_interface *interface)
+{
+ fl2000_suspend(interface, PMSG_SUSPEND);
return 0;
}
@@ -168,6 +207,9 @@ static struct usb_driver fl2000_driver = {
.disconnect = fl2000_disconnect,
.suspend = fl2000_suspend,
.resume = fl2000_resume,
+ .reset_resume = fl2000_resume,
+ .pre_reset = fl2000_pre_reset,
+ .post_reset = fl2000_resume,
.id_table = fl2000_id_table,
.supports_autosuspend = false,
.disable_hub_initiated_lpm = true,
diff --git a/drivers/gpu/drm/fl2000/fl2000_interrupt.c b/drivers/gpu/drm/fl2000/fl2000_interrupt.c
new file mode 100644
index 000000000..8f8f9fac1
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_interrupt.c
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#include "fl2000.h"
+
+#define INTR_BUFSIZE 1
+
+struct fl2000_intr {
+ struct usb_device *usb_dev;
+ struct regmap *regmap;
+ struct drm_device *drm;
+ struct urb *urb;
+ u8 *buf;
+ dma_addr_t transfer_dma;
+ struct work_struct work;
+ struct workqueue_struct *work_queue;
+ /* Sink events read by anyone, not yet reported to userspace */
+ atomic_t events;
+};
+
+#define FL2000_SINK_EVENTS (FL2000_VGA_STATUS_HDMI_EVENT | FL2000_VGA_STATUS_MONITOR_EVENT | \
+ FL2000_VGA_STATUS_EDID_EVENT)
+
+/*
+ * The event bits of the status register clear on read, and both the work below and the connector
+ * detection read it: keep the events from every read until the work reports them.
+ */
+static int fl2000_intr_read_status(struct fl2000_intr *intr, u32 *status)
+{
+ int ret;
+
+ ret = fl2000_read_status(intr->regmap, status);
+ if (ret)
+ return ret;
+
+ atomic_or(*status & FL2000_SINK_EVENTS, &intr->events);
+
+ return 0;
+}
+
+/**
+ * fl2000_intr_monitor_connected() - does the FL2000 sense a monitor on its monitor input
+ * @intr: interrupt context
+ *
+ * The chip powers the DAC and the PLL only while this is the case. On the boards with both
+ * outputs the input follows whichever monitor is connected, so it does not tell VGA from HDMI.
+ * While HDMI is being driven the sense is forced on and tells nothing: that counts as not sensing,
+ * or an HDMI monitor going away would look like a VGA one showing up, until the CRTC is disabled.
+ *
+ * Return: 1 if a monitor is sensed, 0 if not, a negative error code if the status cannot be read
+ */
+int fl2000_intr_monitor_connected(struct fl2000_intr *intr)
+{
+ u32 status;
+ int ret;
+
+ ret = fl2000_intr_read_status(intr, &status);
+ if (ret)
+ return ret;
+
+ if (fl2000_force_connect_active(intr->regmap))
+ return 0;
+
+ return !!(status & FL2000_VGA_STATUS_MONITOR_STATUS);
+}
+
+static void fl2000_intr_work(struct work_struct *work)
+{
+ u32 status, events;
+ struct fl2000_intr *intr = container_of(work, struct fl2000_intr, work);
+
+ if (fl2000_intr_read_status(intr, &status))
+ return;
+
+ events = atomic_xchg(&intr->events, 0);
+ if (events)
+ fl2000_drm_hotplug(intr->drm, events);
+}
+
+static void fl2000_intr_completion(struct urb *urb)
+{
+ int ret;
+ struct usb_device *usb_dev = urb->dev;
+ struct fl2000_intr *intr = urb->context;
+
+ ret = fl2000_urb_status(usb_dev, urb->status);
+ if (ret) {
+ /* Killed on purpose (suspend, unplug) is no reason to complain */
+ if (ret != -ENOENT && ret != -ECONNRESET && ret != -ESHUTDOWN)
+ dev_err(&usb_dev->dev, "Stopping interrupts\n");
+ return;
+ }
+
+ /* Processing reads registers over USB, which sleeps: defer it to a work item */
+ queue_work(intr->work_queue, &intr->work);
+
+ /* Restart urb. It may be getting poisoned, or the device may have just gone. */
+ ret = usb_submit_urb(urb, GFP_ATOMIC);
+ if (ret && ret != -EPERM && ret != -ENODEV)
+ dev_err(&usb_dev->dev, "URB submission failed (%d)\n", ret);
+}
+
+/**
+ * fl2000_intr_create() - interrupt processing context creation
+ * @usb_dev: FL2000 USB device
+ * @regmap: FL2000 registers
+ * @drm: DRM device that receives the hotplug events
+ *
+ * Called when the DRM device is set up. fl2000_intr_start() starts polling the interrupt endpoint.
+ *
+ * Return: interrupt context, or an ERR_PTR() on failure
+ */
+struct fl2000_intr *fl2000_intr_create(struct usb_device *usb_dev, struct regmap *regmap,
+ struct drm_device *drm)
+{
+ int ret;
+ struct fl2000_intr *intr;
+ struct usb_endpoint_descriptor *desc;
+ struct usb_interface *interface = usb_ifnum_to_if(usb_dev, FL2000_USBIF_INTERRUPT);
+
+ /*
+ * There's only one altsetting (#0) and one endpoint (#3) in the interrupt interface (#2)
+ * but let's try and "find" it anyway.
+ */
+ ret = usb_find_int_in_endpoint(interface->cur_altsetting, &desc);
+ if (ret) {
+ dev_err(&usb_dev->dev, "Cannot find interrupt endpoint\n");
+ return ERR_PTR(ret);
+ }
+
+ intr = kzalloc_obj(*intr);
+ if (!intr)
+ return ERR_PTR(-ENOMEM);
+
+ intr->usb_dev = usb_dev;
+ intr->regmap = regmap;
+ intr->drm = drm;
+ INIT_WORK(&intr->work, &fl2000_intr_work);
+ atomic_set(&intr->events, 0);
+
+ ret = -ENOMEM;
+ intr->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!intr->urb)
+ goto err_destroy;
+
+ intr->buf = usb_alloc_coherent(usb_dev, INTR_BUFSIZE, GFP_KERNEL, &intr->transfer_dma);
+ if (!intr->buf)
+ goto err_destroy;
+
+ intr->work_queue = alloc_ordered_workqueue("fl2000_interrupt", 0);
+ if (!intr->work_queue)
+ goto err_destroy;
+
+ /* Interrupt URB configuration is static, including allocated buffer */
+ usb_fill_int_urb(intr->urb, usb_dev, usb_rcvintpipe(usb_dev, usb_endpoint_num(desc)),
+ intr->buf, INTR_BUFSIZE, fl2000_intr_completion, intr, desc->bInterval);
+ intr->urb->transfer_dma = intr->transfer_dma;
+ intr->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ return intr;
+
+err_destroy:
+ fl2000_intr_destroy(intr);
+
+ return ERR_PTR(ret);
+}
+
+/**
+ * fl2000_intr_quiesce() - stop reporting hotplug events
+ * @intr: interrupt context
+ *
+ * Called before the DRM device is unplugged: an event raised after that would
+ * announce a change on a node that was just removed, and probing the connector
+ * would talk to hardware that is already gone.
+ */
+void fl2000_intr_quiesce(struct fl2000_intr *intr)
+{
+ usb_poison_urb(intr->urb);
+ cancel_work_sync(&intr->work);
+}
+
+/**
+ * fl2000_intr_suspend() - stop polling the interrupt endpoint for system sleep
+ * @intr: interrupt context
+ *
+ * Unlike fl2000_intr_quiesce(), the URB can be submitted again by fl2000_intr_start().
+ */
+void fl2000_intr_suspend(struct fl2000_intr *intr)
+{
+ usb_kill_urb(intr->urb);
+ cancel_work_sync(&intr->work);
+}
+
+/**
+ * fl2000_intr_start() - poll the interrupt endpoint
+ * @intr: interrupt context
+ *
+ * When the display is ready to hear about monitors: its hotplug events must find the DRM device
+ * registered, and after system sleep the pipeline restored.
+ *
+ * Return: 0 on success, the URB submission error otherwise
+ */
+int fl2000_intr_start(struct fl2000_intr *intr)
+{
+ int ret;
+
+ ret = usb_submit_urb(intr->urb, GFP_KERNEL);
+ if (ret)
+ dev_err(&intr->usb_dev->dev, "URB submission failed (%d)\n", ret);
+
+ return ret;
+}
+
+void fl2000_intr_destroy(struct fl2000_intr *intr)
+{
+ usb_poison_urb(intr->urb);
+ cancel_work_sync(&intr->work);
+ if (intr->work_queue)
+ destroy_workqueue(intr->work_queue);
+ usb_free_coherent(intr->usb_dev, INTR_BUFSIZE, intr->buf, intr->transfer_dma);
+ usb_free_urb(intr->urb);
+ kfree(intr);
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_vga.c b/drivers/gpu/drm/fl2000/fl2000_vga.c
new file mode 100644
index 000000000..01e77d11c
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_vga.c
@@ -0,0 +1,223 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * VGA output of the FL2000: the analog DAC of the chip, on an encoder and a connector of its own
+ * next to the HDMI ones. All of them share the one CRTC, so a compositor can use either output
+ * alone, and a dual-output adapter can show the same frame on both ("clone mode").
+ */
+
+#include <drm/drm_atomic_state_helper.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_managed.h>
+#include <drm/drm_modes.h>
+
+#include "fl2000.h"
+#include "fl2000_it66121.h"
+
+/*
+ * Highest pixel clock offered on VGA, kHz. The DAC has no documented limit; 1920x1080 at 60 Hz is
+ * what the vendor advertises for it.
+ */
+#define FL2000_VGA_MAX_CLOCK 165000
+
+/* Offered as the preferred mode when there is no EDID: any VGA monitor takes it */
+#define FL2000_VGA_FALLBACK_WIDTH 1024
+#define FL2000_VGA_FALLBACK_HEIGHT 768
+
+/* Largest mode listed from the standard timings when there is no EDID */
+#define FL2000_VGA_NOEDID_WIDTH 1920
+#define FL2000_VGA_NOEDID_HEIGHT 1200
+
+static inline struct fl2000_vga *to_fl2000_vga(struct drm_connector *connector)
+{
+ return container_of(connector, struct fl2000_vga, connector);
+}
+
+static int fl2000_vga_read_block(void *context, u8 *buf, unsigned int block, size_t len)
+{
+ struct fl2000_vga *vga = context;
+
+ return fl2000_ddc_read_block(vga->adapter, buf, block, len);
+}
+
+/*
+ * The chip has one monitor sense input, and on the adapters with both outputs it follows whichever
+ * monitor is plugged in. The HDMI transmitter has a hot plug line of its own, so a sensed monitor
+ * with no HDMI sink behind it is a VGA one. With both connected the HDMI sink wins and the VGA
+ * output reports nothing: it can still be forced on (video=VGA-1:e) to mirror the picture.
+ */
+static enum drm_connector_status fl2000_vga_sense(struct fl2000_vga *vga)
+{
+ int ret = fl2000_intr_monitor_connected(vga->intr);
+
+ if (ret < 0)
+ return connector_status_unknown;
+ if (!ret)
+ return connector_status_disconnected;
+
+ if (vga->hdmi_bridge) {
+ /*
+ * Not knowing whether there is an HDMI sink is not the same as there being none:
+ * two connected outputs on the one CRTC can leave a compositor with neither.
+ */
+ ret = it66121_hpd_connected(vga->hdmi_bridge);
+ if (ret < 0)
+ return connector_status_unknown;
+ if (ret)
+ return connector_status_disconnected;
+ }
+
+ return connector_status_connected;
+}
+
+static int fl2000_vga_detect_ctx(struct drm_connector *connector,
+ struct drm_modeset_acquire_ctx *ctx, bool force)
+{
+ struct fl2000_vga *vga = to_fl2000_vga(connector);
+ enum drm_connector_status status;
+ int idx;
+
+ if (!drm_dev_enter(connector->dev, &idx))
+ return connector_status_disconnected;
+
+ status = fl2000_vga_sense(vga);
+ if (status == connector_status_unknown)
+ /* An unreadable status says nothing: keep what userspace was told */
+ status = connector->status;
+
+ drm_dev_exit(idx);
+
+ return status;
+}
+
+/* The EDID is read through the FL2000 I2C engine */
+static int fl2000_vga_get_modes(struct drm_connector *connector)
+{
+ struct fl2000_vga *vga = to_fl2000_vga(connector);
+ const struct drm_edid *drm_edid;
+ int count = 0, idx;
+
+ if (!drm_dev_enter(connector->dev, &idx))
+ return 0;
+
+ drm_edid = drm_edid_read_custom(connector, fl2000_vga_read_block, vga);
+ drm_edid_connector_update(connector, drm_edid);
+ if (drm_edid)
+ count = drm_edid_connector_add_modes(connector);
+ drm_edid_free(drm_edid);
+
+ if (!count) {
+ /*
+ * No EDID, or nothing usable in it: offer the standard timings and let 1024x768
+ * be the default. Another mode can be chosen from the desktop, or forced on the
+ * kernel command line (video=VGA-1:1366x768@60e), and an EDID file can replace
+ * the missing one (drm.edid_firmware=VGA-1:edid/monitor.bin).
+ */
+ count = drm_add_modes_noedid(connector, FL2000_VGA_NOEDID_WIDTH,
+ FL2000_VGA_NOEDID_HEIGHT);
+ drm_set_preferred_mode(connector, FL2000_VGA_FALLBACK_WIDTH,
+ FL2000_VGA_FALLBACK_HEIGHT);
+ }
+
+ drm_dev_exit(idx);
+
+ return count;
+}
+
+static const struct drm_connector_helper_funcs fl2000_vga_connector_helper_funcs = {
+ .get_modes = fl2000_vga_get_modes,
+ .detect_ctx = fl2000_vga_detect_ctx,
+};
+
+static const struct drm_connector_funcs fl2000_vga_connector_funcs = {
+ .fill_modes = drm_helper_probe_single_connector_modes,
+ .atomic_create_state = drm_atomic_helper_connector_create_state,
+ .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
+};
+
+/*
+ * Applies to the VGA output only, both when probing and in atomic commits. The PLL limits are
+ * checked by the CRTC.
+ */
+static enum drm_mode_status fl2000_vga_encoder_mode_valid(struct drm_encoder *encoder,
+ const struct drm_display_mode *mode)
+{
+ if (mode->clock > FL2000_VGA_MAX_CLOCK)
+ return MODE_CLOCK_HIGH;
+
+ return MODE_OK;
+}
+
+static const struct drm_encoder_helper_funcs fl2000_vga_encoder_helper_funcs = {
+ .mode_valid = fl2000_vga_encoder_mode_valid,
+};
+
+static void fl2000_vga_release(struct drm_device *drm, void *res)
+{
+ struct fl2000_vga *vga = res;
+
+ drm_bridge_put(vga->hdmi_bridge);
+}
+
+/**
+ * fl2000_vga_init() - add the VGA encoder and connector
+ * @drm: DRM device
+ * @vga: VGA output to set up, part of the driver's device structure
+ * @crtc: the CRTC, which every output shares
+ * @hdmi_encoder: the encoder of the HDMI output, with its bridge already attached
+ * @adapter: the FL2000 I2C adapter, which carries the monitor's DDC
+ * @intr: interrupt context, to sense the monitor
+ *
+ * The HDMI bridge must be attached already, and nothing may be probed before this returns: the
+ * two encoders are marked as clones of each other, in both directions as the core requires.
+ *
+ * Return: 0 on success, a negative error code otherwise
+ */
+int fl2000_vga_init(struct drm_device *drm, struct fl2000_vga *vga, struct drm_crtc *crtc,
+ struct drm_encoder *hdmi_encoder, struct i2c_adapter *adapter,
+ struct fl2000_intr *intr)
+{
+ u32 clones;
+ int ret;
+
+ vga->adapter = adapter;
+ vga->intr = intr;
+ vga->hdmi_bridge = drm_bridge_chain_get_first_bridge(hdmi_encoder);
+
+ ret = drmm_add_action_or_reset(drm, fl2000_vga_release, vga);
+ if (ret)
+ return ret;
+
+ ret = drmm_encoder_init(drm, &vga->encoder, NULL, DRM_MODE_ENCODER_DAC, NULL);
+ if (ret)
+ return ret;
+ vga->encoder.possible_crtcs = drm_crtc_mask(crtc);
+ drm_encoder_helper_add(&vga->encoder, &fl2000_vga_encoder_helper_funcs);
+
+ /*
+ * No ddc adapter: its transfers carry a single byte, so the EDID is read with
+ * fl2000_ddc_read_block() instead of the DDC helpers.
+ */
+ ret = drmm_connector_init(drm, &vga->connector, &fl2000_vga_connector_funcs,
+ DRM_MODE_CONNECTOR_VGA, NULL);
+ if (ret)
+ return ret;
+ drm_connector_helper_add(&vga->connector, &fl2000_vga_connector_helper_funcs);
+
+ /*
+ * The chip raises an event when the monitor comes or goes (fl2000_interrupt.c), but polling
+ * recovers from one that was missed, and it costs a status read every few seconds.
+ */
+ vga->connector.polled = DRM_CONNECTOR_POLL_CONNECT | DRM_CONNECTOR_POLL_DISCONNECT;
+
+ ret = drm_connector_attach_encoder(&vga->connector, &vga->encoder);
+ if (ret)
+ return ret;
+
+ clones = drm_encoder_mask(hdmi_encoder) | drm_encoder_mask(&vga->encoder);
+ hdmi_encoder->possible_clones = clones;
+ vga->encoder.possible_clones = clones;
+
+ return 0;
+}
--
2.56.0
^ permalink raw reply [flat|nested] 5+ messages in thread