* [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter @ 2026-11-09 10:30 Luo Liu 2026-11-09 10:30 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu 2026-11-09 10:30 ` [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 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 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 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 2026-11-09 10:30 ` [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 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
* [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver 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:30 ` Luo Liu 2026-11-09 10:55 ` sashiko-bot 1 sibling, 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 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
* Re: [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver 2026-11-09 10:30 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu @ 2026-11-09 10:55 ` sashiko-bot 0 siblings, 0 replies; 7+ messages in thread From: sashiko-bot @ 2026-11-09 10:55 UTC (permalink / raw) To: Luo Liu; +Cc: dri-devel, devicetree, robh, conor+dt Thank you for your contribution! Sashiko AI review found 16 potential issues to consider: - [High] drm/cix: cix_dp_replay_deferred_hotplug() always fails with -ENODEV - [High] drm/cix: sink DPCD caps not read before detect, mode_valid, and mode_set - [High] drm/cix: PHY ref leak and wedged state in cix_dp_encoder_enable() and prepare - [High] drm/cix: HPD work cancellation and IRQ ordering races on teardown and suspend - [High] drm/cix: unclocked MMIO reads in cix_dp_aux_transfer() and HPD workers - [High] drm/cix/dptx: NULL dereference in cix_dp_panel_hw_cfg() with NO_CONNECTOR - [High] drm/cix/dptx: double connector cleanup and UAF in cix_dp_bridge_detach() - [High] drm/cix/dptx: broken sync flags and YCbCr 4:2:0 selection in atomic_check - [Medium] drm/cix: unbalanced PHY and clock teardown in cix_dptx_shutdown() and remove - [Medium] drm/cix: unbalanced disable_irq() and lost force_hpd state across suspend - [Medium] drm/cix: broken VSC SDP payload and stream override in dptx_setup_vsc_sdp() - [Medium] drm/cix: cix_dp_aux_cmd_submit() reads FIFO and continues on NACK or DEFER - [Medium] drm/cix/dptx: unchecked state allocation in cix_dp_connector_create() - [Medium] drm/cix/dptx: ignored mode_set failure causes divide-by-zero on enable - [Medium] drm/cix/dptx: CIX_DPTX_ENHANCED_FRAMING_ENABLE not cleared when disabled - [Medium] drm/cix/dptx: unlocked access to shared state in mode_set and oob_hotplug -- commit a2a34412966dc5d33f9669af4c794727d4e8b1e4 Author: Luo Liu <Luo.Liu@cixtech.com> drm/cix: add Sky1 DisplayPort transmitter driver This commit adds a DRM bridge driver for the CIX Sky1 DisplayPort transmitter, implementing DP link training, AUX transfers, hotplug handling, and connector management. > 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 [ ... ] > @@ -0,0 +1,2086 @@ > +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; > + } [Severity: Medium] Should CIX_DPTX_ENHANCED_FRAMING_ENABLE be cleared when dp->caps.enhanced_framing is false? If a sink without enhanced framing is connected after one that enabled it, the transmitter keeps sending enhanced framing while DP_LANE_COUNT_ENHANCED_FRAME_EN is cleared in the sink DPCD. [ ... ] > + 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; > +} [Severity: Medium] Does cix_dp_aux_cmd_submit() read from an empty CIX_DPTX_AUX_REPLY_DATA FIFO when the sink replies with NACK or DEFER? When value is non-zero (NACK or DEFER), the reply data count check is skipped, but the loop still reads bytes entries from CIX_DPTX_AUX_REPLY_DATA into buf and returns 0. In cix_dp_aux_transfer(), multi-chunk transfers also keep submitting subsequent chunks after a NACK or DEFER instead of stopping and returning the reply code to the DRM AUX helper. [ ... ] > +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; [ ... ] > + 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; > +} [Severity: Medium] Does dptx_setup_vsc_sdp() program the wrong pixel encoding and bit depth in the VSC SDP payload? CIX_DP_FORMAT_YCBCR422 sets sdp->db[16] to 0x1 << 4 (YCbCr 4:4:4) instead of 0x2 << 4 (YCbCr 4:2:2). On the default (RGB) path, sdp->db[16] is not cleared before ORing the colorimetry bits, leaving stale format and colorimetry values from earlier modesets. In addition, 6 bpc falls into the default branch and encodes sdp->db[17] as 0x1 (8 bpc) instead of 0x0. In cix_dp_set_misc(), the VSC SDP path also only sets the pixel format override for CIX_DP_FORMAT_YCBCR420 (leaving override format bits as RGB for YCbCr 4:2:2 and 4:4:4) and hardcodes config->content_type to DP_CONTENT_TYPE_NOT_DEFINED instead of using the connector's content_type property. > + > +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] Can cix_dp_panel_hw_cfg() and cix_dp_panel_setup_colorimetry_sdp() dereference a NULL dp_panel->connector when the bridge is attached with DRM_BRIDGE_ATTACH_NO_CONNECTOR? When DRM_BRIDGE_ATTACH_NO_CONNECTOR is passed to cix_dp_bridge_attach(), cix_dp_connector_create() is skipped and dp->dp_panel.connector remains NULL. Enabling the bridge then calls cix_dp_panel_hw_cfg(), which ignores the -EINVAL return from cix_dp_set_misc() and dereferences conn in cix_dp_panel_setup_colorimetry_sdp() and cix_dp_mode_set_transfer_unit(). In addition, cix_dp_hpd_event_work_func() returns early when dp->dp_panel.connector is NULL, and cix_dp_bridge_funcs does not implement the bridge detection or EDID callbacks needed when connector creation is delegated to the display controller. [ ... ] > +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] Does drm_dp_dpcd_readb() in cix_dp_replay_deferred_hotplug() always fail with -ENODEV? cix_dp_replay_deferred_hotplug() returns early if cix_dp_get_hpd_state(dp) is true, so reaching drm_dp_dpcd_readb() guarantees cix_dp_get_hpd_state(dp) is false. Because dp->force_hpd is only set to connector_status_connected after drm_dp_dpcd_readb(), cix_dp_aux_transfer() sees !cix_dp_get_hpd_state(dp) and rejects the AUX read with -ENODEV, preventing deferred hotplug from ever triggering. > + > +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] Does probing the connector before cix_dp_handle_connect() leave dp->dpcd, dp->link_config, and dp->caps uninitialized during detection and mode validation? cix_dp_detect() only checks the HPD pin level and never reads DPCD. In cix_dp_hpd_event_work_func(), drm_helper_probe_detect() runs before cix_dp_handle_connect(), so cix_dp_connector_detect() passes zeroed or stale dp->dpcd and dp->downstream_ports to drm_dp_set_subconnector_property(). Likewise, when a native DP monitor is connected before cix_dp_bridge_attach() creates the connector, or after system resume, DRM probes the connector and validates modes before cix_dp_handle_connect() ever runs, and cix_dp_bridge_atomic_enable() calls cix_dp_bridge_mode_set() before encoder_enable() reads the sink's DPCD capabilities. Also, in cix_dp_handle_connect(), DPTX_STATE_CONNECTED is only cleared when cix_dp_panel_read_sink_caps() fails with -ETIMEDOUT or -ENOTCONN; if it fails with -EIO or -EBUSY, DPTX_STATE_CONNECTED remains set with zeroed caps and blocks later retries. > + > +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; [Severity: High] Can cix_dp_get_hpd_state() and cix_dp_aux_transfer() read DPTX registers while dp->apb_clk is gated? As noted above cix_dp_detect(), cix_dp_read() hangs the bus if called when DPTX_STATE_INITIALIZED is clear and dp->apb_clk is disabled. cix_dp_hpd_irq_work_func() calls cix_dp_get_hpd_state() before locking dp->session_lock and checking DPTX_STATE_INITIALIZED, and cix_dp_aux_transfer() (reachable via /dev/drm_dp_aux*) and cix_dp_hpd_event_work_func() access DPTX registers without checking DPTX_STATE_INITIALIZED under dp->session_lock. [ ... ] > +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; > +} [Severity: Medium] If cix_dp_host_init() fails in cix_dp_pm_complete(), does dp->irq stay disabled while a later cix_dp_pm_prepare() or cix_dp_deinit() calls disable_irq() a second time? Because cix_dp_pm_prepare() and cix_dp_deinit() call disable_irq(dp->irq) unconditionally without checking whether DPTX_STATE_SUSPENDED is already set, a failed resume nests the IRQ disable depth. Additionally, when DPTX_STATE_SUSPENDED is set, cix_dp_oob_hotplug_event() overwrites dp->force_hpd with connector_status_disconnected instead of recording hpd_state, losing Type-C plug events that arrive during suspend. > + > +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_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: High] Does a failure in cix_dp_enable() leak PHY references and leave the transmitter wedged with DPTX_STATE_READY set? In cix_dp_encoder_enable(), cix_dp_prepare() calls cix_dp_core_on(), which 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 and cix_dp_core_off() is not called. A subsequent disable will return early in cix_dp_unprepare() because DPTX_STATE_ENABLED is clear, while a subsequent enable will call reset_dp_and_reinit() (leaking another phy_init() call) and then return early in cix_dp_core_on() because DPTX_STATE_READY is still set, skipping link training after the reset. Also, if the display controller resumes and calls cix_dp_prepare() before cix_dptx_pm_resume() clears DPTX_STATE_SUSPENDED, cix_dp_handle_connect() will bail out on DPTX_STATE_SUSPENDED and leave the display disabled. [ ... ] > +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: High] Can HPD delayed work items be re-queued after cancel_delayed_work_sync() during teardown, bridge detach, or suspend? CPU0 CPU1 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(dp) mutex_destroy(&dp->session_lock) cix_dp_hpd_event_work_func() <-- unclocked MMIO / UAF In cix_dp_deinit(), disable_irq(dp->irq) is called after cancel_delayed_work_sync(). In cix_dp_bridge_detach(), hpd_irq_work is cancelled before sw_hpd_irq_work (which queues hpd_irq_work), while dp->irq remains enabled and dp->dp_panel.connector is only cleared after drm_connector_cleanup(). In cix_dp_pm_prepare(), the works are cancelled before DPTX_STATE_SUSPENDED is set, allowing a concurrent cix_dp_oob_hotplug_event() to schedule them right before cix_dp_host_deinit() gates dp->apb_clk. [ ... ] > +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] Does zeroing dp->active_stream_cnt in cix_dptx_shutdown() cause cix_dp_host_deinit() to call phy_exit() while the PHY is still powered on? When a stream is active at shutdown, forcing dp->active_stream_cnt = 0 without calling cix_dp_ctrl_stream_off() and cix_dp_core_off() leaves stream_clk enabled and the PHY powered on when cix_dp_pm_prepare() -> cix_dp_host_deinit() calls phy_exit() and disables dp->apb_clk. Conversely, in cix_dptx_remove() -> cix_dp_deinit(), if dp->active_stream_cnt is non-zero, cix_dp_host_deinit() returns early and leaks the clocks and PHY references. > 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 [ ... ] > @@ -0,0 +1,653 @@ > +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. */\n> + 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 && > + drm_mode_is_420_only(info, adjusted_mode))) > + continue; [Severity: High] Does mutating adjusted_mode->flags in cix_dp_adjust_mode() break CEA mode matching and YCbCr 4:2:0 format selection in cix_dp_pick_format_bpc()? cix_dp_adjust_mode() toggles DRM_MODE_FLAG_PVSYNC and DRM_MODE_FLAG_PHSYNC without updating DRM_MODE_FLAG_NVSYNC and DRM_MODE_FLAG_NHSYNC, leaving adjusted_mode->flags with either both positive and negative sync flags set or neither set. When cix_dp_pick_format_bpc() then passes adjusted_mode to drm_mode_is_420_only(), drm_match_cea_mode() fails to match the mutated flags (and any adjusted htotal/clock) and always returns false. Furthermore, cix_dp_pick_format_bpc() does not skip non-4:2:0 formats when a mode is 4:2:0-only (so RGB444 or YCbCr 4:2:2 is picked whenever link bandwidth allows), and it does not check drm_mode_is_420() or dp->caps.vsc_supported before picking DRM_OUTPUT_COLOR_FORMAT_YCBCR420 (nor does cix_dp_bridge_atomic_check() validate adjusted_mode->clock against CIX_DP_MAX_FREQ). [ ... ] > +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; [Severity: Medium] Can cix_dp_bridge_mode_set() race with cix_dp_hpd_irq_work_func() and cix_dp_handle_connect() on dp->mode, dp->connector.config, and dp->link_config? cix_dp_hpd_irq_work_func() holds dp->session_lock while cix_dp_train_loop() reads dp->connector.config.bpp and updates dp->mode, and cix_dp_handle_connect() holds dp->session_lock while updating dp->link_config, but cix_dp_bridge_mode_set() reads dp->link_config and writes dp->connector.config and dp->mode without holding dp->session_lock. Similarly, cix_dp_oob_hotplug_event() reads dp->state and updates dp->force_hpd without holding dp->session_lock. > + 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); [Severity: Medium] If cix_dp_pick_config() or dp->ops->mode_configure() fails in cix_dp_bridge_mode_set(), can cix_dp_bridge_atomic_enable() divide by zero? When cix_dp_bridge_mode_set() fails before dp->mode is populated, cix_dp_bridge_atomic_enable() still calls dp->ops->encoder_enable() with dp->mode.lane_cnt == 0 and dp->mode.bw_code == 0. Link training succeeds vacuously for 0 lanes, and cix_dp_mode_set_transfer_unit() and cix_dp_mode_set_timing() then divide by dp->mode.lane_cnt. [ ... ] > +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); [Severity: Medium] Can cix_dp_add_properties() dereference a NULL connector->state if the allocation inside drm_atomic_helper_connector_reset() fails? drm_connector_attach_max_bpc_property() unconditionally writes to connector->state->max_requested_bpc and connector->state->max_bpc. [ ... ] > +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); [Severity: High] Does calling drm_connector_cleanup() in cix_dp_bridge_detach() cause a double cleanup when the DRM mode_config or an atomic state still holds a reference to the connector? cix_dp_connector_funcs sets .destroy = drm_connector_cleanup, so the connector is reference-counted and cleaned up when its last reference is dropped (such as during drm_mode_config_cleanup()). Calling drm_connector_cleanup() directly in cix_dp_bridge_detach() zeroes the embedded struct drm_connector while references remain, leading to a refcount underflow and a NULL connector->funcs->destroy dereference in drm_connector_put(). -- Sashiko AI review · https://sashiko.dev/#/patchset/20261109103010.110911-1-Luo.Liu@cixtech.com?part=2 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter @ 2026-11-09 9:26 Luo Liu 2026-11-09 9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu 0 siblings, 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 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 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 ` Luo Liu 2026-11-09 20:30 ` sashiko-bot 0 siblings, 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
* 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 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 2026-11-09 10:30 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu 2026-11-09 10:55 ` sashiko-bot -- strict thread matches above, loose matches on Subject: below -- 2026-11-09 9:26 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu 2026-11-09 9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu 2026-11-09 20:30 ` sashiko-bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox