Skip to content

Commit d01fb82

Browse files
committed
v3.8.2 - support for tickless execution under FreeRTOS
1 parent c0b9c5a commit d01fb82

File tree

6 files changed

+191
-15
lines changed

6 files changed

+191
-15
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Task Scheduler
22
### Cooperative multitasking for Arduino, ESPx, STM32 and other microcontrollers
3-
#### Version 3.8.0: 2023-01-24 [Latest updates](https://github.com/arkhipenko/TaskScheduler/wiki/Latest-Updates)
3+
#### Version 3.8.2: 2023-09-27 [Latest updates](https://github.com/arkhipenko/TaskScheduler/wiki/Latest-Updates)
44

55
[![arduino-library-badge](https://www.ardu-badge.com/badge/TaskScheduler.svg?)](https://www.ardu-badge.com/TaskScheduler)[![xscode](https://img.shields.io/badge/Available%20on-xs%3Acode-blue?style=?style=plastic&logo=appveyor&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRF////////VXz1bAAAAAJ0Uk5T/wDltzBKAAAAlUlEQVR42uzXSwqAMAwE0Mn9L+3Ggtgkk35QwcnSJo9S+yGwM9DCooCbgn4YrJ4CIPUcQF7/XSBbx2TEz4sAZ2q1RAECBAiYBlCtvwN+KiYAlG7UDGj59MViT9hOwEqAhYCtAsUZvL6I6W8c2wcbd+LIWSCHSTeSAAECngN4xxIDSK9f4B9t377Wd7H5Nt7/Xz8eAgwAvesLRjYYPuUAAAAASUVORK5CYII=)](https://xscode.com/arkhipenko/TaskScheduler)
66

@@ -36,6 +36,7 @@ _“Everybody who learns concurrency and thinks they understand it, ends up find
3636
15. Ability to pause/resume and enable/disable scheduling
3737
16. Thread-safe scheduling while running under preemptive scheduler (i. e., FreeRTOS)
3838
17. Optional self-destruction of dynamically created tasks upon disable
39+
18. Support for "tickless" execution under FreeRTOS (continous sleep until next scheduled task invocation)
3940

4041
Scheduling overhead: between `15` and `18` microseconds per scheduling pass (Arduino UNO rev 3 @ `16MHz` clock, single scheduler w/o prioritization)
4142

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* TaskScheduler Test
3+
*
4+
* This test illustrates how to use TS's tickless support functionality
5+
* Tickless support enables more deterministic sleep by calculating time delay until next task invocation
6+
* during every pass. That delay could then be used to put microcontroller in sleep mode continously
7+
* instead of in small intervals
8+
*
9+
* Initially only tasks 1 and 2 are enabled
10+
* Task1 runs every 2 seconds 10 times and then stops
11+
* Task2 runs every 3 seconds indefinitely
12+
* Task1 enables Task3 at its first run
13+
* Task3 run every 5 seconds
14+
* Task1 disables Task3 on its last iteration and changed Task2 to run every 1/2 seconds
15+
* At the end Task2 is the only task running every 1/2 seconds
16+
*/
17+
18+
19+
#include <TaskScheduler.h>
20+
21+
// Callback methods prototypes
22+
void t1Callback();
23+
void t2Callback();
24+
void t3Callback();
25+
26+
//Tasks
27+
Task t4();
28+
Task t1(2000, 10, &t1Callback);
29+
Task t2(3000, TASK_FOREVER, &t2Callback);
30+
Task t3(5000, TASK_FOREVER, &t3Callback);
31+
32+
Scheduler runner;
33+
34+
35+
void t1Callback() {
36+
Serial.print("t1: ");
37+
Serial.println(millis());
38+
39+
if (t1.isFirstIteration()) {
40+
runner.addTask(t3);
41+
t3.enable();
42+
Serial.println("t1: enabled t3 and added to the chain");
43+
}
44+
45+
if (t1.isLastIteration()) {
46+
t3.disable();
47+
runner.deleteTask(t3);
48+
t2.setInterval(500);
49+
Serial.println("t1: disable t3 and delete it from the chain. t2 interval set to 500");
50+
}
51+
}
52+
53+
void t2Callback() {
54+
Serial.print("t2: ");
55+
Serial.println(millis());
56+
57+
// comment this line out if you want to test t2's 500 ms explicit delay
58+
// as-is this delay tests that task in catch up mode will prevent explicit tickless delay
59+
delay(501);
60+
61+
}
62+
63+
void t3Callback() {
64+
Serial.print("t3: ");
65+
Serial.println(millis());
66+
67+
}
68+
69+
void setup () {
70+
Serial.begin(115200);
71+
Serial.println("Scheduler TEST");
72+
73+
runner.init();
74+
Serial.println("Initialized scheduler");
75+
76+
runner.addTask(t1);
77+
Serial.println("added t1");
78+
79+
runner.addTask(t2);
80+
Serial.println("added t2");
81+
82+
delay(1000);
83+
84+
t1.enable();
85+
Serial.println("Enabled t1");
86+
t2.enable();
87+
Serial.println("Enabled t2");
88+
}
89+
90+
unsigned long nr = 0;
91+
void loop () {
92+
runner.execute(&nr);
93+
if ( nr ) {
94+
Serial.print("Next run in ");
95+
Serial.print(nr);
96+
Serial.println(" ms");
97+
delay(nr-1);
98+
}
99+
}

library.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"maintainer": true
1717
}
1818
],
19-
"version": "3.8.1",
19+
"version": "3.8.2",
2020
"frameworks": "arduino",
2121
"platforms": "*"
2222
}

library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name=TaskScheduler
2-
version=3.8.1
2+
version=3.8.2
33
author=Anatoli Arkhipenko <arkhipenko@hotmail.com>
44
maintainer=Anatoli Arkhipenko <arkhipenko@hotmail.com>
55
sentence=Cooperative multitasking for Arduino, ESPx, STM32 and other microcontrollers.

src/TaskScheduler.h

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ v3.8.0:
228228
v3.8.1:
229229
2023-05-11 - bug: conditional compile options missing from *.hpp files (Adafruit support)
230230
231+
v3.8.2:
232+
2023-09-27 - feature: _TASK_TICKLESS - support for tickless execution under FreeRTOS
233+
- feature: _TASK_DO_NOT_YIELD - ability to disable yield() in execute() method
234+
231235
*/
232236

233237

@@ -267,6 +271,9 @@ extern "C" {
267271
// #define _TASK_EXTERNAL_TIME // Custom millis() and micros() methods
268272
// #define _TASK_THREAD_SAFE // Enable additional checking for thread safety
269273
// #define _TASK_SELF_DESTRUCT // Enable tasks to "self-destruct" after disable
274+
// #define _TASK_TICKLESS // Enable support for tickless sleep under FreeRTOS
275+
// #define _TASK_DO_NOT_YIELD // Disable yield() method in execute()
276+
270277

271278
#ifdef _TASK_MICRO_RES
272279

@@ -308,7 +315,9 @@ extern "C" {
308315

309316
#ifndef _TASK_EXTERNAL_TIME
310317
static uint32_t _task_millis() {return millis();}
318+
#ifdef _TASK_MICRO_RES
311319
static uint32_t _task_micros() {return micros();}
320+
#endif // _TASK_MICRO_RES
312321
#endif // _TASK_EXTERNAL_TIME
313322

314323
/** Constructor, uses default values for the parameters
@@ -1323,17 +1332,19 @@ void Scheduler::setSleepMethod( SleepCallback aCallback ) {
13231332
* by running task more frequently
13241333
*/
13251334

1335+
#ifdef _TASK_TICKLESS
1336+
bool Scheduler::execute(unsigned long* aNextRun) {
1337+
#else
13261338
bool Scheduler::execute() {
1339+
#endif
13271340

13281341
bool idleRun = true;
13291342
unsigned long m, i; // millis, interval;
13301343

1331-
#ifdef _TASK_SLEEP_ON_IDLE_RUN
13321344
unsigned long tFinish;
1333-
unsigned long tStart = micros();
1334-
#endif // _TASK_SLEEP_ON_IDLE_RUN
1345+
unsigned long tStart;
13351346

1336-
#ifdef _TASK_TIMECRITICAL
1347+
#if defined(_TASK_TIMECRITICAL)
13371348
unsigned long tPassStart;
13381349
unsigned long tTaskStart, tTaskFinish;
13391350

@@ -1357,9 +1368,18 @@ bool Scheduler::execute() {
13571368
// after the higher priority scheduler has been invoked.
13581369
if ( !iEnabled ) return true; // consider this to be an idle run
13591370

1371+
// scheduling pass starts
1372+
tStart = micros();
1373+
1374+
#ifdef _TASK_TICKLESS
1375+
iNextRun = UINT32_MAX; // we do not know yet if we can tell when next run will be
1376+
iNextRunDetermined = _TASK_NEXTRUN_UNDEFINED;
1377+
#endif
1378+
1379+
13601380
while (!iPaused && iCurrent) {
13611381

1362-
#ifdef _TASK_TIMECRITICAL
1382+
#if defined(_TASK_TIMECRITICAL)
13631383
tPassStart = micros();
13641384
tTaskStart = tTaskFinish = 0;
13651385
#endif // _TASK_TIMECRITICAL
@@ -1412,6 +1432,13 @@ bool Scheduler::execute() {
14121432
// Otherwise, continue with execution as usual. Tasks waiting to StatusRequest need to be rescheduled according to
14131433
// how they were placed into waiting state (waitFor or waitForDelayed)
14141434
if ( iCurrent->iStatus.waiting ) {
1435+
1436+
#ifdef _TASK_TICKLESS
1437+
// if there is a task waiting on a status request we are obligated to run continously
1438+
// because event can trigger at any point at time.
1439+
iNextRunDetermined |= _TASK_NEXTRUN_IMMEDIATE; // immediate
1440+
#endif
1441+
14151442
#ifdef _TASK_TIMEOUT
14161443
StatusRequest *sr = iCurrent->iStatusRequest;
14171444
if ( sr->iTimeout && (m - sr->iStarttime > sr->iTimeout) ) {
@@ -1436,7 +1463,26 @@ bool Scheduler::execute() {
14361463
// this is the main scheduling decision point
14371464
// if the interval between current time and previous invokation time is less than the current delay - task should NOT be activated yet.
14381465
// this is millis-rollover-safe way of scheduling
1439-
if ( m - iCurrent->iPreviousMillis < iCurrent->iDelay ) break;
1466+
if ( m - iCurrent->iPreviousMillis < iCurrent->iDelay ) {
1467+
#ifdef _TASK_TICKLESS
1468+
// catch the reamining time until invocation as next time this should run
1469+
// this does not handle millis rollover well - so for the rollover situation (once every 47 days)
1470+
// we will require immediate execution
1471+
unsigned long nextrun = iCurrent->iDelay + iCurrent->iPreviousMillis;
1472+
// nextrun should be after current millis() (except rollover)
1473+
// nextrun should be sooner than previously determined
1474+
if ( nextrun > m && nextrun < iNextRun ) {
1475+
iNextRun = nextrun;
1476+
iNextRunDetermined |= _TASK_NEXTRUN_TIMED; // next run timed
1477+
}
1478+
#endif // _TASK_TICKLESS
1479+
break;
1480+
}
1481+
1482+
1483+
#ifdef _TASK_TICKLESS
1484+
iNextRunDetermined |= _TASK_NEXTRUN_IMMEDIATE; // next run timed
1485+
#endif
14401486

14411487
if ( iCurrent->iIterations > 0 ) iCurrent->iIterations--; // do not decrement (-1) being a signal of never-ending task
14421488
iCurrent->iRunCounter++;
@@ -1473,8 +1519,8 @@ bool Scheduler::execute() {
14731519
#endif // _TASK_TIMECRITICAL
14741520

14751521
iCurrent->iDelay = i;
1476-
1477-
#ifdef _TASK_TIMECRITICAL
1522+
1523+
#if defined(_TASK_TIMECRITICAL)
14781524
tTaskStart = micros();
14791525
#endif // _TASK_TIMECRITICAL
14801526

@@ -1487,7 +1533,7 @@ bool Scheduler::execute() {
14871533
}
14881534
#endif // _TASK_OO_CALLBACKS
14891535

1490-
#ifdef _TASK_TIMECRITICAL
1536+
#if defined(_TASK_TIMECRITICAL)
14911537
tTaskFinish = micros();
14921538
#endif // _TASK_TIMECRITICAL
14931539

@@ -1501,14 +1547,27 @@ bool Scheduler::execute() {
15011547
iCPUCycle += ( (micros() - tPassStart) - (tTaskFinish - tTaskStart) );
15021548
#endif // _TASK_TIMECRITICAL
15031549

1504-
#if defined (ARDUINO_ARCH_ESP8266) || defined (ARDUINO_ARCH_ESP32)
1550+
#if defined (ARDUINO_ARCH_ESP8266) || defined (ARDUINO_ARCH_ESP32)
1551+
#if !defined(_TASK_DO_NOT_YIELD)
15051552
yield();
1506-
#endif // ARDUINO_ARCH_ESPxx
1553+
#endif // _TASK_DO_NOT_YIELD
1554+
#endif // ARDUINO_ARCH_ESPxx
15071555
}
15081556

1557+
tFinish = micros(); // Scheduling pass end time in microseconds.
1558+
1559+
#ifdef _TASK_TICKLESS
1560+
if ( aNextRun ) {
1561+
*aNextRun = 0; // next iteration should be immediate by default
1562+
// if the pass was "idle" and there are tasks scheduled
1563+
if ( idleRun && iNextRunDetermined & _TASK_NEXTRUN_TIMED ) {
1564+
m = millis();
1565+
if ( iNextRun > m ) *aNextRun = ( iNextRun - m );
1566+
}
1567+
}
1568+
#endif
15091569

15101570
#ifdef _TASK_SLEEP_ON_IDLE_RUN
1511-
tFinish = micros(); // Scheduling pass end time in microseconds.
15121571

15131572
if (idleRun && iAllowSleep) {
15141573
if ( iSleepScheduler == this ) { // only one scheduler should make the MC go to sleep.

src/TaskSchedulerDeclarations.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
// #define _TASK_EXTERNAL_TIME // Custom millis() and micros() methods
3030
// #define _TASK_THREAD_SAFE // Enable additional checking for thread safety
3131
// #define _TASK_SELF_DESTRUCT // Enable tasks to "self-destruct" after disable
32+
// #define _TASK_TICKLESS // Enable support for tickless sleep on FreeRTOS
33+
// #define _TASK_DO_NOT_YIELD // Disable yield() method in execute()
3234

3335
class Scheduler;
3436

@@ -87,6 +89,12 @@ class Scheduler;
8789

8890
#endif // _TASK_MICRO_RES
8991

92+
#ifdef _TASK_TICKLESS
93+
#define _TASK_NEXTRUN_UNDEFINED 0b0
94+
#define _TASK_NEXTRUN_IMMEDIATE 0b1
95+
#define _TASK_NEXTRUN_TIMED 0x10
96+
#endif // _TASK_TICKLESS
97+
9098
#ifdef _TASK_STATUS_REQUEST
9199

92100
#define TASK_SR_OK 0
@@ -364,7 +372,11 @@ class Scheduler {
364372
INLINE void enableAll();
365373
INLINE void startNow(); // reset ALL active tasks to immediate execution NOW.
366374
#endif
375+
#ifdef _TASK_TICKLESS
376+
INLINE bool execute(unsigned long* aNextRun); // Returns true if none of the tasks' callback methods was invoked (true = idle run)
377+
#else
367378
INLINE bool execute(); // Returns true if none of the tasks' callback methods was invoked (true = idle run)
379+
#endif
368380
INLINE Task& currentTask() ; // DEPRICATED
369381
INLINE Task* getCurrentTask() ; // Returns pointer to the currently active task
370382
INLINE long timeUntilNextIteration(Task& aTask); // return number of ms until next iteration of a given Task
@@ -413,6 +425,11 @@ class Scheduler {
413425
unsigned long iCPUCycle;
414426
unsigned long iCPUIdle;
415427
#endif // _TASK_TIMECRITICAL
428+
429+
#ifdef _TASK_TICKLESS
430+
unsigned long iNextRun;
431+
unsigned int iNextRunDetermined;
432+
#endif
416433
};
417434

418435

0 commit comments

Comments
 (0)