* [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter
@ 2026-11-09 9:26 Luo Liu
2026-11-09 9:26 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
2026-11-09 9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
0 siblings, 2 replies; 7+ messages in thread
From: Luo Liu @ 2026-11-09 9:26 UTC (permalink / raw)
To: dri-devel, devicetree
Cc: Luo.Liu, maarten.lankhorst, mripard, tzimmermann, robh, krzk+dt,
conor+dt, wuyi.shen, cix-kernel-upstream, gary.yang, fugang.duan
Add the binding and the DRM bridge driver for the CIX Sky1
DisplayPort transmitter.
The devicetree nodes are left out of this series. They depend on the
DisplayPort PHY binding and driver, which are not yet upstream. The
SoC and board nodes will be sent once that PHY series is accepted.
Luo Liu (2):
dt-bindings: display: add CIX Sky1 DisplayPort transmitter
drm/cix: add Sky1 DisplayPort transmitter driver
.../bindings/display/cix,sky1-dptx.yaml | 108 +
MAINTAINERS | 9 +
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/cix/Kconfig | 3 +
drivers/gpu/drm/cix/Makefile | 3 +
drivers/gpu/drm/cix/dptx/Kconfig | 16 +
drivers/gpu/drm/cix/dptx/Makefile | 4 +
drivers/gpu/drm/cix/dptx/cix_dptx.c | 2086 +++++++++++++++++
drivers/gpu/drm/cix/dptx/cix_dptx.h | 370 +++
drivers/gpu/drm/cix/dptx/cix_dptx_drm.c | 653 ++++++
drivers/gpu/drm/cix/dptx/cix_dptx_drm.h | 16 +
12 files changed, 3270 insertions(+)
create mode 100644 Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
create mode 100644 drivers/gpu/drm/cix/Kconfig
create mode 100644 drivers/gpu/drm/cix/Makefile
create mode 100644 drivers/gpu/drm/cix/dptx/Kconfig
create mode 100644 drivers/gpu/drm/cix/dptx/Makefile
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx.c
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx.h
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx_drm.c
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx_drm.h
base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
--
2.25.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 1/2] dt-bindings: display: add CIX Sky1 DisplayPort transmitter
2026-11-09 9:26 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
@ 2026-11-09 9:26 ` Luo Liu
2026-11-09 20:04 ` sashiko-bot
2026-11-09 9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
1 sibling, 1 reply; 7+ messages in thread
From: Luo Liu @ 2026-11-09 9:26 UTC (permalink / raw)
To: dri-devel, devicetree
Cc: Luo.Liu, maarten.lankhorst, mripard, tzimmermann, robh, krzk+dt,
conor+dt, wuyi.shen, cix-kernel-upstream, gary.yang, fugang.duan
Document the CIX Sky1 DisplayPort transmitter. Each instance sits
between one of the SoC display pipelines and a DisplayPort PHY, and
exposes the DP register block together with the RCSU block used for the
link setup.
The controller is described as a DRM bridge whose video input is linked
through the OF graph, and the DisplayPort PHY is referenced through the
generic PHY framework. The RCSU adapter port, the video stream and the
maximum link rate are described by "cix,adapter-port", "cix,stream-id"
and "cix,dp-max-rate".
Signed-off-by: Luo Liu <Luo.Liu@cixtech.com>
---
.../bindings/display/cix,sky1-dptx.yaml | 108 ++++++++++++++++++
MAINTAINERS | 8 ++
2 files changed, 116 insertions(+)
create mode 100644 Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
diff --git a/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
new file mode 100644
index 000000000000..9087b5edd9f0
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
@@ -0,0 +1,108 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/display/cix,sky1-dptx.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: CIX Sky1 Trilinear DisplayPort Transmitter
+
+maintainers:
+ - Luo Liu <Luo.Liu@cixtech.com>
+
+description:
+ DisplayPort transmitter controller based on the Trilinear DP IP on CIX Sky1.
+
+properties:
+ compatible:
+ const: cix,sky1-dptx
+
+ reg:
+ items:
+ - description: DisplayPort transmitter registers.
+ - description: RCSU video adapter registers used for link setup.
+
+ reg-names:
+ items:
+ - const: dp
+ - const: rcsu
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: APB clock of the controller.
+ - description: Video clock of stream 0.
+ - description: Video clock of stream 1.
+
+ clock-names:
+ items:
+ - const: apb
+ - const: vid0
+ - const: vid1
+
+ resets:
+ maxItems: 1
+ description: DisplayPort controller reset.
+
+ phys:
+ maxItems: 1
+ description: DisplayPort PHY the controller transmits through.
+
+ port:
+ $ref: /schemas/graph.yaml#/properties/port
+ description:
+ Video input from the display pipeline that drives this controller.
+
+ cix,adapter-port:
+ description:
+ RCSU video adapter port of this transmitter. Sky1 numbers its five
+ transmitters from 1 to 5. Port 3 is the native DP/eDP output.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 1
+ maximum: 5
+
+ cix,dp-max-rate:
+ description:
+ Maximum link rate the board is wired for, in units of 10 kbit/s.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [162000, 270000, 540000, 810000]
+ default: 810000
+
+ cix,stream-id:
+ description:
+ Index of the video stream this controller uses. Sky1 has two streams,
+ numbered 0 and 1.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 1
+ default: 0
+
+required:
+ - compatible
+ - reg
+ - reg-names
+ - interrupts
+ - clocks
+ - clock-names
+ - resets
+ - cix,adapter-port
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ dp0: dp@14064000 {
+ compatible = "cix,sky1-dptx";
+ reg = <0x14064000 0x4000>,
+ <0x14050000 0x400>;
+ reg-names = "dp", "rcsu";
+ interrupts = <GIC_SPI 332 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk 0>, <&clk 1>, <&clk 2>;
+ clock-names = "apb", "vid0", "vid1";
+ resets = <&rst 0>;
+ cix,adapter-port = <1>;
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 93abbb315065..7d5283d83022 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6522,6 +6522,14 @@ M: Satish Kharat <satishkh@cisco.com>
S: Supported
F: drivers/infiniband/hw/usnic/
+CIX SKY1 DISPLAYPORT TRANSMITTER DRIVER
+M: Luo Liu <Luo.Liu@cixtech.com>
+M: Wuyi Shen <wuyi.shen@cixtech.com>
+R: CIX Linux Kernel Upstream Group <cix-kernel-upstream@cixtech.com>
+L: dri-devel@lists.freedesktop.org
+S: Maintained
+F: Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
+
CLANG CONTEXT ANALYSIS
M: Marco Elver <elver@google.com>
R: Bart Van Assche <bvanassche@acm.org>
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver
2026-11-09 9:26 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
2026-11-09 9:26 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
@ 2026-11-09 9:26 ` Luo Liu
2026-11-09 20:30 ` sashiko-bot
1 sibling, 1 reply; 7+ messages in thread
From: Luo Liu @ 2026-11-09 9:26 UTC (permalink / raw)
To: dri-devel, devicetree
Cc: Luo.Liu, maarten.lankhorst, mripard, tzimmermann, robh, krzk+dt,
conor+dt, wuyi.shen, cix-kernel-upstream, gary.yang, fugang.duan
Add a DRM bridge driver for the CIX Sky1 DisplayPort transmitter. The
controller implements the DP link training and the source side of the
DisplayPort protocol, and is fed by one of the SoC display pipelines.
The transmitter is exposed as a DRM bridge. The CIX display pipeline
attaches bridges without DRM_BRIDGE_ATTACH_NO_CONNECTOR, so the driver
creates the connector from .attach. When that flag is set, connector
creation is left to the display controller. The connector reads the
EDID, validates modes against the link bandwidth, supports YCbCr 4:2:0
and signals hotplug through drm_helper_hpd_irq_event().
The DisplayPort PHY is driven through the generic PHY API: link-rate and
voltage-swing updates are forwarded to the PHY and the link is retrained
when the sink requests it. Audio and DSC are not supported yet.
Signed-off-by: Luo Liu <Luo.Liu@cixtech.com>
---
MAINTAINERS | 1 +
drivers/gpu/drm/Kconfig | 1 +
drivers/gpu/drm/Makefile | 1 +
drivers/gpu/drm/cix/Kconfig | 3 +
drivers/gpu/drm/cix/Makefile | 3 +
drivers/gpu/drm/cix/dptx/Kconfig | 16 +
drivers/gpu/drm/cix/dptx/Makefile | 4 +
drivers/gpu/drm/cix/dptx/cix_dptx.c | 2086 +++++++++++++++++++++++
drivers/gpu/drm/cix/dptx/cix_dptx.h | 370 ++++
drivers/gpu/drm/cix/dptx/cix_dptx_drm.c | 653 +++++++
drivers/gpu/drm/cix/dptx/cix_dptx_drm.h | 16 +
11 files changed, 3154 insertions(+)
create mode 100644 drivers/gpu/drm/cix/Kconfig
create mode 100644 drivers/gpu/drm/cix/Makefile
create mode 100644 drivers/gpu/drm/cix/dptx/Kconfig
create mode 100644 drivers/gpu/drm/cix/dptx/Makefile
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx.c
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx.h
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx_drm.c
create mode 100644 drivers/gpu/drm/cix/dptx/cix_dptx_drm.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 7d5283d83022..18ee884ae158 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6529,6 +6529,7 @@ R: CIX Linux Kernel Upstream Group <cix-kernel-upstream@cixtech.com>
L: dri-devel@lists.freedesktop.org
S: Maintained
F: Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
+F: drivers/gpu/drm/cix/dptx/
CLANG CONTEXT ANALYSIS
M: Marco Elver <elver@google.com>
diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index 61111d50b1c6..a1808db8b9c8 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -311,6 +311,7 @@ source "drivers/gpu/drm/aspeed/Kconfig"
source "drivers/gpu/drm/ast/Kconfig"
source "drivers/gpu/drm/atmel-hlcdc/Kconfig"
source "drivers/gpu/drm/bridge/Kconfig"
+source "drivers/gpu/drm/cix/Kconfig"
source "drivers/gpu/drm/etnaviv/Kconfig"
source "drivers/gpu/drm/exynos/Kconfig"
source "drivers/gpu/drm/fsl-dcu/Kconfig"
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index 2c6bc093a923..6ed7456bb993 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -169,6 +169,7 @@ obj-y += tests/
obj-$(CONFIG_DRM_MIPI_DBI) += drm_mipi_dbi.o
obj-$(CONFIG_DRM_MIPI_DSI) += drm_mipi_dsi.o
obj-y += arm/
+obj-y += cix/
obj-y += clients/
obj-y += display/
obj-$(CONFIG_DRM_TTM) += ttm/
diff --git a/drivers/gpu/drm/cix/Kconfig b/drivers/gpu/drm/cix/Kconfig
new file mode 100644
index 000000000000..6cbaa5bf9fe9
--- /dev/null
+++ b/drivers/gpu/drm/cix/Kconfig
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+source "drivers/gpu/drm/cix/dptx/Kconfig"
diff --git a/drivers/gpu/drm/cix/Makefile b/drivers/gpu/drm/cix/Makefile
new file mode 100644
index 000000000000..a93591fc49e8
--- /dev/null
+++ b/drivers/gpu/drm/cix/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_DRM_CIX_DP) += dptx/
diff --git a/drivers/gpu/drm/cix/dptx/Kconfig b/drivers/gpu/drm/cix/dptx/Kconfig
new file mode 100644
index 000000000000..0b099f7d2ed5
--- /dev/null
+++ b/drivers/gpu/drm/cix/dptx/Kconfig
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+config DRM_CIX_DP
+ tristate "Cix DisplayPort Controller Driver"
+ depends on COMMON_CLK && DRM && OF
+ depends on GENERIC_PHY
+ select DRM_KMS_HELPER
+ select DRM_DISPLAY_HELPER
+ select DRM_DISPLAY_DP_HELPER
+ help
+ DRM/KMS driver for the Cix DisplayPort controller. It implements
+ the standard DRM bridge interface and drives an external
+ DisplayPort PHY through the generic PHY framework.
+
+ To compile this driver as a module, choose M here: the module will
+ be called cix-dptx.
diff --git a/drivers/gpu/drm/cix/dptx/Makefile b/drivers/gpu/drm/cix/dptx/Makefile
new file mode 100644
index 000000000000..d156178d9a34
--- /dev/null
+++ b/drivers/gpu/drm/cix/dptx/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0-only
+
+cix-dptx-y := cix_dptx.o cix_dptx_drm.o
+obj-$(CONFIG_DRM_CIX_DP) += cix-dptx.o
diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx.c b/drivers/gpu/drm/cix/dptx/cix_dptx.c
new file mode 100644
index 000000000000..8915c50bc9db
--- /dev/null
+++ b/drivers/gpu/drm/cix/dptx/cix_dptx.c
@@ -0,0 +1,2086 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * CIX Sky1 DisplayPort transmitter driver
+ *
+ * Copyright (C) 2026 Trilinear Technologies
+ * Copyright 2026 Cix Technology Group Co., Ltd.
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/irq.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/string.h>
+
+#include <drm/drm_probe_helper.h>
+
+#include "cix_dptx.h"
+#include "cix_dptx_drm.h"
+
+#define DEFAULT_DP_POWER_ON_DELAY_MS 4
+#define AUX_READ_BIT 0x1
+#define DP_HPD_MAX_TRIES 100
+
+#define BIT31_30_MASK (3UL << 30)
+#define BIT20_19_MASK (3UL << 19)
+#define BIT18_17_MASK (3UL << 17)
+#define BIT21_20_MASK (3UL << 20)
+
+#define CFG_ADAPTER_VIDEO0_OFFSET 0x300
+#define CFG_ADAPTER_VIDEO1_OFFSET 0x304
+#define CFG_ADAPTER_VIDEO_OFFSET_DP2 0x320
+/* 1-based RCSU port of the native DP/eDP transmitter. */
+#define CIX_DP_ADAPTER_PORT_DP 3
+#define DPTX_SDP_DB_MAX 32
+#define DPTX_MST_SOURCE_NUMBER 2
+
+/*
+ * Frequency of the DPC APB clock. The AUX channel divider is derived from it
+ * (see cix_dp_aux_clk_divider()), and the DP PHY registers are only reachable
+ * while it runs at this rate.
+ */
+#define CIX_DP_APB_CLK_RATE 200000000UL
+
+/* RCSU adapter video-config bits, per (port, pipe) */
+struct cix_rcsu_video_bits {
+ u32 fmt_mask; /* mask of the format field */
+ u32 yuv422; /* YCbCr422 value to set */
+ u32 yuv420; /* YCbCr420 value to set */
+ u32 ppc_bit; /* 2 pixels/cycle flag */
+};
+
+enum {
+ LINK_TRAIN_CLOCK_RECOVERY = 100,
+ LINK_TRAIN_CHANNEL_EQUALIZATION,
+};
+
+static const char * const stream_clk_names[] = {
+ "vid0", "vid1",
+};
+
+static unsigned int cix_dp_power_on_delay_ms = DEFAULT_DP_POWER_ON_DELAY_MS;
+module_param_named(power_on_delay_ms, cix_dp_power_on_delay_ms, uint, 0444);
+MODULE_PARM_DESC(power_on_delay_ms,
+ "Delay after powering the sink up, in msec (default: 4)");
+
+static void cix_dp_write(struct cix_dp *dp, int offset, u32 val)
+{
+ writel(val, dp->dp_iomem + offset);
+}
+
+static u32 cix_dp_read(struct cix_dp *dp, int offset)
+{
+ return readl(dp->dp_iomem + offset);
+}
+
+static bool cix_dp_get_hpd_state(struct cix_dp *dp)
+{
+ return (cix_dp_read(dp, CIX_DPTX_HPD_INPUT_STATE) & 0x01) ||
+ (dp->force_hpd == connector_status_connected);
+}
+
+static int cix_dp_phy_link_rate_update(struct cix_dp *dp, u8 bw_code)
+{
+ union phy_configure_opts opts = { 0 };
+
+ opts.dp.link_rate = drm_dp_bw_code_to_link_rate(bw_code) / 100;
+ opts.dp.set_rate = 1;
+
+ return phy_configure(dp->phy, &opts);
+}
+
+static int cix_dp_phy_vs_emph_update(struct cix_dp *dp)
+{
+ union phy_configure_opts opts = { 0 };
+ u8 train;
+ int i;
+ int ret = 0;
+
+ opts.dp.lanes = dp->mode.lane_cnt;
+ for (i = 0; i < opts.dp.lanes; i++) {
+ train = dp->train_set[i];
+ opts.dp.voltage[i] =
+ (train & DP_TRAIN_VOLTAGE_SWING_MASK) >>
+ DP_TRAIN_VOLTAGE_SWING_SHIFT;
+ opts.dp.pre[i] = (train & DP_TRAIN_PRE_EMPHASIS_MASK) >>
+ DP_TRAIN_PRE_EMPHASIS_SHIFT;
+ }
+
+ opts.dp.set_voltages = 1;
+ ret = phy_configure(dp->phy, &opts);
+
+ return ret;
+}
+
+static int cix_dp_phy_ready(struct cix_dp *dp)
+{
+ u32 value;
+ u32 ready;
+ int i;
+
+ ready = (1 << dp->num_lanes) - 1;
+
+ for (i = 0; i < 100; i++) {
+ value = cix_dp_read(dp, CIX_DPTX_PHY_STATUS);
+ if ((value & ready) == ready)
+ return 0;
+
+ usleep_range(1000, 1100);
+ }
+
+ return -ETIMEDOUT;
+}
+
+static void cix_dp_mode_set(struct cix_dp *dp, u8 bw_code, int link_rate,
+ u8 lane_cnt, int pclock)
+{
+ dp->mode.bw_code = bw_code;
+ dp->mode.link_rate = link_rate;
+ dp->mode.lane_cnt = lane_cnt;
+ dp->mode.pclock = pclock;
+}
+
+static int cix_dp_get_lanes(struct cix_dp *dp, int link_rate, int pclk, u8 bpp)
+{
+ int max_lanes = dp->link_config.max_lanes;
+ int i;
+
+ for (i = 1; i <= max_lanes; i <<= 1) {
+ if (pclk <= CIX_DP_MAX_RATE(link_rate, i, bpp ? bpp : 24))
+ return i;
+ }
+
+ return 0;
+}
+
+static int cix_dp_mode_configure(struct cix_dp *dp, int pclk, u8 bw, u8 bpp,
+ bool downshift)
+{
+ int max_rate = dp->link_config.max_rate;
+ int lane_cnt;
+ int bw_code;
+ int link_rate;
+
+ if (!downshift)
+ bw_code = bw;
+ else
+ switch (bw) {
+ case DP_LINK_BW_8_1:
+ bw_code = DP_LINK_BW_5_4;
+ break;
+ case DP_LINK_BW_5_4:
+ bw_code = DP_LINK_BW_2_7;
+ break;
+ case DP_LINK_BW_2_7:
+ bw_code = DP_LINK_BW_1_62;
+ break;
+ case DP_LINK_BW_1_62:
+ bw_code = DP_LINK_BW_1_62;
+ break;
+ default:
+ bw_code = drm_dp_link_rate_to_bw_code(max_rate);
+ break;
+ }
+
+ link_rate = drm_dp_bw_code_to_link_rate(bw_code);
+ lane_cnt = cix_dp_get_lanes(dp, link_rate, pclk, bpp);
+ if (!lane_cnt)
+ return -EINVAL;
+
+ cix_dp_mode_set(dp, bw_code, link_rate, lane_cnt, pclk);
+
+ return 0;
+}
+
+static void cix_dp_adjust_train(struct cix_dp *dp, u8 *link_status)
+{
+ u8 voltage = 0;
+ u8 preemphasis = 0;
+ u8 v;
+ u8 p;
+ int i;
+
+ for (i = 0; i < dp->mode.lane_cnt; i++) {
+ v = drm_dp_get_adjust_request_voltage(link_status, i);
+ p = drm_dp_get_adjust_request_pre_emphasis(link_status, i);
+ if (v > voltage)
+ voltage = v;
+ if (p > preemphasis)
+ preemphasis = p;
+ }
+
+ if (voltage >= DP_TRAIN_VOLTAGE_SWING_LEVEL_3)
+ voltage |= DP_TRAIN_MAX_SWING_REACHED;
+
+ if (preemphasis >= DP_TRAIN_PRE_EMPH_LEVEL_2)
+ preemphasis |= DP_TRAIN_MAX_PRE_EMPHASIS_REACHED;
+
+ for (i = 0; i < dp->mode.lane_cnt; i++)
+ dp->train_set[i] = voltage | preemphasis;
+}
+
+static int cix_dp_update_vs_emph(struct cix_dp *dp)
+{
+ int ret;
+
+ ret = drm_dp_dpcd_write(&dp->aux, DP_TRAINING_LANE0_SET, dp->train_set,
+ dp->mode.lane_cnt);
+ if (ret < 0)
+ return ret;
+
+ return cix_dp_phy_vs_emph_update(dp);
+}
+
+static int cix_dp_link_set_pattern(struct cix_dp *dp, int phase,
+ bool fast_train)
+{
+ u32 pattern;
+
+ switch (phase) {
+ case LINK_TRAIN_CLOCK_RECOVERY:
+ pattern = DP_TRAINING_PATTERN_1;
+ break;
+ case LINK_TRAIN_CHANNEL_EQUALIZATION:
+ if (dp->caps.tps4_supported)
+ pattern = DP_TRAINING_PATTERN_4;
+ else if (dp->caps.tps3_supported)
+ pattern = DP_TRAINING_PATTERN_3;
+ else
+ pattern = DP_TRAINING_PATTERN_2;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (pattern == DP_TRAINING_PATTERN_4) {
+ cix_dp_write(dp, CIX_DPTX_TRAINING_PATTERN_SET, 0x4);
+ cix_dp_write(dp, CIX_DPTX_DISABLE_SCRAMBLING, 0);
+
+ } else {
+ cix_dp_write(dp, CIX_DPTX_TRAINING_PATTERN_SET, pattern);
+ pattern = pattern | DP_LINK_SCRAMBLING_DISABLE;
+ }
+
+ if (!fast_train)
+ return drm_dp_dpcd_writeb(&dp->aux, DP_TRAINING_PATTERN_SET,
+ pattern);
+
+ return 0;
+}
+
+static int cix_dp_link_configure(struct cix_dp *dp)
+{
+ u8 bw_code = dp->mode.bw_code;
+ u8 lane_cnt = dp->mode.lane_cnt;
+ int ret;
+
+ cix_dp_write(dp, CIX_DPTX_LANE_COUNT_SET, lane_cnt);
+
+ if (dp->caps.enhanced_framing) {
+ cix_dp_write(dp, CIX_DPTX_ENHANCED_FRAMING_ENABLE, 1);
+ lane_cnt |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
+ }
+
+ if (dp->dpcd[DP_MAX_DOWNSPREAD] & DP_MAX_DOWNSPREAD_0_5) {
+ cix_dp_write(dp, CIX_DPTX_DOWNSPREAD_CONTROL, 1);
+ drm_dp_dpcd_writeb(&dp->aux, DP_DOWNSPREAD_CTRL,
+ DP_SPREAD_AMP_0_5);
+ } else {
+ cix_dp_write(dp, CIX_DPTX_DOWNSPREAD_CONTROL, 0);
+ drm_dp_dpcd_writeb(&dp->aux, DP_DOWNSPREAD_CTRL, 0);
+ }
+
+ ret = drm_dp_dpcd_writeb(&dp->aux, DP_LANE_COUNT_SET, lane_cnt);
+ if (ret < 0)
+ return ret;
+
+ ret = drm_dp_dpcd_writeb(&dp->aux, DP_MAIN_LINK_CHANNEL_CODING_SET,
+ DP_SET_ANSI_8B10B);
+ if (ret < 0)
+ return ret;
+
+ ret = drm_dp_dpcd_writeb(&dp->aux, DP_LINK_BW_SET, bw_code);
+ if (ret < 0)
+ return ret;
+
+ cix_dp_write(dp, CIX_DPTX_LINK_BW_SET, bw_code);
+ ret = cix_dp_phy_link_rate_update(dp, bw_code);
+ if (ret)
+ return ret;
+
+ return cix_dp_phy_ready(dp);
+}
+
+static int cix_dp_link_train_clock_recovery(struct cix_dp *dp)
+{
+ u8 link_status[DP_LINK_STATUS_SIZE];
+ u8 lane_cnt = dp->mode.lane_cnt;
+ bool train_done;
+ u8 prev = 0;
+ int tries = 0;
+ int i;
+ int ret;
+
+ ret = cix_dp_link_set_pattern(dp, LINK_TRAIN_CLOCK_RECOVERY, false);
+ if (ret < 0)
+ return ret;
+
+ for (i = 0; i < 512; i++) {
+ ret = cix_dp_update_vs_emph(dp);
+ if (ret)
+ return ret;
+
+ drm_dp_link_train_clock_recovery_delay(&dp->aux, dp->dpcd);
+ ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+ if (ret < 0)
+ return ret;
+
+ train_done = drm_dp_clock_recovery_ok(link_status, lane_cnt);
+ if (train_done)
+ break;
+
+ if ((dp->train_set[0] &
+ (DP_TRAIN_VOLTAGE_SWING_MASK |
+ DP_TRAIN_PRE_EMPHASIS_MASK)) == prev)
+ tries++;
+ else
+ tries = 0;
+
+ if (tries == DP_MAX_TRAINING_TRIES)
+ break;
+
+ prev = dp->train_set[0] &
+ (DP_TRAIN_VOLTAGE_SWING_MASK |
+ DP_TRAIN_PRE_EMPHASIS_MASK);
+ cix_dp_adjust_train(dp, link_status);
+ }
+
+ if (train_done)
+ return 0;
+
+ return -EIO;
+}
+
+static int cix_dp_link_train_channel_equalization(struct cix_dp *dp)
+{
+ u8 link_status[DP_LINK_STATUS_SIZE];
+ u8 lane_cnt = dp->mode.lane_cnt;
+ bool train_done;
+ int i;
+ int ret;
+
+ ret = cix_dp_link_set_pattern(dp, LINK_TRAIN_CHANNEL_EQUALIZATION,
+ false);
+ if (ret < 0)
+ return ret;
+
+ for (i = 0; i < DP_MAX_TRAINING_TRIES; i++) {
+ ret = cix_dp_update_vs_emph(dp);
+ if (ret)
+ return ret;
+
+ drm_dp_link_train_channel_eq_delay(&dp->aux, dp->dpcd);
+ ret = drm_dp_dpcd_read_link_status(&dp->aux, link_status);
+ if (ret < 0)
+ return ret;
+
+ train_done = drm_dp_channel_eq_ok(link_status, lane_cnt);
+ if (train_done)
+ break;
+
+ cix_dp_adjust_train(dp, link_status);
+ }
+
+ if (train_done)
+ return 0;
+
+ return -EIO;
+}
+
+static void cix_dp_train_end(struct cix_dp *dp)
+{
+ drm_dp_dpcd_writeb(&dp->aux, DP_TRAINING_PATTERN_SET, 0);
+
+ cix_dp_write(dp, CIX_DPTX_TRAINING_PATTERN_SET, 0);
+ cix_dp_write(dp, CIX_DPTX_DISABLE_SCRAMBLING, 0);
+}
+
+static int cix_dp_train(struct cix_dp *dp)
+{
+ int ret;
+
+ ret = cix_dp_link_configure(dp);
+ if (ret < 0)
+ return ret;
+
+ cix_dp_write(dp, CIX_DPTX_DISABLE_SCRAMBLING, 1);
+ memset(dp->train_set, 0, sizeof(dp->train_set));
+
+ ret = cix_dp_link_train_clock_recovery(dp);
+ if (ret)
+ goto end;
+
+ ret = cix_dp_link_train_channel_equalization(dp);
+
+end:
+ cix_dp_train_end(dp);
+
+ return ret;
+}
+
+static int cix_dp_train_loop(struct cix_dp *dp)
+{
+ struct cix_dp_mode *mode = &dp->mode;
+ u8 bpp = dp->connector.config.bpp;
+ u8 bw_code = mode->bw_code;
+ int i;
+ int j;
+ int ret;
+
+ for (i = 0; i < DP_MAX_TRAINING_LOOP; i++) {
+ for (j = 0; j <= 3; j++) {
+ if (!(dp->state & DPTX_STATE_READY))
+ return 0;
+
+ ret = cix_dp_train(dp);
+ if (!ret)
+ return 0;
+
+ ret = cix_dp_mode_configure(dp, mode->pclock, bw_code,
+ bpp, true);
+ if (ret < 0)
+ break;
+
+ bw_code = dp->mode.bw_code;
+ }
+
+ cix_dp_mode_configure(dp, mode->pclock, 0, bpp, true);
+ bw_code = dp->mode.bw_code;
+ }
+
+ return -EIO;
+}
+
+static int cix_dp_aux_cmd_submit(struct cix_dp *dp, u32 cmd, u32 addr,
+ u8 *buf, u32 bytes, u8 *reply)
+{
+ bool is_read = cmd & AUX_READ_BIT;
+ u32 value;
+ int i;
+
+ value = cix_dp_read(dp, CIX_DPTX_AUX_STATUS);
+ if (value & (CIX_DPTX_AUX_STATUS_REQUEST_IN_PROGRESS |
+ CIX_DPTX_AUX_STATUS_REPLY_IN_PROGRESS))
+ return -EBUSY;
+
+ cix_dp_write(dp, CIX_DPTX_AUX_ADDRESS, addr);
+
+ if (!is_read)
+ for (i = 0; i < bytes; i++)
+ cix_dp_write(dp, CIX_DPTX_AUX_WRITE_FIFO, buf[i]);
+
+ value = cmd << CIX_DPTX_AUX_COMMAND_CMD_SHIFT;
+ if (!buf || !bytes)
+ value |= CIX_DPTX_AUX_COMMAND_ADDRESS_ONLY;
+ else
+ value |= (bytes - 1) << CIX_DPTX_AUX_COMMAND_BYTES_SHIFT;
+
+ cix_dp_write(dp, CIX_DPTX_AUX_COMMAND, value);
+
+ for (i = 0; i < 3; i++) {
+ value = cix_dp_read(dp, CIX_DPTX_AUX_STATUS);
+ if (value & CIX_DPTX_AUX_STATUS_REPLY_RECEIVED)
+ break;
+
+ if ((value & CIX_DPTX_AUX_STATUS_REPLY_ERROR) || i == 2)
+ return -ETIMEDOUT;
+
+ usleep_range(1000, 1100);
+ }
+
+ value = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_CODE);
+ if (reply)
+ *reply = value;
+
+ if (is_read) {
+ if (value == CIX_DPTX_AUX_REPLY_CODE_AUX_ACK ||
+ value == CIX_DPTX_AUX_REPLY_CODE_I2C_ACK) {
+ value = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_DATA_COUNT);
+ if ((value & CIX_DPTX_REPLY_DATA_COUNT_MASK) != bytes)
+ return -EIO;
+ }
+ for (i = 0; i < bytes; i++)
+ buf[i] = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_DATA);
+ }
+
+ return 0;
+}
+
+static ssize_t cix_dp_aux_transfer(struct drm_dp_aux *aux,
+ struct drm_dp_aux_msg *msg)
+{
+ struct cix_dp *dp = container_of(aux, struct cix_dp, aux);
+ size_t to_access = 0;
+ size_t accessed_bytes = 0;
+ int ret;
+
+ if (!cix_dp_get_hpd_state(dp))
+ return -ENODEV;
+
+ do {
+ to_access = min_t(size_t, DP_AUX_MAX_PAYLOAD_BYTES,
+ msg->size - accessed_bytes);
+ ret = cix_dp_aux_cmd_submit(dp, msg->request,
+ msg->address + accessed_bytes,
+ msg->buffer + accessed_bytes,
+ to_access, &msg->reply);
+ if (ret)
+ return accessed_bytes ? (ssize_t)accessed_bytes : ret;
+
+ accessed_bytes += to_access;
+ } while (accessed_bytes < msg->size);
+
+ return msg->size;
+}
+
+static int cix_dp_aux_register(struct cix_dp *dp)
+{
+ int ret;
+
+ dp->aux.name = "Cix DP AUX";
+ dp->aux.dev = dp->dev;
+ dp->aux.drm_dev = dp->drm;
+ dp->aux.transfer = cix_dp_aux_transfer;
+
+ ret = drm_dp_aux_register(&dp->aux);
+ if (ret)
+ return ret;
+
+ dp->aux_registered = true;
+
+ return 0;
+}
+
+static void cix_dp_aux_clk_divider(struct cix_dp *dp)
+{
+ unsigned int rate;
+
+ rate = dp->aux_clock_divider;
+ if (!rate) {
+ if (!IS_ERR_OR_NULL(dp->apb_clk))
+ rate = clk_get_rate(dp->apb_clk) / (1000 * 1000);
+ if (!rate)
+ rate = 25;
+ dp->aux_clock_divider = rate;
+ }
+
+ cix_dp_write(dp, CIX_DPTX_AUX_CLOCK_DIVIDER, rate);
+}
+
+static void cix_dp_aux_cleanup(struct cix_dp *dp)
+{
+ if (!dp->aux_registered)
+ return;
+
+ drm_dp_aux_unregister(&dp->aux);
+ dp->aux.drm_dev = NULL;
+ dp->aux_registered = false;
+}
+
+static void cix_dp_mode_set_transfer_unit(struct cix_dp *dp,
+ int stream_id,
+ struct drm_display_mode *mode,
+ int bpp)
+{
+ u32 tu = CIX_DPTX_MSA_TRANSFER_UNIT_SIZE_TU_SIZE_DEF;
+ u32 bw;
+ u32 video_kbytes;
+ u32 bytes_per_tu_scaled;
+ u32 symbols_per_tu;
+ u32 frac_symbols_per_tu;
+
+ video_kbytes = mode->clock * bpp / 8;
+ bw = drm_dp_bw_code_to_link_rate(dp->mode.bw_code);
+
+ /*
+ * Scaled by 1000 (fixed point) so the fraction below can be split
+ * into integer symbols + 1/16 fractional symbols without floating
+ * point.
+ */
+ bytes_per_tu_scaled = video_kbytes * tu /
+ (dp->mode.lane_cnt * bw / 1000);
+
+ /* bit 23:16 - SYMBOLS_PER_TU */
+ symbols_per_tu = (bytes_per_tu_scaled / 1000) & 0xff;
+ tu |= (symbols_per_tu << 16);
+
+ /*
+ * bit 27:24 - FRAC_SYMBOLS_PER_TU: sets the fractional number of
+ * valid symbols per TU in units of 1/16th. An accumulator is used
+ * to increase the number of data per transaction unit by 1 every
+ * 1/N TUs.
+ */
+ frac_symbols_per_tu = ((bytes_per_tu_scaled % 1000) * 16 / 1000) & 0x0f;
+ tu |= (frac_symbols_per_tu << 24);
+
+ cix_dp_write(dp, CIX_DPTX_TU_CONFIG(stream_id), tu);
+}
+
+static void cix_dp_mode_set_timing(struct cix_dp *dp, int stream_id,
+ struct drm_display_mode *mode, int bpp)
+{
+ u8 lane_cnt = dp->mode.lane_cnt;
+ u32 data_control = CIX_DPTX_DATA_CONTROL_DEFAULT;
+ u32 value;
+
+ if (dp->pixel_per_cycle == 2 && dp->mode.lane_cnt == 4 &&
+ dp->mode.link_rate == 810000)
+ data_control = CIX_DPTX_DATA_CONTROL_2PPC_4LANE_HBR3;
+
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_HTOTAL(stream_id), mode->htotal);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_VTOTAL(stream_id), mode->vtotal);
+
+ value = (!!(mode->flags & DRM_MODE_FLAG_PVSYNC)
+ << CIX_DPTX_MAIN_STREAM_POLARITY_VSYNC_SHIFT) |
+ (!!(mode->flags & DRM_MODE_FLAG_PHSYNC)
+ << CIX_DPTX_MAIN_STREAM_POLARITY_HSYNC_SHIFT);
+
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_POLARITY(stream_id), value);
+ cix_dp_write(dp, CIX_DPTX_USER_SYNC_POLARITY(stream_id),
+ CIX_DPTX_MAIN_STREAM_POLARITY_DATAENABLE_HIGH | value);
+
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_HSWIDTH(stream_id),
+ mode->hsync_end - mode->hsync_start);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_VSWIDTH(stream_id),
+ mode->vsync_end - mode->vsync_start);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_HRES(stream_id), mode->hdisplay);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_VRES(stream_id), mode->vdisplay);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_HSTART(stream_id),
+ mode->htotal - mode->hsync_start);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_VSTART(stream_id),
+ mode->vtotal - mode->vsync_start);
+
+ value = ((mode->hdisplay * bpp + 7) / 8 + lane_cnt - 1) / lane_cnt;
+ cix_dp_write(dp, CIX_DPTX_USER_DATA_COUNT(stream_id), value);
+
+ value = (mode->htotal - mode->hdisplay) *
+ (dp->mode.link_rate / 40) * 9 / mode->clock;
+
+ cix_dp_write(dp, CIX_DPTX_SECONDARY_DATA_WINDOW(stream_id), value);
+ cix_dp_write(dp, CIX_DPTX_DATA_CONTROL(stream_id), data_control);
+ cix_dp_write(dp, CIX_DPTX_USER_PIXEL_COUNT(stream_id),
+ dp->pixel_per_cycle);
+}
+
+static int cix_dpcd_power_up(struct cix_dp *dp)
+{
+ struct drm_dp_aux *aux = &dp->aux;
+ u8 value;
+ int i;
+ int ret;
+
+ ret = drm_dp_dpcd_readb(aux, DP_SET_POWER, &value);
+ if (ret < 0)
+ return ret;
+
+ value = (value & ~DP_SET_POWER_MASK) | DP_SET_POWER_D0;
+
+ for (i = 0; i < 3; i++) {
+ ret = drm_dp_dpcd_writeb(aux, DP_SET_POWER, value);
+ if (ret == 1)
+ break;
+ usleep_range(300, 500);
+ }
+
+ if (ret < 0)
+ return ret;
+
+ msleep(cix_dp_power_on_delay_ms);
+
+ return 0;
+}
+
+static int cix_dpcd_power_down(struct cix_dp *dp)
+{
+ struct drm_dp_aux *aux = &dp->aux;
+ u8 value;
+ int ret;
+
+ if (!cix_dp_get_hpd_state(dp))
+ return 0;
+
+ ret = drm_dp_dpcd_readb(aux, DP_SET_POWER, &value);
+ if (ret < 0)
+ return ret;
+
+ value = (value & ~DP_SET_POWER_MASK) | DP_SET_POWER_D3;
+
+ ret = drm_dp_dpcd_writeb(aux, DP_SET_POWER, value);
+ if (ret < 0)
+ return ret;
+
+ usleep_range(200, 205);
+
+ return 0;
+}
+
+static void cix_dp_update_misc(struct cix_dp *dp, int stream_id,
+ struct cix_dp_config *config)
+{
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_MISC0(stream_id), config->misc0);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_MISC1(stream_id), config->misc1);
+ cix_dp_write(dp, CIX_DPTX_MAIN_STREAM_OVERRIDE(stream_id),
+ config->override);
+}
+
+static bool cix_dp_needs_vsc_sdp(enum cix_dp_format output_format,
+ int colorspace)
+{
+ if (output_format == CIX_DP_FORMAT_YCBCR420)
+ return true;
+
+ switch (colorspace) {
+ case DRM_MODE_COLORIMETRY_SYCC_601:
+ case DRM_MODE_COLORIMETRY_OPYCC_601:
+ case DRM_MODE_COLORIMETRY_BT2020_YCC:
+ case DRM_MODE_COLORIMETRY_BT2020_RGB:
+ case DRM_MODE_COLORIMETRY_BT2020_CYCC:
+ return true;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+static int cix_dp_set_misc(struct cix_dp *dp,
+ struct cix_dp_panel *dp_panel)
+{
+ struct cix_dp_config *config;
+
+ if (!dp_panel || !dp_panel->connector)
+ return -EINVAL;
+
+ config = &dp_panel->connector->config;
+
+ config->misc0 &= ~CIX_DPTX_STREAM_MISC0_SYNC_LOCK;
+ config->misc1 &= ~CIX_DPTX_VSC_COLORIMETRY_EN;
+ config->override = 0;
+
+ if (dp->caps.vsc_supported &&
+ cix_dp_needs_vsc_sdp(config->format, config->colorspace)) {
+ config->misc1 |= CIX_DPTX_VSC_COLORIMETRY_EN;
+
+ switch (config->bpc) {
+ case 6:
+ config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_6;
+ break;
+ case 8:
+ config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_8;
+ break;
+ case 10:
+ config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_10;
+ break;
+ case 12:
+ config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_12;
+ break;
+ case 16:
+ config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_16;
+ break;
+ default:
+ config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_8;
+ break;
+ }
+
+ if (config->format == CIX_DP_FORMAT_YCBCR420)
+ config->override |= CIX_DPTX_STREAM_OVERRIDE_YCbCR_420;
+
+ config->override |= CIX_DPTX_STREAM_OVERRIDE_ENABLE;
+ config->dynamic_range = DP_DYNAMIC_RANGE_CTA;
+ config->content_type = DP_CONTENT_TYPE_NOT_DEFINED;
+
+ goto end;
+ }
+
+ config->misc0 &= ~CIX_DPTX_STREAM_MISC0_COMP_FORMAT_MASK;
+ config->misc1 &= ~CIX_DPTX_STREAM_MISC1_Y_ONLY_EN;
+
+ switch (config->format) {
+ case CIX_DP_FORMAT_RGB:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_COMP_FORMAT_RGB;
+ break;
+
+ case CIX_DP_FORMAT_YCBCR444:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_COMP_FORMAT_YCRCB_444;
+ break;
+
+ case CIX_DP_FORMAT_YCBCR422:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_COMP_FORMAT_YCRCB_422;
+ break;
+
+ case CIX_DP_FORMAT_YONLY:
+ config->misc1 |= CIX_DPTX_STREAM_MISC1_Y_ONLY_EN;
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ config->misc0 &= ~CIX_DPTX_STREAM_MISC0_BPC_MASK;
+
+ switch (config->bpc) {
+ case 6:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_BPC_6;
+ break;
+ case 8:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_BPC_8;
+ break;
+ case 10:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_BPC_10;
+ break;
+ case 12:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_BPC_12;
+ break;
+ case 16:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_BPC_16;
+ break;
+ default:
+ config->misc0 |= CIX_DPTX_STREAM_MISC0_BPC_8;
+ break;
+ }
+
+end:
+ cix_dp_update_misc(dp, dp_panel->source_id, config);
+ return 0;
+}
+
+static void cix_dp_cfg_adapter(struct cix_dp *dp, int pipe_id)
+{
+ struct cix_rcsu_video_bits fmt_bits;
+ u32 value;
+ int offset;
+
+ if (dp->cfg_adapter_port == CIX_DP_ADAPTER_PORT_DP) {
+ /* eDP/DP port */
+ offset = CFG_ADAPTER_VIDEO_OFFSET_DP2;
+ if (pipe_id == 0) {
+ fmt_bits.fmt_mask = BIT18_17_MASK;
+ fmt_bits.yuv422 = 2UL << 17;
+ fmt_bits.yuv420 = 3UL << 17;
+ fmt_bits.ppc_bit = BIT(16);
+ } else {
+ fmt_bits.fmt_mask = BIT21_20_MASK;
+ fmt_bits.yuv422 = 2UL << 20;
+ fmt_bits.yuv420 = 3UL << 20;
+ fmt_bits.ppc_bit = BIT(19);
+ }
+ } else {
+ /* USBDP port */
+ offset = pipe_id == 0 ? CFG_ADAPTER_VIDEO0_OFFSET :
+ CFG_ADAPTER_VIDEO1_OFFSET;
+ if (pipe_id == 0) {
+ fmt_bits.fmt_mask = BIT31_30_MASK;
+ fmt_bits.yuv422 = 2UL << 30;
+ fmt_bits.yuv420 = 3UL << 30;
+ fmt_bits.ppc_bit = BIT(29);
+ } else {
+ fmt_bits.fmt_mask = BIT20_19_MASK;
+ fmt_bits.yuv422 = 2UL << 19;
+ fmt_bits.yuv420 = 3UL << 19;
+ fmt_bits.ppc_bit = BIT(18);
+ }
+ }
+
+ value = readl(dp->rcsu_iomem + offset);
+
+ value &= ~fmt_bits.fmt_mask;
+ if (dp->connector.config.format == CIX_DP_FORMAT_YCBCR422)
+ value |= fmt_bits.yuv422;
+ else if (dp->connector.config.format == CIX_DP_FORMAT_YCBCR420)
+ value |= fmt_bits.yuv420;
+
+ if (dp->pixel_per_cycle == 2)
+ value |= fmt_bits.ppc_bit;
+ else
+ value &= ~fmt_bits.ppc_bit;
+
+ writel(value, dp->rcsu_iomem + offset);
+}
+
+static void dptx_infoframe_write_content(struct cix_dp *dp, u32 stream_id,
+ struct dp_sdp *sdp)
+{
+ int i;
+
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_DATA(stream_id),
+ sdp->sdp_header.HB0);
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_DATA(stream_id),
+ sdp->sdp_header.HB1);
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_DATA(stream_id),
+ sdp->sdp_header.HB2);
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_DATA(stream_id),
+ sdp->sdp_header.HB3);
+
+ for (i = 0; i < DPTX_SDP_DB_MAX; i++)
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_DATA(stream_id),
+ sdp->db[i]);
+}
+
+static void dptx_infoframe_select_buffer_pre(struct cix_dp *dp,
+ u32 stream_id, u32 select)
+{
+ u32 value;
+
+ value = cix_dp_read(dp, CIX_DPTX_INFOFRAME_ENABLE(stream_id));
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_ENABLE(stream_id),
+ value & ~(1 << select));
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_SELECT(stream_id), select);
+}
+
+static void dptx_infoframe_select_buffer_end(struct cix_dp *dp,
+ u32 stream_id, u32 select)
+{
+ u32 value;
+
+ value = cix_dp_read(dp, CIX_DPTX_INFOFRAME_ENABLE(stream_id));
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_ENABLE(stream_id),
+ value | (1 << select));
+}
+
+static void dptx_infoframe_rate(struct cix_dp *dp, u32 stream_id, u32 select,
+ u32 rate)
+{
+ u32 value = cix_dp_read(dp, CIX_DPTX_INFOFRAME_RATE(stream_id));
+
+ if (rate)
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_RATE(stream_id),
+ value | (1 << select));
+ else
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_RATE(stream_id),
+ value & ~(1 << select));
+}
+
+static void infoframe_write_packet(struct cix_dp *dp, u32 stream_id,
+ u32 select, struct dp_sdp *sdp, u32 rate)
+{
+ if (stream_id >= DPTX_MST_SOURCE_NUMBER)
+ return;
+
+ if (select >= CIX_MAX_SDP)
+ return;
+
+ dptx_infoframe_select_buffer_pre(dp, stream_id, select);
+ dptx_infoframe_write_content(dp, stream_id, sdp);
+ dptx_infoframe_rate(dp, stream_id, select, rate);
+ dptx_infoframe_select_buffer_end(dp, stream_id, select);
+}
+
+static int dptx_setup_vsc_sdp(struct dp_sdp *sdp,
+ struct cix_dp_config *config)
+{
+ sdp->sdp_header.HB0 = 0;
+ sdp->sdp_header.HB1 = 0x7;
+ sdp->sdp_header.HB2 = 0x5;
+ sdp->sdp_header.HB3 = 0x13;
+
+ switch (config->format) {
+ case CIX_DP_FORMAT_YCBCR444:
+ sdp->db[16] = 0x1 << 4;
+ break;
+ case CIX_DP_FORMAT_YCBCR422:
+ sdp->db[16] = 0x1 << 4;
+ break;
+ case CIX_DP_FORMAT_YCBCR420:
+ sdp->db[16] = 0x3 << 4;
+ break;
+ default:
+ break;
+ }
+
+ switch (config->colorspace) {
+ case DRM_MODE_COLORIMETRY_BT709_YCC:
+ sdp->db[16] |= 0x1;
+ break;
+ case DRM_MODE_COLORIMETRY_XVYCC_601:
+ sdp->db[16] |= 0x2;
+ break;
+ case DRM_MODE_COLORIMETRY_XVYCC_709:
+ sdp->db[16] |= 0x3;
+ break;
+ case DRM_MODE_COLORIMETRY_SYCC_601:
+ sdp->db[16] |= 0x4;
+ break;
+ case DRM_MODE_COLORIMETRY_OPYCC_601:
+ sdp->db[16] |= 0x5;
+ break;
+ case DRM_MODE_COLORIMETRY_BT2020_CYCC:
+ case DRM_MODE_COLORIMETRY_BT2020_RGB:
+ sdp->db[16] |= 0x6;
+ break;
+ case DRM_MODE_COLORIMETRY_BT2020_YCC:
+ sdp->db[16] |= 0x7;
+ break;
+ case DRM_MODE_COLORIMETRY_DCI_P3_RGB_D65:
+ case DRM_MODE_COLORIMETRY_DCI_P3_RGB_THEATER:
+ sdp->db[16] |= 0x4;
+ break;
+ default:
+ sdp->db[16] |= 0x1;
+ break;
+ }
+
+ /*
+ * For pixel encoding formats YCbCr444, YCbCr422, YCbCr420, and Y Only,
+ * the following Component Bit Depth values are defined:
+ * 001b = 8bpc.
+ * 010b = 10bpc.
+ * 011b = 12bpc.
+ * 100b = 16bpc.
+ */
+ switch (config->bpc) {
+ case 16:
+ sdp->db[17] = 0x4;
+ break;
+ case 12:
+ sdp->db[17] = 0x3;
+ break;
+ case 10:
+ sdp->db[17] = 0x2;
+ break;
+ case 8:
+ default:
+ sdp->db[17] = 0x1;
+ break;
+ }
+
+ /* Dynamic Range and Component Bit Depth */
+ if (config->dynamic_range == DP_DYNAMIC_RANGE_CTA)
+ sdp->db[17] |= 0x80; /* DB17[7] */
+
+ sdp->db[18] = config->content_type & 0x7;
+
+ return 0;
+}
+
+static int cix_dp_panel_setup_colorimetry_sdp(struct cix_dp *dp,
+ struct cix_dp_panel *dp_panel)
+{
+ struct cix_connector *conn = dp_panel->connector;
+
+ if (conn->config.override & CIX_DPTX_STREAM_OVERRIDE_ENABLE) {
+ dptx_setup_vsc_sdp(&conn->sdp[CIX_VSC_SDP], &conn->config);
+ infoframe_write_packet(dp, dp_panel->source_id, CIX_VSC_SDP,
+ &conn->sdp[CIX_VSC_SDP], 1);
+ }
+
+ return 0;
+}
+
+static int cix_dp_ctrl_enable_stream_clocks(struct cix_dp *dp,
+ int stream_id, bool enable)
+{
+ struct clk *stream_clk;
+ int ret;
+
+ if (stream_id < 0 || stream_id >= DP_STREAM_MAX)
+ return -EINVAL;
+
+ stream_clk = dp->stream_clk[stream_id];
+ if (IS_ERR_OR_NULL(stream_clk))
+ return -EINVAL;
+
+ if (enable) {
+ ret = clk_prepare_enable(stream_clk);
+ if (ret)
+ return ret;
+ } else {
+ clk_disable_unprepare(stream_clk);
+ }
+
+ return 0;
+}
+
+static struct drm_display_mode *cix_dp_get_mode(struct cix_dp *dp,
+ int stream_id)
+{
+ struct drm_crtc *crtc;
+
+ if (stream_id < 0 || stream_id >= dp->max_streams)
+ return NULL;
+
+ if (!dp->active_panels[stream_id] || !dp->bridge.encoder)
+ return NULL;
+
+ crtc = dp->bridge.encoder->crtc;
+ if (!crtc || !crtc->state)
+ return NULL;
+
+ return &crtc->state->adjusted_mode;
+}
+
+static int cix_dp_panel_hw_cfg(struct cix_dp *dp, int stream_id)
+{
+ struct cix_dp_panel *dp_panel = dp->active_panels[stream_id];
+ struct drm_display_mode *mode = cix_dp_get_mode(dp, stream_id);
+ struct cix_connector *conn;
+
+ if (!mode)
+ return -EINVAL;
+
+ conn = dp_panel->connector;
+
+ cix_dp_cfg_adapter(dp, stream_id);
+
+ cix_dp_set_misc(dp, dp_panel);
+ cix_dp_panel_setup_colorimetry_sdp(dp, dp_panel);
+
+ cix_dp_mode_set_transfer_unit(dp, dp_panel->source_id, mode,
+ conn->config.bpp);
+ cix_dp_mode_set_timing(dp, dp_panel->source_id, mode, conn->config.bpp);
+ return 0;
+}
+
+static int cix_dp_ctrl_stream_on(struct cix_dp *dp, int stream_id)
+{
+ struct drm_display_mode *mode;
+ struct clk *stream_clk;
+ unsigned long rate;
+ int ret;
+
+ if (stream_id < 0 || stream_id >= dp->max_streams)
+ return -EINVAL;
+
+ mode = cix_dp_get_mode(dp, stream_id);
+ if (!mode)
+ return -EINVAL;
+
+ /*
+ * Firmware only pre-programs the video clock of the port it drove
+ * during boot; the others come up without a rate, which leaves the
+ * stream with no pixel clock. Reading the rate first also refreshes
+ * the SCMI clock state, without which setting it fails.
+ */
+ stream_clk = dp->stream_clk[stream_id];
+ if (!IS_ERR_OR_NULL(stream_clk)) {
+ rate = (unsigned long)mode->clock * 1000;
+ if (clk_get_rate(stream_clk) != rate) {
+ ret = clk_set_rate(stream_clk, rate);
+ if (ret)
+ return ret;
+ }
+ }
+
+ ret = cix_dp_ctrl_enable_stream_clocks(dp, stream_id, true);
+ if (ret)
+ return ret;
+
+ ret = cix_dp_panel_hw_cfg(dp, stream_id);
+ if (ret) {
+ cix_dp_ctrl_enable_stream_clocks(dp, stream_id, false);
+ return ret;
+ }
+
+ cix_dp_write(dp, CIX_DPTX_VIDEO_STREAM_ENABLE(stream_id), 1);
+ cix_dp_write(dp, CIX_DPTX_SECONDARY_STREAM_ENABLE(stream_id), 1);
+
+ dp->active_stream_cnt++;
+ return 0;
+}
+
+static int cix_dp_ctrl_stream_off(struct cix_dp *dp, int stream_id)
+{
+ if (stream_id < 0 || stream_id >= dp->max_streams)
+ return -EINVAL;
+
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_ENABLE(stream_id), 0);
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_SELECT(stream_id), 0);
+ cix_dp_write(dp, CIX_DPTX_INFOFRAME_RATE(stream_id), 0);
+ cix_dp_write(dp, CIX_DPTX_SECONDARY_STREAM_ENABLE(stream_id), 0);
+ cix_dp_write(dp, CIX_DPTX_VIDEO_STREAM_ENABLE(stream_id), 0);
+
+ cix_dp_ctrl_enable_stream_clocks(dp, stream_id, false);
+
+ dp->active_stream_cnt--;
+ return 0;
+}
+
+static int cix_dp_panel_read_sink_caps(struct cix_dp *dp)
+{
+ struct drm_dp_aux *drm_aux = &dp->aux;
+ struct cix_dp_link_config *config = &dp->link_config;
+ u8 value = 0;
+ u8 lanes;
+ int ret;
+
+ /* Never leave the previous sink's capabilities behind on failure. */
+ memset(&dp->caps, 0, sizeof(dp->caps));
+
+ ret = drm_dp_read_dpcd_caps(drm_aux, dp->dpcd);
+ if (ret)
+ return ret;
+
+ ret = drm_dp_read_downstream_info(drm_aux, dp->dpcd,
+ dp->downstream_ports);
+ if (ret)
+ return ret;
+
+ dp->caps.enhanced_framing = drm_dp_enhanced_frame_cap(dp->dpcd);
+ dp->caps.tps3_supported = drm_dp_tps3_supported(dp->dpcd);
+ dp->caps.tps4_supported = drm_dp_tps4_supported(dp->dpcd);
+
+ /*
+ * DP_DPRX_FEATURE_ENUMERATION_LIST is DP 1.3+. A NACK here just means
+ * an older sink, so degrade to "unsupported" instead of failing.
+ */
+ if (dp->dpcd[DP_DPCD_REV] >= DP_DPCD_REV_13 &&
+ drm_dp_dpcd_readb(drm_aux, DP_DPRX_FEATURE_ENUMERATION_LIST,
+ &value) == 1)
+ dp->caps.vsc_supported =
+ !!(value & DP_VSC_SDP_EXT_FOR_COLORIMETRY_SUPPORTED);
+
+ lanes = drm_dp_max_lane_count(dp->dpcd);
+ if (lanes > 0) {
+ config->max_lanes = min_t(u8, lanes, dp->num_lanes);
+ config->max_rate = min_t(int, drm_dp_max_link_rate(dp->dpcd),
+ dp->max_rate);
+ }
+
+ return 0;
+}
+
+static int cix_dp_core_on(struct cix_dp *dp, bool shallow)
+{
+ int enable_sources = (1 << dp->dp_panel.source_id);
+ int i;
+ int ret;
+
+ if (dp->state & DPTX_STATE_READY)
+ return 0;
+
+ dp->state |= DPTX_STATE_READY;
+
+ for (i = 0; i < dp->max_streams; i++)
+ cix_dp_write(dp, CIX_DPTX_VIDEO_STREAM_ENABLE(i), 0);
+
+ ret = phy_init(dp->phy);
+ if (ret)
+ goto end1;
+
+ ret = phy_power_on(dp->phy);
+ if (ret)
+ goto end2;
+
+ ret = cix_dpcd_power_up(dp);
+ if (ret)
+ goto end3;
+
+ ret = cix_dp_panel_read_sink_caps(dp);
+ if (ret)
+ goto end3;
+
+ ret = cix_dp_train_loop(dp);
+ if (ret)
+ goto end3;
+
+ cix_dp_write(dp, CIX_DPTX_SOFT_RESET, enable_sources);
+ cix_dp_write(dp, CIX_DPTX_SOURCE_ENABLE, enable_sources);
+ cix_dp_write(dp, CIX_DPTX_FORCE_SCRAMBLER_RESET, enable_sources);
+ cix_dp_write(dp, CIX_DPTX_MST_ENABLE, 0);
+ cix_dp_write(dp, CIX_DPTX_SST_SOURCE_SELECT, dp->dp_panel.source_id);
+
+ dp->state |= DPTX_STATE_INIT_TRAIN;
+
+ return 0;
+
+end3:
+ phy_power_off(dp->phy);
+end2:
+ phy_exit(dp->phy);
+end1:
+ dp->state &= ~DPTX_STATE_READY;
+ return ret;
+}
+
+static int cix_dp_core_off(struct cix_dp *dp)
+{
+ if (!(dp->state & DPTX_STATE_READY))
+ return 0;
+
+ cix_dpcd_power_down(dp);
+
+ phy_power_off(dp->phy);
+ phy_exit(dp->phy);
+
+ cix_dp_write(dp, CIX_DPTX_MST_ENABLE, 0);
+ cix_dp_write(dp, CIX_DPTX_SOURCE_ENABLE, 0);
+ cix_dp_write(dp, CIX_DPTX_SOFT_RESET, 0);
+ cix_dp_write(dp, CIX_DPTX_FORCE_SCRAMBLER_RESET, 0);
+
+ dp->state &= ~(DPTX_STATE_READY | DPTX_STATE_INIT_TRAIN);
+
+ return 0;
+}
+
+static int reset_dp_and_reinit(struct cix_dp *dp)
+{
+ int ret;
+
+ reset_control_assert(dp->reset);
+ usleep_range(10, 20);
+ reset_control_deassert(dp->reset);
+
+ ret = phy_reset(dp->phy);
+ if (ret)
+ return ret;
+
+ usleep_range(100, 200);
+
+ cix_dp_write(dp, CIX_DPTX_TRANSMITTER_ENABLE, 1);
+ cix_dp_write(dp, CIX_DPTX_INTERRUPT_MASK, CIX_DPTX_INTERRUPT_CFG);
+ cix_dp_aux_clk_divider(dp);
+
+ ret = phy_init(dp->phy);
+ if (ret)
+ return ret;
+
+ return ret;
+}
+
+static int cix_dp_host_init(struct cix_dp *dp)
+{
+ int ret;
+
+ if (dp->state & DPTX_STATE_INITIALIZED)
+ return 0;
+
+ /*
+ * APB is shared with the DP PHY. Enable it before phy_init/phy_reset
+ * and keep one consumer reference until host_deinit so phy_exit cannot
+ * gate the bus under ongoing DPTx register access.
+ */
+ if (!IS_ERR_OR_NULL(dp->apb_clk)) {
+ /*
+ * Firmware leaves the APB clock of ports the bootloader never
+ * brought up without a rate, so the whole DP block is dead
+ * until we program it ourselves.
+ */
+ if (!clk_get_rate(dp->apb_clk)) {
+ ret = clk_set_rate(dp->apb_clk, CIX_DP_APB_CLK_RATE);
+ if (ret) {
+ dev_err(dp->dev,
+ "failed to set apb clock rate: %d\n",
+ ret);
+ return ret;
+ }
+ }
+
+ ret = clk_prepare_enable(dp->apb_clk);
+ if (ret)
+ return ret;
+ }
+
+ ret = reset_dp_and_reinit(dp);
+ if (ret)
+ goto err_clk;
+
+ dp->state |= DPTX_STATE_INITIALIZED;
+
+ return 0;
+
+err_clk:
+ if (!IS_ERR_OR_NULL(dp->apb_clk))
+ clk_disable_unprepare(dp->apb_clk);
+ return ret;
+}
+
+static void cix_dp_host_deinit(struct cix_dp *dp)
+{
+ if (dp->active_stream_cnt)
+ return;
+
+ if (!(dp->state & DPTX_STATE_INITIALIZED))
+ return;
+
+ /* Drop DPTx state while APB is still held by this driver. */
+ cix_dp_write(dp, CIX_DPTX_TRANSMITTER_ENABLE, 0);
+ cix_dp_write(dp, CIX_DPTX_INTERRUPT_MASK,
+ CIX_DPTX_INTERRUPT_MASK_ALL);
+
+ phy_exit(dp->phy);
+
+ /*
+ * Balance host_init; do not use __clk_is_enabled() with a shared
+ * clock.
+ */
+ if (!IS_ERR_OR_NULL(dp->apb_clk))
+ clk_disable_unprepare(dp->apb_clk);
+
+ dp->state &= ~(DPTX_STATE_INITIALIZED | DPTX_STATE_INIT_TRAIN);
+}
+
+static int cix_dp_handle_connect(struct cix_dp *dp)
+{
+ int ret = 0;
+
+ mutex_lock(&dp->session_lock);
+ if (dp->state & DPTX_STATE_CONNECTED) {
+ mutex_unlock(&dp->session_lock);
+ return 0;
+ }
+
+ if (dp->state & DPTX_STATE_SUSPENDED)
+ goto end;
+
+ dp->state |= DPTX_STATE_CONNECTED;
+
+ ret = cix_dp_panel_read_sink_caps(dp);
+ if (ret == -ETIMEDOUT || ret == -ENOTCONN)
+ dp->state &= ~DPTX_STATE_CONNECTED;
+
+end:
+ mutex_unlock(&dp->session_lock);
+ return ret;
+}
+
+static int cix_dp_handle_disconnect(struct cix_dp *dp)
+{
+ mutex_lock(&dp->session_lock);
+
+ if (!(dp->state & DPTX_STATE_CONNECTED))
+ goto end;
+
+ dp->state &= ~DPTX_STATE_CONNECTED;
+end:
+ mutex_unlock(&dp->session_lock);
+ return 0;
+}
+
+/*
+ * Pure probe: report the current HPD level without touching the session state
+ * or the hardware. This runs on every DRM probe, poll and hotplug, so
+ * DPTX_STATE_CONNECTED is advanced from the HPD event work and the encoder
+ * prepare path instead.
+ *
+ * The host is initialized by probe, resume and cix_dp_prepare(). Bail out
+ * loudly if we are ever called before that: cix_dp_read() is a plain readl()
+ * and would hang on a gated APB clock.
+ */
+static int cix_dp_detect(struct cix_dp *dp)
+{
+ int ret = 0;
+
+ mutex_lock(&dp->session_lock);
+
+ if (dp->state & DPTX_STATE_SUSPENDED) {
+ ret = -EBUSY;
+ } else if (!(dp->state & DPTX_STATE_INITIALIZED)) {
+ dev_warn_once(dp->dev, "detect with uninitialized host\n");
+ ret = -ENODEV;
+ }
+
+ mutex_unlock(&dp->session_lock);
+
+ if (ret)
+ return ret;
+
+ return cix_dp_get_hpd_state(dp) ? 0 : -ENODEV;
+}
+
+static void cix_dp_sw_hpd_irq_work_func(struct work_struct *work)
+{
+ struct cix_dp *dp =
+ container_of(work, struct cix_dp, sw_hpd_irq_work.work);
+
+ if (dp->state & DPTX_STATE_SUSPENDED)
+ return;
+
+ /*
+ * The out-of-band hotplug callback only reports the Type-C plug
+ * event; handle it like any other IRQ_HPD and let the generic link
+ * status check decide whether a retrain is needed.
+ */
+ queue_delayed_work(system_freezable_wq, &dp->hpd_irq_work, 0);
+}
+
+/*
+ * USB-C combo PHY only. Its HPD pin is not wired to this controller, so a
+ * sink that was plugged in before the connector existed never raises
+ * HPD_EVENT. Replay that plug once the PHY is already in DP mode and AUX
+ * answers.
+ *
+ * A native DP port wires HPD to the controller and leaves the PHY mode at
+ * PHY_MODE_INVALID, so the checks below skip it. That port is reported by
+ * the HPD interrupt instead.
+ */
+static void cix_dp_replay_deferred_hotplug(struct cix_dp *dp)
+{
+ struct phy *phy = dp->phy;
+ union phy_configure_opts opts = { 0 };
+ u8 dpcd_rev;
+
+ if (!(dp->state & DPTX_STATE_INITIALIZED))
+ return;
+ if (dp->force_hpd != connector_status_unknown)
+ return;
+ if (cix_dp_get_hpd_state(dp))
+ return;
+
+ phy_validate(phy, PHY_MODE_DP, 0, &opts);
+ if (phy_get_mode(phy) != PHY_MODE_DP)
+ return;
+
+ /*
+ * The combo PHY is already muxed to DP, so the sink may have been
+ * plugged in before the connector existed. Probe the AUX channel to
+ * make sure one is really there before forcing a hotplug event.
+ */
+ if (drm_dp_dpcd_readb(&dp->aux, DP_DPCD_REV, &dpcd_rev) < 0)
+ return;
+
+ dp->force_hpd = connector_status_connected;
+ dp->status = connector_status_unknown;
+ schedule_delayed_work(&dp->hpd_event_work, 0);
+}
+
+static void cix_dp_hpd_event_work_func(struct work_struct *work)
+{
+ struct cix_dp *dp = container_of(work, struct cix_dp,
+ hpd_event_work.work);
+ struct phy *phy = dp->phy;
+ union phy_configure_opts opts = { 0 };
+ enum drm_connector_status old_status;
+ enum phy_mode mode;
+ int i;
+
+ if (!dp->dp_panel.connector)
+ return;
+
+ /*
+ * On a USB-C DP alt-mode PHY the hotplug only becomes valid once the
+ * PHY has been switched to DP mode, so wait for the mode change.
+ * A native DP PHY has no ->set_mode() operation, which leaves the
+ * mode at PHY_MODE_INVALID forever; there is nothing to wait for.
+ */
+ if (cix_dp_get_hpd_state(dp) &&
+ phy_get_mode(phy) != PHY_MODE_INVALID) {
+ for (i = 0; i < DP_HPD_MAX_TRIES; i++) {
+ phy_validate(phy, PHY_MODE_DP, 0, &opts);
+ mode = phy_get_mode(phy);
+ if (mode == PHY_MODE_DP)
+ break;
+ msleep(100);
+ }
+ }
+
+ old_status = dp->status;
+ drm_helper_probe_detect(&dp->connector.base, NULL, false);
+
+ if (dp->status == connector_status_connected)
+ cix_dp_handle_connect(dp);
+ else
+ cix_dp_handle_disconnect(dp);
+
+ if (old_status != dp->status && dp->drm)
+ drm_helper_hpd_irq_event(dp->drm);
+}
+
+static void cix_dp_hpd_irq_work_func(struct work_struct *work)
+{
+ struct cix_dp *dp = container_of(work, struct cix_dp,
+ hpd_irq_work.work);
+ u8 status[DP_LINK_STATUS_SIZE + 2];
+ int ret;
+
+ if (!cix_dp_get_hpd_state(dp))
+ return;
+
+ mutex_lock(&dp->session_lock);
+
+ if (!(dp->state & DPTX_STATE_INITIALIZED) ||
+ !(dp->state & DPTX_STATE_ENABLED))
+ goto end;
+
+ ret = drm_dp_dpcd_read(&dp->aux, DP_SINK_COUNT, status,
+ DP_LINK_STATUS_SIZE + 2);
+ if (ret < 0)
+ goto end;
+
+ if (drm_dp_clock_recovery_ok(&status[2], dp->mode.lane_cnt) &&
+ drm_dp_channel_eq_ok(&status[2], dp->mode.lane_cnt))
+ goto end;
+
+ cix_dp_train_loop(dp);
+
+end:
+ mutex_unlock(&dp->session_lock);
+}
+
+static irqreturn_t cix_dp_irq_handler(int irq, void *data)
+{
+ struct cix_dp *dp = data;
+ u32 value;
+ u32 mask;
+
+ value = cix_dp_read(dp, CIX_DPTX_INTERRUPT_CAUSE);
+ mask = cix_dp_read(dp, CIX_DPTX_INTERRUPT_MASK);
+ if (!(value & ~mask))
+ return IRQ_NONE;
+
+ if (value & CIX_DPTX_INTERRUPT_GP_TIMER_IRQ)
+ dev_dbg_ratelimited(dp->dev, "gp tmr\n");
+
+ if (value & CIX_DPTX_INTERRUPT_REPLY_TIMEOUT)
+ dev_dbg_ratelimited(dp->dev, "reply timeout\n");
+
+ if (value & CIX_DPTX_INTERRUPT_REPLY_RECIEVED)
+ dev_dbg_ratelimited(dp->dev, "reply received\n");
+
+ if (value & CIX_DPTX_INTERRUPT_HPD_EVENT)
+ queue_delayed_work(system_freezable_wq, &dp->hpd_event_work, 0);
+
+ if (value & CIX_DPTX_INTERRUPT_HPD_IRQ)
+ queue_delayed_work(system_freezable_wq, &dp->hpd_irq_work, 0);
+
+ return IRQ_HANDLED;
+}
+
+static int cix_dp_pm_prepare(struct cix_dp *dp)
+{
+ if (dp->drm && dp->drm->mode_config.poll_enabled)
+ drm_kms_helper_poll_disable(dp->drm);
+
+ disable_irq(dp->irq);
+
+ cancel_delayed_work_sync(&dp->sw_hpd_irq_work);
+ cancel_delayed_work_sync(&dp->hpd_irq_work);
+ cancel_delayed_work_sync(&dp->hpd_event_work);
+
+ mutex_lock(&dp->session_lock);
+ dp->state |= DPTX_STATE_SUSPENDED;
+ if (!dp->active_stream_cnt)
+ cix_dp_host_deinit(dp);
+ dp->status = connector_status_unknown;
+ dp->state &= ~DPTX_STATE_CONNECTED;
+ mutex_unlock(&dp->session_lock);
+
+ return 0;
+}
+
+static int cix_dp_pm_complete(struct cix_dp *dp)
+{
+ int ret = 0;
+
+ mutex_lock(&dp->session_lock);
+
+ if (!(dp->state & DPTX_STATE_SUSPENDED))
+ goto end;
+
+ ret = cix_dp_host_init(dp);
+ if (ret)
+ goto end;
+
+ enable_irq(dp->irq);
+ dp->state &= ~DPTX_STATE_SUSPENDED;
+
+ if (dp->drm && dp->drm->mode_config.poll_enabled)
+ drm_kms_helper_poll_enable(dp->drm);
+end:
+ mutex_unlock(&dp->session_lock);
+ return ret;
+}
+
+static int cix_dp_prepare(struct cix_dp *dp)
+{
+ int ret;
+
+ mutex_lock(&dp->session_lock);
+
+ if (dp->state & (DPTX_STATE_ENABLED)) {
+ ret = 0;
+ goto end;
+ }
+
+ if (!(dp->state & DPTX_STATE_INITIALIZED)) {
+ ret = cix_dp_host_init(dp);
+ if (ret)
+ goto end;
+ } else if (dp->state & DPTX_STATE_INIT_TRAIN) {
+ reset_dp_and_reinit(dp);
+ usleep_range(100, 200);
+ }
+
+ if (cix_dp_get_hpd_state(dp) &&
+ !(dp->state & DPTX_STATE_CONNECTED)) {
+ mutex_unlock(&dp->session_lock);
+ cix_dp_handle_connect(dp);
+ mutex_lock(&dp->session_lock);
+ }
+
+ if (!(cix_dp_get_hpd_state(dp) && (dp->state & DPTX_STATE_CONNECTED))) {
+ ret = 0;
+ goto end;
+ }
+
+ ret = cix_dp_core_on(dp, true);
+
+end:
+ mutex_unlock(&dp->session_lock);
+ return ret;
+}
+
+static int cix_dp_unprepare(struct cix_dp *dp)
+{
+ mutex_lock(&dp->session_lock);
+
+ if (!(dp->state & DPTX_STATE_ENABLED))
+ goto end;
+
+ if (dp->active_stream_cnt)
+ goto end;
+
+ cix_dp_core_off(dp);
+
+ if (dp->state & DPTX_STATE_SUSPENDED)
+ cix_dp_host_deinit(dp);
+
+ dp->state &= ~DPTX_STATE_ENABLED;
+
+end:
+ mutex_unlock(&dp->session_lock);
+
+ return 0;
+}
+
+static int cix_dp_enable(struct cix_dp *dp, int stream_id)
+{
+ struct cix_dp_panel *dp_panel;
+ int ret;
+
+ if (stream_id < 0 || stream_id >= dp->max_streams)
+ return -EINVAL;
+
+ dp_panel = &dp->dp_panel;
+
+ mutex_lock(&dp->session_lock);
+
+ if (!(dp->state & DPTX_STATE_INITIALIZED) ||
+ !(dp->state & DPTX_STATE_READY)) {
+ dev_dbg(dp->dev, "enable skipped, state=0x%x\n", dp->state);
+ ret = 0;
+ goto end;
+ }
+
+ dp->active_panels[stream_id] = dp_panel;
+ ret = cix_dp_ctrl_stream_on(dp, stream_id);
+ if (ret) {
+ dp->active_panels[stream_id] = NULL;
+ goto end;
+ }
+
+ dp->state |= DPTX_STATE_ENABLED;
+end:
+ mutex_unlock(&dp->session_lock);
+ return ret;
+}
+
+static int cix_dp_disable(struct cix_dp *dp, int stream_id)
+{
+ struct cix_dp_panel *dp_panel;
+ struct cix_connector *conn;
+
+ if (stream_id < 0 || stream_id >= dp->max_streams)
+ return -EINVAL;
+
+ dp_panel = &dp->dp_panel;
+
+ mutex_lock(&dp->session_lock);
+
+ if (!(dp->state & DPTX_STATE_ENABLED))
+ goto end;
+
+ if (!(dp->state & DPTX_STATE_INITIALIZED))
+ goto end;
+
+ if (dp->active_stream_cnt && dp->active_panels[stream_id]) {
+ cix_dp_ctrl_stream_off(dp, stream_id);
+ dp->active_panels[stream_id] = NULL;
+ }
+
+ conn = dp_panel->connector;
+ if (conn)
+ memset(conn->sdp, 0, sizeof(conn->sdp));
+end:
+ mutex_unlock(&dp->session_lock);
+
+ return 0;
+}
+
+static int cix_dp_encoder_enable(struct cix_dp *dp, int stream_id, int enable)
+{
+ int ret;
+
+ if (enable) {
+ ret = cix_dp_prepare(dp);
+ if (ret)
+ return ret;
+
+ ret = cix_dp_enable(dp, stream_id);
+ } else {
+ if (!(dp->state & DPTX_STATE_INITIALIZED))
+ return 0;
+
+ ret = cix_dp_disable(dp, stream_id);
+ if (ret)
+ return ret;
+
+ ret = cix_dp_unprepare(dp);
+ }
+
+ return ret;
+}
+
+static int cix_dp_init(struct cix_dp *dp)
+{
+ int ret;
+
+ INIT_DELAYED_WORK(&dp->hpd_event_work, cix_dp_hpd_event_work_func);
+ INIT_DELAYED_WORK(&dp->hpd_irq_work, cix_dp_hpd_irq_work_func);
+ INIT_DELAYED_WORK(&dp->sw_hpd_irq_work, cix_dp_sw_hpd_irq_work_func);
+
+ mutex_init(&dp->session_lock);
+ dp->state |= (DPTX_STATE_CONFIGURED);
+
+ ret = cix_dp_host_init(dp);
+ if (ret)
+ goto end1;
+
+ irq_set_status_flags(dp->irq, IRQ_DISABLE_UNLAZY);
+ ret = devm_request_threaded_irq(dp->dev, dp->irq, NULL,
+ cix_dp_irq_handler, IRQF_ONESHOT,
+ dev_name(dp->dev), dp);
+ if (ret < 0)
+ goto end2;
+
+ return 0;
+
+end2:
+ mutex_lock(&dp->session_lock);
+ cix_dp_host_deinit(dp);
+ mutex_unlock(&dp->session_lock);
+end1:
+ dp->state &= ~DPTX_STATE_CONFIGURED;
+ mutex_destroy(&dp->session_lock);
+ return ret;
+}
+
+static void cix_dp_deinit(struct cix_dp *dp)
+{
+ cancel_delayed_work_sync(&dp->sw_hpd_irq_work);
+ cancel_delayed_work_sync(&dp->hpd_irq_work);
+ cancel_delayed_work_sync(&dp->hpd_event_work);
+ disable_irq(dp->irq);
+
+ mutex_lock(&dp->session_lock);
+ cix_dp_host_deinit(dp);
+ dp->state &= ~(DPTX_STATE_CONFIGURED);
+ mutex_unlock(&dp->session_lock);
+
+ mutex_destroy(&dp->session_lock);
+}
+
+static int cix_dp_parse_dt(struct cix_dp *dp)
+{
+ struct device *dev = dp->dev;
+ struct platform_device *pdev = to_platform_device(dev);
+ u32 value;
+ int i;
+
+ dp->dp_iomem = devm_platform_ioremap_resource_byname(pdev, "dp");
+ if (IS_ERR(dp->dp_iomem))
+ return dev_err_probe(dev, PTR_ERR(dp->dp_iomem),
+ "failed to map dp registers\n");
+
+ dp->rcsu_iomem = devm_platform_ioremap_resource_byname(pdev, "rcsu");
+ if (IS_ERR(dp->rcsu_iomem))
+ return dev_err_probe(dev, PTR_ERR(dp->rcsu_iomem),
+ "failed to map rcsu registers\n");
+
+ dp->phy = devm_phy_get(dev, NULL);
+ if (IS_ERR(dp->phy))
+ return dev_err_probe(dev, PTR_ERR(dp->phy),
+ "failed to get dp phy\n");
+
+ if (device_property_read_u32(dev, "cix,adapter-port", &value))
+ return dev_err_probe(dev, -EINVAL,
+ "failed to read adapter port\n");
+ if (value < 1 || value > 5)
+ return dev_err_probe(dev, -EINVAL,
+ "invalid adapter port %u\n", value);
+ dp->cfg_adapter_port = value;
+
+ if (!device_property_read_u32(dev, "cix,dp-max-rate", &value)) {
+ if (value != CIX_DP_LINK_RATE_RBR &&
+ value != CIX_DP_LINK_RATE_HBR &&
+ value != CIX_DP_LINK_RATE_HBR2 &&
+ value != CIX_DP_LINK_RATE_HBR3)
+ return dev_err_probe(dev, -EINVAL,
+ "invalid link rate %u\n",
+ value);
+ dp->max_rate = value;
+ }
+
+ if (!device_property_read_u32(dev, "cix,stream-id", &value)) {
+ if (value >= dp->max_streams)
+ return dev_err_probe(dev, -EINVAL,
+ "invalid stream id %u\n", value);
+ dp->dp_panel.source_id = value;
+ }
+
+ dp->reset = devm_reset_control_get(dev, NULL);
+ if (IS_ERR(dp->reset))
+ return dev_err_probe(dev, PTR_ERR(dp->reset),
+ "failed to get reset\n");
+
+ dp->apb_clk = devm_clk_get(dev, "apb");
+ if (IS_ERR(dp->apb_clk))
+ return dev_err_probe(dev, PTR_ERR(dp->apb_clk),
+ "failed to get apb clock\n");
+
+ for (i = 0; i < dp->max_streams &&
+ i < ARRAY_SIZE(stream_clk_names); i++) {
+ dp->stream_clk[i] = devm_clk_get(dev, stream_clk_names[i]);
+ if (IS_ERR(dp->stream_clk[i]))
+ return dev_err_probe(dev, PTR_ERR(dp->stream_clk[i]),
+ "failed to get %s\n",
+ stream_clk_names[i]);
+ }
+
+ dp->irq = platform_get_irq(pdev, 0);
+ if (dp->irq < 0)
+ return dev_err_probe(dev, dp->irq, "failed to get irq\n");
+
+ return 0;
+}
+
+static const struct cix_dp_ops cix_dp_hw_ops = {
+ .detect = cix_dp_detect,
+ .encoder_enable = cix_dp_encoder_enable,
+ .mode_configure = cix_dp_mode_configure,
+ .aux_register = cix_dp_aux_register,
+ .aux_cleanup = cix_dp_aux_cleanup,
+ .replay_deferred_hotplug = cix_dp_replay_deferred_hotplug,
+};
+
+static int cix_dptx_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct cix_dp *dp;
+ int ret;
+
+ dp = cix_dp_bridge_alloc(dev);
+ if (IS_ERR(dp))
+ return PTR_ERR(dp);
+
+ dp->dev = dev;
+ dp->ops = &cix_dp_hw_ops;
+ dp->status = connector_status_unknown;
+ dp->force_hpd = connector_status_unknown;
+ dp->num_lanes = CIX_DPTX_MAX_LANES;
+ dp->max_rate = CIX_DP_LINK_RATE_HBR3;
+ dp->max_streams = DPTX_MST_SOURCE_NUMBER;
+ dp->state = DPTX_STATE_DISCONNECTED;
+ dp->pixel_per_cycle = 1;
+
+ ret = cix_dp_parse_dt(dp);
+ if (ret)
+ return ret;
+
+ dp->link_config.max_lanes = dp->num_lanes;
+ dp->link_config.max_rate = dp->max_rate;
+
+ ret = cix_dp_init(dp);
+ if (ret)
+ return ret;
+
+ dev_set_drvdata(dev, dp);
+
+ dp->bridge.of_node = dev->of_node;
+
+ drm_bridge_add(&dp->bridge);
+
+ return 0;
+}
+
+static void cix_dptx_remove(struct platform_device *pdev)
+{
+ struct cix_dp *dp = dev_get_drvdata(&pdev->dev);
+
+ if (!dp)
+ return;
+
+ drm_bridge_remove(&dp->bridge);
+ cix_dp_deinit(dp);
+}
+
+static int cix_dptx_pm_suspend(struct device *dev)
+{
+ struct cix_dp *dp = dev_get_drvdata(dev);
+
+ if (!dp)
+ return 0;
+
+ return cix_dp_pm_prepare(dp);
+}
+
+static int cix_dptx_pm_resume(struct device *dev)
+{
+ struct cix_dp *dp = dev_get_drvdata(dev);
+
+ if (!dp)
+ return 0;
+
+ return cix_dp_pm_complete(dp);
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(cix_dptx_pm_ops, cix_dptx_pm_suspend,
+ cix_dptx_pm_resume);
+
+static void cix_dptx_shutdown(struct platform_device *pdev)
+{
+ struct cix_dp *dp = platform_get_drvdata(pdev);
+
+ if (dp) {
+ mutex_lock(&dp->session_lock);
+ dp->active_stream_cnt = 0;
+ mutex_unlock(&dp->session_lock);
+ cix_dp_pm_prepare(dp);
+ }
+}
+
+static const struct of_device_id cix_dptx_dt_ids[] = {
+ { .compatible = "cix,sky1-dptx" },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, cix_dptx_dt_ids);
+
+static struct platform_driver cix_dp_driver = {
+ .probe = cix_dptx_probe,
+ .remove = cix_dptx_remove,
+ .shutdown = cix_dptx_shutdown,
+ .driver = {
+ .name = "cix-dptx",
+ .of_match_table = cix_dptx_dt_ids,
+ .pm = pm_sleep_ptr(&cix_dptx_pm_ops),
+ },
+};
+
+module_platform_driver(cix_dp_driver);
+
+MODULE_AUTHOR("Luo Liu <Luo.Liu@cixtech.com>");
+MODULE_AUTHOR("Wuyi Shen <wuyi.shen@cixtech.com>");
+MODULE_DESCRIPTION("Cix DisplayPort Controller Driver");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:cix-dptx");
diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx.h b/drivers/gpu/drm/cix/dptx/cix_dptx.h
new file mode 100644
index 000000000000..5767a48d849d
--- /dev/null
+++ b/drivers/gpu/drm/cix/dptx/cix_dptx.h
@@ -0,0 +1,370 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * CIX Sky1 DisplayPort transmitter driver
+ *
+ * Copyright (C) 2026 Trilinear Technologies
+ * Copyright 2026 Cix Technology Group Co., Ltd.
+ */
+
+#ifndef CIX_DPTX_H
+#define CIX_DPTX_H
+
+#include <linux/bits.h>
+#include <linux/phy/phy.h>
+
+#include <drm/display/drm_dp_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_connector.h>
+
+#define CIX_DP_MAX_RATE(rate, lanes, bpp) ((rate) * (lanes) * 8 / (bpp))
+
+#define bridge_to_dp(_bridge) \
+ container_of(_bridge, struct cix_dp, bridge)
+#define connector_to_cix(connector) \
+ container_of(connector, struct cix_connector, base)
+
+#define CIX_DP_MAX_FREQ 600000
+/*
+ * DP link rates in units of 10 kbit/s, i.e. the DPCD bandwidth code
+ * (0x06, 0x0a, 0x14, 0x1e) times 27000, as returned by
+ * drm_dp_bw_code_to_link_rate(). For these 8b/10b rates the value also
+ * equals the link symbol clock in kHz.
+ */
+#define CIX_DP_LINK_RATE_RBR 162000
+#define CIX_DP_LINK_RATE_HBR 270000
+#define CIX_DP_LINK_RATE_HBR2 540000
+#define CIX_DP_LINK_RATE_HBR3 810000
+#define DP_MAX_TRAINING_TRIES 5
+#define DP_MAX_TRAINING_LOOP 3
+
+#define CIX_VSC_SDP 1
+#define CIX_MAX_SDP 7
+
+#define CIX_DPTX_MAX_LANES 4
+
+#define DP_STREAM_MAX 4
+
+/* ------------------------------------------------------------------------ */
+/* Register offsets */
+/* ------------------------------------------------------------------------ */
+/* link configuration */
+#define CIX_DPTX_LINK_BW_SET 0x000ul
+#define CIX_DPTX_LANE_COUNT_SET 0x004ul
+#define CIX_DPTX_ENHANCED_FRAMING_ENABLE 0x008ul
+#define CIX_DPTX_TRAINING_PATTERN_SET 0x00cul
+#define CIX_DPTX_DISABLE_SCRAMBLING 0x014ul
+#define CIX_DPTX_DOWNSPREAD_CONTROL 0x018ul
+
+/* core enables */
+#define CIX_DPTX_TRANSMITTER_ENABLE 0x080ul
+#define CIX_DPTX_SOFT_RESET 0x090ul
+#define CIX_DPTX_SOURCE_ENABLE 0x094ul
+
+/* misc control */
+#define CIX_DPTX_FORCE_SCRAMBLER_RESET 0x0c0ul
+
+/* AUX channel */
+#define CIX_DPTX_AUX_COMMAND 0x100ul
+#define CIX_DPTX_AUX_WRITE_FIFO 0x104ul
+#define CIX_DPTX_AUX_ADDRESS 0x108ul
+#define CIX_DPTX_AUX_CLOCK_DIVIDER 0x10cul
+#define CIX_DPTX_HPD_INPUT_STATE 0x128ul
+#define CIX_DPTX_AUX_REPLY_DATA 0x134ul
+#define CIX_DPTX_AUX_REPLY_CODE 0x138ul
+#define CIX_DPTX_INTERRUPT_CAUSE 0x140ul
+#define CIX_DPTX_INTERRUPT_MASK 0x144ul
+#define CIX_DPTX_AUX_REPLY_DATA_COUNT 0x148ul
+#define CIX_DPTX_AUX_STATUS 0x14cul
+
+/* PHY status */
+#define CIX_DPTX_PHY_STATUS 0x280ul
+
+/* MST / SST control */
+#define CIX_DPTX_MST_ENABLE 0x500ul
+#define CIX_DPTX_SST_SOURCE_SELECT 0x50cul
+
+/* main stream control, virtual source 0 (bases for per-stream helpers) */
+#define CIX_DPTX_SRC0_VIDEO_STREAM_ENABLE 0x800ul
+#define CIX_DPTX_SRC0_SECONDARY_STREAM_ENABLE 0x804ul
+#define CIX_DPTX_SRC0_SECONDARY_DATA_WINDOW 0x808ul
+#define CIX_DPTX_SRC0_DATA_CONTROL 0x810ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_OVERRIDE 0x814ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_HTOTAL 0x820ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_VTOTAL 0x824ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_POLARITY 0x828ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_HSWIDTH 0x82cul
+#define CIX_DPTX_SRC0_MAIN_STREAM_VSWIDTH 0x830ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_HRES 0x834ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_VRES 0x838ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_HSTART 0x83cul
+#define CIX_DPTX_SRC0_MAIN_STREAM_VSTART 0x840ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_MISC0 0x844ul
+#define CIX_DPTX_SRC0_MAIN_STREAM_MISC1 0x848ul
+#define CIX_DPTX_SRC0_TU_CONFIG 0x850ul
+#define CIX_DPTX_SRC0_USER_PIXEL_COUNT 0x858ul
+#define CIX_DPTX_SRC0_USER_DATA_COUNT 0x85cul
+#define CIX_DPTX_SRC0_USER_SYNC_POLARITY 0x864ul
+
+/* secondary channel, source 0 */
+#define CIX_DPTX_SEC0_INFOFRAME_ENABLE 0x910ul
+#define CIX_DPTX_SEC0_INFOFRAME_RATE 0x914ul
+#define CIX_DPTX_SEC0_INFOFRAME_SELECT 0x934ul
+#define CIX_DPTX_SEC0_INFOFRAME_DATA 0x938ul
+
+/* spacing between consecutive virtual source register blocks */
+#define CIX_DPTX_SOURCE_OFFSET 0x200ul
+
+/* per-stream helpers, indexed by source/stream id */
+#define CIX_DPTX_INFOFRAME_ENABLE(stream_id) \
+ (CIX_DPTX_SEC0_INFOFRAME_ENABLE + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_INFOFRAME_SELECT(stream_id) \
+ (CIX_DPTX_SEC0_INFOFRAME_SELECT + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_INFOFRAME_RATE(stream_id) \
+ (CIX_DPTX_SEC0_INFOFRAME_RATE + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_INFOFRAME_DATA(stream_id) \
+ (CIX_DPTX_SEC0_INFOFRAME_DATA + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_VIDEO_STREAM_ENABLE(stream_id) \
+ (CIX_DPTX_SRC0_VIDEO_STREAM_ENABLE + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_SECONDARY_STREAM_ENABLE(stream_id) \
+ (CIX_DPTX_SRC0_SECONDARY_STREAM_ENABLE + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_MISC0(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_MISC0 + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_MISC1(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_MISC1 + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_OVERRIDE(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_OVERRIDE + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_HTOTAL(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_HTOTAL + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_VTOTAL(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_VTOTAL + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_POLARITY(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_POLARITY + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_HSWIDTH(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_HSWIDTH + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_VSWIDTH(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_VSWIDTH + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_HRES(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_HRES + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_VRES(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_VRES + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_HSTART(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_HSTART + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_MAIN_STREAM_VSTART(stream_id) \
+ (CIX_DPTX_SRC0_MAIN_STREAM_VSTART + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_TU_CONFIG(stream_id) \
+ (CIX_DPTX_SRC0_TU_CONFIG + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_USER_DATA_COUNT(stream_id) \
+ (CIX_DPTX_SRC0_USER_DATA_COUNT + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_SECONDARY_DATA_WINDOW(stream_id) \
+ (CIX_DPTX_SRC0_SECONDARY_DATA_WINDOW + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_DATA_CONTROL(stream_id) \
+ (CIX_DPTX_SRC0_DATA_CONTROL + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_USER_PIXEL_COUNT(stream_id) \
+ (CIX_DPTX_SRC0_USER_PIXEL_COUNT + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+#define CIX_DPTX_USER_SYNC_POLARITY(stream_id) \
+ (CIX_DPTX_SRC0_USER_SYNC_POLARITY + \
+ CIX_DPTX_SOURCE_OFFSET * (stream_id))
+
+/* ------------------------------------------------------------------------ */
+/* Register CIX_DPTX_AUX_COMMAND bits */
+/* ------------------------------------------------------------------------ */
+#define CIX_DPTX_AUX_COMMAND_CMD_SHIFT 8
+#define CIX_DPTX_AUX_COMMAND_ADDRESS_ONLY BIT(12)
+#define CIX_DPTX_AUX_COMMAND_BYTES_SHIFT 0
+#define CIX_DPTX_AUX_STATUS_REPLY_ERROR BIT(3)
+#define CIX_DPTX_AUX_STATUS_REQUEST_IN_PROGRESS BIT(2)
+#define CIX_DPTX_AUX_STATUS_REPLY_IN_PROGRESS BIT(1)
+#define CIX_DPTX_AUX_STATUS_REPLY_RECEIVED BIT(0)
+#define CIX_DPTX_AUX_REPLY_CODE_AUX_ACK (0)
+#define CIX_DPTX_AUX_REPLY_CODE_I2C_ACK (0)
+
+/* ------------------------------------------------------------------------ */
+/* Register CIX_DPTX_REPLY_DATA_COUNT bits */
+/* ------------------------------------------------------------------------ */
+#define CIX_DPTX_REPLY_DATA_COUNT_MASK 0xff
+
+/* ------------------------------------------------------------------------ */
+/* Register CIX_DPTX_MAIN_STREAM_MISC0 bits */
+/* ------------------------------------------------------------------------ */
+#define CIX_DPTX_STREAM_MISC0_SYNC_LOCK BIT(0)
+#define CIX_DPTX_STREAM_MISC0_COMP_FORMAT_RGB (0 << 1)
+#define CIX_DPTX_STREAM_MISC0_COMP_FORMAT_YCRCB_422 (5 << 1)
+#define CIX_DPTX_STREAM_MISC0_COMP_FORMAT_YCRCB_444 (6 << 1)
+#define CIX_DPTX_STREAM_MISC0_COMP_FORMAT_MASK (7 << 1)
+#define CIX_DPTX_STREAM_MISC0_BPC_6 (0 << 5)
+#define CIX_DPTX_STREAM_MISC0_BPC_8 BIT(5)
+#define CIX_DPTX_STREAM_MISC0_BPC_10 (2 << 5)
+#define CIX_DPTX_STREAM_MISC0_BPC_12 (3 << 5)
+#define CIX_DPTX_STREAM_MISC0_BPC_16 (4 << 5)
+#define CIX_DPTX_STREAM_MISC0_BPC_MASK (7 << 5)
+
+/* ------------------------------------------------------------------------ */
+/* Register CIX_DPTX_MAIN_STREAM_MISC1 bits */
+/* ------------------------------------------------------------------------ */
+#define CIX_DPTX_STREAM_MISC1_Y_ONLY_EN BIT(7)
+
+#define CIX_DPTX_VSC_COLORIMETRY_EN BIT(6)
+#define CIX_DPTX_STREAM_OVERRIDE_ENABLE BIT(6)
+#define CIX_DPTX_STREAM_OVERRIDE_YCbCR_420 (4 << 3)
+#define CIX_DPTX_STREAM_OVERRIDE_BPC_6 (0)
+#define CIX_DPTX_STREAM_OVERRIDE_BPC_8 (1)
+#define CIX_DPTX_STREAM_OVERRIDE_BPC_10 (2)
+#define CIX_DPTX_STREAM_OVERRIDE_BPC_12 (3)
+#define CIX_DPTX_STREAM_OVERRIDE_BPC_16 (4)
+
+#define CIX_DPTX_MSA_TRANSFER_UNIT_SIZE_TU_SIZE_DEF 64
+#define CIX_DPTX_MAIN_STREAM_POLARITY_HSYNC_SHIFT 0
+#define CIX_DPTX_MAIN_STREAM_POLARITY_VSYNC_SHIFT 1
+#define CIX_DPTX_MAIN_STREAM_POLARITY_DATAENABLE_HIGH 0xc
+
+#define CIX_DPTX_DATA_CONTROL_DEFAULT (BIT(13) | BIT(2))
+#define CIX_DPTX_DATA_CONTROL_2PPC_4LANE_HBR3 BIT(2)
+
+#define CIX_DPTX_INTERRUPT_GP_TIMER_IRQ BIT(4)
+#define CIX_DPTX_INTERRUPT_REPLY_TIMEOUT BIT(3)
+#define CIX_DPTX_INTERRUPT_REPLY_RECIEVED BIT(2)
+#define CIX_DPTX_INTERRUPT_HPD_IRQ BIT(1)
+#define CIX_DPTX_INTERRUPT_HPD_EVENT BIT(0)
+
+#define CIX_DPTX_INTERRUPT_MASK_ALL 0x3f
+#define CIX_DPTX_INTERRUPT_CFG 0x1c
+
+enum cix_dp_format {
+ CIX_DP_FORMAT_RGB,
+ CIX_DP_FORMAT_YCBCR444,
+ CIX_DP_FORMAT_YCBCR422,
+ CIX_DP_FORMAT_YCBCR420,
+ CIX_DP_FORMAT_YONLY,
+};
+
+enum cix_dptx_state {
+ DPTX_STATE_DISCONNECTED = 0,
+ DPTX_STATE_CONFIGURED = BIT(0),
+ DPTX_STATE_INITIALIZED = BIT(1),
+ DPTX_STATE_READY = BIT(2),
+ DPTX_STATE_CONNECTED = BIT(3),
+ DPTX_STATE_ENABLED = BIT(6),
+ DPTX_STATE_SUSPENDED = BIT(7),
+ DPTX_STATE_INIT_TRAIN = BIT(8),
+};
+
+struct cix_dp_link_config {
+ int max_rate;
+ int max_lanes;
+};
+
+struct cix_dp_mode {
+ u8 bw_code;
+ u8 lane_cnt;
+ int pclock;
+ int link_rate;
+};
+
+struct cix_dp_config {
+ u8 misc0;
+ u8 misc1;
+ int bpp;
+ int bpc;
+ u8 override;
+ enum cix_dp_format format;
+ u32 colorspace;
+ u8 dynamic_range;
+ u8 content_type;
+};
+
+/* Output configuration and SDP packets of the DisplayPort link. */
+struct cix_connector {
+ struct drm_connector base;
+ struct cix_dp *dp;
+ struct cix_dp_config config;
+ struct dp_sdp sdp[CIX_MAX_SDP];
+};
+
+struct cix_dp_link_caps {
+ bool enhanced_framing;
+ bool tps3_supported;
+ bool tps4_supported;
+ bool vsc_supported;
+};
+
+struct cix_dp_panel {
+ u32 source_id;
+ struct cix_connector *connector;
+};
+
+struct cix_dp;
+
+struct cix_dp_ops {
+ int (*detect)(struct cix_dp *dp);
+ int (*encoder_enable)(struct cix_dp *dp, int stream_id, int enable);
+ int (*mode_configure)(struct cix_dp *dp, int pclk, u8 bw, u8 bpp,
+ bool downshift);
+ int (*aux_register)(struct cix_dp *dp);
+ void (*aux_cleanup)(struct cix_dp *dp);
+ void (*replay_deferred_hotplug)(struct cix_dp *dp);
+};
+
+struct cix_dp {
+ struct drm_bridge bridge;
+ struct cix_connector connector;
+ struct device *dev;
+ struct drm_device *drm;
+ struct clk *apb_clk;
+ struct clk *stream_clk[DP_STREAM_MAX];
+ void __iomem *dp_iomem;
+ void __iomem *rcsu_iomem;
+ struct reset_control *reset;
+ struct drm_dp_aux aux;
+ bool aux_registered;
+ struct phy *phy;
+ int irq;
+ u32 max_rate;
+ u32 num_lanes;
+ u32 max_streams;
+ u32 aux_clock_divider;
+ u32 cfg_adapter_port;
+ u8 dpcd[DP_RECEIVER_CAP_SIZE];
+ u8 downstream_ports[DP_MAX_DOWNSTREAM_PORTS];
+ struct delayed_work hpd_event_work;
+ struct delayed_work hpd_irq_work;
+ struct delayed_work sw_hpd_irq_work;
+ enum drm_connector_status status;
+ enum drm_connector_status force_hpd;
+ struct cix_dp_link_config link_config;
+ struct cix_dp_mode mode;
+ struct cix_dp_link_caps caps;
+ enum cix_dptx_state state;
+ u32 pixel_per_cycle;
+ u8 train_set[CIX_DPTX_MAX_LANES];
+ u32 active_stream_cnt;
+ /* Serializes link training, HPD handling and stream enable. */
+ struct mutex session_lock;
+ struct cix_dp_panel dp_panel;
+ struct cix_dp_panel *active_panels[DP_STREAM_MAX];
+ const struct cix_dp_ops *ops;
+};
+
+#endif /* CIX_DPTX_H */
diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c
new file mode 100644
index 000000000000..0ddcd76691e0
--- /dev/null
+++ b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c
@@ -0,0 +1,653 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * CIX Sky1 DisplayPort DRM bridge helpers
+ *
+ * Copyright 2026 Cix Technology Group Co., Ltd.
+ */
+
+#include "cix_dptx_drm.h"
+
+#include <linux/device.h>
+#include <linux/kernel.h>
+
+#include <drm/drm_atomic.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_bridge.h>
+#include <drm/drm_edid.h>
+#include <drm/drm_probe_helper.h>
+
+#include "cix_dptx.h"
+
+#define CIX_DPTX_MIN_H_BACKPORCH 12
+#define CIX_DP_MIN_BPP (6 * 3)
+
+#define connector_to_dp(connector) (connector_to_cix(connector)->dp)
+
+struct fmt_prio_entry {
+ int format;
+ int bpc;
+};
+
+/* SST: RGB -> YUV422 10 -> YUV422 8 -> RGB 6 -> YUV420 8 */
+static const struct fmt_prio_entry fmt_prio[] = {
+ { DRM_OUTPUT_COLOR_FORMAT_RGB444, 0 }, /* use suggest_bpc (8 or 10) */
+ { DRM_OUTPUT_COLOR_FORMAT_YCBCR422, 10 }, /* YUV422 10bit second */
+ { DRM_OUTPUT_COLOR_FORMAT_YCBCR422, 8 }, /* YUV422 8bit third */
+ { DRM_OUTPUT_COLOR_FORMAT_RGB444, 6 }, /* RGB 6bit fourth */
+ { DRM_OUTPUT_COLOR_FORMAT_YCBCR420, 8 }, /* YUV420 8bit last */
+};
+
+static const int dp_common_link_rates[] = {
+ CIX_DP_LINK_RATE_RBR,
+ CIX_DP_LINK_RATE_HBR,
+ CIX_DP_LINK_RATE_HBR2,
+ CIX_DP_LINK_RATE_HBR3
+};
+
+static enum drm_connector_status
+cix_dp_connector_detect(struct drm_connector *connector, bool force)
+{
+ struct cix_dp *dp = connector_to_dp(connector);
+ enum drm_connector_status status;
+
+ if (dp->ops->detect(dp))
+ status = connector_status_disconnected;
+ else
+ status = connector_status_connected;
+
+ dp->status = status;
+ drm_dp_set_subconnector_property(connector, status, dp->dpcd,
+ dp->downstream_ports);
+
+ return status;
+}
+
+static void cix_dp_oob_hotplug_event(struct drm_connector *connector,
+ enum drm_connector_status hpd_state)
+{
+ struct cix_dp *dp = connector_to_dp(connector);
+
+ if (dp->state & DPTX_STATE_SUSPENDED) {
+ dp->force_hpd = connector_status_disconnected;
+ return;
+ }
+
+ if (dp->force_hpd != hpd_state) {
+ dp->force_hpd = hpd_state;
+ schedule_delayed_work(&dp->hpd_event_work, 0);
+ }
+
+ if (hpd_state == connector_status_connected)
+ schedule_delayed_work(&dp->sw_hpd_irq_work, 1);
+}
+
+static int cix_dp_connector_get_modes(struct drm_connector *connector)
+{
+ struct cix_dp *dp = connector_to_dp(connector);
+ const struct drm_edid *edid;
+ struct drm_display_mode *mode;
+ int ret;
+
+ edid = drm_edid_read_ddc(connector, &dp->aux.ddc);
+ if (!edid) {
+ drm_edid_connector_update(connector, NULL);
+
+ mode = drm_dp_downstream_mode(connector->dev, dp->dpcd,
+ dp->downstream_ports);
+ if (!mode)
+ return 0;
+
+ drm_mode_probed_add(connector, mode);
+ return 1;
+ }
+
+ drm_edid_connector_update(connector, edid);
+ ret = drm_edid_connector_add_modes(connector);
+ drm_edid_free(edid);
+
+ return ret;
+}
+
+static struct drm_encoder *
+cix_dp_connector_best_encoder(struct drm_connector *connector)
+{
+ struct cix_dp *dp = connector_to_dp(connector);
+
+ return dp->bridge.encoder;
+}
+
+static u32 cix_dp_pick_ppc(const struct drm_display_mode *mode,
+ enum cix_dp_format format)
+{
+ if (mode->clock > CIX_DP_MAX_FREQ &&
+ mode->clock < CIX_DP_MAX_FREQ * 2 &&
+ format != CIX_DP_FORMAT_YCBCR420)
+ return 2;
+
+ return 1;
+}
+
+static enum drm_mode_status
+cix_dp_connector_mode_valid(struct drm_connector *connector,
+ const struct drm_display_mode *mode)
+{
+ const struct drm_display_info *info = &connector->display_info;
+ struct cix_dp *dp = connector_to_dp(connector);
+ int max_lanes = dp->link_config.max_lanes;
+ int max_rate = dp->link_config.max_rate;
+ int clock = mode->clock;
+ u32 pixel_per_cycle = cix_dp_pick_ppc(mode, CIX_DP_FORMAT_RGB);
+ int minbpp = CIX_DP_MIN_BPP;
+
+ if (connector->ycbcr_420_allowed && drm_mode_is_420_only(info, mode)) {
+ minbpp = 8 * 3 / 2;
+ pixel_per_cycle = 1;
+ }
+
+ if (pixel_per_cycle == 1 && clock > CIX_DP_MAX_FREQ) {
+ drm_mode_debug_printmodeline(mode);
+ return MODE_CLOCK_HIGH;
+ }
+
+ if (clock > CIX_DP_MAX_RATE(max_rate, max_lanes, minbpp)) {
+ drm_mode_debug_printmodeline(mode);
+ return MODE_CLOCK_HIGH;
+ }
+
+ return MODE_OK;
+}
+
+static int cix_dp_cal_bpc(struct cix_dp *dp,
+ struct drm_connector_state *connector_state, int bpc)
+{
+ struct drm_display_info *info =
+ &connector_state->connector->display_info;
+
+ if (connector_state->colorspace >= DRM_MODE_COLORIMETRY_BT2020_CYCC &&
+ connector_state->colorspace <= DRM_MODE_COLORIMETRY_BT2020_YCC)
+ bpc = 10;
+
+ if (info->bpc && bpc > info->bpc)
+ bpc = info->bpc;
+
+ if (connector_state->max_requested_bpc &&
+ bpc > connector_state->max_requested_bpc)
+ bpc = connector_state->max_requested_bpc;
+
+ return bpc;
+}
+
+static int cix_dp_cal_bpp(struct cix_dp *dp, int bpc, int format)
+{
+ int bpp = bpc * 3;
+
+ switch (format) {
+ case DRM_OUTPUT_COLOR_FORMAT_RGB444:
+ case DRM_OUTPUT_COLOR_FORMAT_YCBCR444:
+ bpp = bpc * 3;
+ break;
+ case DRM_OUTPUT_COLOR_FORMAT_YCBCR422:
+ bpp = bpc * 2;
+ break;
+ case DRM_OUTPUT_COLOR_FORMAT_YCBCR420:
+ bpp = bpc * 3 / 2;
+ break;
+ default:
+ dev_warn(dp->dev, "invalid color format %d\n", format);
+ }
+ return bpp;
+}
+
+static void cix_dp_adjust_mode(struct drm_display_mode *mode,
+ struct drm_display_mode *adjusted_mode)
+{
+ int diff = mode->htotal - mode->hsync_end;
+
+ /*
+ * Cix DP requires horizontal backporch to be greater than 12.
+ * This limitation may not be compatible with the sink device.
+ *
+ * Everything below is derived from @mode instead of accumulating on
+ * @adjusted_mode, so that repeated atomic checks of the same state
+ * produce the same result.
+ */
+ if (diff < CIX_DPTX_MIN_H_BACKPORCH) {
+ adjusted_mode->htotal = mode->htotal +
+ CIX_DPTX_MIN_H_BACKPORCH - diff;
+ adjusted_mode->clock = (int64_t)mode->clock *
+ adjusted_mode->htotal / mode->htotal;
+ }
+
+ /* The sync polarities are inverted with respect to the source mode. */
+ if (mode->flags & DRM_MODE_FLAG_PVSYNC)
+ adjusted_mode->flags &= ~DRM_MODE_FLAG_PVSYNC;
+ else
+ adjusted_mode->flags |= DRM_MODE_FLAG_PVSYNC;
+
+ if (mode->flags & DRM_MODE_FLAG_PHSYNC)
+ adjusted_mode->flags &= ~DRM_MODE_FLAG_PHSYNC;
+ else
+ adjusted_mode->flags |= DRM_MODE_FLAG_PHSYNC;
+}
+
+static int compute_available_clock_rate(struct cix_dp *dp,
+ struct drm_connector_state *conn_state,
+ int suggest_bpc, int clock,
+ int color_format, int *rt_bpc,
+ int *rt_bpp)
+{
+ int max_lanes = dp->link_config.max_lanes;
+ int max_rate = dp->link_config.max_rate;
+ int rate;
+ int bpc;
+ int bpp;
+
+ bpc = cix_dp_cal_bpc(dp, conn_state, suggest_bpc);
+ bpp = cix_dp_cal_bpp(dp, bpc, color_format);
+ rate = CIX_DP_MAX_RATE(max_rate, max_lanes, bpp);
+
+ /* clock is adjusted_mode->clock, also in kHz */
+ if (clock <= rate) {
+ *rt_bpc = bpc;
+ *rt_bpp = bpp;
+
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+static int cix_dp_pick_format_bpc(struct cix_dp *dp,
+ struct drm_connector_state *conn_state,
+ struct drm_display_mode *adjusted_mode,
+ int info_formats, int suggest_bpc,
+ int *color_format, int *bpc, int *bpp)
+{
+ struct drm_display_info *info =
+ &conn_state->connector->display_info;
+ int curr_bpc;
+ int format;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(fmt_prio); i++) {
+ format = fmt_prio[i].format;
+ curr_bpc = fmt_prio[i].bpc ? fmt_prio[i].bpc : suggest_bpc;
+
+ /* Only use 420_only flag for YUV420 format */
+ if (!(info_formats & BIT(format)) &&
+ !(format == DRM_OUTPUT_COLOR_FORMAT_YCBCR420 &&
+ drm_mode_is_420_only(info, adjusted_mode)))
+ continue;
+
+ /*
+ * First entry (RGB): prefer 10bit before falling back to
+ * suggest_bpc, and step down to 8bit before giving up on RGB.
+ * The DPU always outputs RGB, so a lower RGB depth is still
+ * preferable to a converted YUV format.
+ */
+ if (i == 0) {
+ if (suggest_bpc > 10 &&
+ !compute_available_clock_rate(dp, conn_state, 10,
+ adjusted_mode->clock,
+ format, bpc, bpp)) {
+ if (color_format)
+ *color_format = format;
+ return 0;
+ }
+
+ if (!compute_available_clock_rate(dp, conn_state,
+ suggest_bpc,
+ adjusted_mode->clock,
+ format, bpc, bpp)) {
+ if (color_format)
+ *color_format = format;
+ return 0;
+ }
+
+ if (suggest_bpc > 8 &&
+ !compute_available_clock_rate(dp, conn_state, 8,
+ adjusted_mode->clock,
+ format, bpc, bpp)) {
+ if (color_format)
+ *color_format = format;
+ return 0;
+ }
+
+ continue;
+ }
+
+ if (!compute_available_clock_rate(dp, conn_state, curr_bpc,
+ adjusted_mode->clock,
+ format, bpc, bpp)) {
+ if (color_format)
+ *color_format = format;
+ return 0;
+ }
+ }
+
+ return -EINVAL;
+}
+
+static enum cix_dp_format cix_dp_drm_format_to_dp(int color_format)
+{
+ switch (color_format) {
+ case DRM_OUTPUT_COLOR_FORMAT_YCBCR444:
+ return CIX_DP_FORMAT_YCBCR444;
+ case DRM_OUTPUT_COLOR_FORMAT_YCBCR422:
+ return CIX_DP_FORMAT_YCBCR422;
+ case DRM_OUTPUT_COLOR_FORMAT_YCBCR420:
+ return CIX_DP_FORMAT_YCBCR420;
+ default:
+ return CIX_DP_FORMAT_RGB;
+ }
+}
+
+static int cix_dp_pick_link_rate(struct cix_dp *dp,
+ struct drm_display_mode *adjusted_mode,
+ int bpp)
+{
+ int max_rate = dp->link_config.max_rate;
+ int max_lanes = dp->link_config.max_lanes;
+ int link_rate = max_rate;
+ int rate;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(dp_common_link_rates); i++) {
+ if (dp_common_link_rates[i] > max_rate)
+ break;
+ rate = CIX_DP_MAX_RATE(dp_common_link_rates[i], max_lanes, bpp);
+ if (adjusted_mode->clock <= rate) {
+ link_rate = dp_common_link_rates[i];
+ break;
+ }
+ }
+
+ return link_rate;
+}
+
+static int cix_dp_pick_config(struct cix_dp *dp,
+ struct drm_connector_state *conn_state,
+ struct drm_display_mode *adjusted_mode,
+ int *color_format, int *bpc, int *bpp)
+{
+ struct drm_display_info *info = &conn_state->connector->display_info;
+ u32 color_formats = info->color_formats;
+
+ /*
+ * RGB444 is mandatory for DP sources and some sinks do not advertise
+ * it in their EDID, so always keep it in the candidate set.
+ */
+ color_formats |= BIT(DRM_OUTPUT_COLOR_FORMAT_RGB444);
+
+ return cix_dp_pick_format_bpc(dp, conn_state, adjusted_mode,
+ color_formats, max(info->bpc, 6),
+ color_format, bpc, bpp);
+}
+
+static void cix_dp_bridge_mode_set(struct cix_dp *dp,
+ struct drm_crtc_state *crtc_state,
+ struct drm_connector_state *conn_state)
+{
+ struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
+ struct cix_connector *cix_conn = &dp->connector;
+ int color_format;
+ int link_rate;
+ int bw_code;
+ int bpc;
+ int bpp;
+ int ret;
+
+ ret = cix_dp_pick_config(dp, conn_state, adjusted_mode,
+ &color_format, &bpc, &bpp);
+ if (ret) {
+ dev_err(dp->dev, "no usable DP link configuration: %d\n", ret);
+ return;
+ }
+
+ cix_conn->config.format = cix_dp_drm_format_to_dp(color_format);
+ cix_conn->config.colorspace = conn_state->colorspace;
+ cix_conn->config.bpc = bpc;
+ cix_conn->config.bpp = bpp;
+ dp->pixel_per_cycle = cix_dp_pick_ppc(adjusted_mode,
+ cix_conn->config.format);
+
+ link_rate = cix_dp_pick_link_rate(dp, adjusted_mode, bpp);
+ bw_code = drm_dp_link_rate_to_bw_code(link_rate);
+
+ dp->ops->mode_configure(dp, adjusted_mode->clock, bw_code, bpp, false);
+
+ dev_dbg(dp->dev,
+ "%dx%d@%d fmt=%d bpc=%d bpp=%d link=%u lanes=%u ppc=%u\n",
+ adjusted_mode->hdisplay, adjusted_mode->vdisplay,
+ drm_mode_vrefresh(adjusted_mode), cix_conn->config.format,
+ bpc, bpp, bw_code, dp->mode.lane_cnt,
+ dp->pixel_per_cycle);
+}
+
+static int cix_dp_bridge_atomic_check(struct drm_bridge *bridge,
+ struct drm_bridge_state *bridge_state,
+ struct drm_crtc_state *crtc_state,
+ struct drm_connector_state *conn_state)
+{
+ struct cix_dp *dp = bridge_to_dp(bridge);
+ int bpc;
+ int bpp;
+ int ret;
+
+ cix_dp_adjust_mode(&crtc_state->mode, &crtc_state->adjusted_mode);
+
+ /*
+ * The DPU always outputs RGB, so only validate that the mode fits the
+ * link. The colour format used to drive the sink is picked again when
+ * the bridge is enabled.
+ */
+ ret = cix_dp_pick_config(dp, conn_state, &crtc_state->adjusted_mode,
+ NULL, &bpc, &bpp);
+ if (ret)
+ return ret;
+
+ /*
+ * The colour space is programmed through a VSC SDP, so a change of
+ * the requested colour space requires a full modeset.
+ */
+ if (dp->caps.vsc_supported) {
+ const struct drm_connector_state *old_conn_state;
+
+ old_conn_state =
+ drm_atomic_get_old_connector_state(bridge_state->base.state,
+ conn_state->connector);
+ if (old_conn_state &&
+ old_conn_state->colorspace != conn_state->colorspace)
+ crtc_state->mode_changed = true;
+ }
+
+ return 0;
+}
+
+static void
+cix_dp_bridge_atomic_enable(struct drm_bridge *bridge,
+ struct drm_atomic_commit *state)
+{
+ struct cix_dp *dp = bridge_to_dp(bridge);
+ struct drm_connector_state *conn_state;
+ struct drm_crtc_state *crtc_state;
+ struct drm_connector *connector;
+ int ret;
+
+ connector = drm_atomic_get_new_connector_for_encoder(state,
+ bridge->encoder);
+ if (!connector)
+ return;
+
+ conn_state = drm_atomic_get_new_connector_state(state, connector);
+ if (!conn_state || !conn_state->crtc)
+ return;
+
+ crtc_state = drm_atomic_get_new_crtc_state(state, conn_state->crtc);
+ if (!crtc_state)
+ return;
+
+ cix_dp_bridge_mode_set(dp, crtc_state, conn_state);
+
+ ret = dp->ops->encoder_enable(dp, dp->dp_panel.source_id, true);
+ if (ret)
+ dev_err(dp->dev, "failed to enable DP output: %d\n", ret);
+}
+
+static void
+cix_dp_bridge_atomic_disable(struct drm_bridge *bridge,
+ struct drm_atomic_commit *state)
+{
+ struct cix_dp *dp = bridge_to_dp(bridge);
+
+ dp->ops->encoder_enable(dp, dp->dp_panel.source_id, false);
+}
+
+static int cix_dp_connector_atomic_check(struct drm_connector *conn,
+ struct drm_atomic_commit *state)
+{
+ struct drm_connector_state *new_con_state =
+ drm_atomic_get_new_connector_state(state, conn);
+
+ if (!new_con_state)
+ return 0;
+
+ /* PSR is not implemented, so never advertise self refresh support. */
+ new_con_state->self_refresh_aware = false;
+
+ return 0;
+}
+
+static const struct drm_connector_funcs cix_dp_connector_funcs = {
+ .detect = cix_dp_connector_detect,
+ .fill_modes = drm_helper_probe_single_connector_modes,
+ .destroy = drm_connector_cleanup,
+ .reset = drm_atomic_helper_connector_reset,
+ .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+ .atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
+ .oob_hotplug_event = cix_dp_oob_hotplug_event,
+};
+
+static const struct drm_connector_helper_funcs
+cix_dp_connector_helper_funcs = {
+ .get_modes = cix_dp_connector_get_modes,
+ .best_encoder = cix_dp_connector_best_encoder,
+ .mode_valid = cix_dp_connector_mode_valid,
+ .atomic_check = cix_dp_connector_atomic_check,
+};
+
+static void cix_dp_add_properties(struct drm_connector *connector)
+{
+ if (!drm_mode_create_dp_colorspace_property(connector, 0))
+ drm_connector_attach_colorspace_property(connector);
+
+ drm_connector_attach_max_bpc_property(connector, 8, 10);
+ drm_connector_attach_dp_subconnector_property(connector);
+ drm_connector_attach_content_type_property(connector);
+}
+
+static int cix_dp_connector_create(struct cix_dp *dp,
+ struct drm_encoder *encoder)
+{
+ struct cix_connector *conn = &dp->connector;
+ struct drm_connector *connector = &conn->base;
+ int ret;
+
+ conn->dp = dp;
+
+ connector->polled = DRM_CONNECTOR_POLL_HPD;
+ connector->ycbcr_420_allowed = true;
+
+ ret = drm_connector_init_with_ddc(encoder->dev, connector,
+ &cix_dp_connector_funcs,
+ DRM_MODE_CONNECTOR_DisplayPort,
+ &dp->aux.ddc);
+ if (ret)
+ return ret;
+
+ drm_connector_helper_add(connector, &cix_dp_connector_helper_funcs);
+
+ drm_atomic_helper_connector_reset(connector);
+
+ ret = drm_connector_attach_encoder(connector, encoder);
+ if (ret)
+ goto err_cleanup;
+
+ cix_dp_add_properties(connector);
+
+ dp->dp_panel.connector = conn;
+
+ return 0;
+
+err_cleanup:
+ drm_connector_cleanup(connector);
+ return ret;
+}
+
+static int cix_dp_bridge_attach(struct drm_bridge *bridge,
+ struct drm_encoder *encoder,
+ enum drm_bridge_attach_flags flags)
+{
+ struct cix_dp *dp = bridge_to_dp(bridge);
+ int ret;
+
+ dp->drm = bridge->dev;
+
+ ret = dp->ops->aux_register(dp);
+ if (ret)
+ goto err_clear_drm;
+
+ if (flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR)
+ return 0;
+
+ ret = cix_dp_connector_create(dp, encoder);
+ if (ret)
+ goto err_aux;
+
+ /* The bridge may have been attached long after the sink was plugged. */
+ dp->ops->replay_deferred_hotplug(dp);
+
+ return 0;
+
+err_aux:
+ dp->ops->aux_cleanup(dp);
+err_clear_drm:
+ dp->drm = NULL;
+ return ret;
+}
+
+static void cix_dp_bridge_detach(struct drm_bridge *bridge)
+{
+ struct cix_dp *dp = bridge_to_dp(bridge);
+
+ /* The hotplug workers reach the connector, stop them first. */
+ cancel_delayed_work_sync(&dp->hpd_event_work);
+ cancel_delayed_work_sync(&dp->hpd_irq_work);
+ cancel_delayed_work_sync(&dp->sw_hpd_irq_work);
+
+ if (dp->connector.base.dev)
+ drm_connector_cleanup(&dp->connector.base);
+
+ dp->dp_panel.connector = NULL;
+
+ dp->ops->aux_cleanup(dp);
+
+ dp->drm = NULL;
+}
+
+static const struct drm_bridge_funcs cix_dp_bridge_funcs = {
+ .attach = cix_dp_bridge_attach,
+ .detach = cix_dp_bridge_detach,
+ .atomic_check = cix_dp_bridge_atomic_check,
+ .atomic_enable = cix_dp_bridge_atomic_enable,
+ .atomic_disable = cix_dp_bridge_atomic_disable,
+ .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,
+};
+
+struct cix_dp *cix_dp_bridge_alloc(struct device *dev)
+{
+ return devm_drm_bridge_alloc(dev, struct cix_dp, bridge,
+ &cix_dp_bridge_funcs);
+}
diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx_drm.h b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.h
new file mode 100644
index 000000000000..3ac57faa349e
--- /dev/null
+++ b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * CIX Sky1 DisplayPort DRM helpers
+ *
+ * Copyright 2026 Cix Technology Group Co., Ltd.
+ */
+
+#ifndef CIX_DPTX_DRM_H
+#define CIX_DPTX_DRM_H
+
+struct cix_dp;
+struct device;
+
+struct cix_dp *cix_dp_bridge_alloc(struct device *dev);
+
+#endif /* CIX_DPTX_DRM_H */
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 1/2] dt-bindings: display: add CIX Sky1 DisplayPort transmitter
2026-11-09 10:30 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
@ 2026-11-09 10:30 ` Luo Liu
2026-11-09 10:42 ` sashiko-bot
0 siblings, 1 reply; 7+ messages in thread
From: Luo Liu @ 2026-11-09 10:30 UTC (permalink / raw)
To: maarten.lankhorst, mripard, tzimmermann, robh, krzk+dt, conor+dt
Cc: dri-devel, devicetree, Luo.Liu, wuyi.shen, cix-kernel-upstream
Document the CIX Sky1 DisplayPort transmitter. Each instance sits
between one of the SoC display pipelines and a DisplayPort PHY, and
exposes the DP register block together with the RCSU block used for the
link setup.
The controller is described as a DRM bridge whose video input is linked
through the OF graph, and the DisplayPort PHY is referenced through the
generic PHY framework. The RCSU adapter port, the video stream and the
maximum link rate are described by "cix,adapter-port", "cix,stream-id"
and "cix,dp-max-rate".
Signed-off-by: Luo Liu <Luo.Liu@cixtech.com>
---
.../bindings/display/cix,sky1-dptx.yaml | 108 ++++++++++++++++++
MAINTAINERS | 8 ++
2 files changed, 116 insertions(+)
create mode 100644 Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
diff --git a/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
new file mode 100644
index 000000000000..9087b5edd9f0
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
@@ -0,0 +1,108 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/display/cix,sky1-dptx.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: CIX Sky1 Trilinear DisplayPort Transmitter
+
+maintainers:
+ - Luo Liu <Luo.Liu@cixtech.com>
+
+description:
+ DisplayPort transmitter controller based on the Trilinear DP IP on CIX Sky1.
+
+properties:
+ compatible:
+ const: cix,sky1-dptx
+
+ reg:
+ items:
+ - description: DisplayPort transmitter registers.
+ - description: RCSU video adapter registers used for link setup.
+
+ reg-names:
+ items:
+ - const: dp
+ - const: rcsu
+
+ interrupts:
+ maxItems: 1
+
+ clocks:
+ items:
+ - description: APB clock of the controller.
+ - description: Video clock of stream 0.
+ - description: Video clock of stream 1.
+
+ clock-names:
+ items:
+ - const: apb
+ - const: vid0
+ - const: vid1
+
+ resets:
+ maxItems: 1
+ description: DisplayPort controller reset.
+
+ phys:
+ maxItems: 1
+ description: DisplayPort PHY the controller transmits through.
+
+ port:
+ $ref: /schemas/graph.yaml#/properties/port
+ description:
+ Video input from the display pipeline that drives this controller.
+
+ cix,adapter-port:
+ description:
+ RCSU video adapter port of this transmitter. Sky1 numbers its five
+ transmitters from 1 to 5. Port 3 is the native DP/eDP output.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 1
+ maximum: 5
+
+ cix,dp-max-rate:
+ description:
+ Maximum link rate the board is wired for, in units of 10 kbit/s.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [162000, 270000, 540000, 810000]
+ default: 810000
+
+ cix,stream-id:
+ description:
+ Index of the video stream this controller uses. Sky1 has two streams,
+ numbered 0 and 1.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0
+ maximum: 1
+ default: 0
+
+required:
+ - compatible
+ - reg
+ - reg-names
+ - interrupts
+ - clocks
+ - clock-names
+ - resets
+ - cix,adapter-port
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+ dp0: dp@14064000 {
+ compatible = "cix,sky1-dptx";
+ reg = <0x14064000 0x4000>,
+ <0x14050000 0x400>;
+ reg-names = "dp", "rcsu";
+ interrupts = <GIC_SPI 332 IRQ_TYPE_LEVEL_HIGH>;
+ clocks = <&clk 0>, <&clk 1>, <&clk 2>;
+ clock-names = "apb", "vid0", "vid1";
+ resets = <&rst 0>;
+ cix,adapter-port = <1>;
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 93abbb315065..7d5283d83022 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6522,6 +6522,14 @@ M: Satish Kharat <satishkh@cisco.com>
S: Supported
F: drivers/infiniband/hw/usnic/
+CIX SKY1 DISPLAYPORT TRANSMITTER DRIVER
+M: Luo Liu <Luo.Liu@cixtech.com>
+M: Wuyi Shen <wuyi.shen@cixtech.com>
+R: CIX Linux Kernel Upstream Group <cix-kernel-upstream@cixtech.com>
+L: dri-devel@lists.freedesktop.org
+S: Maintained
+F: Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
+
CLANG CONTEXT ANALYSIS
M: Marco Elver <elver@google.com>
R: Bart Van Assche <bvanassche@acm.org>
--
2.25.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH 1/2] dt-bindings: display: add CIX Sky1 DisplayPort transmitter
2026-11-09 10:30 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
@ 2026-11-09 10:42 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09 10:42 UTC (permalink / raw)
To: Luo Liu; +Cc: dri-devel, devicetree, robh, conor+dt
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [Medium] dt-bindings: display: cix,sky1-dptx: missing output port and
required phys
- [Low] dt-bindings: display: cix,sky1-dptx: non-standard
cix,dp-max-rate property
- [Low] dt-bindings: display: cix,sky1-dptx: instance index properties
in binding
- [Low] dt-bindings: display: cix,sky1-dptx: unused dp0 label in DTS
example
--
commit 8c60d6c22ca4c6d5157c09f9c5eab3f993001d1b
Author: Luo Liu <Luo.Liu@cixtech.com>
dt-bindings: display: add CIX Sky1 DisplayPort transmitter
This commit adds the Devicetree binding schema and MAINTAINERS entry for
the CIX Sky1 DisplayPort transmitter controller.
> diff --git a/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
> new file mode 100644
> index 0000000000000..9087b5edd9f02
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
> @@ -0,0 +1,108 @@
[ ... ]
> + phys:
> + maxItems: 1
> + description: DisplayPort PHY the controller transmits through.
> +
> + port:
> + $ref: /schemas/graph.yaml#/properties/port
> + description:
> + Video input from the display pipeline that drives this controller.
[Severity: Medium]
Should this binding define ports with both an input port from the display
pipeline and an output port to the downstream DisplayPort connector, eDP
panel, or Type-C bridge, and mark phys and ports as required?
With only a single input port and additionalProperties: false, board DTS
files that connect the transmitter output to a connector or panel via
ports will fail dtbs_check validation. In addition, cix_dp_parse_dt()
unconditionally calls devm_phy_get(dev, NULL) and fails probe if phys is
absent, yet phys and the OF graph port are omitted from both required and
the example.
> +
> + cix,adapter-port:
> + description:
> + RCSU video adapter port of this transmitter. Sky1 numbers its five
> + transmitters from 1 to 5. Port 3 is the native DP/eDP output.
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 1
> + maximum: 5
> +
> + cix,dp-max-rate:
> + description:
> + Maximum link rate the board is wired for, in units of 10 kbit/s.
> + $ref: /schemas/types.yaml#/definitions/uint32
> + enum: [162000, 270000, 540000, 810000]
> + default: 810000
[Severity: Low]
Could this use the standard link-frequencies property from
video-interfaces.yaml on the output endpoint instead of a custom
cix,dp-max-rate property?
The values 162000, 270000, 540000, and 810000 in 10 kbit/s units come
from Linux DRM's internal drm_dp_bw_code_to_link_rate() representation and
lack a standard unit suffix, whereas DisplayPort bindings normally specify
board-supported link rates in Hz via link-frequencies on the output
endpoint.
> +
> + cix,stream-id:
> + description:
> + Index of the video stream this controller uses. Sky1 has two streams,
> + numbered 0 and 1.
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 1
> + default: 0
[Severity: Low]
Should cix,adapter-port and cix,stream-id be avoided in favor of
describing hardware differences via compatible strings or OF graph
endpoints?
In cix_dp_cfg_adapter(), each instance already maps its own rcsu region
from reg, and cix,adapter-port is only checked against port 3 because the
native DP/eDP port uses a different RCSU register layout (offset 0x320)
than the USB-C DP ports (offsets 0x300 and 0x304). Similarly,
cix,stream-id encodes the display pipeline stream index as a custom vendor
property rather than representing the stream routing through OF graph
ports or endpoints.
[ ... ]
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/arm-gic.h>
> +
> + dp0: dp@14064000 {
[Severity: Low]
Can the unreferenced dp0 label be dropped from the example? It is never
referenced within the example and triggers an unused-labels warning from
scripts/dtc/dt-check-style in strict mode.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109103010.110911-1-Luo.Liu@cixtech.com?part=1
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH 1/2] dt-bindings: display: add CIX Sky1 DisplayPort transmitter
2026-11-09 9:26 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
@ 2026-11-09 20:04 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09 20:04 UTC (permalink / raw)
To: Luo Liu, dri-devel, devicetree; +Cc: robh, conor+dt
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] dt-bindings: display: custom vendor properties duplicate
standard DT bindings
- [Medium] dt-bindings: display: incomplete OF graph ports and missing
required phys/port
--
commit 7968e7b18915fdc94006fbc915ec20e0e3171502
Author: Luo Liu <Luo.Liu@cixtech.com>
dt-bindings: display: add CIX Sky1 DisplayPort transmitter
This commit adds the Device Tree binding schema for the CIX Sky1 Trilinear
DisplayPort transmitter controller and adds a corresponding MAINTAINERS entry.
> diff --git a/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
> new file mode 100644
> index 0000000000000..9087b5edd9f02
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/display/cix,sky1-dptx.yaml
> @@ -0,0 +1,108 @@
[ ... ]
> + phys:
> + maxItems: 1
> + description: DisplayPort PHY the controller transmits through.
> +
> + port:
> + $ref: /schemas/graph.yaml#/properties/port
> + description:
> + Video input from the display pipeline that drives this controller.
> +
> + cix,adapter-port:
> + description:
> + RCSU video adapter port of this transmitter. Sky1 numbers its five
> + transmitters from 1 to 5. Port 3 is the native DP/eDP output.
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 1
> + maximum: 5
> +
> + cix,dp-max-rate:
> + description:
> + Maximum link rate the board is wired for, in units of 10 kbit/s.
> + $ref: /schemas/types.yaml#/definitions/uint32
> + enum: [162000, 270000, 540000, 810000]
> + default: 810000
> +
> + cix,stream-id:
> + description:
> + Index of the video stream this controller uses. Sky1 has two streams,
> + numbered 0 and 1.
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0
> + maximum: 1
> + default: 0
[Severity: Medium]
Can standard Devicetree mechanisms replace cix,adapter-port, cix,dp-max-rate,
and cix,stream-id instead of introducing custom vendor properties?
cix,adapter-port encodes a 1-to-5 instance index where port 3 is the native
DP/eDP output with a different RCSU register offset and bit layout from the
USB-C DP transmitters; per writing-bindings.rst, differing programming models
should be distinguished by compatible strings rather than an instance index
property.
cix,dp-max-rate exposes Linux DRM internal software units of 10 kbit/s
without a standard unit suffix, duplicating the standard link-frequencies
property (in Hz) from video-interfaces.yaml on the output graph endpoint.
cix,stream-id hardcodes the input video stream index as a vendor property
instead of modeling the display pipeline to transmitter routing through OF
graph ports and endpoints.
> +
> +required:
> + - compatible
> + - reg
> + - reg-names
> + - interrupts
> + - clocks
> + - clock-names
> + - resets
> + - cix,adapter-port
> +
> +additionalProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/arm-gic.h>
> +
> + dp0: dp@14064000 {
> + compatible = "cix,sky1-dptx";
> + reg = <0x14064000 0x4000>,
> + <0x14050000 0x400>;
> + reg-names = "dp", "rcsu";
> + interrupts = <GIC_SPI 332 IRQ_TYPE_LEVEL_HIGH>;
> + clocks = <&clk 0>, <&clk 1>, <&clk 2>;
> + clock-names = "apb", "vid0", "vid1";
> + resets = <&rst 0>;
> + cix,adapter-port = <1>;
> + };
[Severity: Medium]
Should the binding define an output graph port and list phys and the OF graph
ports as required?
The schema only defines a single input port from the display pipeline and
omits the output port connecting this bridge to the downstream sink (such as
a dp-connector, usb-c-connector, or eDP panel), which prevents describing
board-level endpoint properties such as data-lanes and link-frequencies.
In addition, each transmitter sits between a display pipeline and a
DisplayPort PHY (and the driver unconditionally requires phys during probe),
so phys and the OF graph ports should be listed under required and included in
the example. The unreferenced dp0 label in the example should also be
dropped per writing-schema.rst.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109092623.102291-1-Luo.Liu@cixtech.com?part=1
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver
2026-11-09 9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
@ 2026-11-09 20:30 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09 20:30 UTC (permalink / raw)
To: Luo Liu, dri-devel, devicetree; +Cc: robh, conor+dt
Thank you for your contribution! Sashiko AI review found 16 potential
issues to consider:
- [High] drm/cix: unread sink DPCD caps and status race on boot, resume,
and HPD
- [High] drm/cix: AUX probe in cix_dp_replay_deferred_hotplug() always
fails with -ENODEV
- [High] drm/cix: broken VSC SDP packing, stream override, and YCbCr420
validation
- [High] drm/cix: unclocked MMIO register accesses when apb_clk is gated
- [High] drm/cix: NULL pointer dereference when
DRM_BRIDGE_ATTACH_NO_CONNECTOR is set
- [High] drm/cix: broken YCbCr 4:2:0 mode matching, format selection,
and VSC SDP
- [Medium] drm/cix: PHY refcount leak and state desync when
cix_dp_enable() or resume fails
- [Medium] drm/cix: HPD workqueue and IRQ teardown races in deinit,
detach, and suspend
- [Medium] drm/cix: active streams and PHY power left enabled in
shutdown and remove
- [Medium] drm/cix: display stays off if cix_dp_prepare() runs before
cix_dp_pm_complete()
- [Medium] drm/cix: missing stream TU/timing update and unchecked mode
config on retrain
- [Medium] drm/cix: broken BT.2020 bpc fallback in cix_dp_cal_bpc() and
6 bpc default
- [Medium] drm/cix: unlocked mode state update in
cix_dp_bridge_mode_set() before prepare
- [Medium] drm/cix: FIFO underflow read on AUX NACK/DEFER in
cix_dp_aux_cmd_submit()
- [Medium] drm/cix: plain 64-bit division in cix_dp_adjust_mode() breaks
32-bit builds
- [Low] drm/cix: cix_dp_aux_cmd_submit() reads reply FIFO on AUX/I2C
NACK or DEFER
--
commit 002cf156569e15175af86684eb3509dc08da82b1
Author: Luo Liu <Luo.Liu@cixtech.com>
drm/cix: add Sky1 DisplayPort transmitter driver
This commit adds a DRM bridge and connector driver for the CIX Sky1
DisplayPort transmitter, driving the DisplayPort PHY via the generic PHY
API and supporting link training, EDID probing, YCbCr 4:2:0, and hotplug.
> diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx.c b/drivers/gpu/drm/cix/dptx/cix_dptx.c
> new file mode 100644
> index 0000000000000..8915c50bc9db3
> --- /dev/null
> +++ b/drivers/gpu/drm/cix/dptx/cix_dptx.c
[ ... ]
> +static int cix_dp_train_loop(struct cix_dp *dp)
> +{
> + struct cix_dp_mode *mode = &dp->mode;
> + u8 bpp = dp->connector.config.bpp;
> + u8 bw_code = mode->bw_code;
> + int i;
> + int j;
> + int ret;
> +
> + for (i = 0; i < DP_MAX_TRAINING_LOOP; i++) {
> + for (j = 0; j <= 3; j++) {
> + if (!(dp->state & DPTX_STATE_READY))
> + return 0;
> +
> + ret = cix_dp_train(dp);
> + if (!ret)
> + return 0;
> +
> + ret = cix_dp_mode_configure(dp, mode->pclock, bw_code,
> + bpp, true);
> + if (ret < 0)
> + break;
> +
> + bw_code = dp->mode.bw_code;
> + }
> +
> + cix_dp_mode_configure(dp, mode->pclock, 0, bpp, true);
> + bw_code = dp->mode.bw_code;
> + }
> +
> + return -EIO;
> +}
[Severity: Medium]
When cix_dp_hpd_irq_work_func() calls cix_dp_train_loop() while a stream is
enabled and link training downshifts dp->mode.bw_code or changes
dp->mode.lane_cnt, does anything reprogram the active stream's transfer unit
and timing registers? cix_dp_mode_set_transfer_unit() and
cix_dp_mode_set_timing() depend on bw_code, link_rate, and lane_cnt, so
leaving CIX_DPTX_TU_CONFIG, CIX_DPTX_USER_DATA_COUNT,
CIX_DPTX_SECONDARY_DATA_WINDOW, and CIX_DPTX_DATA_CONTROL at their old values
corrupts the running video stream. Also, the fallback
cix_dp_mode_configure() call at the end of the outer loop ignores its return
value.
> +
> +static int cix_dp_aux_cmd_submit(struct cix_dp *dp, u32 cmd, u32 addr,
> + u8 *buf, u32 bytes, u8 *reply)
> +{
[ ... ]
> + value = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_CODE);
> + if (reply)
> + *reply = value;
> +
> + if (is_read) {
> + if (value == CIX_DPTX_AUX_REPLY_CODE_AUX_ACK ||
> + value == CIX_DPTX_AUX_REPLY_CODE_I2C_ACK) {
> + value = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_DATA_COUNT);
> + if ((value & CIX_DPTX_REPLY_DATA_COUNT_MASK) != bytes)
> + return -EIO;
> + }
> + for (i = 0; i < bytes; i++)
> + buf[i] = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_DATA);
> + }
> +
> + return 0;
> +}
[Severity: Low]
Should the CIX_DPTX_AUX_REPLY_DATA read loop be inside the ACK check? When a
sink responds to a read with NACK or DEFER, the loop still runs and overwrites
buf with stale or empty FIFO reads before drm_dp_aux retries.
> +
> +static ssize_t cix_dp_aux_transfer(struct drm_dp_aux *aux,
> + struct drm_dp_aux_msg *msg)
> +{
> + struct cix_dp *dp = container_of(aux, struct cix_dp, aux);
> + size_t to_access = 0;
> + size_t accessed_bytes = 0;
> + int ret;
> +
> + if (!cix_dp_get_hpd_state(dp))
> + return -ENODEV;
> +
> + do {
> + to_access = min_t(size_t, DP_AUX_MAX_PAYLOAD_BYTES,
> + msg->size - accessed_bytes);
> + ret = cix_dp_aux_cmd_submit(dp, msg->request,
> + msg->address + accessed_bytes,
> + msg->buffer + accessed_bytes,
> + to_access, &msg->reply);
> + if (ret)
> + return accessed_bytes ? (ssize_t)accessed_bytes : ret;
> +
> + accessed_bytes += to_access;
> + } while (accessed_bytes < msg->size);
> +
> + return msg->size;
> +}
[Severity: Medium]
In addition to cix_dp_aux_cmd_submit() reading from the empty
CIX_DPTX_AUX_REPLY_DATA FIFO when the sink replies with NACK or DEFER, does
cix_dp_aux_transfer() keep submitting later 16-byte chunks after a non-ACK
reply? cix_dp_aux_cmd_submit() returns 0 on NACK and DEFER, so a multi-chunk
transfer continues looping and overwrites msg->reply instead of stopping and
returning to the caller.
[ ... ]
> +static int cix_dp_set_misc(struct cix_dp *dp,
> + struct cix_dp_panel *dp_panel)
> +{
> + struct cix_dp_config *config;
> +
> + if (!dp_panel || !dp_panel->connector)
> + return -EINVAL;
> +
> + config = &dp_panel->connector->config;
> +
> + config->misc0 &= ~CIX_DPTX_STREAM_MISC0_SYNC_LOCK;
> + config->misc1 &= ~CIX_DPTX_VSC_COLORIMETRY_EN;
> + config->override = 0;
> +
> + if (dp->caps.vsc_supported &&
> + cix_dp_needs_vsc_sdp(config->format, config->colorspace)) {
> + config->misc1 |= CIX_DPTX_VSC_COLORIMETRY_EN;
> +
> + switch (config->bpc) {
> + case 6:
> + config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_6;
> + break;
[ ... ]
> + default:
> + config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_8;
> + break;
> + }
> +
> + if (config->format == CIX_DP_FORMAT_YCBCR420)
> + config->override |= CIX_DPTX_STREAM_OVERRIDE_YCbCR_420;
> +
> + config->override |= CIX_DPTX_STREAM_OVERRIDE_ENABLE;
> + config->dynamic_range = DP_DYNAMIC_RANGE_CTA;
> + config->content_type = DP_CONTENT_TYPE_NOT_DEFINED;
> +
> + goto end;
> + }
[ ... ]
> +static int dptx_setup_vsc_sdp(struct dp_sdp *sdp,
> + struct cix_dp_config *config)
> +{
> + sdp->sdp_header.HB0 = 0;
> + sdp->sdp_header.HB1 = 0x7;
> + sdp->sdp_header.HB2 = 0x5;
> + sdp->sdp_header.HB3 = 0x13;
> +
> + switch (config->format) {
> + case CIX_DP_FORMAT_YCBCR444:
> + sdp->db[16] = 0x1 << 4;
> + break;
> + case CIX_DP_FORMAT_YCBCR422:
> + sdp->db[16] = 0x1 << 4;
> + break;
> + case CIX_DP_FORMAT_YCBCR420:
> + sdp->db[16] = 0x3 << 4;
> + break;
> + default:
> + break;
> + }
[Severity: High]
Does dptx_setup_vsc_sdp() program the wrong VSC SDP payload for YCbCr 4:2:2,
RGB, and 6 bpc?
CIX_DP_FORMAT_YCBCR422 sets sdp->db[16] to 0x1 << 4 (YCbCr 4:4:4) instead of
0x2 << 4 (YCbCr 4:2:2). In the default (RGB) branch, sdp->db[16] is not
cleared before the colorspace switch ORs in the colorimetry bits, leaving
stale format and colorimetry bits from earlier modesets in
conn->sdp[CIX_VSC_SDP]. Further down in dptx_setup_vsc_sdp(), switch
(config->bpc) has no case 6, so 6 bpc RGB falls into default and encodes
0x1 (8 bpc) instead of 0x0 in sdp->db[17].
In cix_dp_set_misc() above, the VSC path only sets the pixel format bits in
config->override for CIX_DP_FORMAT_YCBCR420 (leaving YCbCr 4:2:2 and 4:4:4
as 0/RGB in the override register) and overwrites config->content_type with
DP_CONTENT_TYPE_NOT_DEFINED instead of using conn_state->content_type. Also,
cix_dp_pick_format_bpc() and cix_dp_connector_mode_valid() select
DRM_OUTPUT_COLOR_FORMAT_YCBCR420 without checking dp->caps.vsc_supported;
when vsc_supported is false, cix_dp_set_misc() falls through and returns
-EINVAL (which cix_dp_panel_hw_cfg() ignores), leaving MISC0, MISC1,
OVERRIDE, and the VSC SDP unconfigured while the RCSU adapter is set to
YCbCr 4:2:0.
[ ... ]
> +static int cix_dp_panel_setup_colorimetry_sdp(struct cix_dp *dp,
> + struct cix_dp_panel *dp_panel)
> +{
> + struct cix_connector *conn = dp_panel->connector;
> +
> + if (conn->config.override & CIX_DPTX_STREAM_OVERRIDE_ENABLE) {
> + dptx_setup_vsc_sdp(&conn->sdp[CIX_VSC_SDP], &conn->config);
> + infoframe_write_packet(dp, dp_panel->source_id, CIX_VSC_SDP,
> + &conn->sdp[CIX_VSC_SDP], 1);
> + }
> +
> + return 0;
> +}
[ ... ]
> +static int cix_dp_panel_hw_cfg(struct cix_dp *dp, int stream_id)
> +{
> + struct cix_dp_panel *dp_panel = dp->active_panels[stream_id];
> + struct drm_display_mode *mode = cix_dp_get_mode(dp, stream_id);
> + struct cix_connector *conn;
> +
> + if (!mode)
> + return -EINVAL;
> +
> + conn = dp_panel->connector;
> +
> + cix_dp_cfg_adapter(dp, stream_id);
> +
> + cix_dp_set_misc(dp, dp_panel);
> + cix_dp_panel_setup_colorimetry_sdp(dp, dp_panel);
> +
> + cix_dp_mode_set_transfer_unit(dp, dp_panel->source_id, mode,
> + conn->config.bpp);
> + cix_dp_mode_set_timing(dp, dp_panel->source_id, mode, conn->config.bpp);
> + return 0;
> +}
[Severity: High]
Does enabling the bridge when attached with DRM_BRIDGE_ATTACH_NO_CONNECTOR
cause a NULL pointer dereference on dp_panel->connector?
When DRM_BRIDGE_ATTACH_NO_CONNECTOR is passed to cix_dp_bridge_attach(), it
returns 0 without calling cix_dp_connector_create(), leaving
dp->dp_panel.connector NULL. On modeset enable, cix_dp_panel_hw_cfg()
ignores the -EINVAL return from cix_dp_set_misc() and calls
cix_dp_panel_setup_colorimetry_sdp(), which dereferences
conn->config.override, followed by cix_dp_panel_hw_cfg() dereferencing
conn->config.bpp. In addition, cix_dp_hpd_event_work_func() exits early when
!dp->dp_panel.connector and cix_dp_bridge_funcs does not implement the bridge
detect, EDID, or HPD callbacks needed without an internal connector.
[ ... ]
> +/*
> + * Pure probe: report the current HPD level without touching the session state
> + * or the hardware. This runs on every DRM probe, poll and hotplug, so
> + * DPTX_STATE_CONNECTED is advanced from the HPD event work and the encoder
> + * prepare path instead.
> + *
> + * The host is initialized by probe, resume and cix_dp_prepare(). Bail out
> + * loudly if we are ever called before that: cix_dp_read() is a plain readl()
> + * and would hang on a gated APB clock.
> + */
> +static int cix_dp_detect(struct cix_dp *dp)
> +{
> + int ret = 0;
> +
> + mutex_lock(&dp->session_lock);
> +
> + if (dp->state & DPTX_STATE_SUSPENDED) {
> + ret = -EBUSY;
> + } else if (!(dp->state & DPTX_STATE_INITIALIZED)) {
> + dev_warn_once(dp->dev, "detect with uninitialized host\n");
> + ret = -ENODEV;
> + }
> +
> + mutex_unlock(&dp->session_lock);
> +
> + if (ret)
> + return ret;
> +
> + return cix_dp_get_hpd_state(dp) ? 0 : -ENODEV;
> +}
[Severity: High]
Can MMIO registers be read while dp->apb_clk is gated, hanging the bus?
As noted in the comment above cix_dp_detect(), reading DPTX registers while
the host is uninitialized and apb_clk is gated hangs the bus:
- cix_dp_aux_transfer() calls cix_dp_get_hpd_state() and
cix_dp_aux_cmd_submit() without acquiring dp->session_lock or checking
DPTX_STATE_INITIALIZED / DPTX_STATE_SUSPENDED, so /dev/drm_dp_aux* or I2C
transfers during suspend read unclocked MMIO registers.
- cix_dp_hpd_irq_work_func() and cix_dp_hpd_event_work_func() call
cix_dp_get_hpd_state() before acquiring dp->session_lock and checking
DPTX_STATE_INITIALIZED.
- cix_dp_detect() drops dp->session_lock before calling
cix_dp_get_hpd_state(), and cix_dp_prepare() drops and re-acquires
dp->session_lock around cix_dp_handle_connect() without rechecking
DPTX_STATE_INITIALIZED, allowing a concurrent cix_dp_pm_prepare() to gate
apb_clk in between.
[ ... ]
> +static void cix_dp_replay_deferred_hotplug(struct cix_dp *dp)
> +{
> + struct phy *phy = dp->phy;
> + union phy_configure_opts opts = { 0 };
> + u8 dpcd_rev;
> +
> + if (!(dp->state & DPTX_STATE_INITIALIZED))
> + return;
> + if (dp->force_hpd != connector_status_unknown)
> + return;
> + if (cix_dp_get_hpd_state(dp))
> + return;
> +
> + phy_validate(phy, PHY_MODE_DP, 0, &opts);
> + if (phy_get_mode(phy) != PHY_MODE_DP)
> + return;
> +
> + /*
> + * The combo PHY is already muxed to DP, so the sink may have been
> + * plugged in before the connector existed. Probe the AUX channel to
> + * make sure one is really there before forcing a hotplug event.
> + */
> + if (drm_dp_dpcd_readb(&dp->aux, DP_DPCD_REV, &dpcd_rev) < 0)
> + return;
> +
> + dp->force_hpd = connector_status_connected;
> + dp->status = connector_status_unknown;
> + schedule_delayed_work(&dp->hpd_event_work, 0);
> +}
[Severity: High]
Won't drm_dp_dpcd_readb() in cix_dp_replay_deferred_hotplug() always fail
with -ENODEV?
cix_dp_replay_deferred_hotplug() only reaches drm_dp_dpcd_readb() when
cix_dp_get_hpd_state(dp) is false, and it does not set
dp->force_hpd = connector_status_connected until after the read succeeds.
However, drm_dp_dpcd_readb() calls cix_dp_aux_transfer(), which immediately
checks if (!cix_dp_get_hpd_state(dp)) and returns -ENODEV, so a USB-C display
plugged in before bridge attach is never detected.
> +
> +static void cix_dp_hpd_event_work_func(struct work_struct *work)
> +{
> + struct cix_dp *dp = container_of(work, struct cix_dp,
> + hpd_event_work.work);
> + struct phy *phy = dp->phy;
> + union phy_configure_opts opts = { 0 };
> + enum drm_connector_status old_status;
> + enum phy_mode mode;
> + int i;
> +
> + if (!dp->dp_panel.connector)
> + return;
> +
> + /*
> + * On a USB-C DP alt-mode PHY the hotplug only becomes valid once the
> + * PHY has been switched to DP mode, so wait for the mode change.
> + * A native DP PHY has no ->set_mode() operation, which leaves the
> + * mode at PHY_MODE_INVALID forever; there is nothing to wait for.
> + */
> + if (cix_dp_get_hpd_state(dp) &&
> + phy_get_mode(phy) != PHY_MODE_INVALID) {
> + for (i = 0; i < DP_HPD_MAX_TRIES; i++) {
> + phy_validate(phy, PHY_MODE_DP, 0, &opts);
> + mode = phy_get_mode(phy);
> + if (mode == PHY_MODE_DP)
> + break;
> + msleep(100);
> + }
> + }
> +
> + old_status = dp->status;
> + drm_helper_probe_detect(&dp->connector.base, NULL, false);
> +
> + if (dp->status == connector_status_connected)
> + cix_dp_handle_connect(dp);
> + else
> + cix_dp_handle_disconnect(dp);
> +
> + if (old_status != dp->status && dp->drm)
> + drm_helper_hpd_irq_event(dp->drm);
> +}
[Severity: High]
Can sink DPCD capabilities remain unread across boot, resume, and hotplug,
and can concurrent connector probes suppress hotplug events?
If a native DP/eDP sink is already connected at boot, the HPD interrupt fires
in cix_dp_init() before cix_dp_bridge_attach() sets dp->dp_panel.connector,
so cix_dp_hpd_event_work_func() returns early; later,
cix_dp_replay_deferred_hotplug() also returns early because
cix_dp_get_hpd_state(dp) is true. Likewise, cix_dp_pm_prepare() clears
DPTX_STATE_CONNECTED on suspend without cix_dp_pm_complete() re-probing DPCD
on resume. Because cix_dp_connector_detect() only checks HPD level without
calling cix_dp_handle_connect(), dp->dpcd, dp->downstream_ports, dp->caps,
and dp->link_config remain unpopulated during mode validation, atomic_check,
and cix_dp_bridge_mode_set() (which runs before encoder_enable() ->
cix_dp_prepare() calls cix_dp_handle_connect()).
Even when cix_dp_hpd_event_work_func() runs, it calls
drm_helper_probe_detect() before cix_dp_handle_connect(), so
cix_dp_connector_detect() passes stale dp->dpcd and dp->downstream_ports to
drm_dp_set_subconnector_property(), and any concurrent DRM connector probe
updates dp->status before old_status = dp->status is read:
CPU0 (kworker) CPU1 (userspace probe)
cix_dp_hpd_event_work_func()
msleep(100) waiting for PHY
cix_dp_connector_detect()
dp->status = connected
old_status = dp->status (connected)
drm_helper_probe_detect()
if (old_status != dp->status) <-- false, HPD event dropped
Finally, cix_dp_handle_connect() only clears DPTX_STATE_CONNECTED when
cix_dp_panel_read_sink_caps() returns -ETIMEDOUT or -ENOTCONN; if it fails
with -EIO, -EPROTO, -EBUSY, or -ENODEV, DPTX_STATE_CONNECTED stays set and
prevents later cix_dp_handle_connect() calls from reading the DPCD.
[ ... ]
> +static int cix_dp_prepare(struct cix_dp *dp)
> +{
> + int ret;
> +
> + mutex_lock(&dp->session_lock);
> +
> + if (dp->state & (DPTX_STATE_ENABLED)) {
> + ret = 0;
> + goto end;
> + }
> +
> + if (!(dp->state & DPTX_STATE_INITIALIZED)) {
> + ret = cix_dp_host_init(dp);
> + if (ret)
> + goto end;
> + } else if (dp->state & DPTX_STATE_INIT_TRAIN) {
> + reset_dp_and_reinit(dp);
> + usleep_range(100, 200);
> + }
> +
> + if (cix_dp_get_hpd_state(dp) &&
> + !(dp->state & DPTX_STATE_CONNECTED)) {
> + mutex_unlock(&dp->session_lock);
> + cix_dp_handle_connect(dp);
> + mutex_lock(&dp->session_lock);
> + }
> +
> + if (!(cix_dp_get_hpd_state(dp) && (dp->state & DPTX_STATE_CONNECTED))) {
> + ret = 0;
> + goto end;
> + }
> +
> + ret = cix_dp_core_on(dp, true);
> +
> +end:
> + mutex_unlock(&dp->session_lock);
> + return ret;
> +}
[Severity: Medium]
If the display controller resumes and calls cix_dp_bridge_atomic_enable() ->
cix_dp_prepare() before cix_dptx_pm_resume() -> cix_dp_pm_complete() clears
DPTX_STATE_SUSPENDED, will the display stay blank?
cix_dp_prepare() handles !(dp->state & DPTX_STATE_INITIALIZED) by calling
cix_dp_host_init(), and then calls cix_dp_handle_connect() to restore
DPTX_STATE_CONNECTED (which cix_dp_pm_prepare() cleared). However,
cix_dp_handle_connect() bails out immediately when DPTX_STATE_SUSPENDED is
still set, leaving DPTX_STATE_CONNECTED clear so cix_dp_prepare() skips
cix_dp_core_on() and cix_dp_enable() skips enabling the stream.
[ ... ]
> +static int cix_dp_encoder_enable(struct cix_dp *dp, int stream_id, int enable)
> +{
> + int ret;
> +
> + if (enable) {
> + ret = cix_dp_prepare(dp);
> + if (ret)
> + return ret;
> +
> + ret = cix_dp_enable(dp, stream_id);
> + } else {
> + if (!(dp->state & DPTX_STATE_INITIALIZED))
> + return 0;
> +
> + ret = cix_dp_disable(dp, stream_id);
> + if (ret)
> + return ret;
> +
> + ret = cix_dp_unprepare(dp);
> + }
> +
> + return ret;
> +}
[Severity: Medium]
Does a failure in cix_dp_enable() leak PHY init/power references and leave
dp->state inconsistent?
cix_dp_prepare() -> cix_dp_core_on() calls phy_init() and phy_power_on() and
sets DPTX_STATE_READY | DPTX_STATE_INIT_TRAIN. If cix_dp_enable() then fails,
DPTX_STATE_ENABLED is not set, cix_dp_encoder_enable() does not roll back via
cix_dp_core_off(), and a later disable also skips cix_dp_core_off() because
cix_dp_unprepare() returns early when !(dp->state & DPTX_STATE_ENABLED). On
the next cix_dp_prepare() call, the else if (dp->state &
DPTX_STATE_INIT_TRAIN) branch calls reset_dp_and_reinit(), which resets the
controller and calls phy_init() again without a matching phy_exit(), while
cix_dp_core_on() sees DPTX_STATE_READY still set and returns 0 without
retraining the link.
Similarly, in cix_dp_pm_complete(), if cix_dp_host_init() fails on resume,
enable_irq(dp->irq) is skipped and DPTX_STATE_SUSPENDED stays set, while the
next cix_dp_pm_prepare() or cix_dp_deinit() calls disable_irq(dp->irq)
unconditionally.
[ ... ]
> +static void cix_dp_deinit(struct cix_dp *dp)
> +{
> + cancel_delayed_work_sync(&dp->sw_hpd_irq_work);
> + cancel_delayed_work_sync(&dp->hpd_irq_work);
> + cancel_delayed_work_sync(&dp->hpd_event_work);
> + disable_irq(dp->irq);
> +
> + mutex_lock(&dp->session_lock);
> + cix_dp_host_deinit(dp);
> + dp->state &= ~(DPTX_STATE_CONFIGURED);
> + mutex_unlock(&dp->session_lock);
> +
> + mutex_destroy(&dp->session_lock);
> +}
[Severity: Medium]
Can HPD delayed works be re-queued while cix_dp_deinit(),
cix_dp_bridge_detach(), or cix_dp_pm_prepare() is tearing down the driver?
In cix_dp_deinit(), cancel_delayed_work_sync() runs before
disable_irq(dp->irq):
CPU0 CPU1 (irq/kworker)
cix_dp_deinit()
cancel_delayed_work_sync(...)
cix_dp_irq_handler()
queue_delayed_work(...,
&dp->hpd_event_work, 0)
disable_irq(dp->irq)
cix_dp_host_deinit()
mutex_destroy(&dp->session_lock)
(devm frees dp)
cix_dp_hpd_event_work_func()
<-- use-after-free / gated APB read
Two related races exist in the other teardown paths:
- cix_dp_bridge_detach() cancels hpd_irq_work before sw_hpd_irq_work (even
though cix_dp_sw_hpd_irq_work_func() queues hpd_irq_work) and leaves
dp->irq enabled and dp->dp_panel.connector non-NULL while calling
drm_connector_cleanup(&dp->connector.base).
- cix_dp_pm_prepare() cancels the delayed works before setting
DPTX_STATE_SUSPENDED, while cix_dp_oob_hotplug_event() checks
DPTX_STATE_SUSPENDED unlocked and can schedule hpd_event_work and
sw_hpd_irq_work after the cancel calls.
[ ... ]
> +static void cix_dptx_remove(struct platform_device *pdev)
> +{
> + struct cix_dp *dp = dev_get_drvdata(&pdev->dev);
> +
> + if (!dp)
> + return;
> +
> + drm_bridge_remove(&dp->bridge);
> + cix_dp_deinit(dp);
> +}
[ ... ]
> +static void cix_dptx_shutdown(struct platform_device *pdev)
> +{
> + struct cix_dp *dp = platform_get_drvdata(pdev);
> +
> + if (dp) {
> + mutex_lock(&dp->session_lock);
> + dp->active_stream_cnt = 0;
> + mutex_unlock(&dp->session_lock);
> + cix_dp_pm_prepare(dp);
> + }
> +}
[Severity: Medium]
What happens to active streams and PHY power if cix_dptx_shutdown() or
cix_dptx_remove() runs while a display stream is enabled?
In cix_dptx_shutdown(), zeroing dp->active_stream_cnt without calling
cix_dp_ctrl_stream_off() or cix_dp_core_off() causes cix_dp_pm_prepare() ->
cix_dp_host_deinit() to call phy_exit(dp->phy) while the PHY is still powered
on (and with the second phy_init() reference from cix_dp_core_on() and
stream_clk still active). In cix_dptx_remove(), dp->active_stream_cnt is not
cleared and streams are not stopped, so cix_dp_host_deinit() returns early at
if (dp->active_stream_cnt) and leaves the transmitter, clocks, and PHY
enabled.
> diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c
> new file mode 100644
> index 0000000000000..0ddcd76691e03
> --- /dev/null
> +++ b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c
[ ... ]
> +static int cix_dp_cal_bpc(struct cix_dp *dp,
> + struct drm_connector_state *connector_state, int bpc)
> +{
> + struct drm_display_info *info =
> + &connector_state->connector->display_info;
> +
> + if (connector_state->colorspace >= DRM_MODE_COLORIMETRY_BT2020_CYCC &&
> + connector_state->colorspace <= DRM_MODE_COLORIMETRY_BT2020_YCC)
> + bpc = 10;
> +
> + if (info->bpc && bpc > info->bpc)
> + bpc = info->bpc;
> +
> + if (connector_state->max_requested_bpc &&
> + bpc > connector_state->max_requested_bpc)
> + bpc = connector_state->max_requested_bpc;
> +
> + return bpc;
> +}
[Severity: Medium]
When a BT.2020 colorspace is set, doesn't overwriting bpc = 10 in
cix_dp_cal_bpc() prevent cix_dp_pick_format_bpc() from stepping down to 8 or
6 bpc when link bandwidth cannot fit 10 bpc? Each fallback call to
compute_available_clock_rate() with curr_bpc = 8 or 6 gets forced back to 10.
Also, in cix_dp_pick_config(), passing max(info->bpc, 6) as suggest_bpc
selects 6 bpc instead of 8 bpc whenever an EDID leaves info->bpc as 0.
[ ... ]
> +static void cix_dp_adjust_mode(struct drm_display_mode *mode,
> + struct drm_display_mode *adjusted_mode)
> +{
> + int diff = mode->htotal - mode->hsync_end;
> +
> + /*
> + * Cix DP requires horizontal backporch to be greater than 12.
> + * This limitation may not be compatible with the sink device.
> + *
> + * Everything below is derived from @mode instead of accumulating on
> + * @adjusted_mode, so that repeated atomic checks of the same state
> + * produce the same result.
> + */
> + if (diff < CIX_DPTX_MIN_H_BACKPORCH) {
> + adjusted_mode->htotal = mode->htotal +
> + CIX_DPTX_MIN_H_BACKPORCH - diff;
> + adjusted_mode->clock = (int64_t)mode->clock *
> + adjusted_mode->htotal / mode->htotal;
> + }
[Severity: Medium]
Will the plain 64-bit division in (int64_t)mode->clock *
adjusted_mode->htotal / mode->htotal fail to link with an undefined reference
to __divdi3 / __aeabi_ldivmod on 32-bit architectures? DRM_CIX_DP does not
depend on 64BIT, so this needs div_s64() or mul_u32_u32() with div_u64().
> +
> + /* The sync polarities are inverted with respect to the source mode. */
> + if (mode->flags & DRM_MODE_FLAG_PVSYNC)
> + adjusted_mode->flags &= ~DRM_MODE_FLAG_PVSYNC;
> + else
> + adjusted_mode->flags |= DRM_MODE_FLAG_PVSYNC;
> +
> + if (mode->flags & DRM_MODE_FLAG_PHSYNC)
> + adjusted_mode->flags &= ~DRM_MODE_FLAG_PHSYNC;
> + else
> + adjusted_mode->flags |= DRM_MODE_FLAG_PHSYNC;
> +}
[ ... ]
> +static int cix_dp_pick_format_bpc(struct cix_dp *dp,
> + struct drm_connector_state *conn_state,
> + struct drm_display_mode *adjusted_mode,
> + int info_formats, int suggest_bpc,
> + int *color_format, int *bpc, int *bpp)
> +{
> + struct drm_display_info *info =
> + &conn_state->connector->display_info;
> + int curr_bpc;
> + int format;
> + int i;
> +
> + for (i = 0; i < ARRAY_SIZE(fmt_prio); i++) {
> + format = fmt_prio[i].format;
> + curr_bpc = fmt_prio[i].bpc ? fmt_prio[i].bpc : suggest_bpc;
> +
> + /* Only use 420_only flag for YUV420 format */
> + if (!(info_formats & BIT(format)) &&
> + !(format == DRM_OUTPUT_COLOR_FORMAT_YCBCR420 &&\n+ drm_mode_is_420_only(info, adjusted_mode)))
> + continue;
[Severity: High]
Does inverting the sync polarity flags on adjusted_mode in
cix_dp_adjust_mode() break drm_mode_is_420_only(info, adjusted_mode) and
YCbCr 4:2:0 format selection?
cix_dp_bridge_atomic_check() calls cix_dp_adjust_mode() before
cix_dp_pick_config(). Because cix_dp_adjust_mode() flips
DRM_MODE_FLAG_PVSYNC and DRM_MODE_FLAG_PHSYNC on adjusted_mode (and may alter
htotal and clock), drm_match_cea_mode(adjusted_mode) inside
drm_mode_is_420_only() fails to match the CEA mode table and returns 0.
Even when a mode is 4:2:0-only, cix_dp_pick_format_bpc() iterates fmt_prio[]
from RGB444 and YCBCR422 without checking drm_mode_is_420_only() first, so any
4:2:0-only mode whose clock fits on the link at RGB or YCbCr 4:2:2 picks a
non-4:2:0 format. Conversely, when info->color_formats has
DRM_OUTPUT_COLOR_FORMAT_YCBCR420 set, fmt_prio[4] can pick YCBCR420 on modes
where the sink does not support 4:2:0, and without checking that
adjusted_mode->clock <= CIX_DP_MAX_FREQ (since YCbCr 4:2:0 forces 1 pixel per
cycle).
[ ... ]
> +static void cix_dp_bridge_mode_set(struct cix_dp *dp,
> + struct drm_crtc_state *crtc_state,
> + struct drm_connector_state *conn_state)
> +{
[ ... ]
> + cix_conn->config.format = cix_dp_drm_format_to_dp(color_format);
> + cix_conn->config.colorspace = conn_state->colorspace;
> + cix_conn->config.bpc = bpc;
> + cix_conn->config.bpp = bpp;
> + dp->pixel_per_cycle = cix_dp_pick_ppc(adjusted_mode,
> + cix_conn->config.format);
> +
> + link_rate = cix_dp_pick_link_rate(dp, adjusted_mode, bpp);
> + bw_code = drm_dp_link_rate_to_bw_code(link_rate);
> +
> + dp->ops->mode_configure(dp, adjusted_mode->clock, bw_code, bpp, false);
[ ... ]
> +}
[ ... ]
> +static void
> +cix_dp_bridge_atomic_enable(struct drm_bridge *bridge,
> + struct drm_atomic_commit *state)
> +{
[ ... ]
> + cix_dp_bridge_mode_set(dp, crtc_state, conn_state);
> +
> + ret = dp->ops->encoder_enable(dp, dp->dp_panel.source_id, true);
> + if (ret)
> + dev_err(dp->dev, "failed to enable DP output: %d\n", ret);
> +}
[Severity: Medium]
Should cix_dp_bridge_mode_set() run under dp->session_lock and after
cix_dp_prepare() has read the sink's DPCD?
cix_dp_bridge_atomic_enable() calls cix_dp_bridge_mode_set() unlocked before
dp->ops->encoder_enable() -> cix_dp_prepare(). This mutates
dp->connector.config, dp->pixel_per_cycle, and dp->mode without holding
dp->session_lock (racing with cix_dp_hpd_irq_work_func() ->
cix_dp_train_loop()), and computes link_rate and lane_cnt from
dp->link_config before cix_dp_prepare() -> cix_dp_handle_connect() /
cix_dp_core_on() updates dp->link_config from the sink's DPCD.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109092623.102291-1-Luo.Liu@cixtech.com?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-11-09 20:30 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-09 9:26 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
2026-11-09 9:26 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
2026-11-09 20:04 ` sashiko-bot
2026-11-09 9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
2026-11-09 20:30 ` sashiko-bot
-- strict thread matches above, loose matches on Subject: below --
2026-11-09 10:30 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
2026-11-09 10:30 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
2026-11-09 10:42 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox