CCS News

Tech Note: Event-driven Programming Using the Timeouts Library

Tuesday 09 June, 2020

In a multitasking environment using state machines, it is quite common to poll for events that may have happened, and then go to a state based on which event that happened. For example, a push-button and LCD GUI polling the button to see if it is pressed, or polling to see if a time has expired to change or refresh what is on the LCD screen. If a polling scheme is used, eventually one may find that as the program gets bigger, that it spends most of its time polling for events that rarely happen. By switching to event-driven programming, one can create a callback that is instead called when an event happens that needs to be processed. The CCS C Compiler provides a portable callback library, timeouts.c. The following is a simple LCD GUI example using the the timeouts library:

#include <timeouts.c>

// debounce button, on debounce send event
void ButtonDebounceOnTimeout(void *pArg)
{
if (BUTTON_PRESSED())
{
if (++pArg == 4)
{
// debounced, send event
TimeoutsRemove(LCDGUIOnTimeout, NULL);
TimeoutsImmediate(LCDGUIOnTimeout, 1);
}
}
else
pArg = 0;

TimeoutsAdd(ButtonDebounceOnTimeout, pArg, 16);
}

// handle button press or periodically refresh the screen
void LCDGUIOnTimeout(void *pArg)
{
static int screenIndex;

if (pArg)
{
if (++screenIndex > 4)
screenIndex = 0;
}

switch(screenIndex)
{
case 0: ShowLCDGUIScreen0(); break;
case 1: ShowLCDGUIScreen1(); break;
case 2: ShowLCDGUIScreen2(); break;
case 3: ShowLCDGUIScreen3(); break;
}

TimeoutsAdd(LCDGUIOnTimeout, 0, 333); //refresh
}

// init button debouncer and screen handler.
void LCDGUIInit(void)
{
TimeoutsImmediate(ButtonDebounceOnTimeout, 0);
TimeoutsImmediate(LCDGUIOnTimeout, 0);
}


LCDGUIInit() calls TimeoutsImmediate(), which places functions onto the timeouts callback stack to be called. The first parameter of TimeoutsImmediate() is the function that is to be called, and must match the prototype void function_name(void *). The library will create a pointer to that function and push it onto the stack. The second parameter of TimeoutsImmedate() is the parameter that is passed to the function when the timeouts library is called. Since this parameter is of type void*, it could either be pointer to state variables, or it could be just used as a int. In the examples shown here it is simply used as an int, to hold an int variable from one call to the next. That means what LCDGUIInit() is doing is pushing ButtonDebounceOnTimeout and LCDGUIOnTimeout to be called, to be passed a value of 0.

ButtonDebounceOnTimeout() will debounce the button. BUTTON_PRESSED() would be a hardware dependent macro that returns true if the button is currently held down. In this example, if BUTTON_PRESSED() returns true a counter is incremented and if the counter is incremented a fourth time it will then use the timeouts library to call LCDGUIOnTimeout() with a parameter of 1. At the end of ButtonDebounceOnTimeout(), TimeoutsAdd() is then used to push ButtonDebounceOnTimeout() to be called again in 16 milliseconds. That means ButtonDebounceOnTimeout() is called every 16ms, and it takes 64ms of debouncing before a button pressed event is sent. Since the pArg is passed back to the repeat call of ButtonDebounceOnTimeout(), it can be used as a state variable of this function to measure how many times the button was held. If you look ahead to the LCDGUIOnTimeout(), you will see that it pushes itself back onto the timeouts call stack every 333ms to refresh the screen; for this reason ButtonDebounceOnTimeout() is doing a TimeoutsRemove() to remove all other calls to LCDGuiOnTimeout() so the only even that is going to be called is a button press event.

LCDGuiOnTimeout() will either handle a button event or redraw the screen depending on the pArg passed to it. In this example a pArg of 1 is a new button, in which case it goes to the next screen. If pArg is a 0 then it's a screen refresh, just redraw the current screen. At the end of LCDGUIOnTimeout() a TimeoutsAdd() is used to push another call to LCDGuiOnTimeout in 333ms, meaning the screen is redrawn 3 times a second. What isn't shown in the above example is the main loop, which must call TimeoutTask(). TimeoutTask() looks at the top of the call stack, and if it's expiration has expired it then calls that function. Functions that add to the call stack always keep the call stack sorted so the top element, the one being checked, is always the next to expire. That means TimeoutTask() only needs to poll one task, regardless of how many elements are pushed.

This library can also be used in some other situations:

* Interrupts can push a function to be called on the main loop, to prevent execution of lengthy or low priority code to be running in an interrupt.

* If completely using an ISR and timeouts library based design, the processor could be put to sleep until the next event. The timeouts library has a function, TimeoutsNext(), which tells you how long until the next event. The processor could be put to sleep for that duration, as long as it's configured so interrupts will wake it.

* Diagnostic and metrics could be added to TimeoutTask() to measure how long each event takes, to find or debug certain events that take too long to execute.


Like us on Facebook. Follow us on Twitter.

About CCS:

CCS is a leading worldwide supplier of embedded software development tools that enable companies to develop premium products based on Microchip PIC® MCU and dsPIC® DSC devices. Complete proven tool chains from CCS include a code optimizing C compiler, application specific hardware platforms and software development kits. CCS' products accelerate development of energy saving industrial automation, wireless and wired communication, automotive, medical device and consumer product applications. Established in 1992, CCS is a Microchip Premier 3rd Party Partner. For more information, please visit http://www.ccsinfo.com.

PIC® MCU, MPLAB® IDE, MPLAB® ICD2, MPLAB® ICD3 and dsPIC® are registered trademarks of Microchip Technology Inc. in the U.S. and other countries.