Dipl. Phys. Helmut Weber In Umarbeitung ! (23.11.2019)






Achtung:

Alle Beiträge sind Copyright (C) 2018-2019 Dipl. Phys. Helmut Weber

Hier werden einige Sourcen exemplarisch vorgestellt.

ESP8266:
Der ESP8266 ist ein sehr preiswerter (~2.50€) Microcontroller mit 32 Bit und verglichen mit dem
Arduino sehr schnell. Zudem ist er nach Erscheinen des Nachfolgers ESP32 sehr günstig geworden.
Auch wenn er wegen seiner WiFi-Fähigkeiten meist für IoT eingesetzt wird, ist er auch ideal geeignet,
um sehr schneller Meß- und Steuerungsaufgaben zu übernehmen.
Es gibt eine IDF (vom Hersteller) mit und eine ohne RTOS.

Für die Arduino-IDE steht nur die ohne RTOS zur Verfügung.
Wesentlich aber ist, dass das RTOS sehr viel Platz verbraucht und obendrein auch sehr langsam ist.

Hier eine Übersicht der Programme:


I   Sehr schnelles Multitasking - Tutorial in 7 Lektionen

Lektion1    Lektion2     Lektion3    Lektion4    Lektion5    Lektion6    Lektion7

Ein Kursus in 7 Lektionen, sehr kompakt und leicht verständlich. Zunächst ohne jede Bibliothek !
Es werden bis zu 1000.000 Taskswitches/s (Lektion4) erreicht, während gleichzeitig 100.000 Timer-Interrupts/s feuern.

 





















I    Sehr schnelles Multitasking - Tutorial in 7 Lektionen

Lektion 1
In Lektion 1 lernen wir das Taskswitching mit cont.h kennen.

Im ESP8266 core (nonrtos) für Arduino gibt es eine sehr selten beachtete Möglichkeit für Taskswitching: cont.h
cont.h enthält 3 dafür wichtige Befehle:
    1) cont_init(&context)
        Ein context enthält:
          Platz für einen eigenen Stack (per default 4096 bytes)
          Platz für den Inhalt aller Register inkl. PC, SP etc
        cont_init() reserviert diesen Platz

   2) cont_run(&context, Taskfunction)
       Beim ersten Aufruf setzt cont_run den PC (program counter) auf den Wert von
       Taskfunction, speichert den Zustand der machine auf dem aktellen Stack und schaltet
       auf den context um.
       Dann wird Taskfunction ausgeführt.

  3) cont_yield(&context)
      Der aktuelle Zustand der machine wird in context gespeichert und die Kontrolle an
      den gespeicherten contex zurückgegeben. Es entspricht in der Wirkung von "return"

  4) cont_run(&context, Taskfunction)
      Nach dem ersten Aufruf wird Taskfunction nicht vom Anfang an aufgerufen, sondern
      nach dem Befehl des letzten cont_yield.
      Taskfunction wird also effektiv fortgesetzt.
      Es können viele cont_yield in einem Task vorkommen. Damit wird Taskfunction in
      einzelne Funktionseinheiten zerlegt.
      Insbesondere erlaubt dies auch  while(1) - Schleifen, wie sie auch in RTOS-Tasks üblich
      sind.

cont.h.jpg


Das folgende Programm möge das verdeutlichen:



// ESP8266_CoopOS_Stack_Lesson1

// Esp8266 Wemos D1 R1 and Esp8266-12F Wemos D1 mini

/*
ESP8266_CoopOS_Stack_Lesson1

(C) 2019 Helmut Weber Dph.HelmutWeber.123@gmail.com

Arduino IDE, Esp8266 Wemos D1 R1

The simplest form of Multitasking: (tick time is 1s)

All tasks are called every second from loop

>cont_init( &context ):
reserve a context with a stackspace of 4096 bytes (defined in cont.h)

>cont_run( &context, functionpointer):
switch all registers and PC to the named context - then run the function
1) first time: start the function
2) next times: got to the point of the last cont_yield in the function

>cont_yield( &context):
save context with PC and registers, switch context to caller(where cont_run occured) and return
*/



// This is the base of task switching from espressif:
extern "C" {
#include <cont.h>
}



cont_t A, B, C;

void TaskA() {
static unsigned long cnt;
Serial.println("Start TaskA");
while(1) {
cnt++;
Serial.print("TaskA-1 ");
Serial.println(cnt);
cont_yield(&A);
Serial.print("TaskA-2 ");
Serial.println(cnt);
cont_yield(&A); // save PC and all registers
// get old PC, stack (==loop)
}
}


void TaskB() {
static unsigned long cnt;
Serial.println("Start TaskB");
while(1) {
cnt++;
Serial.print("TaskB-1 ");
Serial.println(cnt);
cont_yield(&B);
Serial.print("TaskB-2 ");
Serial.println(cnt);
cont_yield(&B);
}
}


void TaskC() { // toggle internal LED
static unsigned long cnt;
Serial.println("Start TaskC");
while(1) {
digitalWrite(2,!digitalRead(2));
cont_yield(&C);
}
}



void setup() {
Serial.begin(500000);
pinMode(2,OUTPUT);

cont_init( & A); // prepare an own stack for TaskA
cont_init( & B);
cont_init( & C);
Serial.print("Setup End");
}

void loop() {
Serial.println(" loop");

cont_run( & A, TaskA); // start TaskA from beginning or last cont_yield
cont_run( & B, TaskB); // start TaskB
cont_run( & C, TaskC); // start TaskC


delay(1000);
}


Auf den ersten Blick sieht es nicht sehr spektakulär aus. TaskA, -B, -C werden nacheinander jede
Sekunde aufgerufen.
Auf den 2. Blick erkennt man jedoch die while(1)-Schleifen in den Tasks, die so ohne Taskswitching
nicht funktionieren könnten!
Die interne LED blinkt im Sekundentakt, aber es laufen weitere Tasks.







Lektion 2

In Lektion 1 hatten wir in loop ein delay(1000), also von einer Sekunde.
In Lektion 2 setzten wir die "Ticktime" auf 10 ms.
Jeder Task für sich entscheidet durch das Zählen von Ticks, wie oft er reagiert.
Die Schleife zum Aufrufen der Tasks wir nach setup() verlagert.
loop muss zwar noch vorhanden sein (wird vom Linker gefordert), aber es wird nie aufgerufen.


// ESP8266_CoopOS_Stack_Lesson2

/*
ESP8266_CoopOS_Stack_Lesson2

(C) 2019 Helmut Weber Dph.HelmutWeber.123@gmail.com

The simplest Form of Multitasking: (tick time is 1ms)

All tasks are called every 10 ms from loop.
Every task organizes ist own timing counting calls

cont_init( &context ):
reserve a context with a stackspace of 4096 bytes (defined in cont.h)

cont_run( &context, functionpointer):
switch all registers and PC to the named context - then run the function
1) first time: start the function
2) next times: got to the point of the last cont_yield in the function

cont_yield( &context):
save context with PC and registers, switch context to caller(where cont_run occured) and return
*/



// This is the base of task switching from espressif:
extern "C" {
#include <cont.h>
}



cont_t A, B, C;

void TaskA() {
static unsigned long cnt;
Serial.println("Start TaskA");
while(1) {
cnt++;
if (cnt==100) { // tick time = 10ms, do this once a second
cnt=0;
Serial.print("TaskA-1 "); Serial.println(millis());
cont_yield(&A);
Serial.print("TaskA-2 "); Serial.println(millis()); // one ticktime later
}
cont_yield(&A); // save PC and all registers
// get old PC, stack (==loop)
}

}


void TaskB() {
static unsigned long cnt;
Serial.println("Start TaskB");
while(1) {

cnt++;
if (cnt==50) { // twice a second
cnt=0;
Serial.print(" TaskB-1 "); Serial.println(millis());
cont_yield(&B);
Serial.print(" TaskB-2 "); Serial.println(millis()); // one ticktime later
}
cont_yield(&B);

}
}


void TaskC() { // toggle internal LED
static unsigned long cnt;
Serial.println("Start TaskC");

while(1) {
cnt++;
if (cnt==5) {
cnt=0;
digitalWrite(2,!digitalRead(2));
}
cont_yield(&C);

}
}



void setup() {
Serial.begin(500000);
pinMode(2,OUTPUT);

cont_init( & A); // prepare an own stack for TaskA
cont_init( & B);
cont_init( & C);
Serial.print("Setup End");

// we can do it in loop OR here

while(1) {
cont_run( & A, TaskA); // start TaskA from beginning or last cont_yield
cont_run( & B, TaskB); // start TaskB
cont_run( & C, TaskC); // start TaskC
delay(10);
}
}


void loop() {

// cont_run( & A, TaskA); // start TaskA from beginning or last cont_yield
// cont_run( & B, TaskB); // start TaskB
// cont_run( & C, TaskC); // start TaskC
//
//
// delay(10);
}

Hier scheinen schon 3 Tasks völlig unabhängig von einander zu arbeiten !
Aber das delay(10) im loop sind noch Verschwendung. Besser wäre es, wenn die einzelnen Tasks nicht Ticks zählen,
sondern ihr Timing durch Vergleich mit  micros()  unabhängig von einer Tickzeit selbst bestimmen würden.
Dies geschieht in ...







Lektion 3
Jeder Task bezieht seine Aktionen auf die aktelle Zeit in Mikrosekunden.
Damit wird er unabhängig von einer Tickzeit. Und das delay(10) im loop kann entfallen!
Und damit wird es richtig schnell:
Tasks A-2 gibt die Zahl der Tasksswitches pro Sekunde aus:
       TaskA-2: Counts/s 1163507
Mehr als eine Million Taskswitches pro Sekunde!
Die Ausgabe zeigt, dass die Tasks auf die Millisekunde genau zu den festgelegten Zeiten aufgerufen werden:


Ausgabe in ms:

TaskA-1 384109

TaskA-2: Counts/s 1163515

TaskA-1 385109

TaskA-2: Counts/s 1163555

TaskB-1 385109

TaskB-2 385109

TaskA-1 386109

TaskA-2: Counts/s 1163507

TaskA-1 387109

TaskA-2: Counts/s 1163559

TaskB-1 387109

TaskB-2 387109

TaskA-1 388109

TaskA-2: Counts/s 1163507

TaskA-1 389109

TaskA-2: Counts/s 1163555

TaskB-1 389109

TaskB-2 389109

TaskA-1 390109

TaskA-2: Counts/s 1163515

TaskA-1 391109

TaskA-2: Counts/s 1163555

TaskB-1 391109

TaskB-2 391109

TaskA-1 392109

TaskA-2: Counts/s 1163511


Das Programm:


// ESP8266_CoopOS_Stack_Lesson2

/*
ESP8266_CoopOS_Stack_Lesson3

(C) 2019 Helmut Weber Dph.HelmutWeber.123@gmail.com

The simplest Form of Multitasking: (tick time is 1ms)

All tasks are called as fasr as possible and do their timings comparing micros
Every task organizes ist own timing counting calls

cont_init( &context ):
reserve a context with a stackspace of 4096 bytes (defined in cont.h)

cont_run( &context, functionpointer):
switch all registers and PC to the named context - then run the function
1) first time: start the function
2) next times: got to the point of the last cont_yield in the function

cont_yield( &context):
save context with PC and registers, switch context to caller(where cont_run occured) and return
*/



// This is the base of task switching from espressif:
extern "C" {
#include <cont.h>
}



cont_t A, B, C;

unsigned long cnt;



void TaskA() {
static unsigned long lastCalled=micros();
Serial.println("Start TaskA");
while(1) {
cnt++;
if ((micros()-lastCalled)>=1000000) {
lastCalled=micros();
Serial.print("TaskA-1 "); Serial.println(millis());
cont_yield(&A);
Serial.print("TaskA-2: Counts/s "); Serial.println(cnt); // one ticktime later
cnt=0;
}
ESP.wdtFeed(); // if we avoid loop we have do feed the watchdog
cont_yield(&A); // save PC and all registers
// get old PC, stack (==loop)
}

}


void TaskB() {
static unsigned long lastCalled=micros();
Serial.println("Start TaskB");
while(1) {
if ((micros()-lastCalled)>=2000000) {
lastCalled=micros();
Serial.print(" TaskB-1 "); Serial.println(millis());
cont_yield(&B);
Serial.print(" TaskB-2 "); Serial.println(millis()); // one ticktime later
}
cont_yield(&B);

}
}


void TaskC() { // toggle internal LED
static unsigned lastCalled=micros();
Serial.println("Start TaskC");

while(1) {
if ((micros()-lastCalled)>=50000) { // toggle every 50 ms
lastCalled=micros();
digitalWrite(2,!digitalRead(2));
}
cont_yield(&C);

}
}



void setup() {
Serial.begin(500000);
pinMode(2,OUTPUT);

cont_init( & A); // prepare an own stack for TaskA
cont_init( & B);
cont_init( & C);
Serial.print("Setup End");

// we can do it in loop OR here

while(1) {
cont_run( & A, TaskA); // start TaskA from beginning or last cont_yield
cont_run( & B, TaskB); // start TaskB
cont_run( & C, TaskC); // start TaskC
cnt+=3; // 3 taskcalls per loop
//delay(10);
}
}


void loop() {

// cont_run( & A, TaskA); // start TaskA from beginning or last cont_yield
// cont_run( & B, TaskB); // start TaskB
// cont_run( & C, TaskC); // start TaskC
//
//
// delay(10);
}






Lektion 4
Cooperatives Multitasking bedeutet immer, das die längste Verweildauer in einem Task die Reaktionszeit des ganzen
Systems bestimmt!
Auch, wenn alle Tasks nach kurzer Verweildauer    cont_yield()    aufrufen, ist eine auf die Mikrosekunde genaue
Ausführungszeit damit nicht zu gewährleisten.
Das ist nut mit einem Timerinterrupt zu erreichen. Deshalb wird in dieser Lektion ein Timerinterrupt installiert, der
alle 10 µs !!! feuert. Das sind  100.000 Interrupts pro Sekunde.
Kann das System damit überhaupt noch funktionieren?

Weiterhin wollen wir vom Timerinterrupt den Start eines CooOS-Tasks initiieren.
Dazu wird TaskD alle 10.000 Timerinterrupts gestartet und meldet sich mit
--- USR_FLAG und gibt die Anzahl der gezählten Timerinterrupts wieder.




TaskA-1 277117

TaskA-2: Counts/s 1022784

---USR_FLAG: 200001

TaskB-1 277119

TaskB-2 IRQs/s100001

---USR_FLAG: 10001

---USR_FLAG: 20001

---USR_FLAG: 30001

---USR_FLAG: 40001

---USR_FLAG: 50001

---USR_FLAG: 60001

---USR_FLAG: 70001

---USR_FLAG: 80001

---USR_FLAG: 90001

TaskA-1 278117

TaskA-2: Counts/s 1033874

---USR_FLAG: 100001

---USR_FLAG: 110001

---USR_FLAG: 120001

---USR_FLAG: 130001

---USR_FLAG: 140001

---USR_FLAG: 150001

---USR_FLAG: 160001

---USR_FLAG: 170001

---USR_FLAG: 180001

---USR_FLAG: 190001

TaskA-1 279117
ick010µs)
TaskA-2: Counts/s 1022784

---USR_FLAG: 200001

TaskB-1 279119

TaskB-2 IRQs/s100001



Es funktioniert. Es werden jeweils 10.000 Timerinterrupts (angezeigt mit 1 Timertick = 10 µs Verzögerung) gezählt.

Hier das Programm:


// ESP8266_CoopOS_Stack_Lesson4

// Esp8266 Wemos D1 R1 and Esp8266-12F Wemos D1 mini
/*
ESP8266_CoopOS_Stack_Lesson4

(C) 2019 Helmut Weber Dph.HelmutWeber.123@gmail.com

The simplest Form of Multitasking: (no ticktime)

All tasks are called as fast as possible and do their timings comparing micros
Every task organizes ist own timing counting calls
Here we add a TimerInterrupt every 10 µs !
From TimerInterupt we are able to start tasks.

======================================================================
More than a 1000.000 tasks are called per second with high precision !
(less than 1µs for an average TaskSwitch and -Execution)
100.000 Timer Interrupts/s (every 10 µs)
======================================================================






// This is the base of task switching from espressif:
extern "C" {
#include <cont.h>
}



cont_t A, B, C, D;

unsigned long cnt;
unsigned long cntTimerIrqs;
volatile int usrFlag=0;



void TaskA() {
static unsigned long lastCalled=micros();
Serial.println("Start TaskA");
while(1) {
cnt++;
if ((micros()-lastCalled)>=1000000) {
lastCalled=micros();
Serial.print("TaskA-1 "); Serial.println(millis());
cont_yield(&A);
Serial.print("TaskA-2: Counts/s "); Serial.println(cnt); // one ticktime later
cnt=0;
}
ESP.wdtFeed(); // if we avoid loop we have do feed the watchdog
cont_yield(&A); // save PC and all registers
// get old PC, stack (==loop)
}

}



void TaskB() {
static unsigned long lastCalled=micros();
unsigned long cnt;
Serial.println("Start TaskB");
while(1) {
if ((micros()-lastCalled)>=2000000) { // every 2 seconds
lastCalled=micros();
cnt=cntTimerIrqs;
cntTimerIrqs=0;
Serial.print("TaskB-1 "); Serial.println(millis());
cont_yield(&B);
Serial.print("TaskB-2 IRQs/s"); Serial.println(cnt/2); // for 1 second

}
cont_yield(&B);

}
}




void TaskC() { // toggle internal LED
static unsigned lastCalled=micros();
Serial.println("Start TaskC");

while(1) {
if ((micros()-lastCalled)>=50000) { // toggle every 50 ms
lastCalled=micros();
digitalWrite(2,!digitalRead(2));
}
cont_yield(&C);

}
}




void TaskD() { // start a task from Timer Interrupt
Serial.println("Start TaskD");

while(1) {
while (usrFlag==0) {
cont_yield(&D);
}

usrFlag=0;
Serial.print("---USR_FLAG: "); Serial.println(cntTimerIrqs);
cont_yield(&D);

}
}


void ICACHE_RAM_ATTR onTimerISR() {
cntTimerIrqs++;
if ((cntTimerIrqs%10000)==0) {
usrFlag=1;
}
}


void ICACHE_FLASH_ATTR TimerInit(int mys) {
// Now we start a timer:
timer1_isr_init();
timer1_attachInterrupt(onTimerISR);
//while ((micros() % (1000000)) != 0) {}
// 10 is possible, but with some jitter
//timer1_write(1600 - 1); //20 us, timer runs with 80MHz, even if CPU runs with 160MHz!
//timer1_write(mys*80);
//timer1_enable(TIM_DIV1, TIM_EDGE, TIM_LOOP);

timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);
timer1_write(mys*5); // 0.2 µs per tick

}


void ICACHE_RAM_ATTR setup() {
Serial.begin(500000);
pinMode(2,OUTPUT);

cont_init( & A); // prepare an own stack for TaskA
cont_init( & B);
cont_init( & C);
cont_init( & D);

Serial.println("Setup End");


TimerInit(10); // 100000 timer ticks per second

// we can do it in loop OR here

while(1) {
cont_run( & D, TaskD); // start TaskD
cont_run( & A, TaskA); // start TaskA from beginning or last cont_yield
cont_run( & B, TaskB); // start TaskB
cont_run( & C, TaskC); // start TaskC

cnt+=4;
//delay(10);
}
}


void loop() {

}

Dies dürfte wohl jedes System auf vergleichbaren Microcontrollern hinsichtlich Präzision und Geschwindigkeit schlagen!

Und dies alles ohne jede Bibliothek in einem Programm von etwa 200 Zeilen






Lektion 5

Bei Echtzeitaufgaben muss oft gewählt werden zwischen Komfort für den Programmierer und Geschwindigkeit !

Die Sourcen aus Lektion 4 sind an Geschwindigkeit nicht zu überbieten!

Für ein allgemein einsetzbares System verzichten wir auf ein wenig Geschwindigkei und führen einen Scheduler ein,
der seinerseit die Tasks aufruft.

Es soll auch ein "Init" für die Tasks eingeführt werden, um folgende Zielsetzung zu erreichen:

---code
void setup() {

InitTask( TaskA, ...);
InitTask( TaskB, ...);
InitTask( TaskC, ...);

while(1) {
Scheduler();
}
}


Vorteil: Modularität.

Es können die Tasks jederzeit aus verschiedenen Programmen zu einem neuen Programm zusammengestellt
werden. Es bleibt dabei: die längste Ausführungszeit in einem Task bestimmt die Reaktionsfähigkeit des
gesamten Systems. Solche Messungen lassen sich leicht in den Scheduler einbauen.

Der Scheduler entscheidet, ob ein Tasks aufgerufen wird.
Der Scheduler startet bei jedem Aufruf den Tasks der
    1) Im Zustand READY ist
    2) Der von allen READY-Tasks die höchste Priorität hat

Unabhängig von ihrer Priorität werden Tasks, die auf einen Interrupt warten, zuerst ausgeführt.
Die Priorität wird dann schlicht durch die Stellung in der Tasklist bestimmt.
Die ersten werden zuerst bedient.

Außerdem sollen noch Externe Interrupts ermöglicht werden. Das sind Interrupts, die ausgelöst werden, wenn
sich der Zustand an einem Pin ändert.
Vorteil: Tasks, die auf einen Interrupt warten, werden - und das geht schnell - vom Scheduler ignoriert.
Wenn dann der ausgewählte Interrupt kommt, wird in der Interrupt-Routine der Status des dazugehörigen Tasks
von WAITIRQ auf IRQ gesetzt und beim nächsten Aufruf des Schedulers dieser Task ausgeführt.


Für die Tasks gibt es neue Befehle:

TaskYield()
Die Kontrolle wird sofort an den Scheduler zurückgegeben

TaskDelay(Microseconds)
Der Task wird für "Mikroseconds" schlafen gelegt.

TaskWaitIrq()
Dieser Task stoppt hier und wartet auf ein TaskSetIrq() von einer Interrupt-Routine oder einem anderen Task.

Um das Programm etwas übersichtlicher zu gestalten, werden neue Dateien (in dr Arduino-IDE als neue Tabs)
erstellt:

Defines.h
Enthält die neuen Befehle

TaskSwitch.h
Enthält die Initialisierung von Tasks und den Scheduler und die Anzahl maximal verfügbarer Tasks
TaskInit(..) legt beim ersten Aufruf auch immer automatisch einen Tasks   idle()  an.
idle()  wird vom Scheduler immer dann aufgerufen, wenn kein Task READY ist.
Hier wird einfach ein Zähler inkrementiert. CntIdle ist auch ein Maß für die Auslastung des Systems.
In  idle()  können auch Aufgaben im Hintergrund erledigt werden. Diese haben immer niedrigste Priorität
und werden so im Hintergrund abgearbeitet, ohne das System nennenswert zu belasten.

cont.h und cont_util.h
Sind Kopieen der originalen Source (s.o.) sie werden hierher kopiert, um Stacksize verändern zu können.
Espressif hat Stacksize fest auf 4096 Bytes festgelegt. Das ist dem WiFi geschuldet.
Da wir hier kein WiFi benutzen, ist bei meinen Experimenten ein Stacksize von 1024 ausreichend!


Diese Maßnahmen ermöglichen ein sehr kompaktes setup:

void  ICACHE_RAM_ATTR setup() {
Serial.begin(500000);
pinMode(2,OUTPUT); // internal LED


// we replace cont_init with InitTask:
//InitTask(char * nam, void( * f)(), int prio, State stat, unsigned long delay)
TaskID_A = InitTask((char *)"A", TaskA, 100, READY, 113 );
TaskID_B = InitTask((char *)"B", TaskB, 100, READY, 223 );
TaskID_C = InitTask((char *)"C", TaskC, 100, READY, 337 );
TaskID_D = InitTask((char *)"D", TaskD, 100, READY, 443 );


Serial.println("Setup End");


TimerInit(10); // 100000 timer ticks per second



// Set up external interrupts
noInterrupts();
pinMode(interruptPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt\
(interruptPin), onExternalISR, FALLING); // Init: External Interrupt, CHANGE will double interrupt frequency
interrupts();



// we can do it in loop OR here

while(1) {
Schedule();
}


}

Jetzt ist es sehr simpel, für dieses System Tasks zu schreiben und sie in eine Programm zu integrieren:

Eine Zeile   IntitTask()  genügt!

Welche zeitlichen Auswirkungen hat jetzt der Scheduler auf das System.
Die Zahl der Taskaufrufe geht von 1000000 auf ca. 200000 pro Sekunde zurück.
Im Mittel also statt 1 µs pro Taskswitch nun etwa 5µs.

Das ist trotzdem in vielen Fällen zu verschmerzen. Zum Vergleich: RTOS laufen meist mit einer Ticktime
von 1000 µs !

Das Programm von Lektion5  läuft sehr präzise.
TasksD wird vom TimerIRQ 10 x pro Sekunde mit einem Delay von 1-5 µs aufgerufen.
TaskA und TaskB steuern ihr Timing mit TaskDelay - auch noch nach einer Stunde auf die Millisekunde genau.
Es werden 100000 Interrupts pro Sekunde gezählt. Scheduler Counts sind hier eta 190000 pro Sekunde.

Der Link dazu befindet sich hier:                                                        Download Source Lesson5
Bitte in .../Arduino (dem Sektch-Folder) entpacken.

TaskA-1 3585000 ms

TaskA-2: Sched-Counts/s 189546

-From Timer: 357990000 Delay (µs): 2 Micros now: 3585038663

-From Timer: 358000000 Delay (µs): 3 Micros now: 3585138664

-From Timer: 358010000 Delay (µs): 3 Micros now: 3585238664

-From Timer: 358020000 Delay (µs): 2 Micros now: 3585338663

-From Timer: 358030000 Delay (µs): 4 Micros now: 3585438665

-From Timer: 358040000 Delay (µs): 2 Micros now: 3585538663

-From Timer: 358050000 Delay (µs): 3 Micros now: 3585638665

-From Timer: 358060000 Delay (µs): 5 Micros now: 3585738666

-From Timer: 358070000 Delay (µs): 4 Micros now: 3585838666

-From Timer: 358080000 Delay (µs): 4 Micros now: 3585938666

TaskA-1 3586000 ms

TaskA-2: Sched-Counts/s 189802

TaskB-1 3586000 ms

TaskB-2 IRQs/s 100000

External IRQs/s 0

-From Timer: 358090000 Delay (µs): 4 Micros now: 3586038666

-From Timer: 358100000 Delay (µs): 4 Micros now: 3586138666

-From Timer: 358110000 Delay (µs): 5 Micros now: 3586238666

-From Timer: 358120000 Delay (µs): 3 Micros now: 3586338664

-From Timer: 358130000 Delay (µs): 3 Micros now: 3586438665

-From Timer: 358140000 Delay (µs): 2 Micros now: 3586538663

-From Timer: 358150000 Delay (µs): 4 Micros now: 3586638665

-From Timer: 358160000 Delay (µs): 2 Micros now: 3586738663

-From Timer: 358170000 Delay (µs): 2 Micros now: 3586838663

-From Timer: 358180000 Delay (µs): 4 Micros now: 3586938665

TaskA-1 3587000 ms

TaskA-2: Sched-Counts/s 189547

-From Timer: 358190000 Delay (µs): 2 Micros now: 3587038663

-From Timer: 358200000 Delay (µs): 3 Micros now: 3587138665

-From Timer: 358210000 Delay (µs): 1 Micros now: 3587238663

-From Timer: 358220000 Delay (µs): 3 Micros now: 3587338664

-From Timer: 358230000 Delay (µs): 2 Micros now: 3587438664

-From Timer: 358240000 Delay (µs): 2 Micros now: 3587538664

-From Timer: 358250000 Delay (µs): 3 Micros now: 3587638665

-From Timer: 358260000 Delay (µs): 2 Micros now: 3587738663

-From Timer: 358270000 Delay (µs): 4 Micros now: 3587838665

-From Timer: 358280000 Delay (µs): 5 Micros now: 3587938666

TaskA-1 3588000 ms

TaskA-2: Sched-Counts/s 189834

TaskB-1 3588000 ms

TaskB-2 IRQs/s 100000



Ein Problem bleibt noch:
Serielle Ausgaben können viel Zeit benötigen - gemessen an den Zeiten, mit denen wir es hir zu tun haben.
Diesem Problem widmet sich ...





Lektion 6

Der Link dazu befindet sich hier:                                                        Download Source Lesson6
Bitte in .../Arduino (dem Sketch-Folder) entpacken.

Es wird ein Klasse eingführt, um serielle Ausgaben in einen Ringpuffer zu schreiben. Das geht erheblich schneller,
als die direkte Ausgabe an die serielle Schnittstelle.
Hierzu gibt es 2 neue Tabs:
MySer.h  und  MySerial.h

MySer.h
enthält den Task für die serielle Ausgabe aus dem Ringpuffer,
Dieser Task sollte als letzte Zeile bein InitTask(...) mit der niedrigsten Taskpriorität eingefügt werden.
Hier ist jetzt das komplette setup():

void ICACHE_RAM_ATTR setup() {
MySerial.begin(500000);

Serial.println("--- Setup Start ---");
pinMode(2,OUTPUT);

// Initialize the Tasks:
//InitTask(char * nam, void( * f)(), int prio, State stat, unsigned long delay)
TaskID_A = InitTask((char *)"A", TaskA, 100, READY, 113 );
TaskID_B = InitTask((char *)"B", TaskB, 100, READY, 223 );
TaskID_C = InitTask((char *)"C", TaskC, 100, READY, 337 );
TaskID_D = InitTask((char *)"D", TaskD, 100, READY, 443 );
InitTask((char *)"MyS",MySer_Task, 90, READY, 400); // <<<<<<<<<<< MySer-Task


OutLn("--- Setup Interrupts ---");
TimerInit(10); // 100000 timer ticks per second
ExternalInit(irqPin);
OutLn("--- Setup Interrupts End---");
OutLn("--- Setup End ---");
OutLn("--- Run the Scheduler ---");

// run the OS:
while(1) {
Schedule(); // we can do it in loop OR here
}
}


void loop() {
OutLn("Upps - this should not happen !");
}
MySerial.h enthält die Klasse mySerial mit der Instanz MySerial.
Es enthält auch die Größenangabe für den Ringpuffer.
Wie im  setup()  zu sehen wird   MySerial   wie  Serial benutzt.


Serial.print  wird ersetzt durch MySerial.print,  Serial.println   durch    MySerial.println.
In Defines.h wurde deklariert:
    #define Out(x)     MySerial.print(x)
    #define OutLn(x)   MySerial.println(x)
um Schreibarbeit zu sparen.
Ausgaben erfolgen also mit    Out(...)    bzw.  OutLn(...)
Alle Ausgaben werden in der Klasse aufbereitet. Erkennbar ist die an den Punkten in ausgegebenen UNSIGNED LONG

Floating point Ausgaben bitte mit    _ftoa( Wert, Nachkommastellen)


Ergebnis:
Microsekunden genaue Bearbeitung im Timer-Interrupt (100000 pro Sekunde)
Microsekunden genaue Bearbeit in Externen Interrupt
In Interrupt Routinen können Tasks gestartet werden - mit einem Jitter von wenigen Microsekunden.
Schnelle serielle Ausgabe mit Ringpuffer
Kooperative Tasks werden können sehr genau wiederholt werden.
Platzsparend: Das vorgestellte System benötigt <25% des Programmspeicherplatzes und <50% des dynamischen Speichers!



Eigentlich sind wir damit am Ende angekommen. Es bleibt nur noch die verschiedenen Tabs zu einer Library zusammenzufassen. Dies geschieht in





Lektion 7

Die Sourcen sind fast identisch mit Lektion 6, nur dass die zusätzlichen Tabs jetzt nach .../Arduino/libraries/ESP8266_CoopOS_Stack
verschoben wurden.
Hier ist die ZIP-Datei der  Library, die in  .../Arduino/libraries entpackt werden muss:
Der Link dazu befindet sich hier:                                                        Download Libray ESP8266_CoopOS_Stack
Bitte in   .../Arduino/libraries   entpacken.

Der Sketch steht jetzt wieder allein - ohne weiter Tabs:

// ESP8266_CoopOS_Stack_Lesson7
// Esp8266 Wemos D1 R1 and Esp8266-12F Wemos D1 mini


/*
ESP8266_CoopOS_Stack_Lesson7

(C) 2019 Helmut Weber Dph.HelmutWeber.123@gmail.com


This is exactly the same as Lesson6.
But we transfered all files (leaving only the .INO) to
libraries/coopOS_Stack
and include all the stuff with
#include
<ESP8266_CoopOS_Stack.h>


Now our sketch gets really simple.

*/



// in library/ESP8266_CoopOS_Stack/cont.h; CONT_STACKSIZE 1024

#include <ESP8266_CoopOS_Stack.h>






volatile unsigned long cntTimerIrqs; // Timer IRQ counter
volatile unsigned long cntExternalIrqs; // External IRQ counter

extern unsigned long lastSchedMax; // Longest time of a task
extern char longestID; // ID of this task

volatile unsigned long SignalStart; // Time of signal set from Timer IRQ
unsigned long GotIt; // Time, when Task(D) reacts

int TaskID_A, TaskID_B, TaskID_C, TaskID_D ; // Task-IDs



const int irqPin=13;


/* =====================================================================
* Tasks
* =====================================================================
*
*/

/*
* ---------------------------------------------------------------------
* TaskA
* Show Time n millies, Scheduöer counts every second
* ---------------------------------------------------------------------
*/
void TaskA() {
unsigned long start;
//OutLn("Start TaskA");
OutLn("Start TaskA");
//MySerial << "Start TaskA" ;
while ((millis() % 1000) !=0);
TaskDelay(500);
while(1) {
start=micros();
Out("TaskA-1 "); Out(millis()); OutLn(" ms");
TaskYield();
Out("TaskA-2: Sched-Counts/s ");
TaskYield();
OutLn(cnt_sched); // Scheduler calls per second
TaskYield();
cnt_sched=0;
ESP.wdtFeed();
TaskDelay(1000000 - (micros() - start) -10); // -10: experimental finetuning
}

}





/*
* ---------------------------------------------------------------------
* TaskB
* Show millies, Timer-IRQs per second, External interrupts,
* longest time for a task
* ---------------------------------------------------------------------
*/
void TaskB() {
static unsigned long lastCalled=micros();
unsigned long cnt, lastCount;
OutLn("Start TaskB");
while ((millis() % 1000) !=0);
TaskDelay(500);
while(1) {
//lastCount=cnt;
lastCalled=micros();
lastCount=cnt;
cnt=cntTimerIrqs;
//cntTimerIrqs=0;
Out("TaskB-1 ");
Out(millis());
OutLn(" ms");
TaskYield();
Out("TaskB-2 IRQs/s ");
OutLn((cnt-lastCount)/2); // for 2 second
TaskYield();
Out("External IRQs/s ");
OutLn(cntExternalIrqs);

TaskYield();
Out("Longest Tasktime ");
Out(Tasks[longestID].name);
Out(": ");
Out(lastSchedMax);
Out(" Line: ");
OutLn(longestLine); // for 2 second
lastSchedMax=0;
TaskDelay(2000000- (micros() - lastCalled) +20); // +20: experimental finetuning

}
}



/*
* ---------------------------------------------------------------------
* TaskC
* just toogles the internal LED 10 times a second
* ---------------------------------------------------------------------
*/
void TaskC() { // toggle internal LED
static unsigned lastCalled=micros();
OutLn("Start TaskC");

while(1) {
digitalWrite(2,!digitalRead(2));
TaskDelay(50000);
}
}






/*
* ---------------------------------------------------------------------
* TaskD
* Waits for a signal from Timer interrupt
* Measures the time from Interrupt to Taskstart
* ---------------------------------------------------------------------
*/
void ICACHE_RAM_ATTR TaskD() { // TaskD waits to be waken up by Timer Interrupt
unsigned long start, cnt;
OutLn("Start TaskD"); // It is suspended until then

while(1) {
TaskWaitIrq();
GotIt=micros();
cnt=cntTimerIrqs;
start=SignalStart;
Out("->TaskD: ");
TaskYield();
Out(cnt); // 10 times a second
TaskDelay(10);
Out(" Delay (µs): "); // How long does it take from Timer Interrupt to this task ?
TaskDelay(10);
Out(GotIt-start);
TaskDelay(10);
Out(" Micros now: ");
TaskDelay(10);
Out(GotIt);
TaskDelay(10);
Out(" TimerIrq: ");
TaskDelay(10);
OutLn(start);

}
}



/* =====================================================================
* Interrupt Service Routines
* =====================================================================
*
*/

/*
* ---------------------------------------------------------------------
* Timer ISR
* ---------------------------------------------------------------------
*/


void ICACHE_RAM_ATTR onTimerISR() {
cntTimerIrqs++;
if ((cntTimerIrqs%10000)==0) {
//usrFlag=1;
SignalStart=micros();
TaskSetIrq(TaskID_D);

}
}


/*
* ---------------------------------------------------------------------
* External ISR
* ---------------------------------------------------------------------
*/

void ICACHE_RAM_ATTR onExternalISR() {
cntExternalIrqs++;
}


/* =====================================================================
* Init Interrupt Service Routines
* =====================================================================
*
*/

/*
* ---------------------------------------------------------------------
* Init Timer ISR
* ---------------------------------------------------------------------
*/

void ICACHE_FLASH_ATTR TimerInit(int mys) {
// Now we start a timer:
timer1_isr_init();
timer1_attachInterrupt(onTimerISR);
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP);
timer1_write(mys*5); // 0.2 µs per tick
}


/*
* ---------------------------------------------------------------------
* Init External ISR
* ---------------------------------------------------------------------
*/

void ICACHE_FLASH_ATTR ExternalInit(int iPin) {
// Set up external interrupts
noInterrupts();
pinMode(iPin, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt\
(iPin), onExternalISR, FALLING); // Init: External Interrupt, CHANGE will double interrupt frequency
interrupts();
}












/* =====================================================================
* Arduino setup - loop must be defined, but is not used
* =====================================================================
*
*/

void ICACHE_RAM_ATTR setup() {
MySerial.begin(500000);

Serial.println("--- Setup Start ---");
pinMode(2,OUTPUT);

// Initialize the Tasks:
//InitTask(char * nam, void( * f)(), int prio, State stat,
// unsigned long delay)
TaskID_A = InitTask((char *)"A", TaskA, 100, READY, 113 ); // Using primes for first delay
TaskID_B = InitTask((char *)"B", TaskB, 100, READY, 223 );
TaskID_C = InitTask((char *)"C", TaskC, 100, READY, 337 );
TaskID_D = InitTask((char *)"D", TaskD, 100, READY, 443 );
InitTask((char *)"MyS",MySer_Task, 90, READY, 400);


OutLn("--- Setup Interrupts ---");
TimerInit(10); // 100000 timer ticks per second
ExternalInit(irqPin); // Allow external interrupts
OutLn("--- Setup Interrupts End---");
OutLn("--- Setup End ---");
OutLn("--- Run the Scheduler ---");

// run the OS:
while(1) {
Schedule(); // we can do it in loop OR here
}
}


void loop() {
OutLn("Upps - this should not happen !");
}


Die Ausgabe:


TaskB-1 52.000 ms
TaskB-2 IRQs/s 100.000
External IRQs/s 0
Longest Tasktime D: 28 Line: 187
TaskA-1 52.000 ms
TaskA-2: Sched-Counts/s 173.217
->TaskD: 4.690.000 Delay (µs): 4 Micros now: 52.048.446 TimerIrq: 52.048.442
->TaskD: 4.700.000 Delay (µs): 4 Micros now: 52.148.446 TimerIrq: 52.148.442
->TaskD: 4.710.000 Delay (µs): 2 Micros now: 52.248.444 TimerIrq: 52.248.442
->TaskD: 4.720.000 Delay (µs): 2 Micros now: 52.348.444 TimerIrq: 52.348.442
->TaskD: 4.730.000 Delay (µs): 2 Micros now: 52.448.444 TimerIrq: 52.448.442
->TaskD: 4.740.000 Delay (µs): 1 Micros now: 52.548.443 TimerIrq: 52.548.442
->TaskD: 4.750.000 Delay (µs): 4 Micros now: 52.648.446 TimerIrq: 52.648.442
->TaskD: 4.760.000 Delay (µs): 2 Micros now: 52.748.444 TimerIrq: 52.748.442
->TaskD: 4.770.000 Delay (µs): 4 Micros now: 52.848.446 TimerIrq: 52.848.442
->TaskD: 4.780.000 Delay (µs): 4 Micros now: 52.948.446 TimerIrq: 52.948.442
TaskA-1 53.000 ms
TaskA-2: Sched-Counts/s 173.421
->TaskD: 4.790.000 Delay (µs): 4 Micros now: 53.048.445 TimerIrq: 53.048.441
->TaskD: 4.800.000 Delay (µs): 2 Micros now: 53.148.444 TimerIrq: 53.148.442
->TaskD: 4.810.000 Delay (µs): 1 Micros now: 53.248.443 TimerIrq: 53.248.442
->TaskD: 4.820.000 Delay (µs): 1 Micros now: 53.348.443 TimerIrq: 53.348.442
->TaskD: 4.830.000 Delay (µs): 2 Micros now: 53.448.444 TimerIrq: 53.448.442
->TaskD: 4.840.000 Delay (µs): 2 Micros now: 53.548.444 TimerIrq: 53.548.442
->TaskD: 4.850.000 Delay (µs): 2 Micros now: 53.648.444 TimerIrq: 53.648.442
->TaskD: 4.860.000 Delay (µs): 2 Micros now: 53.748.444 TimerIrq: 53.748.442
->TaskD: 4.870.000 Delay (µs): 4 Micros now: 53.848.446 TimerIrq: 53.848.442
->TaskD: 4.880.000 Delay (µs): 4 Micros now: 53.948.446 TimerIrq: 53.948.442
TaskB-1 54.000 ms
TaskB-2 IRQs/s 100.000
External IRQs/s 0
Longest Tasktime D: 28 Line: 187
TaskA-1 54.000 ms
TaskA-2: Sched-Counts/s 173.305,
Der Aufruf eines Tasks von einem Timer-Interrupt kann grundsätzlich so lange verzögert werden,
wie die längste Ausführungszeit eines Tasks (von allen Tasks) dauert.
Die Delay-Zeit von Interrupt -> Tasks beträgt meistens 1-5 µs. Aber hier misst jetzt der Scheduler, welches
die längste Zeit ist von TasksSwitch zu taskSwitch und gibt den Tasknamen und die Zeilennummer im Programm aus.
Es ist nicht erstaunlich, dass es die Ausgabe von UNSIGNED LONG in der Klasse myserial ist, obwohl in den
Ringpuffer geschrieben wird.
Die dauert ungefähr 27 - 28 µs, Das muss als "worst case" angesetzt werden - es kann, wenn auch selten, vorkommen.
Wenn sich die Ausgabe auf kurze Texte beschränkt, dann sind wesentlich kürzere "worst case" Zeiten erreichbar: <10µs


Zusammenfassung:

Kooperatives Multitasking kann durchaus mit einem RTOS mithalten oder je nach Fall weit übertreffen.
Es wird allerdings eine Menge Zeit im TaskSwitching verbraucht - so dass rechenintensive Vorgänge recht
langsam werden
können.

Dieses System ist dafür gedacht und geeignet, viele verschiedene Vorgänge schnell und deterministish zu
verarbeiten. Reagieren auf externe Ereignisse (externe Interrupts),

Dem stehen eine Menge Vorteile gegenüber:

Die Arduino Libraries sind in der Regel nicht für RTOS geeignet. Unter CoopOS könne sie weiter genutzt werden.
Der größte Vorteil ist die Geschwindigkeit.

Ein oft genanntes Argument gegen kooperativr Systeme:
Ein Task ist in der Lage, das ganze System zu blockieren. Dies mag für Rechner, auf denen ein Menge Programme
von unbekannter Herkunft laufen sollen, in wichtiges Argument sein.
Bei geschlossenen Systemen, die von einem oder einigen wenigen Programmieren erstellt wurden, sieht das anders aus,
Und es lassen sich durch Prüfungen im TimerInterrupt auch gezielt einzelne Tasks abschalten, wenn wirklich einmal
ein solcher Fall eintritt.

Beispiele:

Aufbau von vituellen Ports. Es werden 8 Serial_to_Parallel Wandler (74164) als Output in Reihe geschaltet
angeschlossen.
Ebenso 8  Parallel_to Serial Wandler (74166). Bei jedem Timer-Interrupt (jede 10 µs) wird ein Bit rausgeschoben
und ein Bit empfangen, Nach 64x10 = 640 µs sind alle 64 Bit (Output) und 64 Bit (Input) übertragrn,
Zwei 64 Bit Input- und Output-Register stehen also wirklich als Register für Tasks zur Verfügung, die jede
Millisekunde aufgerufen werden.
Damit lassen sich z. Bsp. 16 Steppermotoren gleichzeitig und völlig unabhängig ansteuern !
Und es kann ebenfalls auf 64 Inputsignale jede Millisekunde reagiert werden. Ideal für jede Robotersteuerung.

Die Steuerung einer  Waschmaschine mit all ihren Sensoren und Aktoren inklusive Anzeige und  Bedienselementen
kann komplett realisiert werden.

Zündzeitpunkt (Otto-) und Einspritz-Menge,  -Dauer, -Zeitpunkt  (Diesel-/Ottomotor)  können bei  jedem Takt eines
jeden Zylinders neu angepasst werden: 6000 RPM, 6 Zylinder = 100*6 =600 pro Sekunde = 1.5 ms Zeittakt.