Tickless Mode

From SEGGER Wiki
Revision as of 15:27, 15 July 2021 by Michael (talk | contribs) (Implementation of OS_Idle())
Jump to: navigation, search

This article describes how the embOS provides support for a tickless mode and what should be considered when it is implemented in an application. It shall facilitate the implementation and make the user aware of what he needs to do, and why. For more information on the tickless API, please refer to the embOS manual.

General Information

An RTOS needs a time base so that it can schedule preemptive tasks with higher priority and software timers. This time base is often realized using a hardware timer which generates periodic interrupts incrementing a system tick variable. We call the periodic timer interrupt the system tick interrupt. With embOS the system tick is incremented by calling OS_TICK_Handle() in the system tick interrupt. The periodic incrementation of the system tick, however, does not imply that there is an action that needs to be performed by the scheduler, leading to redundant system tick interrupts that just consumses CPU time.

Stopping the periodic system tick interrupt allows the microcontroller to remain for a longer period in a low power mode. This helps to save power for e.g. battery powered devices. The embOS tickless support provides the needed information and functionality so that the user can stop the periodic system tick interrupt for a specific period of time. This information and functionality provided by embOS is composed of the idle period and the ability to dynamically adjust the embOS system tick. Idle periods are periods of time when there are no tasks or software timers ready for execution. In other words, when the idle period elapses, then a task or software timer is ready for execution.

How does embOS work without Tickless Mode?

Let's take a very simple example application:

 1 #include "RTOS.h"
 2 #include "BSP.h"
 3 
 4 static OS_STACKPTR int Stack[128];  // Task stacks
 5 static OS_TASK         TCB;         // Task control blocks
 6 
 7 static void Task(void) {
 8   while (1) {
 9     BSP_ToggleLED(0);
10     OS_TASK_Delay(5);
11   }
12 }
13 
14 int main(void) {
15   OS_Init();    // Initialize embOS
16   OS_InitHW();  // Initialize required hardware
17   BSP_Init();   // Initialize LED ports
18   OS_TASK_CREATE(&TCB, "Task", 100, Task, Stack);
19   OS_Start();   // Start embOS
20   return 0;
21 }

This application creates a single task which periodically toggles an LED and delays itself for 5 ticks. As soon as the embOS was started, the task is executed and toggles the LED. When it delays itself, the scheduler is executed. The scheduler saves the task's context and looks if another task is ready for execution. Because the scheduler can't find any other task to execute, it simply calls the OS_Idle() function.

 1 void OS_Idle(void) {  // Idle loop: No task is ready to execute
 2   while (1) {
 3     // Nothing to do ... wait for interrupt
 4   }
 5 }
 6 
 7 void SysTick_Handler(void) {
 8   OS_INT_EnterNestable();
 9   OS_TICK_Handle();
10   OS_INT_LeaveNestable();
11 }

The OS_Idle() function contains an endless loop, as it may not return. It idles until the next system tick interrupt occurs. The system tick interrupt increments the embOS system tick, returns from the ISR, and the application continues where it was interrupted inside OS_Idle(). We idle until we receive the next system tick interrupt. This happens five times, but the fifth time the application won't continue execution in OS_Idle(), because the OS_TICK_Handle() function noticed that a task delay has expired and the scheduler needs to be executed. The ISR hands over the execution to the scheduler instead of continuing execution in OS_Idle(). The context of the OS_Idle() function is discarded, because OS_Idle() was using the stack of the scheduler while the scheduler wasn't running. Now, the scheduler starts the task which toggles the LED and delays itself again. The next time OS_Idle() is executed, it won't continue where it was interrupted, but it will be simply called again by the scheduler.

Application with redundant system tick interrupts.

How does the embOS work with Tickless Mode?

It is the user's responsibility to make sure that the hardware is reconfigured properly on tickless mode entry and exit and the embOS system tick is adjusted correctly afterwards. The goal is to remove all redundant system tick interrupts and instead program the timer so that there is only one system tick interrupt that occurs when the scheduler needs to be executed again. The tickless mode ends when it is either stopped earlier using OS_TICKLESS_Stop() or when the tickless mode expires. The tickless mode expires with the first call of OS_TICK_Handle() after OS_TICKLESS_Start() was called. The OS_TICK_Handle() call that ends the tickless mode needs to be called from an ISR, but it doesn't necessarily need to be called by the default timer interrupt used for the system tick. Technically, any embOS interrupt can be used to wake the microcontroller up from low power mode and call OS_TICK_Handle(). That means, that a second timer can be used just for the tickless period. The following ticks would then be handled again by the non tickless mode timer.

Application without redundant system tick interrupts.

The removal of 'redundant' system ticks does also reduce the CPU load. Even if there is no task ready to run, disabling the system tick during idle periods can prevent other interrupt from being executed directly.

Entering Tickless Mode

If the scheduler calls OS_Idle(), the user can put the application into the tickless mode by reconfiguring the timer, so that it generates the next system tick interrupt when a task or software timer needs to be scheduled again. The number of ticks until a task or software timer needs to run again is returned by the embOS API OS_TICKLESS_GetNumIdleTicks(). After the timer was reconfigured, the tickless period and a callback function, which is called when tickless mode ends, are passed to embOS via OS_TICKLESS_Start(). OS_TICKLESS_Start() does nothing more than saving the tickless period and the callback for later use.

The longest period that the application can stay in tickless mode is limited by the maximum period (in ticks) of the used timer. If the idle ticks returned by OS_TICKLESS_GetNumIdleTicks() exceed the maximum ticks that the timer can handle, then the tickless mode needs to be entered for a shorter period. For example, if the maximum ticks, after which the timer can generate an interrupt, are 150 and OS_TICKLESS_GetNumIdleTicks() returns 200, then the application can enter the tickless mode for the maximum of 150 ticks. When the idle period ends, the scheduler will call OS_Idle() again, allowing the application to enter the tickless mode for the remaining time, as OS_TICKLESS_GetNumIdleTicks() will now return 50.

An OS_Idle() implementation might look like this:

 1 void OS_Idle(void) {                                  // Idle loop: No task is ready to execute
 2   OS_TIME IdleTicks;
 3 
 4   OS_INT_Disable();                                   // Following calculations may not be interrupted by a system tick interrupt
 5   IdleTicks = OS_TICKLESS_GetNumIdleTicks();          // Read the number of ticks the application can stay in tickless mode
 6   if (IdleTicks > 1) {                                // Only reconfigure the timer if the we can skip at least one tick
 7     IdleTicks = MAX(IdleTicks, MAX_TICKS);            // Make sure IdleTicks does not exceed the max ticks of the timer
 8     _ReconfigureTimerForTicklessMode(IdleTicks);      // Reconfigure timer according to the time to spend in tickless mode
 9     OS_TICKLESS_Start(IdleTicks, _EndTicklessMode);   // Pass the tickless period and the callback to embOS
10   }
11   OS_INT_Enablde();
12 
13   _PrepareLowPowerMode();                             // If needed, prepare the device for the low power mode
14   while (1) {
15     _EnterLowPowerMode();                             // Enter low power mode
16   }
17 }

The Tickless Callback Function

When the tickless mode ends, the user needs to adjust the embOS system tick and reconfigure the timer again. This is done within the user callback function that is passed to OS_TICKLESS_Start(). The callback function is called by embOS when the tickless mode ends. Adjusting the embOS system tick is necessary, because the embOS isn't aware of how many ticks actually elapsed during the tickless period. OS_TICKLESS_Start() also receives the period for which the user wants to stay in the tickless mode. If the tickless mode has expired, the value returned by OS_TICKLESS_GetPeriod(), which is the period passed to OS_TICKLESS_Start(), can be used to adjust the embOS system tick via OS_TICKLESS_AdjustTime(). However, there might be scenarios in which the tickless mode is stopped earlier than expected using the OS_TICKLESS_Stop() function. The callback function can use OS_TICKLESS_IsExpired() to determine if the tickless mode expired or if it was stopped earlier. If the latter is true, the callback function can't use OS_TICKLESS_GetPeriod() but instead needs to calculate the exact elapsed time.

Optionally, the user wants or needs to revert some settings that were applied for the low power mode. This can be also done within the callback function. The _PrepareLowPowerMode() function called in the OS_Idle() example implementation could for instance disable clocks or peripherals, or change the clock frequency. If the tickless mode ended, this settings, of course, need to be reverted again, so that all needed peripherals are working again and the device is operating with the desired performance.

A tickless callback implementation might look like this:

 1 static void _EndTicklessMode(void) {
 2   OS_TIME ElapsedTime;
 3 
 4   _PrepraeNormalPowerMode();                // Optionally, revert low power mode configuration
 5   if (OS_TICKLESS_IsExpired() == 0) {       // Check if the tickless mode was stopped earlier
 6     ElapsedTime = _CalculateElapsedTime();  // If so, calculate the elapsed time
 7   } else {
 8     ElapsedTime = OS_TICKLESS_GetPeriod();  // Else, use the period the tickless mode was started with
 9   }
10   _ReconfigureTimerForNormalMode();         // Reconfigure the timer to generate the periodic system tick again
11   OS_TICKLESS_AdjustTime(ElapsedTime);      // Update the embOS system tick
12 }

FAQ

When do I need to use OS_TICKLESS_Stop()?

OS_TICKLESS_Stop() is used to exit the tickless mode earlier than expected. This can be the case if an unexpected interrupt occurs while the application is in tickless mode. The user can then call OS_TICKLESS_Stop() in order to execute the tickless callback so that the application is in non tickless mode again. However, this isn't necessary for every interrupt that might occur during tickless mode. If the interrupt doesn't use embOS API, it can simply do what it is supposed to do without calling OS_TICKLESS_Stop(). When the interrupt returns, the application will continue in OS_Idle() and enter low power mode again.

When is the Tickless Callback Function called?

The callback function is either called directly within OS_TICKLESS_Stop() or at the start of the scheduler when the tickless mode is active.

Can I enter Tickless Mode from within a Task?

Theoretically, there are no restrictions when an application can enter tickless mode. The tickless mode is technically just the replacement of the periodic system tick interrupts by a single (timer) interrupt and the adjustment of the embOS system time. However, there is a reason why the tickless mode was designed to be entered in OS_Idle() only. This is because it is usually only useful to enter tickless mode when the application wants to enter low power mode, and an application will only enter low power mode if there is nothing to do for the application. If an application has nothing to do, i.e. no task is ready for execution, the application will automatically enter OS_Idle().

Examples

Please note that these implementations are only examples how to implement the tickless mode and do not initialize and enter any device specific low power modes.

STM32L476

The following example is written for an ST STM32L476 using the LPTIM1 with the 32,000Hz LSI clock for the tickless mode as well as the non tickless mode. The LPTIM1 is used in continuous mode with the auto reload mechanic. That means that the counter starts at zero and counts up to the auto reload value contained in LPTIM1_ARR minus one. As soon as it reaches LPTIM1_ARR, an interrupt is triggered and the counter starts again from zero.

As long as the embOS stays in the non tickless mode, the LPTIM1_ARR register doesn't need to be changed. However, when the application wants to enter tickless mode, it has to load the tickless period as timer cycles into the LPTIM1_ARR register.

After the tickless mode has ended, LPTIM1_ARR needs to be set to its initial value again, so that a periodic tick interrupt is generated.

Example project: STM32L476_STM32L476RG_Nucleo_TicklessMode.zip

Implementation of OS_Idle()

 1 void OS_Idle(void) {
 2   OS_TIME IdleTicks;
 3 
 4   OS_INT_IncDI();
 5   IdleTicks = OS_TICKLESS_GetNumIdleTicks();                // Get idle period
 6   if (IdleTicks > 1) {                                      // Start tickless mode only if idle period >1
 7     if (IdleTicks > _LPTIM_MAX_TICKS) {                     // Make sure the timer can ahndle the idle period
 8       IdleTicks = _LPTIM_MAX_TICKS;                         // Else, enter tickless mode for the maximum possible period
 9     }
10     LPTIM1_CR = 0;                                          // Reset counter
11     LPTIM1_CR = (1u << 0);                                  // ENABLE: Enable timer
12     LPTIM1_ARR = (IdleTicks * _LPTIM_CYCLES_PER_TICK) - 1;  // Configure 1ms timer interrupt
13     LPTIM1_CR = (1u << 0) | (1u << 2);                      // CNTSTRT: Start counter in continuous mode
14     OS_TICKLESS_Start(IdleTicks, _EndTicklessMode);         // Start tickless mode
15   }
16   OS_INT_DecRI();
17   _EnterVoltageRange2();                                    // Enter voltage range 2, decrease CPU frequency
18   while (1) {
19     __WFI();
20   }
21 }

The tickless related code in OS_Idle() is located between OS_INT_IncDI() and OS_INT_DecRI(). Here we call OS_TICKLESS_GetNumIdleTicks() which returns the idle period. If the idle period is greater than one, i.e. there is at least one system tick that we can skip, then we can go on and initialize the tickless mode.

It is also possible to enter tickless mode only if the idle period exceeds a specific time. The time for reconfiguring the timer and entering a deep sleep mode might take longer than one or two system tick interrupts that would be avoided. The application could then, for example, enter tickless if the idle period is greater than 4, enter tickless mode and low power mode if the idle period is greater 10 and enter tickless mode and deep sleep mode with lowest power consumption when the idle period is at least 20 ticks.

If the idle period exceeds the maximum ticks that the timer can handle, then we set IdleTicks to the maximum possible ticks of the timer. The timer counter register width is 16 bits and one tick is 32 timer cycles. Thus, the maximum ticks are (2^16) / 32 = 2048.

Now, we need to configure the timer to generate an interrupt when the idle period ends. We reset the timer in order to reset the counter and write our idle period into the LPTIM1_ARR register. LPTIM1_ARR is calculated by multiplying the number of idle ticks that we want to spend in tickless mode with the number of cycles needed for one tick. We also need to subtract one from the value written to LPTIM1_ARR. Else, we would wait one cycle too long.

At last, we need to pass the idle period and the tickless callback function to OS_TICKLESS_Start(). If this is done, we can re-enable interrupts, enter the device specific voltage range 2 with decreased CPU clock and execute a WFI.

Implementation of the callback function

 1 static void _EndTicklessMode(void) {
 2   if (OS_TICKLESS_IsExpired() == 0) {
 3     OS_TICKLESS_AdjustTime(LPTIM1_CNT / _LPTIM_CYCLES_PER_TICK);
 4     LPTIM1_CR = 0;                                // Reset counter
 5     LPTIM1_CR = (1u << 0);                        // ENABLE: Enable counter
 6     LPTIM1_ARR = _LPTIM_CYCLES_PER_TICK - 1;      // Configure 1ms timer interrupt
 7     LPTIM1_CR = (1u << 0) | (1u << 2);            // CNTSTRT: Start continuous mode
 8   } else {
 9     OS_TICKLESS_AdjustTime(OS_TICKLESS_GetPeriod());
10     LPTIM1_ARR = _LPTIM_CYCLES_PER_TICK - 1;      // Configure 1ms timer interrupt
11   }
12 }

The tickless callback function checks whether the tickless period expired. If OS_TICKLESS_IsExpired() returns zero, it hasn't expired but was stopped earlier than expected. In this case we need to calculate how much time elapsed since we entered tickless mode. This is easily done by reading the counter and dividing it by the number of cycles needed for one tick. We pass this value to OS_TICKLESS_AdjustTime() to correct the embOS time. We also need to reset the counter value by disabling and enabling the timer.

If the tickless mode has expired, we retrieve the idle period using OS_TICKLESS_GetPeriod() and pass the result to OS_TICKLESS_AdjustTime(). Here we don't need to reset the counter, because it was just reseted by the timer interrupt. So we simply write the default reload value for the periodic 1ms timer interrupt into LPTIM1_ARR .

Note that we haven't entered the voltage range 1 and increased the CPU frequency in the callback function. This is because this should be done earlier, for example at the beginning of the system tick IRQ handler, so that the CPU executes the ISR at full speed.

nRF52832

The following example is written for a Nordic Semiconductor nRF52832 using the RTC with a 32,768Hz frequency for the tickless mode as well as the non tickless mode.

The example code uses the nRF SDK API to operate the RTC.

The RTC is used as a free running counter. This means that the counter is never stopped or written which makes it easier to provide long-term time accuracy, as long as the RTC itself is accurate. Timer interrupts are generated by using the compare and match functionality of the RTC which generates an interrupt when the counter equals the previously defined compare value.

Example project: nRF52832_nRF52_DK_TicklessMode.zip

Timer Specifics

The Frequency

Usually, with RTOSs timers are configured so that they generate an interrupt each millisecond. Since the RTC is running with 32,768Hz, it is not possible to generate accurate millisecond interrupts. Let's assume the counter is zero. Now we want to set the compare value register to generate an interrupt after one millisecond. We can't set the compare value to a float like 32.768, but only to either 32 or 33. This would result in an interrupt after either (1/32768)*32*1000=0.9765625ms or (1/32768)*33*1000=1.007080078125ms. If we're always adding 32 to the previous compare value in order to generate the next tick, the embOS time would actually run faster than desired. The opposite for 33. That means that we always need to calculate the closest next compare value to prevent the embOS time to steadily deviate.

Let's say that the embOS time is 49 and we want to generate an interrupt for the 50th tick. The calculation would be:

NextCompareValue = (49 + 1) / (1/32768 * 1000) = 1638.4 => 1638

So, we need to set the compare match register to 1638 which will generate an interrupt 49.98779296875ms after the counter was started.

We can turn this into the following formula which uses the embOS API function OS_TIME_GetTicks() to retrieve the embOS time and the float variable CounterResolution which contains the time that one counter cycle needs in milliseconds:

NextCompareValue = (OS_TIME_GetTicks() + 1) / CounterResolution

Setting the Compare Match Register

Due to the nature of the RTC and its compare and match functionality, it is possible that the RTC doesn't observe a compare and match event if the compare value wasn't at least 2 cycles ahead of the counter when it was set. This means that we need to compare our NextCompareValue with the counter and potentially increase the NextCompareValue value to prevent any missed compare and match events.

1 if (NextCompareValue - Counter < 2) {
2   NextCompareValue = Counter + 2;
3 }

RTC Register Width

The register width of the RTC counter register and the compare match register is only 24 bits. On a Cortex-M we're only able to read the register either as an 32-bit or 16-bit value. Since 16 bits aren't enough, we're reading all 32 bits. The most significant byte is hardwired to zero. Unfortunately, when reading the register as a 32-bit value, there is no sign extension applied to the 24-bit value. Reading 24-bit negative values therefore result in positive 32-bit values. This is problematic when we're calculating the difference of the counter and the compare match register and use the result to check whether the counter value already passed the compare value by a specific amount of cycles. Usually, if we subtract the counter value from the compare value, a negative result means that the compare value lies in the past and a positive value means it lies in the future. As we're calculating the difference, it is irrelevant if the minuend and subtrahend are positive or negative, as long as the actual difference isn't greater than 2^(N-1) where N is the register width.

Let's assume the 24-bit counter value is 0xFFFFFF and the next calculated compare value is 0x00000F. We want to check whether the 0x00000F is at least 2 cycles ahead of 0xFFFFFF. The result of the calculation using 24-bit integers is 0x00000F-0xFFFFFF=0x000010 => 16. Here we see that the compare value is 16 cycles ahead of the counter. Now, let's do this calculation with 32-bit integers: 0x0000000F-0x00FFFFFF=0xFF000010 => -16,777,200. Unfortunately, this result is wrong. However, looks like we get our desired result if we mask our 24-bit value: 0xFF000010&0x00FFFFFF=0x00000010 => 16. But, if we always mask the result, correct negative results would turn into positive ones. Here another example: 0x00000000-0x00000001=0xFFFFFFFF => -1. Here, our compare value is one cycle behind the counter, thus in the past. The condition (NextCompareValue - Counter < 2) would then be true. If we mask the result, the result isn't negative anymore: 0xFFFFFFFF&0x00FFFFFF=0x00FFFFFF => 16,777,215 Remember that the comparison is still done with 32-bit integers. The condition would then be false, although it should be true.

So, how can we perform an inequation like NextCompareValue - Counter < 2 with 24-bit values to check whether our compare value is at least 2 cycles ahead of the counter or not?

The solution is to apply a multiplication to both sides of the inequation. This doesn't change the meaning of what we actually want to do, but multiplicated with the right value, we have a left shift which moves the former MSB of the 24-bit value to the MSB of the 32-bit value. As our 32-bit value has 8 more MSBs, we multiply both sides of the inequation with 2^8.

((NextCompareValue - Counter) * 2^8) < (2 * 2^8)

Instead of multiplying, we can simply apply the left shift.

((NextCompareValue - Counter) << 8) < (2 << 8)

The resulting preprocessor macro for signed 24-bit integer comparisons could then look like this:

1 #define SIGNED_24BIT_CMP(a, op, b)  ((OS_I32)((a) << 8) op (OS_I32)((b) << 8))

Implementation of OS_Idle()

 1 void OS_Idle(void) {
 2   OS_U32 IdleTicks;
 3 
 4   OS_INT_IncDI();
 5   IdleTicks = OS_TICKLESS_GetNumIdleTicks();                      // Get ticks until next task or software timer needs to be scheduled
 6   if (IdleTicks > 1) {                                            // Enter tickless mode only if we can enter tickless mode for more than 1 tick
 7     if (IdleTicks > MaxRTCTime) {                                 // Make sure we don't exceed the timers maximum period
 8       IdleTicks = MaxRTCTime;
 9     }
10     PrevCompareValue = COMPUTE_COMPARE_VALUE(0);
11     NextCompareValue = COMPUTE_COMPARE_VALUE(IdleTicks);          // Calculate the compare match value at which the tickless mode ends.
12     nrfx_rtc_cc_set(&m_oRtcInstance, 0, NextCompareValue, true);  // Set the RTC compare register 0
13     OS_TICKLESS_Start(IdleTicks, _EndTicklessMode);               // Tell embOS that we have started tickless mode
14   }
15   OS_INT_DecRI();
16   while (1) {
17     __WFI();
18   }
19 }

The tickless related code in OS_Idle() is located between OS_INT_IncDI() and OS_INT_DecRI(). Here we call OS_TICKLESS_GetNumIdleTicks() which returns the idle period. If the idle period is greater than one, then we can go on and initialize the tickless mode.

If the idle period exceeds the maximum ticks that the timer can handle, then we set IdleTicks to the maximum possible ticks of the timer. The timer counter register width is 24 bits and the timer runs with 32,768Hz. Thus, the maximum ticks are MaxRTCTime = (2^24) / 32.768 = 512,000.

Now, we need to configure the timer to generate an interrupt when the idle period ends. We calculate the next compare value and write it to the CC register. Here, we don't need to check whether the next compare value is at least 2 cycles ahead of the counter, because we're only entering tickless mode if the idle period is at least 1 tick. This means that the next compare value is already 32.768 cycles ahead of the timer.

The next compare value is compute using the macro COMPUTE_COMPARE_VALUE(x) which looks like this:

1 #define COMPUTE_COMPARE_VALUE(x)    ((OS_U32)((((OS_TIME_GetTicks() + (x))) % MaxRTCTime) / CounterResolution) & 0xFFFFFFu)

This macro computes the next compare value in RTC counter cycles relative to the current embOS system time. The parameter x is the offset that is added to the current embOS system time. This way OS_Idle() can simply pass the IdleTicks to this macro to calculate the compare value needed to genrate an interrupt at the end of the tickless period. Before we're dividing the time by the counter resolution, a modulo operation is performed which clips the ticks to the range [0:MaxRTCTime) with MaxRTCTime equal to 512000. This prevents inaccurate results caused by the floating point calculation if the calculation is performed with too large numbers. By clipping the ticks, the result is always within the range of the RTC counter which is [0:2^24) and the most significant byte will be always zero. This is no problem, because we would mask the result with 0x00FFFFFF anyway. Here an example:

MaxRTCTime = 512000
CounterResolution = (1/32768)*1000
Ticks = 512001

Without Clipping:
Ticks / CounterResolution = 16,777,248 => 0x1000020
16777216 & 0x00FFFFFF     = 32         => 0x0000020

With Clipping:
(Ticks % MaxRTCTime)  = 1
1 / CounterResolution = 32 => 0x0000020
32 & 0x00FFFFFF       = 32 => 0x0000020

At last, we need to pass the idle period and the tickless callback function to OS_TICKLESS_Start(). If this is done, we can re-enable interrupts and execute the WFI.

Note that we also calculate the previous compare value and store it in PrevCompareValue which is needed in the tickless callback function.

Implementation of the callback function

 1 static void _EndTicklessMode(void) {
 2   if (OS_TICKLESS_IsExpired() == 0) {
 3     OS_U32 Counter;
 4     OS_U32 Time;
 5 
 6     Counter = nrfx_rtc_counter_get(&m_oRtcInstance);                       // Read RTC counter
 7     Time    = (OS_U32)((Counter - PrevCompareValue) * CounterResolution);  // Calculate elapsed time in ticks
 8     OS_TICKLESS_AdjustTime((OS_TIME)Time);                                 // Adjust embOS system tick
 9   } else {
10     OS_TICKLESS_AdjustTime(OS_TICKLESS_GetPeriod());
11   }
12   _ConfigureNextCC();
13 }

The tickless callback function checks whether the tickless period expired. If OS_TICKLESS_IsExpired() returns zero, it hasn't expired but was stopped earlier than expected. In this case we need to calculate how much time elapsed since we entered tickless mode. With the previous compare value and current counter value we can calculate the elapsed ticks. Here, we don't need to perform any multiplications or shifts when using 32-bit values for formerly 24-bit values, because we don't want to do any signed comparison. Here, we just need to mask the result of the subtraction. This gives us always a positive 32-bit result which is the difference between those 2 values.

Here an example:

24-bit calculation:
Counter = 0x000000
PrevCompareValue = 0x000001
Counter - PrevCompareValue = 0xFFFFFF => -1 (signed) => 16,777,215 (unsigned)

32-bit calculation without masking:
Counter = 0x00000000
PrevCompareValue = 0x00000001
Counter - PrevCompareValue = 0xFFFFFFFF => -1 (signed) => 4,294,967,295 (unsigned)

32-bit calculation with masking:
Counter = 0x00000000
PrevCompareValue = 0x00000001
(Counter - PrevCompareValue) & 0xFFFFFF = 0x00FFFFFF =>  => 16,777,215

We multiply the elapsed time in counter cycles with the counter resolution to convert the counter cycles into system ticks. The elapsed system ticks can then be passed to OS_TICKLESS_AdjustTime() to correct the embOS time.

If the tickless mode has expired, we retrieve the elapsed idle period using OS_TICKLESS_GetPeriod() and pass the result directly to OS_TICKLESS_AdjustTime().

At the end of the callback function _ConfigureNextCC() is called which configures the timer to generate an interrupt for the next system tick again.

 1 static void _ConfigureNextCC(void) {
 2   OS_U32 Counter;
 3 
 4   PrevCompareValue = COMPUTE_COMPARE_VALUE(0);
 5   NextCompareValue = COMPUTE_COMPARE_VALUE(1);
 6   //
 7   // RTC compare value may not be too close to counter value as it may skip the compare and don't trigger the CC event.
 8   //
 9   Counter = nrfx_rtc_counter_get(&m_oRtcInstance);
10   if (SIGNED_24BIT_CMP(NextCompareValue - Counter, <, RTC_MIN_COUNTER_DIFF)) {
11     NextCompareValue = nrfx_rtc_counter_get(&m_oRtcInstance) + RTC_MIN_COUNTER_DIFF;
12   }
13 
14   nrfx_rtc_cc_set(&m_oRtcInstance, 0, NextCompareValue, true);  // Set the RTC compare register 0
15 }

We calculate the next compare value, check if it's at least 2 cycles (RTC_MIN_COUNTER_DIFF == 2) ahead of the counter and write it into the compare register. If it's not 2 cycles ahead of the counter, then we adjust the compare value by 2 cycles.

The RTC IRQ Handler

 1 static void RTC0_Handler(nrfx_rtc_int_type_t a_eEventType) {
 2   if (a_eEventType == NRFX_RTC_INT_COMPARE0) {
 3     OS_INT_Enter();
 4     OS_TICK_Handle();
 5     //
 6     // Configure the timer to generate an interrupt for the next system
 7     // tick only when this interrupt does not end the tickless mode,
 8     // because the tickless callback will already configure the timer.
 9     // OS_TICKLESS_IsExpired() needs to be calle after OS_TICK_Handle().
10     //
11     if (OS_TICKLESS_IsExpired() == 0) {
12       _ConfigureNextCC();
13     }
14     OS_INT_Leave();
15   }

The RTC IRQ Handler needs some special handling, because we're not using some kind of auto reload mechanic, but need to set the CC register for each system tick manually. We're calling the _ConfigureNextCC() function to configure the timer for the next system tick interrupt. But this is done only if this interrupt isn't the interrupt that ends the tickless mode. This is because if this interrupt was the one that was configured in OS_Idle() to end the tickless mode, then the tickless callback will be called. Since the tickless callback already configures the timer for the next system tick interrupt, we don't need to do it twice. It should be mentioned, that OS_TICKLESS_IsExpired() only returns that the tickless mode has ended after OS_TICK_Handle() was called at the end of the tickless mode.