Skip to content

Instantly share code, notes, and snippets.

@JayKickliter
Created January 16, 2020 22:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JayKickliter/a3afab9af3908e42faf9f81b442bc263 to your computer and use it in GitHub Desktop.
Save JayKickliter/a3afab9af3908e42faf9f81b442bc263 to your computer and use it in GitHub Desktop.
FreeRTOS tickless idle with STM32 LPTIM
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include "system_config_lptim.h"
#include "system_config_power.h"
#include "system_config_irq.h"
#include "FreeRTOS.h"
#include "FreeRTOSConfig.h"
#include "task.h"
#include "stm32l4xx.h"
#include "stm32l4xx_hal.h"
#include "debug_util.h"
/* Frequency of clock feeding LPTIM1
* This cannot be asserted statically since it depend on RCC configuration */
#define LPTIM1_CLK_FRQ 32768
/* This is the maximum value that the LPTIM1 CNT register can hold. */
#define LPTIM1_CNT_MAXVAL UINT16_MAX
/* How much we're dividing the input clock before feeding it to LPTIM1 */
#define LPTIM1_PRESCALER_DIV LPTIM_PRESCALER_DIV2
#define LPTIM1_PRESCALER (1 << (LPTIM1_PRESCALER_DIV >> LPTIM_CFGR_PRESC_Pos))
/* Frequency at which LPTIM1's CNT register is incremented by 1 */
#define LPTIM1_INC_FREQ (LPTIM1_CLK_FRQ / LPTIM1_PRESCALER)
static_assert(LPTIM1_INC_FREQ == 16384, "");
/* How many LPTIM1 CNT register to use as 1 tick */
#define LPTIM1_CNTS_PER_TICK (LPTIM1_INC_FREQ / configTICK_RATE_HZ)
static_assert(LPTIM1_CNTS_PER_TICK == 16, "");
/* Longest LPTIM1 delay before overflow */
#define LPTIM1_MAX_DELAY_SECONDS ((UINT16_MAX + 1) / LPTIM1_INC_FREQ)
static_assert(LPTIM1_MAX_DELAY_SECONDS == 4, "");
/* How many ticks the system can sleep before CNT wraps around to 0 */
#define LPTIM1_MAX_SUPPRESSED_TICKS ((UINT16_MAX + 1) / LPTIM1_CNTS_PER_TICK)
/* How many CNTs it takes to load a new value into LPTIMx's register
* See page 14 of AN4865 for more detail
* CAUTION: This value was derived empirically and by increasing it by one
* until the tick frequency was correct in low power run mode, then 1 was
* added for safety */
#define LPTIM_REG_LOAD_DLY 3
#define MIN(A,B) (A < B ? A : B)
#define MAX(A,B) (A > B ? A : B)
static LPTIM_HandleTypeDef lptim_handle;
static inline uint32_t lptim_read_cnt(LPTIM_HandleTypeDef * t);
static inline bool lptim_should_load_arr(uint32_t cnt_current,
uint32_t arr_current,
uint32_t arr_new);
static inline uint32_t lptim_calculate_arr_for_next_n_ticks(uint32_t cnt,
uint32_t n_ticks);
static inline uint32_t lptim_calculate_tick_from_cnt(uint32_t cnt);
static inline void lptim_load_arr_from_init(LPTIM_HandleTypeDef * t,
uint32_t arr);
static inline void lptim_load_arr(LPTIM_HandleTypeDef * t,
uint32_t arr);
/* Uncomment to enable debug hooks */
//#define USE_DEBUG_HOOKS
#ifdef USE_DEBUG_HOOKS
#include "sensor_int.h"
#define INIT_DEBUG_PIN(PIN) sensor_int_configure_debug_out((enum sensor_int_pin)PIN, false)
#define DEBUG_PIN(PIN, STATE) sensor_int_set_debug_out((enum sensor_int_pin)PIN, STATE)
#define CLEAR_DEBUG() \
sensor_int_set_debug_out(SENSOR_INT_0, false); \
sensor_int_set_debug_out(SENSOR_INT_1, false)
#define hook_TIMER_SETUP() \
do \
{ \
INIT_DEBUG_PIN(SENSOR_INT_0); \
INIT_DEBUG_PIN(SENSOR_INT_1); \
CLEAR_DEBUG(); \
} while (0)
#define hook_IRQ_START() \
do \
{ \
DEBUG_PIN(SENSOR_INT_0, true); \
} while (0)
#define hook_IRQ_FINISH() \
do \
{ \
DEBUG_PIN(SENSOR_INT_0, false); \
} while (0)
#define hook_PRE_SLEEP() \
do \
{ \
} while (0)
#define hook_POST_SLEEP() \
do \
{ \
} while (0)
#define hook_PRE_ARR() \
do \
{ \
DEBUG_PIN(SENSOR_INT_1, true); \
} while (0)
#define hook_POST_ARR() \
do \
{ \
DEBUG_PIN(SENSOR_INT_1, false); \
} while (0)
#else
#define hook_TIMER_SETUP() do {} while (0)
#define hook_IRQ_START() do {} while (0)
#define hook_IRQ_FINISH() do {} while (0)
#define hook_PRE_SLEEP() do {} while (0)
#define hook_POST_SLEEP() do {} while (0)
#define hook_PRE_ARR() do {} while (0)
#define hook_POST_ARR() do {} while (0)
#endif
/** LPTIM1 Init
*
* Initializes LPTIM1 to act as a 1 ms(ish) tick timer */
void system_config_lptim1_init(void)
{
lptim_handle = (LPTIM_HandleTypeDef){
.Instance = LPTIM1,
.Init =
{
.Clock =
{
/* Clock LPTIM1 from whatever RCC is feeding it */
.Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC,
/* Divide input clock (32768 Hz oscillator) by N for an
* increment rate of 32768/N Hz */
.Prescaler = LPTIM1_PRESCALER_DIV,
},
.Trigger =
{
/* Control (start) the timer trigger from software */
.Source = LPTIM_TRIGSOURCE_SOFTWARE,
},
/* Immediately use new autoreload value when it's changed */
.UpdateMode = LPTIM_UPDATE_IMMEDIATE,
/* Increment timer with every clock tick */
.CounterSource = LPTIM_COUNTERSOURCE_INTERNAL,
},
};
/* Initialize timer */
HAL_StatusTypeDef const stat = HAL_LPTIM_Init(&lptim_handle);
assert(stat == HAL_OK);
/* Enable global LPTIM1 interrupt */
NVIC_SetPriority(LPTIM1_IRQn, SYSTEM_CONFIG_IRQ_PRIORITY_LPTIM1);
NVIC_EnableIRQ(LPTIM1_IRQn);
assert(0 == lptim_read_cnt(&lptim_handle));
/* Enable LPTIM1 compare match interrupt
*
* CAUTION: Can only be set before LPTIMx's ENABLE bit is set to 1 */
__HAL_LPTIM_ENABLE_IT(&lptim_handle, LPTIM_IT_ARRM);
/* Enable LPTIM1 */
__HAL_LPTIM_ENABLE(&lptim_handle);
/* Set autoreload value to trigger an interrupt at the next whole tick
* boundary
*
* CAUTION: Can only be set after LPTIMx's ENABLE bit is set to 1 */
lptim_load_arr_from_init(&lptim_handle, LPTIM1_CNTS_PER_TICK - 1);
/* Start LPTIM1
*
* CAUTION: Can only be set after LPTIMx's ENABLE bit is set to 1 */
__HAL_LPTIM_START_CONTINUOUS(&lptim_handle);
}
void LPTIM1_IRQHandler(void)
{
dbg_util_event_log(dbg_lptim1_isr_start);
/* Make sure that LPTIM's interrupts are configured correctly */
assert(__HAL_LPTIM_GET_FLAG(&lptim_handle, LPTIM_FLAG_ARRM));
assert(__HAL_LPTIM_GET_IT_SOURCE(&lptim_handle, LPTIM_FLAG_ARRM));
/* Clear the interrupt */
__HAL_LPTIM_CLEAR_FLAG(&lptim_handle, LPTIM_FLAG_ARRM);
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
* executes all interrupts must be unmasked. There is therefore no need to
* save and then restore the interrupt mask value as its value is already
* known. */
(void)portSET_INTERRUPT_MASK_FROM_ISR();
{
hook_IRQ_START();
/* Increment the RTOS tick. */
if (xTaskIncrementTick() != pdFALSE)
{
/* A context switch is required. Context switching is performed in
the PendSV interrupt. Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
hook_IRQ_FINISH();
}
portCLEAR_INTERRUPT_MASK_FROM_ISR(0);
dbg_util_event_log(dbg_lptim1_isr_end);
}
/** Returns true only when there are enough CNTs left before the next tick based
* on current ARR and the next desired tick based on the new ARR
*/
static inline bool lptim_should_load_arr(uint32_t cnt_current,
uint32_t arr_current,
uint32_t arr_new)
{
assert(((arr_current + 1) % LPTIM1_CNTS_PER_TICK) == 0);
assert(((arr_new + 1) % LPTIM1_CNTS_PER_TICK) == 0);
/* Should not load ARR if the current and new ARRs match */
/* Check that are at least LPTIM_REG_LOAD_DLY clock-cycles (CNTs) until the
* current ARR will trigger an ARRM interrupt */
if (arr_current - cnt_current <= LPTIM_REG_LOAD_DLY)
{
return false;
}
/* Check that are at least LPTIM_REG_LOAD_DLY clock-cycles (CNTs) until the
* new ARR will trigger an ARRM interrupt */
if (arr_new - cnt_current <= LPTIM_REG_LOAD_DLY)
{
return false;
}
return true;
}
/** Given an LPTIM count, returns the autoreload (ARR) required to fire an
* autoreload match (ARRM) interrupt `n_ticks` from now */
static inline uint32_t lptim_calculate_arr_for_next_n_ticks(uint32_t cnt,
uint32_t n_ticks)
{
/* Must be at least 1 tick */
assert(n_ticks > 0);
/* If we try to delay more than this, we will get a bogus high value in arr
* (cnt + rem_cnts + ...) >= 2^33 that could underflow (when == 2^33) when
* we -= 1 at the end. So instead we return the max value that LPTIM1 can
* count to, because that is always going to be sooner than this very long
* delay. */
const uint32_t tick_limit = UINT16_MAX / LPTIM1_CNTS_PER_TICK;
if (n_ticks > tick_limit) {
return LPTIM1_CNT_MAXVAL;
}
/* Calculate how counts are left until LPTIM1->CNT is a multiple of
* LPTIM1_CNTS_PER_TICK */
const uint32_t rem_cnts =
LPTIM1_CNTS_PER_TICK - (cnt % LPTIM1_CNTS_PER_TICK);
/* Calculate ARR
* Adding rem_cnts to cnt takes us to the next tick boundry, so we need to
* subtract one tick from n_ticks */
uint32_t arr = cnt + rem_cnts + ((n_ticks - 1) * LPTIM1_CNTS_PER_TICK);
/* Clamp arr to the maximum possible ARR */
if (arr > LPTIM1_CNT_MAXVAL + 1)
{
arr = LPTIM1_CNT_MAXVAL + 1;
}
assert(arr % LPTIM1_CNTS_PER_TICK == 0);
/* The ARRM interrupt fires 1 LPTIM clock-cycle after LPTIMx->ARRM ==
* LPTIMx->CNT. We must subtract one from our calculated ARR to avoid tick
* drift */
arr -= 1;
return arr;
}
/** Returns which tick boundry a timer count belongs to
*
* The returned tick boundry is not absolute since LPTIM's CNT wraps back around
* to 0 on every autoreload match.
*/
static inline uint32_t lptim_calculate_tick_from_cnt(uint32_t cnt)
{
return cnt / LPTIM1_CNTS_PER_TICK;
}
static inline bool lptim_arr_is_stable(LPTIM_HandleTypeDef * t)
{
return __HAL_LPTIM_GET_FLAG(t, LPTIM_FLAG_ARROK);
}
/** Load a new autoreload value into LPTIMx->ARR
*
* The ARROK flag must be true.
*/
static inline void lptim_load_arr(LPTIM_HandleTypeDef * t,
uint32_t arr)
{
/* ARR only uses 16 bits out of its 32 bit register */
assert(arr <= UINT16_MAX);
/* The ARROK flag must be false before setting a new value.
* Otherwise, it's undefined behavior. */
assert(lptim_arr_is_stable(t));
/* Only load the ARR if the requested ARR is different. */
if (t->Instance->ARR != arr)
{
hook_PRE_ARR();
/* Clear the flag */
__HAL_LPTIM_CLEAR_FLAG(t, LPTIM_FLAG_ARROK);
/* Load the new autoreload value */
__HAL_LPTIM_AUTORELOAD_SET(t, arr);
hook_POST_ARR();
}
}
static inline void lptim_load_arr_from_init(LPTIM_HandleTypeDef * t,
uint32_t arr)
{
/* ARR only uses 16 bits out of its 32 bit register */
assert(arr <= UINT16_MAX);
/* There's one case where we expect the ARROK flag to be false
* when setting it. That's from the initialization routine. Here
* we'll assert that it's not OK (which indicates it's never been
* set.) */
assert(!lptim_arr_is_stable(t));
/* Only load the ARR if the requested ARR is different. */
if (t->Instance->ARR != arr)
{
/* Clear the flag */
__HAL_LPTIM_CLEAR_FLAG(t, LPTIM_FLAG_ARROK);
/* Load the new autoreload value */
__HAL_LPTIM_AUTORELOAD_SET(t, arr);
}
}
/** Read the CNT value until it's stable.
*
* CNT must return the same result twice in a row before we can assume
* the value is stable.
*/
static inline uint32_t lptim_read_cnt(LPTIM_HandleTypeDef * t)
{
uint32_t cnt0;
uint32_t cnt1;
do
{
cnt0 = t->Instance->CNT;
cnt1 = t->Instance->CNT;
} while (cnt1 != cnt0);
return cnt1;
}
/** Read the ARR value.
*
* Convenience function to access the ARR value of a LPTIM.
*/
static inline uint32_t lptim_read_arr(LPTIM_HandleTypeDef * t)
{
return t->Instance->ARR;
}
/*
* FreeRTOS specific code
*/
/* Use custom tick source and tickless idle when true */
#if configUSE_TICKLESS_IDLE == 2
/** Start LPTIM1
*
* Called from FreeRTOS so it doesn't start receiving ticks before it's
* ready.
*/
void vPortSetupTimerInterrupt(void)
{
hook_TIMER_SETUP();
system_config_lptim1_init();
}
extern bool system_config_power_sleep_permitted(void);
void vPortSuppressTicksAndSleep(TickType_t expected_idle_ticks)
{
/* If the ARR isn't yet stable, return immediately. We'll spend
* more time in the Idle task until the ARR is ready. */
if (!lptim_arr_is_stable(&lptim_handle))
{
return;
}
/* Read the current ARR out of LPTIM */
const uint32_t entry_arr = lptim_read_arr(&lptim_handle);
/* Read the current count value out of LPTIM and calculate tick boundary */
const uint32_t pre_sleep_cnt = lptim_read_cnt(&lptim_handle);
const uint32_t pre_sleep_tick =
lptim_calculate_tick_from_cnt(pre_sleep_cnt);
/* Calculate the ARR required to wait expected_idle_ticks tick periods */
/* Enter a critical section but don't use the taskENTER_CRITICAL() method as
* that will mask interrupts that should exit sleep mode. */
__asm volatile("cpsid i");
/* NOTE: system_config_power_sleep_permitted must be called from
* inside a critical section. */
if (!system_config_power_sleep_permitted()) {
/* If we're not going to be going to sleep, we need to make
* sure that the LPTIM ISR fires at least as often as we need
* the watchdog pet. However, due to "difficulties" of the hardware,
* we can sometimes get woken up from a sleep by an ISR and not be
* able to reset the LPTIM ISR to go off at a regular period - see the
* `dbg_lptim_complete_unstable_arr` log message below. By making sure
* we only idle for 1 tick, we're making sure that the complete with
* unstable arr case is impossible.
* We only reduce sleeps to 1 tick when a power consumer is active
* because we assume that is the only time we will wake due to
* interrupts while ARR is still unstable. However, in deep sleep, it
* is still possible for a sensor int wake to cause this bug. We don't
* have a workaround for that for now.
*/
expected_idle_ticks = MIN(1, expected_idle_ticks);
}
const uint32_t arr_for_sleep = lptim_calculate_arr_for_next_n_ticks(
pre_sleep_cnt, expected_idle_ticks);
/* If a context switch is pending or a task is waiting for the scheduler to
* be unsuspended then abandon the low power entry. */
if (eTaskConfirmSleepModeStatus() == eAbortSleep)
{
/* Re-enable interrupts - see comments above the cpsid instruction */
__asm volatile("cpsie i");
}
else
{
/* Set new LPTIM1 autoreload value */
if (lptim_should_load_arr(pre_sleep_cnt, entry_arr, arr_for_sleep))
{
lptim_load_arr(&lptim_handle, arr_for_sleep);
}
else
{
/* Exit early if there is not enough time to load `arr_for_sleep`
* before the next tick will fire.
*
* If we do not exit early the following scenario can happen:
* 1. Something other than LPTIM wakes the CPU
* 2. The tick fires and LPTIM1 wraps around _after_ checking what
* woke us up
* 3. `assert(post_sleep_cnt >= pre_sleep_cnt)` in the 'woke by
* other than LPTIM' branch fails
*/
/* Re-enable interrupts and return without sleeping */
__asm volatile("cpsie i");
return;
}
/* Sleep until something happens. configPRE_SLEEP_PROCESSING() can set
* its parameter to 0 to indicate that its implementation contains its
* own wait for interrupt or wait for event instruction, and so wfi
* should not be executed again. However, the original expected idle
* time variable must remain unmodified, so a copy is taken. */
uint32_t modifiable_idle_ticks = expected_idle_ticks;
hook_PRE_SLEEP();
configPRE_SLEEP_PROCESSING(&modifiable_idle_ticks);
if (modifiable_idle_ticks > 0)
{
__asm volatile("dsb");
__asm volatile("wfi");
__asm volatile("isb");
}
configPOST_SLEEP_PROCESSING(&modifiable_idle_ticks);
hook_POST_SLEEP();
/* Immediately save LPTIM's count value for future calculation */
uint32_t const post_sleep_cnt = lptim_read_cnt(&lptim_handle);
uint32_t const post_sleep_arr = lptim_read_arr(&lptim_handle);
/* Read LPTIM1 autoreload match flag before enabling interrupts */
bool woke_by_lptim_arrm =
__HAL_LPTIM_GET_FLAG(&lptim_handle, LPTIM_FLAG_ARRM);
#if 0
/* THIS DOES NOT HOLD WHILE DEBUGGING. IT CAN BE USED TO TEST
* OTHER ASSUMPTIONS, BUT HALTING THE PROCESSOR WILL MAKE THIS
* WEIRD. */
assert(woke_by_lptim_arrm ? post_sleep_cnt == 0 : true);
#endif
/* If `post_sleep_cnt == 0`, we must have been woken up by
* LPTIM ARRM or by an extremely well timed interrupt. Either
* way, treat it as if we've been woken by the lptim_arrm.
*/
if (post_sleep_cnt == 0) {
woke_by_lptim_arrm = true;
}
/* Re-enable interrupts - see comments above the cpsid instruction */
__asm volatile("cpsie i");
/**************************************************************
* LPTIM1_IRQHandler is called between these two statements if
* the ARR matched/we got a normal wakeup.
**************************************************************/
/* Will be set to the number of ticks that were completed
* between the time we went to sleep and woke up again. */
uint32_t completed_ticks = 0;
if (!lptim_arr_is_stable(&lptim_handle))
{
/* We went to sleep, but something woke us up before the
* ARR had settled completely. We can't do anything about
* this right now so bail out to the idle task. It will
* attempt again shortly. */
dbg_util_event_log(dbg_lptim_complete_unstable_arr);
return;
} else if (woke_by_lptim_arrm)
{
/* We're awake because of LPTIM compare match match interrupt, which
* means we completed the tickless period. */
/* Determine which tick of the N ticks we can hold in the
* LPTIM1 CNT register. */
uint32_t const post_sleep_tick =
lptim_calculate_tick_from_cnt(post_sleep_arr);
/* We will use this value to update FreeRTOS's tick count */
completed_ticks = post_sleep_tick - pre_sleep_tick;
/* Configure timer to fire the next interrupt at a normal rate */
uint32_t const next_arr =
lptim_calculate_arr_for_next_n_ticks(0, 1);
if (lptim_should_load_arr(post_sleep_cnt, post_sleep_arr, next_arr))
{
lptim_load_arr(&lptim_handle, next_arr);
}
}
else
{
/* Something other than LPTIM1 woke us up, figure out how long we
* actually slept */
assert(post_sleep_cnt >= pre_sleep_cnt);
uint32_t const post_sleep_tick =
lptim_calculate_tick_from_cnt(post_sleep_cnt);
completed_ticks = post_sleep_tick - pre_sleep_tick;
assert(completed_ticks <= post_sleep_tick);
/* Calculate ARR for the next tick boundary */
uint32_t const next_arr_0 =
lptim_calculate_arr_for_next_n_ticks(post_sleep_cnt, 1);
/* In case we're too close to the next tick boundary, calculate ARR
* for two tick boundaries from now */
uint32_t const next_arr_1 =
lptim_calculate_arr_for_next_n_ticks(post_sleep_cnt, 2);
if (lptim_should_load_arr(
post_sleep_cnt, arr_for_sleep, next_arr_0))
{
/* We can load ARR for the next tick boundary */
assert(next_arr_0 <= arr_for_sleep);
lptim_load_arr(&lptim_handle, next_arr_0);
}
else if (lptim_should_load_arr(
post_sleep_cnt, arr_for_sleep, next_arr_1))
{
/* We can load ARR for two tick boundaries from now */
assert(next_arr_1 <= arr_for_sleep);
lptim_load_arr(&lptim_handle, next_arr_1);
}
else if ((post_sleep_tick + 1) == LPTIM1_MAX_SUPPRESSED_TICKS)
{
/* ARR is already set to its maximum value. All we can do set it
* for first tick boundary after wrap-around */
lptim_load_arr(
&lptim_handle, lptim_calculate_arr_for_next_n_ticks(0, 1));
}
else
{
/* We're too close to the next time the tick would
* have expired. This happens when both the previous
* checks for lptim_should_load_arr fail. Since we
* can't reset the ARRM and since it's going to expire
* very soon any way, we're going to just update the
* completed ticks from this interrupt and wait for
* the next ARRM interrupt. */
}
}
portENTER_CRITICAL();
{
vTaskStepTick(completed_ticks);
}
portEXIT_CRITICAL();
}
}
#endif /* configUSE_TICKLESS_IDLE == 2 */
@iwasz
Copy link

iwasz commented Feb 14, 2020

Hey Jay

I've been working on a LPTIM1 tickless implementation for a few days from now, and found out your Gist. I made some tests, and it proven to be much more stable than mine. And when I say "stable" I mean the "time slippage" between the system time and real, clock time when the sleep mode is interrupted by some unexpected IRQ (other than LPTIM1_IRQ).

But I don't understand one thing in your code, and I wonder if it's a bug. At the end of vPortSuppressTicksAndSleep between line 599 and 630 where you finally set the ARR, you don't restore it to the "usual" value which would generate 1ms-ish ticks (ARR == 16-1) but instead you have 4 different possibilities (and I believe one of them restores the 1ms "usual tick").

This code works great if the Idle task is the only one that is running; in such a case next call to vPortSuppressTicksAndSleep just continues right where it was interrupted (at least this is what I understand).

But what if vPortSuppressTicksAndSleep was interrupted by an IRQ which unblocks (gives) a semaphore, and then some other task (which takes this semaphore) starts to perform some lengthy calculations, virtually preventing the task to go to the blocking state for some time? Idle task would not be run immediately (but only after this other task finishes) but ARR can be now set to some bigger (and random) value than our usual "one-tick" value. And now our system tick can be anything between 1 tick and expected_idle_ticks.

In the tests which can be found in this branch I simply flash 2 LEDs (NUCLEO-L476). And I observed exactly what I described above (I can send you wave forms if you want). In my prvFlashTask3 task I simple flash a led with 100ms delays, but the delays gets disturbed by SysTick IRQ.

Of course I can be wrong, because of my limited experience with RTOSes. I started experimenting with FreeRTOS like 10 days ago. Looking forward for your thoughts about this.

@JayKickliter
Copy link
Author

JayKickliter commented Feb 14, 2020

@iwasz Are you trying to use SysTick and LPTIM at the same time? If so, maybe that's the problem? This code only uses LPTIM for ticks, and we completely disabled the SysTick timer.

That said, it's been so long since we wrote this code that I'm not 100% sure. What I do remember is that this took a ridiculous amount of human hours to get right with all the edge cases. Although I wouldn't be terribly surprised if there were more lingering edge cases, we did run this code for quite some time in production and it settled after a while.

@iwasz
Copy link

iwasz commented Feb 14, 2020

Thanks for a quick reply, I'm not surprised It took some time to write this, Internet is full of posts with questions about FreeRTOS + LPTIM not working. I used SysTick only for a test, to disturb sleep mode (ie. to woke the µC prematurely). This way every time MCU wakes up, it runs the else branch on line 581, as this is the most troublesome part for me.

@JayKickliter
Copy link
Author

JayKickliter commented Feb 15, 2020

Yeah, sorry. I should have read your writeup more carefully before commenting. I hate to say it, but I don't have an answer for you. And not because it's unsolvable, but I haven't thought about it for so long.

The only other things I can think to check are kinda generic:

  1. interrupt priorities
  2. priority inversion
  3. maybe it's working fine but you're expectations are wrong? (read that in a friendly manner)

I would be interested in seeing the LED waveforms. You can drop the traces/screenshots right in the comments here.

If you do figure it out, please update me so I can update this gist. I only posted it here because I figured it was probably the only working LPTIM tick implementation and would hate to mislead people.

I'm not sure if the project this came from even exists anymore, but if you're interested, I see about tracking it down for more info.

@JayKickliter
Copy link
Author

I see that Zephyr has a LPTIM-based tick. I haven't looked closely, but maybe you can glean something from them. Their code is usually pretty good.

@iwasz
Copy link

iwasz commented Feb 17, 2020

OK, here are the screenshots. First one shows your gist working with two tasks which can be seen here.
jays
To make long story short, the Task3 (top waveform) looks like that:

static void prvFlashTask3 (void *pvParameters)
{
        (void)pvParameters;
        TickType_t xLastExecutionTime = xTaskGetTickCount ();

        for (;;) {
                vTaskDelayUntil (&xLastExecutionTime, (TickType_t)100 / portTICK_PERIOD_MS);
                HAL_GPIO_TogglePin (GPIOA, GPIO_PIN_4);
        }
}

So the sole purpose of this is to blink a LED. And as you can see it is disturbed by the "Task1" which from time to time runs a very long for loop and starves the "Task3" even though the latter has higher priority (I tried this with both priorities set to the same number as well). I mean technically it does not starve it, but disturbs the perception of time by the FreeRTOS (or at least I think so). The waveform which is in my opinion correct is this:
mine

I'll investigate this further, and will try to understand how Zephyr does that, thanks for the link.

@jefftenney
Copy link

@iwasz I just finished a very clean version of no-drift tickless idle for STM32 using LPTIM. I plan to make it available soon. Would you be interested in helping to test it?

@iwasz
Copy link

iwasz commented Feb 17, 2020

@iwasz I just finished a very clean version of no-drift tickless idle for STM32 using LPTIM. I plan to make it available soon. Would you be interested in helping to test it?

I'm not an expert but sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment