Jump to content
 

Schedulers, Timers and RTCOUNTER


Orunmila

2,000 views

 Share

Quote

Time is what keeps everything from happening at once.

      -- RAY CUMMINGS, The Girl in the Golden Atom

Whenever I start a new project I always start off reaching for a simple while(1) "superloop" architecture https://en.wikibooks.org/wiki/Embedded_Systems/Super_Loop_Architecture . This works well for doing the basics but more often than not I quickly end up short and looking to employ a timer to get some kind of scheduling going.

MCC makes this pretty easy and convenient to set up. It contains a library called "Foundation Services" which has 2 different timer implementations, TIMEOUT and RTCOUNTER. These two library modules have pretty much the same interface, but they are implemented very differently under the hood. For my little "Operating System" I am going to prefer the RTCOUNTER version as keeping time accurately is more important to me than latency.

The TIMEOUT module is capable of providing low latency reaction times whenever a timer expires by adjusting the timer overflow point so that an interrupt will occur right when the next timer expires. This allows you to use the ISR to call the action you want to happen directly and immediately from the interrupt context.

Nice as that may be in some cases, it always increases the complexity, the code size and the overall cost of the system. In our case RTCOUNTER is more than good enough so we will stick with that.

RTCOUNTER Operation

First a little bit more about RTCOUNTER.

In short RTCOUNTER keeps track of a list of running timers. Whenever you call the "check" function it will compare the expiry time of the next timer and call the task function for that timer if it has expired.

It achieves this by using a single hardware timer which will operate in "Free Running" mode. This means the hardware timer will never be "re-loaded" by the code, it will simply overflow back to it's starting value naturally, and every time this happens the module will count that another overflow has happened in an overflow counter. The count of the timer is made up by a combination of the actual hardware timer and the overflow counter. By "hiding" or abstracting the real size of the hardware timer like this the module can easily be switched over to use any of the PIC timers, regardless if they count up or down or how many bits they implement in hardware. 

Mode 32-bit Timer value
General (32-x)-bit of g_rtcounterH x-bit Hardware Timer
Using TMR0 in 8-bit mode 24-bits of g_rtcounterH TMR0 (8-bit)
Using TMR1 16-bits of g_rtcounterH TMR1H (8-bit) TMR1L (8-bit)

RTCOUNTER is compatible with all the PIC timers, and you can switch it to a different timer later without modifying your application code which is nice. Since all that happens when the timer overflows is updating the counter the Timer ISR is as short and simple as possible.

// We only increment the overflow counter and clear the flag on every interrupt
void rtcount_isr(void) {
    g_rtcounterH++;
    PIR4bits.TMR1IF = 0;
}

When we run the "check" function it will construct the 32-bit time value by combining the hardware timer and the overflow counter (g_rtcounterH). It will then compare this value to the expiry time of the next timer in the list to expire. By keeping the list of timers sorted by expiry time it saves time during the checking (which happens often) by doing the sorting work during creation of the timer (which happens infrequently). 

How to use it

Using it is failry straight-forward. 

  1. Create a callback function which returns the "re-schedule" time for the timer.
  2. Allocate memory for your timer/task and tie it to your callback function
  3. Create the timer (which starts it) specifying how long before it will first expire
  4. Regularly call the check function to check if the next timer has expired, and call it's callback if it has.

In C the whole program may look something like this example:

#include "mcc_generated_files/mcc.h"

int32_t ledFlasher(void* p);
rtcountStruct_t  myTimer = {ledFlasher};

void main(void)
{
    SYSTEM_Initialize();
        
    INTERRUPT_GlobalInterruptEnable();
    INTERRUPT_PeripheralInterruptEnable();

    rtcount_create(&myTimer, 1000); // Create a new timer using the memory at &myTimer
    
    // This is my main Scheduler or OS loop, all tasks are executed from here from now on
    while (1)
    {
        // Check if the next timer has expired, call it's callback from here if it did
        rtcount_callNextCallback();
    }
}


int32_t ledFlasher(void* p)
{
    LATAbits.RA0 ^= 1;  // Toggle our pin
    
    return 1000;  // When we are done we want to restart this timer 1000 ticks later, return 0 to stop
}

 

Example with Multiple Timers/Tasks

Ok, admittedly blinking an LED with a timer is not rocket science and not really impressive, so let's step it up and show how we can use this concept to write an application which is more event-driven than imperative

NOTE: If you have not seen it yet I recommend reading Martin Fowler's article on Event Sourcing and how this design pattern reduces the probability of errors in your system on his website here.

By splitting our program into tasks (or modules) which each perform a specific action and works independently of other tasks, we can generate code modules which are completely independent and re-usable quite easily. Independent and re-usable (or mobile as Uncle Bob says) does not only mean that the code is maintainable, it also means that we can test and debug each task by itself, and if we do this well it will make the code much less fragile. Code is "fragile" when you are fixing something in one place and something seemingly unrelated breaks elsewhere ... that will be much less likely to happen.

For my example I am going to construct some typical tasks which need to be done in an embedded system. To accomplish this we will

  1. Create a task function for each of these by creating a timer for it.
  2. Control/set the amount of CPU time afforded to each task by controlling how often the timer times out
  3. Communicate between tasks only through a small number of shared variables (this is best done using Message Queue's - we will post about those in a later blog some time)

Let's go ahead and construct our system. Here is the big picture view

Scheduler.png

This system has 6 tasks being managed by the Scheduler/OS for us.

  1. Sampling the ADC to check the battery level. This has to happen every 5 seconds
  2. Process keys, we are looking at a button which needs to be de-bounced (100 ms)
  3. Process serial port for any incoming messages. The port is on interrupt, baud is 9600. Our buffer is 16 bytes so we want to check it every 10ms to ensure we do not loose data.
  4. Update system LCD. We only update the LCD when the data has changed, we want to check for a change every 100ms
  5. Update LED's. We want this to happen every 500ms
  6. Drive Outputs. Based on our secret sauce we will decide when to toggle some pins, we do this every 1s

These tasks will work together, or co-operate, by keeping to the promise never to run for a long time (let's agree 10ms is a long time, tasks taking longer than that needs to be broken into smaller steps). This arrangement is called Co-operative Multitasking . This is a well-known mechanism of multi-tasking on a microcontroller, and has been implemented in systems like "Windows 3.1" and "Windows 95" as well as "Classic Mac-OS" in the past.

By using the Scheduler and event driven paradigm here we can implement and test each of these subsystems independently. Even when we have it all put together we can easily replace one of these subsystems with a "Test" version of it and use that to generate test conditions for us to ensure everything will work correctly under typical operation conditions. We can "disable" any part of the system by simply commenting out the "create" function for that timer and it will not run. We can also adjust how often things happen or adjust priorities by modifying the task time values.

As before we first allocate some memory to store all of our tasks. We will initialize each task with a pointer to the callback function used to perform this task as before. The main program= ends up looking something like this.

void main(void)
{
    SYSTEM_Initialize();
        
    INTERRUPT_GlobalInterruptEnable();
    INTERRUPT_PeripheralInterruptEnable();

    rtcount_create(&adcTaskTimer, 5000);
    rtcount_create(&keyTaskTimer, 100);
    rtcount_create(&serialTaskTimer, 10);
    rtcount_create(&lcdTaskTimer, 100);
    rtcount_create(&ledTaskTimer, 500);
    rtcount_create(&outputTaskTimer, 1000);
    
    // This is my main Scheduler or OS loop, all tasks are executed from events
    while (1)
    {
        // Check if the next timer has expired, call it's callback from here if it did
        rtcount_callNextCallback();
    }
}

 

As always the full project incuding the task functions and the timer variable declarations can be downloaded. The skeleton of this program which does initialize the other peripherals, but runs the timers completely compiles to only 703 words of code or 8.6% on this device, and it runs all 6 program tasks using a single hardware timer.

 

  • Like 1
 Share

1 Comment


Recommended Comments

  • Member

I have to add here that the single thing which trips most people up on RTCOUNTER is the timer period. The timer MUST be free-running, which mean that you can NOT use MCC to set a timer period by using a reload or PR value, the timer HAS to be set to maximum period and the only thing you can modify is the prescaler for the timer to adjust the period.

I know this is limiting, but it allows us to have a pure monotonic upwards counting number which we can concatenate with our sw variable to construct the timer. Remember that we want as few interrupts as possible, so running the timer for MAX period is exactly what we want to do here! This library does not have an event when the timer gets an interrupt or overflow (that is what the TIMEOUT driver does if you want that, but there is quite a price for this). 

When you call the check function it will simply compare the current timer register with the absolute time variable for the next timer to expire, it is a quick compare, so pretty cheap to do this.

  • Like 1
Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...