git: dcb10e3add17 - main - clk: Initial support for the SpacemiT K1 clock control units

From: Bojan Novković <bnovkov_at_FreeBSD.org>
Date: Sun, 16 Aug 2026 17:17:12 UTC
The branch main has been updated by bnovkov:

URL: https://cgit.FreeBSD.org/src/commit/?id=dcb10e3add17259715352885dfc55ca92061d2ed

commit dcb10e3add17259715352885dfc55ca92061d2ed
Author:     Bojan Novković <bnovkov@FreeBSD.org>
AuthorDate: 2026-05-15 09:13:13 +0000
Commit:     Bojan Novković <bnovkov@FreeBSD.org>
CommitDate: 2026-08-16 17:14:19 +0000

    clk: Initial support for the SpacemiT K1 clock control units
    
    This change implements the core clknode methods for the SpacemiT K1
    clock control units. These methods were used to implement drivers
    for the APMU and PLL CCUs. The initial driver for the APMU CCU
    only contains clock definitions for the SDHCI controller for now.
    
    Differential Revision:  https://reviews.freebsd.org/D57176
    Reviewed by:    mmel, mhorne, manu
---
 sys/dev/clk/spacemit/k1_apmu.c    | 114 +++++++++++
 sys/dev/clk/spacemit/k1_clk.c     | 409 ++++++++++++++++++++++++++++++++++++++
 sys/dev/clk/spacemit/k1_clk.h     | 146 ++++++++++++++
 sys/dev/clk/spacemit/k1_pll.c     | 200 +++++++++++++++++++
 sys/riscv/conf/std.spacemit       |   1 +
 sys/riscv/spacemit/files.spacemit |   3 +
 6 files changed, 873 insertions(+)

diff --git a/sys/dev/clk/spacemit/k1_apmu.c b/sys/dev/clk/spacemit/k1_apmu.c
new file mode 100644
index 000000000000..8869c7e74f47
--- /dev/null
+++ b/sys/dev/clk/spacemit/k1_apmu.c
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2026 Bojan Novković <bnovkov@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Driver for the SpacemiT K1 SoC APMU clock control unit.
+ *
+ * Written using SpacemiT's K1 Chip Product Documentation,
+ * Section 9., "Top System (1/2)" and the accompanying clock
+ * tree schematic, available at https://www.spacemit.com/community/document/
+ */
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/bus.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/mutex.h>
+#include <sys/resource.h>
+#include <sys/rman.h>
+
+#include <machine/bus.h>
+
+#include <dev/fdt/simplebus.h>
+#include <dev/ofw/ofw_bus.h>
+#include <dev/ofw/ofw_bus_subr.h>
+
+#include <dev/clk/clk.h>
+#include <dev/hwreset/hwreset.h>
+
+#include <dt-bindings/clock/spacemit,k1-syscon.h>
+
+#include "k1_clk.h"
+
+#define K1_SDH0_CLK_RES_CTRL 0x54
+#define K1_SDH1_CLK_RES_CTRL 0x58
+#define K1_SDH2_CLK_RES_CTRL 0xE0
+#define K1_PMUA_ACLK_CTRL    0x388
+
+static const struct k1_clk_def apmu_clks[] = {
+K1_CLK_DIV_MUX(pmua_aclk, CLK_PMUA_ACLK, K1_PMUA_ACLK_CTRL,
+	1, 0, 2, 1, 4,
+	"pll1_d10_aud", "pll1_d8"),
+
+K1_CLK_GATE(sdh_axi_aclk, CLK_SDH_AXI, K1_SDH0_CLK_RES_CTRL, 4,
+	"pmua_aclk"),
+
+K1_CLK_DIV_MUX_GATE(sdh0_clk, CLK_SDH0, K1_SDH0_CLK_RES_CTRL,
+	3, 8, 3, 5, 4, 11,
+	"pll1_d6", "pll1_d4", "pll2_d8", "pll2_d5",
+	"pll1_d11", "pll1_d11" , "pll1_d13", "pll1_d23"),
+
+K1_CLK_DIV_MUX_GATE(sdh1_clk, CLK_SDH1, K1_SDH1_CLK_RES_CTRL,
+	3, 8, 3, 5, 4, 11,
+	"pll1_d6", "pll1_d4", "pll2_d8", "pll2_d5",
+	"pll1_d11", "pll1_d11" , "pll1_d13", "pll1_d23"),
+
+K1_CLK_DIV_MUX_GATE(sdh2_clk, CLK_SDH2, K1_SDH2_CLK_RES_CTRL,
+	3, 8, 3, 5, 4, 11,
+	"pll1_d6", "pll1_d4", "pll2_d8", "pll1_d3",
+	"pll1_d11", "pll1_d11" , "pll1_d13", "pll1_d23")
+};
+
+static const struct k1_reset_def apmu_resets[] = {
+K1_RESET(RESET_SDH_AXI, K1_SDH0_CLK_RES_CTRL, 0, 3),
+K1_RESET(RESET_SDH0, K1_SDH0_CLK_RES_CTRL, 0, 1),
+K1_RESET(RESET_SDH1, K1_SDH1_CLK_RES_CTRL, 0, 1),
+K1_RESET(RESET_SDH2, K1_SDH2_CLK_RES_CTRL, 0, 1),
+};
+
+#define K1_APMU_NRESETS (RESET_SDH2 + 1);
+
+static int
+k1_apmu_probe(device_t dev)
+{
+
+	if (!ofw_bus_status_okay(dev))
+		return (ENXIO);
+	if (!ofw_bus_is_compatible(dev, "spacemit,k1-syscon-apmu"))
+		return (ENXIO);
+	device_set_desc(dev, "SpacemitT K1 APMU Clock Controller");
+
+	return (BUS_PROBE_DEFAULT);
+}
+
+static int
+k1_apmu_attach(device_t dev)
+{
+	struct k1_clkdev_softc *sc;
+
+	sc = device_get_softc(dev);
+	sc->clks = apmu_clks;
+	sc->nclks = nitems(apmu_clks);
+
+	sc->resets = apmu_resets;
+	sc->nresets = K1_APMU_NRESETS;
+
+	return (k1_clk_attach(dev));
+}
+
+static device_method_t k1_apmu_clkdev_methods[] = {
+	/* device_if */
+	DEVMETHOD(device_probe, 	k1_apmu_probe),
+	DEVMETHOD(device_attach, 	k1_apmu_attach),
+
+	DEVMETHOD_END
+};
+
+DEFINE_CLASS_1(k1_apmu, k1_apmu_driver, k1_apmu_clkdev_methods,
+    sizeof(struct k1_clkdev_softc), k1_clkdev_driver);
+EARLY_DRIVER_MODULE(k1_apmu, simplebus, k1_apmu_driver, 0, 0,
+    BUS_PASS_BUS + BUS_PASS_ORDER_LAST);
diff --git a/sys/dev/clk/spacemit/k1_clk.c b/sys/dev/clk/spacemit/k1_clk.c
new file mode 100644
index 000000000000..3ee1ae66c1b5
--- /dev/null
+++ b/sys/dev/clk/spacemit/k1_clk.c
@@ -0,0 +1,409 @@
+/*
+ * Copyright (c) 2026 Bojan Novković <bnovkov@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Generic clock routines for the SpacemiT K1 SoC.
+ *
+ * Written using SpacemiT's K1 Chip Product Documentation,
+ * Section 9., "Top System (1/2)", available at
+ * https://www.spacemit.com/community/document/
+ */
+
+#include <sys/cdefs.h>
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/bus.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/mutex.h>
+
+#include <machine/bus.h>
+#include <sys/rman.h>
+#include <machine/resource.h>
+
+#include <dev/fdt/simplebus.h>
+
+#include <dev/ofw/ofw_bus.h>
+#include <dev/ofw/ofw_bus_subr.h>
+
+#include <dev/clk/clk.h>
+
+#include <dev/hwreset/hwreset.h>
+
+#include "k1_clk.h"
+#include "clkdev_if.h"
+#include "hwreset_if.h"
+
+#define K1_CLK_FCREQ_NRETRIES 5
+
+#define DEVICE_LOCK(_clk)				\
+	CLKDEV_DEVICE_LOCK(clknode_get_device(_clk))
+#define DEVICE_UNLOCK(_clk)				\
+	CLKDEV_DEVICE_UNLOCK(clknode_get_device(_clk))
+
+#define READ4(sc, reg)		bus_read_4((sc)->res, (reg))
+#define WRITE4(sc, reg, val)	bus_write_4((sc)->res, (reg), (val))
+
+static int
+k1_clk_register(struct clkdom *clkdom,
+    const struct k1_clk_def *clkdef, clknode_class_t class)
+{
+	struct k1_clk_def *sc;
+	struct clknode *clk;
+
+	clk = clknode_create(clkdom, class,
+	    &clkdef->clkdef);
+	if (clk == NULL)
+		return (1);
+
+	sc = clknode_get_softc(clk);
+	*sc = *clkdef;
+	clknode_register(clkdom, clk);
+
+	return (0);
+}
+
+static void
+k1_clkdev_lock(device_t dev)
+{
+	struct k1_clkdev_softc *sc;
+
+	sc = device_get_softc(dev);
+	mtx_lock(&sc->mtx);
+}
+
+static void
+k1_clkdev_unlock(device_t dev)
+{
+	struct k1_clkdev_softc *sc;
+
+	sc = device_get_softc(dev);
+	mtx_unlock(&sc->mtx);
+}
+
+int
+k1_clk_attach(device_t dev)
+{
+	struct k1_clkdev_softc *sc;
+	clknode_class_t class;
+	int error, rid, i;
+
+	sc = device_get_softc(dev);
+	mtx_init(&sc->mtx, device_get_nameunit(dev), NULL, MTX_DEF);
+
+	if (sc->resets != NULL)
+		hwreset_register_ofw_provider(dev);
+
+	sc->clkdom = clkdom_create(dev);
+	if (sc->clkdom == NULL) {
+		device_printf(dev, "Couldn't create clkdom\n");
+		return (ENXIO);
+	}
+
+	rid = 0;
+	sc->res = bus_alloc_resource_any(dev, SYS_RES_MEMORY,
+	    &rid, RF_ACTIVE);
+	if (sc->res == NULL) {
+		device_printf(dev, "Can't allocate memory\n");
+		return (ENXIO);
+	}
+
+	class = sc->clknode_class != NULL ?
+	    sc->clknode_class : &k1_clknode_class;
+	for (i = 0; i < sc->nclks; i++) {
+		error = k1_clk_register(sc->clkdom, &sc->clks[i], class);
+		if (error != 0) {
+			device_printf(dev, "Failed to register clock '%s'\n",
+			    sc->clks[i].clkdef.name);
+			bus_free_resource(dev, SYS_RES_MEMORY, sc->res);
+			return (error);
+		}
+	}
+
+	error = clkdom_finit(sc->clkdom);
+	if (error != 0) {
+		device_printf(dev, "clkdom_finit() returned %d\n", error);
+		bus_free_resource(dev, SYS_RES_MEMORY, sc->res);
+		return (error);
+	}
+
+	if (bootverbose)
+		clkdom_dump(sc->clkdom);
+
+	return (0);
+}
+
+static int
+k1_clkdev_reset_assert(device_t dev, intptr_t id, bool assert)
+{
+	struct k1_clkdev_softc *sc;
+	const struct k1_reset_def *rdef;
+	uint32_t val;
+
+	sc = device_get_softc(dev);
+
+	if (id >= sc->nresets)
+		return (0);
+	rdef = &sc->resets[id];
+	if (rdef->reg == 0)
+		return (0);
+
+	mtx_lock(&sc->mtx);
+	val = READ4(sc, rdef->reg);
+	if (assert)
+		val |= rdef->assert_mask;
+	else
+		val &= ~rdef->deassert_mask;
+	WRITE4(sc, rdef->reg, val);
+	mtx_unlock(&sc->mtx);
+
+	return (0);
+}
+
+static device_method_t k1_clkdev_methods[] = {
+	/* clkdev interface */
+	DEVMETHOD(clkdev_device_lock,	k1_clkdev_lock),
+	DEVMETHOD(clkdev_device_unlock,	k1_clkdev_unlock),
+
+	/* Reset interface */
+	DEVMETHOD(hwreset_assert,	k1_clkdev_reset_assert),
+
+	DEVMETHOD_END
+};
+DEFINE_CLASS_0(k1_clkdev, k1_clkdev_driver, k1_clkdev_methods,
+    sizeof(struct k1_clkdev_softc));
+
+static int
+k1_clk_set_gate(struct clknode *clk, bool enable)
+{
+	struct k1_clkdev_softc *sc;
+	struct k1_clk_def *clk_sc;
+	uint32_t reg;
+
+	sc = device_get_softc(clknode_get_device(clk));
+	clk_sc = clknode_get_softc(clk);
+	if (clk_sc->gate_mask == 0 || clk_sc->reg == 0)
+		return (0);
+
+	DEVICE_LOCK(clk);
+	reg = READ4(sc, clk_sc->reg);
+	if (enable)
+		reg |= clk_sc->gate_mask;
+	else
+		reg &= ~clk_sc->gate_mask;
+	WRITE4(sc, clk_sc->reg, reg);
+	DEVICE_UNLOCK(clk);
+
+	return (0);
+}
+
+static bool
+k1_clk_rounding_check(uint64_t curfreq, uint64_t req, int flags)
+{
+	switch (CLK_SET_ROUND(flags)) {
+	case CLK_SET_ROUND_DOWN:
+		return curfreq <= req;
+	case CLK_SET_ROUND_UP:
+		return curfreq >= req;
+	case CLK_SET_ROUND_EXACT:
+		/* CLK_SET_ROUND_EXACT is enforced in 'k1_clk_set_freq'. */
+	case CLK_SET_ROUND_ANY:
+	default:
+		return true;
+	}
+}
+
+/*
+ * Find the closest matching rate given the requested frequency 'req' and
+ * rounding mode in 'flags' by going through all possible combinations of
+ * frequency divisor values and parent clock rates.
+ */
+static uint64_t
+k1_clk_find_best_rate(struct clknode *clk, struct k1_clk_def *sc, uint64_t req,
+    int flags, int *parentp, uint32_t *divp)
+{
+	uint64_t best_match, best_delta, p_freq, candidate_freq;
+	uint32_t i, divmax, best_div;
+	struct clknode *p_clk;
+	const char **p_names;
+	int best_parent;
+	int p_idx;
+
+	best_div = 1;
+	best_match = 0;
+	best_parent = 0;
+	best_delta = UINT64_MAX;
+	p_names = clknode_get_parent_names(clk);
+	for (p_idx = 0; p_idx != clknode_get_parents_num(clk); p_idx++) {
+		p_clk = clknode_find_by_name(p_names[p_idx]);
+		clknode_get_freq(p_clk, &p_freq);
+
+		divmax = 1 << sc->div_nbits;
+		for (i = 1; i <= divmax; i++) {
+			candidate_freq = p_freq / i;
+			if (clk_freq_diff(req, candidate_freq) < best_delta &&
+			    k1_clk_rounding_check(candidate_freq, req, flags)) {
+				best_match = candidate_freq;
+				best_parent = p_idx;
+				best_delta = clk_freq_diff(req, candidate_freq);
+				best_div = i;
+			}
+		}
+	}
+
+	*parentp = best_parent;
+	*divp = best_div;
+
+	return (best_match);
+}
+
+/*
+ * Certain clocks must perform a "frequency change request" after
+ * reparenting or changing the clock's frequency divisor. This is done
+ * by setting the appropriate FC bit in the clock's configuration
+ * register and waiting until the hardware clears it, indicating
+ * that the clock's divisor or parent index was successfully updated.
+ */
+static int
+k1_clk_send_fc_req(struct clknode *clk)
+{
+	struct k1_clkdev_softc *sc;
+	struct k1_clk_def *clk_sc;
+	uint32_t val;
+	int retries;
+
+	sc = device_get_softc(clknode_get_device(clk));
+	clk_sc = clknode_get_softc(clk);
+
+	if (clk_sc->fc_mask == 0)
+		return (0);
+
+	retries = K1_CLK_FCREQ_NRETRIES;
+	val = READ4(sc, clk_sc->reg);
+	val |= clk_sc->fc_mask;
+	WRITE4(sc, clk_sc->reg, val);
+	while (retries-- > 0) {
+		DELAY(1000);
+		val = READ4(sc, clk_sc->reg);
+		if ((val & clk_sc->fc_mask) == 0)
+			break;
+	}
+	if ((val & clk_sc->fc_mask) != 0) {
+		/* The FC bit is still set, something went wrong. */
+		return (ENXIO);
+	}
+
+	return (0);
+}
+
+static int
+k1_clk_set_freq(struct clknode *clk, uint64_t fparent, uint64_t *fout,
+    int flags, int *stop)
+{
+	uint32_t div_mask, mux_mask;
+	int best_div, best_parent;
+	struct k1_clkdev_softc *sc;
+	struct k1_clk_def *clk_sc;
+	uint64_t best_match;
+	uint32_t val;
+	device_t dev;
+	int p_idx;
+
+	dev = clknode_get_device(clk);
+	sc = device_get_softc(clknode_get_device(clk));
+	clk_sc = clknode_get_softc(clk);
+	p_idx = clknode_get_parent_idx(clk);
+
+	best_match = k1_clk_find_best_rate(clk, clk_sc, *fout, flags,
+	    &best_parent, &best_div);
+	*stop = 1;
+	if (best_match == 0 ||
+	    (best_match != *fout &&
+		CLK_SET_ROUND(flags) == CLK_SET_ROUND_EXACT))
+		return (ERANGE);
+
+	*fout = best_match;
+	if ((flags & CLK_SET_DRYRUN) != 0)
+		return (0);
+
+	DEVICE_LOCK(clk);
+	if (clk_sc->mux_nbits != 0 && p_idx != best_parent) {
+
+		clknode_set_parent_by_idx(clk, best_parent);
+		mux_mask = (1 << clk_sc->mux_nbits) - 1;
+		mux_mask <<= clk_sc->mux_shift;
+		val = READ4(sc, clk_sc->reg);
+		val &= ~mux_mask;
+		val |= (p_idx << clk_sc->mux_shift);
+		WRITE4(sc, clk_sc->reg, val);
+
+		if (k1_clk_send_fc_req(clk) != 0) {
+			device_printf(dev,
+			    "%s: failed mux frequency change request for '%s'\n",
+			    __func__, clk_sc->clkdef.name);
+			DEVICE_UNLOCK(clk);
+			return (ENXIO);
+		}
+	}
+
+	if (clk_sc->div_nbits != 0) {
+		div_mask = (1 << clk_sc->div_nbits) - 1;
+		val = READ4(sc, clk_sc->reg);
+		val &= ~(div_mask << clk_sc->div_shift);
+
+		/* The final divisor value is calculated as 'div_value' + 1. */
+		val |= (best_div - 1) << clk_sc->div_shift;
+		WRITE4(sc, clk_sc->reg, val);
+
+		if (k1_clk_send_fc_req(clk) != 0) {
+			device_printf(dev,
+			    "%s: failed div frequency change request for '%s'\n",
+			    __func__, clk_sc->clkdef.name);
+			DEVICE_UNLOCK(clk);
+			return (ENXIO);
+		}
+	}
+	DEVICE_UNLOCK(clk);
+
+	return (0);
+}
+
+static int
+k1_clk_init(struct clknode *clk, device_t dev)
+{
+	struct k1_clkdev_softc *sc;
+	struct k1_clk_def *clk_sc;
+	uint32_t val, mux_mask;
+	int pidx;
+
+	sc = device_get_softc(clknode_get_device(clk));
+	clk_sc = clknode_get_softc(clk);
+
+	if (clk_sc->mux_shift == 0) {
+		clknode_init_parent_idx(clk, 0);
+		return (0);
+	}
+	DEVICE_LOCK(clk);
+	val = READ4(sc, clk_sc->reg);
+	DEVICE_UNLOCK(clk);
+	mux_mask = (1 << clk_sc->mux_nbits) - 1;
+	pidx = (val >> clk_sc->mux_shift) & mux_mask;
+
+	clknode_init_parent_idx(clk, pidx);
+
+	return (0);
+}
+
+static clknode_method_t k1_clknode_methods[] = {
+	CLKNODEMETHOD(clknode_init,		k1_clk_init),
+	CLKNODEMETHOD(clknode_set_gate,		k1_clk_set_gate),
+	CLKNODEMETHOD(clknode_set_freq,		k1_clk_set_freq),
+	CLKNODEMETHOD_END
+};
+
+DEFINE_CLASS_1(k1_clknode, k1_clknode_class,
+    k1_clknode_methods, sizeof(struct k1_clk_def), clknode_class);
diff --git a/sys/dev/clk/spacemit/k1_clk.h b/sys/dev/clk/spacemit/k1_clk.h
new file mode 100644
index 000000000000..384e3aa8467e
--- /dev/null
+++ b/sys/dev/clk/spacemit/k1_clk.h
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2026 Bojan Novković <bnovkov@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+
+/*
+ * Common helper macros for the Spacemit K1 SoC clocks.
+ */
+
+#ifndef _K1_CLK_H_
+#define	_K1_CLK_H_
+
+#include <dev/clk/clk.h>
+
+/*
+ * Helper macro to count the number of parent clocks passed
+ * to the K1_CLK_COMMON macro. The maximum number of parents
+ * for the K1 clocks is 8.
+ */
+#define _K1_CLK_PARENT_CNT_IMPL(_0, _1, _2, _3, _4, _5, _6, _7, _8, N, ...) N
+#define K1_CLK_PARENT_CNT(...) _K1_CLK_PARENT_CNT_IMPL(_, ## __VA_ARGS__, 8, \
+    7, 6, 5, 4, 3, 2, 1, 0)
+
+#define K1_CLK_COMMON(_clkname, _id, ...)			\
+	.clkdef = {						\
+	.id = _id,						\
+	.name = #_clkname,					\
+	.parent_names = (const char*[]) { __VA_ARGS__ },	\
+	.parent_cnt = K1_CLK_PARENT_CNT(__VA_ARGS__),		\
+}
+
+#define K1_CLK_GATE(_clkname, _id, _reg, _gate_bit, ...)	\
+	{							\
+	K1_CLK_COMMON(_clkname, _id, __VA_ARGS__),		\
+	.reg = _reg,						\
+	.gate_mask = 1 << _gate_bit,				\
+}
+
+#define K1_CLK_DIV(_clkname, _id, _reg, _div_bits, _div_shift,	\
+   _fc_bit, ...)						\
+	{							\
+	K1_CLK_COMMON(_clkname, _id, __VA_ARGS__),		\
+	.reg = _reg,						\
+	.div_nbits = _div_bits,					\
+	.div_shift = _div_shift,				\
+	.fc_mask = 1 << _fc_bit,				\
+}
+
+#define K1_CLK_DIV_MUX(_clkname, _id, _reg, _div_bits,	\
+    _div_shift, _mux_nbits, _mux_shift, _fc_bit, ...)	\
+	{						\
+	K1_CLK_COMMON(_clkname, _id, __VA_ARGS__),	\
+	.reg = _reg,					\
+	.div_nbits = _div_bits,				\
+	.div_shift = _div_shift,			\
+	.mux_nbits = _mux_nbits,			\
+	.mux_shift = _mux_shift,			\
+	.fc_mask =  1 <<_fc_bit,			\
+}
+
+#define K1_CLK_DIV_GATE(_clkname, _id, _reg, _div_bits, _div_shift,	\
+    _gate_bit, ...)							\
+	{								\
+	K1_CLK_COMMON(_clkname, _id, __VA_ARGS__),			\
+	.reg = _reg,							\
+	.gate_mask = (1 << _gate_bit),					\
+	.div_nbits = _div_bits,						\
+	.div_shift = _div_shift,					\
+}
+
+#define K1_CLK_MUX_GATE(_clkname, _id, _reg, _mux_nbits, _mux_shift,	\
+    _gate_bit, ...)							\
+	{								\
+	K1_CLK_COMMON(_clkname, _id, __VA_ARGS__),			\
+	.reg = _reg,							\
+	.gate_mask = (1 << _gate_bit),					\
+	.mux_nbits = _mux_nbits,					\
+	.mux_shift = _mux_shift,					\
+}
+
+#define K1_CLK_DIV_MUX_GATE(_clkname, _id, _reg, _div_bits,	\
+    _div_shift, _mux_nbits, _mux_shift,				\
+    _gate_bit, _fc_bit, ...)					\
+	{							\
+	K1_CLK_COMMON(_clkname, _id, __VA_ARGS__),		\
+	.reg = _reg,						\
+	.gate_mask = (1 << _gate_bit),				\
+	.div_nbits = _div_bits,					\
+	.div_shift = _div_shift,				\
+	.mux_nbits = _mux_nbits,				\
+	.mux_shift = _mux_shift,				\
+	.fc_mask =  1 <<_fc_bit,				\
+}
+
+#define K1_RESET(_id, _reg, _assert_bit, _deassert_bit)	\
+	[_id] = {					\
+	.reg = _reg,					\
+	.assert_mask = (1 << _assert_bit),		\
+	.deassert_mask = (1 << _deassert_bit),		\
+}
+
+struct k1_clk_def {
+	struct clknode_init_def	clkdef;
+
+	uint64_t rate; /* Fixed clock frequency. */
+	uint32_t fixed_div; /* Fixed clock divisor. */
+	uint32_t reg; /* Control register offset.*/
+	uint32_t div_nbits; /* Frequency divisor. */
+	uint32_t div_shift;
+	uint32_t gate_mask; /* Gate bit. */
+	uint32_t mux_nbits; /* Multiplexer offset. */
+	uint32_t mux_shift;
+	uint32_t fc_mask; /* Frequency change request bit. */
+};
+
+struct k1_reset_def {
+	uint32_t reg;
+	uint32_t assert_mask;
+	uint32_t deassert_mask;
+};
+
+struct k1_clkdev_softc {
+	struct mtx mtx;
+	struct clkdom *clkdom;
+	struct resource *res;
+
+	const struct k1_clk_def *clks;
+	const struct k1_reset_def *resets;
+	int nclks;
+	int nresets;
+
+	/*
+	 * Allow derived clknode implementations to
+	 * override the generic K1 clock routines.
+	 */
+	clknode_class_t clknode_class;
+};
+
+DECLARE_CLASS(k1_clkdev_driver);
+DECLARE_CLASS(k1_clknode_class);
+
+int k1_clk_attach(device_t dev);
+
+#endif
diff --git a/sys/dev/clk/spacemit/k1_pll.c b/sys/dev/clk/spacemit/k1_pll.c
new file mode 100644
index 000000000000..c90eab0789bc
--- /dev/null
+++ b/sys/dev/clk/spacemit/k1_pll.c
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2026 Bojan Novković <bnovkov@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Driver for the SpacemiT K1 SoC PLL clocks.
+ *
+ * Written using SpacemiT's K1 Chip Product Documentation,
+ * Section 9., "Top System (1/2)" and the accompanying clock
+ * tree schematic, available at https://www.spacemit.com/community/document/
+ */
+
+#include <sys/cdefs.h>
+
+#include <sys/param.h>
+#include <sys/systm.h>
+#include <sys/bus.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/mutex.h>
+
+#include <machine/bus.h>
+#include <sys/rman.h>
+#include <machine/resource.h>
+
+#include <dev/fdt/simplebus.h>
+#include <dev/ofw/ofw_bus.h>
+#include <dev/ofw/ofw_bus_subr.h>
+
+#include <dev/clk/clk.h>
+
+#include <dt-bindings/clock/spacemit,k1-syscon.h>
+
+#include "clkdev_if.h"
+#include "k1_clk.h"
+
+#define K1_PLL1_SW2_CTRL 0x110
+#define K1_PLL2_SW2_CTRL 0x11C
+#define K1_PLL3_SW2_CTRL 0x128
+
+#define K1_PLL_ROOT(_clkname, _id, _rate)	\
+	{					\
+	.clkdef = {				\
+		.id = _id,			\
+		.name = #_clkname,		\
+	},					\
+	.rate = _rate,				\
+}
+
+#define K1_PLL_DIV(_clkname, _id, _div, ...)				\
+	{								\
+	.clkdef = {							\
+		.id = _id,						\
+		.name = #_clkname,					\
+		.parent_names = (const char * []) { __VA_ARGS__ },	\
+		.parent_cnt = K1_CLK_PARENT_CNT(__VA_ARGS__),		\
+	},								\
+	.fixed_div = _div,						\
+}
+
+#define K1_PLL_DIV_GATE(_clkname, _id, _div, _reg, _gate_bit, ...)	\
+	{								\
+	.clkdef = {							\
+		.id = _id,						\
+		.name = #_clkname,					\
+		.parent_names = (const char * []) { __VA_ARGS__ },	\
+		.parent_cnt = K1_CLK_PARENT_CNT(__VA_ARGS__),		\
+	},								\
+	.fixed_div = _div,						\
+	.reg = _reg,							\
+	.gate_mask = (1 << _gate_bit),					\
+}
+
+static struct k1_clk_def k1_plls[] = {
+    K1_PLL_ROOT(pll1, CLK_PLL1, 2457600000UL),
+    K1_PLL_ROOT(pll2, CLK_PLL2, 3000000000UL),
+    K1_PLL_ROOT(pll3, CLK_PLL3, 3200000000UL),
+
+    K1_PLL_DIV_GATE(pll1_d2, CLK_PLL1_D2, 2, K1_PLL1_SW2_CTRL, 1, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d3, CLK_PLL1_D3, 3, K1_PLL1_SW2_CTRL, 2, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d4, CLK_PLL1_D4, 4, K1_PLL1_SW2_CTRL, 3, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d5, CLK_PLL1_D5, 5, K1_PLL1_SW2_CTRL, 4, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d6, CLK_PLL1_D6, 6, K1_PLL1_SW2_CTRL, 5, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d7, CLK_PLL1_D7, 7, K1_PLL1_SW2_CTRL, 6, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d8, CLK_PLL1_D8, 8, K1_PLL1_SW2_CTRL, 7, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d11, CLK_PLL1_D11, 11, K1_PLL1_SW2_CTRL, 15, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d13, CLK_PLL1_D13, 13, K1_PLL1_SW2_CTRL, 16, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d23, CLK_PLL1_D23, 23, K1_PLL1_SW2_CTRL, 20, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d64, CLK_PLL1_D64, 64, K1_PLL1_SW2_CTRL, 0, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d10_aud, CLK_PLL1_D10_AUD, 10, K1_PLL1_SW2_CTRL, 10, "pll1"),
+    K1_PLL_DIV_GATE(pll1_d100_aud, CLK_PLL1_D100_AUD, 100, K1_PLL1_SW2_CTRL, 11, "pll1"),
+
+    K1_PLL_DIV_GATE(pll2_d2, CLK_PLL2_D2, 2, K1_PLL2_SW2_CTRL, 1, "pll2"),
+    K1_PLL_DIV_GATE(pll2_d3, CLK_PLL2_D3, 3, K1_PLL2_SW2_CTRL, 2, "pll2"),
+    K1_PLL_DIV_GATE(pll2_d4, CLK_PLL2_D4, 4, K1_PLL2_SW2_CTRL, 3, "pll2"),
+    K1_PLL_DIV_GATE(pll2_d5, CLK_PLL2_D5, 5, K1_PLL2_SW2_CTRL, 4, "pll2"),
+    K1_PLL_DIV_GATE(pll2_d6, CLK_PLL2_D6, 6, K1_PLL2_SW2_CTRL, 5, "pll2"),
+    K1_PLL_DIV_GATE(pll2_d7, CLK_PLL2_D7, 7, K1_PLL2_SW2_CTRL, 6, "pll2"),
+    K1_PLL_DIV_GATE(pll2_d8, CLK_PLL2_D8, 8, K1_PLL2_SW2_CTRL, 7, "pll2"),
+
+    K1_PLL_DIV_GATE(pll3_d2, CLK_PLL3_D2, 2, K1_PLL3_SW2_CTRL, 1, "pll3"),
+    K1_PLL_DIV_GATE(pll3_d3, CLK_PLL3_D3, 3, K1_PLL3_SW2_CTRL, 2, "pll3"),
+    K1_PLL_DIV_GATE(pll3_d4, CLK_PLL3_D4, 4, K1_PLL3_SW2_CTRL, 3, "pll3"),
+    K1_PLL_DIV_GATE(pll3_d5, CLK_PLL3_D5, 5, K1_PLL3_SW2_CTRL, 4, "pll3"),
+    K1_PLL_DIV_GATE(pll3_d6, CLK_PLL3_D6, 6, K1_PLL3_SW2_CTRL, 5, "pll3"),
+    K1_PLL_DIV_GATE(pll3_d7, CLK_PLL3_D7, 7, K1_PLL3_SW2_CTRL, 6, "pll3"),
+    K1_PLL_DIV_GATE(pll3_d8, CLK_PLL3_D8, 8, K1_PLL3_SW2_CTRL, 7, "pll3"),
+    K1_PLL_DIV(pll3_80, CLK_PLL3_80, 20, "pll3_d8"),
+    K1_PLL_DIV(pll3_40, CLK_PLL3_40, 10, "pll3_d8"),
+    K1_PLL_DIV(pll3_20, CLK_PLL3_20, 5, "pll3_d8"),
+};
+
+/*
+ * The SpacemiT K1 manual states that "Changes of the run-time frequency
+ * in the PLL{1,2,3} output are only available for debugging purposes and
+ * should not be used in production systems", which is why this driver does
+ * not support changing the top-level PLL frequency and instead treats all
+ * PLLs as gateable fixed clocks.
+ */
+static int
+k1_pll_recalc_freq(struct clknode *clk, uint64_t *freq)
+{
+	struct k1_clk_def *sc;
+
+	sc = clknode_get_softc(clk);
+	if (sc->fixed_div != 0)
+		*freq = *freq / sc->fixed_div;
+	else
+		*freq = sc->rate;
+	return (0);
+}
+
+static int
+k1_pll_set_freq(struct clknode *clk, uint64_t fin, uint64_t *fout,
+    int flags, int *stop)
+{
+	struct k1_clk_def *sc;
+
+	sc = clknode_get_softc(clk);
+	if (sc->fixed_div == 0) {
+		/* Top-level PLL */
+		*stop = 1;
+		if (*fout != sc->rate)
+			return (ERANGE);
+		return (0);
+	}
+	*stop = 0;
+	*fout = *fout / sc->fixed_div;
+
+	return (0);
+}
+
+static clknode_method_t k1_pll_clknode_methods[] = {
+	CLKNODEMETHOD(clknode_set_freq,		k1_pll_set_freq),
+	CLKNODEMETHOD(clknode_recalc_freq,	k1_pll_recalc_freq),
+	CLKNODEMETHOD_END
+};
+
+DEFINE_CLASS_1(k1_pll_clknode, k1_pll_clknode_class, k1_pll_clknode_methods,
+    sizeof(struct k1_clk_def), k1_clknode_class);
+
+static int
+k1_pll_probe(device_t dev)
+{
+	if (!ofw_bus_status_okay(dev))
+		return (ENXIO);
+
+	if (!ofw_bus_is_compatible(dev, "spacemit,k1-pll"))
+		return (ENXIO);
+	device_set_desc(dev, "SpacemiT K1 PLL Clock Control Unit");
+
+	return (BUS_PROBE_DEFAULT);
+}
+
+static int
+k1_pll_attach(device_t dev)
+{
+	struct k1_clkdev_softc *sc;
+
+	sc = device_get_softc(dev);
+	sc->clks = k1_plls;
+	sc->nclks = nitems(k1_plls);
+	sc->clknode_class = &k1_pll_clknode_class;
+
+	return (k1_clk_attach(dev));
+}
+
+static device_method_t k1_pll_methods[] = {
+	DEVMETHOD(device_probe,		k1_pll_probe),
+	DEVMETHOD(device_attach,	k1_pll_attach),
+
+	DEVMETHOD_END
+};
+
+DEFINE_CLASS_1(k1_pll, k1_pll_driver, k1_pll_methods,
+    sizeof(struct k1_clkdev_softc), k1_clkdev_driver);
+EARLY_DRIVER_MODULE(k1_pll, simplebus, k1_pll_driver, 0, 0,
+    BUS_PASS_BUS + BUS_PASS_ORDER_EARLY);
diff --git a/sys/riscv/conf/std.spacemit b/sys/riscv/conf/std.spacemit
index 3cbbda20c765..bc3b2a6f9b8b 100644
--- a/sys/riscv/conf/std.spacemit
+++ b/sys/riscv/conf/std.spacemit
@@ -4,6 +4,7 @@
 
 device	fdt
 device	sdhci_spacemit
+device	k1_ccu
 
 makeoptions	MODULES_EXTRA+="dtb/spacemit"
 
diff --git a/sys/riscv/spacemit/files.spacemit b/sys/riscv/spacemit/files.spacemit
index e69de29bb2d1..b4ab14e9240b 100644
--- a/sys/riscv/spacemit/files.spacemit
+++ b/sys/riscv/spacemit/files.spacemit
@@ -0,0 +1,3 @@
+dev/clk/spacemit/k1_clk.c	optional k1_ccu fdt
+dev/clk/spacemit/k1_pll.c	optional k1_ccu fdt
+dev/clk/spacemit/k1_apmu.c	optional k1_ccu fdt