Skip to content

Commit

Permalink
timer: cortex-m systick: add idle timer
Browse files Browse the repository at this point in the history
Some chips, that use Cortex-M SysTick as the system timer, disable a
clock in a low power mode, that is the input for the SysTick e.g.
STM32Fx family.

It blocks enabling power management for these chips. The wake-up
function doesn't work and the time measurement is lost.

Add an additional IDLE timer that handles these functionality when the
system is about to enter IDLE. It has to wake up the chip and update the
cycle counter by time not measured by the SysTick. The IDLE timer has to
support counter API (setting alarm and reading current value).

Signed-off-by: Dawid Niedzwiecki <[email protected]>
  • Loading branch information
niedzwiecki-dawid committed Sep 28, 2023
1 parent b4da11f commit 914db9d
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 0 deletions.
19 changes: 19 additions & 0 deletions drivers/timer/Kconfig.cortex_m_systick
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Copyright (c) 2019 Intel Corp.
# SPDX-License-Identifier: Apache-2.0

DT_CHOSEN_IDLE_TIMER := zephyr,cortex-m-idle-timer

config CORTEX_M_SYSTICK
bool "Cortex-M SYSTICK timer"
depends on CPU_CORTEX_M_HAS_SYSTICK
Expand Down Expand Up @@ -40,3 +42,20 @@ config CORTEX_M_SYSTICK_64BIT_CYCLE_COUNTER

This is set to y by default when the hardware clock is fast enough
to wrap sys_clock_cycle_get_32() in about a minute or less.

config CORTEX_M_SYSTICK_IDLE_TIMER
bool "Use an additional timer while entering IDLE"
default $(dt_chosen_enabled,$(DT_CHOSEN_IDLE_TIMER))
depends on COUNTER
depends on TICKLESS_KERNEL
help
There are chips e.g. STMFX family that use SysTick as a system timer,
but SysTick is not clocked in low power mode. These chips usually have
another timer that is not stoped, but it has lower frequency e.g. RTC,
thus it can't be used as a main system timer.

Use the IDLE timer for timeout (wakeup) when the system is entering
IDLE state.

The chosen IDLE timer node has to support setting alarm from the
counter API.
108 changes: 108 additions & 0 deletions drivers/timer/cortex_m_systick.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <cmsis_core.h>
#include <zephyr/irq.h>
#include <zephyr/sys/util.h>
#include <zephyr/drivers/counter.h>

#define COUNTER_MAX 0x00ffffff
#define TIMER_STOPPED 0xff000000
Expand Down Expand Up @@ -77,6 +78,29 @@ static cycle_t announced_cycles;
*/
static volatile uint32_t overflow_cyc;

#ifdef CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER
/* This local variable indicates that the timeout was set right before
* entering idle state.
*
* It is used for chips that has to use a separate idle timer in such
* case because the Cortex-m SysTick is not clocked in the low power
* mode state.
*/
static bool timeout_idle;

/* Cycle counter before entering the idle state. */
static cycle_t cycle_pre_idle;

/* Idle timer value before entering the idle state. */
static uint32_t idle_timer_pre_idle;

/* IDLE lock needed to updated the cycle counter */
static struct k_spinlock idle_lock;

/* Idle timer used for timer while entering the idle state */
static const struct device *idle_timer = DEVICE_DT_GET(DT_CHOSEN(zephyr_cortex_m_idle_timer));
#endif /* CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER */

/* This internal function calculates the amount of HW cycles that have
* elapsed since the last time the absolute HW cycles counter has been
* updated. 'cycle_count' may be updated either by the ISR, or when we
Expand Down Expand Up @@ -143,6 +167,19 @@ void sys_clock_isr(void *arg)
cycle_count += overflow_cyc;
overflow_cyc = 0;

#ifdef CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER
/* Rare case, when the interrupt was triggered, with previously programmed
* LOAD value, just before entering the idle mode (SysTick is clocked) or right
* after exiting the idle mode, before executing the procedure in the
* sys_clock_idle_exit function.
*/
if (timeout_idle) {
z_arm_int_exit();

return;
}
#endif /* CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER */

if (TICKLESS) {
/* In TICKLESS mode, the SysTick.LOAD is re-programmed
* in sys_clock_set_timeout(), followed by resetting of
Expand Down Expand Up @@ -180,6 +217,36 @@ void sys_clock_set_timeout(int32_t ticks, bool idle)
return;
}

#ifdef CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER
if (idle) {
uint64_t timeout_us =
((uint64_t)ticks * USEC_PER_SEC) / CONFIG_SYS_CLOCK_TICKS_PER_SEC;
struct counter_alarm_cfg cfg = {
.callback = NULL,
.ticks = counter_us_to_ticks(idle_timer, timeout_us),
.user_data = NULL,
.flags = 0,
};

timeout_idle = true;

/* Set the alarm using timer that runs the idle.
* Needed rump-up/setting time, lower accurency etc. should be
* included in the exit-latency in the power state definition.
*/
counter_cancel_channel_alarm(idle_timer, 0);
counter_set_channel_alarm(idle_timer, 0, &cfg);

/* Store current values to calculate a difference in
* measurements after exiting the idle state.
*/
counter_get_value(idle_timer, &idle_timer_pre_idle);
cycle_pre_idle = cycle_count + elapsed();

return;
}
#endif /* CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER */

#if defined(CONFIG_TICKLESS_KERNEL)
uint32_t delay;
uint32_t val1, val2;
Expand Down Expand Up @@ -284,6 +351,47 @@ uint64_t sys_clock_cycle_get_64(void)

void sys_clock_idle_exit(void)
{
#ifdef CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER
if (timeout_idle) {
k_spinlock_key_t key = k_spin_lock(&idle_lock);
uint32_t idle_timer_diff;
uint32_t idle_timer_post;
cycle_t systick_diff;
cycle_t missed_cycles;
uint64_t systick_us;
uint64_t idle_timer_us;
uint64_t measurement_diff_us;

/* Get current values for both timers */
counter_get_value(idle_timer, &idle_timer_post);
systick_diff = cycle_count + elapsed() - cycle_pre_idle;

/* Calculate has much time has pasted since last measurement for both timers */
idle_timer_diff = idle_timer_post - idle_timer_pre_idle;
idle_timer_us = counter_ticks_to_us(idle_timer, idle_timer_diff);
systick_us =
((uint64_t)systick_diff * USEC_PER_SEC) / sys_clock_hw_cycles_per_sec();

/* Calculate difference in measurements to get how much time
* the SysTick missed in idle state.
*/
measurement_diff_us = idle_timer_us - systick_us;
missed_cycles =
(sys_clock_hw_cycles_per_sec() * measurement_diff_us) / USEC_PER_SEC;

/* Update the cycle counter to include the cycles missed in idle */
cycle_count += missed_cycles;

/* We've alredy performed all needed operations */
timeout_idle = false;

/* Request sys_clock_announce ASAP */
sys_clock_set_timeout(0, false);

k_spin_unlock(&idle_lock, key);
}
#endif /* CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER */

if (last_load == TIMER_STOPPED) {
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
}
Expand Down

0 comments on commit 914db9d

Please sign in to comment.