Simple and Efficient Task Scheduler
In embedded applications, punctuality is very critical. You always need to put the tasks on schedules and run them on specific moments. This is why some applications use a real-time operating system such as FreeRTOS or at least a task scheduler. If you are looking for a small-footprint task scheduler, you hit the jackpot because Punctual covers all of your needs!
What is a task scheduler
Let’s consider we have 2 different tasks and they need to run on different periods such that 5s for the first and 2s for the second task. Imagine the following.
1 2 3 4 5 6 7 8 |
//... while(true) { task1(); delay(5); task2(); delay(2); } //... |
This doesn’t work of course. You can’t put them into the same infinite loop and use blocking delays. You have 2 options here: putting one/both of them into interrupt routine or building a non-blocking scheduler system. Check the following structure where the loop can continue running regardless of the tasks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//... while(true) { task1(); task2(); } //... void task1() { if(did5secPass()) { // Do the job. } } //... void task2() { if(did2secPass()) { // Do the job. } } //... |
But what if task1() takes 3s to complete? In this scenario task2() can never do its job on the scheduled time and it is obvious that we need a better solution. Putting task2() in an interrupt does the trick. When task1() spends more time then the running period of the other task, elevation in an ISR solves the problem and they don’t interfere each other in anyway. This concept proves the necessity of a scheduler.
What is Punctual
Punctual is a system that helps you implement both of the solutions provided above for multi-tasking problems. It has an ISR routine where you can attach your tasks and it also provides interface to communicate with these elevated tasks in a thread-safe way. In addition, Punctual has timeout functionality that come in handy for building non-blocking delays or sleeps.
Project implementation
Punctual is written in C language and aimed to be extremely simple but functional. There are only 2 source files that you need to care about; punctual.c and punctual.h.
Task scheduler
Punctual uses an array of the following struct to keep the scheduled tasks. As the first step, you need to call PunctualInit() while interrupts are disabled in order to initialize the task list to NULL.
1 2 3 4 5 6 |
// Type definition. typedef struct{ uint32_t (*handle)(void *); ///< Handle of task body. uint32_t result; ///< return value of the last call. void *param; ///< parameter to pass to the task. }tPunctualItem; |
When you create a task, PunctualCreate() puts the handle of the task function into first open slot of that array and then calls the handles of all created tasks in PunctualISR() one by one. When you PunctualDestroy() a task, its slot is freed and becomes available again.
To handle the communication between different tasks or between tasks and non-tasks, 2 functions are available. You can send any type of data to a task using PunctualSend() or you can read the latest return value of a task using PunctualReceive() from any thread.
There are only 3 settings to go.
1 2 3 4 5 |
// Settings. #define PUNCTUAL_MAX_TASKS (4) ///< Maximum numeber of scheduled tasks. #define PUNCTUAL_ISR_PERIOD_US (TICKER_PERIOD_US) ///< ISR calling period in [us]. #define PUNCTUAL_ISR_POSTSCALER (1) ///< Scheduled task ISR postscaler. #define PUNCTUAL_TO_TICKTIME_US (TICKER_TICKTIME_US) ///< SysTick time in [us] |
Punctual is flexible and you can change the maximum number of tasks by changing PUNCTUAL_MAX_TASKS. You should define the calling period of the ISR with PUNCTUAL_ISR_PERIOD_US. The other option is defining a post-scaler to reduce the calling frequency of PunctualISR() when you need to slow it down. Finally, PUNCTUAL_TO_TICKTIME_US is a definition to use in your time-aware application to get the interval between two calls to your Punctual task (multiply by PUNCTUAL_ISR_POSTSCALER value).
How to create scheduled tasks
The simplest use case is a simple task that blinks an eye every second. Imagine that you want to add a blinking LED to your multi-thousand-line software without any impact on the other stuff. As I mentioned before, you cant just put a blocking delay in the center of main() because that ruins everything without exception. You must either set a timeout and poll if the timer expired in a corner of main loop or put your code in one of the interrupts that is driven by a timer. I’m sure that both of the options are not practical, let’s say, if you want to blink 10 LEDs. A more proper way is creating a periodically-called function, so called “task” and leave the rest to your scheduler.
The prototype for Punctual tasks is uint32_t (*handle)(void *); and an example is given below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include "heartbeat.h" /** * @brief Blink task to be called every 1ms. * @param pause bool flag to pause for 1s. (To comply with Punctual.) * @return Value of the counter. (To comply with Punctual.) */ uint32_t heartBeat(void *pause) { static uint32_t counter = 0; // Extend blinking time. if ((bool) pause) { counter += 1000; } // Periodic led blinker. if (counter == 0) { mPinWrite(LED_Green, !mPinRead(LED_Green)); counter = 250; } counter--; return counter; } |
When we set this as a scheduled-task (it is called once a millisecond), it will blink the LED_Green at 2Hz and it will put extra 1s delay when pause = true is received. To define the task do the following.
1 2 3 4 5 6 7 8 |
// Init the scheduler. PunctualInit(); // Create HeartBeat task. tPunctualItem *pHeartBeat = PunctualCreate(heartBeat); // Enable global interrupts. mIntEnable(); |
And when you want to send pause signal to the ongoing task (preferable not in an interrupt) do it like this.
1 2 |
// Send signal to the HeartBeat task. PunctualSend(pHeartBeat, (void *)true); |
Here, the parameter type is (void *) by default but the task used is as (bool). When the default parameter is not used (void)param; must be declared in task body to avoid unused-parameter warning.
Last but not least, it is possible to read the last return value of the scheduled-task with the following call.
1 2 |
// Read the task's counter value from main. uint32_t counterValue = PunctualReceive(pHeartBeat); |
Now you know how to add more tasks to run with these easy steps. The only thing to avoid it keeping the tasks as small as possible because they run in interrupts. When possible, use PunctualTimeout to run your functions in main without elevation because this is the most proper way.
Timeouts
Punctual provides timeout methods to build up time delays in your project. A timeout is basically an object that you need to create with PunctualTimeoutSet() and poll with PunctualTimeoutCheck() for expiration, let’s say in the main().
1 2 3 4 5 |
typedef uint32_t tTime; ///< Keeps time. typedef struct{ tTime due; ///< Timestamp of the next timeout. tTime period; ///< The period for reloading the timeout. }tTimeout; |
All timeouts are periodic and they continues by default without any delay or overhead that means you can use a timeout to build periodic tasks. When a timeout expires, it sets up a new due time immediately by starting from the previous due. In other words, if you set a 1000ms timeout once, it will expire at 1000th, 2000th, 3000th ms and so on; which means checking it at 1100th millisecond doesn’t shift the next due. You can also change the period of a previously created timeout using PunctualTimeoutEdit().
If you want to implement your own timer, you must modify the following parameter only.
1 2 3 4 5 |
/** * @brief Macro for reading the time from SysTick. * @return tTime time in [ms]. */ #define PunctualGetTime() TickerRead_ms() |
An example will be given below.
How to create non-blocking delay
This simple example uses Punctual methods to create a 1Hz blinking LED.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
tTimeout timeOut; mPinOutput(LED_Green); // Set timeout. PunctualTimeoutSet(&timeOut, 1000); // Infinite loop. while(true){ //... // Other stuff. //... if (PunctualTimeoutCheck(&timeOut)) { // Toggle the LED. mPinWrite(LED_Green, !mPinRead(LED_Green)); } //... // Other stuff. //... } |
Note that PunctualTimeoutCheck() returns true only once in a period then updates the due with respect to the period. You can do other things in the same loop with the blinker since they don’t affect each other.
Project Files
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
/** * @file punctual.c * @author Atakan S. * @date 01/01/2018 * @version 1.0 * @brief Scheduled task and timeout framework. * * @copyright Copyright (c) 2018 Atakan SARIOGLU ~ www.atakansarioglu.com * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include <punctual.h> #include <string.h> // Variables. tPunctualItem puncList[PUNCTUAL_MAX_TASKS]; ///< Array of punctual task items. uint32_t puncPostscaler = 0; ///< Postscaler to slow down the ISR. uint8_t puncCnt = 0; ///< Dummy counter for thread safe read write operations. // Init. void PunctualInit(void) { // Clear the divider. puncPostscaler = 0; // Clear the list. memset((uint8_t *) puncList, 0, sizeof(puncList)); } // Punctual task scheduler. tPunctualItem *PunctualCreate(uint32_t (*handle)(void *)) { uint8_t i; uint8_t intEn; tPunctualItem *ptr = NULL; // Save state and disable the interrupts. intEn = mIsIntEnabled(); mIntDisable(); // Find a slot. for (i = 0; i < PUNCTUAL_MAX_TASKS; i++) { if (puncList[i].handle == NULL) { // Place the handle. puncList[i].handle = handle; ptr = &(puncList[i]); break; } } // Re-enable interrupts. if (intEn) { mIntEnable(); } return ptr; } // Destroys a task. void PunctualDestroy(tPunctualItem *ptr) { uint8_t intEn; // Consistency check. if ((ptr >= puncList) && (ptr <= &puncList[PUNCTUAL_MAX_TASKS])) { // Save state and disable the interrupts. intEn = mIsIntEnabled(); mIntDisable(); // Disable ISR operation. ptr->handle = NULL; // Re-enable interrupts. if (intEn) { mIntEnable(); } } } // The interrupt routine of the scheduler. void PunctualISR(void) { tPunctualItem *ptr; // Increase the divider. if ((++puncPostscaler) >= PUNCTUAL_ISR_POSTSCALER) { puncPostscaler = 0; // Increase the counter. puncCnt++; // Scan all slots. for (ptr = puncList; ptr < &puncList[PUNCTUAL_MAX_TASKS]; ptr++) { if (ptr->handle != NULL) { // Call it. ptr->result = (ptr->handle)(ptr->param); // Clear the param. ptr->param = NULL; } } } } // Send data to the task. void PunctualSend(tPunctualItem *ptr, void *data) { uint8_t intEn; // Consistency check. if ((ptr >= puncList) && (ptr <= &puncList[PUNCTUAL_MAX_TASKS])) { // Save state and disable the interrupts. intEn = mIsIntEnabled(); mIntDisable(); // Assign the data. ptr->param = data; // Re-enable interrupts. if (intEn) { mIntEnable(); } } } // Thread-safe read the last result. uint32_t PunctualReceive(tPunctualItem *ptr) { uint8_t cnt; uint32_t result; // Thread-safe operation. do{ cnt = puncCnt; result = ptr->result; }while(cnt != puncCnt); return result; } // Creates a timeout handle. void PunctualTimeoutSet(tTimeout *to, tTime ms){ to->period = ms; to->due = PunctualGetTime() + ms; } // Edits a timeout handle. void PunctualTimeoutEdit(tTimeout *to, tTime ms){ tTime timeNew = to->due - to->period; to->period = ms; to->due = timeNew + ms; } // Check timeout. uint8_t PunctualTimeoutCheck(tTimeout *to){ int32_t timeDiff; // Get signed time difference. timeDiff = to->due - PunctualGetTime(); // Expiration check. if(timeDiff <= 0){ // Increase due time. to->due = to->due + to->period; // Check the new due. timeDiff = to->due - PunctualGetTime(); if(timeDiff <= 0){ // If already expired, reset the timeout. to->due = PunctualGetTime() + to->period; } // Expired. return true; }else{ // Not expired. return false; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
/** * @file punctual.h * @author Atakan S. * @date 01/01/2018 * @version 1.1 * @brief Scheduled task and timeout framework. * * @copyright Copyright (c) 2018 Atakan SARIOGLU ~ www.atakansarioglu.com * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #ifndef _H_PUNCTUAL #define _H_PUNCTUAL #include <porty.h> // Settings. #ifndef PUNCTUAL_MAX_TASKS #define PUNCTUAL_MAX_TASKS (4) ///< Maximum numeber of scheduled tasks. #endif #ifndef PUNCTUAL_ISR_PERIOD_US #define PUNCTUAL_ISR_PERIOD_US (TICKER_PERIOD_US) ///< ISR calling period in [us]. #endif #ifndef PUNCTUAL_ISR_POSTSCALER #define PUNCTUAL_ISR_POSTSCALER (1) ///< Scheduled task ISR postscaler. #endif #ifndef PUNCTUAL_TO_TICKTIME_US #define PUNCTUAL_TO_TICKTIME_US (TICKER_TICKTIME_US) ///< SysTick time in [us] #endif // Don't touch. #define PUNCTUAL_PERIOD_US (PUNCTUAL_ISR_PERIOD_US*PUNCTUAL_ISR_POSTSCALER)///< Task calling period in [us]. #define PUNCTUAL_TO_MAXTIME_MS (0x10000000 / TO_TICKS_PER1MS) ///< This is the maximum timeout period in [ms]. #define PUNCTUAL_TO_MAXTIME_SEC (0x10000000 / TO_TICKS_PER1S) ///< This is the maximum timeout period in [s]. #ifdef _H_TICKER #undef mTickerHook_ms #define mTickerHook_ms() PunctualISR() ///< Setup hook to Ticker. #endif // Type definition. typedef struct{ uint32_t (*handle)(void *); ///< Handle of task body. uint32_t result; ///< return value of the last call. void *param; ///< parameter to passs to the task. }tPunctualItem; typedef uint32_t tTime; ///< Keeps time. typedef struct{ tTime due; ///< Timestamp of the next timeout. tTime period; ///< The period for reloading the timeout. }tTimeout; /** * @brief Init. */ void PunctualInit(void); /** * @brief Punctual task scheduler. * @param handle handle of the task body to be called. * @return pointer of the created task. */ tPunctualItem *PunctualCreate(uint32_t (*handle)(void *)); /** * @bried Destroys a task. * @param ptr task pointer. */ void PunctualDestroy(tPunctualItem *ptr); /** * @brief The interrupt routine of the scheduler. */ void PunctualISR(void); /** * @brief Send data to the task. * @param ptr task pointer. * @param data pointer to the data or data itself. */ void PunctualSend(tPunctualItem *ptr, void *data); /** * @brief Thread-safe read the last result. * @param ptr task pointer. * @return gives the return value of the scheduled task. */ uint32_t PunctualReceive(tPunctualItem *ptr); /** * @brief Sets a timeout object with the given period. * @param to the handle of the created timeout. * @param ms desired period in [ms]. */ void PunctualTimeoutSet(tTimeout *to, tTime ms); /** * @brief Edits a timeout object with the given period without restarting. * @param to the handle of the created timeout. * @param ms new period in [ms]. */ void PunctualTimeoutEdit(tTimeout *to, tTime ms); /** * @brief Gets the timeout state of the provided handle. * @param to the handle of the created timeout. * @return true if the timeout has been reached. */ uint8_t PunctualTimeoutCheck(tTimeout *to); /** * @brief Gets the timeout state of the provided handle. * @param to the handle of the created timeout. * @return true if the timeout has been reached. */ #define isPunctualTimeoutExpired(to) PunctualTimeoutCheck(to) /** * @brief Macro for reading the time from SysTick. * @return tTime time in [ms]. */ #define PunctualGetTime() TickerRead_ms() #endif |
You can find an example project below that uses Punctual and runs on STM32 VL Discovery board.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
/** * @file main.c * @author Atakan S. * @date 01/01/2018 * @version 1.2 * @brief Punctual project. * * @copyright Copyright (c) 2018 Atakan SARIOGLU ~ www.atakansarioglu.com * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "porty.h" // Variables. tPunctualItem *pHeartBeat; tTimeout timeOut; uint32_t counterValue; // The main routine. int main(void){ // Init cpu. Board_Init(); // Init Ticker. TickerInit(); // Init the scheduler. PunctualInit(); // Create HeartBeat task. pHeartBeat = PunctualCreate(heartBeat); // Set timeout. PunctualTimeoutSet(&timeOut, 2000); // Enable global interrupts. mIntEnable(); // The main loop. while (true) { if (PunctualTimeoutCheck(&timeOut)) { // Send signal to the HeartBeat task. PunctualSend(pHeartBeat, (void *) true); // Read the task's counter value from main. counterValue = PunctualReceive(pHeartBeat); (void) counterValue; // (but unused). } } } |
Porting
This project uses Porty Ticker by default but it can easily be modified to be used with any project. The only thing to do is calling the PunctualISR() in a (preferably 1ms) timer interrupt and defining PunctualGetTime() such that it return current time in milliseconds.
Download
A complete project that runs on STM32F100 microcontroller: Punctual-0.1.0. For compilation check here.
GitHub: https://github.com/atakansarioglu/punctual_demo_stm32f10x