* [PATCH RFC 0/4] drm: Add Fresco Logic FL2000 USB display driver
@ 2026-11-06 2:34 Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 1/4] drm/fl2000: Add USB driver with register and I2C access Angel Gabriel Crispin Valdivia via B4 Relay
` (3 more replies)
0 siblings, 4 replies; 6+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-06 2:34 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
David Airlie, Simona Vetter, Angel Gabriel Crispin Valdivia
Cc: linux-kernel, dri-devel
Hi,
This series adds a DRM driver for USB display adapters built around the
Fresco Logic FL2000DX (USB ID 1d5c:2000). These are sold under many
brands as cheap USB 3.0 to VGA/HDMI dongles; the HDMI variants pair the
FL2000 with an ITE IT66121 HDMI transmitter on the FL2000's I2C bus.
Today these adapters only work on Linux with out-of-tree drivers.
The driver started as Artem Mygaiev's out-of-tree fl2000_drm driver,
itself partly based on Fresco Logic's reference driver; their copyright
lines are kept. I ported it to current kernels, fixed the problems found
on real hardware (EDID, bridge lifetime across USB resets, crashes on
disconnect, frames that were never terminated, imported buffers read
from the wrong VRAM), added the analog VGA output and cleaned it up for
submission.
I intend to maintain this driver for the long term: patch 1 adds a
MAINTAINERS entry with me as maintainer, I have the hardware to test
on, and I will keep following review and bug reports on dri-devel.
The series builds the driver up in four steps, each of which compiles
on its own:
1. USB driver, FL2000 register access (regmap) and I2C adapter
2. IT66121 HDMI transmitter bridge, including the EDID fallback
3. frame streaming over bulk URBs
4. the KMS pipeline, which makes the display usable, including the
analog VGA output of adapters that have both connectors
It is posted as an RFC because there are design points I would like
feedback on before doing the larger reworks:
1. IT66121 support. The series carries its own small IT66121 bridge
(fl2000_it66121.c) instead of reusing drivers/gpu/drm/bridge/
ite-it66121.c, because I could not find a way to use the in-tree
driver on a PC: it depends on OF (x86 distribution kernels have no
CONFIG_OF), reads bus-width and its next bridge through of_graph,
requires a reset GPIO the FL2000 boards do not have, and DRM has no
bridge lookup without OF. It also reads the EDID through its DDC
engine, which on these boards is not connected to the monitor (the
EDID is read with the FL2000 I2C engine instead).
Reusing it would take converting ite-it66121 to the fwnode API with an
optional reset GPIO, a fwnode-based bridge lookup in drm_bridge.c, and
a small bridge of this driver providing the EDID. Is that the direction
you would like, or is a private IT66121 bridge acceptable for a USB
device like this one?
2. Hotplug. The bridge attaches with DRM_BRIDGE_ATTACH_NO_CONNECTOR and
the HDMI connector comes from drm_bridge_connector. The bridge
reports DETECT and EDID only: the IT66121 interrupt reaches the host
through the FL2000 interrupt endpoint, which makes the driver send a
hotplug event for the device, so there is no DRM_BRIDGE_OP_HPD and
the connector is also polled. Would reporting HPD with
drm_bridge_hpd_notify() from that path be preferred?
3. The FL2000 exposes three USB interfaces, and the state they share
(regmap, I2C adapter, IT66121 client, component match) is kept as
drvdata and devres of the struct usb_device rather than of an
interface, so that it survives the unbind/rebind cycles of
usb_reset_device(). Is there a better established pattern for a
multi-interface device that needs this?
4. Pixel conversion. The stream converts with its own small line
converters instead of drm_fb_xrgb8888_to_*(). The framebuffer is
usually imported from the primary GPU and sits in write-combined
VRAM; the format helpers read such I/O memory with memcpy_fromio(),
which takes ~88 ms per 1080p frame, while drm_memcpy_from_wc() into a
cached line buffer takes ~8 ms. The converters also apply the
FL2000's 32-bit word swap and only touch the damaged rectangle of a
shadow frame. Would a helper that reads its source through
drm_memcpy_from_wc() be welcome, so this could go away?
5. VGA detection. The chip has a single monitor sense input, which on
the adapter I have follows whichever monitor is plugged in, HDMI or
VGA. The VGA connector therefore reports a monitor when the chip
senses one and the IT66121 hot plug line is low, which means a
monitor on VGA is not reported while one is on HDMI (clone mode
needs video=VGA-1:e). Is this an acceptable way to do it, and is
there a better signal I should look for in the chip?
The rest of the pipeline follows the current helpers: regular atomic
plane/CRTC/encoder objects with managed initialisation, a cursor plane
blended by the CPU, GEM SHMEM with shadow-plane access, the DRM vblank
timer (the FL2000 has no vblank interrupt) and fbdev emulation through
drm_client_setup().
Other known limitations: RGB565, used when the USB link cannot carry
RGB888 (2560x1080 on USB 3.0, 640x480 on USB 2.0, as there is no
compression), has not been tested on hardware; and adapters with a VGA
connector only, without an IT66121, are not supported: the analog output
is only added next to the HDMI one.
Tested on x86_64 (amdgpu laptop, GNOME Wayland) with a UGREEN USB 3.0
adapter with HDMI and VGA connectors: a 1920x1080 monitor on HDMI, and
an old 1366x768 monitor without a usable EDID on VGA. EDID, mode
setting, the desktop and the cursor work, at 59 frames per second on
the link.
Every patch builds without warnings with W=1 on top of drm-misc-next,
sparse is clean, and the series passes checkpatch --strict.
Tool use, as described in Documentation/process/generated-content.rst: I
used an AI coding assistant (Claude Code with Anthropic Claude models)
extensively while preparing this series. It helped port the driver to the
current APIs, clean it up to kernel style, write the shadow-frame
streaming and the cursor plane, track down and fix the hangs on unplug,
and draft the commit messages and this letter. I reviewed the result and
tested it on the hardware described above; the Signed-off-by lines are
mine. The series was generated and checked (per-patch builds, checkpatch)
by scripts in the out-of-tree repository this driver is developed in.
---
Angel Gabriel Crispin Valdivia (4):
drm/fl2000: Add USB driver with register and I2C access
drm/fl2000: Add ITE IT66121 HDMI transmitter support
drm/fl2000: Add frame streaming
drm/fl2000: Add KMS display pipeline
MAINTAINERS | 7 +
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/fl2000/Kconfig | 19 +
drivers/gpu/drm/fl2000/Makefile | 13 +
drivers/gpu/drm/fl2000/fl2000.h | 192 +++++
drivers/gpu/drm/fl2000/fl2000_drm.c | 986 ++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_drv.c | 281 +++++++
drivers/gpu/drm/fl2000/fl2000_i2c.c | 182 +++++
drivers/gpu/drm/fl2000/fl2000_interrupt.c | 237 ++++++
drivers/gpu/drm/fl2000/fl2000_it66121.c | 1030 ++++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_it66121.h | 19 +
drivers/gpu/drm/fl2000/fl2000_it66121_regs.h | 283 +++++++
drivers/gpu/drm/fl2000/fl2000_registers.c | 320 ++++++++
drivers/gpu/drm/fl2000/fl2000_registers.h | 272 +++++++
drivers/gpu/drm/fl2000/fl2000_streaming.c | 993 +++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_vga.c | 256 +++++++
17 files changed, 5092 insertions(+)
---
base-commit: 41ecf852c07391687beb129e12a231760b5d783f
change-id: 20261105-fl2000-upstream-3fc2e1f00d24
Best regards,
--
Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH RFC 1/4] drm/fl2000: Add USB driver with register and I2C access
2026-11-06 2:34 [PATCH RFC 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-06 2:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 2/4] drm/fl2000: Add ITE IT66121 HDMI transmitter support Angel Gabriel Crispin Valdivia via B4 Relay
` (2 subsequent siblings)
3 siblings, 0 replies; 6+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-06 2: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 binds the three
FL2000 interfaces, 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.
State shared by the interfaces is attached to the usb_device so that it
survives the unbind/rebind cycles of usb_reset_device().
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 | 88 ++++++++
drivers/gpu/drm/fl2000/fl2000_drv.c | 150 ++++++++++++++
drivers/gpu/drm/fl2000/fl2000_i2c.c | 182 +++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_registers.c | 320 ++++++++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_registers.h | 272 +++++++++++++++++++++++++
10 files changed, 1048 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..956032479
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000.h
@@ -0,0 +1,88 @@
+/* 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/component.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 <drm/drm_vblank.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,
+};
+
+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_device *usb_dev);
+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 usb_device *usb_dev);
+void fl2000_regmap_drop_cache(struct usb_device *usb_dev);
+
+/* Registers interface */
+int fl2000_reset(struct usb_device *usb_dev);
+int fl2000_usb_magic(struct usb_device *usb_dev);
+int fl2000_afe_magic(struct usb_device *usb_dev);
+int fl2000_set_transfers(struct usb_device *usb_dev);
+int fl2000_set_pixfmt(struct usb_device *usb_dev, u32 bytes_pix);
+int fl2000_set_timings(struct usb_device *usb_dev, struct fl2000_timings *timings);
+int fl2000_set_pll(struct usb_device *usb_dev, struct fl2000_pll *pll);
+int fl2000_force_connect(struct usb_device *usb_dev, bool force);
+int fl2000_enable_interrupts(struct usb_device *usb_dev);
+int fl2000_read_status(struct usb_device *usb_dev, u32 *status);
+int fl2000_i2c_dword(struct usb_device *usb_dev, 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..ea089d3c1
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_drv.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#include "fl2000.h"
+
+#define USB_CLASS_AV 0x10
+#define USB_SUBCLASS_AV_CONTROL 0x01
+#define USB_SUBCLASS_AV_VIDEO 0x02
+#define USB_SUBCLASS_AV_AUDIO 0x03
+
+#define USB_VENDOR_FRESCO_LOGIC 0x1D5C
+#define USB_PRODUCT_FL2000 0x2000
+
+static const struct usb_device_id fl2000_id_table[] = {
+ { USB_DEVICE_INTERFACE_CLASS(USB_VENDOR_FRESCO_LOGIC, USB_PRODUCT_FL2000, USB_CLASS_AV) },
+ {},
+};
+MODULE_DEVICE_TABLE(usb, fl2000_id_table);
+
+/* Devices that are independent of interfaces, created for the lifetime of the USB device */
+struct fl2000_devs {
+ struct regmap *regmap;
+ struct i2c_adapter *adapter;
+ int active_if;
+};
+
+static struct fl2000_devs *fl2000_get_devices(struct usb_device *usb_dev)
+{
+ struct fl2000_devs *devs;
+
+ devs = devm_kzalloc(&usb_dev->dev, sizeof(*devs), GFP_KERNEL);
+ if (!devs)
+ return ERR_PTR(-ENOMEM);
+
+ devs->regmap = fl2000_regmap_init(usb_dev);
+ if (IS_ERR(devs->regmap)) {
+ dev_err(&usb_dev->dev, "Cannot initialize regmap (%pe)\n", devs->regmap);
+ return ERR_CAST(devs->regmap);
+ }
+
+ devs->adapter = fl2000_i2c_init(usb_dev);
+ if (IS_ERR(devs->adapter)) {
+ dev_err(&usb_dev->dev, "Cannot initialize I2C adapter (%pe)\n", devs->adapter);
+ return ERR_CAST(devs->adapter);
+ }
+
+ dev_set_drvdata(&usb_dev->dev, devs);
+
+ return devs;
+}
+
+static int fl2000_probe(struct usb_interface *interface, const struct usb_device_id *usb_dev_id)
+{
+ int ret = 0;
+ u8 iface_num = interface->cur_altsetting->desc.bInterfaceNumber;
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+ struct fl2000_devs *devs = dev_get_drvdata(&usb_dev->dev);
+
+ if (usb_dev->speed < USB_SPEED_HIGH) {
+ dev_err(&usb_dev->dev, "USB 1.1 is not supported\n");
+ return -ENODEV;
+ }
+
+ if (!devs) {
+ devs = fl2000_get_devices(usb_dev);
+ if (IS_ERR(devs))
+ return PTR_ERR(devs);
+ }
+
+ switch (iface_num) {
+ case FL2000_USBIF_AVCONTROL:
+ case FL2000_USBIF_STREAMING:
+ case FL2000_USBIF_INTERRUPT:
+ devs->active_if |= BIT(iface_num);
+ break;
+
+ default:
+ /* Device does not have any other interfaces */
+ dev_warn(&interface->dev, "Unexpected interface %d\n", iface_num);
+ ret = -ENODEV;
+ break;
+ }
+
+ return ret;
+}
+
+static void fl2000_disconnect(struct usb_interface *interface)
+{
+ u8 iface_num = interface->cur_altsetting->desc.bInterfaceNumber;
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+ struct fl2000_devs *devs = dev_get_drvdata(&usb_dev->dev);
+
+ if (!devs)
+ return;
+
+ switch (iface_num) {
+ case FL2000_USBIF_AVCONTROL:
+ case FL2000_USBIF_STREAMING:
+ case FL2000_USBIF_INTERRUPT:
+ devs->active_if &= ~BIT(iface_num);
+ break;
+
+ default:
+ /* Device does not have any other interfaces */
+ dev_warn(&interface->dev, "Unexpected interface %d\n", iface_num);
+ break;
+ }
+}
+
+static int fl2000_suspend(struct usb_interface *interface, pm_message_t message)
+{
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+
+ /*
+ * Nothing to save yet. Providing the callback keeps the USB core from
+ * unbinding the driver across system sleep.
+ */
+ dev_dbg(&usb_dev->dev, "suspend\n");
+
+ return 0;
+}
+
+static int fl2000_resume(struct usb_interface *interface)
+{
+ struct usb_device *usb_dev = interface_to_usbdev(interface);
+
+ dev_dbg(&usb_dev->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..eadbe0f67
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_i2c.c
@@ -0,0 +1,182 @@
+// 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 usb_device *usb_dev, u16 addr, u8 offset, u32 *data)
+{
+ return fl2000_i2c_dword(usb_dev, true, addr, offset, data);
+}
+
+static inline int fl2000_i2c_write_dword(struct usb_device *usb_dev, u16 addr, u8 offset, u32 *data)
+{
+ return fl2000_i2c_dword(usb_dev, 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;
+
+ addr = msgs[0].addr;
+ offset = msgs[0].buf[0] & I2C_XFER_ADDR_MASK;
+ idx = msgs[0].buf[0] - offset;
+
+ /*
+ * We expect the following:
+ * - 2 messages, each 1 byte, first write then read
+ * - 1 message, 2 bytes, write
+ */
+ if (num == 2) {
+ read = true;
+ } else if (num == 1) {
+ if (msgs[0].len == 2 && !(msgs[0].flags & I2C_M_RD))
+ read = false;
+ else
+ return -EOPNOTSUPP;
+ } else {
+ return -EOPNOTSUPP;
+ }
+
+ /*
+ * 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 FIFO does not work on the tested boards, 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 usb_device *usb_dev = 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(usb_dev, 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 | I2C_FUNC_NOSTART;
+}
+
+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 data, on the same address */
+ .flags = I2C_AQ_COMB | I2C_AQ_COMB_WRITE_FIRST | I2C_AQ_COMB_SAME_ADDR,
+ .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_device *usb_dev)
+{
+ int ret;
+ struct i2c_adapter *adapter;
+ u8 usb_path[32];
+
+ /* Adapter must be allocated before anything else */
+ adapter = devm_kzalloc(&usb_dev->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 = usb_dev;
+ adapter->dev.parent = &usb_dev->dev;
+ strscpy(adapter->name, "FL2000 bridge I2C bus", sizeof(adapter->name));
+
+ ret = devm_i2c_add_adapter(&usb_dev->dev, adapter);
+ if (ret)
+ return ERR_PTR(ret);
+
+ usb_make_path(usb_dev, usb_path, sizeof(usb_path));
+ dev_dbg(&usb_dev->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..231814471
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_registers.c
@@ -0,0 +1,320 @@
+// 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 usb_device *usb_dev, struct fl2000_pll *pll)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+ 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 usb_device *usb_dev, bool force)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+
+ 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);
+}
+
+int fl2000_set_timings(struct usb_device *usb_dev, struct fl2000_timings *timings)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+
+ 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 usb_device *usb_dev, u32 bytes_pix)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+ 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 usb_device *usb_dev)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+ 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 usb_device *usb_dev)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+
+ 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 usb_device *usb_dev)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+
+ regmap_write_bits(regmap, FL2000_USB_LPM_REG, FL2000_USB_LPM_MAGIC, FL2000_USB_LPM_MAGIC);
+
+ return 0;
+}
+
+int fl2000_usb_magic(struct usb_device *usb_dev)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+ u32 val;
+
+ val = FL2000_VGA_I2C_SC_MONITOR_DETECT | FL2000_VGA_I2C_SC_EDID_DETECT;
+ 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;
+}
+
+int fl2000_enable_interrupts(struct usb_device *usb_dev)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+ u32 mask, val;
+
+ val = FL2000_VGA_CTRL_ACLK_VGA_ERR_INT_EN | FL2000_VGA_CTRL_ACLK_LBUF_ERR_INT_EN |
+ FL2000_VGA_CTRL_ACLK_EDID_MON_INT_EN;
+ mask = val | FL2000_VGA_CTRL_ACLK_FEEDBACK_INT_EN;
+ regmap_write_bits(regmap, FL2000_VGA_CTRL_REG_ACLK, mask, val);
+
+ 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;
+}
+
+/*
+ * 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 usb_device *usb_dev, u32 *status)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+ unsigned int val;
+ u32 mask;
+ int ret;
+
+ ret = regmap_read(regmap, FL2000_VGA_STATUS_REG, &val);
+ if (ret)
+ return ret;
+
+ /* LBUF issues are recoverable */
+ mask = val & (FL2000_VGA_STATUS_LBUF_OVERFLOW | FL2000_VGA_STATUS_LBUF_UNDERFLOW);
+ if (mask)
+ regmap_write_bits(regmap, FL2000_VGA_STATUS_REG, mask, val);
+
+ /* lbuf_halt and vga_error are not handled: no recovery sequence is known */
+
+ *status = val;
+
+ return 0;
+}
+
+int fl2000_i2c_dword(struct usb_device *usb_dev, bool read, u16 addr, u8 offset, u32 *data)
+{
+ int ret;
+ unsigned int val;
+ u32 mask;
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+
+ 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 usb_device *usb_dev)
+{
+ struct regmap *regmap = dev_get_regmap(&usb_dev->dev, NULL);
+
+ regcache_drop_region(regmap, 0, fl2000_regmap_config.max_register);
+}
+
+struct regmap *fl2000_regmap_init(struct usb_device *usb_dev)
+{
+ struct regmap *regmap;
+
+ regmap = devm_regmap_init(&usb_dev->dev, NULL, usb_dev, &fl2000_regmap_config);
+ if (IS_ERR(regmap))
+ dev_err(&usb_dev->dev, "Registers map failed (%pe)\n", regmap);
+
+ return regmap;
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_registers.h b/drivers/gpu/drm/fl2000/fl2000_registers.h
new file mode 100644
index 000000000..7ca977f1d
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_registers.h
@@ -0,0 +1,272 @@
+/* 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_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] 6+ messages in thread
* [PATCH RFC 2/4] drm/fl2000: Add ITE IT66121 HDMI transmitter support
2026-11-06 2:34 [PATCH RFC 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 1/4] drm/fl2000: Add USB driver with register and I2C access Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-06 2:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 3/4] drm/fl2000: Add frame streaming Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 4/4] drm/fl2000: Add KMS display pipeline Angel Gabriel Crispin Valdivia via B4 Relay
3 siblings, 0 replies; 6+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-06 2: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.
EDID is first read through the IT66121 DDC engine. On the tested
adapter, as on the Fresco Logic reference design, the monitor's DDC
lines are wired to the FL2000 I2C bus instead and that engine never
completes, so the driver falls back to reading the EDID at address 0x50
directly with the FL2000 I2C engine.
The bridge is registered as a component of the FL2000 I2C adapter and
lives as long as the USB device: re-creating it on every bind would
leave stale pointers in the component match data across USB resets.
Signed-off-by: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
---
drivers/gpu/drm/fl2000/Makefile | 1 +
drivers/gpu/drm/fl2000/fl2000_drv.c | 60 ++
drivers/gpu/drm/fl2000/fl2000_it66121.c | 1030 ++++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_it66121.h | 19 +
drivers/gpu/drm/fl2000/fl2000_it66121_regs.h | 283 +++++++
5 files changed, 1393 insertions(+)
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 ea089d3c1..107eba92c 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_CLASS_AV 0x10
#define USB_SUBCLASS_AV_CONTROL 0x01
@@ -24,6 +25,7 @@ MODULE_DEVICE_TABLE(usb, fl2000_id_table);
struct fl2000_devs {
struct regmap *regmap;
struct i2c_adapter *adapter;
+ struct i2c_client *bridge_client;
int active_if;
};
@@ -52,6 +54,44 @@ static struct fl2000_devs *fl2000_get_devices(struct usb_device *usb_dev)
return devs;
}
+static void fl2000_bridge_release(struct device *dev, void *res)
+{
+ struct i2c_client **client = res;
+
+ dev_dbg(dev, "Destroying IT66121 bridge\n");
+ it66121_destroy(*client);
+}
+
+/*
+ * Create the IT66121 bridge and tie its destruction to the usb_device
+ * lifetime: it must survive component master del/add cycles (USB resets) and
+ * die only when the USB device is actually gone. The devres is released
+ * before the I2C adapter devres (LIFO), so the client is destroyed while its
+ * adapter still exists.
+ */
+static int fl2000_bridge_create(struct usb_device *usb_dev, struct fl2000_devs *devs)
+{
+ struct i2c_client *client;
+ struct i2c_client **holder;
+
+ holder = devres_alloc(fl2000_bridge_release, sizeof(*holder), GFP_KERNEL);
+ if (!holder)
+ return -ENOMEM;
+
+ client = it66121_create(devs->adapter);
+ if (IS_ERR(client)) {
+ dev_err(&usb_dev->dev, "Cannot create HDMI bridge (%pe)\n", client);
+ devres_free(holder);
+ return -ENODEV;
+ }
+
+ *holder = client;
+ devres_add(&usb_dev->dev, holder);
+ devs->bridge_client = client;
+
+ return 0;
+}
+
static int fl2000_probe(struct usb_interface *interface, const struct usb_device_id *usb_dev_id)
{
int ret = 0;
@@ -70,6 +110,20 @@ static int fl2000_probe(struct usb_interface *interface, const struct usb_device
return PTR_ERR(devs);
}
+ /*
+ * Create the IT66121 bridge if it does not exist yet. Retrying from
+ * every interface probe covers transient detection failures on cold
+ * boot. Once created, the bridge component persists for the lifetime of
+ * the usb_device (across usb_reset_device() unbind/rebind cycles):
+ * destroying and re-creating it per bind cycle leaves dangling
+ * component pointers in the aggregate match data and crashes on rebind.
+ */
+ if (!devs->bridge_client) {
+ ret = fl2000_bridge_create(usb_dev, devs);
+ if (ret)
+ return ret;
+ }
+
switch (iface_num) {
case FL2000_USBIF_AVCONTROL:
case FL2000_USBIF_STREAMING:
@@ -96,6 +150,12 @@ static void fl2000_disconnect(struct usb_interface *interface)
if (!devs)
return;
+ /*
+ * The IT66121 bridge is intentionally NOT destroyed here: disconnect
+ * also runs for the transient unbind/rebind cycles of
+ * usb_reset_device(), where the usb_device survives. Its destruction is
+ * a devres of the usb_device, see fl2000_bridge_create().
+ */
switch (iface_num) {
case FL2000_USBIF_AVCONTROL:
case FL2000_USBIF_STREAMING:
diff --git a/drivers/gpu/drm/fl2000/fl2000_it66121.c b/drivers/gpu/drm/fl2000/fl2000_it66121.c
new file mode 100644
index 000000000..208f92823
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_it66121.c
@@ -0,0 +1,1030 @@
+// 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"
+
+/* HW defect leading to losing 3 first bytes during EDID read operation */
+#define EDID_LOSS_LEN 3
+
+#define VENDOR_ID 0x4954
+#define DEVICE_ID 0x0612
+#define REVISION_MASK 0xF000
+#define REVISION_SHIFT 12
+
+#define IRQ_POLL_INTRVL 100
+
+struct it66121_priv {
+ struct i2c_adapter *adapter;
+ struct i2c_client *client;
+ struct regmap *regmap;
+ struct drm_bridge bridge;
+
+ struct delayed_work work;
+ struct workqueue_struct *work_queue;
+
+ /* Serializes users of the DDC engine (multi-step EDID FIFO reads) */
+ struct mutex ddc_lock;
+ /* The EDID is read through the FL2000 bus, not the IT66121 FIFO, on this board */
+ bool edid_direct;
+ /* Set when the monitor went away: the cached EDID is no longer valid */
+ bool edid_stale;
+
+ struct regmap_field *irq_pending;
+ struct regmap_field *hpd;
+ struct regmap_field *clr_irq;
+
+ /* EDID of the monitor, kept for as long as it stays; NULL if it could not be read */
+ const struct drm_edid *drm_edid;
+};
+
+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 EDID_SLEEP 5000
+#define EDID_TIMEOUT 500000
+
+#define EDID_HDCP_ADDR 0x74
+#define EDID_DDC_ADDR 0xA0
+
+#define EDID_FIFO_SIZE 32
+
+enum it66121_ddc_cmd {
+ DDC_CMD_BURST_READ = 0x0,
+ DDC_CMD_LINK_CHECK = 0x2,
+ DDC_CMD_EDID_READ = 0x3,
+ DDC_CMD_ASKV_WRITE = 0x4,
+ DDC_CMD_AINFO_WRITE = 0x5,
+ DDC_CMD_AN_WRITE = 0x6,
+ DDC_CMD_FIFO_CLEAR = 0x9,
+ DDC_CMD_SCL_PULSE = 0xA,
+ 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, 0x80);
+ 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)
+#define DDC_STATUS_ACTIVE BIT(6)
+#define DDC_STATUS_ERROR_MASK GENMASK(5, 3)
+
+static int it66121_wait_ddc_ready(struct it66121_priv *priv)
+{
+ int ret;
+ unsigned int val;
+
+ /*
+ * 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, EDID_SLEEP, EDID_TIMEOUT);
+ if (ret) {
+ dev_err(&priv->client->dev, "DDC not done (%d), status 0x%02X\n", ret, val);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int it66121_clear_ddc_fifo(struct it66121_priv *priv)
+{
+ int ret;
+
+ ret = regmap_write(priv->regmap, IT66121_DDC_COMMAND, DDC_CMD_FIFO_CLEAR);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int it66121_abort_ddc_ops(struct it66121_priv *priv)
+{
+ int ret;
+ unsigned int swreset, cpdesire;
+
+ /*
+ * The HDCP engine shares the DDC master and holds the bus after chip
+ * reset (DDC status reports 'wait bus' + 'arbitration lose', 0x1A).
+ * 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.
+ */
+ ret = regmap_read(priv->regmap, IT66121_SW_RST, &swreset);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(priv->regmap, IT66121_HDCP_CONTROL, &cpdesire);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(priv->regmap, IT66121_HDCP_CONTROL,
+ cpdesire & ~IT66121_HDCP_CP_DESIRE);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(priv->regmap, IT66121_SW_RST, swreset | 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 ret;
+
+ ret = regmap_field_read(priv->hpd, &val);
+ if (ret)
+ return ret;
+
+ return !!val;
+}
+
+/*
+ * Acknowledges pending IT66121 interrupts. The IT66121 interrupt line is
+ * routed to the FL2000, which reports it on its own interrupt endpoint (see
+ * fl2000_interrupt.c); hotplug is handled from there, by re-probing the
+ * connector. The interrupt sources themselves are not decoded here.
+ */
+static void it66121_intr_work(struct work_struct *work_item)
+{
+ int ret;
+ unsigned int val;
+ struct delayed_work *dwork = to_delayed_work(work_item);
+ struct it66121_priv *priv = container_of(dwork, struct it66121_priv, work);
+ /* Do not use bridge.dev here: it is NULL while the bridge is detached */
+ struct device *dev = &priv->client->dev;
+
+ ret = regmap_field_read(priv->irq_pending, &val);
+ if (ret) {
+ dev_err(dev, "Cannot read interrupt status (%d)\n", ret);
+ return;
+ }
+
+ if (val) {
+ ret = regmap_read(priv->regmap, IT66121_INT_STATUS_1, &val);
+ if (ret) {
+ dev_err(dev, "Cannot read IT66121_INT_STATUS_1 (%d)\n", ret);
+ return;
+ }
+
+ regmap_field_write(priv->clr_irq, 1);
+ }
+
+ queue_delayed_work(priv->work_queue, &priv->work, msecs_to_jiffies(IRQ_POLL_INTRVL));
+}
+
+/*
+ * EDID read through the IT66121 DDC master (EDID FIFO command), for boards
+ * where the monitor's DDC lines are wired to the IT66121.
+ */
+static int it66121_do_get_edid_block(struct it66121_priv *priv, u8 *buf, unsigned int block,
+ size_t len)
+{
+ int ret;
+ size_t remain = len;
+ unsigned int val;
+ unsigned int segment = block >> 1;
+ unsigned int offset = (block & 1) ? 128 : 0;
+ static const u8 header[EDID_LOSS_LEN] = { 0x00, 0xFF, 0xFF };
+
+ /* Abort any ongoing DDC operation */
+ ret = it66121_abort_ddc_ops(priv);
+ if (ret)
+ return ret;
+
+ /*
+ * Statically fill first 3 bytes (due to EDID reading HW bug they are
+ * lost, but they are constant in any valid EDID header).
+ */
+ while (offset < EDID_LOSS_LEN && remain > 0) {
+ *(buf++) = header[offset];
+ remain--;
+ offset++;
+ }
+
+ while (remain > 0) {
+ /* Add bytes that will be lost during EDID read */
+ size_t size = remain + EDID_LOSS_LEN;
+
+ /* ... and check size fits FIFO */
+ size = min_t(size_t, size, EDID_FIFO_SIZE);
+
+ ret = it66121_clear_ddc_fifo(priv);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(priv->regmap, IT66121_DDC_ADDRESS, EDID_DDC_ADDR);
+ if (ret)
+ return ret;
+
+ /* Account 3 bytes that will be lost */
+ ret = regmap_write(priv->regmap, IT66121_DDC_OFFSET, offset - EDID_LOSS_LEN);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(priv->regmap, IT66121_DDC_SIZE, (unsigned int)size);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(priv->regmap, IT66121_DDC_SEGMENT, segment);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(priv->regmap, IT66121_DDC_COMMAND, DDC_CMD_EDID_READ);
+ if (ret)
+ return ret;
+
+ ret = it66121_wait_ddc_ready(priv);
+ if (ret) {
+ it66121_abort_ddc_ops(priv);
+ return ret;
+ }
+
+ /* Deduct lost bytes when reading from FIFO */
+ size -= EDID_LOSS_LEN;
+
+ for (size_t i = 0; i < size; i++) {
+ ret = regmap_read(priv->regmap, IT66121_DDC_RD_FIFO, &val);
+ if (ret)
+ return ret;
+
+ *(buf++) = val & 0xFF;
+ }
+
+ remain -= size;
+ offset += size;
+ }
+
+ return 0;
+}
+
+static int it66121_get_edid_block(void *context, u8 *buf, unsigned int block, size_t len)
+{
+ struct it66121_priv *priv = context;
+ int ret;
+
+ mutex_lock(&priv->ddc_lock);
+ ret = it66121_do_get_edid_block(priv, buf, block, len);
+ mutex_unlock(&priv->ddc_lock);
+
+ if (ret)
+ dev_dbg(&priv->client->dev, "EDID block %u read failed (%d)\n", block, ret);
+
+ return ret;
+}
+
+/*
+ * Fallback EDID path: on Fresco reference designs the monitor's DDC lines hang
+ * directly off the FL2000 I2C bus, and the vendor driver reads EDID at address
+ * 0x50 with the FL2000's own dword I2C engine, bypassing the IT66121 DDC
+ * master entirely. The engine reads 4 bytes per operation with an 8-bit
+ * offset, which covers EDID blocks 0 and 1.
+ */
+static int it66121_get_edid_block_direct(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, "Direct EDID block %u read failed (%d)\n", block, ret);
+
+ return ret;
+}
+
+static void it66121_read_edid(struct it66121_priv *priv, struct drm_connector *connector)
+{
+ int (*first)(void *context, u8 *buf, unsigned int block, size_t len);
+ int (*second)(void *context, u8 *buf, unsigned int block, size_t len);
+
+ /*
+ * Board wirings differ: try the direct FL2000-bus DDC read if the
+ * IT66121 EDID FIFO path did not work, and start with whichever worked
+ * last time so the FIFO path's timeouts are paid once.
+ */
+ if (priv->edid_direct) {
+ first = it66121_get_edid_block_direct;
+ second = it66121_get_edid_block;
+ } else {
+ first = it66121_get_edid_block;
+ second = it66121_get_edid_block_direct;
+ }
+
+ drm_edid_free(priv->drm_edid);
+ priv->drm_edid = drm_edid_read_custom(connector, first, priv);
+ if (!priv->drm_edid) {
+ dev_dbg(&priv->client->dev, "EDID read failed, trying the other DDC path\n");
+ priv->drm_edid = drm_edid_read_custom(connector, second, priv);
+ if (priv->drm_edid)
+ priv->edid_direct = !priv->edid_direct;
+ }
+}
+
+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;
+
+ /* Whatever comes next may be another monitor: its EDID must be read again */
+ WRITE_ONCE(priv->edid_stale, true);
+
+ 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;
+}
+
+/*
+ * Reading the EDID takes about a second over USB, and the connector asks for it on every probe,
+ * with the mode config locked: keep it until the monitor goes away (see it66121_bridge_detect())
+ * and hand out copies. It must still be handed over every time: the DRM core drops the
+ * connector's copy when the connector goes through '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);
+
+ if (!priv->drm_edid || READ_ONCE(priv->edid_stale)) {
+ WRITE_ONCE(priv->edid_stale, false);
+ it66121_read_edid(priv, connector);
+ if (!priv->drm_edid)
+ dev_warn(&priv->client->dev, "EDID read failed, using fallback modes\n");
+ }
+
+ return drm_edid_dup(priv->drm_edid);
+}
+
+static int it66121_bind(struct device *comp, struct device *master, void *master_data)
+{
+ int ret;
+ struct drm_bridge *bridge = dev_get_drvdata(comp);
+ struct drm_device *drm = master_data;
+ struct i2c_adapter *adapter = i2c_verify_adapter(master);
+ struct drm_encoder *encoder;
+
+ if (!adapter)
+ return -ENODEV;
+
+ /* The FL2000 has a single encoder */
+ encoder = list_first_entry_or_null(&drm->mode_config.encoder_list, struct drm_encoder,
+ head);
+ if (!encoder)
+ return -ENODEV;
+
+ dev_dbg(comp, "Binding IT66121 component\n");
+
+ /* The display driver makes the connector, from what the bridge reports */
+ ret = drm_bridge_attach(encoder, bridge, NULL, DRM_BRIDGE_ATTACH_NO_CONNECTOR);
+ if (ret)
+ dev_err(comp, "Cannot attach IT66121 bridge (%d)\n", ret);
+
+ return ret;
+}
+
+static void it66121_unbind(struct device *comp, struct device *master, void *master_data)
+{
+ /* The bridge is detached by the DRM core when the encoder is cleaned up */
+ dev_dbg(comp, "Unbinding IT66121 component\n");
+}
+
+static const struct component_ops it66121_component_ops = {
+ .bind = it66121_bind,
+ .unbind = it66121_unbind,
+};
+
+/*
+ * 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);
+}
+
+/* Unmask the DDC interrupt and start acknowledging the chip's interrupts */
+static void it66121_start_interrupts(struct it66121_priv *priv)
+{
+ regmap_write_bits(priv->regmap, IT66121_INT_MASK_1, IT66121_MASK_DDC, 0);
+ queue_delayed_work(priv->work_queue, &priv->work, msecs_to_jiffies(IRQ_POLL_INTRVL));
+}
+
+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_kms(bridge->dev, "Bridge attached\n");
+
+ return 0;
+}
+
+static void it66121_bridge_detach(struct drm_bridge *bridge)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+
+ /*
+ * Stop the HPD polling work started by attach so it cannot notify a
+ * bridge that is no longer attached to a DRM device.
+ */
+ cancel_delayed_work_sync(&priv->work);
+}
+
+/**
+ * it66121_suspend() - stop talking to the IT66121 before system sleep
+ * @bridge: the IT66121 bridge
+ *
+ * Its interrupt polling would fail while the USB device sleeps, and a failed read ends it.
+ */
+void it66121_suspend(struct drm_bridge *bridge)
+{
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+
+ cancel_delayed_work_sync(&priv->work);
+}
+
+/**
+ * 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);
+
+ /* Another monitor may have been plugged in meanwhile */
+ WRITE_ONCE(priv->edid_stale, true);
+}
+
+/*
+ * 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_kms(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;
+
+ 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;
+
+ it66121_set_mode(priv, connector, &crtc_state->mode);
+
+ /* Unmute AV */
+ regmap_write(priv->regmap, IT66121_HDMI_AV_MUTE, 0);
+}
+
+static void it66121_bridge_atomic_disable(struct drm_bridge *bridge,
+ struct drm_atomic_commit *state)
+{
+ int ret;
+ struct it66121_priv *priv = container_of(bridge, struct it66121_priv, bridge);
+
+ /* Mute AV */
+ ret = regmap_write_bits(priv->regmap, IT66121_HDMI_AV_MUTE, IT66121_HDMI_AV_MUTE_ON,
+ IT66121_HDMI_AV_MUTE_ON);
+ if (ret)
+ return;
+}
+
+static const struct drm_bridge_funcs it66121_bridge_funcs = {
+ .attach = it66121_bridge_attach,
+ .detach = it66121_bridge_detach,
+ .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;
+}
+
+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);
+
+ /* We rely on full I2C protocol + 1 byte SMBUS read for detection */
+ ret = i2c_check_functionality(adapter, I2C_FUNC_I2C | I2C_FUNC_SMBUS_READ_BYTE);
+ if (!ret) {
+ dev_dbg(&adapter->dev, "Adapter does not support I2C functions properly\n");
+ return -ENODEV;
+ }
+
+ 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 < 0) {
+ dev_err(&adapter->dev, "I2C transfer failed (%d)\n", ret);
+ return -ENODEV;
+ }
+ }
+
+ vendor = get_unaligned_le16(&id[0]);
+ device = get_unaligned_le16(&id[2]);
+ if (vendor != VENDOR_ID || (device & ~REVISION_MASK) != DEVICE_ID) {
+ dev_dbg(&adapter->dev, "IT66121 not found (0x%X-0x%X)\n", vendor, device);
+ return -ENODEV;
+ }
+
+ dev_dbg(&adapter->dev, "IT66121 found, revision %d\n",
+ (device & REVISION_MASK) >> REVISION_SHIFT);
+
+ return 0;
+}
+
+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 };
+
+ return i2c_new_scanned_device(adapter, &board_info, it66121_addr, it66121_i2c_probe);
+}
+
+void it66121_destroy(struct i2c_client *client)
+{
+ struct drm_bridge *bridge;
+ struct it66121_priv *priv;
+
+ if (!client)
+ return;
+
+ bridge = dev_get_drvdata(&client->dev);
+ priv = container_of(bridge, struct it66121_priv, bridge);
+
+ cancel_delayed_work_sync(&priv->work);
+
+ destroy_workqueue(priv->work_queue);
+
+ component_del(&priv->client->dev, &it66121_component_ops);
+
+ drm_edid_free(priv->drm_edid);
+ priv->drm_edid = NULL;
+
+ drm_bridge_remove(&priv->bridge);
+
+ /*
+ * Drops the devm reference to the bridge; priv is freed together with
+ * the bridge once DRM releases the remaining references.
+ */
+ i2c_unregister_device(priv->client);
+}
+
+struct i2c_client *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, "Cannot find IT66121 I2C client\n");
+ 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);
+ }
+
+ INIT_DELAYED_WORK(&priv->work, &it66121_intr_work);
+ mutex_init(&priv->ddc_lock);
+
+ /*
+ * 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);
+
+ /* Setup work queue for interrupt processing work */
+ priv->work_queue = alloc_ordered_workqueue("it66121_work", 0);
+ if (!priv->work_queue) {
+ drm_bridge_remove(&priv->bridge);
+ i2c_unregister_device(client);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ /* Store bridge pointer at I2C client device for further binding */
+ dev_set_drvdata(&priv->client->dev, &priv->bridge);
+
+ /* Bind component of I2C client to master I2C adapter */
+ ret = component_add(&priv->client->dev, &it66121_component_ops);
+ if (ret) {
+ dev_err(&client->dev, "Cannot register IT66121 component (%d)\n", ret);
+ destroy_workqueue(priv->work_queue);
+ drm_bridge_remove(&priv->bridge);
+ i2c_unregister_device(client);
+ return ERR_PTR(ret);
+ }
+
+ return client;
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_it66121.h b/drivers/gpu/drm/fl2000/fl2000_it66121.h
new file mode 100644
index 000000000..72c0fe18b
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_it66121.h
@@ -0,0 +1,19 @@
+/* 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 i2c_client;
+
+struct i2c_client *it66121_create(struct i2c_adapter *adapter);
+void it66121_destroy(struct i2c_client *client);
+int it66121_hpd_connected(struct drm_bridge *bridge);
+void it66121_suspend(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..6f19fb2a1
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_it66121_regs.h
@@ -0,0 +1,283 @@
+/* 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_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] 6+ messages in thread
* [PATCH RFC 3/4] drm/fl2000: Add frame streaming
2026-11-06 2:34 [PATCH RFC 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 1/4] drm/fl2000: Add USB driver with register and I2C access Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 2/4] drm/fl2000: Add ITE IT66121 HDMI transmitter support Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-06 2:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 4/4] drm/fl2000: Add KMS display pipeline Angel Gabriel Crispin Valdivia via B4 Relay
3 siblings, 0 replies; 6+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-06 2: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, after
switching the AV control interface to altsetting 1. 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 framebuffer 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. A cursor sprite is blended into each outgoing buffer rather
than into the shadow, so moving the pointer does not touch the
framebuffer.
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 | 67 ++
drivers/gpu/drm/fl2000/fl2000_streaming.c | 993 ++++++++++++++++++++++++++++++
3 files changed, 1062 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 956032479..d800d943e 100644
--- a/drivers/gpu/drm/fl2000/fl2000.h
+++ b/drivers/gpu/drm/fl2000/fl2000.h
@@ -42,6 +42,48 @@ enum fl2000_interface {
FL2000_USBIF_INTERRUPT = 2,
};
+/* Process context only: may sleep between retries */
+static inline int fl2000_submit_urb(struct urb *urb)
+{
+ int ret;
+ int attempts = 10;
+
+ do {
+ ret = usb_submit_urb(urb, GFP_KERNEL);
+ switch (ret) {
+ case -ENXIO:
+ case -ENOMEM:
+ if (attempts--) {
+ cond_resched();
+ ret = -EAGAIN;
+ }
+ break;
+ default:
+ break;
+ }
+ } while (ret == -EAGAIN);
+
+ return ret;
+}
+
+/* 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;
@@ -64,6 +106,31 @@ 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 usb_device *usb_dev);
+
+/* 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,
+ bool imported, u32 format, unsigned int width, unsigned int height,
+ unsigned int pitch, const struct drm_rect *clip);
+void fl2000_stream_note_render(struct fl2000_stream *stream, unsigned int conv_us,
+ unsigned int damage_px);
+
+/* Largest cursor sprite the blend path accepts, advertised to userspace */
+#define FL2000_CURSOR_MAX 64
+
+void fl2000_stream_set_cursor(struct fl2000_stream *stream, const u32 *argb, unsigned int w,
+ unsigned int h, int x, int y);
+void fl2000_stream_move_cursor(struct fl2000_stream *stream, int x, int y);
+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_device *usb_dev);
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..2d2c30cb7
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_streaming.c
@@ -0,0 +1,993 @@
+// 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,
+ * with the cursor blended in on the way out.
+ *
+ * (C) Copyright 2017, Fresco Logic, Incorporated.
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#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_URB_TIMEOUT 100
+
+/*
+ * 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_buf {
+ struct list_head list;
+ /*
+ * Generation of the desktop content and of the cursor this buffer was
+ * built from. Both matching means the buffer can be resent as it is.
+ */
+ u32 seq;
+ u32 cursor_seq;
+ /* Where the cursor was blended in, so it can be lifted again */
+ struct drm_rect cursor_rect;
+ 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 shadow and cursor 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];
+ /*
+ * Bumped by cursor changes only, so moving the pointer never
+ * invalidates the desktop content a buffer already holds.
+ */
+ u32 cursor_seq;
+ /*
+ * 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;
+ /* Cursor sprite in ARGB8888, blended on the way out */
+ u32 *cursor_argb;
+ unsigned int cursor_w;
+ unsigned int cursor_h;
+ int cursor_x;
+ int cursor_y;
+ 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 XRGB8888 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(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->nr_pages = nr_pages;
+
+ sb->pages = kzalloc_objs(*sb->pages, nr_pages);
+ if (!sb->pages)
+ goto error;
+
+ 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(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_release(struct device *dev, void *res)
+{
+ struct fl2000_stream *stream = res;
+
+ fl2000_stream_disable(stream);
+ destroy_workqueue(stream->work_queue);
+ fl2000_stream_put_buffers(stream);
+ vfree(stream->shadow);
+ kfree(stream->cursor_argb);
+ kfree(stream->line_buf);
+ mutex_destroy(&stream->shadow_lock);
+}
+
+static void fl2000_stream_data_completion(struct urb *urb)
+{
+ struct fl2000_stream_buf *cur_sb = urb->transfer_buffer;
+ struct usb_device *usb_dev = urb->dev;
+ struct fl2000_stream *stream = urb->context;
+ unsigned long flags;
+
+ if (stream) {
+ 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(usb_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.
+ */
+static int fl2000_stream_submit(struct fl2000_stream *stream, struct urb *urb)
+{
+ int ret;
+
+ usb_anchor_urb(urb, &stream->anchor);
+ ret = fl2000_submit_urb(urb);
+ 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.
+ *
+ * Cursor compositing. The sprite is blended into each outgoing frame rather
+ * than into the shadow, so moving the pointer costs one 64x64 blend and never
+ * re-reads the source framebuffer.
+ */
+static inline u8 fl2000_blend_channel(u8 dst, u8 src, u8 alpha)
+{
+ return (u8)((src * alpha + dst * (255 - alpha) + 127) / 255);
+}
+
+/* 'line' is the offset of the line in the frame, in 16-bit words */
+static void fl2000_blend_cursor_rgb565_line(u16 *frame, size_t line, const u32 *cbuf, u32 x_start,
+ u32 x_end)
+{
+ for (unsigned int x = x_start; x < x_end; x++) {
+ size_t idx = (line + x) ^ 2;
+ u32 pix = *cbuf++;
+ u8 alpha = (u8)(pix >> 24);
+ u16 dst;
+ u8 r, g, b;
+
+ if (!alpha)
+ continue;
+
+ /* Expand the destination to 8 bits per channel, blend, pack again */
+ dst = frame[idx];
+ r = (u8)(((dst & 0xF800) >> 8) | ((dst & 0xF800) >> 13));
+ g = (u8)(((dst & 0x07E0) >> 3) | ((dst & 0x07E0) >> 9));
+ b = (u8)(((dst & 0x001F) << 3) | ((dst & 0x001F) >> 2));
+
+ r = fl2000_blend_channel(r, (u8)(pix >> 16), alpha);
+ g = fl2000_blend_channel(g, (u8)(pix >> 8), alpha);
+ b = fl2000_blend_channel(b, (u8)pix, alpha);
+
+ frame[idx] = (u16)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
+ }
+}
+
+/* 'line' is the offset of the line in the frame, in bytes */
+static void fl2000_blend_cursor_rgb888_line(u8 *frame, size_t line, const u32 *cbuf, 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 = *cbuf++;
+ u8 alpha = (u8)(pix >> 24);
+
+ if (!alpha) {
+ xx += 3;
+ continue;
+ }
+
+ /* Read and write through the swizzle */
+ frame[xx ^ 4] = fl2000_blend_channel(frame[xx ^ 4], (u8)pix, alpha);
+ xx++;
+ frame[xx ^ 4] = fl2000_blend_channel(frame[xx ^ 4], (u8)(pix >> 8), alpha);
+ xx++;
+ frame[xx ^ 4] = fl2000_blend_channel(frame[xx ^ 4], (u8)(pix >> 16), alpha);
+ xx++;
+ }
+}
+
+/*
+ * Blend the cursor into 'dst' and report the area it covered in 'rect', so the
+ * next rebuild knows exactly what to restore.
+ */
+static void fl2000_blend_cursor(struct fl2000_stream *stream, void *dst, struct drm_rect *rect)
+{
+ u32 dst_line_len = stream->width * stream->bytes_pix;
+ int cy_start, cy_end, cx_start, cx_end;
+
+ *rect = (struct drm_rect){ 0, 0, 0, 0 };
+
+ if (!stream->cursor_w || !stream->cursor_h || !stream->width)
+ return;
+
+ /* The cursor may hang off any edge of the frame */
+ cx_start = max(stream->cursor_x, 0);
+ cy_start = max(stream->cursor_y, 0);
+ cx_end = min(stream->cursor_x + (int)stream->cursor_w, (int)stream->width);
+ cy_end = min(stream->cursor_y + (int)stream->cursor_h, (int)stream->height);
+ if (cx_start >= cx_end || cy_start >= cy_end)
+ return;
+
+ *rect = (struct drm_rect){ cx_start, cy_start, cx_end, cy_end };
+
+ for (int y = cy_start; y < cy_end; y++) {
+ const u32 *cline = stream->cursor_argb +
+ (size_t)(y - stream->cursor_y) * stream->cursor_w +
+ (cx_start - stream->cursor_x);
+
+ if (stream->bytes_pix == 2)
+ fl2000_blend_cursor_rgb565_line(dst, (size_t)y * stream->width, cline,
+ cx_start, cx_end);
+ else if (stream->bytes_pix == 3)
+ fl2000_blend_cursor_rgb888_line(dst, (size_t)y * dst_line_len, cline,
+ cx_start, cx_end);
+ }
+}
+
+/*
+ * 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 plus
+ * the cursor. A buffer that is only a few generations behind replays the
+ * damage rectangles since, rather than copying the whole frame, and a buffer
+ * whose desktop content is current only has the cursor lifted and re-blended.
+ */
+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 || sb->cursor_seq != newest->cursor_seq)) {
+ memcpy(sb->vaddr, newest->vaddr, stream->buf_size);
+ sb->seq = newest->seq;
+ sb->cursor_seq = newest->cursor_seq;
+ sb->cursor_rect = newest->cursor_rect;
+ }
+
+ 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;
+ /* The copy may have overwritten part of the blended cursor */
+ sb->cursor_seq = 0;
+ }
+
+ if (sb->cursor_seq != stream->cursor_seq) {
+ /* Lift the old cursor by restoring the desktop underneath it */
+ fl2000_copy_rect(stream, sb->vaddr, &sb->cursor_rect);
+ fl2000_blend_cursor(stream, sb->vaddr, &sb->cursor_rect);
+ sb->cursor_seq = stream->cursor_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;
+ }
+
+ /*
+ * Endpoint 1 bulk out. We store pointer to current stream buffer structure in
+ * transfer_buffer field of URB which is unused due to SGT.
+ */
+ usb_fill_bulk_urb(data_urb, usb_dev, usb_sndbulkpipe(usb_dev, 1), cur_sb,
+ (int)stream->buf_size, fl2000_stream_data_completion, stream);
+ 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, stream);
+
+ ret = fl2000_stream_submit(stream, data_urb);
+ if (ret)
+ usb_free_urb(eof_urb);
+ else
+ ret = fl2000_stream_submit(stream, eof_urb);
+ if (ret) {
+ 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_xrgb8888_to_rgb565_line(u16 *frame, size_t line, const u32 *sbuf, u32 x_start,
+ u32 x_end)
+{
+ for (unsigned int x = x_start; x < x_end; x++) {
+ u32 pix = *sbuf++;
+
+ frame[(line + x) ^ 2] = ((pix & 0x00F80000) >> 8) | ((pix & 0x0000FC00) >> 5) |
+ ((pix & 0x000000F8) >> 3);
+ }
+}
+
+/*
+ * The 10-bit formats keep the colour in bits 29:0 (bits 31:30 are unused or
+ * alpha, which the primary plane ignores). Drop the two low bits of each
+ * channel, in place, so the converters above only deal with XRGB8888.
+ */
+static void fl2000_xrgb2101010_to_xrgb8888_line(u32 *buf, u32 x_start, u32 x_end)
+{
+ for (unsigned int x = x_start; x < x_end; x++) {
+ u32 pix = buf[x - x_start];
+
+ buf[x - x_start] = (((pix >> 22) & 0xFF) << 16) | (((pix >> 12) & 0xFF) << 8) |
+ ((pix >> 2) & 0xFF);
+ }
+}
+
+/*
+ * 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. 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.
+ */
+void fl2000_stream_compress(struct fl2000_stream *stream, const struct iosys_map *src_map,
+ bool imported, u32 format, unsigned int width, unsigned int height,
+ unsigned int pitch, const struct drm_rect *clip)
+{
+ bool wc = imported || src_map->is_iomem;
+ u32 dst_line_len;
+ u32 x_start, x_end, y_start, y_end;
+ size_t span;
+
+ x_start = min_t(u32, max_t(int, clip->x1, 0), width);
+ x_end = min_t(u32, max_t(int, clip->x2, 0), width);
+ y_start = min_t(u32, max_t(int, clip->y1, 0), height);
+ y_end = min_t(u32, max_t(int, clip->y2, 0), height);
+ if (x_start >= x_end || y_start >= y_end)
+ return;
+
+ guard(mutex)(&stream->shadow_lock);
+
+ if (!stream->shadow || width != stream->width || height != stream->height)
+ return;
+
+ /*
+ * Streaming loads need 16-byte alignment, which is 4 source pixels.
+ * Widening the span converts a few pixels twice, which is free next to
+ * losing the fast path.
+ */
+ x_start &= ~3U;
+ x_end = min(ALIGN(x_end, 4), width);
+
+ dst_line_len = width * stream->bytes_pix;
+ span = (x_end - x_start) * sizeof(u32);
+
+ 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 * pitch + (size_t)x_start * sizeof(u32), span);
+
+ if (format != DRM_FORMAT_XRGB8888)
+ fl2000_xrgb2101010_to_xrgb8888_line(stream->line_buf, x_start, x_end);
+
+ switch (stream->bytes_pix) {
+ case 2:
+ fl2000_xrgb8888_to_rgb565_line(stream->shadow, (size_t)y * width,
+ stream->line_buf, x_start, x_end);
+ break;
+ case 3:
+ fl2000_xrgb8888_to_rgb888_line(stream->shadow, (size_t)y * dst_line_len,
+ stream->line_buf, x_start, x_end);
+ break;
+ default:
+ /* Rejected by fl2000_get_bytes_pix() */
+ break;
+ }
+ }
+
+ stream->shadow_seq++;
+ stream->damage_log[stream->shadow_seq % FL2000_DAMAGE_LOG] =
+ (struct drm_rect){ x_start, y_start, x_end, y_end };
+}
+
+/*
+ * Hand the cursor sprite and its position to the send path. 'argb' may be NULL
+ * to hide the cursor.
+ */
+void fl2000_stream_set_cursor(struct fl2000_stream *stream, const u32 *argb, unsigned int w,
+ unsigned int h, int x, int y)
+{
+ if (w > FL2000_CURSOR_MAX || h > FL2000_CURSOR_MAX)
+ return;
+
+ guard(mutex)(&stream->shadow_lock);
+
+ /* Nothing visible before or after: do not rebuild every outgoing frame */
+ if (!argb && !stream->cursor_w)
+ return;
+
+ if (argb) {
+ memcpy(stream->cursor_argb, argb, (size_t)w * h * sizeof(u32));
+ stream->cursor_w = w;
+ stream->cursor_h = h;
+ stream->cursor_x = x;
+ stream->cursor_y = y;
+ } else {
+ stream->cursor_w = 0;
+ stream->cursor_h = 0;
+ }
+
+ stream->cursor_seq++;
+}
+
+/* Move the cursor without touching its image, the common case by far */
+void fl2000_stream_move_cursor(struct fl2000_stream *stream, int x, int y)
+{
+ guard(mutex)(&stream->shadow_lock);
+
+ if (!stream->cursor_w || (stream->cursor_x == x && stream->cursor_y == y))
+ return;
+
+ stream->cursor_x = x;
+ stream->cursor_y = y;
+ stream->cursor_seq++;
+}
+
+int fl2000_stream_mode_set(struct fl2000_stream *stream, unsigned int width,
+ unsigned int height, u32 bytes_pix)
+{
+ int ret;
+ unsigned int size;
+ void *shadow;
+
+ /* Round buffer size up to multiple of 8 to meet HW expectations */
+ size = ALIGN(width * height * bytes_pix, 8);
+
+ guard(mutex)(&stream->shadow_lock);
+
+ stream->bytes_pix = bytes_pix;
+ stream->width = width;
+ stream->height = height;
+
+ /* If there are buffers with same size - keep them */
+ if (stream->buf_size == size)
+ return 0;
+
+ /* Destroy wrong size buffers if they exist */
+ if (!list_empty(&stream->render_list))
+ fl2000_stream_put_buffers(stream);
+
+ /* Allocate new buffers */
+ ret = fl2000_stream_get_buffers(stream, size);
+ if (ret) {
+ fl2000_stream_put_buffers(stream);
+ stream->buf_size = 0;
+ return ret;
+ }
+
+ /* One full frame in the device format; only the CPU ever reads it */
+ shadow = vzalloc(size);
+ if (!shadow) {
+ fl2000_stream_put_buffers(stream);
+ stream->buf_size = 0;
+ return -ENOMEM;
+ }
+ vfree(stream->shadow);
+ stream->shadow = shadow;
+ /*
+ * Both generations move on from zero: a buffer carrying zero has never
+ * been built and always takes the full-copy path.
+ */
+ stream->shadow_seq++;
+ stream->cursor_seq++;
+
+ stream->buf_size = size;
+
+ return 0;
+}
+
+int fl2000_stream_enable(struct fl2000_stream *stream)
+{
+ if (WARN_ON(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 on DRM master bind. It shall 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;
+
+ /* Altsetting 1 on interface 0 */
+ ret = usb_set_interface(usb_dev, FL2000_USBIF_AVCONTROL, 1);
+ if (ret) {
+ dev_err(&usb_dev->dev, "Cannot set streaming interface for bulk transfers\n");
+ return ERR_PTR(ret);
+ }
+
+ stream = devres_alloc(&fl2000_stream_release, sizeof(*stream), GFP_KERNEL);
+ 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);
+ /* The largest cursor, allocated once: the atomic update must not fail */
+ stream->cursor_argb = kmalloc_objs(*stream->cursor_argb,
+ FL2000_CURSOR_MAX * FL2000_CURSOR_MAX);
+ stream->work_queue = alloc_ordered_workqueue("fl2000_stream", 0);
+ if (!stream->line_buf || !stream->cursor_argb || !stream->work_queue) {
+ if (stream->work_queue)
+ destroy_workqueue(stream->work_queue);
+ kfree(stream->cursor_argb);
+ kfree(stream->line_buf);
+ mutex_destroy(&stream->shadow_lock);
+ devres_free(stream);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ devres_add(&usb_dev->dev, stream);
+
+ return stream;
+}
+
+void fl2000_stream_destroy(struct usb_device *usb_dev)
+{
+ devres_release(&usb_dev->dev, fl2000_stream_release, NULL, NULL);
+}
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH RFC 4/4] drm/fl2000: Add KMS display pipeline
2026-11-06 2:34 [PATCH RFC 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
` (2 preceding siblings ...)
2026-11-06 2:34 ` [PATCH RFC 3/4] drm/fl2000: Add frame streaming Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-06 2:34 ` Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 9:59 ` Thomas Zimmermann
3 siblings, 1 reply; 6+ messages in thread
From: Angel Gabriel Crispin Valdivia via B4 Relay @ 2026-11-06 2: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 once all FL2000 interfaces are bound and the
IT66121 component is available, and connect the pieces:
- a primary plane, a cursor plane, a CRTC and an encoder, with GEM
SHMEM framebuffers accessed through the shadow-plane helpers and
XRGB8888 (and the two 10-bit formats compositors render in, reduced
to 8 bits) as formats. The framebuffer is only read by the CPU, so it
does not need to be physically contiguous. The primary plane passes
its merged damage to the stream; the cursor plane hands over its
sprite, or only its position when the framebuffer is unchanged.
Without a cursor plane, compositors keep the pointer on the primary
GPU and it never appears on this output
- mode validation and PLL search in the CRTC's atomic_check (which
adjusts the clock in adjusted_mode), 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, kept while the monitor stays, and
without one the standard timings are offered
- vblank from the DRM vblank timer, as the FL2000 has no vblank
interrupt, and fbdev emulation
- the FL2000 interrupt endpoint, which reports monitor and IT66121
events and triggers a hotplug event
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
and a GNOME Wayland desktop work, with the cursor, at 59 frames per
second on the link and no measurable effect on the rest of the system.
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 | 37 ++
drivers/gpu/drm/fl2000/fl2000_drm.c | 986 ++++++++++++++++++++++++++++++
drivers/gpu/drm/fl2000/fl2000_drv.c | 93 ++-
drivers/gpu/drm/fl2000/fl2000_interrupt.c | 237 +++++++
drivers/gpu/drm/fl2000/fl2000_vga.c | 256 ++++++++
6 files changed, 1602 insertions(+), 12 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 d800d943e..ee452576e 100644
--- a/drivers/gpu/drm/fl2000/fl2000.h
+++ b/drivers/gpu/drm/fl2000/fl2000.h
@@ -131,6 +131,36 @@ void fl2000_stream_move_cursor(struct fl2000_stream *stream, int x, int y);
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 drm_device *drm);
+void fl2000_intr_quiesce(struct fl2000_intr *intr);
+void fl2000_intr_suspend(struct fl2000_intr *intr);
+int fl2000_intr_resume(struct fl2000_intr *intr);
+int fl2000_intr_monitor_connected(struct fl2000_intr *intr);
+void fl2000_intr_destroy(struct usb_device *usb_dev);
+
+/* VGA output: a DAC encoder and connector next to the HDMI ones, on the same CRTC */
+struct drm_edid;
+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;
+ /* EDID of the monitor, kept for as long as it stays connected; NULL if it has none */
+ const struct drm_edid *edid;
+ unsigned int edid_tries;
+ /* The monitor changed or went away: read the EDID again on the next probe */
+ bool edid_stale;
+};
+
+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);
+void fl2000_vga_sink_event(struct fl2000_vga *vga);
+
/* I2C adapter interface creation */
struct i2c_adapter *fl2000_i2c_init(struct usb_device *usb_dev);
int fl2000_ddc_read_block(struct i2c_adapter *adapter, u8 *buf, unsigned int block, size_t len);
@@ -152,4 +182,11 @@ int fl2000_enable_interrupts(struct usb_device *usb_dev);
int fl2000_read_status(struct usb_device *usb_dev, u32 *status);
int fl2000_i2c_dword(struct usb_device *usb_dev, bool read, u16 addr, u8 offset, u32 *data);
+/* DRM device creation */
+int fl2000_drm_bind(struct device *master);
+void fl2000_drm_unbind(struct device *master);
+void fl2000_drm_hotplug(struct drm_device *drm, u32 events);
+int fl2000_drm_suspend(struct device *master);
+int fl2000_drm_resume(struct device *master);
+
#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..2c0c87854
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_drm.c
@@ -0,0 +1,986 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * (C) Copyright 2012, Red Hat
+ * (C) Copyright 2018-2020, Artem Mygaiev
+ */
+
+#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_modeset_helper.h>
+#include <drm/drm_probe_helper.h>
+#include <drm/drm_vblank_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
+
+/* Maximum supported resolution, no documented hardware limit */
+#define FL2000_MAX_WIDTH 4000
+#define FL2000_MAX_HEIGHT 4000
+
+/*
+ * 32-bit input. The 10-bit formats are accepted because the compositor hands
+ * us its scanout buffer as it is: Mutter renders ARGB2101010 and, when the
+ * secondary output cannot take that format, falls back to a copy path that
+ * has leaked GPU memory until the desktop stalled.
+ */
+#define FL2000_FB_BPP 32
+static const u32 fl2000_pixel_formats[] = {
+ DRM_FORMAT_XRGB8888,
+ DRM_FORMAT_XRGB2101010,
+ DRM_FORMAT_ARGB2101010,
+};
+
+/* The cursor sprite is blended into the outgoing frame by the CPU */
+static const u32 fl2000_cursor_formats[] = {
+ DRM_FORMAT_ARGB8888,
+};
+
+/*
+ * 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 the USB link can carry for @mode: 3 (RGB888), 2 (RGB565) or 0 for neither. 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 unsigned int fl2000_get_bytes_pix(enum usb_device_speed speed,
+ const struct drm_display_mode *mode)
+{
+ u64 max_bw, rate;
+ u32 total = (u32)mode->htotal * mode->vtotal;
+ u64 bytes_pix;
+
+ 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 0;
+ }
+
+ if (!total)
+ return 0;
+
+ /* Active pixels per second; at most 500 MHz x 4000 x 4000, so no overflow */
+ rate = div_u64((u64)mode->clock * 1000 * mode->hdisplay * mode->vdisplay, total);
+ if (!rate)
+ return 0;
+
+ bytes_pix = div64_u64(max_bw, rate);
+ if (bytes_pix >= 3)
+ return 3; /* RGB888 */
+ if (bytes_pix == 2)
+ return 2; /* RGB565 */
+
+ /* RGB332 is not supported */
+ return 0;
+}
+
+struct fl2000_drm_if {
+ struct usb_device *usb_dev;
+ struct drm_device drm;
+ struct drm_plane plane;
+ /*
+ * Without a cursor plane of its own the compositor keeps the pointer on
+ * the primary GPU's hardware cursor, where it never reaches this output.
+ */
+ struct drm_plane cursor;
+ struct drm_crtc crtc;
+ struct drm_encoder encoder;
+ struct fl2000_vga vga;
+ struct fl2000_stream *stream;
+ struct fl2000_intr *intr;
+ /* Set once userspace can see the device: events before that have nobody to tell */
+ bool registered;
+};
+
+static inline struct fl2000_drm_if *to_fl2000_drm_if(struct drm_device *drm)
+{
+ return container_of(drm, struct fl2000_drm_if, 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_MODESET | DRIVER_GEM | DRIVER_ATOMIC,
+ .fops = &fl2000_drm_driver_fops,
+
+ /*
+ * Paged shmem buffers: the framebuffer is only ever read by the CPU
+ * (fl2000_stream_compress), never DMA'd to the device, so it does not
+ * need to be physically contiguous.
+ */
+ .dumb_create = drm_gem_shmem_dumb_create,
+ /*
+ * Import with a mapping, not DRM_GEM_SHMEM_DRIVER_OPS' no-map import.
+ * Mapping the attachment makes the exporter pin the buffer where this
+ * device can reach it, system memory for amdgpu. Left unmapped, amdgpu
+ * keeps it in VRAM and vmaps it as one linear range from where it
+ * thinks it starts, which for a buffer split over several VRAM blocks
+ * is right for the last block only: at 1360x768 and 1366x768 all but
+ * the bottom ~10 % of the desktop came out black or garbled.
+ */
+ .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,
+};
+
+static const struct drm_mode_config_funcs fl2000_mode_config_funcs = {
+ .fb_create = drm_gem_fb_create_with_dirty,
+ .atomic_check = drm_atomic_helper_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;
+}
+
+/*
+ * Find the PLL setting for a mode, accepting a pixel clock off by up to @max_ppm parts per
+ * million. The timings of the mode are never touched, only the clock the PLL can make for it.
+ */
+static int fl2000_mode_calc(const struct drm_display_mode *mode,
+ struct drm_display_mode *adjusted_mode, struct fl2000_pll *pll,
+ u64 max_ppm)
+{
+ u32 clock_calculated;
+ u64 ppm_err;
+
+ if (mode->clock * 1000 > FL2000_MAX_PIXCLOCK)
+ return -EINVAL;
+
+ /* To keep precision use clock multiplied by 10^6 */
+ ppm_err = fl2000_pll_calc((u64)mode->clock * 1000 * FL2000_PLL_PRECISION, pll,
+ &clock_calculated);
+ if (ppm_err >= max_ppm)
+ return -EINVAL;
+
+ if (adjusted_mode) {
+ drm_mode_copy(adjusted_mode, mode);
+ adjusted_mode->clock = clock_calculated / 1000;
+ }
+
+ return 0;
+}
+
+static enum drm_mode_status fl2000_crtc_mode_valid(struct drm_crtc *crtc,
+ const struct drm_display_mode *mode)
+{
+ struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
+ struct drm_display_mode adjusted_mode;
+ struct fl2000_pll pll;
+
+ /* Get PLL configuration and check if mode adjustments are needed */
+ if (fl2000_mode_calc(mode, &adjusted_mode, &pll, FL2000_PPM_ERR_MAX_VGA))
+ return MODE_BAD;
+
+ if (fl2000_get_bytes_pix(drm_if->usb_dev->speed, &adjusted_mode) == 0)
+ return MODE_CLOCK_HIGH;
+
+ return MODE_OK;
+}
+
+/*
+ * 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;
+ bool is_cursor = plane->type == DRM_PLANE_TYPE_CURSOR;
+
+ /* Clipping a larger sprite would put a truncated pointer on screen */
+ if (is_cursor && new_plane_state->fb &&
+ (new_plane_state->fb->width > FL2000_CURSOR_MAX ||
+ new_plane_state->fb->height > FL2000_CURSOR_MAX))
+ return -EINVAL;
+
+ if (new_plane_state->crtc)
+ new_crtc_state = drm_atomic_get_new_crtc_state(state, new_plane_state->crtc);
+
+ /* The cursor may be placed anywhere; the primary plane is the frame */
+ return drm_atomic_helper_check_plane_state(new_plane_state, new_crtc_state,
+ DRM_PLANE_NO_SCALING, DRM_PLANE_NO_SCALING,
+ is_cursor, false);
+}
+
+/*
+ * Only the damaged rectangle is converted, into the stream's shadow frame,
+ * which is the one persistent copy every outgoing buffer is built from.
+ */
+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_drm_if *drm_if = to_fl2000_drm_if(plane->dev);
+ struct drm_framebuffer *fb = plane_state->fb;
+ struct drm_rect damage;
+ ktime_t start;
+ int idx;
+
+ if (!fb)
+ return; /* plane is disabled */
+
+ if (!drm_atomic_helper_damage_merged(old_plane_state, plane_state, &damage))
+ return;
+
+ if (drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE))
+ return;
+
+ if (drm_dev_enter(plane->dev, &idx)) {
+ start = ktime_get();
+ fl2000_stream_compress(drm_if->stream, &shadow_plane_state->data[0],
+ drm_gem_is_imported(fb->obj[0]), fb->format->format,
+ fb->width, fb->height, fb->pitches[0], &damage);
+ fl2000_stream_note_render(drm_if->stream,
+ (unsigned int)ktime_us_delta(ktime_get(), start),
+ drm_rect_width(&damage) * drm_rect_height(&damage));
+ drm_dev_exit(idx);
+ }
+
+ drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
+}
+
+/*
+ * Hand the cursor sprite to the stream, which blends it into every outgoing
+ * frame. Moving the pointer keeps the same framebuffer and skips the copy.
+ */
+static void fl2000_cursor_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_drm_if *drm_if = to_fl2000_drm_if(plane->dev);
+ struct drm_framebuffer *fb = plane_state->fb;
+ unsigned int w, h;
+ u32 *sprite;
+ int idx;
+
+ if (!fb || !plane_state->visible) {
+ fl2000_stream_set_cursor(drm_if->stream, NULL, 0, 0, 0, 0);
+ return;
+ }
+
+ if (fb == old_plane_state->fb && old_plane_state->visible) {
+ fl2000_stream_move_cursor(drm_if->stream, plane_state->crtc_x, plane_state->crtc_y);
+ return;
+ }
+
+ /* Size is bounded by the atomic check */
+ w = fb->width;
+ h = fb->height;
+
+ /* Repack into a tightly packed sprite: the framebuffer has its own pitch */
+ sprite = kmalloc_objs(*sprite, (size_t)w * h);
+ if (!sprite)
+ return;
+
+ if (drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE))
+ goto out_free;
+
+ if (drm_dev_enter(plane->dev, &idx)) {
+ for (unsigned int y = 0; y < h; y++)
+ iosys_map_memcpy_from(sprite + (size_t)y * w, &shadow_plane_state->data[0],
+ (size_t)y * fb->pitches[0], (size_t)w * sizeof(u32));
+
+ fl2000_stream_set_cursor(drm_if->stream, sprite, w, h, plane_state->crtc_x,
+ plane_state->crtc_y);
+ drm_dev_exit(idx);
+ }
+
+ drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
+out_free:
+ kfree(sprite);
+}
+
+static const struct drm_plane_helper_funcs fl2000_cursor_helper_funcs = {
+ DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
+ .atomic_check = fl2000_plane_atomic_check,
+ .atomic_update = fl2000_cursor_atomic_update,
+};
+
+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
+ */
+
+static int fl2000_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *state)
+{
+ struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
+ struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
+ struct fl2000_pll pll;
+ int ret;
+
+ ret = drm_crtc_helper_atomic_check(crtc, state);
+ if (ret)
+ return ret;
+
+ if (!crtc_state->enable || !drm_atomic_crtc_needs_modeset(crtc_state))
+ return 0;
+
+ /* PLL configuration, with htotal and clock adjusted to what the PLL can produce */
+ ret = fl2000_mode_calc(&crtc_state->mode, &crtc_state->adjusted_mode, &pll,
+ FL2000_PPM_ERR_MAX_VGA);
+ if (ret)
+ return ret;
+
+ /* The USB link must be able to carry the adjusted pixel clock */
+ if (!fl2000_get_bytes_pix(drm_if->usb_dev->speed, &crtc_state->adjusted_mode))
+ return -EINVAL;
+
+ return 0;
+}
+
+/*
+ * 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_crtc_force_connect(struct fl2000_drm_if *drm_if,
+ const struct drm_crtc_state *crtc_state)
+{
+ fl2000_force_connect(drm_if->usb_dev,
+ crtc_state->encoder_mask & drm_encoder_mask(&drm_if->encoder));
+}
+
+/* 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_drm_if *drm_if = to_fl2000_drm_if(drm);
+ struct usb_device *usb_dev = drm_if->usb_dev;
+ 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;
+
+ /*
+ * Same computation as in atomic_check, which already validated the mode
+ * and stored the adjusted htotal and clock in adjusted_mode.
+ */
+ if (WARN_ON(fl2000_mode_calc(mode, NULL, &pll, FL2000_PPM_ERR_MAX_VGA)))
+ return;
+ bytes_pix = fl2000_get_bytes_pix(usb_dev->speed, adjusted_mode);
+
+ drm_dbg_kms(drm, "Mode requested: " DRM_MODE_FMT "\n", DRM_MODE_ARG(mode));
+ drm_dbg_kms(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(usb_dev, &pll);
+
+ fl2000_crtc_force_connect(drm_if, crtc->state);
+
+ /* Reset FL2000 and confirm PLL settings */
+ fl2000_reset(usb_dev);
+
+ /* Set timings settings */
+ fl2000_set_timings(usb_dev, &timings);
+
+ /* Pixel format according to number of bytes per pixel */
+ fl2000_set_pixfmt(usb_dev, bytes_pix);
+
+ /* Configure frame transfers */
+ fl2000_set_transfers(usb_dev);
+
+ /* Enable interrupts */
+ fl2000_enable_interrupts(usb_dev);
+
+ fl2000_afe_magic(usb_dev);
+
+ fl2000_stream_mode_set(drm_if->stream, mode->hdisplay, mode->vdisplay, bytes_pix);
+}
+
+static void fl2000_crtc_atomic_enable(struct drm_crtc *crtc, struct drm_atomic_commit *state)
+{
+ struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
+
+ /* mode_set_nofb does not run when only the connector changed, and disable cleared it */
+ fl2000_crtc_force_connect(drm_if, drm_atomic_get_new_crtc_state(state, crtc));
+
+ fl2000_stream_enable(drm_if->stream);
+
+ drm_crtc_vblank_atomic_enable(crtc, state);
+}
+
+static void fl2000_crtc_atomic_disable(struct drm_crtc *crtc, struct drm_atomic_commit *state)
+{
+ struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
+
+ fl2000_stream_disable(drm_if->stream);
+
+ /* Nothing is shown any more: let the chip's own sense report the monitors again */
+ fl2000_force_connect(drm_if->usb_dev, false);
+
+ drm_crtc_vblank_atomic_disable(crtc, state);
+}
+
+static const struct drm_crtc_helper_funcs fl2000_crtc_helper_funcs = {
+ .mode_valid = fl2000_crtc_mode_valid,
+ .atomic_check = fl2000_crtc_atomic_check,
+ .mode_set_nofb = fl2000_crtc_mode_set_nofb,
+ .atomic_flush = drm_crtc_vblank_atomic_flush,
+ .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,
+ /*
+ * The FL2000 has no vblank interrupt: vblank events come from a timer
+ * running at the mode's refresh rate.
+ */
+ DRM_CRTC_VBLANK_TIMER_FUNCS,
+};
+
+/*
+ * 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;
+
+ return fl2000_mode_calc(mode, NULL, &pll, FL2000_PPM_ERR_MAX) ? MODE_NOCLOCK : MODE_OK;
+}
+
+static const struct drm_encoder_helper_funcs fl2000_hdmi_encoder_helper_funcs = {
+ .mode_valid = fl2000_hdmi_encoder_mode_valid,
+};
+
+/**
+ * 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.
+ */
+void fl2000_drm_hotplug(struct drm_device *drm, u32 events)
+{
+ struct fl2000_drm_if *drm_if = to_fl2000_drm_if(drm);
+ int idx;
+
+ /* The device sends uevents only once it has been registered */
+ if (!smp_load_acquire(&drm_if->registered) || !drm_dev_enter(drm, &idx))
+ return;
+
+ if (events & (FL2000_VGA_STATUS_MONITOR_EVENT | FL2000_VGA_STATUS_EDID_EVENT))
+ fl2000_vga_sink_event(&drm_if->vga);
+
+ drm_kms_helper_hotplug_event(drm);
+
+ drm_dev_exit(idx);
+}
+
+static void fl2000_drm_if_release(struct device *dev, void *res)
+{
+ struct fl2000_drm_if *drm_if = *(struct fl2000_drm_if **)res;
+ struct drm_device *drm = &drm_if->drm;
+ struct usb_device *usb_dev = drm_if->usb_dev;
+
+ dev_dbg(dev, "Unbinding FL2000 master\n");
+
+ /* No hotplug events for a node that is about to disappear */
+ fl2000_intr_quiesce(drm_if->intr);
+ drm_kms_helper_poll_fini(drm);
+
+ /*
+ * Block userspace access and shut down the display pipeline while all
+ * the components are still bound.
+ */
+ drm_dev_unplug(drm);
+ drm_atomic_helper_shutdown(drm);
+
+ /*
+ * Detach bridge component. This release function runs with the global
+ * component mutex held (component_master_del() -> master unbind ->
+ * devres_release()), so it must never call component_del() - that would
+ * self-deadlock. The IT66121 is destroyed by a usb_device devres instead.
+ */
+ component_unbind_all(dev, drm);
+
+ /* Stop streaming interface */
+ fl2000_stream_destroy(usb_dev);
+
+ /* Stop interrupts interface */
+ fl2000_intr_destroy(usb_dev);
+
+ /*
+ * Drop the driver's reference. The DRM device is freed when the last
+ * userspace file is closed; it only pins the USB device, never the I2C
+ * adapter, so the disconnect does not wait for userspace.
+ */
+ drm_dev_put(drm);
+}
+
+/* The DRM device while the master is bound, NULL otherwise */
+static struct fl2000_drm_if *fl2000_drm_if_of(struct device *master)
+{
+ struct fl2000_drm_if **holder = devres_find(master, fl2000_drm_if_release, NULL, NULL);
+
+ return holder ? *holder : NULL;
+}
+
+/**
+ * fl2000_drm_suspend() - stop the display for system sleep
+ * @master: the component master
+ *
+ * Called once for the device, before any of its interfaces is suspended.
+ *
+ * Return: 0 on success, a negative error code otherwise, which aborts the sleep
+ */
+int fl2000_drm_suspend(struct device *master)
+{
+ struct fl2000_drm_if *drm_if = fl2000_drm_if_of(master);
+ struct drm_bridge *bridge;
+ int ret;
+
+ if (!drm_if)
+ return 0;
+
+ /* No hotplug events while going to sleep */
+ fl2000_intr_suspend(drm_if->intr);
+
+ /* Saves the state, turns the outputs off and stops the stream */
+ ret = drm_mode_config_helper_suspend(&drm_if->drm);
+ if (ret) {
+ fl2000_intr_resume(drm_if->intr);
+ return ret;
+ }
+
+ bridge = drm_bridge_chain_get_first_bridge(&drm_if->encoder);
+ if (bridge) {
+ it66121_suspend(bridge);
+ drm_bridge_put(bridge);
+ }
+
+ return 0;
+}
+
+/**
+ * fl2000_drm_resume() - bring the display back after system sleep
+ * @master: the component master
+ *
+ * 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 device *master)
+{
+ struct fl2000_drm_if *drm_if = fl2000_drm_if_of(master);
+ struct usb_device *usb_dev;
+ struct drm_bridge *bridge;
+ int ret;
+
+ if (!drm_if)
+ return 0;
+ usb_dev = drm_if->usb_dev;
+
+ /* First, as the IT66121 is reached through the FL2000 I2C engine */
+ fl2000_regmap_drop_cache(usb_dev);
+
+ bridge = drm_bridge_chain_get_first_bridge(&drm_if->encoder);
+ if (bridge) {
+ it66121_resume(bridge);
+ drm_bridge_put(bridge);
+ }
+
+ /* The same order as in fl2000_drm_bind() */
+ fl2000_reset(usb_dev);
+ fl2000_usb_magic(usb_dev);
+
+ /* Without interrupts the connectors are still polled */
+ fl2000_intr_resume(drm_if->intr);
+
+ /* Another monitor may have been plugged in meanwhile */
+ fl2000_vga_sink_event(&drm_if->vga);
+
+ ret = drm_mode_config_helper_resume(&drm_if->drm);
+ if (ret)
+ drm_err(&drm_if->drm, "Cannot restore the display after sleep (%d)\n", ret);
+
+ drm_kms_helper_hotplug_event(&drm_if->drm);
+
+ return ret;
+}
+
+int fl2000_drm_bind(struct device *master)
+{
+ int ret = 0;
+ struct usb_device *usb_dev = to_usb_device(master->parent);
+ struct fl2000_drm_if *drm_if;
+ struct fl2000_drm_if **holder;
+ struct drm_device *drm;
+ struct drm_mode_config *mode_config;
+ struct drm_connector_list_iter conn_iter;
+ struct drm_connector *connector;
+
+ dev_dbg(master, "Binding FL2000 master\n");
+
+ /*
+ * The DRM device is a child of the USB device, not of the I2C adapter
+ * that is the component master. It holds a reference on its parent, and
+ * i2c_del_adapter() (run when the USB device is unbound) waits for every
+ * reference on the adapter: parenting it there deadlocks the hub worker
+ * on unplug and no device enumerates again. The put is explicit, in
+ * fl2000_drm_if_release().
+ */
+ drm_if = __drm_dev_alloc(&usb_dev->dev, &fl2000_drm_driver, sizeof(*drm_if),
+ offsetof(struct fl2000_drm_if, drm));
+ if (IS_ERR(drm_if)) {
+ dev_err(master, "Cannot allocate DRM structure (%pe)\n", drm_if);
+ return PTR_ERR(drm_if);
+ }
+ drm = &drm_if->drm;
+ drm_if->usb_dev = usb_dev;
+
+ /* A USB device cannot DMA by itself: dma-buf imports go through its host controller */
+ drm_dev_set_dma_dev(drm, usb_dev->bus->sysdev);
+
+ /*
+ * Devres that fl2000_drm_unbind() releases to tear everything down,
+ * registered once binding succeeded.
+ */
+ holder = devres_alloc(fl2000_drm_if_release, sizeof(*holder), GFP_KERNEL);
+ if (!holder) {
+ drm_dev_put(drm);
+ return -ENOMEM;
+ }
+
+ ret = drmm_mode_config_init(drm);
+ if (ret) {
+ drm_err(drm, "Cannot initialize DRM mode (%d)\n", ret);
+ goto err_free_holder;
+ }
+
+ mode_config = &drm->mode_config;
+ mode_config->funcs = &fl2000_mode_config_funcs;
+ mode_config->min_width = 1;
+ mode_config->max_width = FL2000_MAX_WIDTH;
+ mode_config->min_height = 1;
+ mode_config->max_height = FL2000_MAX_HEIGHT;
+ /* Userspace sizes its sprite to this; a larger one is rejected */
+ mode_config->cursor_width = FL2000_CURSOR_MAX;
+ mode_config->cursor_height = FL2000_CURSOR_MAX;
+
+ ret = drm_universal_plane_init(drm, &drm_if->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);
+ goto err_free_holder;
+ }
+ drm_plane_helper_add(&drm_if->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(&drm_if->plane);
+
+ ret = drm_universal_plane_init(drm, &drm_if->cursor, 0, &fl2000_plane_funcs,
+ fl2000_cursor_formats, ARRAY_SIZE(fl2000_cursor_formats),
+ fl2000_plane_format_modifiers, DRM_PLANE_TYPE_CURSOR, NULL);
+ if (ret) {
+ drm_err(drm, "Cannot initialize cursor plane (%d)\n", ret);
+ goto err_free_holder;
+ }
+ drm_plane_helper_add(&drm_if->cursor, &fl2000_cursor_helper_funcs);
+
+ ret = drmm_crtc_init_with_planes(drm, &drm_if->crtc, &drm_if->plane, &drm_if->cursor,
+ &fl2000_crtc_funcs, NULL);
+ if (ret) {
+ drm_err(drm, "Cannot initialize CRTC (%d)\n", ret);
+ goto err_free_holder;
+ }
+ drm_crtc_helper_add(&drm_if->crtc, &fl2000_crtc_helper_funcs);
+ drm_if->plane.possible_crtcs = drm_crtc_mask(&drm_if->crtc);
+ drm_if->cursor.possible_crtcs = drm_crtc_mask(&drm_if->crtc);
+
+ /* The FL2000 is programmed from the CRTC; the IT66121 bridge hangs off this encoder */
+ ret = drmm_encoder_init(drm, &drm_if->encoder, NULL, DRM_MODE_ENCODER_NONE, NULL);
+ if (ret) {
+ drm_err(drm, "Cannot initialize encoder (%d)\n", ret);
+ goto err_free_holder;
+ }
+ drm_if->encoder.possible_crtcs = drm_crtc_mask(&drm_if->crtc);
+ drm_encoder_helper_add(&drm_if->encoder, &fl2000_hdmi_encoder_helper_funcs);
+
+ /* Start streaming interface */
+ drm_if->stream = fl2000_stream_create(usb_dev);
+ if (IS_ERR(drm_if->stream)) {
+ ret = PTR_ERR(drm_if->stream);
+ drm_err(drm, "Cannot create stream (%d)\n", ret);
+ goto err_free_holder;
+ }
+
+ /* Start interrupts interface */
+ drm_if->intr = fl2000_intr_create(usb_dev, drm);
+ if (IS_ERR(drm_if->intr)) {
+ ret = PTR_ERR(drm_if->intr);
+ drm_err(drm, "Cannot start interrupt polling (%d)\n", ret);
+ goto err_destroy_stream;
+ }
+
+ /* Attach bridge */
+ ret = component_bind_all(master, drm);
+ 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, &drm_if->encoder);
+ if (IS_ERR(connector)) {
+ ret = PTR_ERR(connector);
+ drm_err(drm, "Cannot initialize HDMI connector (%d)\n", ret);
+ goto err_unbind;
+ }
+
+ /*
+ * The VGA output goes in after the bridge: the IT66121 attaches to the first encoder of
+ * the device, which has to be the HDMI one.
+ */
+ ret = fl2000_vga_init(drm, &drm_if->vga, &drm_if->crtc, &drm_if->encoder,
+ i2c_verify_adapter(master), drm_if->intr);
+ if (ret) {
+ drm_err(drm, "Cannot initialize VGA output (%d)\n", ret);
+ goto err_unbind;
+ }
+
+ drm_mode_config_reset(drm);
+
+ ret = drm_vblank_init(drm, drm->mode_config.num_crtc);
+ if (ret) {
+ drm_err(drm, "Failed to initialize %d VBLANK(s) (%d)\n",
+ drm->mode_config.num_crtc, ret);
+ goto err_unbind;
+ }
+
+ fl2000_reset(usb_dev);
+ fl2000_usb_magic(usb_dev);
+
+ /*
+ * 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_unbind;
+ }
+
+ *holder = drm_if;
+ devres_add(master, holder);
+
+ /* From here on the interrupt work may tell userspace about monitors */
+ smp_store_release(&drm_if->registered, true);
+
+ /* 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 0;
+
+err_unbind:
+ component_unbind_all(master, drm);
+err_destroy_intr:
+ fl2000_intr_destroy(usb_dev);
+err_destroy_stream:
+ fl2000_stream_destroy(usb_dev);
+err_free_holder:
+ devres_free(holder);
+ drm_dev_put(drm);
+ return ret;
+}
+
+void fl2000_drm_unbind(struct device *master)
+{
+ devres_release(master, fl2000_drm_if_release, NULL, NULL);
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_drv.c b/drivers/gpu/drm/fl2000/fl2000_drv.c
index 107eba92c..2e71f0aec 100644
--- a/drivers/gpu/drm/fl2000/fl2000_drv.c
+++ b/drivers/gpu/drm/fl2000/fl2000_drv.c
@@ -15,6 +15,9 @@
#define USB_VENDOR_FRESCO_LOGIC 0x1D5C
#define USB_PRODUCT_FL2000 0x2000
+#define FL2000_ALL_IFS \
+ (BIT(FL2000_USBIF_AVCONTROL) | BIT(FL2000_USBIF_STREAMING) | BIT(FL2000_USBIF_INTERRUPT))
+
static const struct usb_device_id fl2000_id_table[] = {
{ USB_DEVICE_INTERFACE_CLASS(USB_VENDOR_FRESCO_LOGIC, USB_PRODUCT_FL2000, USB_CLASS_AV) },
{},
@@ -26,9 +29,41 @@ struct fl2000_devs {
struct regmap *regmap;
struct i2c_adapter *adapter;
struct i2c_client *bridge_client;
+ struct component_match *match;
int active_if;
+ /* The display was stopped for system sleep and has not been restarted yet */
+ bool suspended;
+};
+
+static const struct component_master_ops fl2000_master_ops = {
+ .bind = fl2000_drm_bind,
+ .unbind = fl2000_drm_unbind,
};
+static int fl2000_compare(struct device *dev, void *data)
+{
+ struct i2c_client *client = i2c_verify_client(dev);
+ struct i2c_adapter *adapter = data;
+ static const char *const fl2000_supported_bridges[] = {
+ "fl2000-it66121",
+ };
+
+ /*
+ * Only match clients sitting on this FL2000 instance's own I2C bus: a
+ * client with a matching name may belong to another FL2000 device that
+ * is disconnecting or re-enumerating concurrently.
+ */
+ if (!client || client->adapter != adapter)
+ return 0;
+
+ /* Check this is a supported DRM bridge */
+ for (int i = 0; i < ARRAY_SIZE(fl2000_supported_bridges); i++)
+ if (!strncmp(fl2000_supported_bridges[i], client->name, sizeof(client->name)))
+ return 1;
+
+ return 0;
+}
+
static struct fl2000_devs *fl2000_get_devices(struct usb_device *usb_dev)
{
struct fl2000_devs *devs;
@@ -49,6 +84,8 @@ static struct fl2000_devs *fl2000_get_devices(struct usb_device *usb_dev)
return ERR_CAST(devs->adapter);
}
+ component_match_add(&devs->adapter->dev, &devs->match, fl2000_compare, devs->adapter);
+
dev_set_drvdata(&usb_dev->dev, devs);
return devs;
@@ -138,6 +175,16 @@ static int fl2000_probe(struct usb_interface *interface, const struct usb_device
break;
}
+ /* When all interfaces are up - proceed with registration */
+ if (devs->active_if == FL2000_ALL_IFS) {
+ ret = component_master_add_with_match(&devs->adapter->dev, &fl2000_master_ops,
+ devs->match);
+ if (ret) {
+ dev_err(&usb_dev->dev, "Cannot register component master (%d)\n", ret);
+ return ret;
+ }
+ }
+
return ret;
}
@@ -151,11 +198,18 @@ static void fl2000_disconnect(struct usb_interface *interface)
return;
/*
- * The IT66121 bridge is intentionally NOT destroyed here: disconnect
- * also runs for the transient unbind/rebind cycles of
- * usb_reset_device(), where the usb_device survives. Its destruction is
- * a devres of the usb_device, see fl2000_bridge_create().
+ * The IT66121 bridge component is intentionally NOT destroyed here.
+ * Disconnect also runs for the transient unbind/rebind cycles of
+ * usb_reset_device(), where the usb_device (and the I2C adapter and
+ * aggregate match data) survive: the component must persist across
+ * those. Its destruction is a devres of the usb_device, see
+ * fl2000_bridge_create(). Destroying it from the master unbind path is
+ * also forbidden - component_del() would self-deadlock on the global
+ * component mutex held across the unbind callback.
*/
+ if (devs->active_if == FL2000_ALL_IFS)
+ component_master_del(&devs->adapter->dev, &fl2000_master_ops);
+
switch (iface_num) {
case FL2000_USBIF_AVCONTROL:
case FL2000_USBIF_STREAMING:
@@ -170,15 +224,27 @@ static void fl2000_disconnect(struct usb_interface *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)
{
struct usb_device *usb_dev = interface_to_usbdev(interface);
+ struct fl2000_devs *devs = dev_get_drvdata(&usb_dev->dev);
+ int ret;
- /*
- * Nothing to save yet. Providing the callback keeps the USB core from
- * unbinding the driver across system sleep.
- */
- dev_dbg(&usb_dev->dev, "suspend\n");
+ if (!devs || devs->suspended)
+ return 0;
+
+ ret = fl2000_drm_suspend(&devs->adapter->dev);
+ if (ret)
+ return ret;
+
+ devs->suspended = true;
return 0;
}
@@ -186,10 +252,14 @@ static int fl2000_suspend(struct usb_interface *interface, pm_message_t message)
static int fl2000_resume(struct usb_interface *interface)
{
struct usb_device *usb_dev = interface_to_usbdev(interface);
+ struct fl2000_devs *devs = dev_get_drvdata(&usb_dev->dev);
- dev_dbg(&usb_dev->dev, "resume\n");
+ if (!devs || !devs->suspended)
+ return 0;
- return 0;
+ devs->suspended = false;
+
+ return fl2000_drm_resume(&devs->adapter->dev);
}
static struct usb_driver fl2000_driver = {
@@ -198,6 +268,7 @@ static struct usb_driver fl2000_driver = {
.disconnect = fl2000_disconnect,
.suspend = fl2000_suspend,
.resume = fl2000_resume,
+ .reset_resume = 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..6ad0f1dd0
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_interrupt.c
@@ -0,0 +1,237 @@
+// 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 drm_device *drm;
+ u8 poll_interval;
+ 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->usb_dev, status);
+ if (!ret)
+ atomic_or(*status & FL2000_SINK_EVENTS, &intr->events);
+
+ return ret;
+}
+
+/**
+ * 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.
+ *
+ * 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;
+
+ 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_release(struct device *dev, void *res)
+{
+ struct fl2000_intr *intr = res;
+ struct usb_device *usb_dev = to_usb_device(dev);
+
+ usb_poison_urb(intr->urb);
+ cancel_work_sync(&intr->work);
+ if (intr->work_queue)
+ destroy_workqueue(intr->work_queue);
+ usb_free_coherent(usb_dev, INTR_BUFSIZE, intr->buf, intr->transfer_dma);
+ usb_free_urb(intr->urb);
+}
+
+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)
+ 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);
+
+ /*
+ * For interrupt URBs, as part of successful URB submission urb->interval is modified to
+ * reflect the actual transfer period used, so we need to restore it.
+ */
+ urb->interval = intr->poll_interval;
+ urb->start_frame = -1;
+
+ /* Restart urb */
+ ret = usb_submit_urb(urb, GFP_ATOMIC);
+ if (ret)
+ dev_err(&usb_dev->dev, "URB submission failed (%d)\n", ret);
+}
+
+/**
+ * fl2000_intr_create() - interrupt processing context creation
+ * @usb_dev: FL2000 USB device
+ * @drm: DRM device that receives the hotplug events
+ *
+ * Called on DRM master bind. 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 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 = devres_alloc(&fl2000_intr_release, sizeof(*intr), GFP_KERNEL);
+ if (!intr)
+ return ERR_PTR(-ENOMEM);
+ devres_add(&usb_dev->dev, intr);
+
+ intr->poll_interval = desc->bInterval;
+ intr->usb_dev = usb_dev;
+ intr->drm = drm;
+ INIT_WORK(&intr->work, &fl2000_intr_work);
+ atomic_set(&intr->events, 0);
+
+ intr->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!intr->urb) {
+ devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ intr->buf = usb_alloc_coherent(usb_dev, INTR_BUFSIZE, GFP_KERNEL, &intr->transfer_dma);
+ if (!intr->buf) {
+ devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ intr->work_queue = alloc_ordered_workqueue("fl2000_interrupt", 0);
+ if (!intr->work_queue) {
+ devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
+ return ERR_PTR(-ENOMEM);
+ }
+
+ /* Interrupt URB configuration is static, including allocated buffer */
+ usb_fill_int_urb(intr->urb, usb_dev, usb_rcvintpipe(usb_dev, 3), intr->buf, INTR_BUFSIZE,
+ fl2000_intr_completion, intr, intr->poll_interval);
+ intr->urb->transfer_dma = intr->transfer_dma;
+ intr->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ /* Start checking for interrupts */
+ ret = usb_submit_urb(intr->urb, GFP_KERNEL);
+ if (ret) {
+ dev_err(&usb_dev->dev, "URB submission failed (%d)\n", ret);
+ devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
+ return ERR_PTR(ret);
+ }
+
+ return intr;
+}
+
+/**
+ * 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_resume().
+ */
+void fl2000_intr_suspend(struct fl2000_intr *intr)
+{
+ usb_kill_urb(intr->urb);
+ cancel_work_sync(&intr->work);
+}
+
+/**
+ * fl2000_intr_resume() - poll the interrupt endpoint again after system sleep
+ * @intr: interrupt context
+ *
+ * Return: 0 on success, the URB submission error otherwise
+ */
+int fl2000_intr_resume(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 usb_device *usb_dev)
+{
+ devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
+}
diff --git a/drivers/gpu/drm/fl2000/fl2000_vga.c b/drivers/gpu/drm/fl2000/fl2000_vga.c
new file mode 100644
index 000000000..322cd8b16
--- /dev/null
+++ b/drivers/gpu/drm/fl2000/fl2000_vga.c
@@ -0,0 +1,256 @@
+// 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
+
+/* The first probes of a connection retry the EDID, in case the monitor is still waking up */
+#define FL2000_VGA_EDID_TRIES 3
+
+/* 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;
+ else if (status != connector_status_connected)
+ /* Whatever comes next may be another monitor: its EDID must be read again */
+ WRITE_ONCE(vga->edid_stale, true);
+
+ drm_dev_exit(idx);
+
+ return status;
+}
+
+/*
+ * The EDID is read through the FL2000 I2C engine. It is kept for as long as the monitor stays, and
+ * only a few probes try for it: a read that finds nothing is quick, but old monitors often do not
+ * answer DDC at all, and every probe runs with the mode config locked.
+ */
+static int fl2000_vga_get_modes(struct drm_connector *connector)
+{
+ struct fl2000_vga *vga = to_fl2000_vga(connector);
+ int count = 0, idx;
+
+ if (!drm_dev_enter(connector->dev, &idx))
+ return 0;
+
+ if (READ_ONCE(vga->edid_stale)) {
+ WRITE_ONCE(vga->edid_stale, false);
+ drm_edid_free(vga->edid);
+ vga->edid = NULL;
+ vga->edid_tries = 0;
+ }
+
+ if (!vga->edid && vga->edid_tries < FL2000_VGA_EDID_TRIES) {
+ vga->edid_tries++;
+ vga->edid = drm_edid_read_custom(connector, fl2000_vga_read_block, vga);
+ drm_dbg_kms(connector->dev, "VGA EDID read %s (try %u)\n",
+ vga->edid ? "ok" : "failed", vga->edid_tries);
+ }
+
+ drm_edid_connector_update(connector, vga->edid);
+ if (vga->edid)
+ count = drm_edid_connector_add_modes(connector);
+
+ 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,
+};
+
+/**
+ * fl2000_vga_sink_event() - the chip reports that a monitor appeared, went away or changed EDID
+ * @vga: VGA output
+ *
+ * Called from the interrupt work. The EDID is read again by the next probe.
+ */
+void fl2000_vga_sink_event(struct fl2000_vga *vga)
+{
+ WRITE_ONCE(vga->edid_stale, true);
+}
+
+static void fl2000_vga_release(struct drm_device *drm, void *res)
+{
+ struct fl2000_vga *vga = res;
+
+ drm_edid_free(vga->edid);
+ 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] 6+ messages in thread
* Re: [PATCH RFC 4/4] drm/fl2000: Add KMS display pipeline
2026-11-06 2:34 ` [PATCH RFC 4/4] drm/fl2000: Add KMS display pipeline Angel Gabriel Crispin Valdivia via B4 Relay
@ 2026-11-06 9:59 ` Thomas Zimmermann
0 siblings, 0 replies; 6+ messages in thread
From: Thomas Zimmermann @ 2026-11-06 9:59 UTC (permalink / raw)
To: angelcrispinvaldivia, Maarten Lankhorst, Maxime Ripard,
David Airlie, Simona Vetter
Cc: linux-kernel, dri-devel
Hi,
you'll find a review below.
Am 06.11.26 um 03:34 schrieb Angel Gabriel Crispin Valdivia via B4 Relay:
> From: Angel Gabriel Crispin Valdivia <angelcrispinvaldivia@gmail.com>
>
> Register the DRM device once all FL2000 interfaces are bound and the
> IT66121 component is available, and connect the pieces:
>
> - a primary plane, a cursor plane, a CRTC and an encoder, with GEM
> SHMEM framebuffers accessed through the shadow-plane helpers and
> XRGB8888 (and the two 10-bit formats compositors render in, reduced
> to 8 bits) as formats. The framebuffer is only read by the CPU, so it
> does not need to be physically contiguous. The primary plane passes
> its merged damage to the stream; the cursor plane hands over its
> sprite, or only its position when the framebuffer is unchanged.
> Without a cursor plane, compositors keep the pointer on the primary
> GPU and it never appears on this output
> - mode validation and PLL search in the CRTC's atomic_check (which
> adjusts the clock in adjusted_mode), 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, kept while the monitor stays, and
> without one the standard timings are offered
> - vblank from the DRM vblank timer, as the FL2000 has no vblank
> interrupt, and fbdev emulation
> - the FL2000 interrupt endpoint, which reports monitor and IT66121
> events and triggers a hotplug event
>
> 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
> and a GNOME Wayland desktop work, with the cursor, at 59 frames per
> second on the link and no measurable effect on the rest of the system.
> 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 | 37 ++
> drivers/gpu/drm/fl2000/fl2000_drm.c | 986 ++++++++++++++++++++++++++++++
> drivers/gpu/drm/fl2000/fl2000_drv.c | 93 ++-
> drivers/gpu/drm/fl2000/fl2000_interrupt.c | 237 +++++++
> drivers/gpu/drm/fl2000/fl2000_vga.c | 256 ++++++++
> 6 files changed, 1602 insertions(+), 12 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 d800d943e..ee452576e 100644
> --- a/drivers/gpu/drm/fl2000/fl2000.h
> +++ b/drivers/gpu/drm/fl2000/fl2000.h
> @@ -131,6 +131,36 @@ void fl2000_stream_move_cursor(struct fl2000_stream *stream, int x, int y);
> 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 drm_device *drm);
> +void fl2000_intr_quiesce(struct fl2000_intr *intr);
> +void fl2000_intr_suspend(struct fl2000_intr *intr);
> +int fl2000_intr_resume(struct fl2000_intr *intr);
> +int fl2000_intr_monitor_connected(struct fl2000_intr *intr);
> +void fl2000_intr_destroy(struct usb_device *usb_dev);
> +
> +/* VGA output: a DAC encoder and connector next to the HDMI ones, on the same CRTC */
> +struct drm_edid;
> +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;
> + /* EDID of the monitor, kept for as long as it stays connected; NULL if it has none */
> + const struct drm_edid *edid;
> + unsigned int edid_tries;
> + /* The monitor changed or went away: read the EDID again on the next probe */
> + bool edid_stale;
Don't cache the EDID. Rather re-read it from the device. We have
elaborate helpers for EDID and best it to not try to out-smart them.
> +};
> +
> +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);
> +void fl2000_vga_sink_event(struct fl2000_vga *vga);
> +
> /* I2C adapter interface creation */
> struct i2c_adapter *fl2000_i2c_init(struct usb_device *usb_dev);
> int fl2000_ddc_read_block(struct i2c_adapter *adapter, u8 *buf, unsigned int block, size_t len);
> @@ -152,4 +182,11 @@ int fl2000_enable_interrupts(struct usb_device *usb_dev);
> int fl2000_read_status(struct usb_device *usb_dev, u32 *status);
> int fl2000_i2c_dword(struct usb_device *usb_dev, bool read, u16 addr, u8 offset, u32 *data);
>
> +/* DRM device creation */
> +int fl2000_drm_bind(struct device *master);
> +void fl2000_drm_unbind(struct device *master);
> +void fl2000_drm_hotplug(struct drm_device *drm, u32 events);
> +int fl2000_drm_suspend(struct device *master);
> +int fl2000_drm_resume(struct device *master);
> +
> #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..2c0c87854
> --- /dev/null
> +++ b/drivers/gpu/drm/fl2000/fl2000_drm.c
> @@ -0,0 +1,986 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * (C) Copyright 2012, Red Hat
> + * (C) Copyright 2018-2020, Artem Mygaiev
> + */
> +
> +#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_modeset_helper.h>
> +#include <drm/drm_probe_helper.h>
> +#include <drm/drm_vblank_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
> +
> +/* Maximum supported resolution, no documented hardware limit */
> +#define FL2000_MAX_WIDTH 4000
> +#define FL2000_MAX_HEIGHT 4000
How large a value could you write into the registers?
> +
> +/*
> + * 32-bit input. The 10-bit formats are accepted because the compositor hands
> + * us its scanout buffer as it is: Mutter renders ARGB2101010 and, when the
> + * secondary output cannot take that format, falls back to a copy path that
> + * has leaked GPU memory until the desktop stalled.
> + */
> +#define FL2000_FB_BPP 32
> +static const u32 fl2000_pixel_formats[] = {
> + DRM_FORMAT_XRGB8888,
> + DRM_FORMAT_XRGB2101010,
> + DRM_FORMAT_ARGB2101010,
No ARGB on planes that cannot handle it. Primary planes most likely don't.
I assume that XRGB2101010 is the hardware's native or preferred format?
It should go first then.
> +};
> +
> +/* The cursor sprite is blended into the outgoing frame by the CPU */
> +static const u32 fl2000_cursor_formats[] = {
> + DRM_FORMAT_ARGB8888,
> +};
> +
> +/*
> + * 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 the USB link can carry for @mode: 3 (RGB888), 2 (RGB565) or 0 for neither. 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.
That line buffer sounds awful to work with.
> + */
> +static unsigned int fl2000_get_bytes_pix(enum usb_device_speed speed,
> + const struct drm_display_mode *mode)
> +{
> + u64 max_bw, rate;
> + u32 total = (u32)mode->htotal * mode->vtotal;
> + u64 bytes_pix;
> +
> + 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 0;
> + }
> +
> + if (!total)
> + return 0;
> +
> + /* Active pixels per second; at most 500 MHz x 4000 x 4000, so no overflow */
> + rate = div_u64((u64)mode->clock * 1000 * mode->hdisplay * mode->vdisplay, total);
> + if (!rate)
> + return 0;
> +
> + bytes_pix = div64_u64(max_bw, rate);
> + if (bytes_pix >= 3)
> + return 3; /* RGB888 */
> + if (bytes_pix == 2)
> + return 2; /* RGB565 */
> +
> + /* RGB332 is not supported */
> + return 0;
> +}
> +
> +struct fl2000_drm_if {
I'd rather call this structure fl2000_device. Although located at a USB
interface, it is a DRM device.
> + struct usb_device *usb_dev;
> + struct drm_device drm;
> + struct drm_plane plane;
> + /*
> + * Without a cursor plane of its own the compositor keeps the pointer on
> + * the primary GPU's hardware cursor, where it never reaches this output.
> + */
That comment ???
If this refers to the cursor disappearing if another device has a
hardware cursor, I'm aware of this problem wrt Gnome. Please fix this in
the compositor. It's not a driver bug.
> + struct drm_plane cursor;
From what I understand, the frame-streaming helpers emulate the cursor
plane. We don't do this in the kernel. Compositors need to handle this.
Please remove cursor support.
> + struct drm_crtc crtc;
> + struct drm_encoder encoder;
> + struct fl2000_vga vga;
> + struct fl2000_stream *stream;
> + struct fl2000_intr *intr;
> + /* Set once userspace can see the device: events before that have nobody to tell */
> + bool registered;
Drop that state please. No other driver uses this, as it's something
that DRM helpers take care of.
> +};
> +
> +static inline struct fl2000_drm_if *to_fl2000_drm_if(struct drm_device *drm)
> +{
> + return container_of(drm, struct fl2000_drm_if, 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_MODESET | DRIVER_GEM | DRIVER_ATOMIC,
Sorted flags please.
> + .fops = &fl2000_drm_driver_fops,
> +
> + /*
> + * Paged shmem buffers: the framebuffer is only ever read by the CPU
> + * (fl2000_stream_compress), never DMA'd to the device, so it does not
> + * need to be physically contiguous.
> + */
Please drop the comment. It's not the right place to document gem-shmem
behavior.
> + .dumb_create = drm_gem_shmem_dumb_create,
> + /*
> + * Import with a mapping, not DRM_GEM_SHMEM_DRIVER_OPS' no-map import.
> + * Mapping the attachment makes the exporter pin the buffer where this
> + * device can reach it, system memory for amdgpu. Left unmapped, amdgpu
> + * keeps it in VRAM and vmaps it as one linear range from where it
> + * thinks it starts, which for a buffer split over several VRAM blocks
> + * is right for the last block only: at 1360x768 and 1366x768 all but
> + * the bottom ~10 % of the desktop came out black or garbled.
> + */
Also please drop. It's not the right place to document this.
> + .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,
> +};
> +
> +static const struct drm_mode_config_funcs fl2000_mode_config_funcs = {
> + .fb_create = drm_gem_fb_create_with_dirty,
> + .atomic_check = drm_atomic_helper_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;
> +}
> +
> +/*
> + * Find the PLL setting for a mode, accepting a pixel clock off by up to @max_ppm parts per
> + * million. The timings of the mode are never touched, only the clock the PLL can make for it.
> + */
> +static int fl2000_mode_calc(const struct drm_display_mode *mode,
> + struct drm_display_mode *adjusted_mode, struct fl2000_pll *pll,
> + u64 max_ppm)
> +{
> + u32 clock_calculated;
> + u64 ppm_err;
> +
> + if (mode->clock * 1000 > FL2000_MAX_PIXCLOCK)
> + return -EINVAL;
> +
> + /* To keep precision use clock multiplied by 10^6 */
> + ppm_err = fl2000_pll_calc((u64)mode->clock * 1000 * FL2000_PLL_PRECISION, pll,
> + &clock_calculated);
> + if (ppm_err >= max_ppm)
> + return -EINVAL;
Rather return MODE_* status codes here and forward them in mode_valid
below...
> +
> + if (adjusted_mode) {
> + drm_mode_copy(adjusted_mode, mode);
> + adjusted_mode->clock = clock_calculated / 1000;
> + }
... or just inline it into the respective calls.
> +
> + return 0;
> +}
> +
> +static enum drm_mode_status fl2000_crtc_mode_valid(struct drm_crtc *crtc,
> + const struct drm_display_mode *mode)
> +{
> + struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
> + struct drm_display_mode adjusted_mode;
> + struct fl2000_pll pll;
> +
> + /* Get PLL configuration and check if mode adjustments are needed */
> + if (fl2000_mode_calc(mode, &adjusted_mode, &pll, FL2000_PPM_ERR_MAX_VGA))
> + return MODE_BAD;
> +
> + if (fl2000_get_bytes_pix(drm_if->usb_dev->speed, &adjusted_mode) == 0)
> + return MODE_CLOCK_HIGH;
CRTC's mode_valid should only validate against the limits of the given
CRTC. This call seems to validate something with pixel formats. That's
at least questionable.
> +
> + return MODE_OK;
> +}
> +
> +/*
> + * 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;
> + bool is_cursor = plane->type == DRM_PLANE_TYPE_CURSOR;
> +
> + /* Clipping a larger sprite would put a truncated pointer on screen */
> + if (is_cursor && new_plane_state->fb &&
> + (new_plane_state->fb->width > FL2000_CURSOR_MAX ||
> + new_plane_state->fb->height > FL2000_CURSOR_MAX))
> + return -EINVAL;
We check this limit when we create the framebuffer. Please don't do this
here unless the plane has different limits.
> +
> + if (new_plane_state->crtc)
> + new_crtc_state = drm_atomic_get_new_crtc_state(state, new_plane_state->crtc);
> +
> + /* The cursor may be placed anywhere; the primary plane is the frame */
> + return drm_atomic_helper_check_plane_state(new_plane_state, new_crtc_state,
> + DRM_PLANE_NO_SCALING, DRM_PLANE_NO_SCALING,
> + is_cursor, false);
This block should always go first in plane atomic check.
> +}
> +
> +/*
> + * Only the damaged rectangle is converted, into the stream's shadow frame,
> + * which is the one persistent copy every outgoing buffer is built from.
> + */
> +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_drm_if *drm_if = to_fl2000_drm_if(plane->dev);
> + struct drm_framebuffer *fb = plane_state->fb;
> + struct drm_rect damage;
> + ktime_t start;
> + int idx;
> +
> + if (!fb)
> + return; /* plane is disabled */
> +
> + if (!drm_atomic_helper_damage_merged(old_plane_state, plane_state, &damage))
> + return;
> +
> + if (drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE))
> + return;
> +
> + if (drm_dev_enter(plane->dev, &idx)) {
> + start = ktime_get();
> + fl2000_stream_compress(drm_if->stream, &shadow_plane_state->data[0],
> + drm_gem_is_imported(fb->obj[0]), fb->format->format,
> + fb->width, fb->height, fb->pitches[0], &damage);
> + fl2000_stream_note_render(drm_if->stream,
> + (unsigned int)ktime_us_delta(ktime_get(), start),
> + drm_rect_width(&damage) * drm_rect_height(&damage));
> + drm_dev_exit(idx);
> + }
> +
> + drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
> +}
> +
> +/*
> + * Hand the cursor sprite to the stream, which blends it into every outgoing
> + * frame. Moving the pointer keeps the same framebuffer and skips the copy.
> + */
> +static void fl2000_cursor_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_drm_if *drm_if = to_fl2000_drm_if(plane->dev);
> + struct drm_framebuffer *fb = plane_state->fb;
> + unsigned int w, h;
> + u32 *sprite;
> + int idx;
> +
> + if (!fb || !plane_state->visible) {
> + fl2000_stream_set_cursor(drm_if->stream, NULL, 0, 0, 0, 0);
> + return;
> + }
> +
> + if (fb == old_plane_state->fb && old_plane_state->visible) {
> + fl2000_stream_move_cursor(drm_if->stream, plane_state->crtc_x, plane_state->crtc_y);
> + return;
> + }
> +
> + /* Size is bounded by the atomic check */
> + w = fb->width;
> + h = fb->height;
> +
> + /* Repack into a tightly packed sprite: the framebuffer has its own pitch */
> + sprite = kmalloc_objs(*sprite, (size_t)w * h);
> + if (!sprite)
> + return;
> +
> + if (drm_gem_fb_begin_cpu_access(fb, DMA_FROM_DEVICE))
> + goto out_free;
> +
> + if (drm_dev_enter(plane->dev, &idx)) {
> + for (unsigned int y = 0; y < h; y++)
> + iosys_map_memcpy_from(sprite + (size_t)y * w, &shadow_plane_state->data[0],
> + (size_t)y * fb->pitches[0], (size_t)w * sizeof(u32));
> +
> + fl2000_stream_set_cursor(drm_if->stream, sprite, w, h, plane_state->crtc_x,
> + plane_state->crtc_y);
> + drm_dev_exit(idx);
> + }
> +
> + drm_gem_fb_end_cpu_access(fb, DMA_FROM_DEVICE);
> +out_free:
> + kfree(sprite);
> +}
> +
> +static const struct drm_plane_helper_funcs fl2000_cursor_helper_funcs = {
> + DRM_GEM_SHADOW_PLANE_HELPER_FUNCS,
> + .atomic_check = fl2000_plane_atomic_check,
> + .atomic_update = fl2000_cursor_atomic_update,
> +};
> +
> +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
> + */
> +
> +static int fl2000_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *state)
> +{
> + struct drm_crtc_state *crtc_state = drm_atomic_get_new_crtc_state(state, crtc);
> + struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
> + struct fl2000_pll pll;
> + int ret;
> +
> + ret = drm_crtc_helper_atomic_check(crtc, state);
> + if (ret)
> + return ret;
Please open-code this function here. It's meant to be a drop-in
implementation for the atomic_check callback.
> +
> + if (!crtc_state->enable || !drm_atomic_crtc_needs_modeset(crtc_state))
> + return 0;
> +
> + /* PLL configuration, with htotal and clock adjusted to what the PLL can produce */
> + ret = fl2000_mode_calc(&crtc_state->mode, &crtc_state->adjusted_mode, &pll,
> + FL2000_PPM_ERR_MAX_VGA);
> + if (ret)
> + return ret;
Fixing up display modes goes into the mode_fixup callback. That's where
you can adjust the PLL.
> +
> + /* The USB link must be able to carry the adjusted pixel clock */
> + if (!fl2000_get_bytes_pix(drm_if->usb_dev->speed, &crtc_state->adjusted_mode))
> + return -EINVAL;
Same here.
> +
> + return 0;
> +}
> +
> +/*
> + * 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_crtc_force_connect(struct fl2000_drm_if *drm_if,
> + const struct drm_crtc_state *crtc_state)
> +{
> + fl2000_force_connect(drm_if->usb_dev,
> + crtc_state->encoder_mask & drm_encoder_mask(&drm_if->encoder));
> +}
> +
> +/* 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_drm_if *drm_if = to_fl2000_drm_if(drm);
> + struct usb_device *usb_dev = drm_if->usb_dev;
> + 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;
> +
> + /*
> + * Same computation as in atomic_check, which already validated the mode
> + * and stored the adjusted htotal and clock in adjusted_mode.
> + */
> + if (WARN_ON(fl2000_mode_calc(mode, NULL, &pll, FL2000_PPM_ERR_MAX_VGA)))
> + return;
If you use mode_fixup, state->adjusted_mode will already have the
correctly adjusted mode.
> + bytes_pix = fl2000_get_bytes_pix(usb_dev->speed, adjusted_mode);
CRTC should not mess with the pixel format.
If the hardware cannot handle a certain pixel format at a resolution,
please report this from the plane's atomic_check. Let the compositor
figure it out. DRM drivers don't do these behind-the-back conversion;
except for supporting XRGB8888.
> +
> + drm_dbg_kms(drm, "Mode requested: " DRM_MODE_FMT "\n", DRM_MODE_ARG(mode));
> + drm_dbg_kms(drm, "Mode configured: " DRM_MODE_FMT "\n", DRM_MODE_ARG(adjusted_mode));
Drivers should always use drm_dbg(). The others are severed for DRM
core and helpers.
> +
> + /* 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(usb_dev, &pll);
> +
> + fl2000_crtc_force_connect(drm_if, crtc->state);
> +
> + /* Reset FL2000 and confirm PLL settings */
> + fl2000_reset(usb_dev);
> +
> + /* Set timings settings */
> + fl2000_set_timings(usb_dev, &timings);
> +
> + /* Pixel format according to number of bytes per pixel */
> + fl2000_set_pixfmt(usb_dev, bytes_pix);
> +
> + /* Configure frame transfers */
> + fl2000_set_transfers(usb_dev);
> +
> + /* Enable interrupts */
> + fl2000_enable_interrupts(usb_dev);
This helper enable various interrupts unconditionally. That's likely not
correct. Here you should only enable those that the CRTC need for running.
If you have vblank interrupts, enable them in ->enable_vblank; disable
them in ->disable_vblank.
If your hardware has display sensing, the interrupt must remain enabled
all the time so that you detect display hotplugs.
> +
> + fl2000_afe_magic(usb_dev);
> +
> + fl2000_stream_mode_set(drm_if->stream, mode->hdisplay, mode->vdisplay, bytes_pix);
> +}
> +
> +static void fl2000_crtc_atomic_enable(struct drm_crtc *crtc, struct drm_atomic_commit *state)
> +{
> + struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
> +
> + /* mode_set_nofb does not run when only the connector changed, and disable cleared it */
> + fl2000_crtc_force_connect(drm_if, drm_atomic_get_new_crtc_state(state, crtc));
> +
> + fl2000_stream_enable(drm_if->stream);
> +
> + drm_crtc_vblank_atomic_enable(crtc, state);
Please open-code this function. It's a drop-in implementation for
atomic_enable
> +}
> +
> +static void fl2000_crtc_atomic_disable(struct drm_crtc *crtc, struct drm_atomic_commit *state)
> +{
> + struct fl2000_drm_if *drm_if = to_fl2000_drm_if(crtc->dev);
> +
> + fl2000_stream_disable(drm_if->stream);
> +
> + /* Nothing is shown any more: let the chip's own sense report the monitors again */
> + fl2000_force_connect(drm_if->usb_dev, false);
I'm not quite sure what this does. It looks like it's code for the
encoder/connector.
> +
> + drm_crtc_vblank_atomic_disable(crtc, state);
> +}
> +
> +static const struct drm_crtc_helper_funcs fl2000_crtc_helper_funcs = {
> + .mode_valid = fl2000_crtc_mode_valid,
> + .atomic_check = fl2000_crtc_atomic_check,
> + .mode_set_nofb = fl2000_crtc_mode_set_nofb,
> + .atomic_flush = drm_crtc_vblank_atomic_flush,
> + .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,
> + /*
> + * The FL2000 has no vblank interrupt: vblank events come from a timer
> + * running at the mode's refresh rate.
> + */
> + DRM_CRTC_VBLANK_TIMER_FUNCS,
Please remove this for now. Your driver will still work correctly. That
also obsoletes the other vblank support code in the driver.
We currently only have vblank timers for virtual displays. We might
reconsider using it for other drivers at some later time.
> +};
> +
> +/*
> + * 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;
> +
> + return fl2000_mode_calc(mode, NULL, &pll, FL2000_PPM_ERR_MAX) ? MODE_NOCLOCK : MODE_OK;
If you have strict limits on what the encoder accepts, you can again use
mode_fixup to adjust the frequency.
> +}
> +
> +static const struct drm_encoder_helper_funcs fl2000_hdmi_encoder_helper_funcs = {
> + .mode_valid = fl2000_hdmi_encoder_mode_valid,
> +};
> +
> +/**
> + * 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.
> + */
> +void fl2000_drm_hotplug(struct drm_device *drm, u32 events)
> +{
> + struct fl2000_drm_if *drm_if = to_fl2000_drm_if(drm);
> + int idx;
> +
> + /* The device sends uevents only once it has been registered */
> + if (!smp_load_acquire(&drm_if->registered) || !drm_dev_enter(drm, &idx))
> + return;
> +
> + if (events & (FL2000_VGA_STATUS_MONITOR_EVENT | FL2000_VGA_STATUS_EDID_EVENT))
> + fl2000_vga_sink_event(&drm_if->vga);
That's the kind of EDID tracking that should usually not be done by the
driver.
> +
> + drm_kms_helper_hotplug_event(drm);
> +
> + drm_dev_exit(idx);
> +}
> +
> +static void fl2000_drm_if_release(struct device *dev, void *res)
> +{
> + struct fl2000_drm_if *drm_if = *(struct fl2000_drm_if **)res;
> + struct drm_device *drm = &drm_if->drm;
> + struct usb_device *usb_dev = drm_if->usb_dev;
> +
> + dev_dbg(dev, "Unbinding FL2000 master\n");
> +
> + /* No hotplug events for a node that is about to disappear */
> + fl2000_intr_quiesce(drm_if->intr);
> + drm_kms_helper_poll_fini(drm);
> +
> + /*
> + * Block userspace access and shut down the display pipeline while all
> + * the components are still bound.
> + */
> + drm_dev_unplug(drm);
> + drm_atomic_helper_shutdown(drm);
> +
> + /*
> + * Detach bridge component. This release function runs with the global
> + * component mutex held (component_master_del() -> master unbind ->
> + * devres_release()), so it must never call component_del() - that would
> + * self-deadlock. The IT66121 is destroyed by a usb_device devres instead.
> + */
> + component_unbind_all(dev, drm);
> +
> + /* Stop streaming interface */
> + fl2000_stream_destroy(usb_dev);
> +
> + /* Stop interrupts interface */
> + fl2000_intr_destroy(usb_dev);
> +
> + /*
> + * Drop the driver's reference. The DRM device is freed when the last
> + * userspace file is closed; it only pins the USB device, never the I2C
> + * adapter, so the disconnect does not wait for userspace.
> + */
Please drop all these comment here. It's not the right place to document
all this.
> + drm_dev_put(drm);
> +}
> +
> +/* The DRM device while the master is bound, NULL otherwise */
> +static struct fl2000_drm_if *fl2000_drm_if_of(struct device *master)
> +{
> + struct fl2000_drm_if **holder = devres_find(master, fl2000_drm_if_release, NULL, NULL);
> +
> + return holder ? *holder : NULL;
> +}
> +
> +/**
> + * fl2000_drm_suspend() - stop the display for system sleep
> + * @master: the component master
> + *
> + * Called once for the device, before any of its interfaces is suspended.
> + *
> + * Return: 0 on success, a negative error code otherwise, which aborts the sleep
> + */
> +int fl2000_drm_suspend(struct device *master)
> +{
> + struct fl2000_drm_if *drm_if = fl2000_drm_if_of(master);
> + struct drm_bridge *bridge;
> + int ret;
> +
> + if (!drm_if)
> + return 0;
> +
> + /* No hotplug events while going to sleep */
> + fl2000_intr_suspend(drm_if->intr);
> +
> + /* Saves the state, turns the outputs off and stops the stream */
> + ret = drm_mode_config_helper_suspend(&drm_if->drm);
> + if (ret) {
> + fl2000_intr_resume(drm_if->intr);
> + return ret;
> + }
> +
> + bridge = drm_bridge_chain_get_first_bridge(&drm_if->encoder);
> + if (bridge) {
> + it66121_suspend(bridge);
> + drm_bridge_put(bridge);
> + }
> +
> + return 0;
> +}
> +
> +/**
> + * fl2000_drm_resume() - bring the display back after system sleep
> + * @master: the component master
> + *
> + * 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 device *master)
> +{
> + struct fl2000_drm_if *drm_if = fl2000_drm_if_of(master);
> + struct usb_device *usb_dev;
> + struct drm_bridge *bridge;
> + int ret;
> +
> + if (!drm_if)
> + return 0;
> + usb_dev = drm_if->usb_dev;
> +
> + /* First, as the IT66121 is reached through the FL2000 I2C engine */
> + fl2000_regmap_drop_cache(usb_dev);
> +
> + bridge = drm_bridge_chain_get_first_bridge(&drm_if->encoder);
> + if (bridge) {
> + it66121_resume(bridge);
> + drm_bridge_put(bridge);
> + }
> +
> + /* The same order as in fl2000_drm_bind() */
> + fl2000_reset(usb_dev);
> + fl2000_usb_magic(usb_dev);
> +
> + /* Without interrupts the connectors are still polled */
> + fl2000_intr_resume(drm_if->intr);
> +
> + /* Another monitor may have been plugged in meanwhile */
> + fl2000_vga_sink_event(&drm_if->vga);
> +
> + ret = drm_mode_config_helper_resume(&drm_if->drm);
> + if (ret)
> + drm_err(&drm_if->drm, "Cannot restore the display after sleep (%d)\n", ret);
I think it's better to resume mode setting first before resuming
hardware. Otherwise the incoming interrupt might run on a suspended
pipeline. (Not sure about the resume logic.)
> +
> + drm_kms_helper_hotplug_event(&drm_if->drm);
> +
> + return ret;
> +}
> +
> +int fl2000_drm_bind(struct device *master)
> +{
> + int ret = 0;
> + struct usb_device *usb_dev = to_usb_device(master->parent);
> + struct fl2000_drm_if *drm_if;
> + struct fl2000_drm_if **holder;
> + struct drm_device *drm;
> + struct drm_mode_config *mode_config;
> + struct drm_connector_list_iter conn_iter;
> + struct drm_connector *connector;
> +
> + dev_dbg(master, "Binding FL2000 master\n");
> +
> + /*
> + * The DRM device is a child of the USB device, not of the I2C adapter
> + * that is the component master. It holds a reference on its parent, and
> + * i2c_del_adapter() (run when the USB device is unbound) waits for every
> + * reference on the adapter: parenting it there deadlocks the hub worker
> + * on unplug and no device enumerates again. The put is explicit, in
> + * fl2000_drm_if_release().
> + */
> + drm_if = __drm_dev_alloc(&usb_dev->dev, &fl2000_drm_driver, sizeof(*drm_if),
> + offsetof(struct fl2000_drm_if, drm));
> + if (IS_ERR(drm_if)) {
> + dev_err(master, "Cannot allocate DRM structure (%pe)\n", drm_if);
> + return PTR_ERR(drm_if);
> + }
> + drm = &drm_if->drm;
> + drm_if->usb_dev = usb_dev;
> +
> + /* A USB device cannot DMA by itself: dma-buf imports go through its host controller */
> + drm_dev_set_dma_dev(drm, usb_dev->bus->sysdev);
> +
> + /*
> + * Devres that fl2000_drm_unbind() releases to tear everything down,
> + * registered once binding succeeded.
> + */
> + holder = devres_alloc(fl2000_drm_if_release, sizeof(*holder), GFP_KERNEL);
> + if (!holder) {
> + drm_dev_put(drm);
> + return -ENOMEM;
> + }
> +
> + ret = drmm_mode_config_init(drm);
> + if (ret) {
> + drm_err(drm, "Cannot initialize DRM mode (%d)\n", ret);
> + goto err_free_holder;
> + }
> +
> + mode_config = &drm->mode_config;
> + mode_config->funcs = &fl2000_mode_config_funcs;
> + mode_config->min_width = 1;
> + mode_config->max_width = FL2000_MAX_WIDTH;
> + mode_config->min_height = 1;
> + mode_config->max_height = FL2000_MAX_HEIGHT;
These are the limits of the framebuffer. Setting 1 likely makes no
sense. Just keep it to zero.
> + /* Userspace sizes its sprite to this; a larger one is rejected */
> + mode_config->cursor_width = FL2000_CURSOR_MAX;
> + mode_config->cursor_height = FL2000_CURSOR_MAX;
> +
> + ret = drm_universal_plane_init(drm, &drm_if->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);
> + goto err_free_holder;
> + }
> + drm_plane_helper_add(&drm_if->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(&drm_if->plane);
> +
> + ret = drm_universal_plane_init(drm, &drm_if->cursor, 0, &fl2000_plane_funcs,
> + fl2000_cursor_formats, ARRAY_SIZE(fl2000_cursor_formats),
> + fl2000_plane_format_modifiers, DRM_PLANE_TYPE_CURSOR, NULL);
> + if (ret) {
> + drm_err(drm, "Cannot initialize cursor plane (%d)\n", ret);
> + goto err_free_holder;
> + }
> + drm_plane_helper_add(&drm_if->cursor, &fl2000_cursor_helper_funcs);
The cursor plane needs to go.
> +
> + ret = drmm_crtc_init_with_planes(drm, &drm_if->crtc, &drm_if->plane, &drm_if->cursor,
> + &fl2000_crtc_funcs, NULL);
> + if (ret) {
> + drm_err(drm, "Cannot initialize CRTC (%d)\n", ret);
> + goto err_free_holder;
> + }
> + drm_crtc_helper_add(&drm_if->crtc, &fl2000_crtc_helper_funcs);
> + drm_if->plane.possible_crtcs = drm_crtc_mask(&drm_if->crtc);
> + drm_if->cursor.possible_crtcs = drm_crtc_mask(&drm_if->crtc);
> +
> + /* The FL2000 is programmed from the CRTC; the IT66121 bridge hangs off this encoder */
> + ret = drmm_encoder_init(drm, &drm_if->encoder, NULL, DRM_MODE_ENCODER_NONE, NULL);
> + if (ret) {
> + drm_err(drm, "Cannot initialize encoder (%d)\n", ret);
> + goto err_free_holder;
> + }
> + drm_if->encoder.possible_crtcs = drm_crtc_mask(&drm_if->crtc);
> + drm_encoder_helper_add(&drm_if->encoder, &fl2000_hdmi_encoder_helper_funcs);
> +
> + /* Start streaming interface */
> + drm_if->stream = fl2000_stream_create(usb_dev);
> + if (IS_ERR(drm_if->stream)) {
> + ret = PTR_ERR(drm_if->stream);
> + drm_err(drm, "Cannot create stream (%d)\n", ret);
> + goto err_free_holder;
> + }
> +
> + /* Start interrupts interface */
> + drm_if->intr = fl2000_intr_create(usb_dev, drm);
> + if (IS_ERR(drm_if->intr)) {
> + ret = PTR_ERR(drm_if->intr);
> + drm_err(drm, "Cannot start interrupt polling (%d)\n", ret);
> + goto err_destroy_stream;
> + }
Once that is up, do you receive interrupts? If so, it would better go
after drm_mode_config_reset().
> +
> + /* Attach bridge */
> + ret = component_bind_all(master, drm);
> + 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, &drm_if->encoder);
> + if (IS_ERR(connector)) {
> + ret = PTR_ERR(connector);
> + drm_err(drm, "Cannot initialize HDMI connector (%d)\n", ret);
> + goto err_unbind;
> + }
> +
> + /*
> + * The VGA output goes in after the bridge: the IT66121 attaches to the first encoder of
> + * the device, which has to be the HDMI one.
> + */
> + ret = fl2000_vga_init(drm, &drm_if->vga, &drm_if->crtc, &drm_if->encoder,
> + i2c_verify_adapter(master), drm_if->intr);
> + if (ret) {
> + drm_err(drm, "Cannot initialize VGA output (%d)\n", ret);
> + goto err_unbind;
> + }
> +
> + drm_mode_config_reset(drm);
> +
> + ret = drm_vblank_init(drm, drm->mode_config.num_crtc);
Not needed without vblank timers.
Best regards
Thomas
> + if (ret) {
> + drm_err(drm, "Failed to initialize %d VBLANK(s) (%d)\n",
> + drm->mode_config.num_crtc, ret);
> + goto err_unbind;
> + }
> +
> + fl2000_reset(usb_dev);
> + fl2000_usb_magic(usb_dev);
> +
> + /*
> + * 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_unbind;
> + }
> +
> + *holder = drm_if;
> + devres_add(master, holder);
> +
> + /* From here on the interrupt work may tell userspace about monitors */
> + smp_store_release(&drm_if->registered, true);
> +
> + /* 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 0;
> +
> +err_unbind:
> + component_unbind_all(master, drm);
> +err_destroy_intr:
> + fl2000_intr_destroy(usb_dev);
> +err_destroy_stream:
> + fl2000_stream_destroy(usb_dev);
> +err_free_holder:
> + devres_free(holder);
> + drm_dev_put(drm);
> + return ret;
> +}
> +
> +void fl2000_drm_unbind(struct device *master)
> +{
> + devres_release(master, fl2000_drm_if_release, NULL, NULL);
> +}
> diff --git a/drivers/gpu/drm/fl2000/fl2000_drv.c b/drivers/gpu/drm/fl2000/fl2000_drv.c
> index 107eba92c..2e71f0aec 100644
> --- a/drivers/gpu/drm/fl2000/fl2000_drv.c
> +++ b/drivers/gpu/drm/fl2000/fl2000_drv.c
> @@ -15,6 +15,9 @@
> #define USB_VENDOR_FRESCO_LOGIC 0x1D5C
> #define USB_PRODUCT_FL2000 0x2000
>
> +#define FL2000_ALL_IFS \
> + (BIT(FL2000_USBIF_AVCONTROL) | BIT(FL2000_USBIF_STREAMING) | BIT(FL2000_USBIF_INTERRUPT))
> +
> static const struct usb_device_id fl2000_id_table[] = {
> { USB_DEVICE_INTERFACE_CLASS(USB_VENDOR_FRESCO_LOGIC, USB_PRODUCT_FL2000, USB_CLASS_AV) },
> {},
> @@ -26,9 +29,41 @@ struct fl2000_devs {
> struct regmap *regmap;
> struct i2c_adapter *adapter;
> struct i2c_client *bridge_client;
> + struct component_match *match;
> int active_if;
> + /* The display was stopped for system sleep and has not been restarted yet */
> + bool suspended;
> +};
> +
> +static const struct component_master_ops fl2000_master_ops = {
> + .bind = fl2000_drm_bind,
> + .unbind = fl2000_drm_unbind,
> };
>
> +static int fl2000_compare(struct device *dev, void *data)
> +{
> + struct i2c_client *client = i2c_verify_client(dev);
> + struct i2c_adapter *adapter = data;
> + static const char *const fl2000_supported_bridges[] = {
> + "fl2000-it66121",
> + };
> +
> + /*
> + * Only match clients sitting on this FL2000 instance's own I2C bus: a
> + * client with a matching name may belong to another FL2000 device that
> + * is disconnecting or re-enumerating concurrently.
> + */
> + if (!client || client->adapter != adapter)
> + return 0;
> +
> + /* Check this is a supported DRM bridge */
> + for (int i = 0; i < ARRAY_SIZE(fl2000_supported_bridges); i++)
> + if (!strncmp(fl2000_supported_bridges[i], client->name, sizeof(client->name)))
> + return 1;
> +
> + return 0;
> +}
> +
> static struct fl2000_devs *fl2000_get_devices(struct usb_device *usb_dev)
> {
> struct fl2000_devs *devs;
> @@ -49,6 +84,8 @@ static struct fl2000_devs *fl2000_get_devices(struct usb_device *usb_dev)
> return ERR_CAST(devs->adapter);
> }
>
> + component_match_add(&devs->adapter->dev, &devs->match, fl2000_compare, devs->adapter);
> +
> dev_set_drvdata(&usb_dev->dev, devs);
>
> return devs;
> @@ -138,6 +175,16 @@ static int fl2000_probe(struct usb_interface *interface, const struct usb_device
> break;
> }
>
> + /* When all interfaces are up - proceed with registration */
> + if (devs->active_if == FL2000_ALL_IFS) {
> + ret = component_master_add_with_match(&devs->adapter->dev, &fl2000_master_ops,
> + devs->match);
> + if (ret) {
> + dev_err(&usb_dev->dev, "Cannot register component master (%d)\n", ret);
> + return ret;
> + }
> + }
> +
> return ret;
> }
>
> @@ -151,11 +198,18 @@ static void fl2000_disconnect(struct usb_interface *interface)
> return;
>
> /*
> - * The IT66121 bridge is intentionally NOT destroyed here: disconnect
> - * also runs for the transient unbind/rebind cycles of
> - * usb_reset_device(), where the usb_device survives. Its destruction is
> - * a devres of the usb_device, see fl2000_bridge_create().
> + * The IT66121 bridge component is intentionally NOT destroyed here.
> + * Disconnect also runs for the transient unbind/rebind cycles of
> + * usb_reset_device(), where the usb_device (and the I2C adapter and
> + * aggregate match data) survive: the component must persist across
> + * those. Its destruction is a devres of the usb_device, see
> + * fl2000_bridge_create(). Destroying it from the master unbind path is
> + * also forbidden - component_del() would self-deadlock on the global
> + * component mutex held across the unbind callback.
> */
> + if (devs->active_if == FL2000_ALL_IFS)
> + component_master_del(&devs->adapter->dev, &fl2000_master_ops);
> +
> switch (iface_num) {
> case FL2000_USBIF_AVCONTROL:
> case FL2000_USBIF_STREAMING:
> @@ -170,15 +224,27 @@ static void fl2000_disconnect(struct usb_interface *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)
> {
> struct usb_device *usb_dev = interface_to_usbdev(interface);
> + struct fl2000_devs *devs = dev_get_drvdata(&usb_dev->dev);
> + int ret;
>
> - /*
> - * Nothing to save yet. Providing the callback keeps the USB core from
> - * unbinding the driver across system sleep.
> - */
> - dev_dbg(&usb_dev->dev, "suspend\n");
> + if (!devs || devs->suspended)
> + return 0;
> +
> + ret = fl2000_drm_suspend(&devs->adapter->dev);
> + if (ret)
> + return ret;
> +
> + devs->suspended = true;
>
> return 0;
> }
> @@ -186,10 +252,14 @@ static int fl2000_suspend(struct usb_interface *interface, pm_message_t message)
> static int fl2000_resume(struct usb_interface *interface)
> {
> struct usb_device *usb_dev = interface_to_usbdev(interface);
> + struct fl2000_devs *devs = dev_get_drvdata(&usb_dev->dev);
>
> - dev_dbg(&usb_dev->dev, "resume\n");
> + if (!devs || !devs->suspended)
> + return 0;
>
> - return 0;
> + devs->suspended = false;
> +
> + return fl2000_drm_resume(&devs->adapter->dev);
> }
>
> static struct usb_driver fl2000_driver = {
> @@ -198,6 +268,7 @@ static struct usb_driver fl2000_driver = {
> .disconnect = fl2000_disconnect,
> .suspend = fl2000_suspend,
> .resume = fl2000_resume,
> + .reset_resume = 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..6ad0f1dd0
> --- /dev/null
> +++ b/drivers/gpu/drm/fl2000/fl2000_interrupt.c
> @@ -0,0 +1,237 @@
> +// 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 drm_device *drm;
> + u8 poll_interval;
> + 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->usb_dev, status);
> + if (!ret)
> + atomic_or(*status & FL2000_SINK_EVENTS, &intr->events);
> +
> + return ret;
> +}
> +
> +/**
> + * 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.
> + *
> + * 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;
> +
> + 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_release(struct device *dev, void *res)
> +{
> + struct fl2000_intr *intr = res;
> + struct usb_device *usb_dev = to_usb_device(dev);
> +
> + usb_poison_urb(intr->urb);
> + cancel_work_sync(&intr->work);
> + if (intr->work_queue)
> + destroy_workqueue(intr->work_queue);
> + usb_free_coherent(usb_dev, INTR_BUFSIZE, intr->buf, intr->transfer_dma);
> + usb_free_urb(intr->urb);
> +}
> +
> +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)
> + 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);
> +
> + /*
> + * For interrupt URBs, as part of successful URB submission urb->interval is modified to
> + * reflect the actual transfer period used, so we need to restore it.
> + */
> + urb->interval = intr->poll_interval;
> + urb->start_frame = -1;
> +
> + /* Restart urb */
> + ret = usb_submit_urb(urb, GFP_ATOMIC);
> + if (ret)
> + dev_err(&usb_dev->dev, "URB submission failed (%d)\n", ret);
> +}
> +
> +/**
> + * fl2000_intr_create() - interrupt processing context creation
> + * @usb_dev: FL2000 USB device
> + * @drm: DRM device that receives the hotplug events
> + *
> + * Called on DRM master bind. 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 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 = devres_alloc(&fl2000_intr_release, sizeof(*intr), GFP_KERNEL);
> + if (!intr)
> + return ERR_PTR(-ENOMEM);
> + devres_add(&usb_dev->dev, intr);
> +
> + intr->poll_interval = desc->bInterval;
> + intr->usb_dev = usb_dev;
> + intr->drm = drm;
> + INIT_WORK(&intr->work, &fl2000_intr_work);
> + atomic_set(&intr->events, 0);
> +
> + intr->urb = usb_alloc_urb(0, GFP_KERNEL);
> + if (!intr->urb) {
> + devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + intr->buf = usb_alloc_coherent(usb_dev, INTR_BUFSIZE, GFP_KERNEL, &intr->transfer_dma);
> + if (!intr->buf) {
> + devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + intr->work_queue = alloc_ordered_workqueue("fl2000_interrupt", 0);
> + if (!intr->work_queue) {
> + devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
> + return ERR_PTR(-ENOMEM);
> + }
> +
> + /* Interrupt URB configuration is static, including allocated buffer */
> + usb_fill_int_urb(intr->urb, usb_dev, usb_rcvintpipe(usb_dev, 3), intr->buf, INTR_BUFSIZE,
> + fl2000_intr_completion, intr, intr->poll_interval);
> + intr->urb->transfer_dma = intr->transfer_dma;
> + intr->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
> +
> + /* Start checking for interrupts */
> + ret = usb_submit_urb(intr->urb, GFP_KERNEL);
> + if (ret) {
> + dev_err(&usb_dev->dev, "URB submission failed (%d)\n", ret);
> + devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
> + return ERR_PTR(ret);
> + }
> +
> + return intr;
> +}
> +
> +/**
> + * 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_resume().
> + */
> +void fl2000_intr_suspend(struct fl2000_intr *intr)
> +{
> + usb_kill_urb(intr->urb);
> + cancel_work_sync(&intr->work);
> +}
> +
> +/**
> + * fl2000_intr_resume() - poll the interrupt endpoint again after system sleep
> + * @intr: interrupt context
> + *
> + * Return: 0 on success, the URB submission error otherwise
> + */
> +int fl2000_intr_resume(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 usb_device *usb_dev)
> +{
> + devres_release(&usb_dev->dev, fl2000_intr_release, NULL, NULL);
> +}
> diff --git a/drivers/gpu/drm/fl2000/fl2000_vga.c b/drivers/gpu/drm/fl2000/fl2000_vga.c
> new file mode 100644
> index 000000000..322cd8b16
> --- /dev/null
> +++ b/drivers/gpu/drm/fl2000/fl2000_vga.c
> @@ -0,0 +1,256 @@
> +// 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
> +
> +/* The first probes of a connection retry the EDID, in case the monitor is still waking up */
> +#define FL2000_VGA_EDID_TRIES 3
> +
> +/* 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;
> + else if (status != connector_status_connected)
> + /* Whatever comes next may be another monitor: its EDID must be read again */
> + WRITE_ONCE(vga->edid_stale, true);
> +
> + drm_dev_exit(idx);
> +
> + return status;
> +}
> +
> +/*
> + * The EDID is read through the FL2000 I2C engine. It is kept for as long as the monitor stays, and
> + * only a few probes try for it: a read that finds nothing is quick, but old monitors often do not
> + * answer DDC at all, and every probe runs with the mode config locked.
> + */
> +static int fl2000_vga_get_modes(struct drm_connector *connector)
> +{
> + struct fl2000_vga *vga = to_fl2000_vga(connector);
> + int count = 0, idx;
> +
> + if (!drm_dev_enter(connector->dev, &idx))
> + return 0;
> +
> + if (READ_ONCE(vga->edid_stale)) {
> + WRITE_ONCE(vga->edid_stale, false);
> + drm_edid_free(vga->edid);
> + vga->edid = NULL;
> + vga->edid_tries = 0;
> + }
> +
> + if (!vga->edid && vga->edid_tries < FL2000_VGA_EDID_TRIES) {
> + vga->edid_tries++;
> + vga->edid = drm_edid_read_custom(connector, fl2000_vga_read_block, vga);
> + drm_dbg_kms(connector->dev, "VGA EDID read %s (try %u)\n",
> + vga->edid ? "ok" : "failed", vga->edid_tries);
> + }
> +
> + drm_edid_connector_update(connector, vga->edid);
> + if (vga->edid)
> + count = drm_edid_connector_add_modes(connector);
> +
> + 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,
> +};
> +
> +/**
> + * fl2000_vga_sink_event() - the chip reports that a monitor appeared, went away or changed EDID
> + * @vga: VGA output
> + *
> + * Called from the interrupt work. The EDID is read again by the next probe.
> + */
> +void fl2000_vga_sink_event(struct fl2000_vga *vga)
> +{
> + WRITE_ONCE(vga->edid_stale, true);
> +}
> +
> +static void fl2000_vga_release(struct drm_device *drm, void *res)
> +{
> + struct fl2000_vga *vga = res;
> +
> + drm_edid_free(vga->edid);
> + 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;
> +}
>
--
--
Thomas Zimmermann
Graphics Driver Developer
SUSE Software Solutions Germany GmbH
Frankenstr. 146, 90461 Nürnberg, Germany, www.suse.com
GF: Stefan Gaiser, Jochen Jaser, Abhinav Puri, (HRB 36809, AG Nürnberg)
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-11-06 9:59 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-06 2:34 [PATCH RFC 0/4] drm: Add Fresco Logic FL2000 USB display driver Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 1/4] drm/fl2000: Add USB driver with register and I2C access Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 2/4] drm/fl2000: Add ITE IT66121 HDMI transmitter support Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 3/4] drm/fl2000: Add frame streaming Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 2:34 ` [PATCH RFC 4/4] drm/fl2000: Add KMS display pipeline Angel Gabriel Crispin Valdivia via B4 Relay
2026-11-06 9:59 ` Thomas Zimmermann
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®