Devicetree
 help / color / mirror / Atom feed
* [PATCH v6 0/5] phy: Add the StarFive JH7110 Innosilicon HDMI PHY
       [not found] <CGME20261028152225eucas1p133ea1b7981dfa2afe085b83e1bd6de3b@eucas1p1.samsung.com>
@ 2026-10-28 15:21 ` Michal Wilczynski
  2026-10-28 15:21   ` [PATCH v6 1/5] dt-bindings: phy: Add starfive,jh7110-inno-hdmi-phy Michal Wilczynski
                     ` (4 more replies)
  0 siblings, 5 replies; 8+ messages in thread
From: Michal Wilczynski @ 2026-10-28 15:21 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
	Emil Renner Berthing, Hal Feng, Dominique Belhachemi,
	Michal Wilczynski, Andy Yan
  Cc: linux-phy, devicetree, linux-kernel, linux-arm-kernel,
	linux-rockchip, linux-riscv, Marek Szyprowski, Icenowy Zheng,
	Chaoyi Chen, Joshua Peisach, Byron Stanoszek, OOHehir

This series adds the HDMI PHY driver for the StarFive JH7110 and shares
the pre-PLL code it has in common with the Rockchip RK3328 Innosilicon
HDMI PHY.

Up to v5 it was part of the JH7110 display series [1]. It is split out
as Vinod suggested, so it can go through linux-phy on its own; the rest
continues as v6 of "drm: starfive: jh7110: Enable display subsystem".
Nothing here depends on the display series. The display series depends
on this one: its HDMI subsystem binding references the PHY binding from
patch 1, and it needs the PHY driver at runtime.

[1] https://lore.kernel.org/all/20260929-jh7110-clean-send-v5-0-82b4d8e3c6c7@samsung.com/

The JH7110 has the same Innosilicon PHY as the RK3328, offset by 0x100
because it sits behind the HDMI controller in a shared register block.
Patch 2 factors out the pre-PLL config format, table lookup,
determine_rate, recalc_rate and the pre-PLL programming; patch 3 moves
Rockchip onto it; patch 4 adds the JH7110 driver. Pixel clock tables,
post-PLL and analog config stay SoC specific.

Patch 3 should be a no-op for Rockchip - same writes, same order, same
values - and RK3228, whose pre-PLL is at different addresses, keeps its
own register code and shares only the lookup. I have no Rockchip
hardware, so it is build tested only (arm and riscv). A Tested-by would
help.

Patch 5 adds a MAINTAINERS entry for the binding and the driver, as the
other JH7110 PHY drivers have.

Testing
=======

Tested on a VisionFive 2 v1.3B together with the display series, using
modetest: all 42 modes the sink advertises work, with pixel clocks from
25.175 MHz up to 297 MHz.

Every commit builds for riscv and arm.

Changes in v6:
- Split out of the JH7110 display series (Vinod)
- inno_read() returns the regmap_read() error and recalc_rate checks it,
  instead of a failed read turning into a zero divisor (Vinod)
- Bound the pre-PLL and post-PLL table walks with the table size instead
  of relying on a sentinel (Vinod)
- Lowercase hex in the JH7110 pixel clock table (Vinod)
- New patch 5: a MAINTAINERS entry for the JH7110 HDMI PHY, moved out of
  the display series' entry
- Rebased onto v7.3, collected tags
- Link to v5: https://lore.kernel.org/all/20260929-jh7110-clean-send-v5-0-82b4d8e3c6c7@samsung.com/

---
Michal Wilczynski (5):
      dt-bindings: phy: Add starfive,jh7110-inno-hdmi-phy
      phy: Add common Innosilicon HDMI PHY helpers
      phy: rockchip: inno-hdmi: Use the common Innosilicon PHY helpers
      phy: starfive: Add jh7110-inno-hdmi-phy driver
      MAINTAINERS: Add StarFive JH7110 HDMI PHY entry

 .../phy/starfive,jh7110-inno-hdmi-phy.yaml         |  49 ++
 MAINTAINERS                                        |   6 +
 drivers/phy/Kconfig                                |   8 +
 drivers/phy/Makefile                               |   1 +
 drivers/phy/phy-inno-hdmi.c                        | 358 +++++++++++++
 drivers/phy/rockchip/Kconfig                       |   1 +
 drivers/phy/rockchip/phy-rockchip-inno-hdmi.c      | 169 +-----
 drivers/phy/starfive/Kconfig                       |  20 +
 drivers/phy/starfive/Makefile                      |   1 +
 drivers/phy/starfive/phy-jh7110-inno-hdmi.c        | 587 +++++++++++++++++++++
 include/linux/phy/inno-hdmi-phy.h                  |  88 +++
 11 files changed, 1144 insertions(+), 144 deletions(-)
---
base-commit: c03c1a14507dfaf8b856c2608f9ac216021fc222
change-id: 20261028-jh7110-inno-hdmi-phy-83e5cfa643e3

Best regards,
-- 
Michal Wilczynski <m.wilczynski@samsung.com>


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH v6 1/5] dt-bindings: phy: Add starfive,jh7110-inno-hdmi-phy
  2026-10-28 15:21 ` [PATCH v6 0/5] phy: Add the StarFive JH7110 Innosilicon HDMI PHY Michal Wilczynski
@ 2026-10-28 15:21   ` Michal Wilczynski
  2026-10-28 15:21   ` [PATCH v6 2/5] phy: Add common Innosilicon HDMI PHY helpers Michal Wilczynski
                     ` (3 subsequent siblings)
  4 siblings, 0 replies; 8+ messages in thread
From: Michal Wilczynski @ 2026-10-28 15:21 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
	Emil Renner Berthing, Hal Feng, Dominique Belhachemi,
	Michal Wilczynski, Andy Yan
  Cc: linux-phy, devicetree, linux-kernel, linux-arm-kernel,
	linux-rockchip, linux-riscv, Marek Szyprowski, Icenowy Zheng,
	Chaoyi Chen, Joshua Peisach

Add the dt-binding for the StarFive JH7110 Innosilicon HDMI PHY.

This device is a child of the starfive,jh7110-hdmi-subsystem node. It
functions as both a PHY provider for the controller and as a clock
provider for the variable pixel clock (hdmi_pclk), which it generates
from its refoclk.

Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Michal Wilczynski <m.wilczynski@samsung.com>
---
 .../phy/starfive,jh7110-inno-hdmi-phy.yaml         | 49 ++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/Documentation/devicetree/bindings/phy/starfive,jh7110-inno-hdmi-phy.yaml b/Documentation/devicetree/bindings/phy/starfive,jh7110-inno-hdmi-phy.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6417553e0f5ba295b0451301ca7c1a25534993a3
--- /dev/null
+++ b/Documentation/devicetree/bindings/phy/starfive,jh7110-inno-hdmi-phy.yaml
@@ -0,0 +1,49 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/phy/starfive,jh7110-inno-hdmi-phy.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: StarFive JH7110 Innosilicon INNO HDMI PHY
+
+maintainers:
+  - Michal Wilczynski <m.wilczynski@samsung.com>
+
+description:
+  The PHY portion of the StarFive JH7110 INNO HDMI IP.
+
+properties:
+  compatible:
+    const: starfive,jh7110-inno-hdmi-phy
+
+  clocks:
+    maxItems: 1
+    description: Reference oscillator.
+
+  '#clock-cells':
+    const: 0
+
+  '#phy-cells':
+    const: 0
+
+required:
+  - compatible
+  - clocks
+  - '#clock-cells'
+  - '#phy-cells'
+
+additionalProperties: false
+
+examples:
+  # Shown as a child of the HDMI subsystem node; see
+  # Documentation/devicetree/bindings/soc/starfive/starfive,jh7110-hdmi-subsystem.yaml
+  # for the full node.
+  - |
+    phy {
+        compatible = "starfive,jh7110-inno-hdmi-phy";
+        #clock-cells = <0>;
+        clocks = <&xin24m>;
+        #phy-cells = <0>;
+    };
+
+...

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v6 2/5] phy: Add common Innosilicon HDMI PHY helpers
  2026-10-28 15:21 ` [PATCH v6 0/5] phy: Add the StarFive JH7110 Innosilicon HDMI PHY Michal Wilczynski
  2026-10-28 15:21   ` [PATCH v6 1/5] dt-bindings: phy: Add starfive,jh7110-inno-hdmi-phy Michal Wilczynski
@ 2026-10-28 15:21   ` Michal Wilczynski
  2026-10-28 15:30     ` sashiko-bot
  2026-10-28 15:21   ` [PATCH v6 3/5] phy: rockchip: inno-hdmi: Use the common Innosilicon " Michal Wilczynski
                     ` (2 subsequent siblings)
  4 siblings, 1 reply; 8+ messages in thread
From: Michal Wilczynski @ 2026-10-28 15:21 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
	Emil Renner Berthing, Hal Feng, Dominique Belhachemi,
	Michal Wilczynski, Andy Yan
  Cc: linux-phy, devicetree, linux-kernel, linux-arm-kernel,
	linux-rockchip, linux-riscv, Marek Szyprowski, Icenowy Zheng,
	Chaoyi Chen, Joshua Peisach, Byron Stanoszek, OOHehir

The Innosilicon HDMI PHY IP is used by several SoCs. They differ in
where the PHY register block sits in the register space and in which
pixel clocks they support, but the pre-PLL programming sequence and the
layout of its registers are the same.

Add a small library holding that shared part: the pre-PLL configuration
table format, a lookup, clk_ops determine_rate and recalc_rate helpers,
and the pre-PLL register programming. Callers pass a regmap, a register
offset for the PHY block, and their own pixel clock table and its
size.

No driver uses it yet; the users are converted separately.

Reviewed-by: Chaoyi Chen <chaoyi.chen@rock-chips.com>
Reviewed-by: Joshua Peisach <jpeisach@ubuntu.com>
Reviewed-by: Heiko Stuebner <heiko@sntech.de>
Tested-by: Byron Stanoszek <gandalf@winds.org>
Tested-by: OOHehir <oo.hehir@gmail.com>
Signed-off-by: Michal Wilczynski <m.wilczynski@samsung.com>
---
 drivers/phy/Kconfig               |   8 +
 drivers/phy/Makefile              |   1 +
 drivers/phy/phy-inno-hdmi.c       | 358 ++++++++++++++++++++++++++++++++++++++
 include/linux/phy/inno-hdmi-phy.h |  88 ++++++++++
 4 files changed, 455 insertions(+)

diff --git a/drivers/phy/Kconfig b/drivers/phy/Kconfig
index 19f3b7d12b7d492d16000fa52483dc5002590d7a..0e973b3b02a30704d339db9f15d4695bcbec1c75 100644
--- a/drivers/phy/Kconfig
+++ b/drivers/phy/Kconfig
@@ -168,6 +168,14 @@ config PHY_XGENE
 	help
 	  This option enables support for APM X-Gene SoC multi-purpose PHY.
 
+config PHY_INNO_HDMI
+	tristate
+	depends on GENERIC_PHY
+	select REGMAP
+	help
+	  Common helpers for the Innosilicon HDMI PHY, shared by the SoC
+	  drivers that instantiate it. Selected by those drivers.
+
 source "drivers/phy/allwinner/Kconfig"
 source "drivers/phy/amlogic/Kconfig"
 source "drivers/phy/apple/Kconfig"
diff --git a/drivers/phy/Makefile b/drivers/phy/Makefile
index d7aa516bcc49e7662c9c9db421833fe591dfbc05..e93c473dfc6d837ed6ba540f0b0204a0d68d25bf 100644
--- a/drivers/phy/Makefile
+++ b/drivers/phy/Makefile
@@ -7,6 +7,7 @@ obj-$(CONFIG_PHY_COMMON_PROPS)		+= phy-common-props.o
 obj-$(CONFIG_PHY_COMMON_PROPS_TEST)	+= phy-common-props-test.o
 obj-$(CONFIG_GENERIC_PHY)		+= phy-core.o
 obj-$(CONFIG_GENERIC_PHY_MIPI_DPHY)	+= phy-core-mipi-dphy.o
+obj-$(CONFIG_PHY_INNO_HDMI)		+= phy-inno-hdmi.o
 obj-$(CONFIG_PHY_AIROHA_PCIE)		+= phy-airoha-pcie.o
 obj-$(CONFIG_PHY_CAN_TRANSCEIVER)	+= phy-can-transceiver.o
 obj-$(CONFIG_PHY_ECONET_PCIE)		+= phy-econet-pcie.o
diff --git a/drivers/phy/phy-inno-hdmi.c b/drivers/phy/phy-inno-hdmi.c
new file mode 100644
index 0000000000000000000000000000000000000000..6900d4d83e46489e4bc47c5688421a3c0b9a2370
--- /dev/null
+++ b/drivers/phy/phy-inno-hdmi.c
@@ -0,0 +1,358 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Common helpers for the Innosilicon HDMI PHY.
+ *
+ * Copyright (c) 2017 Rockchip Electronics Co. Ltd.
+ * Copyright (c) 2025 Samsung Electronics Co., Ltd.
+ *
+ * Author: Zheng Yang <zhengyang@rock-chips.com>
+ * Author: Michal Wilczynski <m.wilczynski@samsung.com>
+ *
+ * Derived from drivers/phy/rockchip/phy-rockchip-inno-hdmi.c.
+ *
+ * The same PHY IP is used by several SoCs, which differ in where the PHY
+ * register block sits and in the pixel clock table they support, but share
+ * the pre-PLL programming sequence.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/export.h>
+#include <linux/iopoll.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#include <linux/phy/inno-hdmi-phy.h>
+
+#define INNO_PRE_PLL_CONTROL			0xa0
+#define INNO_PRE_PLL_POWER_DOWN			BIT(0)
+#define INNO_PCLK_VCO_DIV_5_MASK		BIT(1)
+#define INNO_PCLK_VCO_DIV_5(x)			FIELD_PREP(BIT(1), x)
+
+#define INNO_PRE_PLL_DIV_1			0xa1
+#define INNO_PRE_PLL_PRE_DIV_MASK		GENMASK(5, 0)
+#define INNO_PRE_PLL_PRE_DIV(x)			FIELD_PREP(GENMASK(5, 0), x)
+
+#define INNO_PRE_PLL_DIV_2			0xa2
+#define INNO_SPREAD_SPECTRUM_MOD_DISABLE	BIT(6)
+#define INNO_PRE_PLL_FRAC_DIV_DISABLE		FIELD_PREP(GENMASK(5, 4), 3)
+#define INNO_PRE_PLL_FB_DIV_11_8_MASK		GENMASK(3, 0)
+#define INNO_PRE_PLL_FB_DIV_11_8(x)		FIELD_PREP(GENMASK(3, 0), (x) >> 8)
+
+#define INNO_PRE_PLL_DIV_3			0xa3
+#define INNO_PRE_PLL_FB_DIV_7_0(x)		FIELD_PREP(GENMASK(7, 0), x)
+
+#define INNO_PRE_PLL_TMDSCLK_DIV		0xa4
+#define INNO_PRE_PLL_TMDSCLK_DIV_C(x)		FIELD_PREP(GENMASK(1, 0), x)
+#define INNO_PRE_PLL_TMDSCLK_DIV_B(x)		FIELD_PREP(GENMASK(3, 2), x)
+#define INNO_PRE_PLL_TMDSCLK_DIV_A(x)		FIELD_PREP(GENMASK(5, 4), x)
+
+#define INNO_PCLK_DIV_AB			0xa5
+#define INNO_PCLK_DIV_B_MASK			GENMASK(6, 5)
+#define INNO_PCLK_DIV_B(x)			FIELD_PREP(GENMASK(6, 5), x)
+#define INNO_PCLK_DIV_A_MASK			GENMASK(4, 0)
+#define INNO_PCLK_DIV_A(x)			FIELD_PREP(GENMASK(4, 0), x)
+
+#define INNO_PCLK_DIV_CD			0xa6
+#define INNO_PCLK_DIV_C(x)			FIELD_PREP(GENMASK(6, 5), x)
+#define INNO_PCLK_DIV_D_MASK			GENMASK(4, 0)
+#define INNO_PCLK_DIV_D(x)			FIELD_PREP(GENMASK(4, 0), x)
+
+#define INNO_PRE_PLL_LOCK_STATUS		0xa9
+#define INNO_PRE_PLL_LOCK			BIT(0)
+
+#define INNO_PRE_PLL_FRAC_DIV_23_16		0xd1
+#define INNO_PRE_PLL_FRAC_DIV_15_8		0xd2
+#define INNO_PRE_PLL_FRAC_DIV_7_0		0xd3
+#define INNO_PRE_PLL_FRAC_DIV(x)		FIELD_PREP(GENMASK(7, 0), x)
+
+#define INNO_FRAC_DIV_WIDTH			24
+
+static unsigned int inno_reg(const struct inno_hdmi_phy_pre_pll *pll,
+			     unsigned int reg)
+{
+	return (pll->offset + reg) * 4;
+}
+
+static void inno_write(const struct inno_hdmi_phy_pre_pll *pll,
+		       unsigned int reg, u8 val)
+{
+	regmap_write(pll->regmap, inno_reg(pll, reg), val);
+}
+
+static int inno_read(const struct inno_hdmi_phy_pre_pll *pll, unsigned int reg,
+		     unsigned int *val)
+{
+	return regmap_read(pll->regmap, inno_reg(pll, reg), val);
+}
+
+static void inno_update_bits(const struct inno_hdmi_phy_pre_pll *pll,
+			     unsigned int reg, u8 mask, u8 val)
+{
+	regmap_update_bits(pll->regmap, inno_reg(pll, reg), mask, val);
+}
+
+/**
+ * inno_hdmi_phy_pre_pll_lookup - find the settings for a pixel clock
+ * @pll: pre-PLL instance
+ * @pixclock: requested pixel clock, in Hz
+ * @tmdsclock: requested TMDS clock, in Hz
+ *
+ * Return: the matching table entry, or an ERR_PTR if the PHY cannot generate
+ * the requested combination.
+ */
+const struct inno_hdmi_phy_pre_pll_config *
+inno_hdmi_phy_pre_pll_lookup(const struct inno_hdmi_phy_pre_pll *pll,
+			     unsigned long pixclock, unsigned long tmdsclock)
+{
+	unsigned int i;
+
+	for (i = 0; i < pll->table_size; i++) {
+		const struct inno_hdmi_phy_pre_pll_config *cfg = &pll->table[i];
+
+		if (!cfg->pixclock)
+			continue;
+
+		if (cfg->pixclock == pixclock && cfg->tmdsclock == tmdsclock)
+			return cfg;
+	}
+
+	return ERR_PTR(-EINVAL);
+}
+EXPORT_SYMBOL_GPL(inno_hdmi_phy_pre_pll_lookup);
+
+/**
+ * inno_hdmi_phy_pre_pll_determine_rate - clk_ops.determine_rate helper
+ * @pll: pre-PLL instance
+ * @req: rate request, updated with the rate the PHY would produce
+ *
+ * The PHY can only generate the pixel clocks described by its table, so a
+ * request that does not appear there is rejected rather than rounded.
+ *
+ * Return: 0 on success, -EINVAL if the rate is not supported.
+ */
+int inno_hdmi_phy_pre_pll_determine_rate(const struct inno_hdmi_phy_pre_pll *pll,
+					 struct clk_rate_request *req)
+{
+	unsigned long rate = rounddown(req->rate, 1000);
+	unsigned int i;
+
+	for (i = 0; i < pll->table_size; i++) {
+		const struct inno_hdmi_phy_pre_pll_config *cfg = &pll->table[i];
+
+		if (!cfg->pixclock)
+			continue;
+
+		if (cfg->pixclock == rate) {
+			req->rate = cfg->pixclock;
+			return 0;
+		}
+	}
+
+	return -EINVAL;
+}
+EXPORT_SYMBOL_GPL(inno_hdmi_phy_pre_pll_determine_rate);
+
+/**
+ * inno_hdmi_phy_pre_pll_recalc_rate - clk_ops.recalc_rate helper
+ * @pll: pre-PLL instance
+ * @parent_rate: rate of the reference clock, in Hz
+ *
+ * Return: the pixel clock the pre-PLL is currently programmed for, in Hz.
+ */
+unsigned long
+inno_hdmi_phy_pre_pll_recalc_rate(const struct inno_hdmi_phy_pre_pll *pll,
+				  unsigned long parent_rate)
+{
+	unsigned int control, div_1, div_2, div_3, pclk_div_ab, pclk_div_cd;
+	unsigned int frac_div_7_0, frac_div_15_8, frac_div_23_16, div;
+	u8 nd, no_a, no_b, no_d;
+	unsigned long frac, rate;
+	u64 vco, vco_rate;
+	u16 nf;
+	int ret;
+
+	ret = inno_read(pll, INNO_PRE_PLL_CONTROL, &control);
+	if (ret)
+		goto err;
+	ret = inno_read(pll, INNO_PRE_PLL_DIV_1, &div_1);
+	if (ret)
+		goto err;
+	ret = inno_read(pll, INNO_PRE_PLL_DIV_2, &div_2);
+	if (ret)
+		goto err;
+	ret = inno_read(pll, INNO_PRE_PLL_DIV_3, &div_3);
+	if (ret)
+		goto err;
+	ret = inno_read(pll, INNO_PCLK_DIV_AB, &pclk_div_ab);
+	if (ret)
+		goto err;
+	ret = inno_read(pll, INNO_PCLK_DIV_CD, &pclk_div_cd);
+	if (ret)
+		goto err;
+
+	nd = div_1 & INNO_PRE_PLL_PRE_DIV_MASK;
+	nf = (div_2 & INNO_PRE_PLL_FB_DIV_11_8_MASK) << 8;
+	nf |= div_3;
+	vco = parent_rate * nf;
+
+	if (!(div_2 & INNO_PRE_PLL_FRAC_DIV_DISABLE)) {
+		ret = inno_read(pll, INNO_PRE_PLL_FRAC_DIV_7_0, &frac_div_7_0);
+		if (ret)
+			goto err;
+		ret = inno_read(pll, INNO_PRE_PLL_FRAC_DIV_15_8, &frac_div_15_8);
+		if (ret)
+			goto err;
+		ret = inno_read(pll, INNO_PRE_PLL_FRAC_DIV_23_16,
+				&frac_div_23_16);
+		if (ret)
+			goto err;
+
+		frac = frac_div_7_0 | (frac_div_15_8 << 8) |
+		       (frac_div_23_16 << 16);
+		vco += DIV_ROUND_CLOSEST(parent_rate * frac,
+					 BIT(INNO_FRAC_DIV_WIDTH));
+	}
+
+	vco_rate = vco;
+
+	if (control & INNO_PCLK_VCO_DIV_5_MASK) {
+		div = nd * 5;
+	} else {
+		no_a = pclk_div_ab & INNO_PCLK_DIV_A_MASK;
+		no_b = FIELD_GET(INNO_PCLK_DIV_B_MASK, pclk_div_ab) + 2;
+		no_d = pclk_div_cd & INNO_PCLK_DIV_D_MASK;
+
+		div = nd * (no_a == 1 ? no_b : no_a) * no_d * 2;
+	}
+
+	/*
+	 * An unprogrammed pre-PLL reads its dividers back as zero, which would
+	 * leave do_div() dividing by it.
+	 */
+	if (!div) {
+		dev_dbg(pll->dev, "pre-PLL dividers read back as zero\n");
+		return 0;
+	}
+
+	do_div(vco, div);
+
+	rate = DIV_ROUND_CLOSEST((unsigned long)vco, 1000) * 1000;
+
+	/* The VCO is only in spec between 1.4 and 3.2 GHz, so log it. */
+	dev_dbg(pll->dev, "%s rate %lu vco %llu\n", __func__, rate, vco_rate);
+
+	return rate;
+
+err:
+	dev_err(pll->dev, "failed to read the pre-PLL registers: %d\n", ret);
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(inno_hdmi_phy_pre_pll_recalc_rate);
+
+/**
+ * inno_hdmi_phy_pre_pll_configure - program the pre-PLL dividers
+ * @pll: pre-PLL instance
+ * @cfg: settings to program
+ *
+ * The caller is responsible for powering the pre-PLL down beforehand and back
+ * up afterwards, and for waiting for it to lock.
+ */
+void inno_hdmi_phy_pre_pll_configure(const struct inno_hdmi_phy_pre_pll *pll,
+				     const struct inno_hdmi_phy_pre_pll_config *cfg)
+{
+	u8 val;
+
+	inno_update_bits(pll, INNO_PRE_PLL_CONTROL, INNO_PCLK_VCO_DIV_5_MASK,
+			 INNO_PCLK_VCO_DIV_5(cfg->vco_div_5_en));
+	inno_write(pll, INNO_PRE_PLL_DIV_1, INNO_PRE_PLL_PRE_DIV(cfg->prediv));
+
+	val = INNO_SPREAD_SPECTRUM_MOD_DISABLE;
+	if (!cfg->fracdiv)
+		val |= INNO_PRE_PLL_FRAC_DIV_DISABLE;
+
+	inno_write(pll, INNO_PRE_PLL_DIV_2,
+		   INNO_PRE_PLL_FB_DIV_11_8(cfg->fbdiv) | val);
+	inno_write(pll, INNO_PRE_PLL_DIV_3,
+		   INNO_PRE_PLL_FB_DIV_7_0(cfg->fbdiv));
+
+	inno_write(pll, INNO_PCLK_DIV_AB,
+		   INNO_PCLK_DIV_A(cfg->pclk_div_a) |
+		   INNO_PCLK_DIV_B(cfg->pclk_div_b));
+	inno_write(pll, INNO_PCLK_DIV_CD,
+		   INNO_PCLK_DIV_C(cfg->pclk_div_c) |
+		   INNO_PCLK_DIV_D(cfg->pclk_div_d));
+
+	inno_write(pll, INNO_PRE_PLL_TMDSCLK_DIV,
+		   INNO_PRE_PLL_TMDSCLK_DIV_A(cfg->tmds_div_a) |
+		   INNO_PRE_PLL_TMDSCLK_DIV_B(cfg->tmds_div_b) |
+		   INNO_PRE_PLL_TMDSCLK_DIV_C(cfg->tmds_div_c));
+
+	inno_write(pll, INNO_PRE_PLL_FRAC_DIV_7_0,
+		   INNO_PRE_PLL_FRAC_DIV(cfg->fracdiv));
+	inno_write(pll, INNO_PRE_PLL_FRAC_DIV_15_8,
+		   INNO_PRE_PLL_FRAC_DIV(cfg->fracdiv >> 8));
+	inno_write(pll, INNO_PRE_PLL_FRAC_DIV_23_16,
+		   INNO_PRE_PLL_FRAC_DIV(cfg->fracdiv >> 16));
+}
+EXPORT_SYMBOL_GPL(inno_hdmi_phy_pre_pll_configure);
+
+/**
+ * inno_hdmi_phy_pre_pll_power_down - power the pre-PLL down or up
+ * @pll: pre-PLL instance
+ * @power_down: true to power down, false to power up
+ */
+void inno_hdmi_phy_pre_pll_power_down(const struct inno_hdmi_phy_pre_pll *pll,
+				      bool power_down)
+{
+	inno_update_bits(pll, INNO_PRE_PLL_CONTROL, INNO_PRE_PLL_POWER_DOWN,
+			 power_down ? INNO_PRE_PLL_POWER_DOWN : 0);
+}
+EXPORT_SYMBOL_GPL(inno_hdmi_phy_pre_pll_power_down);
+
+/**
+ * inno_hdmi_phy_pre_pll_is_locked - report whether the pre-PLL has locked
+ * @pll: pre-PLL instance
+ *
+ * Return: true if the pre-PLL has locked, false if it has not or if the lock
+ * status could not be read.
+ */
+bool inno_hdmi_phy_pre_pll_is_locked(const struct inno_hdmi_phy_pre_pll *pll)
+{
+	unsigned int val;
+	int ret;
+
+	ret = inno_read(pll, INNO_PRE_PLL_LOCK_STATUS, &val);
+	if (ret)
+		return false;
+
+	return val & INNO_PRE_PLL_LOCK;
+}
+EXPORT_SYMBOL_GPL(inno_hdmi_phy_pre_pll_is_locked);
+
+/**
+ * inno_hdmi_phy_pre_pll_wait_locked - wait for the pre-PLL to lock
+ * @pll: pre-PLL instance
+ * @timeout_us: how long to wait, in microseconds
+ *
+ * Return: 0 once locked, -ETIMEDOUT otherwise.
+ */
+int inno_hdmi_phy_pre_pll_wait_locked(const struct inno_hdmi_phy_pre_pll *pll,
+				      unsigned int timeout_us)
+{
+	unsigned int val;
+
+	return regmap_read_poll_timeout(pll->regmap,
+					inno_reg(pll, INNO_PRE_PLL_LOCK_STATUS),
+					val, val & INNO_PRE_PLL_LOCK,
+					1000, timeout_us);
+}
+EXPORT_SYMBOL_GPL(inno_hdmi_phy_pre_pll_wait_locked);
+
+MODULE_DESCRIPTION("Innosilicon HDMI PHY common helpers");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/phy/inno-hdmi-phy.h b/include/linux/phy/inno-hdmi-phy.h
new file mode 100644
index 0000000000000000000000000000000000000000..3fe0d965da1f32fc2ac8f871167c0c17045f4642
--- /dev/null
+++ b/include/linux/phy/inno-hdmi-phy.h
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Common helpers for the Innosilicon HDMI PHY.
+ *
+ * Copyright (c) 2017 Rockchip Electronics Co. Ltd.
+ * Copyright (c) 2025 Samsung Electronics Co., Ltd.
+ *
+ * Author: Zheng Yang <zhengyang@rock-chips.com>
+ * Author: Michal Wilczynski <m.wilczynski@samsung.com>
+ *
+ * Derived from drivers/phy/rockchip/phy-rockchip-inno-hdmi.c.
+ */
+
+#ifndef __PHY_INNO_HDMI_H__
+#define __PHY_INNO_HDMI_H__
+
+#include <linux/types.h>
+
+struct clk_rate_request;
+struct regmap;
+
+/**
+ * struct inno_hdmi_phy_pre_pll_config - pre-PLL settings for one pixel clock
+ * @pixclock: pixel clock this entry describes, in Hz
+ * @tmdsclock: TMDS clock this entry describes, in Hz
+ * @prediv: pre-PLL reference divider
+ * @fbdiv: pre-PLL feedback divider
+ * @tmds_div_a: TMDS clock divider A
+ * @tmds_div_b: TMDS clock divider B
+ * @tmds_div_c: TMDS clock divider C
+ * @pclk_div_a: pixel clock divider A
+ * @pclk_div_b: pixel clock divider B
+ * @pclk_div_c: pixel clock divider C
+ * @pclk_div_d: pixel clock divider D
+ * @vco_div_5_en: divide the VCO output by five
+ * @fracdiv: pre-PLL fractional divider, zero to use integer mode
+ */
+struct inno_hdmi_phy_pre_pll_config {
+	unsigned long pixclock;
+	unsigned long tmdsclock;
+	u8 prediv;
+	u16 fbdiv;
+	u8 tmds_div_a;
+	u8 tmds_div_b;
+	u8 tmds_div_c;
+	u8 pclk_div_a;
+	u8 pclk_div_b;
+	u8 pclk_div_c;
+	u8 pclk_div_d;
+	u8 vco_div_5_en;
+	u32 fracdiv;
+};
+
+/**
+ * struct inno_hdmi_phy_pre_pll - a pre-PLL instance
+ * @dev: device used for log messages
+ * @regmap: register map covering the PHY registers
+ * @offset: offset of the PHY register block, in units of the register index.
+ *	    Zero where the PHY block starts the register space, 0x100 on the
+ *	    StarFive JH7110 where it is preceded by the HDMI controller.
+ * @table: pixel clock table
+ * @table_size: number of entries in @table, including any sentinel
+ */
+struct inno_hdmi_phy_pre_pll {
+	struct device *dev;
+	struct regmap *regmap;
+	unsigned int offset;
+	const struct inno_hdmi_phy_pre_pll_config *table;
+	unsigned int table_size;
+};
+
+const struct inno_hdmi_phy_pre_pll_config *
+inno_hdmi_phy_pre_pll_lookup(const struct inno_hdmi_phy_pre_pll *pll,
+			     unsigned long pixclock, unsigned long tmdsclock);
+int inno_hdmi_phy_pre_pll_determine_rate(const struct inno_hdmi_phy_pre_pll *pll,
+					 struct clk_rate_request *req);
+unsigned long
+inno_hdmi_phy_pre_pll_recalc_rate(const struct inno_hdmi_phy_pre_pll *pll,
+				  unsigned long parent_rate);
+void inno_hdmi_phy_pre_pll_configure(const struct inno_hdmi_phy_pre_pll *pll,
+				     const struct inno_hdmi_phy_pre_pll_config *cfg);
+void inno_hdmi_phy_pre_pll_power_down(const struct inno_hdmi_phy_pre_pll *pll,
+				      bool power_down);
+bool inno_hdmi_phy_pre_pll_is_locked(const struct inno_hdmi_phy_pre_pll *pll);
+int inno_hdmi_phy_pre_pll_wait_locked(const struct inno_hdmi_phy_pre_pll *pll,
+				      unsigned int timeout_us);
+
+#endif /* __PHY_INNO_HDMI_H__ */

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v6 3/5] phy: rockchip: inno-hdmi: Use the common Innosilicon PHY helpers
  2026-10-28 15:21 ` [PATCH v6 0/5] phy: Add the StarFive JH7110 Innosilicon HDMI PHY Michal Wilczynski
  2026-10-28 15:21   ` [PATCH v6 1/5] dt-bindings: phy: Add starfive,jh7110-inno-hdmi-phy Michal Wilczynski
  2026-10-28 15:21   ` [PATCH v6 2/5] phy: Add common Innosilicon HDMI PHY helpers Michal Wilczynski
@ 2026-10-28 15:21   ` Michal Wilczynski
  2026-10-28 15:21   ` [PATCH v6 4/5] phy: starfive: Add jh7110-inno-hdmi-phy driver Michal Wilczynski
  2026-10-28 15:21   ` [PATCH v6 5/5] MAINTAINERS: Add StarFive JH7110 HDMI PHY entry Michal Wilczynski
  4 siblings, 0 replies; 8+ messages in thread
From: Michal Wilczynski @ 2026-10-28 15:21 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
	Emil Renner Berthing, Hal Feng, Dominique Belhachemi,
	Michal Wilczynski, Andy Yan
  Cc: linux-phy, devicetree, linux-kernel, linux-arm-kernel,
	linux-rockchip, linux-riscv, Marek Szyprowski, Icenowy Zheng,
	Chaoyi Chen, Joshua Peisach, Byron Stanoszek

The RK3328 pre-PLL programming, its table lookup and its recalc_rate and
determine_rate implementations are the generic Innosilicon ones, so drop
the local copies and use the shared helpers instead.

The RK3228 pre-PLL sits at different register addresses, so it keeps its
own register level code and only shares the table lookup.

The now unused RK3328 pre-PLL register macros go with it, as does the
local pre-PLL config lookup wrapper; both call sites already have the
TMDS clock to hand and call inno_hdmi_phy_pre_pll_lookup() directly.

The RK3328 recalc_rate used to log the pre-PLL output as "vco". The
shared helper logs it too, and reports the actual VCO frequency
alongside the rate, which is the value with the 1.4-3.2 GHz constraint.

The register writes, their order and the values written are unchanged.
No functional change intended.

Reviewed-by: Heiko Stuebner <heiko@sntech.de>
Reviewed-by: Chaoyi Chen <chaoyi.chen@rock-chips.com>
Tested-by: Byron Stanoszek <gandalf@winds.org>
Signed-off-by: Michal Wilczynski <m.wilczynski@samsung.com>
---
 drivers/phy/rockchip/Kconfig                  |   1 +
 drivers/phy/rockchip/phy-rockchip-inno-hdmi.c | 169 ++++----------------------
 2 files changed, 26 insertions(+), 144 deletions(-)

diff --git a/drivers/phy/rockchip/Kconfig b/drivers/phy/rockchip/Kconfig
index 14698571b607590dc0e7a29f3b39eb9a5d4e3d25..9732c5be87717dc3ddbea1c60080952336a07af9 100644
--- a/drivers/phy/rockchip/Kconfig
+++ b/drivers/phy/rockchip/Kconfig
@@ -34,6 +34,7 @@ config PHY_ROCKCHIP_INNO_HDMI
 	depends on COMMON_CLK
 	depends on HAS_IOMEM
 	select GENERIC_PHY
+	select PHY_INNO_HDMI
 	help
 	  Enable this to support the Rockchip Innosilicon HDMI PHY.
 
diff --git a/drivers/phy/rockchip/phy-rockchip-inno-hdmi.c b/drivers/phy/rockchip/phy-rockchip-inno-hdmi.c
index c3d257a0f4a65b4c475ed7246568dd463b46ed86..809583a7030627832794ec0b08295549616f8f28 100644
--- a/drivers/phy/rockchip/phy-rockchip-inno-hdmi.c
+++ b/drivers/phy/rockchip/phy-rockchip-inno-hdmi.c
@@ -17,6 +17,7 @@
 #include <linux/of.h>
 #include <linux/platform_device.h>
 #include <linux/regmap.h>
+#include <linux/phy/inno-hdmi-phy.h>
 #include <linux/phy/phy.h>
 #include <linux/slab.h>
 
@@ -130,42 +131,14 @@
 #define RK3328_INT_VSS_AGND_ESD_DET			BIT(1)
 #define RK3328_INT_AGND_VSS_ESD_DET			BIT(0)
 /* REG: 0xa0 */
-#define RK3328_PCLK_VCO_DIV_5_MASK			BIT(1)
-#define RK3328_PCLK_VCO_DIV_5(x)			UPDATE(x, 1, 1)
 #define RK3328_PRE_PLL_POWER_DOWN			BIT(0)
 /* REG: 0xa1 */
-#define RK3328_PRE_PLL_PRE_DIV_MASK			GENMASK(5, 0)
-#define RK3328_PRE_PLL_PRE_DIV(x)			UPDATE(x, 5, 0)
 /* REG: 0xa2 */
-/* unset means center spread */
-#define RK3328_SPREAD_SPECTRUM_MOD_DOWN			BIT(7)
-#define RK3328_SPREAD_SPECTRUM_MOD_DISABLE		BIT(6)
-#define RK3328_PRE_PLL_FRAC_DIV_DISABLE			UPDATE(3, 5, 4)
-#define RK3328_PRE_PLL_FB_DIV_11_8_MASK			GENMASK(3, 0)
-#define RK3328_PRE_PLL_FB_DIV_11_8(x)			UPDATE((x) >> 8, 3, 0)
 /* REG: 0xa3 */
-#define RK3328_PRE_PLL_FB_DIV_7_0(x)			UPDATE(x, 7, 0)
 /* REG: 0xa4*/
-#define RK3328_PRE_PLL_TMDSCLK_DIV_C_MASK		GENMASK(1, 0)
-#define RK3328_PRE_PLL_TMDSCLK_DIV_C(x)			UPDATE(x, 1, 0)
-#define RK3328_PRE_PLL_TMDSCLK_DIV_B_MASK		GENMASK(3, 2)
-#define RK3328_PRE_PLL_TMDSCLK_DIV_B(x)			UPDATE(x, 3, 2)
-#define RK3328_PRE_PLL_TMDSCLK_DIV_A_MASK		GENMASK(5, 4)
-#define RK3328_PRE_PLL_TMDSCLK_DIV_A(x)			UPDATE(x, 5, 4)
 /* REG: 0xa5 */
-#define RK3328_PRE_PLL_PCLK_DIV_B_SHIFT			5
-#define RK3328_PRE_PLL_PCLK_DIV_B_MASK			GENMASK(6, 5)
-#define RK3328_PRE_PLL_PCLK_DIV_B(x)			UPDATE(x, 6, 5)
-#define RK3328_PRE_PLL_PCLK_DIV_A_MASK			GENMASK(4, 0)
-#define RK3328_PRE_PLL_PCLK_DIV_A(x)			UPDATE(x, 4, 0)
 /* REG: 0xa6 */
-#define RK3328_PRE_PLL_PCLK_DIV_C_SHIFT			5
-#define RK3328_PRE_PLL_PCLK_DIV_C_MASK			GENMASK(6, 5)
-#define RK3328_PRE_PLL_PCLK_DIV_C(x)			UPDATE(x, 6, 5)
-#define RK3328_PRE_PLL_PCLK_DIV_D_MASK			GENMASK(4, 0)
-#define RK3328_PRE_PLL_PCLK_DIV_D(x)			UPDATE(x, 4, 0)
 /* REG: 0xa9 */
-#define RK3328_PRE_PLL_LOCK_STATUS			BIT(0)
 /* REG: 0xaa */
 #define RK3328_POST_PLL_POST_DIV_ENABLE			GENMASK(3, 2)
 #define RK3328_POST_PLL_REFCLK_SEL_TMDS			BIT(1)
@@ -218,11 +191,8 @@
 #define RK3328_TMDS_TERM_RESIST_1000			BIT(1)
 #define RK3328_TMDS_TERM_RESIST_2000			BIT(0)
 /* REG: 0xd1 */
-#define RK3328_PRE_PLL_FRAC_DIV_23_16(x)		UPDATE((x) >> 16, 7, 0)
 /* REG: 0xd2 */
-#define RK3328_PRE_PLL_FRAC_DIV_15_8(x)			UPDATE((x) >> 8, 7, 0)
 /* REG: 0xd3 */
-#define RK3328_PRE_PLL_FRAC_DIV_7_0(x)			UPDATE(x, 7, 0)
 
 struct inno_hdmi_phy_drv_data;
 
@@ -240,6 +210,8 @@ struct inno_hdmi_phy {
 	const struct inno_hdmi_phy_drv_data *plat_data;
 	int chip_version;
 
+	struct inno_hdmi_phy_pre_pll pre_pll;
+
 	/* clk provider */
 	struct clk_hw hw;
 	struct clk *phyclk;
@@ -248,21 +220,6 @@ struct inno_hdmi_phy {
 	unsigned long opts_tmds_char_rate;
 };
 
-struct pre_pll_config {
-	unsigned long pixclock;
-	unsigned long tmdsclock;
-	u8 prediv;
-	u16 fbdiv;
-	u8 tmds_div_a;
-	u8 tmds_div_b;
-	u8 tmds_div_c;
-	u8 pclk_div_a;
-	u8 pclk_div_b;
-	u8 pclk_div_c;
-	u8 pclk_div_d;
-	u8 vco_div_5_en;
-	u32 fracdiv;
-};
 
 struct post_pll_config {
 	unsigned long tmdsclock;
@@ -291,7 +248,7 @@ struct inno_hdmi_phy_drv_data {
 	const struct phy_config		*phy_cfg_table;
 };
 
-static const struct pre_pll_config pre_pll_cfg_table[] = {
+static const struct inno_hdmi_phy_pre_pll_config pre_pll_cfg_table[] = {
 	{ 25175000,  25175000,  3,  125, 3, 1, 1,  1, 3, 3,  4, 0, 0xe00000},
 	{ 25175000,  31468750,  1,   41, 0, 3, 3,  1, 3, 3,  4, 0, 0xf5554f},
 	{ 27000000,  27000000,  1,   36, 0, 3, 3,  1, 2, 3,  4, 0,      0x0},
@@ -603,7 +560,7 @@ static irqreturn_t inno_hdmi_phy_rk3328_irq(int irq, void *dev_id)
 static int inno_hdmi_phy_validate(struct phy *phy, enum phy_mode mode,
 				  int submode, union phy_configure_opts *opts)
 {
-	const struct pre_pll_config *cfg = pre_pll_cfg_table;
+	const struct inno_hdmi_phy_pre_pll_config *cfg = pre_pll_cfg_table;
 	unsigned long tmdsclock;
 
 	if (!(mode == PHY_MODE_HDMI && submode == PHY_HDMI_MODE_TMDS))
@@ -717,23 +674,6 @@ static const struct phy_ops inno_hdmi_phy_ops = {
 	.validate = inno_hdmi_phy_validate,
 };
 
-static const
-struct pre_pll_config *inno_hdmi_phy_get_pre_pll_cfg(struct inno_hdmi_phy *inno,
-						     unsigned long rate)
-{
-	const struct pre_pll_config *cfg = pre_pll_cfg_table;
-	unsigned long tmdsclock = inno_hdmi_phy_get_tmdsclk(inno, rate);
-
-	for (; cfg->pixclock != 0; cfg++)
-		if (cfg->pixclock == rate && cfg->tmdsclock == tmdsclock)
-			break;
-
-	if (cfg->pixclock == 0)
-		return ERR_PTR(-EINVAL);
-
-	return cfg;
-}
-
 static int inno_hdmi_phy_rk3228_clk_is_prepared(struct clk_hw *hw)
 {
 	struct inno_hdmi_phy *inno = to_inno_hdmi_phy(hw);
@@ -797,7 +737,7 @@ unsigned long inno_hdmi_phy_rk3228_clk_recalc_rate(struct clk_hw *hw,
 static int inno_hdmi_phy_rk3228_clk_determine_rate(struct clk_hw *hw,
 						   struct clk_rate_request *req)
 {
-	const struct pre_pll_config *cfg = pre_pll_cfg_table;
+	const struct inno_hdmi_phy_pre_pll_config *cfg = pre_pll_cfg_table;
 
 	req->rate = (req->rate / 1000) * 1000;
 
@@ -818,7 +758,7 @@ static int inno_hdmi_phy_rk3228_clk_set_rate(struct clk_hw *hw,
 					     unsigned long parent_rate)
 {
 	struct inno_hdmi_phy *inno = to_inno_hdmi_phy(hw);
-	const struct pre_pll_config *cfg;
+	const struct inno_hdmi_phy_pre_pll_config *cfg;
 	unsigned long tmdsclock = inno_hdmi_phy_get_tmdsclk(inno, rate);
 	u32 v;
 	int ret;
@@ -829,7 +769,7 @@ static int inno_hdmi_phy_rk3228_clk_set_rate(struct clk_hw *hw,
 	if (inno->pixclock == rate && inno->tmdsclock == tmdsclock)
 		return 0;
 
-	cfg = inno_hdmi_phy_get_pre_pll_cfg(inno, rate);
+	cfg = inno_hdmi_phy_pre_pll_lookup(&inno->pre_pll, rate, tmdsclock);
 	if (IS_ERR(cfg))
 		return PTR_ERR(cfg);
 
@@ -915,39 +855,9 @@ unsigned long inno_hdmi_phy_rk3328_clk_recalc_rate(struct clk_hw *hw,
 						   unsigned long parent_rate)
 {
 	struct inno_hdmi_phy *inno = to_inno_hdmi_phy(hw);
-	unsigned long frac;
-	u8 nd, no_a, no_b, no_d;
-	u64 vco;
-	u16 nf;
-
-	nd = inno_read(inno, 0xa1) & RK3328_PRE_PLL_PRE_DIV_MASK;
-	nf = ((inno_read(inno, 0xa2) & RK3328_PRE_PLL_FB_DIV_11_8_MASK) << 8);
-	nf |= inno_read(inno, 0xa3);
-	vco = parent_rate * nf;
-
-	if (!(inno_read(inno, 0xa2) & RK3328_PRE_PLL_FRAC_DIV_DISABLE)) {
-		frac = inno_read(inno, 0xd3) |
-		       (inno_read(inno, 0xd2) << 8) |
-		       (inno_read(inno, 0xd1) << 16);
-		vco += DIV_ROUND_CLOSEST(parent_rate * frac, (1 << 24));
-	}
-
-	if (inno_read(inno, 0xa0) & RK3328_PCLK_VCO_DIV_5_MASK) {
-		do_div(vco, nd * 5);
-	} else {
-		no_a = inno_read(inno, 0xa5) & RK3328_PRE_PLL_PCLK_DIV_A_MASK;
-		no_b = inno_read(inno, 0xa5) & RK3328_PRE_PLL_PCLK_DIV_B_MASK;
-		no_b >>= RK3328_PRE_PLL_PCLK_DIV_B_SHIFT;
-		no_b += 2;
-		no_d = inno_read(inno, 0xa6) & RK3328_PRE_PLL_PCLK_DIV_D_MASK;
-
-		do_div(vco, (nd * (no_a == 1 ? no_b : no_a) * no_d * 2));
-	}
 
-	inno->pixclock = DIV_ROUND_CLOSEST((unsigned long)vco, 1000) * 1000;
-
-	dev_dbg(inno->dev, "%s rate %lu vco %llu\n",
-		__func__, inno->pixclock, vco);
+	inno->pixclock = inno_hdmi_phy_pre_pll_recalc_rate(&inno->pre_pll,
+							   parent_rate);
 
 	return inno->pixclock;
 }
@@ -955,20 +865,9 @@ unsigned long inno_hdmi_phy_rk3328_clk_recalc_rate(struct clk_hw *hw,
 static int inno_hdmi_phy_rk3328_clk_determine_rate(struct clk_hw *hw,
 						   struct clk_rate_request *req)
 {
-	const struct pre_pll_config *cfg = pre_pll_cfg_table;
-
-	req->rate = (req->rate / 1000) * 1000;
-
-	for (; cfg->pixclock != 0; cfg++)
-		if (cfg->pixclock == req->rate)
-			break;
-
-	if (cfg->pixclock == 0)
-		return -EINVAL;
-
-	req->rate = cfg->pixclock;
+	struct inno_hdmi_phy *inno = to_inno_hdmi_phy(hw);
 
-	return 0;
+	return inno_hdmi_phy_pre_pll_determine_rate(&inno->pre_pll, req);
 }
 
 static int inno_hdmi_phy_rk3328_clk_set_rate(struct clk_hw *hw,
@@ -976,9 +875,8 @@ static int inno_hdmi_phy_rk3328_clk_set_rate(struct clk_hw *hw,
 					     unsigned long parent_rate)
 {
 	struct inno_hdmi_phy *inno = to_inno_hdmi_phy(hw);
-	const struct pre_pll_config *cfg;
+	const struct inno_hdmi_phy_pre_pll_config *cfg;
 	unsigned long tmdsclock = inno_hdmi_phy_get_tmdsclk(inno, rate);
-	u32 val;
 	int ret;
 
 	dev_dbg(inno->dev, "%s rate %lu tmdsclk %lu\n",
@@ -987,39 +885,15 @@ static int inno_hdmi_phy_rk3328_clk_set_rate(struct clk_hw *hw,
 	if (inno->pixclock == rate && inno->tmdsclock == tmdsclock)
 		return 0;
 
-	cfg = inno_hdmi_phy_get_pre_pll_cfg(inno, rate);
+	cfg = inno_hdmi_phy_pre_pll_lookup(&inno->pre_pll, rate, tmdsclock);
 	if (IS_ERR(cfg))
 		return PTR_ERR(cfg);
 
-	inno_update_bits(inno, 0xa0, RK3328_PRE_PLL_POWER_DOWN,
-			 RK3328_PRE_PLL_POWER_DOWN);
+	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, true);
+	inno_hdmi_phy_pre_pll_configure(&inno->pre_pll, cfg);
+	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, false);
 
-	/* Configure pre-pll */
-	inno_update_bits(inno, 0xa0, RK3328_PCLK_VCO_DIV_5_MASK,
-			 RK3328_PCLK_VCO_DIV_5(cfg->vco_div_5_en));
-	inno_write(inno, 0xa1, RK3328_PRE_PLL_PRE_DIV(cfg->prediv));
-
-	val = RK3328_SPREAD_SPECTRUM_MOD_DISABLE;
-	if (!cfg->fracdiv)
-		val |= RK3328_PRE_PLL_FRAC_DIV_DISABLE;
-	inno_write(inno, 0xa2, RK3328_PRE_PLL_FB_DIV_11_8(cfg->fbdiv) | val);
-	inno_write(inno, 0xa3, RK3328_PRE_PLL_FB_DIV_7_0(cfg->fbdiv));
-	inno_write(inno, 0xa5, RK3328_PRE_PLL_PCLK_DIV_A(cfg->pclk_div_a) |
-		   RK3328_PRE_PLL_PCLK_DIV_B(cfg->pclk_div_b));
-	inno_write(inno, 0xa6, RK3328_PRE_PLL_PCLK_DIV_C(cfg->pclk_div_c) |
-		   RK3328_PRE_PLL_PCLK_DIV_D(cfg->pclk_div_d));
-	inno_write(inno, 0xa4, RK3328_PRE_PLL_TMDSCLK_DIV_C(cfg->tmds_div_c) |
-		   RK3328_PRE_PLL_TMDSCLK_DIV_A(cfg->tmds_div_a) |
-		   RK3328_PRE_PLL_TMDSCLK_DIV_B(cfg->tmds_div_b));
-	inno_write(inno, 0xd3, RK3328_PRE_PLL_FRAC_DIV_7_0(cfg->fracdiv));
-	inno_write(inno, 0xd2, RK3328_PRE_PLL_FRAC_DIV_15_8(cfg->fracdiv));
-	inno_write(inno, 0xd1, RK3328_PRE_PLL_FRAC_DIV_23_16(cfg->fracdiv));
-
-	inno_update_bits(inno, 0xa0, RK3328_PRE_PLL_POWER_DOWN, 0);
-
-	/* Wait for Pre-PLL lock */
-	ret = inno_poll(inno, 0xa9, val, val & RK3328_PRE_PLL_LOCK_STATUS,
-			1000, 10000);
+	ret = inno_hdmi_phy_pre_pll_wait_locked(&inno->pre_pll, 10000);
 	if (ret) {
 		dev_err(inno->dev, "Pre-PLL locking failed\n");
 		return ret;
@@ -1418,6 +1292,13 @@ static int inno_hdmi_phy_probe(struct platform_device *pdev)
 	if (IS_ERR(inno->regmap))
 		return PTR_ERR(inno->regmap);
 
+	/* The PHY block starts the register space on these SoCs. */
+	inno->pre_pll.dev = inno->dev;
+	inno->pre_pll.regmap = inno->regmap;
+	inno->pre_pll.offset = 0;
+	inno->pre_pll.table = pre_pll_cfg_table;
+	inno->pre_pll.table_size = ARRAY_SIZE(pre_pll_cfg_table);
+
 	/* only the newer rk3328 hdmiphy has an interrupt */
 	inno->irq = platform_get_irq(pdev, 0);
 	if (inno->irq > 0) {

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v6 4/5] phy: starfive: Add jh7110-inno-hdmi-phy driver
  2026-10-28 15:21 ` [PATCH v6 0/5] phy: Add the StarFive JH7110 Innosilicon HDMI PHY Michal Wilczynski
                     ` (2 preceding siblings ...)
  2026-10-28 15:21   ` [PATCH v6 3/5] phy: rockchip: inno-hdmi: Use the common Innosilicon " Michal Wilczynski
@ 2026-10-28 15:21   ` Michal Wilczynski
  2026-10-28 15:46     ` sashiko-bot
  2026-10-28 15:21   ` [PATCH v6 5/5] MAINTAINERS: Add StarFive JH7110 HDMI PHY entry Michal Wilczynski
  4 siblings, 1 reply; 8+ messages in thread
From: Michal Wilczynski @ 2026-10-28 15:21 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
	Emil Renner Berthing, Hal Feng, Dominique Belhachemi,
	Michal Wilczynski, Andy Yan
  Cc: linux-phy, devicetree, linux-kernel, linux-arm-kernel,
	linux-rockchip, linux-riscv, Marek Szyprowski, Icenowy Zheng,
	Chaoyi Chen, Joshua Peisach, Byron Stanoszek, OOHehir

Add the HDMI PHY driver for the StarFive JH7110.

This driver binds to the starfive,jh7110-inno-hdmi-phy node and
gets its regmap from the parent. Its only clock input is the reference
oscillator (xin24m); it does not depend on voutcrg.

This driver provides two main functions:
 - Clock Provider: It registers clk_ops to provide the variable pixel
   clock. The .set_rate operation configures the Pre-PLL registers
   (0x1a0+) based on the requested rate.

 - PHY Provider: It registers phy_ops for the controller. The .power_on
   op configures and enables the Post-PLL and other analog blocks (BIAS,
   LDO, Serializer, etc.).

The clock ops reach registers in the window shared with the HDMI
controller, and access to that window is gated by the controller's
system clock, which only the sibling controller driver holds. The clock
framework walks every registered clock regardless of which drivers have
bound, so deliberately do not implement .is_prepared, which lets the
framework fall back to the software prepare count, and return the cached
rate from .recalc_rate whenever the clock is not prepared. Without this,
clk_disable_unused() reads a PHY register with that clock gated and the
resulting stalled bus transaction wedges the CPU hard enough that it
stops responding to NMIs. It is reproducible by building the bridge
driver as a module so that the controller has not bound by then.

The JH7110 instantiates the same Innosilicon IP as the Rockchip RK3328,
with the PHY register block shifted by 0x100 because it sits behind the
HDMI controller in the shared register space. The pre-PLL programming
therefore comes from the common Innosilicon helpers; this driver adds
the JH7110 pixel clock table, the post-PLL and the analog configuration.

Co-developed-by: Dominique Belhachemi <db@domibel.de>
Signed-off-by: Dominique Belhachemi <db@domibel.de>
Tested-by: Byron Stanoszek <gandalf@winds.org>
Tested-by: OOHehir <oo.hehir@gmail.com>
Signed-off-by: Michal Wilczynski <m.wilczynski@samsung.com>
---
 drivers/phy/starfive/Kconfig                |  20 +
 drivers/phy/starfive/Makefile               |   1 +
 drivers/phy/starfive/phy-jh7110-inno-hdmi.c | 587 ++++++++++++++++++++++++++++
 3 files changed, 608 insertions(+)

diff --git a/drivers/phy/starfive/Kconfig b/drivers/phy/starfive/Kconfig
index d0cdd7cb4a13de22ff643c89a79d99cce57284d7..1da417ffbf224c13224e42a3ce322e3d0c272824 100644
--- a/drivers/phy/starfive/Kconfig
+++ b/drivers/phy/starfive/Kconfig
@@ -25,6 +25,26 @@ config PHY_STARFIVE_JH7110_DPHY_TX
 	  system. If M is selected, the module will be called
 	  phy-jh7110-dphy-tx.ko.
 
+config PHY_STARFIVE_JH7110_INNO_HDMI
+	tristate "Starfive JH7110 INNO HDMI PHY"
+	select PHY_INNO_HDMI
+	depends on COMMON_CLK
+	select GENERIC_PHY
+	help
+	  This option enables the driver for the analog HDMI PHY (Physical
+	  Layer) on the StarFive JH7110 SoC.
+
+	  This driver binds to a child node of the 'starfive,jh7110-hdmi-subsystem'
+	  parent driver and gets its register map from that parent.
+
+	  It is responsible for two main functions:
+	  1. PHY Provider: It provides standard PHY operations (.power_on,
+	     .power_off) for the HDMI controller (bridge) driver. This
+	     involves configuring the Post-PLL and analog TMDS blocks.
+	  2. Clock Provider: It registers as a clock provider to supply the
+	     variable pixel clock (hdmi_pclk) to the HDMI controller and
+	     the VOUT subsystem, which it generates using the Pre-PLL.
+
 config PHY_STARFIVE_JH7110_PCIE
 	tristate "Starfive JH7110 PCIE 2.0/USB 3.0 PHY support"
 	depends on HAS_IOMEM
diff --git a/drivers/phy/starfive/Makefile b/drivers/phy/starfive/Makefile
index eedc4a6fec156320c99ac0a0da609083b6a6a695..e7b13f00880b500f933f21b6037384d5c6884e3e 100644
--- a/drivers/phy/starfive/Makefile
+++ b/drivers/phy/starfive/Makefile
@@ -1,5 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0
 obj-$(CONFIG_PHY_STARFIVE_JH7110_DPHY_RX)	+= phy-jh7110-dphy-rx.o
 obj-$(CONFIG_PHY_STARFIVE_JH7110_DPHY_TX)	+= phy-jh7110-dphy-tx.o
+obj-$(CONFIG_PHY_STARFIVE_JH7110_INNO_HDMI)	+= phy-jh7110-inno-hdmi.o
 obj-$(CONFIG_PHY_STARFIVE_JH7110_PCIE)		+= phy-jh7110-pcie.o
 obj-$(CONFIG_PHY_STARFIVE_JH7110_USB)		+= phy-jh7110-usb.o
diff --git a/drivers/phy/starfive/phy-jh7110-inno-hdmi.c b/drivers/phy/starfive/phy-jh7110-inno-hdmi.c
new file mode 100644
index 0000000000000000000000000000000000000000..ebda363e50a3e3b5e01629590e68fae4aae4ae32
--- /dev/null
+++ b/drivers/phy/starfive/phy-jh7110-inno-hdmi.c
@@ -0,0 +1,587 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2017 Rockchip Electronics Co. Ltd.
+ * Copyright (c) 2025 Samsung Electronics Co., Ltd.
+ *
+ * Author: Zheng Yang <zhengyang@rock-chips.com>
+ * Author: Michal Wilczynski <m.wilczynski@samsung.com>
+ *
+ * The register layout and programming sequence derive from
+ * drivers/phy/rockchip/phy-rockchip-inno-hdmi.c; the JH7110 places the
+ * same Innosilicon PHY block at a 0x100 register offset.
+ *
+ * This driver handles the PHY portion of the StarFive Innosilicon HDMI IP,
+ * which is part of a monolithic HDMI block. It provides the variable pixel
+ * clock (from the Pre-PLL) and the PHY operations (for the Post-PLL/analog).
+ */
+
+#include <linux/array_size.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/delay.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/math64.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/phy/inno-hdmi-phy.h>
+#include <linux/phy/phy.h>
+#include <linux/slab.h>
+
+/*
+ * StarFive (JH7110) Innosilicon HDMI PHY Register Definitions
+ */
+
+/* REG: 0x1aa */
+#define STF_INNO_POST_PLL_DIV_1		0x1aa
+#define STF_INNO_POST_PLL_POST_DIV_ENABLE	GENMASK(3, 2)
+#define STF_INNO_POST_PLL_REFCLK_SEL_TMDS	BIT(1)
+#define STF_INNO_POST_PLL_POWER_DOWN		BIT(0)
+
+/* REG: 0x1ab */
+#define STF_INNO_POST_PLL_DIV_2		0x1ab
+#define STF_INNO_POST_PLL_PRE_DIV(x)		FIELD_PREP(GENMASK(5, 0), x)
+#define STF_INNO_POST_PLL_FB_DIV_8(x)		FIELD_PREP(BIT(7), (x) >> 8)
+
+/* REG: 0x1ac */
+#define STF_INNO_POST_PLL_DIV_3		0x1ac
+#define STF_INNO_POST_PLL_FB_DIV_7_0(x)	FIELD_PREP(GENMASK(7, 0), x)
+
+/* REG: 0x1ad */
+#define STF_INNO_POST_PLL_DIV_4		0x1ad
+#define STF_INNO_POST_PLL_POST_DIV_MASK	GENMASK(1, 0)
+
+/* REG: 0x1af */
+#define STF_INNO_POST_PLL_LOCK_STATUS		0x1af
+#define STF_INNO_POST_PLL_LOCK		BIT(0)
+
+/* REG: 0x1b0 */
+#define STF_INNO_BIAS_CONTROL			0x1b0
+#define STF_INNO_BIAS_ENABLE			BIT(2)
+
+/* REG: 0x1b2 */
+#define STF_INNO_TMDS_CONTROL			0x1b2
+#define STF_INNO_TMDS_CLK_DRIVER_EN		BIT(3)
+#define STF_INNO_TMDS_D2_DRIVER_EN		BIT(2)
+#define STF_INNO_TMDS_D1_DRIVER_EN		BIT(1)
+#define STF_INNO_TMDS_D0_DRIVER_EN		BIT(0)
+#define STF_INNO_TMDS_DRIVER_ENABLE		(STF_INNO_TMDS_CLK_DRIVER_EN | \
+						 STF_INNO_TMDS_D2_DRIVER_EN | \
+						 STF_INNO_TMDS_D1_DRIVER_EN | \
+						 STF_INNO_TMDS_D0_DRIVER_EN)
+
+/* REG: 0x1b4 */
+#define STF_INNO_LDO_CONTROL			0x1b4
+#define STF_INNO_LDO_ENABLE			(BIT(2) | BIT(1) | BIT(0))
+
+/* REG: 0x1be */
+#define STF_INNO_SERIALIER_CONTROL		0x1be
+#define STF_INNO_SERIALIER_ENABLE		(BIT(6) | BIT(5) | BIT(4) | BIT(0))
+
+/* REG: 0x1cc */
+#define STF_INNO_RX_CONTROL			0x1cc
+#define STF_INNO_RX_ENABLE			(BIT(3) | BIT(2) | BIT(1) | BIT(0))
+
+/*
+ * These tables are copied from the monolithic driver.
+ * They match the Rockchip PHY driver tables.
+ */
+
+struct post_pll_config {
+	unsigned long tmdsclock;
+	u8 prediv;
+	u16 fbdiv;
+	u8 postdiv;
+	u8 post_div_en;
+};
+
+static const struct inno_hdmi_phy_pre_pll_config pre_pll_cfg_table[] = {
+	{ 25175000, 25175000, 1, 100, 2, 3, 3, 12, 3, 3, 4, 0, 0xf55555 },
+	{ 25200000, 25200000, 1, 100, 2, 3, 3, 12, 3, 3, 4, 0, 0 },
+	{ 27000000, 27000000, 1, 90, 3, 2, 2, 10, 3, 3, 4, 0, 0 },
+	{ 27027000, 27027000, 1, 90, 3, 2, 2, 10, 3, 3, 4, 0, 0x170a3d },
+	{ 28320000, 28320000, 1, 28, 2, 1, 1, 3, 0, 3, 4, 0, 0x51eb85 },
+	{ 30240000, 30240000, 1, 30, 2, 1, 1, 3, 0, 3, 4, 0, 0x3d70a3 },
+	{ 31500000, 31500000, 1, 31, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 33750000, 33750000, 1, 33, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
+	{ 36000000, 36000000, 1, 36, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 40000000, 40000000, 1, 80, 2, 2, 2, 12, 2, 2, 2, 0, 0 },
+	{ 46970000, 46970000, 1, 46, 2, 1, 1, 3, 0, 3, 4, 0, 0xf851eb },
+	{ 49500000, 49500000, 1, 49, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 49000000, 49000000, 1, 49, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 50000000, 50000000, 1, 50, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 54000000, 54000000, 1, 54, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 54054000, 54054000, 1, 54, 2, 1, 1, 3, 0, 3, 4, 0, 0x0dd2f1 },
+	{ 57284000, 57284000, 1, 57, 2, 1, 1, 3, 0, 3, 4, 0, 0x48b439 },
+	{ 58230000, 58230000, 1, 58, 2, 1, 1, 3, 0, 3, 4, 0, 0x3ae147 },
+	{ 59341000, 59341000, 1, 59, 2, 1, 1, 3, 0, 3, 4, 0, 0x574bc6 },
+	{ 59400000, 59400000, 1, 99, 3, 1, 1, 1, 3, 3, 4, 0, 0 },
+	{ 65000000, 65000000, 1, 130, 2, 2, 2, 12, 0, 2, 2, 0, 0 },
+	{ 68250000, 68250000, 1, 68, 2, 1, 1, 3, 0, 3, 4, 0, 0x3fffff },
+	{ 71000000, 71000000, 1, 71, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 74176000, 74176000, 1, 98, 1, 2, 2, 1, 2, 3, 4, 0, 0xe6ae6b },
+	{ 74250000, 74250000, 1, 99, 1, 2, 2, 1, 2, 3, 4, 0, 0 },
+	{ 75000000, 75000000, 1, 75, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 78750000, 78750000, 1, 78, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
+	{ 79500000, 79500000, 1, 79, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 83500000, 83500000, 2, 167, 2, 1, 1, 1, 0, 0, 6, 0, 0 },
+	{ 83500000, 104375000, 1, 104, 2, 1, 1, 1, 1, 0, 5, 0, 0x600000 },
+	{ 85500000, 85500000, 1, 85, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 85750000, 85750000, 1, 85, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
+	{ 85800000, 85800000, 1, 85, 2, 1, 1, 3, 0, 3, 4, 0, 0xcccccc },
+	{ 88750000, 88750000, 1, 88, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
+	{ 89910000, 89910000, 1, 89, 2, 1, 1, 3, 0, 3, 4, 0, 0xe8f5c1 },
+	{ 90000000, 90000000, 1, 90, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 101000000, 101000000, 1, 101, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 102250000, 102250000, 1, 102, 2, 1, 1, 3, 0, 3, 4, 0, 0x3fffff },
+	{ 106500000, 106500000, 1, 106, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 108000000, 108000000, 1, 90, 3, 0, 0, 5, 0, 2, 2, 0, 0 },
+	{ 119000000, 119000000, 1, 119, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 131481000, 131481000, 1, 131, 2, 1, 1, 3, 0, 3, 4, 0, 0x7b22d1 },
+	{ 135000000, 135000000, 1, 135, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 136750000, 136750000, 1, 136, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
+	{ 147180000, 147180000, 1, 147, 2, 1, 1, 3, 0, 3, 4, 0, 0x2e147a },
+	{ 148352000, 148352000, 1, 98, 1, 1, 1, 1, 2, 2, 2, 0, 0xe6ae6b },
+	{ 148500000, 148500000, 1, 99, 1, 1, 1, 1, 2, 2, 2, 0, 0 },
+	{ 154000000, 154000000, 1, 154, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 156000000, 156000000, 1, 156, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 157000000, 157000000, 1, 157, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 162000000, 162000000, 1, 162, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 174250000, 174250000, 1, 145, 3, 0, 0, 5, 0, 2, 2, 0, 0x355555 },
+	{ 174500000, 174500000, 1, 174, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 174570000, 174570000, 1, 174, 2, 1, 1, 3, 0, 3, 4, 0, 0x91eb84 },
+	{ 175500000, 175500000, 1, 175, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 185590000, 185590000, 1, 185, 2, 1, 1, 3, 0, 3, 4, 0, 0x970a3c },
+	{ 187000000, 187000000, 1, 187, 2, 1, 1, 3, 0, 3, 4, 0, 0 },
+	{ 201000000, 201000000, 1, 134, 1, 1, 1, 1, 2, 2, 2, 0, 0 },
+	{ 235690000, 235690000, 1, 235, 2, 1, 1, 3, 0, 3, 4, 0, 0xb0a3d6 },
+	{ 241500000, 241500000, 1, 161, 1, 1, 1, 4, 0, 2, 2, 0, 0 },
+	{ 241700000, 241700000, 1, 241, 2, 1, 1, 3, 0, 3, 4, 0, 0xb33332 },
+	{ 262750000, 262750000, 1, 262, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
+	{ 296500000, 296500000, 1, 296, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
+	{ 296703000, 296703000, 1, 98, 0, 1, 1, 1, 0, 2, 2, 0, 0xe6ae6b },
+	{ 297000000, 297000000, 1, 99, 0, 1, 1, 1, 0, 2, 2, 0, 0 },
+	{ 594000000, 594000000, 1, 99, 0, 2, 0, 1, 0, 1, 1, 0, 0 },
+	{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
+};
+
+static const struct post_pll_config post_pll_cfg_table[] = {
+	{ 25200000, 1, 80, 13, 3 },
+	{ 27000000, 1, 40, 11, 3 },
+	{ 27027000, 1, 40, 11, 3 },
+	{ 33750000, 1, 40, 11, 3 },
+	{ 49000000, 1, 20, 1, 3 },
+	{ 65000000, 1, 20, 1, 3 },
+	{ 74250000, 1, 20, 1, 3 },
+	{ 88750000, 1, 20, 1, 3 },
+	{ 108000000, 1, 20, 1, 3 },
+	{ 148500000, 1, 20, 1, 3 },
+	{ 162000000, 1, 20, 1, 3 },
+	{ 174250000, 1, 20, 1, 3 },
+	{ 187000000, 1, 20, 1, 3 },
+	{ 241700000, 1, 20, 1, 3 },
+	{ 297000000, 4, 20, 0, 0 },
+	{ 594000000, 4, 20, 0, 0 }, /* postpll_postdiv_en = 0 */
+};
+
+struct starfive_hdmi_phy {
+	struct device *dev;
+	struct regmap *regmap;
+	struct phy *phy;
+	struct clk *refoclk;
+
+	struct inno_hdmi_phy_pre_pll pre_pll;
+
+	struct clk_hw hw;
+	struct clk *phyclk;
+	unsigned long pixclock;
+	unsigned long tmdsclock;
+};
+
+static inline void inno_write(struct starfive_hdmi_phy *inno, u32 reg, u8 val)
+{
+	regmap_write(inno->regmap, reg * 4, val);
+}
+
+static inline u8 inno_read(struct starfive_hdmi_phy *inno, u32 reg)
+{
+	u32 val;
+
+	regmap_read(inno->regmap, reg * 4, &val);
+	return val;
+}
+
+static inline void inno_update_bits(struct starfive_hdmi_phy *inno, u16 reg,
+				    u8 mask, u8 val)
+{
+	regmap_update_bits(inno->regmap, reg * 4, mask, val);
+}
+
+#define inno_poll(inno, reg, val, cond, sleep_us, timeout_us) \
+	regmap_read_poll_timeout((inno)->regmap, (reg) * 4, val, cond, \
+				 sleep_us, timeout_us)
+
+static inline struct starfive_hdmi_phy *to_starfive_hdmi_phy(struct clk_hw *hw)
+{
+	return container_of(hw, struct starfive_hdmi_phy, hw);
+}
+
+static int starfive_hdmi_phy_clk_prepare(struct clk_hw *hw)
+{
+	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
+	int ret;
+
+	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, false);
+
+	if (inno_hdmi_phy_pre_pll_is_locked(&inno->pre_pll))
+		return 0;
+
+	ret = inno_hdmi_phy_pre_pll_wait_locked(&inno->pre_pll, 100000);
+	if (ret) {
+		dev_err(inno->dev, "Timeout waiting for pre-PLL lock\n");
+		inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, true);
+		return ret;
+	}
+
+	return 0;
+}
+
+static void starfive_hdmi_phy_clk_unprepare(struct clk_hw *hw)
+{
+	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
+
+	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, true);
+	inno->pixclock = 0; /* Invalidate cached rate */
+}
+
+static unsigned long starfive_hdmi_phy_clk_recalc_rate(struct clk_hw *hw,
+						       unsigned long parent_rate)
+{
+	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
+
+	/*
+	 * Reading the PLL back requires the HDMI controller's system clock,
+	 * which the sibling controller driver holds and this driver cannot
+	 * request without creating a probe cycle with voutcrg. The clock
+	 * framework walks every registered clock regardless of which drivers
+	 * have bound, so fall back to the cached rate whenever this clock is
+	 * not prepared; an unclocked access to the shared register window
+	 * wedges the bus hard enough that the CPU stops taking NMIs.
+	 */
+	if (!clk_hw_is_prepared(hw))
+		return inno->pixclock;
+
+	inno->pixclock = inno_hdmi_phy_pre_pll_recalc_rate(&inno->pre_pll,
+							   parent_rate);
+
+	return inno->pixclock;
+}
+
+static int starfive_hdmi_phy_clk_determine_rate(struct clk_hw *hw,
+						struct clk_rate_request *req)
+{
+	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
+
+	return inno_hdmi_phy_pre_pll_determine_rate(&inno->pre_pll, req);
+}
+
+static int starfive_hdmi_phy_clk_set_rate(struct clk_hw *hw, unsigned long rate,
+					  unsigned long parent_rate)
+{
+	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
+	const struct inno_hdmi_phy_pre_pll_config *cfg;
+
+	/*
+	 * The JH7110 only drives 8bpc, so the TMDS clock always matches the
+	 * pixel clock.
+	 */
+	cfg = inno_hdmi_phy_pre_pll_lookup(&inno->pre_pll, rate, rate);
+	if (IS_ERR(cfg))
+		return PTR_ERR(cfg);
+
+	dev_dbg(inno->dev, "%s rate %lu tmdsclk %lu\n",
+		__func__, rate, cfg->tmdsclock);
+
+	if (inno->pixclock == rate && inno->tmdsclock == cfg->tmdsclock)
+		return 0;
+
+	inno_update_bits(inno, STF_INNO_BIAS_CONTROL,
+			 STF_INNO_BIAS_ENABLE, STF_INNO_BIAS_ENABLE);
+	inno_write(inno, STF_INNO_RX_CONTROL, STF_INNO_RX_ENABLE);
+
+	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, true);
+	inno_hdmi_phy_pre_pll_configure(&inno->pre_pll, cfg);
+	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, false);
+
+	inno->pixclock = rate;
+	inno->tmdsclock = cfg->tmdsclock;
+
+	return 0;
+}
+
+static const struct clk_ops starfive_hdmi_phy_clk_ops = {
+	.prepare = starfive_hdmi_phy_clk_prepare,
+	.unprepare = starfive_hdmi_phy_clk_unprepare,
+	.recalc_rate = starfive_hdmi_phy_clk_recalc_rate,
+	.determine_rate = starfive_hdmi_phy_clk_determine_rate,
+	.set_rate = starfive_hdmi_phy_clk_set_rate,
+};
+
+static void starfive_hdmi_phy_power_down(struct starfive_hdmi_phy *inno)
+{
+	inno_write(inno, STF_INNO_TMDS_CONTROL, 0x00);
+	inno_write(inno, STF_INNO_SERIALIER_CONTROL, 0x00);
+	inno_write(inno, STF_INNO_LDO_CONTROL, 0x00);
+	inno_update_bits(inno, STF_INNO_BIAS_CONTROL,
+			 STF_INNO_BIAS_ENABLE, 0x00);
+	inno_write(inno, STF_INNO_RX_CONTROL, 0x00);
+
+	/* Power down Post-PLL */
+	inno_update_bits(inno, STF_INNO_POST_PLL_DIV_1,
+			 STF_INNO_POST_PLL_POWER_DOWN,
+			 STF_INNO_POST_PLL_POWER_DOWN);
+
+	inno->tmdsclock = 0;
+	inno->pixclock = 0;
+}
+
+static int starfive_hdmi_phy_power_on(struct phy *phy)
+{
+	struct starfive_hdmi_phy *inno = phy_get_drvdata(phy);
+	const struct post_pll_config *cfg = NULL;
+	const struct inno_hdmi_phy_pre_pll_config *pre_cfg;
+	unsigned long tmdsclock;
+	u8 reg_1ad_value;
+	u8 reg_1aa_value;
+	unsigned int i;
+
+	u32 v;
+	int ret;
+
+	tmdsclock = clk_get_rate(inno->phyclk);
+	pre_cfg = inno_hdmi_phy_pre_pll_lookup(&inno->pre_pll, tmdsclock,
+					       tmdsclock);
+	if (IS_ERR(pre_cfg)) {
+		dev_err(inno->dev, "no pre-PLL config for current pixel clock\n");
+		return PTR_ERR(pre_cfg);
+	}
+	tmdsclock = pre_cfg->tmdsclock;
+	inno->tmdsclock = tmdsclock;
+
+	inno_update_bits(inno, STF_INNO_BIAS_CONTROL,
+			 STF_INNO_BIAS_ENABLE, STF_INNO_BIAS_ENABLE);
+	inno_write(inno, STF_INNO_RX_CONTROL, STF_INNO_RX_ENABLE);
+
+	/* Find Post-PLL config */
+	for (i = 0; i < ARRAY_SIZE(post_pll_cfg_table); i++) {
+		if (tmdsclock <= post_pll_cfg_table[i].tmdsclock) {
+			cfg = &post_pll_cfg_table[i];
+			break;
+		}
+	}
+
+	if (!cfg) {
+		dev_err(inno->dev, "Failed to find Post-PLL config\n");
+		ret = -EINVAL;
+		goto err_power_down;
+	}
+	dev_dbg(inno->dev, "Inno HDMI PHY Power On: pixclk %lu, tmdsclk %lu\n",
+		inno->pixclock, tmdsclock);
+
+	reg_1ad_value = cfg->post_div_en ? cfg->postdiv : 0x00;
+	reg_1aa_value = cfg->post_div_en ? 0x0e : 0x02;
+
+	/*
+	 * Pre-PLL is already prepared and running at inno->pixclock
+	 * via the clk_set_rate and prepare calls from the controller/bridge.
+	 * Now, configure and enable the Post-PLL and TMDS outputs.
+	 */
+
+	inno_write(inno, STF_INNO_POST_PLL_DIV_2,
+		   STF_INNO_POST_PLL_PRE_DIV(cfg->prediv));
+	inno_write(inno, STF_INNO_POST_PLL_DIV_3, cfg->fbdiv & 0xff);
+	inno_write(inno, STF_INNO_POST_PLL_DIV_4, reg_1ad_value);
+
+	/* Power up Post-PLL */
+	inno_write(inno, STF_INNO_POST_PLL_DIV_1, reg_1aa_value);
+
+	/* Wait for post PLL lock */
+	ret = inno_poll(inno, STF_INNO_POST_PLL_LOCK_STATUS, v,
+			v & STF_INNO_POST_PLL_LOCK, 1000, 100000);
+	if (ret) {
+		dev_err(inno->dev, "Post-PLL locking failed\n");
+		goto err_power_down;
+	}
+
+	inno_write(inno, STF_INNO_LDO_CONTROL, STF_INNO_LDO_ENABLE);
+	inno_write(inno, STF_INNO_SERIALIER_CONTROL,
+		   STF_INNO_SERIALIER_ENABLE);
+	inno_write(inno, STF_INNO_TMDS_CONTROL, 0x8f);
+
+	return 0;
+
+	/*
+	 * The PHY core leaves power_count at zero when .power_on fails and
+	 * never calls .power_off, so undo the blocks enabled above here.
+	 */
+err_power_down:
+	starfive_hdmi_phy_power_down(inno);
+	return ret;
+}
+
+static int starfive_hdmi_phy_power_off(struct phy *phy)
+{
+	struct starfive_hdmi_phy *inno = phy_get_drvdata(phy);
+
+	dev_dbg(inno->dev, "Inno HDMI PHY Power Off\n");
+
+	starfive_hdmi_phy_power_down(inno);
+
+	return 0;
+}
+
+static const struct phy_ops starfive_hdmi_phy_ops = {
+	.owner = THIS_MODULE,
+	.power_on = starfive_hdmi_phy_power_on,
+	.power_off = starfive_hdmi_phy_power_off,
+};
+
+static int starfive_hdmi_phy_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device *parent = dev->parent;
+	struct starfive_hdmi_phy *inno;
+	struct phy_provider *phy_provider;
+	struct regmap *regmap;
+	struct clk_init_data init = {};
+	const char *refoclk_name;
+	int ret;
+
+	inno = devm_kzalloc(dev, sizeof(*inno), GFP_KERNEL);
+	if (!inno)
+		return -ENOMEM;
+
+	inno->dev = dev;
+
+	/* Get the regmap from the parent device */
+	regmap = dev_get_regmap(parent, NULL);
+	if (!regmap) {
+		dev_err(dev, "Failed to get parent regmap\n");
+		return -ENODEV;
+	}
+	inno->regmap = regmap;
+
+	/*
+	 * The PHY block sits behind the HDMI controller in the shared register
+	 * space, so the common pre-PLL helpers need a 0x100 register offset.
+	 */
+	inno->pre_pll.dev = dev;
+	inno->pre_pll.regmap = regmap;
+	inno->pre_pll.offset = 0x100;
+	inno->pre_pll.table = pre_pll_cfg_table;
+	inno->pre_pll.table_size = ARRAY_SIZE(pre_pll_cfg_table);
+
+	/* Get the input reference clock */
+	inno->refoclk = devm_clk_get(inno->dev, NULL);
+	if (IS_ERR(inno->refoclk)) {
+		ret = PTR_ERR(inno->refoclk);
+		dev_err(inno->dev, "failed to get oscillator-ref clock: %d\n",
+			ret);
+		return ret;
+	}
+
+	/* We must prepare/enable refoclk here so .set_rate/.recalc_rate work */
+	ret = clk_prepare_enable(inno->refoclk);
+	if (ret) {
+		dev_err(dev, "Failed to enable refoclk: %d\n", ret);
+		return ret;
+	}
+
+	platform_set_drvdata(pdev, inno);
+
+	/* Initialize and register the clock provider */
+	refoclk_name = __clk_get_name(inno->refoclk);
+	init.parent_names = &refoclk_name;
+	init.num_parents = 1;
+	init.flags = 0;
+	init.name = "hdmi_pclk";
+	init.ops = &starfive_hdmi_phy_clk_ops;
+
+	inno->hw.init = &init;
+	inno->phyclk = devm_clk_register(dev, &inno->hw);
+	if (IS_ERR(inno->phyclk)) {
+		ret = PTR_ERR(inno->phyclk);
+		dev_err(dev, "Failed to register clock provider: %d\n", ret);
+		goto err_disable_refoclk;
+	}
+
+	ret = of_clk_add_provider(dev->of_node, of_clk_src_simple_get, inno->phyclk);
+	if (ret) {
+		dev_err(dev, "Failed to add clock provider: %d\n", ret);
+		goto err_disable_refoclk;
+	}
+
+	/*
+	 * Do not program a default rate here. The PHY registers live in the
+	 * window gated by the controller's system clock, which this driver
+	 * cannot hold without creating a probe cycle with voutcrg, and
+	 * .set_rate writes to them. Leave the hardware alone until the
+	 * controller drives it from its .enable hook.
+	 */
+
+	/* Create and register the PHY provider */
+	inno->phy = devm_phy_create(inno->dev, NULL, &starfive_hdmi_phy_ops);
+	if (IS_ERR(inno->phy)) {
+		ret = PTR_ERR(inno->phy);
+		dev_err(inno->dev, "failed to create HDMI PHY: %d\n", ret);
+		goto err_del_clk_provider;
+	}
+
+	phy_set_drvdata(inno->phy, inno);
+
+	phy_provider = devm_of_phy_provider_register(inno->dev,
+						     of_phy_simple_xlate);
+	ret = PTR_ERR_OR_ZERO(phy_provider);
+	if (ret)
+		goto err_del_clk_provider;
+
+	return 0;
+
+err_del_clk_provider:
+	of_clk_del_provider(dev->of_node);
+err_disable_refoclk:
+	clk_disable_unprepare(inno->refoclk);
+	return ret;
+}
+
+static void starfive_hdmi_phy_remove(struct platform_device *pdev)
+{
+	struct starfive_hdmi_phy *inno = platform_get_drvdata(pdev);
+
+	of_clk_del_provider(pdev->dev.of_node);
+	clk_disable_unprepare(inno->refoclk);
+}
+
+static const struct of_device_id starfive_hdmi_phy_of_match[] = {
+	{ .compatible = "starfive,jh7110-inno-hdmi-phy", },
+	{ /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, starfive_hdmi_phy_of_match);
+
+static struct platform_driver starfive_hdmi_phy_driver = {
+	.probe = starfive_hdmi_phy_probe,
+	.remove = starfive_hdmi_phy_remove,
+	.driver = {
+		.name = "starfive-inno-hdmi-phy",
+		.of_match_table = starfive_hdmi_phy_of_match,
+	},
+};
+module_platform_driver(starfive_hdmi_phy_driver);
+
+MODULE_AUTHOR("Michal Wilczynski <m.wilczynski@samsung.com>");
+MODULE_DESCRIPTION("StarFive JH7110 Innosilicon HDMI PHY Driver");
+MODULE_LICENSE("GPL");

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH v6 5/5] MAINTAINERS: Add StarFive JH7110 HDMI PHY entry
  2026-10-28 15:21 ` [PATCH v6 0/5] phy: Add the StarFive JH7110 Innosilicon HDMI PHY Michal Wilczynski
                     ` (3 preceding siblings ...)
  2026-10-28 15:21   ` [PATCH v6 4/5] phy: starfive: Add jh7110-inno-hdmi-phy driver Michal Wilczynski
@ 2026-10-28 15:21   ` Michal Wilczynski
  4 siblings, 0 replies; 8+ messages in thread
From: Michal Wilczynski @ 2026-10-28 15:21 UTC (permalink / raw)
  To: Vinod Koul, Neil Armstrong, Manivannan Sadhasivam, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Heiko Stuebner,
	Emil Renner Berthing, Hal Feng, Dominique Belhachemi,
	Michal Wilczynski, Andy Yan
  Cc: linux-phy, devicetree, linux-kernel, linux-arm-kernel,
	linux-rockchip, linux-riscv, Marek Szyprowski, Icenowy Zheng,
	Chaoyi Chen, Joshua Peisach

Add an entry for the StarFive JH7110 HDMI PHY binding and driver, next
to the other JH7110 PHY drivers.

Signed-off-by: Michal Wilczynski <m.wilczynski@samsung.com>
---
 MAINTAINERS | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 782389bd5cd3d9e0117ebdfae707e1f61cb3e772..9e42d1705fcb3fa46325761a19c6b7244c18b7c4 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -26106,6 +26106,12 @@ S:	Supported
 F:	Documentation/devicetree/bindings/phy/starfive,jh7110-dphy-tx.yaml
 F:	drivers/phy/starfive/phy-jh7110-dphy-tx.c
 
+STARFIVE JH7110 HDMI PHY DRIVER
+M:	Michal Wilczynski <m.wilczynski@samsung.com>
+S:	Maintained
+F:	Documentation/devicetree/bindings/phy/starfive,jh7110-inno-hdmi-phy.yaml
+F:	drivers/phy/starfive/phy-jh7110-inno-hdmi.c
+
 STARFIVE JH7110 MMC/SD/SDIO DRIVER
 M:	William Qiu <william.qiu@starfivetech.com>
 S:	Supported

-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH v6 2/5] phy: Add common Innosilicon HDMI PHY helpers
  2026-10-28 15:21   ` [PATCH v6 2/5] phy: Add common Innosilicon HDMI PHY helpers Michal Wilczynski
@ 2026-10-28 15:30     ` sashiko-bot
  0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-28 15:30 UTC (permalink / raw)
  To: Michal Wilczynski
  Cc: linux-phy, devicetree, robh, conor+dt, vkoul, neil.armstrong,
	olteanv

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] phy: inno-hdmi: 32-bit overflow in inno_hdmi_phy_pre_pll_recalc_rate()
--

commit ddb60d944975dab0de39c50bd23d32e884d239ae
Author: Michal Wilczynski <m.wilczynski@samsung.com>

phy: Add common Innosilicon HDMI PHY helpers

This commit introduces a shared helper library for the Innosilicon HDMI PHY
pre-PLL configuration, including table lookup, clock rate calculation helpers,
and register programming.

> diff --git a/drivers/phy/phy-inno-hdmi.c b/drivers/phy/phy-inno-hdmi.c
> new file mode 100644
> index 0000000000000..6900d4d83e464
> --- /dev/null
> +++ b/drivers/phy/phy-inno-hdmi.c
[ ... ]
> +unsigned long
> +inno_hdmi_phy_pre_pll_recalc_rate(const struct inno_hdmi_phy_pre_pll *pll,
> +				  unsigned long parent_rate)
> +{
> +	unsigned int control, div_1, div_2, div_3, pclk_div_ab, pclk_div_cd;
> +	unsigned int frac_div_7_0, frac_div_15_8, frac_div_23_16, div;
> +	u8 nd, no_a, no_b, no_d;
> +	unsigned long frac, rate;
> +	u64 vco, vco_rate;
> +	u16 nf;
> +	int ret;
[ ... ]
> +	nd = div_1 & INNO_PRE_PLL_PRE_DIV_MASK;
> +	nf = (div_2 & INNO_PRE_PLL_FB_DIV_11_8_MASK) << 8;
> +	nf |= div_3;
> +	vco = parent_rate * nf;
> +
> +	if (!(div_2 & INNO_PRE_PLL_FRAC_DIV_DISABLE)) {
> +		ret = inno_read(pll, INNO_PRE_PLL_FRAC_DIV_7_0, &frac_div_7_0);
> +		if (ret)
> +			goto err;
> +		ret = inno_read(pll, INNO_PRE_PLL_FRAC_DIV_15_8, &frac_div_15_8);
> +		if (ret)
> +			goto err;
> +		ret = inno_read(pll, INNO_PRE_PLL_FRAC_DIV_23_16,
> +				&frac_div_23_16);
> +		if (ret)
> +			goto err;
> +
> +		frac = frac_div_7_0 | (frac_div_15_8 << 8) |
> +		       (frac_div_23_16 << 16);
> +		vco += DIV_ROUND_CLOSEST(parent_rate * frac,
> +					 BIT(INNO_FRAC_DIV_WIDTH));
> +	}

[Severity: Medium]
Can the VCO calculations in inno_hdmi_phy_pre_pll_recalc_rate() in
drivers/phy/phy-inno-hdmi.c overflow on 32-bit architectures?

Both parent_rate and frac have type unsigned long, while nf is a u16 and vco
is a u64. On 32-bit targets where unsigned long is 32 bits wide,
parent_rate * nf is evaluated in 32-bit arithmetic before being widened to
u64. With a typical 24 MHz reference clock, any feedback divider nf >= 179
exceeds U32_MAX, whereas the 12-bit feedback divider reaches up to 757 in the
Innosilicon pre-PLL configuration tables.

Similarly, when the fractional divider is enabled, parent_rate * frac and
DIV_ROUND_CLOSEST() operate on 32-bit unsigned long values before adding to
vco. For a 24 MHz parent_rate and a 24-bit frac value, parent_rate * frac
requires up to 49 bits and overflows 32-bit unsigned long whenever frac >= 179.

Should parent_rate be cast to u64 in parent_rate * nf, and should the
fractional calculation use DIV_ROUND_CLOSEST_ULL((u64)parent_rate * frac,
BIT(INNO_FRAC_DIV_WIDTH)) to avoid 32-bit truncation?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261028-jh7110-inno-hdmi-phy-v6-0-f3379684d97e@samsung.com?part=2

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH v6 4/5] phy: starfive: Add jh7110-inno-hdmi-phy driver
  2026-10-28 15:21   ` [PATCH v6 4/5] phy: starfive: Add jh7110-inno-hdmi-phy driver Michal Wilczynski
@ 2026-10-28 15:46     ` sashiko-bot
  0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-28 15:46 UTC (permalink / raw)
  To: Michal Wilczynski
  Cc: linux-phy, devicetree, robh, conor+dt, vkoul, neil.armstrong,
	olteanv

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] phy: starfive: hdmi: fix rate cache and BIAS/RX state across disable/enable
- [High] phy: starfive: jh7110-inno-hdmi: invalid pre_pll_cfg_table divider values
- [Medium] phy: starfive: jh7110-inno-hdmi: broken Pre-PLL power and lock sequencing in clk_ops
- [Low] phy: starfive: unused inno_read() helper and register macros in hdmi phy
--

commit de90e248a2b1a2d110bed58fd3d5de2e75a01d3d
Author: Michal Wilczynski <m.wilczynski@samsung.com>

phy: starfive: Add jh7110-inno-hdmi-phy driver

This commit adds the Innosilicon HDMI PHY driver for the StarFive JH7110 SoC.
The driver acts as both a clock provider for the variable pixel clock via the
Pre-PLL and a PHY provider configuring the Post-PLL and analog blocks.

> diff --git a/drivers/phy/starfive/phy-jh7110-inno-hdmi.c b/drivers/phy/starfive/phy-jh7110-inno-hdmi.c
> new file mode 100644
> index 0000000000000..ebda363e50a3e
> --- /dev/null
> +++ b/drivers/phy/starfive/phy-jh7110-inno-hdmi.c
[ ... ]
> +/* REG: 0x1aa */
> +#define STF_INNO_POST_PLL_DIV_1		0x1aa
> +#define STF_INNO_POST_PLL_POST_DIV_ENABLE	GENMASK(3, 2)
> +#define STF_INNO_POST_PLL_REFCLK_SEL_TMDS	BIT(1)
> +#define STF_INNO_POST_PLL_POWER_DOWN		BIT(0)
> +
> +/* REG: 0x1ab */
> +#define STF_INNO_POST_PLL_DIV_2		0x1ab
> +#define STF_INNO_POST_PLL_PRE_DIV(x)		FIELD_PREP(GENMASK(5, 0), x)
> +#define STF_INNO_POST_PLL_FB_DIV_8(x)		FIELD_PREP(BIT(7), (x) >> 8)
> +
> +/* REG: 0x1ac */
> +#define STF_INNO_POST_PLL_DIV_3		0x1ac
> +#define STF_INNO_POST_PLL_FB_DIV_7_0(x)	FIELD_PREP(GENMASK(7, 0), x)
> +
> +/* REG: 0x1ad */
> +#define STF_INNO_POST_PLL_DIV_4		0x1ad
> +#define STF_INNO_POST_PLL_POST_DIV_MASK	GENMASK(1, 0)
> +
> +/* REG: 0x1af */
> +#define STF_INNO_POST_PLL_LOCK_STATUS		0x1af
> +#define STF_INNO_POST_PLL_LOCK		BIT(0)
> +
> +/* REG: 0x1b0 */
> +#define STF_INNO_BIAS_CONTROL			0x1b0
> +#define STF_INNO_BIAS_ENABLE			BIT(2)
> +
> +/* REG: 0x1b2 */
> +#define STF_INNO_TMDS_CONTROL			0x1b2
> +#define STF_INNO_TMDS_CLK_DRIVER_EN		BIT(3)
> +#define STF_INNO_TMDS_D2_DRIVER_EN		BIT(2)
> +#define STF_INNO_TMDS_D1_DRIVER_EN		BIT(1)
> +#define STF_INNO_TMDS_D0_DRIVER_EN		BIT(0)
> +#define STF_INNO_TMDS_DRIVER_ENABLE		(STF_INNO_TMDS_CLK_DRIVER_EN | \
> +						 STF_INNO_TMDS_D2_DRIVER_EN | \
> +						 STF_INNO_TMDS_D1_DRIVER_EN | \
> +						 STF_INNO_TMDS_D0_DRIVER_EN)
[ ... ]
> +static const struct inno_hdmi_phy_pre_pll_config pre_pll_cfg_table[] = {
> +	{ 25175000, 25175000, 1, 100, 2, 3, 3, 12, 3, 3, 4, 0, 0xf55555 },
> +	{ 25200000, 25200000, 1, 100, 2, 3, 3, 12, 3, 3, 4, 0, 0 },
> +	{ 27000000, 27000000, 1, 90, 3, 2, 2, 10, 3, 3, 4, 0, 0 },
> +	{ 27027000, 27027000, 1, 90, 3, 2, 2, 10, 3, 3, 4, 0, 0x170a3d },
> +	{ 28320000, 28320000, 1, 28, 2, 1, 1, 3, 0, 3, 4, 0, 0x51eb85 },
> +	{ 30240000, 30240000, 1, 30, 2, 1, 1, 3, 0, 3, 4, 0, 0x3d70a3 },
> +	{ 31500000, 31500000, 1, 31, 2, 1, 1, 3, 0, 3, 4, 0, 0x7fffff },
> +	{ 33750000, 33750000, 1, 33, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
[ ... ]
> +	{ 78750000, 78750000, 1, 78, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
[ ... ]
> +	{ 85750000, 85750000, 1, 85, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
> +	{ 85800000, 85800000, 1, 85, 2, 1, 1, 3, 0, 3, 4, 0, 0xcccccc },
> +	{ 88750000, 88750000, 1, 88, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
[ ... ]
> +	{ 136750000, 136750000, 1, 136, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },
[ ... ]
> +	{ 262750000, 262750000, 1, 262, 2, 1, 1, 3, 0, 3, 4, 0, 0xcfffff },

[Severity: High]
Do these eight entries in pre_pll_cfg_table generate the wrong pixel clock
and cause starfive_hdmi_phy_power_on() to fail with -EINVAL?

With the 24 MHz reference clock:
- 25175000 has div = 1 * 12 * 4 * 2 = 96, so 24 MHz * (100 + 0xf55555 / 2^24)
  / 96 yields 25,239,583 Hz, which inno_hdmi_phy_pre_pll_recalc_rate()
  recalculates as 25240000 Hz.
- 25200000 has div = 96 and fracdiv = 0, so 24 MHz * 100 / 96 yields
  25000000 Hz.
- 33750000, 78750000, 85750000, 88750000, 136750000, and 262750000 have
  div = 1 * 3 * 4 * 2 = 24 and fracdiv = 0xcfffff (~0.8125 instead of
  0xc00000 = 0.75), which adds 812.5 kHz instead of 750 kHz and recalculates
  to 33812000, 78812000, 85812000, 88812000, 136812000, and 262812000 Hz.

When hdmi_pclk is prepared and starfive_hdmi_phy_clk_recalc_rate() runs:

    clk_change_rate()
      clk_recalc()
        starfive_hdmi_phy_clk_recalc_rate()
          inno_hdmi_phy_pre_pll_recalc_rate()

the clock framework caches the recalculated rate in core->rate. When
starfive_hdmi_phy_power_on() then calls:

    tmdsclock = clk_get_rate(inno->phyclk);
    pre_cfg = inno_hdmi_phy_pre_pll_lookup(&inno->pre_pll, tmdsclock,
                                           tmdsclock);

inno_hdmi_phy_pre_pll_lookup() requires an exact match against
cfg->pixclock in pre_pll_cfg_table, which fails with -EINVAL for all eight
modes.

[ ... ]
> +static inline void inno_write(struct starfive_hdmi_phy *inno, u32 reg, u8 val)
> +{
> +	regmap_write(inno->regmap, reg * 4, val);
> +}
> +
> +static inline u8 inno_read(struct starfive_hdmi_phy *inno, u32 reg)
> +{
> +	u32 val;
> +
> +	regmap_read(inno->regmap, reg * 4, &val);
> +	return val;
> +}

[Severity: Low]
Is inno_read() needed here? It is never called in this file, and the
register bit macros STF_INNO_POST_PLL_POST_DIV_ENABLE,
STF_INNO_POST_PLL_REFCLK_SEL_TMDS, STF_INNO_POST_PLL_FB_DIV_8,
STF_INNO_POST_PLL_FB_DIV_7_0, STF_INNO_POST_PLL_POST_DIV_MASK, and
STF_INNO_TMDS_DRIVER_ENABLE defined at the top of the file are also unused
(while starfive_hdmi_phy_power_on() open-codes 0x0e, 0x02, cfg->fbdiv & 0xff,
and 0x8f), which triggers compiler warnings under W=2 builds.

[ ... ]
> +static int starfive_hdmi_phy_clk_prepare(struct clk_hw *hw)
> +{
> +	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
> +	int ret;
> +
> +	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, false);
> +
> +	if (inno_hdmi_phy_pre_pll_is_locked(&inno->pre_pll))
> +		return 0;
> +
> +	ret = inno_hdmi_phy_pre_pll_wait_locked(&inno->pre_pll, 100000);
> +	if (ret) {
> +		dev_err(inno->dev, "Timeout waiting for pre-PLL lock\n");
> +		inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, true);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static void starfive_hdmi_phy_clk_unprepare(struct clk_hw *hw)
> +{
> +	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
> +
> +	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, true);
> +	inno->pixclock = 0; /* Invalidate cached rate */
> +}
> +
> +static unsigned long starfive_hdmi_phy_clk_recalc_rate(struct clk_hw *hw,
> +						       unsigned long parent_rate)
> +{
> +	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
> +
> +	/*
> +	 * Reading the PLL back requires the HDMI controller's system clock,
> +	 * which the sibling controller driver holds and this driver cannot
> +	 * request without creating a probe cycle with voutcrg. The clock
> +	 * framework walks every registered clock regardless of which drivers
> +	 * have bound, so fall back to the cached rate whenever this clock is
> +	 * not prepared; an unclocked access to the shared register window
> +	 * wedges the bus hard enough that the CPU stops taking NMIs.
> +	 */
> +	if (!clk_hw_is_prepared(hw))
> +		return inno->pixclock;
> +
> +	inno->pixclock = inno_hdmi_phy_pre_pll_recalc_rate(&inno->pre_pll,
> +							   parent_rate);
> +
> +	return inno->pixclock;
> +}
[ ... ]
> +static int starfive_hdmi_phy_clk_set_rate(struct clk_hw *hw, unsigned long rate,
> +					  unsigned long parent_rate)
> +{
> +	struct starfive_hdmi_phy *inno = to_starfive_hdmi_phy(hw);
> +	const struct inno_hdmi_phy_pre_pll_config *cfg;
> +
> +	/*
> +	 * The JH7110 only drives 8bpc, so the TMDS clock always matches the
> +	 * pixel clock.
> +	 */
> +	cfg = inno_hdmi_phy_pre_pll_lookup(&inno->pre_pll, rate, rate);
> +	if (IS_ERR(cfg))
> +		return PTR_ERR(cfg);
> +
> +	dev_dbg(inno->dev, "%s rate %lu tmdsclk %lu\n",
> +		__func__, rate, cfg->tmdsclock);
> +
> +	if (inno->pixclock == rate && inno->tmdsclock == cfg->tmdsclock)
> +		return 0;
> +
> +	inno_update_bits(inno, STF_INNO_BIAS_CONTROL,
> +			 STF_INNO_BIAS_ENABLE, STF_INNO_BIAS_ENABLE);
> +	inno_write(inno, STF_INNO_RX_CONTROL, STF_INNO_RX_ENABLE);
> +
> +	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, true);
> +	inno_hdmi_phy_pre_pll_configure(&inno->pre_pll, cfg);
> +	inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, false);
> +
> +	inno->pixclock = rate;
> +	inno->tmdsclock = cfg->tmdsclock;
> +
> +	return 0;
> +}

[Severity: Medium]
Should starfive_hdmi_phy_clk_set_rate() wait for the Pre-PLL to lock or keep
it powered down when the clock is unprepared?

In starfive_hdmi_phy_clk_set_rate(), the driver powers down the Pre-PLL,
configures it, and powers it back up via
inno_hdmi_phy_pre_pll_power_down(&inno->pre_pll, false), returning 0 without
calling inno_hdmi_phy_pre_pll_wait_locked().

If clk_set_rate() is called while hdmi_pclk is already prepared,
starfive_hdmi_phy_clk_prepare() is not called again because prepare_count is
already non-zero, so nothing waits for the Pre-PLL to lock before
starfive_hdmi_phy_power_on() configures and enables the Post-PLL (which uses
the Pre-PLL TMDS clock as its reference).

Conversely, if clk_set_rate() is called while hdmi_pclk is unprepared,
starfive_hdmi_phy_clk_set_rate() leaves the Pre-PLL, STF_INNO_BIAS_ENABLE,
and STF_INNO_RX_ENABLE powered on in hardware while prepare_count is 0.

> +
> +static const struct clk_ops starfive_hdmi_phy_clk_ops = {
> +	.prepare = starfive_hdmi_phy_clk_prepare,
> +	.unprepare = starfive_hdmi_phy_clk_unprepare,
> +	.recalc_rate = starfive_hdmi_phy_clk_recalc_rate,
> +	.determine_rate = starfive_hdmi_phy_clk_determine_rate,
> +	.set_rate = starfive_hdmi_phy_clk_set_rate,
> +};
> +
> +static void starfive_hdmi_phy_power_down(struct starfive_hdmi_phy *inno)
> +{
> +	inno_write(inno, STF_INNO_TMDS_CONTROL, 0x00);
> +	inno_write(inno, STF_INNO_SERIALIER_CONTROL, 0x00);
> +	inno_write(inno, STF_INNO_LDO_CONTROL, 0x00);
> +	inno_update_bits(inno, STF_INNO_BIAS_CONTROL,
> +			 STF_INNO_BIAS_ENABLE, 0x00);
> +	inno_write(inno, STF_INNO_RX_CONTROL, 0x00);
> +
> +	/* Power down Post-PLL */
> +	inno_update_bits(inno, STF_INNO_POST_PLL_DIV_1,
> +			 STF_INNO_POST_PLL_POWER_DOWN,
> +			 STF_INNO_POST_PLL_POWER_DOWN);
> +
> +	inno->tmdsclock = 0;
> +	inno->pixclock = 0;
> +}

[Severity: High]
What happens when HDMI output is disabled and then re-enabled at the same
pixel clock rate?

When the display is disabled:
1. starfive_hdmi_phy_power_off() -> starfive_hdmi_phy_power_down() clears
   STF_INNO_BIAS_ENABLE and STF_INNO_RX_CONTROL to 0, and sets
   inno->tmdsclock = 0 and inno->pixclock = 0.
2. clk_disable_unprepare() -> starfive_hdmi_phy_clk_unprepare() powers down
   the Pre-PLL and sets inno->pixclock = 0.

However, clk_core_unprepare() in the clock framework does not clear
core->rate, so the clock framework still caches the previous rate R.

When the display is re-enabled at the same rate R:
1. clk_set_rate(phyclk, R) sees rate == clk_core_get_rate_nolock(core) in
   clk_core_set_rate_nolock() and returns 0 early without calling
   starfive_hdmi_phy_clk_set_rate().
2. clk_prepare_enable(phyclk) calls starfive_hdmi_phy_clk_prepare() before
   phy_power_on(), powering up the Pre-PLL and polling
   inno_hdmi_phy_pre_pll_wait_locked() while STF_INNO_BIAS_ENABLE and
   STF_INNO_RX_ENABLE are still disabled at 0 (and without reprogramming the
   Pre-PLL if the shared register block was reset).

Additionally, if a clock tree walk invokes starfive_hdmi_phy_clk_recalc_rate()
while phyclk is unprepared after inno->pixclock was zeroed, it returns 0 and
overwrites core->rate with 0. If the display is then re-enabled without a new
clk_set_rate() call, clk_get_rate(inno->phyclk) in
starfive_hdmi_phy_power_on() returns 0 and fails
inno_hdmi_phy_pre_pll_lookup() with -EINVAL.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261028-jh7110-inno-hdmi-phy-v6-0-f3379684d97e@samsung.com?part=4

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-10-28 15:46 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
     [not found] <CGME20261028152225eucas1p133ea1b7981dfa2afe085b83e1bd6de3b@eucas1p1.samsung.com>
2026-10-28 15:21 ` [PATCH v6 0/5] phy: Add the StarFive JH7110 Innosilicon HDMI PHY Michal Wilczynski
2026-10-28 15:21   ` [PATCH v6 1/5] dt-bindings: phy: Add starfive,jh7110-inno-hdmi-phy Michal Wilczynski
2026-10-28 15:21   ` [PATCH v6 2/5] phy: Add common Innosilicon HDMI PHY helpers Michal Wilczynski
2026-10-28 15:30     ` sashiko-bot
2026-10-28 15:21   ` [PATCH v6 3/5] phy: rockchip: inno-hdmi: Use the common Innosilicon " Michal Wilczynski
2026-10-28 15:21   ` [PATCH v6 4/5] phy: starfive: Add jh7110-inno-hdmi-phy driver Michal Wilczynski
2026-10-28 15:46     ` sashiko-bot
2026-10-28 15:21   ` [PATCH v6 5/5] MAINTAINERS: Add StarFive JH7110 HDMI PHY entry Michal Wilczynski

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox