mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Troy Mitchell <troy.mitchell@linux.spacemit.com>
To: Troy Mitchell <troy.mitchell@linux.spacemit.com>,
	 Daniel Lezcano <daniel.lezcano@kernel.org>,
	 Thomas Gleixner <tglx@kernel.org>, Rob Herring <robh@kernel.org>,
	 Krzysztof Kozlowski <krzk+dt@kernel.org>,
	 Conor Dooley <conor+dt@kernel.org>, Yixun Lan <dlan@kernel.org>,
	 Philipp Zabel <p.zabel@pengutronix.de>,
	Paul Walmsley <pjw@kernel.org>,
	 Palmer Dabbelt <palmer@dabbelt.com>,
	Albert Ou <aou@eecs.berkeley.edu>,
	 Alexandre Ghiti <alex@ghiti.fr>
Cc: linux-kernel@vger.kernel.org, devicetree@vger.kernel.org,
	 linux-riscv@lists.infradead.org, spacemit@lists.linux.dev
Subject: [PATCH 2/5] clocksource/drivers/spacemit: Add K1 timer support
Date: Tue, 03 Nov 2026 08:36:47 +0000	[thread overview]
Message-ID: <20261103-spacemit-soc-timer-v1-2-36722421db6e@linux.spacemit.com> (raw)
In-Reply-To: <20261103-spacemit-soc-timer-v1-0-36722421db6e@linux.spacemit.com>

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


  parent reply	other threads:[~2026-11-03  8:38 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-03  8:36 [PATCH 0/5] Add SpacemiT K1/K3 SoC " 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 [this message]
2026-11-03  8:36 ` [PATCH 3/5] riscv: dts: spacemit: Add the K1 APB timer Troy Mitchell
2026-11-03  8:36 ` [PATCH 4/5] riscv: dts: spacemit: Add the K3 " Troy Mitchell
2026-11-03  8:36 ` [PATCH 5/5] MAINTAINERS: Add entry for SpacemiT timer Troy Mitchell

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261103-spacemit-soc-timer-v1-2-36722421db6e@linux.spacemit.com \
    --to=troy.mitchell@linux.spacemit.com \
    --cc=alex@ghiti.fr \
    --cc=aou@eecs.berkeley.edu \
    --cc=conor+dt@kernel.org \
    --cc=daniel.lezcano@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dlan@kernel.org \
    --cc=krzk+dt@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-riscv@lists.infradead.org \
    --cc=p.zabel@pengutronix.de \
    --cc=palmer@dabbelt.com \
    --cc=pjw@kernel.org \
    --cc=robh@kernel.org \
    --cc=spacemit@lists.linux.dev \
    --cc=tglx@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®