Explorar el Código

v3.8.2 - support for tickless execution under FreeRTOS

Anatoli Arkhipenko hace 2 años
padre
commit
d01fb826cf

+ 2 - 1
README.md

@@ -1,6 +1,6 @@
 # Task Scheduler
 ### Cooperative multitasking for Arduino, ESPx, STM32 and other microcontrollers
-#### Version 3.8.0: 2023-01-24 [Latest updates](https://github.com/arkhipenko/TaskScheduler/wiki/Latest-Updates)
+#### Version 3.8.2: 2023-09-27 [Latest updates](https://github.com/arkhipenko/TaskScheduler/wiki/Latest-Updates)
 
 [![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)
 
@@ -36,6 +36,7 @@ _“Everybody who learns concurrency and thinks they understand it, ends up find
 15. Ability to pause/resume and enable/disable scheduling
 16. Thread-safe scheduling while running under preemptive scheduler (i. e., FreeRTOS)
 17. Optional self-destruction of dynamically created tasks upon disable
+18. Support for "tickless" execution under FreeRTOS (continous sleep until next scheduled task invocation)
 
 Scheduling overhead: between `15` and `18` microseconds per scheduling pass (Arduino UNO rev 3 @ `16MHz` clock, single scheduler w/o prioritization)
 

+ 99 - 0
examples/Scheduler_example28_Tickless/Scheduler_example28_Tickless.ino

@@ -0,0 +1,99 @@
+/** 
+ *  TaskScheduler Test
+ * 
+ *  This test illustrates how to use TS's tickless support functionality
+ *  Tickless support enables more deterministic sleep by calculating time delay until next task invocation
+ *  during every pass. That delay could then be used to put microcontroller in sleep mode continously
+ *  instead of in small intervals
+ *
+ *  Initially only tasks 1 and 2 are enabled
+ *  Task1 runs every 2 seconds 10 times and then stops
+ *  Task2 runs every 3 seconds indefinitely
+ *  Task1 enables Task3 at its first run
+ *  Task3 run every 5 seconds
+ *  Task1 disables Task3 on its last iteration and changed Task2 to run every 1/2 seconds
+ *  At the end Task2 is the only task running every 1/2 seconds
+ */
+ 
+ 
+#include <TaskScheduler.h>
+
+// Callback methods prototypes
+void t1Callback();
+void t2Callback();
+void t3Callback();
+
+//Tasks
+Task t4();
+Task t1(2000, 10, &t1Callback);
+Task t2(3000, TASK_FOREVER, &t2Callback);
+Task t3(5000, TASK_FOREVER, &t3Callback);
+
+Scheduler runner;
+
+
+void t1Callback() {
+    Serial.print("t1: ");
+    Serial.println(millis());
+    
+    if (t1.isFirstIteration()) {
+      runner.addTask(t3);
+      t3.enable();
+      Serial.println("t1: enabled t3 and added to the chain");
+    }
+    
+    if (t1.isLastIteration()) {
+      t3.disable();
+      runner.deleteTask(t3);
+      t2.setInterval(500);
+      Serial.println("t1: disable t3 and delete it from the chain. t2 interval set to 500");
+    }
+}
+
+void t2Callback() {
+    Serial.print("t2: ");
+    Serial.println(millis());
+	
+	// comment this line out if you want to test t2's 500 ms explicit delay
+	// as-is this delay tests that task in catch up mode will prevent explicit tickless delay
+    delay(501);
+  
+}
+
+void t3Callback() {
+    Serial.print("t3: ");
+    Serial.println(millis());
+  
+}
+
+void setup () {
+  Serial.begin(115200);
+  Serial.println("Scheduler TEST");
+  
+  runner.init();
+  Serial.println("Initialized scheduler");
+  
+  runner.addTask(t1);
+  Serial.println("added t1");
+  
+  runner.addTask(t2);
+  Serial.println("added t2");
+
+  delay(1000);
+  
+  t1.enable();
+  Serial.println("Enabled t1");
+  t2.enable();
+  Serial.println("Enabled t2");
+}
+
+unsigned long nr = 0;
+void loop () {
+  runner.execute(&nr);
+  if ( nr ) {
+    Serial.print("Next run in ");
+    Serial.print(nr);
+    Serial.println(" ms");
+    delay(nr-1);
+  }
+}

+ 1 - 1
library.json

@@ -16,7 +16,7 @@
       "maintainer": true
     }
   ],
-  "version": "3.8.1",
+  "version": "3.8.2",
   "frameworks": "arduino",
   "platforms": "*"
 }

+ 1 - 1
library.properties

@@ -1,5 +1,5 @@
 name=TaskScheduler
-version=3.8.1
+version=3.8.2
 author=Anatoli Arkhipenko <arkhipenko@hotmail.com>
 maintainer=Anatoli Arkhipenko <arkhipenko@hotmail.com>
 sentence=Cooperative multitasking for Arduino, ESPx, STM32 and other microcontrollers.

+ 71 - 12
src/TaskScheduler.h

@@ -228,6 +228,10 @@ v3.8.0:
 v3.8.1:
    2023-05-11 - bug: conditional compile options missing from *.hpp files (Adafruit support)
 
+v3.8.2:
+   2023-09-27 - feature: _TASK_TICKLESS - support for tickless execution under FreeRTOS
+              - feature: _TASK_DO_NOT_YIELD - ability to disable yield() in execute() method
+
 */
 
 
@@ -267,6 +271,9 @@ extern "C" {
 // #define _TASK_EXTERNAL_TIME      // Custom millis() and micros() methods
 // #define _TASK_THREAD_SAFE        // Enable additional checking for thread safety
 // #define _TASK_SELF_DESTRUCT      // Enable tasks to "self-destruct" after disable
+// #define _TASK_TICKLESS           // Enable support for tickless sleep under FreeRTOS
+// #define _TASK_DO_NOT_YIELD       // Disable yield() method in execute()
+
 
  #ifdef _TASK_MICRO_RES
 
@@ -308,7 +315,9 @@ extern "C" {
 
 #ifndef _TASK_EXTERNAL_TIME
 static uint32_t _task_millis() {return millis();}
+#ifdef _TASK_MICRO_RES
 static uint32_t _task_micros() {return micros();}
+#endif  //  _TASK_MICRO_RES
 #endif  //  _TASK_EXTERNAL_TIME
 
 /** Constructor, uses default values for the parameters
@@ -1323,17 +1332,19 @@ void  Scheduler::setSleepMethod( SleepCallback aCallback ) {
  * by running task more frequently
  */
 
+#ifdef _TASK_TICKLESS
+bool Scheduler::execute(unsigned long* aNextRun) {
+#else
 bool Scheduler::execute() {
+#endif
   
     bool     idleRun = true;
     unsigned long m, i;  // millis, interval;
 
-#ifdef _TASK_SLEEP_ON_IDLE_RUN
     unsigned long tFinish;
-    unsigned long tStart = micros();
-#endif  // _TASK_SLEEP_ON_IDLE_RUN
+    unsigned long tStart;
 
-#ifdef _TASK_TIMECRITICAL
+#if defined(_TASK_TIMECRITICAL)
     unsigned long tPassStart;
     unsigned long tTaskStart, tTaskFinish;
 
@@ -1357,9 +1368,18 @@ bool Scheduler::execute() {
     //  after the higher priority scheduler has been invoked.
     if ( !iEnabled ) return true; //  consider this to be an idle run
 
+    // scheduling pass starts
+    tStart = micros();
+
+#ifdef _TASK_TICKLESS
+    iNextRun = UINT32_MAX;  // we do not know yet if we can tell when next run will be
+    iNextRunDetermined = _TASK_NEXTRUN_UNDEFINED;
+#endif
+
+
     while (!iPaused && iCurrent) {
 
-#ifdef _TASK_TIMECRITICAL
+#if defined(_TASK_TIMECRITICAL)
         tPassStart = micros();
         tTaskStart = tTaskFinish = 0; 
 #endif  // _TASK_TIMECRITICAL
@@ -1412,6 +1432,13 @@ bool Scheduler::execute() {
     // Otherwise, continue with execution as usual.  Tasks waiting to StatusRequest need to be rescheduled according to
     // how they were placed into waiting state (waitFor or waitForDelayed)
                 if ( iCurrent->iStatus.waiting ) {
+
+#ifdef _TASK_TICKLESS
+    // if there is a task waiting on a status request we are obligated to run continously
+    // because event can trigger at any point at time. 
+    iNextRunDetermined |= _TASK_NEXTRUN_IMMEDIATE; // immediate
+#endif
+
 #ifdef _TASK_TIMEOUT
                     StatusRequest *sr = iCurrent->iStatusRequest;
                     if ( sr->iTimeout && (m - sr->iStarttime > sr->iTimeout) ) {
@@ -1436,7 +1463,26 @@ bool Scheduler::execute() {
                 // this is the main scheduling decision point
                 // if the interval between current time and previous invokation time is less than the current delay - task should NOT be activated yet.
                 // this is millis-rollover-safe way of scheduling
-                if ( m - iCurrent->iPreviousMillis < iCurrent->iDelay ) break;
+                if ( m - iCurrent->iPreviousMillis < iCurrent->iDelay ) {
+#ifdef _TASK_TICKLESS
+                // catch the reamining time until invocation as next time this should run
+                // this does not handle millis rollover well - so for the rollover situation (once every 47 days)
+                // we will require immediate execution
+                    unsigned long nextrun = iCurrent->iDelay + iCurrent->iPreviousMillis;
+                    // nextrun should be after current millis() (except rollover)
+                    // nextrun should be sooner than previously determined
+                    if ( nextrun > m && nextrun < iNextRun ) { 
+                        iNextRun = nextrun;
+                        iNextRunDetermined |= _TASK_NEXTRUN_TIMED; // next run timed
+                    }
+#endif  //  _TASK_TICKLESS                   
+                    break;
+                }
+
+
+#ifdef _TASK_TICKLESS
+                iNextRunDetermined |= _TASK_NEXTRUN_IMMEDIATE; // next run timed
+#endif  
 
                 if ( iCurrent->iIterations > 0 ) iCurrent->iIterations--;  // do not decrement (-1) being a signal of never-ending task
                 iCurrent->iRunCounter++;
@@ -1473,8 +1519,8 @@ bool Scheduler::execute() {
 #endif  // _TASK_TIMECRITICAL
 
                 iCurrent->iDelay = i;
-                
-#ifdef _TASK_TIMECRITICAL
+
+#if defined(_TASK_TIMECRITICAL)
                 tTaskStart = micros();
 #endif  // _TASK_TIMECRITICAL
 
@@ -1487,7 +1533,7 @@ bool Scheduler::execute() {
                 }
 #endif // _TASK_OO_CALLBACKS
 
-#ifdef _TASK_TIMECRITICAL
+#if defined(_TASK_TIMECRITICAL)
                 tTaskFinish = micros();
 #endif  // _TASK_TIMECRITICAL
 
@@ -1501,14 +1547,27 @@ bool Scheduler::execute() {
         iCPUCycle += ( (micros() - tPassStart) - (tTaskFinish - tTaskStart) );
 #endif  // _TASK_TIMECRITICAL
         
-#if defined (ARDUINO_ARCH_ESP8266) || defined (ARDUINO_ARCH_ESP32)
+#if defined (ARDUINO_ARCH_ESP8266) || defined (ARDUINO_ARCH_ESP32) 
+#if !defined(_TASK_DO_NOT_YIELD)
         yield();
-#endif  // ARDUINO_ARCH_ESPxx
+#endif  //  _TASK_DO_NOT_YIELD
+#endif  //  ARDUINO_ARCH_ESPxx
     }
 
+    tFinish = micros(); // Scheduling pass end time in microseconds.
+
+#ifdef _TASK_TICKLESS
+    if ( aNextRun ) {
+        *aNextRun = 0;  // next iteration should be immediate by default
+        // if the pass was "idle" and there are tasks scheduled
+        if ( idleRun && iNextRunDetermined & _TASK_NEXTRUN_TIMED ) {
+            m = millis();
+            if ( iNextRun > m ) *aNextRun = ( iNextRun - m );
+        }
+    }
+#endif 
 
 #ifdef _TASK_SLEEP_ON_IDLE_RUN
-    tFinish = micros(); // Scheduling pass end time in microseconds.
 
     if (idleRun && iAllowSleep) {
         if ( iSleepScheduler == this ) { // only one scheduler should make the MC go to sleep. 

+ 17 - 0
src/TaskSchedulerDeclarations.h

@@ -29,6 +29,8 @@
 // #define _TASK_EXTERNAL_TIME      // Custom millis() and micros() methods
 // #define _TASK_THREAD_SAFE        // Enable additional checking for thread safety
 // #define _TASK_SELF_DESTRUCT      // Enable tasks to "self-destruct" after disable
+// #define _TASK_TICKLESS           // Enable support for tickless sleep on FreeRTOS
+// #define _TASK_DO_NOT_YIELD       // Disable yield() method in execute()
 
 class Scheduler;
 
@@ -87,6 +89,12 @@ class Scheduler;
 
 #endif  // _TASK_MICRO_RES
 
+#ifdef _TASK_TICKLESS
+#define _TASK_NEXTRUN_UNDEFINED 0b0
+#define _TASK_NEXTRUN_IMMEDIATE 0b1
+#define _TASK_NEXTRUN_TIMED     0x10
+#endif  //  _TASK_TICKLESS
+
 #ifdef _TASK_STATUS_REQUEST
 
 #define TASK_SR_OK          0
@@ -364,7 +372,11 @@ class Scheduler {
     INLINE void enableAll();
     INLINE void startNow();                             // reset ALL active tasks to immediate execution NOW.
 #endif
+#ifdef _TASK_TICKLESS
+    INLINE bool execute(unsigned long* aNextRun);                              // Returns true if none of the tasks' callback methods was invoked (true = idle run)
+#else
     INLINE bool execute();                              // Returns true if none of the tasks' callback methods was invoked (true = idle run)
+#endif
     INLINE Task& currentTask() ;                        // DEPRICATED
     INLINE Task* getCurrentTask() ;                     // Returns pointer to the currently active task
     INLINE long timeUntilNextIteration(Task& aTask);    // return number of ms until next iteration of a given Task
@@ -413,6 +425,11 @@ class Scheduler {
     unsigned long iCPUCycle;
     unsigned long iCPUIdle;
 #endif  // _TASK_TIMECRITICAL
+
+#ifdef _TASK_TICKLESS
+    unsigned long iNextRun;
+    unsigned int  iNextRunDetermined;
+#endif
 };