Fix imminent tick rescheduled after tickless idle

Prior to this commit, if something other than systick wakes the CPU from
tickless idle, vPortSuppressTicksAndSleep() might cause xTickCount to
increment once too many times.  See "bug 2" in this forum post:
https://forums.freertos.org/t/ultasknotifytake-timeout-accuracy/9629/40

SysTick
-------
The SysTick is the hardware timer that provides the OS tick interrupt
in the official ports for Cortex M.  SysTick starts counting down from
the value stored in its reload register.  When SysTick reaches zero, it
requests an interrupt.  On the next SysTick clock cycle, it loads the
counter again from the reload register.  To get periodic interrupts
every N SysTick clock cycles, the reload register must be N - 1.

Bug Example
-----------
- CPU is sleeping in vPortSuppressTicksAndSleep()
- Something other than the SysTick wakes the CPU.
- vPortSuppressTicksAndSleep() calculates the number of SysTick counts
  until the next tick.  The bug occurs only if this number is small.
- vPortSuppressTicksAndSleep() puts this small number into the SysTick
  reload register, and starts SysTick.
- vPortSuppressTicksAndSleep() calls vTaskStepTick()
- While vTaskStepTick() executes, the SysTick expires.  The ISR pends
  because interrupts are masked, and SysTick starts a 2nd period still
  based on the small number of counts in its reload register.  This 2nd
  period is undesirable and is likely to cause the error noted below.
- vPortSuppressTicksAndSleep() puts the normal tick duration into the
  SysTick's reload register.
- vPortSuppressTicksAndSleep() unmasks interrupts before the SysTick
  starts a new period based on the new value in the reload register.
  [This is a race condition that can go either way, but for the bug
  to occur, the race must play out this way.]
- The pending SysTick ISR executes and increments xPendedTicks.
- The SysTick expires again, finishing the second very small period, and
  starts a new period this time based on the full tick duration.
- The SysTick ISR increments xPendedTicks (or xTickCount) even though
  only a tiny fraction of a tick period has elapsed since the previous
  tick.

The bug occurs when *two* consecutive small periods of the SysTick are
both counted as ticks.  The root cause is a race caused by the small
SysTick period.  If vPortSuppressTicksAndSleep() unmasks interrupts
*after* the small period expires but *before* the SysTick starts a
period based on the full tick period, then two small periods are
counted as ticks when only one should be counted.

The end result is xTickCount advancing nearly one full tick more than
time actually elapsed as measured by the SysTick.  This is not the kind
of time slippage normally associated with tickless idle.

After this commit the code starts the SysTick and then immediately
modifies the reload register to ensure the very short cycle (if any) is
conducted only once.  This strategy requires special consideration for
the build option that configures SysTick to use a divided clock.  To
avoid waiting around for the SysTick to load value from the reload
register, the new code temporarily configures the SysTick to use the
undivided clock.  The resulting timing error is typical for tickless
idle.  The error (commonly known as drift or slippage in kernel time)
caused by this strategy is equivalent to one or two counts in
ulStoppedTimerCompensation.

This commit also updates comments and #define symbols related to the
SysTick clock option.  The SysTick can optionally be clocked by a
divided version of the CPU clock (commonly divide-by-8).  The new code
in this commit adjusts these comments and symbols to make them clearer
and more useful in configurations that use the divided clock.  The fix
made in this commit requires the use of these symbols, as noted in the
code comments.
This commit is contained in:
Jeff Tenney 2020-05-11 09:47:54 -07:00
parent 0c7b04bd3a
commit 967acc9b20

View file

@ -37,16 +37,6 @@
#error This port can only be used when the project options are configured to enable hardware floating point support.
#endif
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
/* Ensure the SysTick is clocked at the same frequency as the core. */
#define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
#else
/* The way the SysTick is clocked is not modified in case it is not the same
as the core. */
#define portNVIC_SYSTICK_CLK_BIT ( 0 )
#endif
/* Constants required to manipulate the core. Registers first... */
#define portNVIC_SYSTICK_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) )
@ -54,6 +44,7 @@
#define portNVIC_SYSPRI2_REG ( * ( ( volatile uint32_t * ) 0xe000ed20 ) )
#define portNVIC_ICSR_REG ( * ( ( volatile uint32_t * ) 0xe000ed04 ) )
/* ...then bits in the registers. */
#define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
#define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL )
#define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
#define portNVIC_SYSTICK_COUNT_FLAG_BIT ( 1UL << 16UL )
@ -103,6 +94,19 @@ occurred while the SysTick counter is stopped during tickless idle
calculations. */
#define portMISSED_COUNTS_FACTOR ( 45UL )
/* Let the user override the default SysTick clock rate. If defined by the
user, this symbol must equal the SysTick clock rate when the CLK bit is 0 in the
configuration register. */
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ configCPU_CLOCK_HZ
/* Ensure the SysTick is clocked at the same frequency as the core. */
#define portNVIC_SYSTICK_CLK_BIT_SETTING ( portNVIC_SYSTICK_CLK_BIT )
#else
/* Select the option to clock SysTick not at the same frequency as the core.
The clock used is often a divided version of the core clock. */
#define portNVIC_SYSTICK_CLK_BIT_SETTING ( 0 )
#endif
/* Let the user override the pre-loading of the initial LR with the address of
prvTaskExitError() in case it messes up unwinding of the stack in the
debugger. */
@ -615,7 +619,7 @@ void xPortSysTickHandler( void )
be, but using the tickless mode will inevitably result in some tiny
drift of the time maintained by the kernel with respect to calendar
time*/
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT );
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_SETTING | portNVIC_SYSTICK_INT_BIT );
/* Determine if the SysTick clock has already counted to zero and
been set back to the current reload value (the reload back being
@ -665,13 +669,24 @@ void xPortSysTickHandler( void )
portNVIC_SYSTICK_LOAD_REG = ( ( ulCompleteTickPeriods + 1UL ) * ulTimerCountsForOneTick ) - ulCompletedSysTickDecrements;
}
/* Restart SysTick so it runs from portNVIC_SYSTICK_LOAD_REG
again, then set portNVIC_SYSTICK_LOAD_REG back to its standard
value. */
/* Restart SysTick so it runs from portNVIC_SYSTICK_LOAD_REG again,
then set portNVIC_SYSTICK_LOAD_REG back to its standard value. If
the SysTick is not using the core clock, temporarily configure it to
use the core clock. This configuration forces the SysTick to load
from portNVIC_SYSTICK_LOAD_REG immediately instead of at the next
cycle of the other clock. Then portNVIC_SYSTICK_LOAD_REG is ready
to receive the standard value immediately. */
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
portNVIC_SYSTICK_CTRL_REG |= portNVIC_SYSTICK_ENABLE_BIT;
vTaskStepTick( ulCompleteTickPeriods );
portNVIC_SYSTICK_CTRL_REG = portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT;
#if( portNVIC_SYSTICK_CLK_BIT_SETTING != portNVIC_SYSTICK_CLK_BIT )
{
portNVIC_SYSTICK_CTRL_REG = portNVIC_SYSTICK_CLK_BIT_SETTING | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT;
}
#endif /* portNVIC_SYSTICK_CLK_BIT_SETTING */
portNVIC_SYSTICK_LOAD_REG = ulTimerCountsForOneTick - 1UL;
/* Step the tick to account for any tick periods that elapsed. */
vTaskStepTick( ulCompleteTickPeriods );
/* Exit with interrupts enabled. */
__asm volatile( "cpsie i" ::: "memory" );
@ -702,7 +717,7 @@ __attribute__(( weak )) void vPortSetupTimerInterrupt( void )
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_SETTING | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
/*-----------------------------------------------------------*/