Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

timer: cortex-m systick: add idle timer #63187

Merged
merged 1 commit into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 stopped, but it has lower frequency e.g.
RTC, thus it can't be used as a main system timer.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to document the required interface for this somewhere. "The idle timer must be a counter device chosen in device tree via...", etc...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The requirement is mentioned later in the help message

The chosen IDLE timer node has to support setting alarm from the counter API.


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.
101 changes: 101 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,26 @@ 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 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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that you're predicating this on a DTS counter device already, you could do this entirely with devicetree and avoid the extra kconfig.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, but I would like to keep the kconfig with the help for additional documentation. If you want, I can change the config to non user-configurable one as a follow-up PR. Does it make sense?

#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 +164,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 +214,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 +348,43 @@ uint64_t sys_clock_cycle_get_64(void)

void sys_clock_idle_exit(void)
{
#ifdef CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER
if (timeout_idle) {
cycle_t systick_diff, missed_cycles;
uint32_t idle_timer_diff, idle_timer_post, dcycles, dticks;
uint64_t systick_us, idle_timer_us, 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;

/* Announce the passed ticks to the kernel */
dcycles = cycle_count + elapsed() - announced_cycles;
dticks = dcycles / CYC_PER_TICK;
announced_cycles += dticks * CYC_PER_TICK;
sys_clock_announce(dticks);

/* We've alredy performed all needed operations */
timeout_idle = false;
}
#endif /* CONFIG_CORTEX_M_SYSTICK_IDLE_TIMER */

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