diff mbox series

[v3,08/17] clk: eyeq5: add platform driver

Message ID 20240123-mbly-clk-v3-8-392b010b8281@bootlin.com
State New
Headers show
Series Add support for Mobileye EyeQ5 system controller | expand

Commit Message

Théo Lebrun Jan. 23, 2024, 6:46 p.m. UTC
Add the Mobileye EyeQ5 clock controller driver. It might grow to add
support for other platforms from Mobileye.

It handles 10 read-only PLLs derived from the main crystal on board. It
exposes a table-based divider clock used for OSPI. Other platform
clocks are not configurable and therefore kept as fixed-factor
devicetree nodes.

Two PLLs are required early on and are therefore registered at
of_clk_init(). Those are pll-cpu for the GIC timer and pll-per for the
UARTs.

Signed-off-by: Théo Lebrun <theo.lebrun@bootlin.com>
---
 MAINTAINERS             |   1 +
 drivers/clk/Kconfig     |  11 ++
 drivers/clk/Makefile    |   1 +
 drivers/clk/clk-eyeq5.c | 414 ++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 427 insertions(+)

Comments

Krzysztof Kozlowski Jan. 24, 2024, 7:05 a.m. UTC | #1
On 23/01/2024 19:46, Théo Lebrun wrote:
> Add the Mobileye EyeQ5 clock controller driver. It might grow to add
> support for other platforms from Mobileye.
> 
> It handles 10 read-only PLLs derived from the main crystal on board. It
> exposes a table-based divider clock used for OSPI. Other platform
> clocks are not configurable and therefore kept as fixed-factor
> devicetree nodes.
> 
> Two PLLs are required early on and are therefore registered at
> of_clk_init(). Those are pll-cpu for the GIC timer and pll-per for the
> UARTs.
> 


> +#define OLB_PCSR1_RESET				BIT(0)
> +#define OLB_PCSR1_SSGC_DIV			GENMASK(4, 1)
> +/* Spread amplitude (% = 0.1 * SPREAD[4:0]) */
> +#define OLB_PCSR1_SPREAD			GENMASK(9, 5)
> +#define OLB_PCSR1_DIS_SSCG			BIT(10)
> +/* Down-spread or center-spread */
> +#define OLB_PCSR1_DOWN_SPREAD			BIT(11)
> +#define OLB_PCSR1_FRAC_IN			GENMASK(31, 12)
> +
> +static struct clk_hw_onecell_data *eq5c_clk_data;
> +static struct regmap *eq5c_olb;

Drop these two. No file-scope regmaps for drivers. Use private container
structures.

...

> +static void __init eq5c_init(struct device_node *np)
> +{
> +	struct device_node *parent_np = of_get_parent(np);
> +	int i, ret;
> +
> +	eq5c_clk_data = kzalloc(struct_size(eq5c_clk_data, hws, EQ5C_NB_CLKS),
> +				GFP_KERNEL);
> +	if (!eq5c_clk_data) {
> +		ret = -ENOMEM;
> +		goto err;
> +	}
> +
> +	eq5c_clk_data->num = EQ5C_NB_CLKS;
> +
> +	/*
> +	 * Mark all clocks as deferred. We register some now and others at
> +	 * platform device probe.
> +	 */
> +	for (i = 0; i < EQ5C_NB_CLKS; i++)
> +		eq5c_clk_data->hws[i] = ERR_PTR(-EPROBE_DEFER);
> +
> +	/*
> +	 * Currently, if OLB is not available, we log an error, fail init then

How it could be not available? Only with broken initcall ordering. Fix
your initcall ordering and then simplify all this weird code.

> +	 * fail probe. We might want to change this behavior and assume all
> +	 * clocks are in bypass mode; this is what is being done in the vendor
> +	 * driver.
> +	 *
> +	 * It is unclear if there are valid situations where the OLB region
> +	 * would be inaccessible.



Best regards,
Krzysztof
Théo Lebrun Jan. 24, 2024, 4:41 p.m. UTC | #2
Hello,

On Wed Jan 24, 2024 at 8:05 AM CET, Krzysztof Kozlowski wrote:
> On 23/01/2024 19:46, Théo Lebrun wrote:
> > Add the Mobileye EyeQ5 clock controller driver. It might grow to add
> > support for other platforms from Mobileye.
> > 
> > It handles 10 read-only PLLs derived from the main crystal on board. It
> > exposes a table-based divider clock used for OSPI. Other platform
> > clocks are not configurable and therefore kept as fixed-factor
> > devicetree nodes.
> > 
> > Two PLLs are required early on and are therefore registered at
> > of_clk_init(). Those are pll-cpu for the GIC timer and pll-per for the
> > UARTs.
> > 
>
>
> > +#define OLB_PCSR1_RESET				BIT(0)
> > +#define OLB_PCSR1_SSGC_DIV			GENMASK(4, 1)
> > +/* Spread amplitude (% = 0.1 * SPREAD[4:0]) */
> > +#define OLB_PCSR1_SPREAD			GENMASK(9, 5)
> > +#define OLB_PCSR1_DIS_SSCG			BIT(10)
> > +/* Down-spread or center-spread */
> > +#define OLB_PCSR1_DOWN_SPREAD			BIT(11)
> > +#define OLB_PCSR1_FRAC_IN			GENMASK(31, 12)
> > +
> > +static struct clk_hw_onecell_data *eq5c_clk_data;
> > +static struct regmap *eq5c_olb;
>
> Drop these two. No file-scope regmaps for drivers. Use private container
> structures.

I wouldn't know how to handle the two steps then. Two clocks and the clk
provider are registered at of_clk_init() using CLK_OF_DECLARE_DRIVER().
The rest is at platform device probe. Without a static, there are no
way to pass the struct clk_hw_onecell_data from one to the other.

I've looked at all clock drivers that do CLK_OF_DECLARE_DRIVER() and
register a platform driver.

 - The following use a static variable:
   drivers/clk/axis/clk-artpec6.c
   drivers/clk/clk-aspeed.c
   drivers/clk/clk-ast2600.c
   drivers/clk/clk-eyeq5.c
   drivers/clk/clk-gemini.c
   drivers/clk/clk-milbeaut.c
   drivers/clk/mediatek/clk-mt2701.c
   drivers/clk/mediatek/clk-mt6797.c
   drivers/clk/mediatek/clk-mt8173-infracfg.c
   drivers/clk/nxp/clk-lpc18xx-creg.c
   drivers/clk/ralink/clk-mt7621.c
   drivers/clk/ralink/clk-mtmips.c
   drivers/clk/sunxi/clk-mod0.c
   drivers/clk/axis/clk-artpec6.c

 - Those two declare different clock providers at init and probe:
   drivers/clk/ralink/clk-mt7621.c
   drivers/clk/sunxi/clk-mod0.c

 - It doesn't register new clocks at probe (only resets) so no need to
   share variables.
   drivers/clk/ralink/clk-mtmips.c

>
> ...
>
> > +static void __init eq5c_init(struct device_node *np)
> > +{
> > +	struct device_node *parent_np = of_get_parent(np);
> > +	int i, ret;
> > +
> > +	eq5c_clk_data = kzalloc(struct_size(eq5c_clk_data, hws, EQ5C_NB_CLKS),
> > +				GFP_KERNEL);
> > +	if (!eq5c_clk_data) {
> > +		ret = -ENOMEM;
> > +		goto err;
> > +	}
> > +
> > +	eq5c_clk_data->num = EQ5C_NB_CLKS;
> > +
> > +	/*
> > +	 * Mark all clocks as deferred. We register some now and others at
> > +	 * platform device probe.
> > +	 */
> > +	for (i = 0; i < EQ5C_NB_CLKS; i++)
> > +		eq5c_clk_data->hws[i] = ERR_PTR(-EPROBE_DEFER);
> > +
> > +	/*
> > +	 * Currently, if OLB is not available, we log an error, fail init then
>
> How it could be not available? Only with broken initcall ordering. Fix
> your initcall ordering and then simplify all this weird code.

of_syscon_register() and regmap_init_mmio() lists many reasons for
it to not be available. Am I missing something?

Thanks,

--
Théo Lebrun, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com
Krzysztof Kozlowski Jan. 25, 2024, 7:46 a.m. UTC | #3
On 24/01/2024 17:41, Théo Lebrun wrote:
> Hello,
> 
> On Wed Jan 24, 2024 at 8:05 AM CET, Krzysztof Kozlowski wrote:
>> On 23/01/2024 19:46, Théo Lebrun wrote:
>>> Add the Mobileye EyeQ5 clock controller driver. It might grow to add
>>> support for other platforms from Mobileye.
>>>
>>> It handles 10 read-only PLLs derived from the main crystal on board. It
>>> exposes a table-based divider clock used for OSPI. Other platform
>>> clocks are not configurable and therefore kept as fixed-factor
>>> devicetree nodes.
>>>
>>> Two PLLs are required early on and are therefore registered at
>>> of_clk_init(). Those are pll-cpu for the GIC timer and pll-per for the
>>> UARTs.
>>>
>>
>>
>>> +#define OLB_PCSR1_RESET				BIT(0)
>>> +#define OLB_PCSR1_SSGC_DIV			GENMASK(4, 1)
>>> +/* Spread amplitude (% = 0.1 * SPREAD[4:0]) */
>>> +#define OLB_PCSR1_SPREAD			GENMASK(9, 5)
>>> +#define OLB_PCSR1_DIS_SSCG			BIT(10)
>>> +/* Down-spread or center-spread */
>>> +#define OLB_PCSR1_DOWN_SPREAD			BIT(11)
>>> +#define OLB_PCSR1_FRAC_IN			GENMASK(31, 12)
>>> +
>>> +static struct clk_hw_onecell_data *eq5c_clk_data;
>>> +static struct regmap *eq5c_olb;
>>
>> Drop these two. No file-scope regmaps for drivers. Use private container
>> structures.
> 
> I wouldn't know how to handle the two steps then. Two clocks and the clk
> provider are registered at of_clk_init() using CLK_OF_DECLARE_DRIVER().

Right, if some clocks have to be early, CLK_OF_DECLARE_DRIVER needs
static ones. But your commit subject says it is a platform driver and
all other pieces of this code is rather incompatible with this approach.

Do not use CLK_OF_DECLARE_DRIVER for cases where you have dependencies
because it forces you to manually order initcalls, which is exactly what
we do not want.


> The rest is at platform device probe. Without a static, there are no
> way to pass the struct clk_hw_onecell_data from one to the other.
> 
> I've looked at all clock drivers that do CLK_OF_DECLARE_DRIVER() and
> register a platform driver.

Even though the code is correct, using arguments "other did it" will not
work. You want to say that you implement legacy, poor code because you
saw legacy, poor code?

> 
>  - The following use a static variable:
>    drivers/clk/axis/clk-artpec6.c
>    drivers/clk/clk-aspeed.c
>    drivers/clk/clk-ast2600.c
>    drivers/clk/clk-eyeq5.c
>    drivers/clk/clk-gemini.c
>    drivers/clk/clk-milbeaut.c
>    drivers/clk/mediatek/clk-mt2701.c
>    drivers/clk/mediatek/clk-mt6797.c
>    drivers/clk/mediatek/clk-mt8173-infracfg.c
>    drivers/clk/nxp/clk-lpc18xx-creg.c
>    drivers/clk/ralink/clk-mt7621.c
>    drivers/clk/ralink/clk-mtmips.c
>    drivers/clk/sunxi/clk-mod0.c
>    drivers/clk/axis/clk-artpec6.c
> 
>  - Those two declare different clock providers at init and probe:
>    drivers/clk/ralink/clk-mt7621.c
>    drivers/clk/sunxi/clk-mod0.c
> 
>  - It doesn't register new clocks at probe (only resets) so no need to
>    share variables.
>    drivers/clk/ralink/clk-mtmips.c
> 
>>
>> ...
>>
>>> +static void __init eq5c_init(struct device_node *np)
>>> +{
>>> +	struct device_node *parent_np = of_get_parent(np);
>>> +	int i, ret;
>>> +
>>> +	eq5c_clk_data = kzalloc(struct_size(eq5c_clk_data, hws, EQ5C_NB_CLKS),
>>> +				GFP_KERNEL);
>>> +	if (!eq5c_clk_data) {
>>> +		ret = -ENOMEM;
>>> +		goto err;
>>> +	}
>>> +
>>> +	eq5c_clk_data->num = EQ5C_NB_CLKS;
>>> +
>>> +	/*
>>> +	 * Mark all clocks as deferred. We register some now and others at
>>> +	 * platform device probe.
>>> +	 */
>>> +	for (i = 0; i < EQ5C_NB_CLKS; i++)
>>> +		eq5c_clk_data->hws[i] = ERR_PTR(-EPROBE_DEFER);
>>> +
>>> +	/*
>>> +	 * Currently, if OLB is not available, we log an error, fail init then
>>
>> How it could be not available? Only with broken initcall ordering. Fix
>> your initcall ordering and then simplify all this weird code.
> 
> of_syscon_register() and regmap_init_mmio() lists many reasons for
> it to not be available. Am I missing something?

Yes, initcall ordering.

Best regards,
Krzysztof
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index 6dc4251e1ac2..3ea96ab7d2b8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -14793,6 +14793,7 @@  F:	Documentation/devicetree/bindings/soc/mobileye/
 F:	arch/mips/boot/dts/mobileye/
 F:	arch/mips/configs/eyeq5_defconfig
 F:	arch/mips/mobileye/board-epm5.its.S
+F:	drivers/clk/clk-eyeq5.c
 F:	include/dt-bindings/clock/mobileye,eyeq5-clk.h
 F:	include/dt-bindings/soc/mobileye,eyeq5.h
 
diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig
index 50af5fc7f570..d5043ce2a75c 100644
--- a/drivers/clk/Kconfig
+++ b/drivers/clk/Kconfig
@@ -218,6 +218,17 @@  config COMMON_CLK_EN7523
 	  This driver provides the fixed clocks and gates present on Airoha
 	  ARM silicon.
 
+config COMMON_CLK_EYEQ5
+	bool "Clock driver for the Mobileye EyeQ5 platform"
+	depends on OF
+	depends on MACH_EYEQ5 || COMPILE_TEST
+	default MACH_EYEQ5
+	help
+		This driver provides the clocks found on the Mobileye EyeQ5 SoC. Its
+		registers live in a shared register region called OLB. It provides 10
+		read-only PLLs derived from the main crystal clock which must be constant
+		and one divider clock based on one PLL.
+
 config COMMON_CLK_FSL_FLEXSPI
 	tristate "Clock driver for FlexSPI on Layerscape SoCs"
 	depends on ARCH_LAYERSCAPE || COMPILE_TEST
diff --git a/drivers/clk/Makefile b/drivers/clk/Makefile
index 14fa8d4ecc1f..81c4d11ca437 100644
--- a/drivers/clk/Makefile
+++ b/drivers/clk/Makefile
@@ -32,6 +32,7 @@  obj-$(CONFIG_ARCH_CLPS711X)		+= clk-clps711x.o
 obj-$(CONFIG_COMMON_CLK_CS2000_CP)	+= clk-cs2000-cp.o
 obj-$(CONFIG_ARCH_SPARX5)		+= clk-sparx5.o
 obj-$(CONFIG_COMMON_CLK_EN7523)		+= clk-en7523.o
+obj-$(CONFIG_COMMON_CLK_EYEQ5)		+= clk-eyeq5.o
 obj-$(CONFIG_COMMON_CLK_FIXED_MMIO)	+= clk-fixed-mmio.o
 obj-$(CONFIG_COMMON_CLK_FSL_FLEXSPI)	+= clk-fsl-flexspi.o
 obj-$(CONFIG_COMMON_CLK_FSL_SAI)	+= clk-fsl-sai.o
diff --git a/drivers/clk/clk-eyeq5.c b/drivers/clk/clk-eyeq5.c
new file mode 100644
index 000000000000..475dfcdc8af1
--- /dev/null
+++ b/drivers/clk/clk-eyeq5.c
@@ -0,0 +1,414 @@ 
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * PLL clock driver for the Mobileye EyeQ5 platform.
+ *
+ * This controller handles 10 read-only PLLs, all derived from the same main
+ * crystal clock. It also exposes one divider clock, a child of one of the
+ * PLLs. The parent clock is expected to be constant. This driver's registers
+ * live in a shared region called OLB. Two PLLs must be initialized by
+ * of_clk_init().
+ *
+ * We use eq5c_ as prefix, as-in "EyeQ5 Clock", but way shorter.
+ *
+ * Copyright (C) 2024 Mobileye Vision Technologies Ltd.
+ */
+
+#define pr_fmt(fmt) "clk-eyeq5: " fmt
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk-provider.h>
+#include <linux/mfd/syscon.h>
+#include <linux/mod_devicetable.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+#include <dt-bindings/clock/mobileye,eyeq5-clk.h>
+
+/*
+ * PLL control & status registers, n=0..1
+ * 0x02c..0x078
+ */
+#define OLB_PCSR_CPU(n)				(0x02C + (n) * 4) /* CPU */
+#define OLB_PCSR_VMP(n)				(0x034 + (n) * 4) /* VMP */
+#define OLB_PCSR_PMA(n)				(0x03C + (n) * 4) /* PMA */
+#define OLB_PCSR_VDI(n)				(0x044 + (n) * 4) /* VDI */
+#define OLB_PCSR_DDR0(n)			(0x04C + (n) * 4) /* DDR0 */
+#define OLB_PCSR_PCI(n)				(0x054 + (n) * 4) /* PCI */
+#define OLB_PCSR_PER(n)				(0x05C + (n) * 4) /* PER */
+#define OLB_PCSR_PMAC(n)			(0x064 + (n) * 4) /* PMAC */
+#define OLB_PCSR_MPC(n)				(0x06c + (n) * 4) /* MPC */
+#define OLB_PCSR_DDR1(n)			(0x074 + (n) * 4) /* DDR1 */
+
+/* In frac mode, it enables fractional noise canceling DAC. Else, no function. */
+#define OLB_PCSR0_DAC_EN			BIT(0)
+/* Fractional or integer mode */
+#define OLB_PCSR0_DSM_EN			BIT(1)
+#define OLB_PCSR0_PLL_EN			BIT(2)
+/* All clocks output held at 0 */
+#define OLB_PCSR0_FOUTPOSTDIV_EN		BIT(3)
+#define OLB_PCSR0_POST_DIV1			GENMASK(6, 4)
+#define OLB_PCSR0_POST_DIV2			GENMASK(9, 7)
+#define OLB_PCSR0_REF_DIV			GENMASK(15, 10)
+#define OLB_PCSR0_INTIN				GENMASK(27, 16)
+#define OLB_PCSR0_BYPASS			BIT(28)
+/* Bits 30..29 are reserved */
+#define OLB_PCSR0_PLL_LOCKED			BIT(31)
+
+#define OLB_PCSR1_RESET				BIT(0)
+#define OLB_PCSR1_SSGC_DIV			GENMASK(4, 1)
+/* Spread amplitude (% = 0.1 * SPREAD[4:0]) */
+#define OLB_PCSR1_SPREAD			GENMASK(9, 5)
+#define OLB_PCSR1_DIS_SSCG			BIT(10)
+/* Down-spread or center-spread */
+#define OLB_PCSR1_DOWN_SPREAD			BIT(11)
+#define OLB_PCSR1_FRAC_IN			GENMASK(31, 12)
+
+static struct clk_hw_onecell_data *eq5c_clk_data;
+static struct regmap *eq5c_olb;
+
+struct eq5c_pll {
+	int		index;
+	const char	*name;
+	u32		reg;
+};
+
+/* Required early for the GIC timer (pll-cpu) and UARTs (pll-per). */
+static const struct eq5c_pll eq5c_early_plls[] = {
+	{ .index = EQ5C_PLL_CPU, .name = "pll-cpu",  .reg = OLB_PCSR_CPU(0), },
+	{ .index = EQ5C_PLL_PER, .name = "pll-per",  .reg = OLB_PCSR_PER(0), },
+};
+
+static const struct eq5c_pll eq5c_plls[] = {
+	{ .index = EQ5C_PLL_VMP,  .name = "pll-vmp",  .reg = OLB_PCSR_VMP(0),  },
+	{ .index = EQ5C_PLL_PMA,  .name = "pll-pma",  .reg = OLB_PCSR_PMA(0),  },
+	{ .index = EQ5C_PLL_VDI,  .name = "pll-vdi",  .reg = OLB_PCSR_VDI(0),  },
+	{ .index = EQ5C_PLL_DDR0, .name = "pll-ddr0", .reg = OLB_PCSR_DDR0(0), },
+	{ .index = EQ5C_PLL_PCI,  .name = "pll-pci",  .reg = OLB_PCSR_PCI(0),  },
+	{ .index = EQ5C_PLL_PMAC, .name = "pll-pmac", .reg = OLB_PCSR_PMAC(0), },
+	{ .index = EQ5C_PLL_MPC,  .name = "pll-mpc",  .reg = OLB_PCSR_MPC(0),  },
+	{ .index = EQ5C_PLL_DDR1, .name = "pll-ddr1", .reg = OLB_PCSR_DDR1(0), },
+};
+
+#define EQ5C_OSPI_DIV_CLK_NAME	"div-ospi"
+
+#define EQ5C_NB_CLKS	(ARRAY_SIZE(eq5c_early_plls) + ARRAY_SIZE(eq5c_plls) + 1)
+
+static int eq5c_pll_parse_registers(u32 r0, u32 r1, unsigned long *mult,
+				    unsigned long *div, unsigned long *acc)
+{
+	if (r0 & OLB_PCSR0_BYPASS) {
+		*mult = 1;
+		*div = 1;
+		*acc = 0;
+		return 0;
+	}
+
+	if (!(r0 & OLB_PCSR0_PLL_LOCKED))
+		return -EINVAL;
+
+	*mult = FIELD_GET(OLB_PCSR0_INTIN, r0);
+	*div = FIELD_GET(OLB_PCSR0_REF_DIV, r0);
+	if (r0 & OLB_PCSR0_FOUTPOSTDIV_EN)
+		*div *= FIELD_GET(OLB_PCSR0_POST_DIV1, r0) *
+			FIELD_GET(OLB_PCSR0_POST_DIV2, r0);
+
+	/* Fractional mode, in 2^20 (0x100000) parts. */
+	if (r0 & OLB_PCSR0_DSM_EN) {
+		*div *= 0x100000;
+		*mult = *mult * 0x100000 + FIELD_GET(OLB_PCSR1_FRAC_IN, r1);
+	}
+
+	if (!*mult || !*div)
+		return -EINVAL;
+
+	/* Spread spectrum. */
+	if (!(r1 & (OLB_PCSR1_RESET | OLB_PCSR1_DIS_SSCG))) {
+		/*
+		 * Spread is 1/1000 parts of frequency, accuracy is half of
+		 * that. To get accuracy, convert to ppb (parts per billion).
+		 */
+		u32 spread = FIELD_GET(OLB_PCSR1_SPREAD, r1);
+		*acc = spread * 500000;
+		if (r1 & OLB_PCSR1_DOWN_SPREAD) {
+			/*
+			 * Downspreading: the central frequency is half a
+			 * spread lower.
+			 */
+			*mult *= 2000 - spread;
+			*div *= 2000;
+		}
+	} else {
+		*acc = 0;
+	}
+
+	return 0;
+}
+
+#define OLB_OSPI_REG		0x11C
+#define OLB_OSPI_DIV_MASK	GENMASK(3, 0)
+#define OLB_OSPI_DIV_MASK_WIDTH	4
+
+static const struct clk_div_table eq5c_ospi_div_table[] = {
+	{ .val = 0, .div = 2 },
+	{ .val = 1, .div = 4 },
+	{ .val = 2, .div = 6 },
+	{ .val = 3, .div = 8 },
+	{ .val = 4, .div = 10 },
+	{ .val = 5, .div = 12 },
+	{ .val = 6, .div = 14 },
+	{ .val = 7, .div = 16 },
+	{} /* sentinel */
+};
+
+struct eq5c_ospi_div {
+	struct clk_hw	hw;
+	struct device	*dev;
+	struct regmap	*olb;
+};
+
+static struct eq5c_ospi_div *clk_to_priv(struct clk_hw *hw)
+{
+	return container_of(hw, struct eq5c_ospi_div, hw);
+}
+
+static unsigned long eq5c_ospi_div_recalc_rate(struct clk_hw *hw,
+					       unsigned long parent_rate)
+{
+	struct eq5c_ospi_div *div = clk_to_priv(hw);
+	unsigned int val;
+	int ret;
+
+	ret = regmap_read(div->olb, OLB_OSPI_REG, &val);
+
+	if (ret) {
+		dev_err(div->dev, "regmap_read failed: %d\n", ret);
+		return 0;
+	}
+
+	val = FIELD_GET(OLB_OSPI_DIV_MASK, val);
+
+	return divider_recalc_rate(hw, parent_rate, val,
+				   eq5c_ospi_div_table, 0,
+				   OLB_OSPI_DIV_MASK_WIDTH);
+}
+
+static long eq5c_ospi_div_round_rate(struct clk_hw *hw,
+				     unsigned long rate, unsigned long *prate)
+{
+	return divider_round_rate(hw, rate, prate, eq5c_ospi_div_table,
+				  OLB_OSPI_DIV_MASK_WIDTH, 0);
+}
+
+static int eq5c_ospi_div_determine_rate(struct clk_hw *hw,
+					struct clk_rate_request *req)
+{
+	return divider_determine_rate(hw, req, eq5c_ospi_div_table,
+				      OLB_OSPI_DIV_MASK_WIDTH, 0);
+}
+
+static int eq5c_ospi_div_set_rate(struct clk_hw *hw,
+				  unsigned long rate, unsigned long parent_rate)
+{
+	struct eq5c_ospi_div *div = clk_to_priv(hw);
+	unsigned int val;
+	int value, ret;
+
+	value = divider_get_val(rate, parent_rate, eq5c_ospi_div_table,
+				OLB_OSPI_DIV_MASK_WIDTH, 0);
+	if (value < 0)
+		return value;
+
+	ret = regmap_read(div->olb, OLB_OSPI_REG, &val);
+	if (ret) {
+		dev_err(div->dev, "regmap_read failed: %d\n", ret);
+		return ret;
+	}
+
+	val &= ~OLB_OSPI_DIV_MASK;
+	val |= FIELD_PREP(OLB_OSPI_DIV_MASK, value);
+
+	ret = regmap_write(div->olb, OLB_OSPI_REG, val);
+	if (ret) {
+		dev_err(div->dev, "regmap_write failed: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static const struct clk_ops eq5c_ospi_div_ops = {
+	.recalc_rate = eq5c_ospi_div_recalc_rate,
+	.round_rate = eq5c_ospi_div_round_rate,
+	.determine_rate = eq5c_ospi_div_determine_rate,
+	.set_rate = eq5c_ospi_div_set_rate,
+};
+
+static struct clk_hw *eq5c_init_ospi_div(struct device *dev, const char *name,
+					 struct regmap *olb,
+					 const struct clk_hw *parent)
+{
+	struct eq5c_ospi_div *div;
+	int ret;
+
+	div = kzalloc(sizeof(*div), GFP_KERNEL);
+	if (!div)
+		return ERR_PTR(-ENOENT);
+
+	div->dev = dev;
+	div->olb = olb;
+	div->hw.init = CLK_HW_INIT_HW(name, parent, &eq5c_ospi_div_ops, 0);
+
+	ret = clk_hw_register(dev, &div->hw);
+	if (ret) {
+		dev_err(dev, "failed registering %s: %d\n", name, ret);
+		kfree(div);
+		return ERR_PTR(ret);
+	}
+
+	return &div->hw;
+}
+
+static int eq5c_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct device_node *np = dev->of_node;
+	struct clk_hw *hw;
+	int i;
+
+	if (IS_ERR(eq5c_clk_data))
+		return PTR_ERR(eq5c_clk_data);
+	else if (!eq5c_clk_data)
+		return -EINVAL;
+
+	for (i = 0; i < ARRAY_SIZE(eq5c_plls); i++) {
+		const struct eq5c_pll *pll = &eq5c_plls[i];
+		unsigned long mult, div, acc;
+		u32 r0, r1;
+		int ret;
+
+		regmap_read(eq5c_olb, pll->reg, &r0);
+		regmap_read(eq5c_olb, pll->reg + sizeof(r0), &r1);
+
+		ret = eq5c_pll_parse_registers(r0, r1, &mult, &div, &acc);
+		if (ret) {
+			dev_warn(dev, "failed parsing state of %s\n", pll->name);
+			continue;
+		}
+
+		hw = clk_hw_register_fixed_factor_with_accuracy_fwname(dev, np,
+				pll->name, "ref", 0, mult, div, acc);
+		eq5c_clk_data->hws[pll->index] = hw;
+		if (IS_ERR(hw)) {
+			dev_err(dev, "failed registering %s: %ld\n",
+				pll->name, PTR_ERR(hw));
+		}
+	}
+
+	/*
+	 * Register the OSPI table-based divider clock manually. This is
+	 * equivalent to drivers/clk/clk-divider.c, but using regmap to access
+	 * register.
+	 */
+	hw = eq5c_init_ospi_div(dev, EQ5C_OSPI_DIV_CLK_NAME, eq5c_olb,
+				eq5c_clk_data->hws[EQ5C_PLL_PER]);
+	eq5c_clk_data->hws[EQ5C_DIV_OSPI] = hw;
+
+	return 0;
+}
+
+static const struct of_device_id eq5c_match_table[] = {
+	{ .compatible = "mobileye,eyeq5-clk" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, eq5c_match_table);
+
+static struct platform_driver eq5c_driver = {
+	.probe = eq5c_probe,
+	.driver = {
+		.name = "clk-eyeq5",
+		.of_match_table = eq5c_match_table,
+	},
+};
+
+builtin_platform_driver(eq5c_driver);
+
+static void __init eq5c_init(struct device_node *np)
+{
+	struct device_node *parent_np = of_get_parent(np);
+	int i, ret;
+
+	eq5c_clk_data = kzalloc(struct_size(eq5c_clk_data, hws, EQ5C_NB_CLKS),
+				GFP_KERNEL);
+	if (!eq5c_clk_data) {
+		ret = -ENOMEM;
+		goto err;
+	}
+
+	eq5c_clk_data->num = EQ5C_NB_CLKS;
+
+	/*
+	 * Mark all clocks as deferred. We register some now and others at
+	 * platform device probe.
+	 */
+	for (i = 0; i < EQ5C_NB_CLKS; i++)
+		eq5c_clk_data->hws[i] = ERR_PTR(-EPROBE_DEFER);
+
+	/*
+	 * Currently, if OLB is not available, we log an error, fail init then
+	 * fail probe. We might want to change this behavior and assume all
+	 * clocks are in bypass mode; this is what is being done in the vendor
+	 * driver.
+	 *
+	 * It is unclear if there are valid situations where the OLB region
+	 * would be inaccessible.
+	 */
+	eq5c_olb = ERR_PTR(-ENODEV);
+	if (parent_np)
+		eq5c_olb = syscon_node_to_regmap(parent_np);
+	if (IS_ERR(eq5c_olb)) {
+		pr_err("failed getting regmap: %ld\n", PTR_ERR(eq5c_olb));
+		ret = PTR_ERR(eq5c_olb);
+		goto err;
+	}
+
+	for (i = 0; i < ARRAY_SIZE(eq5c_early_plls); i++) {
+		const struct eq5c_pll *pll = &eq5c_early_plls[i];
+		unsigned long mult, div, acc;
+		struct clk_hw *hw;
+		u32 r0, r1;
+
+		regmap_read(eq5c_olb, pll->reg, &r0);
+		regmap_read(eq5c_olb, pll->reg + sizeof(r0), &r1);
+
+		ret = eq5c_pll_parse_registers(r0, r1, &mult, &div, &acc);
+		if (ret) {
+			pr_warn("failed parsing state of %s\n", pll->name);
+			continue;
+		}
+
+		hw = clk_hw_register_fixed_factor_with_accuracy_fwname(NULL,
+				np, pll->name, "ref", 0, mult, div, acc);
+		eq5c_clk_data->hws[pll->index] = hw;
+		if (IS_ERR(hw)) {
+			pr_err("failed registering %s: %ld\n",
+			       pll->name, PTR_ERR(hw));
+		}
+	}
+
+	ret = of_clk_add_hw_provider(np, of_clk_hw_onecell_get, eq5c_clk_data);
+	if (ret) {
+		pr_err("failed registering clk provider: %d\n", ret);
+		goto err;
+	}
+
+	return;
+
+err:
+	kfree(eq5c_clk_data);
+	/* Signal to platform driver probe that we failed init. */
+	eq5c_clk_data = ERR_PTR(ret);
+	eq5c_olb = NULL;
+}
+
+CLK_OF_DECLARE_DRIVER(eq5c, "mobileye,eyeq5-clk", eq5c_init);