How to Write a Basic Dynamic Tick BSP for μC/OS-III

There are a number of features in µC/OS-III which require time-keeping services. For example, we may want our task to delay for some amount of time before running again, or we may want it to wait on a semaphore but timeout if the semaphore is not available after some time. Time-keeping in µC/OS-III has always been performed by means of a tick interrupt.

Prior to V3.05.00, µC/OS-III only supported Periodic Tick Mode. This is still the most commonly used mode, and it only requires programming a timer once to interrupt the system at a fixed frequency (usually 1 KHz). We call this frequency the Tick Rate, since every interrupt represents a single tick of system time.

Periodic-Tick-Mode

Figure 1 - Periodic Tick Mode

One drawback to using Periodic Tick Mode is that the tick interrupt fires even when there is nothing to wake up. This is unacceptable for low-power applications because it would mean potentially waking the MCU from sleep mode on every tick. Our advice to customers in this situation has heretofore been to disable the tick altogether and to rely only on the other kernel services. In fact, this approach is still valid for low-power applications with no need for timeouts or delays. However, for those applications which need to conserve power without disabling the tick, a different solution had to be developed.

What is Dynamic Tick Mode?

Dynamic Tick Mode is a feature that was first introduced in µC/OS-III V3.05.00. The idea is straightforward: tick interrupts should only occur when a time delay or timeout has expired. Rather than programming the tick timer once at a set frequency, we instead dynamically re-program the timer to always expire at the nearest timeout. When used, this mode provides power savings without having to disable any of the OS features. A key difference between the two tick modes is the relationship between ticks and tick interrupts. In Periodic Tick Mode, each tick interrupt represented a single time tick. This is not the case with Dynamic Tick Mode. As Figure 2 demonstrates, a tick interrupt must now account for the passing of multiple ticks.

Dynamic-Tick-Mode

Figure 2 - Dynamic Tick Mode

With the release of μC/OS-III V3.07.00, the Dynamic Tick API has been improved to simplify the BSP implementation. However, this improvement required us to break compatibility with older versions. The V3.07.00 API is what we will cover throughout the remainder of this blog. It represents the standardized implementation of Dynamic Tick that will be used in future releases.

Requirements

A programmable hardware timer with interrupts and overflow detection.

  • 32-bit timers are preferred, but smaller timers can be used with some caveats which will be covered later.
  • The timer’s frequency should never be less than the OS Tick Frequency. It is highly recommended that it also be an integer multiple of the OS Tick Frequency, otherwise, we may lose accuracy due to integer division.

How Does It Work?

There are two operations that the OS needs to perform when running in Dynamic Tick Mode:

  1. Set an interrupt to fire after a certain number of ticks.

    If the tick-list is empty, the OS will specify 0 ticks to indicate an indefinite wait.

  2. Read the amount of ticks which have elapsed since it last set the interrupt.

The BSP fulfills these requirements by:

  1. Programming the hardware timer to count the requested number of ticks before triggering an interrupt.

    If the OS specified an indefinite wait, we should program the timer to wait the maximum number of ticks that it is able.

  2. Reading the timer count, converting it to ticks, and returning the result.

For brevity, we left out some of the finer details until the section on implementation.

API

In order to use the dynamic tick API the following defines must be enabled in os_cfg.h:

  • _CFG_TICK_EN
  • _CFG_DYN_TICK_EN

The API consists of three functions, two of which are user-defined:

  • void OSTimeDynTick (OS_TICK ticks)

    Function which is defined in the OS and called from the Tick ISR.

    Updates the OS with the number of ticks that have elapsed between starting the timer and servicing the interrupt.

  • OS_TICK OS_DynTickSet (OS_TICK ticks)

    User-Defined function, always called from a critical section. Allows the OS to set the number of ticks that it wants to wait before the next tick interrupt is fired. Returns the number of ticks that will elapse before the interrupt fires. Note that the former is what the OS wants to do, and the latter is what the BSP is capable of delivering. These values will often be the same but will differ if the requested delay exceeds the timer’s capacity.

  • OS_TICK OS_DynTickGet (void)

    User-Defined function, always called from a critical section. Allows the OS to read the number of ticks which have elapsed since it last set an interrupt.

Implementation

The following example runs on the Silabs SLSTK3701A using an EFM32GG11. However, it should serve as a template for new BSPs, regardless of the platform.

Board Startup

The hardware timer needs to be configured during board startup. The following should be called from main after the system clocks are configured and before calling any OS functions.

 1 #define  TIMER_COUNT_HZ            (3125000u)
 2 #define TIMER_TO_OSTICK(count)     (((CPU_INT64U)(count) * OS_CFG_TICK_RATE_HZ) / TIMER_COUNT_HZ)
 3 #define OSTICK_TO_TIMER(ostick)    (((CPU_INT64U)(ostick) * TIMER_COUNT_HZ) / OS_CFG_TICK_RATE_HZ)
 4
 5 #define TIMER_COUNT_MAX (DEF_INT_32U_MAX_VAL - (DEF_INT_32U_MAX_VAL % OSTICK_TO_TIMER(1u))) 6
 7 void BSP_OS_TickInit (void)
 8 { 
 9    CMU_ClockEnable(cmuClock_WTIMER0, true);
10
11    TIMER_Init_TypeDef timerInit = TIMER_INIT_DEFAULT;
12    timerInit.prescale = timerPrescale16; /* 3,125,000 Hz */
13    TIMER_Init(WTIMER0, &timerInit);
14    TIMER_TopSet(WTIMER0, TIMER_COUNT_MAX);
15
16    BSP_IntVectSet(INT_ID_WTIMER0,
17                   CPU_CFG_KA_IPL_BOUNDARY,
18                  (BSP_INT_TYPE)0,
19                   BSP_WTIMER0_ISRHandler);
20 }

The definitions in lines 1-5 are very useful for the remainder of our code. In fact, they can be re-used in new BSPs with minor modifications:

  • TIMER_COUNT_HZ – The hardware timer’s clock frequency. In our example, the timer count increments 3,125,000 times every second.

    This should be modified to match the timer configuration in your BSP.

  • TIMER_TO_OSTICK/OSTICK_TO_TIMER – Function-like macros which help us convert from timer counts to ticks, and vice versa.

    These should not require any modification for new BSPs, but can be optimized if there is no risk of integer overflows using 32-bit registers.

  • TIMER_COUNT_MAX – This represents the maximum value that our timer can count to, with one important modification: We want to keep the maximum count aligned on a tick boundary to avoid interrupting on an incomplete tick. To facilitate this, we’ve reduced the maximum count to make it an integer multiple of a single tick.

    This can be used as-is for a 32-bit timer. If a smaller timer is used, DEF_INT_32U_MAX_VAL should be replaced with the maximum count for that timer.

Lines 9-14 set the initial configuration of the hardware timer. The prescaler divides the timer’s input clock from 50 MHz to 3.125 MHz, giving us the value of TIMER_COUNT_HZ. Line 14 sets the autoreload value to TIMER_COUNT_MAX. This should always be done for the initial timer configuration since the OS starts with an empty tick list and we want the timer to start keeping time until a task is delayed. Note that when we set the dynamic tick interrupt on Line 16, we specify a priority of CPU_CFG_KA_IPL_BOUNDARY. For ports which support Non Kernel-Aware ISRs, It crucial that you assign the tick interrupt a kernel-aware priority.

Once the OS has started, it will switch to our first task. From this task, we must start the timer so that time keeping can begin until the first task delay occurs:

1 void BSP_OS_TickEnable (void) 
2 {
3     BSP_IntEnable(INT_ID_WTIMER0);
4     TIMER_IntEnable(WTIMER0, WTIMER_IF_OF);
5     TIMER_Enable(WTIMER0, 1u);
6 }

OS_DynTickSet()

1 static OS_TICK TickDelta = 0u;
2
3 OS_TICK OS_DynTickSet (OS_TICK ticks) 
4 {
5     CPU_INT32U  tmrcnt;
6
7
8     tmrcnt = OSTICK_TO_TIMER(ticks);
9
10    if ((tmrcnt >= TIMER_COUNT_MAX) ||
11    (tmrcnt ==                  0u)) {
12    tmrcnt = TIMER_COUNT_MAX;
13    }
14
15    TickDelta = TIMER_TO_OSTICK(tmrcnt); 16
17    TIMER_Enable (WTIMER0, 0u);
18    TIMER_IntClear (WTIMER0, WTIMER_IF_OF);
19
20    TIMER_TopSet (WTIMER0, tmrcnt);
21    TIMER_CounterSet(WTIMER0, 0u);
22    TIMER_Enable (WTIMER0, 1u);
23
24    return (TickDelta);
25 }

The first of the two implementations we will look at is OS_DynTickSet(). As we covered earlier, the OS requests that we delay a certain amount of time before triggering an interrupt. The value we receive is naturally in tick units, but our hardware timer runs on its own frequency so we’ll have to convert from ticks into timer counts (Line 8). There are two edge cases to check for with this function: either the OS requests a delay which is too large for our timer, or it requests an indefinite delay by calling the function with ticks == 0. In fact, these two cases are really one and the same: the OS requests a delay that is too large for the timer to fulfill. It therefore makes sense why we would respond to both conditions in the same way (Line 12). We will make our best effort to fulfill the request by telling our timer to count as many ticks as it can before firing an interrupt.

We should quickly pause and consider the role of TickDelta, which was declared on Line 1. The BSP uses it to track the number of ticks that the timer will count before firing the interrupt. This value should always be tmrcnt’s equivalent in tick units (Line 15).

The remainder of the function reconfigures our timer for the next delay, but there is one pitfall to beware. It is crucial that this function clear any pending interrupts before enabling the timer again (Line 18). Otherwise, it might service an old interrupt with the new TickDelta soon after it returns, causing the delays to expire almost immediately. The OS makes sure that any elapsed time is accounted for before calling OS_DynTickSet().

OS_DynTickGet()

OS_DynTickGet() is how the OS reads the number of elapsed ticks since it last requested a time delay. This function, while shorter than the previous one, nevertheless contains a subtle pitfall. Our first thought may be to simply read the timer value, convert it into the equivalent number of OS ticks, and return it:

/* Incorrect solution, but it was a good idea. */ 
1 OS_TICK OS_DynTickGet (void)
2 {
3     CPU_INT32U tmrcnt;
4 
5
6     tmrcnt = TIMER_CounterGet(WTIMER0);
7     tmrcnt = TIMER_TO_OSTICK(tmrcnt); /* What if the timer overflowed? */
8
9     return ((OS_TICK)tmrcnt);
10 }

With our particular hardware, an overflow means that the timer has wrapped around and started counting from 0 again. Once we see that the timer has overflowed, we can’t be sure if the timer count that was read is still valid. To rectify this, the following must be done exactly in order:

  • Read the timer count
  • Check the timer’s overflow flag
    • If the flag is set, the value we read may not be valid. We should simply return TickDelta which is the maximum number of ticks that were expected to elapse before servicing the interrupt.
    • Otherwise,the value we read is valid and can be returned

With this in mind, we can now provide the correct solution:

1 OS_TICK OS_DynTickGet (void) 
2 {
3     CPU_INT32U tmrcnt;
4
5
6     tmrcnt = TIMER_CounterGet(WTIMER0);
7
8     if (TIMER_IntGet(WTIMER0) & WTIMER_IF_OF) {
9         return tmrcnt;
10    }
11
12    tmrcnt = TIMER_TO_OSTICK(tmrcnt); 13
14    return ((OS_TICK)tmrcnt);
15 }

OSTimeDynTick()

1 static void BSP_WTIMER0_ISRHandler (void) 
2 {
3     CPU_SR_ALLOC();
4
5
6     CPU_CRITICAL_ENTER();
7     OSIntEnter();
8     CPU_CRITICAL_EXIT();
9
10    OSTimeDynTick(TickDelta);
11
12    OSIntExit();
13 }

Finally, we’ll examine the ISR handling code, which is the easiest to implement. We simply treat it like any other Kernel-Aware ISR by surrounding OSTimeDynTick() with the appropriate OSIntEnter()/OSIntExit() functions. This is when we report to the OS how many ticks have elapsed and cause it to schedule the next time delay. Notice that we don’t clear the timer interrupt here; this is because OSTimeDynTick() implicitly calls OS_DynTickSet() to clear the interrupt and re-program the timer.

However, it is also possible that this ISR is triggered “prematurely” because the timer did not have the capacity to perform the delay originally requested by the OS. This case is handled correctly by the OS and requires no special logic on the BSP side, but it may negatively impact the power consumption if it happens frequently. It is for this reason that we recommend using a 32-bit timer for Dynamic Tick Mode. What’s more, we can get longer delays by reducing the clock frequency of the hardware timer so long as the ratio of timer frequency to tick rate is >= 1, as was mentioned in the section on requirements.

Conclusion

In conclusion, we’ll list some important points we’ve covered for those wishing to implement a Dynamic Tick BSP.

  • Dynamic Tick Mode requires a programmable hardware timer with interrupt and overflow detection.
    • The timer frequency must meet or exceed the OS tick rate.
    • If possible, we recommend setting the timer frequency to an integer multiple of the tick rate to avoid loss of accuracy.
    • Always round the maximum timer count down to the nearest tick.
    • 32-bit timers are recommended. Smaller timers will work but may cause the OS to wake up prematurely during longer delays, thus increasing power consumption.
    • Decreasing the frequency of the timer will increase the number of ticks it can count, provided we don’t violate the other requirements.
    • The ideal setup is a 32-bit timer whose frequency is exactly equal to the tick rate.
  • Always ensure that the Tick ISR is kernel-aware.
  • Every value passed to and returned from the BSP must be in tick units.
  • The dynamic tick ISR handler does not need to clear the timer interrupt.
  • OS_DynTickSet() must clear the timer interrupt when re-programming the timer.
  • OS_DynTickGet() must check for the overflow condition before returning the timer count.

Tags: ,

Questions or Comments?

Have a question or a suggestion for a future article?
Don't hesitate to contact us and let us know!
All comments and ideas are welcome.