* [PATCH 1/5] dt-bindings: timer: Add SpacemiT K1 timer
2026-11-03 8:36 [PATCH 0/5] Add SpacemiT K1/K3 SoC timer support Troy Mitchell
@ 2026-11-03 8:36 ` Troy Mitchell
2026-11-03 8:36 ` [PATCH 2/5] clocksource/drivers/spacemit: Add K1 timer support Troy Mitchell
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Troy Mitchell @ 2026-11-03 8:36 UTC (permalink / raw)
To: Troy Mitchell, Daniel Lezcano, Thomas Gleixner, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Yixun Lan, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: linux-kernel, devicetree, linux-riscv, spacemit
The SpacemiT timer block provides three 32-bit counters, each with three
match comparators and an interrupt output. Describe its register range,
interrupts, functional and bus clocks, and reset.
K3 uses the same register interface, so describe its timer with a
SoC-specific compatible followed by the K1 fallback.
Signed-off-by: Troy Mitchell <troy.mitchell@linux.spacemit.com>
---
.../bindings/timer/spacemit,k1-timer.yaml | 71 ++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/Documentation/devicetree/bindings/timer/spacemit,k1-timer.yaml b/Documentation/devicetree/bindings/timer/spacemit,k1-timer.yaml
new file mode 100644
index 000000000000..b71785b8aa94
--- /dev/null
+++ b/Documentation/devicetree/bindings/timer/spacemit,k1-timer.yaml
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/timer/spacemit,k1-timer.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: SpacemiT K1 timer
+
+maintainers:
+ - Troy Mitchell <troy.mitchell@linux.spacemit.com>
+
+description:
+ The timer block contains three 32-bit up counters, each with three match
+ comparators and an interrupt output. Each counter can use the functional
+ clock or the internal 32.768 kHz clock and supports free-running and
+ periodic operation.
+
+properties:
+ compatible:
+ oneOf:
+ - const: spacemit,k1-timer
+ - items:
+ - const: spacemit,k3-timer
+ - const: spacemit,k1-timer
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ minItems: 1
+ items:
+ - description: Counter 0 interrupt
+ - description: Counter 1 interrupt
+ - description: Counter 2 interrupt
+
+ clocks:
+ items:
+ - description: Functional clock
+ - description: APB bus clock
+
+ clock-names:
+ items:
+ - const: func
+ - const: bus
+
+ resets:
+ maxItems: 1
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - clocks
+ - clock-names
+ - resets
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/clock/spacemit,k1-syscon.h>
+
+ timer@d4014000 {
+ compatible = "spacemit,k1-timer";
+ reg = <0xd4014000 0x100>;
+ interrupts = <23>;
+ clocks = <&syscon_apbc CLK_TIMERS1>,
+ <&syscon_apbc CLK_TIMERS1_BUS>;
+ clock-names = "func", "bus";
+ resets = <&syscon_apbc RESET_TIMERS1>;
+ };
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH 2/5] clocksource/drivers/spacemit: Add K1 timer support
2026-11-03 8:36 [PATCH 0/5] Add SpacemiT K1/K3 SoC timer support Troy Mitchell
2026-11-03 8:36 ` [PATCH 1/5] dt-bindings: timer: Add SpacemiT K1 timer Troy Mitchell
@ 2026-11-03 8:36 ` Troy Mitchell
2026-11-03 8:36 ` [PATCH 3/5] riscv: dts: spacemit: Add the K1 APB timer Troy Mitchell
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Troy Mitchell @ 2026-11-03 8:36 UTC (permalink / raw)
To: Troy Mitchell, Daniel Lezcano, Thomas Gleixner, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Yixun Lan, Philipp Zabel,
Paul Walmsley, Palmer Dabbelt, Albert Ou, Alexandre Ghiti
Cc: linux-kernel, devicetree, linux-riscv, spacemit
SpacemiT K1 and K3 have APB timers that can provide a shared clockevent
alongside the per-CPU RISC-V timer. Use counter 0 and its first match
comparator as a oneshot device available to the tick broadcast framework.
Counter enable changes cross clock domains. Wait for the synchronized
counter value to stop changing before reprogramming the match register,
and check interrupt acknowledgment through the status register rather
than reading the write-only clear register. Bound all synchronization
waits and propagate programming failures to the clockevent core.
Obtain the counter rate from the functional clock and protect it against
rate changes. Keep the clocks enabled for the clockevent lifetime and
restore counter configuration through the tick resume callback without
sleeping clock operations. Use a platform driver so clock and reset
providers can probe before the timer is registered.
Signed-off-by: Troy Mitchell <troy.mitchell@linux.spacemit.com>
---
drivers/clocksource/Kconfig | 11 ++
drivers/clocksource/Makefile | 1 +
drivers/clocksource/timer-spacemit.c | 312 +++++++++++++++++++++++++++++++++++
3 files changed, 324 insertions(+)
diff --git a/drivers/clocksource/Kconfig b/drivers/clocksource/Kconfig
index da83483296d5..3e7dd5d628cd 100644
--- a/drivers/clocksource/Kconfig
+++ b/drivers/clocksource/Kconfig
@@ -163,6 +163,17 @@ config RZN1_TIMER
- 2 32-bit timers
Two timers blocks are available in RZ/N1 SoCs.
+config SPACEMIT_TIMER
+ bool "SpacemiT K1/K3 timer"
+ depends on ARCH_SPACEMIT || COMPILE_TEST
+ depends on OF && COMMON_CLK && RESET_CONTROLLER && HAS_IOMEM
+ default y if ARCH_SPACEMIT
+ help
+ Enable the SoC timer clockevent device on SpacemiT K1 and K3.
+ It provides a shared oneshot timer for tick broadcast, alongside
+ the per-CPU RISC-V timer. The RISC-V clocksource continues to
+ provide timekeeping.
+
config SUN4I_TIMER
bool "Sun4i timer driver" if COMPILE_TEST
depends on HAS_IOMEM
diff --git a/drivers/clocksource/Makefile b/drivers/clocksource/Makefile
index 464ec6690ab6..dadf9bc77dee 100644
--- a/drivers/clocksource/Makefile
+++ b/drivers/clocksource/Makefile
@@ -33,6 +33,7 @@ obj-$(CONFIG_BCM2835_TIMER) += bcm2835_timer.o
obj-$(CONFIG_CLPS711X_TIMER) += clps711x-timer.o
obj-$(CONFIG_MXS_TIMER) += mxs_timer.o
obj-$(CONFIG_CLKSRC_PXA) += timer-pxa.o
+obj-$(CONFIG_SPACEMIT_TIMER) += timer-spacemit.o
obj-$(CONFIG_SUN4I_TIMER) += timer-sun4i.o
obj-$(CONFIG_SUN5I_HSTIMER) += timer-sun5i.o
obj-$(CONFIG_MESON6_TIMER) += timer-meson6.o
diff --git a/drivers/clocksource/timer-spacemit.c b/drivers/clocksource/timer-spacemit.c
new file mode 100644
index 000000000000..70076c17b503
--- /dev/null
+++ b/drivers/clocksource/timer-spacemit.c
@@ -0,0 +1,312 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * SpacemiT K1/K3 timer clockevent driver
+ *
+ * Copyright (C) 2026 SpacemiT
+ */
+
+#include <linux/bitops.h>
+#include <linux/clk.h>
+#include <linux/clockchips.h>
+#include <linux/delay.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/limits.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/reset.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+
+/* Use counter 0 and its first match comparator. */
+#define TMR_CER 0x00
+#define TMR_CMR 0x04
+#define TMR_CCR 0x0c
+#define TMR_MATCH 0x10
+#define TMR_PRELOAD 0x40
+#define TMR_PRELOAD_CTRL 0x50
+#define TMR_IER 0x60
+#define TMR_ICR 0x70
+#define TMR_SR 0x80
+#define TMR_COUNT 0x90
+
+#define TMR_COUNTER_EN BIT(0)
+#define TMR_FREE_RUNNING BIT(0)
+#define TMR_CLOCK_MASK GENMASK(1, 0)
+#define TMR_MATCH0 BIT(0)
+#define TMR_MATCH_MASK GENMASK(2, 0)
+/* Leave room for match and enable synchronization. */
+#define TMR_MIN_DELTA 16
+
+struct spacemit_timer {
+ void __iomem *base;
+ struct device *dev;
+ struct clock_event_device clkevt;
+ raw_spinlock_t lock;
+ unsigned int sync_us;
+};
+
+static struct spacemit_timer *to_spacemit_timer(struct clock_event_device *evt)
+{
+ return container_of(evt, struct spacemit_timer, clkevt);
+}
+
+static int spacemit_timer_write(struct spacemit_timer *timer, u32 reg, u32 val,
+ u32 mask)
+{
+ u32 tmp;
+
+ writel(val, timer->base + reg);
+ return readl_poll_timeout_atomic(timer->base + reg, tmp,
+ (tmp & mask) == (val & mask), 1,
+ 10 * timer->sync_us);
+}
+
+static int spacemit_timer_stop(struct spacemit_timer *timer)
+{
+ u32 val, prev;
+ int ret, i;
+
+ val = readl(timer->base + TMR_CER) & ~TMR_COUNTER_EN;
+ ret = spacemit_timer_write(timer, TMR_CER, val, TMR_COUNTER_EN);
+ if (ret)
+ return ret;
+
+ /*
+ * CER crosses into the counter clock domain. Its readback alone does
+ * not show that counting has stopped. COUNT is synchronized back to
+ * APB, with up to three counter clocks of latency. Wait for a stable
+ * count before changing the match or preload registers.
+ */
+ prev = readl(timer->base + TMR_COUNT);
+ for (i = 0; i < 10; i++) {
+ udelay(timer->sync_us);
+ val = readl(timer->base + TMR_COUNT);
+ if (val == prev)
+ return 0;
+ prev = val;
+ }
+
+ return -ETIMEDOUT;
+}
+
+static int spacemit_timer_clear(struct spacemit_timer *timer)
+{
+ u32 val;
+
+ /* ICR is write-only; completion is observed through SR. */
+ writel(TMR_MATCH_MASK, timer->base + TMR_ICR);
+ return readl_poll_timeout_atomic(timer->base + TMR_SR, val,
+ !(val & TMR_MATCH_MASK), 1,
+ 10 * timer->sync_us);
+}
+
+static int spacemit_timer_quiesce(struct spacemit_timer *timer)
+{
+ int ret, err;
+
+ ret = spacemit_timer_write(timer, TMR_IER, 0, TMR_MATCH_MASK);
+ /* Still try to stop and acknowledge the timer after a failed write. */
+ err = spacemit_timer_stop(timer);
+ if (!ret)
+ ret = err;
+ err = spacemit_timer_clear(timer);
+
+ return ret ?: err;
+}
+
+static int spacemit_timer_shutdown(struct clock_event_device *evt)
+{
+ struct spacemit_timer *timer = to_spacemit_timer(evt);
+ unsigned long flags;
+ int ret;
+
+ raw_spin_lock_irqsave(&timer->lock, flags);
+ ret = spacemit_timer_quiesce(timer);
+ raw_spin_unlock_irqrestore(&timer->lock, flags);
+
+ return ret;
+}
+
+static int spacemit_timer_next_event(unsigned long delta,
+ struct clock_event_device *evt)
+{
+ struct spacemit_timer *timer = to_spacemit_timer(evt);
+ unsigned long flags;
+ u32 val;
+ int ret;
+
+ raw_spin_lock_irqsave(&timer->lock, flags);
+
+ ret = spacemit_timer_quiesce(timer);
+ if (ret)
+ goto out;
+
+ ret = spacemit_timer_write(timer, TMR_MATCH, delta - 1, U32_MAX);
+ if (ret)
+ goto out;
+
+ ret = spacemit_timer_write(timer, TMR_IER, TMR_MATCH0, TMR_MATCH_MASK);
+ if (ret)
+ goto out;
+
+ /* Enabling the counter reloads the zero preload value. */
+ val = readl(timer->base + TMR_CER) | TMR_COUNTER_EN;
+ ret = spacemit_timer_write(timer, TMR_CER, val, TMR_COUNTER_EN);
+out:
+ if (ret) {
+ /* Leave a failed programming attempt with interrupts masked. */
+ writel(0, timer->base + TMR_IER);
+ val = readl(timer->base + TMR_CER) & ~TMR_COUNTER_EN;
+ writel(val, timer->base + TMR_CER);
+ }
+ raw_spin_unlock_irqrestore(&timer->lock, flags);
+
+ return ret;
+}
+
+static irqreturn_t spacemit_timer_interrupt(int irq, void *data)
+{
+ struct spacemit_timer *timer = data;
+ unsigned long flags;
+ int ret;
+
+ raw_spin_lock_irqsave(&timer->lock, flags);
+ if (!(readl(timer->base + TMR_SR) & TMR_MATCH0)) {
+ raw_spin_unlock_irqrestore(&timer->lock, flags);
+ return IRQ_NONE;
+ }
+
+ ret = spacemit_timer_quiesce(timer);
+ raw_spin_unlock_irqrestore(&timer->lock, flags);
+ if (ret)
+ dev_err_ratelimited(timer->dev, "Failed to acknowledge timer: %d\n", ret);
+
+ timer->clkevt.event_handler(&timer->clkevt);
+ return IRQ_HANDLED;
+}
+
+static int spacemit_timer_resume(struct clock_event_device *evt)
+{
+ struct spacemit_timer *timer = to_spacemit_timer(evt);
+ unsigned long flags;
+ u32 val;
+ int ret;
+
+ raw_spin_lock_irqsave(&timer->lock, flags);
+ ret = spacemit_timer_quiesce(timer);
+ if (ret)
+ goto out;
+
+ /* Select the functional clock and disable automatic match reload. */
+ val = readl(timer->base + TMR_CCR) & ~TMR_CLOCK_MASK;
+ ret = spacemit_timer_write(timer, TMR_CCR, val, TMR_CLOCK_MASK);
+ if (ret)
+ goto out;
+
+ val = readl(timer->base + TMR_CMR) | TMR_FREE_RUNNING;
+ ret = spacemit_timer_write(timer, TMR_CMR, val, TMR_FREE_RUNNING);
+ if (ret)
+ goto out;
+
+ ret = spacemit_timer_write(timer, TMR_PRELOAD_CTRL, 0, GENMASK(2, 0));
+ if (ret)
+ goto out;
+
+ ret = spacemit_timer_write(timer, TMR_PRELOAD, 0, U32_MAX);
+out:
+ raw_spin_unlock_irqrestore(&timer->lock, flags);
+ return ret;
+}
+
+static int spacemit_timer_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct reset_control *reset;
+ struct spacemit_timer *timer;
+ struct clk *func, *bus;
+ unsigned long rate;
+ int irq, ret;
+
+ timer = devm_kzalloc(dev, sizeof(*timer), GFP_KERNEL);
+ if (!timer)
+ return -ENOMEM;
+
+ timer->base = devm_platform_ioremap_resource(pdev, 0);
+ if (IS_ERR(timer->base))
+ return PTR_ERR(timer->base);
+
+ irq = platform_get_irq(pdev, 0);
+ if (irq < 0)
+ return irq;
+
+ /* Keep both clocks enabled for the lifetime of the clockevent. */
+ bus = devm_clk_get_enabled(dev, "bus");
+ if (IS_ERR(bus))
+ return dev_err_probe(dev, PTR_ERR(bus), "Failed to enable bus clock\n");
+
+ func = devm_clk_get_enabled(dev, "func");
+ if (IS_ERR(func))
+ return dev_err_probe(dev, PTR_ERR(func), "Failed to enable functional clock\n");
+
+ ret = devm_clk_rate_exclusive_get(dev, func);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to protect timer rate\n");
+
+ rate = clk_get_rate(func);
+ if (rate < 32768 || rate > U32_MAX)
+ return dev_err_probe(dev, -EINVAL, "Invalid timer rate: %lu\n", rate);
+
+ reset = devm_reset_control_get_exclusive(dev, NULL);
+ if (IS_ERR(reset))
+ return dev_err_probe(dev, PTR_ERR(reset), "Failed to get reset\n");
+
+ ret = reset_control_assert(reset);
+ if (ret)
+ return ret;
+ ret = reset_control_deassert(reset);
+ if (ret)
+ return ret;
+
+ timer->dev = dev;
+ timer->sync_us = DIV_ROUND_UP(3 * USEC_PER_SEC, rate);
+ raw_spin_lock_init(&timer->lock);
+ timer->clkevt.name = dev_name(dev);
+ timer->clkevt.rating = 200;
+ timer->clkevt.features = CLOCK_EVT_FEAT_ONESHOT | CLOCK_EVT_FEAT_DYNIRQ;
+ timer->clkevt.cpumask = cpu_possible_mask;
+ timer->clkevt.irq = irq;
+ timer->clkevt.set_next_event = spacemit_timer_next_event;
+ timer->clkevt.set_state_shutdown = spacemit_timer_shutdown;
+ timer->clkevt.set_state_oneshot = spacemit_timer_shutdown;
+ timer->clkevt.tick_resume = spacemit_timer_resume;
+
+ ret = spacemit_timer_resume(&timer->clkevt);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to initialize timer\n");
+
+ ret = devm_request_irq(dev, irq, spacemit_timer_interrupt, IRQF_TIMER,
+ dev_name(dev), timer);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to request timer interrupt\n");
+
+ clockevents_config_and_register(&timer->clkevt, rate, TMR_MIN_DELTA,
+ U32_MAX - 1);
+ return 0;
+}
+
+static const struct of_device_id spacemit_timer_match[] = {
+ { .compatible = "spacemit,k1-timer" },
+ { }
+};
+
+static struct platform_driver spacemit_timer_driver = {
+ .probe = spacemit_timer_probe,
+ .driver = {
+ .name = "spacemit-timer",
+ .of_match_table = spacemit_timer_match,
+ .suppress_bind_attrs = true,
+ },
+};
+builtin_platform_driver(spacemit_timer_driver);
--
2.56.0
^ permalink raw reply [flat|nested] 6+ messages in thread