blob: 585b6fe39865defd74e0bf5564b0eee686132180 [file]
/*
* Copyright 2026 NXP
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
* NXP System Counter (SYS_CTR) based system timer driver.
*
* SYS_CTR provides a 56-bit free-running counter with two compare frames,
* each capable of generating a maskable interrupt. This driver uses compare
* frame 0 for the system timer.
*
* Shared-resource ownership:
* SYS_CTR is a shared time base for multiple processors. This driver
* assumes the Zephyr core exclusively owns SYS_CTR initialization:
* sys_clock_driver_init() calls SYSCTR_Init(), which resets the shared
* counter value to 0 and clears BOTH compare frames (including frame 1).
* Do not enable this driver on a core if another core or an earlier boot
* stage already relies on the same SYS_CTR instance (its counter value or
* compare frame 1) — that state would be wiped at init.
*
* Tickless operation:
* - ISR disables compare (clears ISTAT) and masks the interrupt.
* - sys_clock_set_timeout() programs a new compare value and re-enables.
* - Under sloppy idle with no near deadline, the compare interrupt
* stays disabled until sys_clock_idle_exit() or the next set_timeout
* call.
*/
#include <zephyr/init.h>
#include <zephyr/drivers/timer/system_timer.h>
#include <zephyr/sys/clock.h>
#include <zephyr/spinlock.h>
#include <zephyr/irq.h>
#include <zephyr/devicetree.h>
#include <fsl_sysctr.h>
#define SYSCTR_NODE DT_CHOSEN(zephyr_system_timer)
BUILD_ASSERT(DT_HAS_CHOSEN(zephyr_system_timer),
"zephyr,system-timer must be set to an nxp,sysctr node");
BUILD_ASSERT(DT_NODE_HAS_COMPAT(DT_CHOSEN(zephyr_system_timer), nxp_sysctr),
"zephyr,system-timer must point to an nxp,sysctr compatible node");
BUILD_ASSERT((CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC % CONFIG_SYS_CLOCK_TICKS_PER_SEC) == 0,
"SYS_CTR: CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC must be an integer "
"multiple of CONFIG_SYS_CLOCK_TICKS_PER_SEC to avoid timer drift");
/* Hardware register bases from devicetree */
#define CTRL_BASE ((SYS_CTR_CONTROL_Type *)DT_REG_ADDR_BY_NAME(SYSCTR_NODE, control))
#define READ_BASE ((SYS_CTR_READ_Type *)DT_REG_ADDR_BY_NAME(SYSCTR_NODE, read))
#define CMP_BASE ((SYS_CTR_COMPARE_Type *)DT_REG_ADDR_BY_NAME(SYSCTR_NODE, compare))
#define SYSCTR_IRQN DT_IRQN(SYSCTR_NODE)
#define SYSCTR_IRQ_PRIO DT_IRQ(SYSCTR_NODE, priority)
/* This driver uses compare frame 0 at runtime and does not touch frame 1. */
#define TIMER_CMP_FRAME kSYSCTR_CompareFrame_0
#define TIMER_CMP_INT_MASK kSYSCTR_Compare0InterruptEnable
/* 56-bit counter maximum value */
#define COUNTER_SPAN ((1ULL << 56) - 1)
#define CYC_PER_TICK ((uint64_t)sys_clock_hw_cycles_per_sec() \
/ (uint64_t)CONFIG_SYS_CLOCK_TICKS_PER_SEC)
/*
* Limit the maximum programmed compare distance to half the 56-bit counter
* span. This keeps the ">=" comparison unambiguous when the counter
* eventually wraps.
*/
#define MAX_CYCLES (COUNTER_SPAN / 2)
#define MIN_DELAY_CYCLES CONFIG_MCUX_SYSCTR_TIMER_MIN_DELAY
/*
* The minimum compare setup distance must stay below one tick. Otherwise a
* normal single-tick timeout would be pushed out to "now + MIN_DELAY_CYCLES"
* in sys_clock_set_timeout(), losing ticks and introducing systematic drift.
*/
BUILD_ASSERT(MIN_DELAY_CYCLES <
(CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC / CONFIG_SYS_CLOCK_TICKS_PER_SEC),
"CONFIG_MCUX_SYSCTR_TIMER_MIN_DELAY must be smaller than one tick "
"(CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC / CONFIG_SYS_CLOCK_TICKS_PER_SEC)");
static struct k_spinlock lock;
static uint64_t last_cycle; /* Counter value at last sys_clock_announce() */
#if defined(CONFIG_TEST)
const int32_t z_sys_timer_irq_for_test = SYSCTR_IRQN;
#endif
static inline uint64_t counter_read(void)
{
uint32_t hi1, hi2, lo;
/*
* The 56-bit count value is read from two 32-bit registers, so a
* combined read is not atomic: if the low word rolls over between
* the two halves the result can be off by ~2^32. Read the high word,
* the low word, then the high word again and retry while the high
* word changes, discarding any sample taken across a rollover.
*/
do {
hi1 = READ_BASE->CNTCV1;
lo = READ_BASE->CNTCV0;
hi2 = READ_BASE->CNTCV1;
} while (hi1 != hi2);
return ((uint64_t)hi2 << 32) | lo;
}
static void sysctr_set_compare(uint64_t val)
{
/*
* Disable compare first to clear ISTAT and prevent a race between
* the two 32-bit writes that make up the 64-bit compare value.
* Then program the new value and re-enable.
*/
SYSCTR_EnableCompare(CMP_BASE, TIMER_CMP_FRAME, false);
SYSCTR_SetCompareValue(CTRL_BASE, CMP_BASE, TIMER_CMP_FRAME, val);
SYSCTR_EnableInterrupts(CMP_BASE, TIMER_CMP_INT_MASK);
SYSCTR_EnableCompare(CMP_BASE, TIMER_CMP_FRAME, true);
}
static void sysctr_timer_isr(const void *arg)
{
ARG_UNUSED(arg);
k_spinlock_key_t key = k_spin_lock(&lock);
uint64_t curr_cycle = counter_read();
uint64_t delta_cycles = curr_cycle - last_cycle;
uint32_t delta_ticks = (uint32_t)(delta_cycles / CYC_PER_TICK);
last_cycle += (uint64_t)delta_ticks * CYC_PER_TICK;
/* Disable compare to clear ISTAT and prevent re-trigger */
SYSCTR_EnableCompare(CMP_BASE, TIMER_CMP_FRAME, false);
if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) {
/* Non-tickless: schedule the next tick immediately */
uint64_t next = last_cycle + CYC_PER_TICK;
sysctr_set_compare(next);
} else {
/* Tickless: mask interrupt until next sys_clock_set_timeout() */
SYSCTR_DisableInterrupts(CMP_BASE, TIMER_CMP_INT_MASK);
}
k_spin_unlock(&lock, key);
sys_clock_announce(delta_ticks);
}
void sys_clock_set_timeout(uint32_t ticks, bool idle)
{
uint64_t next_cycle;
uint64_t now;
uint64_t min;
ARG_UNUSED(idle);
if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) {
return;
}
if (IS_ENABLED(CONFIG_SYSTEM_CLOCK_SLOPPY_IDLE) && ticks == SYS_CLOCK_MAX_WAIT) {
/*
* No near deadline to schedule: under sloppy idle, disable the
* compare interrupt entirely. sys_clock_idle_exit() will
* re-enable it when the CPU wakes from an external source.
*/
SYSCTR_EnableCompare(CMP_BASE, TIMER_CMP_FRAME, false);
SYSCTR_DisableInterrupts(CMP_BASE, TIMER_CMP_INT_MASK);
return;
}
k_spinlock_key_t key = k_spin_lock(&lock);
now = counter_read();
/*
* Compute elapsed ticks from the most recent announce locally
* instead of relying on a cached value from sys_clock_elapsed(),
* which may be stale (or never have been called) by the time we
* arrive here.
*/
uint64_t elapsed_ticks = (now - last_cycle) / CYC_PER_TICK;
next_cycle = last_cycle + (elapsed_ticks + (uint64_t)ticks) * CYC_PER_TICK;
if ((next_cycle - last_cycle) > MAX_CYCLES) {
next_cycle = last_cycle + MAX_CYCLES;
}
min = now + MIN_DELAY_CYCLES;
if ((int64_t)(next_cycle - min) < 0) {
next_cycle = min;
}
sysctr_set_compare(next_cycle);
k_spin_unlock(&lock, key);
}
void sys_clock_idle_exit(void)
{
/*
* The compare interrupt may still be masked after PM idle. Re-enable it
* here, otherwise a pending compare fires late and the periodic timer
* jitters (one tick long, next one short). No tick is lost, and we can't
* trigger spuriously: ISTAT needs the counter to reach an enabled compare.
*/
SYSCTR_EnableInterrupts(CMP_BASE, TIMER_CMP_INT_MASK);
}
uint32_t sys_clock_elapsed(void)
{
if (!IS_ENABLED(CONFIG_TICKLESS_KERNEL)) {
return 0;
}
k_spinlock_key_t key = k_spin_lock(&lock);
uint64_t curr_cycle = counter_read();
uint64_t delta_cycles = curr_cycle - last_cycle;
uint32_t delta_ticks = (uint32_t)(delta_cycles / CYC_PER_TICK);
k_spin_unlock(&lock, key);
return delta_ticks;
}
uint32_t sys_clock_cycle_get_32(void)
{
return (uint32_t)counter_read();
}
uint64_t sys_clock_cycle_get_64(void)
{
return counter_read();
}
void sys_clock_disable(void)
{
SYSCTR_EnableCompare(CMP_BASE, TIMER_CMP_FRAME, false);
SYSCTR_DisableInterrupts(CMP_BASE, TIMER_CMP_INT_MASK);
irq_disable(SYSCTR_IRQN);
}
static int sys_clock_driver_init(void)
{
sysctr_config_t cfg;
SYSCTR_GetDefaultConfig(&cfg);
SYSCTR_Init(CTRL_BASE, CMP_BASE, &cfg);
/*
* The system timer always runs on the base frequency: compare values
* are only valid on the base clock, so the optional clock-source
* devicetree property (alternate/low-power clock) is intentionally not
* honored here.
*/
SYSCTR_SetCounterClockSource(CTRL_BASE, kSYSCTR_BaseFrequency);
IRQ_CONNECT(SYSCTR_IRQN, SYSCTR_IRQ_PRIO, sysctr_timer_isr, NULL, 0);
irq_enable(SYSCTR_IRQN);
SYSCTR_StartCounter(CTRL_BASE);
/* Align to tick boundary */
last_cycle = (counter_read() / CYC_PER_TICK) * CYC_PER_TICK;
/* Arm the first compare */
sysctr_set_compare(last_cycle + CYC_PER_TICK);
return 0;
}
SYS_INIT(sys_clock_driver_init, PRE_KERNEL_2,
CONFIG_SYSTEM_CLOCK_INIT_PRIORITY);