Dipl. Phys. Helmut Weber Dph.HelmutWeber at gmail.com





  English

> 3.000.000 Interrupts/s


10.000.000 Taskswitches/s



4 Cores

1400 Mhz

1 GB RAM

ARM v8 Prozessor

64 Bit



   Das schnellste Microcontroller-System der Welt



Kurzfassung: Was man mit einem 1/4 Raspberry Pi machen kann

 

Bei einem RTOS werden üblicherweise die Tasks jede 1 ms gewechselt.
Schnell zu erfassende Vorgänge werden durch Interrupts erfasst.


Das hier vorgestellte System basiert auf kooperativem Multitasking mit Priorität auf
schnelle und deterministische Reaktionen.



Das passiert hier zum Vergleich  in 1 ms (Logik Analysator):




Nur die Ausgabe von Zeichen auf die serielle Schnittstelle jede 250 us ist erkennbar.

In 100 µs kann man etwas mehr sehenr:


 
Aber einen wirklichen Eindruck bekommt man erst bei einem Zeitfenster von 10 µs:




2 µs:


Erklärung: ( Weiter unten folgt eine ausführliche Erklärung )

FIQ
  Ein externer Interrupt, der alle 500 ns auftaucht und bearbeitet wird.
 
IRQs
  Weitere Interrupts im Abstand von 2 Mikrosekunden werden vom FIQ erkannt und an
  eine eigene Interrupt Routine geleitet.
  Sinn: Erkennen von äußeren Interrupts und - gestaffelt nach Priorität - aufrufen passender
  Interrupt Routinen.
  FIQ- und IRQ-Signale werden Timer gesteuert an 2 Pins erzeugt (ohne Einsatz der CPU)
  Sie werden außen an zwei Input-Pins geleitet, die die Interrupts auslösen.
 
Task-calls
  Jeder Puls zeigt einen Taskswitch mit Bearbeitung eines Tasks
 
 
Idle-calls
  Wird aufgerufen, wenn sonst kein Task READY ist. Kann benutzt werden.
 
 
Variable Change
  Ein Task ändert jede Mikrosekunde einen Wert.  Das wird im FIQ all 500 ns überprüft und
  ein Signal an einen zweiten Task gesandt. Sinn: Überprüfung von interner Signal Latenz,
  genannt MaxLat (s.u.)
 
Char Output
  Alle 250 µs wird ein Zeichen gesendet, wenn vorhanden.
 
Nicht dargestellt, aber in weiteren Tasks bearbeitet:
Erzeugung eines Tones von 440 Hertz (digital) zu akkustischen Kontrolle des Systems
Erzeugung eines Tones von 432 Hertz
Empfang (und Ausführung) von Kommandos über serielle Schnittstelle
Aktualisierung des Bildschirms

Der Raspberry hat 4 Kerne. Die vorgestellten Aufgaben laufen auf einem Kern, entsprechend
25% der Gesamtleistung des Prozessors.

4 MHz (mit 2. Kern, with 2. core)

Zum Test erzeugt ein 2. Kern eine Flanke Low->High an einem Pin (Output).
Der wird außen mit einem weiteren Pin verbunden (Input). Der 2. Kern registriert dann
den Flankenwechsel Low-High und setzt den ersten Pin wieder zurück.
Die Ergebnisse werden regelmäßig an das CoopOS im Kern 1 geschickt.
Das Ergbnis sind mehr als 4000000 erkannte Flanken pro Sekunde - also im Abstand von 250 ns.
Das ist mit externen -  nicht selbst erzeugten - Signalen noch deutlich steigerungsfähig.



Bei bis zu 6 Millionen Taskwechseln pro Sekunde kann jeder Task mikrosekunden genau gestartet werden.


2 Kerne = 50% der Rechenleistung wurden noch nicht genutzt !













Beispiel 4 - ausführlich



Cooperative Multitasking - Raspberry Pi 3+ bare metal



3.000.000 Interrupts pro Sekunde und mehr !
Bis zu 10.000.000 Taskswitches pro Sekunde
4 Kerne
1400 Mhz
1 GB RAM
ARM v8 Prozessor
64 Bit
            Raspberry Pi
                                        Bild: RaspberryPi.org

Einleitung

Seit Mitte der 70er Jahre des vorherigen Jahrhunderts werden Microcontroller eingesetzt. Es handelt sich
in der Regel um bereits vorhandene Mikroprozessoren, bei denen zusätzliche Elemente integriert wurden,
um komplette kleine Systeme in einem Chip zu erhalten, die Programmspeicher, Datenspeicher, Timer,
Ein- / Ausgabepins, AD- / DA-Wandler usw. bereits enthalten.
Ein solches System ist unmittelbar nach dem Einschalten in der Lage, bestimmte Aufgaben auszuführen:
Messungen mit angeschlossenen Sensoren durchzuführen (Temperatur), Steuer- und Regelaufgaben usw.

Lange waren dies 8-Bit Prozessoren mit sehr begrenztem Datenspeicher. Die werden - weil kostengünstig -
auch heute noch in riesigen Stückzahlen eingesetzt.
Inzwischen haben allerdings an vielen Stellen moderne 32-Bit Microcontroller an Boden gewonnen. Mit
höheren Taktraten, schnellerer Verarbeitung, mehr Programm- und Datenspeicher, integrierten Funktionen
wie LAN, WLAN, Bluetooth usw. sind sie den immer komplexeren Aufgaben besser gewachsen.
Diese erfordern eigene Betriebssysteme, um die Vielzahl der Aufgaben zu koordinieren und quasi parallel
durchführen zu können.
Als Betriebssysteme haben sich sogenannte RTOS (RealTime Operating Systems) als Standard etabliert.

Aber auch der Massenmarkt beeinflußt diese Entwicklungen. Mit dem Arduino-Entwicklungssystem wurde
eine ganze Generation an die Entwicklung mit Microcontrollern herangeführt. Inzwischen gibt es kaum
Steueraufgabe, führ die man dafür keine Beispiele findet - bis hin zu Robotern und Quadcoptern.

Aber moderne Microcontroller verdrängen langsam die 8-Bitter. Wer gibt schon 3,25€ für eine AVR 328p
aus (Arduino-Baustein), wenn er für 5€ einen 32-Bitt Prozessor, mindestens 12X schneller, mit WLAN,
mit einem vielfachen an Speicher usw. bekommt - der sich genauso unter der Arduino-Oberfläche
programmieren läßt?

Einen anderen Weg gingen die Raspberry Pi. Von Anfang an darauf ausgelegt, unter Linux zu funktionieren,
wurden sie trotzdem mit einer Reihe von IO-Pins ausgestattet, um auch Mess- und Steueraufgaben zu
übernehmen. Allerdings haben sich die unvorhersagbaren Latenzzeiten von Linux als Problem für
Echtzeitaufgaben herausgestellt. Zur Lösung wurden zwei Ansätze gemacht:
Linux preempt_RT versucht durch patches im Linux dieses besser für Echtzeitaufgaben einsetzen zu können.
Damit sind maximale Latenzzeiten bis herunter zu 50µs (gegenüber mehreren ms) zu erreichen und damit
für viele Echtzeitaufgaben einsetzbar.

Allerdings kann ein kleiner 328p (Arduino) - OHNE Betriebssystem - trotzdem bei vielen Aufgaben sehr viel
schneller reagieren.
Als Folge entstand die Entwicklung, die Raspberry "bare metal" - also ohne jedes Betriebssystem - zu
programmieren und so die wirkliche Geschwindigkeit der Arm-Prozessoren der Raspberry nutzen zu
können. Zunähst unter Verlust all der Möglichkeiten, die Linux bietet.

Allerdings haben sich Arm-Prozessoren bereits seit einiger Zeit in den Bausteinen kommerieller Anbieter
durchgesetzt. Und bei allen Unterschieden: die grundsätzliche Programmierung ist ähnlich.

Bisher waren es durchgängig 32-Bit Prozessoren.
Dann erschien Anfang 2016 der Raspberry PI 3 und Anfang 2018 der 3*.
Der 3+ wird mit 1400 MHz getaktet (zum Vergleich: Arduino mit 18MHz) und verfügt über 1 Gigabyte
RAM (zum Vergleich: Arduino mit 2 Kilobyte).

Die Prozessoren von 3 und 3+ können sowohl mit 32 Bit als auch 64 Bit betrieben werden.


Typische Microcontroller - wie sie in der Industrie und der Automobilbranche benutzt werden, sind deutlich
langsamer und verfügen über weniger Speicher - und sind 32 Bitter.

So taucht also die Frage auf: was kann ein Raspberry 3+ - programmiert als "bare metal" Microcontroller
leisten?

Nun hat man einen Raspberry 3+ mit einem Linux mit preempt-Patch, das wunderbar funktioniert und
für fast alle Aufgaben geeignet ist. Was könnte einen veranlassen, auf dieses Linux zu verzichten, um
den Raspberry wie einen "Arduino" zu nutzen?
Der Rausch der Geschwindigkeit !

Dort der 8 Bit Prozessor mit einem Takt von 18MHz. Hier der 64 Bit Prozessor mit 1400 MHz. Der Raspberry
ist so wohl einer der schnellsten - wenn nicht der schnellste - Microcontroller überhaupt. Zu einem
erschwinglichen Preis.
Er sollte selbst den modernen ESP32 problemlos in die Schranken weisen!
Nun ist es nicht trivial, einen ARM-Cortex-A53 in einen 64-Bit-Mode zu versetzen, mit dem man dann
arbeiten und die Möglichkeiten des Peripheriebausteins Broadcom BCM2837 zu nutzen, zumal der sehr
viel schlechter dokumentiert ist, als seine Vorgänger in den älteren Raspberrys.

Rene Stange hat mit seiner Bibliothek "circle" einen sehr wichtigen Beitrag geleistet: https://github.com/rsta2/circle
Hier wird diese Bibliothek als Grundlage benutztz,
Aber es gibt inzwischen einige Ansätze, die Raspberry Pi unter "bare metal" zu programmieren. Und wem der Raspberry Pi 3+
mit seinen Netzwerk- und USB-Anschlüssen zu groß und zu sperrig ist, um noch als Microcontroller angesehen zu werden:
Der Raspberry Pi Zero W ist es bestimmt. Er verfügt über den gleichen Prozessor, 512kb RAM, Wlan und Bluetooth und hat die
Größe einer Scheckkarte.
Microcontroller wie der ESP32 oder dem Raspberry Pi Zero W setzen definitiv Maßstäbe für einen unaufhaltsamen Trend:
Microcontroller im Gigahertz- und Gigayte-Bereich, die über WLan das IoT (Internet of Things - Internet der Dinge) vorantreiben werden.

                            RaspberryPi Zero 
                            Bild: Kiwi-electronics.nl


Viele Probleme lassen sich elegant unter Linux preempt_RT lösen.
Aber gelegentlich stören die "Ausreißer" doch. Bei manchen Problemen ist es eben nicht egal, dass
99.99% der Reaktionen innerhal von 5µs erfolgen, wenn es eben doch auch vorkommt, dass die
Reaktionszeit auch einmal 50µs erreichen kann.
Das dürfte sich bei aller Fortentwicklung von Linux preempt_RT auch so schnell nicht ändern.

Wie wäre es, wenn man den Raspberry wie einen Arduino programmieren könnte?
Herr über den Prozessor, keine einzige Instruktion, die man nicht vollständig unter Kontrolle hat. In allen
Teilen vorhersehbar. Ohne vorgegebenes Betriebssystem.

Es bliebe ein 64 Bit Prozessor mit 1.4 GHz, der über seine IO-Pins mit der Außenwelt kommunizieren
könnte.
1400 Instruktionen pro Mikrosekunde (sehr vereinfacht), was kann man damit anstellen?
Und kann man diese Geschwindigkeit auch sinnvoll mit einem eigenen OS nutzen?
Ein RTOS mit einem Takt von 100 oder 1000 Hz ist für höchst genaues Timing hier einfach nicht geeignet:
zu überladen, zu langsam, nicht deterministisch genug !

Mein CoopOS bildet hier die richtige Grundlage!

Es schafft im einfachsten Fall (1 Task + Idle+2.000.000 FIQInterrupts, in denen auch gearbeitet wird!)
im Mittel mehr als 10.000.000 TaskSwitches (inkl. Taskausführung!) pro Sekunde - ohne FIQ-Interrupts das Doppelte!

Mit anderen Worten (ohne FIQ-Interrupt):

Scheduler -> Task -> Scheduler

benötigt ca. 50 ns. Davon ca. 10 ns jeweils für den TaskSwitch.


Einschub:
2 MHz FIQ-Interrupt
Ein Task für die Zeichenausgabe
Ein Task zum Hochzählen von TCount
Ein Task gibt jede Sekunde TCount aus:
Taskswitches



Das sind schon sehr abstrakte Zahlen, deshalb ein Beispiel:
Ein Auto, dass sich mit 200 km/h über die Autobahn bewegt, fährt in 1 Sekunde 55,6 m.
In 10 ns fährt das Auto 0,00056 mm.
Das ist der 180ste Teil des Durchmessers eines menschlichen Haares !


Das folgende Beispiel möge die Leistungsfähigkeit der Kombination von "bare metal" mit CoopOS
demonstrieren:

Es werden 3 Demo-Programme vorgestellt. Programm Demo-1 wird besonders ausführlich besprochen.





PRO SEKUNDE:                                                     DEMO 1

2000.000 Externe FIQ Interrupts
1000.000 Externe Interrupts
400.000 Variablen-Änderungen erkennen
2 Töne präzise erzeugen ( an digitalen Pins)
Bildschirm Ausgabe der System Parameter über serielle Schnittstelle
Kommandos abfragen

Maximale Latenz: 1 Mikrosekunde !







0                   Warum ist dieses System so unglaublich schnell?

0.1                Raspberry 3+

Mit 1400 MHz ist der Raspberry 3+ den meisten Microcontrollern um den Faktor 2-100 voraus

• Mit 64 Bit Datenbreite ist er den meisten Microcontrollern um den Faktor 2-8 vorraus

• Mit 100 ns ist die Interrupt-Reaktionszeit extrem schnell


0.2                CoopOS

CoopOS verfügt über nahezu alle Möglichkeiten eines RTOS. In der Tat könnte man ein Programm
unter CoopOS mit einem Programm unter RTOS verwechseln! Aber:

• Bis zu 10.000.000 Taskswitches pro Sekunde

• Vom gesetzten Signal im FIQ ca. 355 ns bis zum Start des passenden Tasks

• Vom gesetzten Signal in einem Task bis zur Reaktion des damit gestarteten Tasks: < 1 µs

• Ausgaben ( z. Bsp. UART) < 1 µs


Besonderheiten von CoopOS:

Mehrfach gepufferte Ausgabe - Ausgabe einzelner Zeichen ist ein eigenerTask.

Tasks haben Prioritäten.

Zu besonderen Anlässen wird die Priorität angehoben, um für schnelle Reaktion

zu sorgen: taskWaitSignal(), taskWaitResource(), ...
Hintergrund: ein Task, der auf etwas wartet, soll schnellstmöglichst gestartet werden, wenn das
entsprechende Ereignis stattfindet. Danach bekommt der Task seine original Priorität zurück.

Bei jedem Taskswitch wird vom Scheduler festgestellt, welcher Task die höchste Priorität hat und
dieser wird gestartet.

Es darf nur einen Task geben, der auf nichts wartet und im While-Loop nur ein taskSwitch() enthält.
Er muss die niedrigeste Priorität haben. Dies entspricht einem Idle-Task.

Jeder Task ist eine State-Machine und speichert seinen Zustand (letzte ausgeführte Zeile) vor jedem
freiwilligen Taskwechsel ab. Daher ist die Umschaltung des Stackbereiches und der Prozessor-Register
nicht notwendig - das macht den Taskwechsel extrem schnell !

Jeder Task wird mit einem Pointer auf seinen eigenen Parametersatz aufgerufen. Diese Parameter sind
durch andere Tasks änderbar.

Beispiel: Ein Task, der einen Schrittmotor steuert, enthält die Daten dafür - vorwärts, rückwärts,
Taktzeit, Anzahl der Takte - aus seinem Parametersatz, der in dem Fall auf eine Struktur verweist.
Ein anderer Task kann diese Parameter einstellen. Damit gelingt der Übergang von einem prozeduralen
zu einem deklarativen System.

CoopOS verfügt über ein vorbereitendes Delay: Irgendwo im Task wird festgelegt: Delay soll ab
hier gelten. Das später aufgerufene taskDelayPrep(Ticks) benutzt den gesetzen Wert als Grundlage.

Sinn: Von einem bestimmten Zeitpunkt an soll mit höchster Genauigkeit eine Wiederholung
erreicht werden - im festgelegten Takt von z. Bsp. 100 µs. Das funktioniert nicht mit einem reinen Delay,
(Pausiere ab jetzt), da z. Bsp die Ausgaben in jedem Zyklus anders sein könnten.

CoopOS benötigt keine Regelung der Zugriffsrechte. Wer den Prozessor hat, kann nur vom FIQ, nicht von
anderen Tasks unterbrochen werden. Daher entfällt auch das bei einem RTOS so häufige - wenn
auch kurzzeitige - Verbieten des Interrupts.

Der FIQ reagiert schneller als normale Interrupts. Alle Vorgänge werden deshalb vom FIQ analysiert
und für zu bearbeitende "Interrupts" Signale gesetzt.
Der FIQ kann dabei gewünschte Prioritäten setzen. Es können mit einem Signal auch mehrere Tasks
gestartet werden. Die geschieht mit absteigenden Prioritäten nacheinander.

Da der FIQ immer gleichmässig (alle 500 ns) ausgelöst wird - daneben aber keine weiteren Interrupts,
gibt es keinerlei durch Interrupts veränderte Timings im System uns machen es hochgradig
deterministisch.

CoopOS vermeidet Reentrance-Probleme von Bibliotheken.

CoopOS benötigt keinerlei Interrupts - auch keine Timer. Es muss lediglich irgendwo der Ablauf der Zeit
in µs verfügbar sein. Auch darauf kann ma zur Not verzichten und die Zahle der Scheduler-Calls
benutzen. Damit ist CoopOS auf praktisch jedem Rechner einsetzbar - vom kleinsten 8-Bit-Controller
bis zum Superrechner.

CoopOS ist so kompakt, dass es sofort für jeden Programmierer intuitiv einsetzbar ist. Und es kann aus
gleichem Grund auch leicht an eigene Bedürfnisse angepasst werden.
Damit ist CoopOS in höchstem Maße transportabel. Es muss für einen neue Prozessor nicht erst ein
umfangreiches RTOS angepasst werden.

CoopOS ist eher ein Paradigma als ein Betriebssystem.



Einige Vorurteile gegenüber koopertiven Betriebssystemen:

Ein falsch laufender Task kann alles zum Absturz bringen.

Bedingt richtig. Aber ein regelmäßiger Interrupt kann das System überprüfen und schlecht laufende Tasks
blockieren. Die Tasks können sich selbst überprüfen: An welchen Stellen zwischen 2 taskSwitch()-
Aufrufen wurde mehr als eine vorgesehene Zeit benötigt.

Ein kooperativer Task muss immer vollständig ablaufen.

Falsch. Ein kooperativer Task kann genauso wie in einem RTOS in einer while(1)-Schleife laufen.
es können problemlos die Primzahlen im Hintergrund berechnet werden.

Ein kooperatives System verhält sich nicht deterministisch.

Besonders falsch! CoopOS ist in dieser Hinsicht jedem RTOS überlegen !

Nur die Umschaltung auf eigene Stack-Bereiche machen einen Task sicher.

Falsch. Im Gegenteil. Ein eigener Stack für jeden Task sorgt bei einem RTOS für manche Probleme.
Entweder wurde der Stackbereich zu klein geschätz. Dan kommt es - vielleicht nur in besonderen
Situationen - zu einem Stacküberlauf.
Um den zu verhindern, muss das OS den Stack regelmäßig überprüfen.
Oder man war großzügig und verschwendet Speicher.
Erst mit dem Einsatzt einer MMU ist dieses Umschalten wirklich sinnvoll.
Bisher sind kaum Microcontroller damit ausgestattet.

Bei CoopOS steht jedem Task jederzeit der komplette Stack zu Verfügung und solche Maßnahmen sind
überflüssig.
Aber: Wenn gewünscht, kann auch bei CoopOS jeder Task einen eigenen Stack bekommen!

Wie man aus den Anfangstagen von Windows weiß, neigen kooperative Systeme zu Abstürzen

In der Tat hat der "Blue-Screen" aus alten Tagen sehr wesentlich dazu beigetragen, kooperative
System in Verruf zu bringen!
Aber auf einem Microcontroller laufen nicht wirkliche Betriebssysteme, unter denen alle - auch schlecht
programierte - Programme laufen müssen.
Die Entwickler von Microprozessor-Lösungen testen ihre einzelnen Tasks ausgiebig. Denn ein fehlerhaftes
System - egal ob es unter einem RTOS läuft oder nicht - hat seine Aufgaben fehlerfrei zu erledigen.
Bei bis zu 70 Mikrocontrollern in einem modernen PKW ist die Gesamtfunktionalität sonst nicht
gegeben.
Es macht aber in jedem Fall sind, Tasks für die Überprüfung anderer Tasks vorzusehen.



0.3 CoopOS in Bildern


0.3.1 FIQ

Der FIQ ( Fast Interrupt ) ist eine Spezialität von ARM-Prozessoren. Er reagiert besonders schnell auf
äußere Signale.

images/19-1.png

Oben: Äußeres Signal von steigender Flanke bis zur nächsten steigenden Flanke sind es 500 ns.
Unten: Steigende Flanke - FIQ setzt Signal ab. Fallende Flanke: ein passender Task wurde
vom Scheduler gestartet






0.3.2 Scheduler

                    Alle 150 ns ein Task !


images/19-2.png



































Oben: FIQ - 3 Perioden = 1.5 µs

Unten: Scheduler startet Tasks - hier ca. 10 in 1.5 µs







Der Scheduler ruft in Demo-1 (im Durchschnitt) mehr als 4 Millionen Tasks pro Sekunde auf



Untere Zeile: Jeder Flankenwechsel ist ein Taskwechsel. Pulse schafft mein Logic-Analyzer aus Zeitgründen nicht mehr
22 Taskwechsel in 5 µs




















FIQ startet über den Scheduler Tasks:
images/19-4.png











1               Aufgaben des Demo-Programms Demo-1

1.1            FIQ


1.1.1        2 Millionen FIQ-Interrupts pro Sekunde

Der Raspberry erzeugt mit seinem Peripheriebaustein (ohne Prozessorbelastung) auf einem Pin
einen Takt von 2 MHz.

Dieser Pin wird per Draht ausserhalb des Pi mit einem Interrupt-Pin verbunden. und erzeugt
so 2 Millionen äquidistante Interrupts - also jeweils im Abstand von 500 Nanosekunden.
Die so von aussen angelegten Interrupts ( es könnte auch eine externe Interruptquelle benutzt
werden) rufen 2 Millionen mal pro Sekunde die FIQ-Interrupt Routine auf.
Diese Routine zählt FIQCount hoch und bildet die Zeit-Grundlage des Interrupt-Systems.
Für die Verwaltung im CoopOS wird ein Timer benutzt, der alle 100ns die Variable "Ticks"
hochzählt. Dies macht Sinn, da der Scheduler bis zu 6 Millionen Mal pro Sekunde aufgerufen
wird. Ein Sleep(10) bedeutet bei einem RTOS in der Regel: 10ms Pause. Bei CoopOS ist das eine
Mikrosekunde Pause - die durchaus von anderen Tasks genutzt werden kann.

Weitere Aufgaben innerhalb des FIQ-Interrupts im Demo 1:



1.1.2                FIQ-Reaktionszeit

In FIQ wird ein Output-Pin bei jedem Aufruf invertiert. Damit läßt sich messen, wie lange der
Prozessor benötigt, bis er auf das eingehende Signal reagiert. Die FIQ-Interrupt-Routine reagiert
etwa 120ns -spätestens nach 188 ns nach dem externen Signal.



Der FIQ-Interrupt ist trotz seiner verschiedenen Aufgabe spätestens nach 312 ns nach Auftreten des äußderen Signals vollständig abgearbeitet.



1.1.3                Kammerton A

Es wird alle 2273 FIQ-Aufrufe (1136 µs) ein externer Pin invertiert, der als digitaler Pin mit einem
Lautsprecher verbunden ist (3.5 mm Buchse des Pi).
Damit wird der Kammerton A erzeugt - genau 439,94 Hz.





1.1.4                Ton B

Es wird auf dem 2. Audiokanal genauso eine Frequenz von 432 Hz erzeugt Alter Kammerton A).
Die Schwebung kann man gut hören.
Diese Frequenz ist variabel (s.u.)



1.1.5                1.000.000 Interrupts auf einem zweiten Interrupt-Pin

Es wird wie unter eine 1.1.1 eine zweite Frequenz an einem externen Pin von 500kHz erzeugt.
Dieser Pin wird mit einem 2. Interrupt-Pin als Eingang verbunden.
Dieser erzeugt sowohl bei fallender als auch steigender Flanke einen "Interrupt" = 1000.000
pro Sekunde.
Da der Aufruf echter Interrupts viel länger dauert, erledigt die Erkennung der Flanken die
FIQ-Routine (FIQ = Fast Interrupt).
Es ist nur eine FIQ-Routine mit der superschnellen Reaktionszeit möglich.
Für jeden "Interrupt an dieser Leitung wird einfach ein Zähler inkrementiert. Die Interrupts
können auch vollständig extern ausgelöst werden.
Die maximale Interrupt Frequenz für weitere externe Signale neben dem FIQ - wenn FIQ mit
2 MHz getaltet wird - ist 1MHz - wie hier
benutzt.




Es wird nur die steigende Flanke des Signals benutzt. Diese wird (vom FIQ) spätestens nach 188 ns erkannt.


Nach spätestens weiteren 563 ns wird per Signal vom FIQ der passende "Interrupt"-Task gestartet .



1.1.6                Veränderungen auf Variablen - Reaktion auf interne Programmzustände

Da immer nur ein Task zur Zeit bei CoopOS läuft ( wie auch bei einem RTOS), aber die Punkte
des Taskswitches von jedem Task freiwillig und damit vorhersehbar gesetzt werden (anders,
als bei RTOS), sind Zugriffe auf Variablen ohne weitere Vorkehrungen (Semaphoren, Mutexe)
möglich.
In einem CoopOS-Task (s.u.) wird eine Variable PCount alle 3µs (300000 mal pro Sekunde)
geändert.
Die FIQ-Routine testet diese Variable auf Veränderung und zählt die Veränderungen.
Bei JEDER Änderung setzt FIQ ein Signal an CoopOS ab (taskSetSignal(2), um einen darauf
wartenden Task anzustoßen.
Dieser Task - nicht FIQ - zählt die "Interrupts".
Um zu sehen, ob alle Änderungen der Variable erfasst wurden, werden 2 Sekunden
Veränderungen mit 200kHz vorgenommen, dann erfolgt eine Pause von 1 Sekunde.
Dies ermöglicht später die genaue Kontrolle, ob alle Signals erkannt und bearbeitet wurden.
Die Zeit zwischen taskSetSignal bis zum erfolgreichen Start des wartenden Tasks wird
gemessen (s.u.)


s.u.Die Veränderung einer Variablen irgendwo innerhalb eines Tasks wird vom FIQ
überwacht. Nach einer Änderung wird vom FIQ spätestens nach  625 ns ein Task aufgerufen,
der darauf reagiert.






1.1.7                Ausgaben alle 2 Millionen Interrupts

Alle 2 Millionen Aufrufe (also jede Sekunde) setzt FIQ ein Signal taskSetSignal(1) ab, um die
die Ausgabe der System-Parameter anzustoßen.
Das Signal wird im gleichen Tick oder spätestens im nächsten bearbeitet (IrqCount)
Damit sind die Aufgaben in FIQ beschrieben. Nicht ganz wenig für 500 Nanosekunden!

Die weiteren Tasks laufen unter CoopOS.



images/19-5.png






1.2                 CoopOS

Aufgaben (Tasks) unter CoopOS

Die meisten Aufgaben unter CoopOS sind in zwei Gruppen einzuteilen:
- Tasks, die auf ein Signal warten.
- Tasks, die zyklisch in einer while(1)-Schleife wiederholt werden. Da diese Tasks innerhalb
oft taskSwitch() aufrufen und eine vollständige Ablaufzeit des Tasks nicht genau vorhersehbar
ist, wird zu Anfang jeweils festgelegt, wann der nächste Aufruf erfolgen soll. Dies ist ein neues
Paradigma gegenüber dem üblichen taskDelay, dass sonst (RTOS) häufig benutzt wird.
CoopOS ist darauf getrimmt, besonders einfach und deshalb SCHNELL zu sein. Das Herz von
CoopOS ist der Scheduler, der ohne Zeitakt in einer Schleife so oft wie möglich in einem
Loop aufgerufen wird.
Es sind bis zu 4000.000 Scheduler-Durchgänge pro Sekunde möglich. (Neue getunte Version)

Die Tasks im einzelnen:



1.2.1                Zeichenausgabe (Task 8)

Der Raspberry funktioniert als "Blackbox", d.h. er benötigt keine Tastatur, keinen Bildschirm oder
irgendeine sonstige Ein- / Ausgabemöglichkeit.

Allerdings ist es schon angenehm, wenn man Ausgaben des Systems in Echtzeit bekommen kann.
CoopOS wurde daher mit einer solchen Ausgabemöglichkeit über eine serielle Schnittstelle
versehen. Allerdings weiß jeder Programmier, das Ausgaben das System sehr belasten können,
was hier unbedingt vermieden werden muss.
Alle Ausgaben wurden daher über mehrstuffiges Buffering neu konzipiert. Es wird - wenn
vorhanden - alle 500µs ein Zeichen an die serielle Schnittstelle gesendet. Diese Ausgabe
findet innerhalb eines CoopOS-Tasks statt und dauert weniger als eine Mikrosekunde.
Die Puffer können beliebig groß sein. Allerdings dürfen so im Mittel nicht mehr als 2000 Zeichen
pro Sekunde gesendet werden.
Diese Ausgaben belasten das System wenig und können immer mitlaufen. Damit ist jederzeit ein
genauer Systemüberblick in Echtzeit möglich.




1.2.2                Ausgabe der Systemparameter (Task 1)

Es werden jede Sekunde folgende Systemparameter ausgegeben:
Micros - Zeit des Tasksstarts in Microsekunden. Die Drift (Abweichung von 1000000 µs
pro Aufruf) ist ein Maß dafür, wie sehr andere Tasks den Aufruf verzögern.
Die System-Parameter im einzelnen:

• Scheduler-calls/s

Alle Tasks geben nach einiger Laufzeit die Kontrolle zurück an den CoopOS-Scheduler, der
alle Tasks verwaltet und startet. Scheduler-calls/s zeigt, wie oft der Scheduler in der
letzten Sekunde gestartet wurde. Dies geschieht zwischen 2.6 und 3.0 Millionen mal pro
Sekunde, d.h. der Scheduler kann im Schnitt alle 300 Nanosekunden einen Task starten.

• TaskSwitches/s

Gibt an, wie oft in der letzten Sekunde vom Scheduler tatsächlich ein Task gestartet wurde.

• Idle calls/s

Nicht jeder Aufruf des Schedulers startet eine Task. Kein Signal vorhanden, kein Task
READY - dann wird der Idle-Task aufgerufen, der den IdleCounter inkrementiert. Der
IdleCounter ist ein gut Maß dafür, wie sehr das System ausgelastet ist. Bei einem hohen Anteil
von Idle-calls an den Scheduler-calls ist das System unterbeschäftigt. Hier liegt der Anteil
an Idle-calls grob bei 90%. Dem CoopOS kann mehr zugemutet werden.



1.2.3                TaskSwitches pro Millisekunde (Task 2)

Um die Tasks zeitlich zu verteilen, wurden sie am Beginn mit jeweils einer Verzögerung von
1ms gegenüber dem Vorgänger gestartet.
Task 2 wird als eine ms später als Task 1 gestartet.
Task 1 setzt nach Ausgabe (genaugenommen vorher nach Zwischenspeicherung) den Wert
von TaskSwitches auf Null.
Task 2 zeigt also an, wie viele Switches in dieser einen ms vorgenommen wurden.
Es ist gut zu beobachten, wie die Änderungen an PinValue diesen Wert beeinflussen. Es
sind hochgerechnet mehr als 400.000 erfolgte Taskswitches pro Sekunde!
Kein Wunder bei zeitweilig 400.000 eingehenden Signalen durch Variablenänderung!



1.2.4                Anzeige Frequenz Kammerton A (Task 3)

Die Tonerzeugung wird von FIQ übernommen (s.o.). Task 3 wird jede Sekunde aufgerufen und
gibt die in FIQ gezählten Tonimpulse aus (/2 ergibt Tonfrequenz).
Die Qualität der Tonerzeugung kann mit Stimmgabel oder Internet verglichen werden.



1.2.5                Anzeige Frequenz Ton B (Task 4)

Auch diese Frequenz wird von FIQ erzeugt. Es ist der alte Kammerton A mit 432 Hz.
Die Schwebung der beiden Frequenzen von 8 Hz auf den 2 verschiedenen Kanälen kann
deutlich vernommen werden.
Ton B kann interaktiv verändert werden (s.u.)
Die Töne sind in Lautsprechern / Ohrhörern an der 3.5mm Buchse hörbar.
Das Ohr ist ein sehr empfindliches Messinstrument. Bei preempt_RT Linux sind gelegentliche
Latenzen von auch nur 20 µs bei solchen Experimenten sehr deutlich als Knistern hörbar.
Das kommt hier nicht vor!



1.2.6                Interaktive Befehle durch Eingaben an serielle Schnittstelle (Task 11)

10 mal pro Sekunde wird an der seriellen Schnittstelle geprüft, ob ein Zeichen eingegangen ist
Damit kann die Frequenz von Ton B verändert werden.
Fein: +,- Grob: 1,2 Sehr Grob: 3,4
Bei sehr hohen Frequenzen sind nicht mehr alle ganzzahligen Frequenzen einstellbar.
Ändern der 1 MHz Irq-Frequenz: Ändern: 8, Normal: 9
Anhalten der PCount-Erzeugung (nach vollen 200000 wirksam): n, wieder normal: m
Einen LatMax von 100µs erzeugen: x (länger drücken) LatMax auf Null setzen: y



1.2.7                Ausgabe der Bildschirm-Maske (Task 12)

Dieser Task bereitet den Bildschirm mit allen statischen Inhalten vor. Anschließend wird der
Task gestoppt.



1.2.8                 Messung der Latenz von taskSetSignal(1) und IrqTicks (Task 9)

Alle 2000000 Interrupts (jede Sekunde) sendet FIQ ein Signal taskSetSignal(1) an CoopOS.
Es wird dabei der FIQ-Interrupt Zählerstand festgehalten.
Das Signal wird vom Scheduler bei seinem nächste Aufruf - nicht vorher - erkannt und der
passende Task gestartet.
Dieser zeigt den ursprünglichen IrqCount (beim Setzen des Signals) und den aktuellen IrqCount.
FIQ läuft ja unabhängig von CoopOS weiter!
Der Vergleich zeigt: die Reaktion erfolgt innerhalb eines IrqCount oder weniger, also innerhalb
von 500 Nanosekunden:
FIQ setzt Signal -> Scheduler erkennt Signal -> Scheduler startet wartenden Task

Weiterhin wird die Zahl IrqTicks der externen 1 MHz Interruptquelle, die von FIQ gezählt wurden,
angezeigt.
Diese Zahl steht wie eingemeißelt bei 1000.000.
Als Tick wird jede Veränderung einer Flanke gezählt.
Bei jeder ansteigenden Flanke wird des äußeren Pin-Signals wird vom FIQ ein Signal
taskSetSignal(sig) an den Scheduler gesendet. Dieser ruft den passenden "Interrupt"-Task, also
eine virtuelle Interruptroutine, auf.
Dieser Task (nicht FIQ) inkrementiert die Variable IrqTasks.
Das passiert 500000 mal pro Sekunde!



1.2.9                Änderungen an einer Variablen PinValue vornehmen (Task 5)

CoopOS startet einen Task, der alle 5µs eine interne Variable PinValue invertiert (true/false).
Das passiert 200.000 mal, also 1 Sekunden lang. Dann wird eine Pause von einer
Sekunde eingelegt. Das Erkennen der Veränderung ist Aufgabe von FIQ.
Die Pause von einer Sekunde wird gemacht, um in der Anzeige (1.2.10) zu sehen, dass
genau 200.000 Änderungen gesehen wurden.



1.2.10                Auf Änderungen an einer Variablen PinValue reagieren (Task 6)

FIQ erkennt Änderungen an PinValue - verursacht durch Task 5 - und ruft bei JEDER
Veränderung per Signal Task 6 auf.
Die Zahl der Veränderungen von PinValue (PCount) werden 5x pro Sekunde ausgegeben.
Da immer eine Sekunde mit 200.000 Wechseln und 1 Sekunde Pause abwechseln, wird
die Anzeige jede Sekunde um genau 200.000 erhöht, die dann eine Sekunde stehen bleibt.
Eine verpasste Änderung wird so sofort erkannt. Stehende Anzeige von xxx99999 oder weniger
wäre die Folge.

Task 6 misst JEDESMAL die Latenz zwischen Signal durch FIQ (400.000 pro Sekunde) und
der eigenen Reaktion von Task 6. Die Maximale Latenz, die über die gesamte Laufzeit
des Programms gemessen wurde, wird als MAXLAT ausgegen.

Die Maximal Latenz wird in MAXLAT gespeichert und gilt seit Programmstart.
Sie beträgt hier eine Mikrosekunde !



1.2.11                Darstellung der Veränderungen von PinValue (Task 7)

Während Tasks 6 (1.2.10) die Änderungen der Variablen registriert, ist Tasks 7 für die Anzeige
zuständig. Dazu wird der Wert 10x pro Sekunde ausgegeben.
Man kann die Messungen verfolgen: 1 Sekunde schnelles Zählen, eine Sekunde Pause.
Es ist leicht zu sehen, dass diese Zahl nach 2 Sekunden für eine Sekunde stagniert - wie
vorgesehen.
..000 zeigt, dass keine Veränderung übersehen wurde, d.h. dass wirklich für jeden
"Interrupt" (400.000 pro Sekunde) der Task via Signal und CoopOS gestartet
wurde.
Die Latenz: Senden des Signals von FIQ bis Ausführung des mit TaskWaitSignal wartenden Tasks
durch CoopOS wird gemessen: MAXLAT

Diese Latenz betrug auch nach über 49 Stunden nicht mehr als 1 Mikrosekunde !!!


images/19-6.png






1.2.12                 Test

Dieser Task ist für Testzwecke gedacht. Z. Beispiel, um zu sehen, wie sich langfristig rechnende
Tasks verhalten.
PrimTest berechnet Primzahlen auf sehr umständliche Weise. Er ist lediglich ein Beleg
dafür, dass auch Tasks sehr lange oder ohne Ende zusätzlich im Hintergrund arbeiten
können - ohne das System in seinem Echtzeitverhalten nennenswert zu stören.
Die maximale Ausgabe sind 5 Primzahlen pro Sekunde.
Der Task wird immerhin alle 6,5 µs aufgerufen!
Mit diesem Task geht MaxLat auf 2 µs.



Insgesamt sind neben den 2.000.000 FIQ-Interrupts mehr als 20 Task an dem Programm Demo-1 beteiligt.

Die Geschwindigkeit und genaue deterministische Reproduzierbarkeit  der Ergebnisse wird bisher wohl
nirgendwo erreicht.







2                   Neues Paradigma



2.1                RTOS vs CoopOS

Bei den üblichen RTOSs wird mit einem Timerinterrupt, der in der Regel alle 1000 µs erfolgt,
einem laufenden Task die CPU entzogen und einem anderen Task mit gleicher oder höherer
Priorität zugeordnet. Der Zustand des unterbrochenen Tasks ( CPU Reginster, Stackpointer usw.)
wird vollständig gespeichert und später wieder hergestellt. Der unterbrochene Task "merkt" nicht,
dass er unterbrochen wurde.
Bei 10 gleichberechtigten Tasks bedeutet das, dass jeder Task 100x pro Sekunde aufgerufen wird.
Jeder Task - aus der Sicht des Programmierers - arbeitet, als wenn er die CPU alleine hätte -
nur 10x langsamer.
Die Millisekunde ist dabei die zentrale Einheit. So kann sich ein Task für n Millisekunden
"schlafen" legen.
Komplizierter wird es, wenn die Tasks miteinander kommunizieren müssen. Der Zugriff auf
gemeinsame Variablen bedarf besonderer Vorkehrungen, um zu vermeiden, dass ein Task
gerade beim Schreiben unterbrochen wurde, während ein anderer Task nach dem Umschalten
darauf zugreifen möchte.

RTOS soll nicht besonders schnell sein - so kann man überall nachlesen - sondern deterministisch,
also vorhersagbar in seiner Ausführung.

Dies führt in heutiger Zeit oft zu dem von mir so genannten "RTOS-Dilemma". Erst zum Taskswitch
- also im Extremfall nach einer ms - wird festgestellt, dass jetzt ein Task mit höherer Priorität laufen
muss. Dies kann ja vom Scheduler immer erst festgestellt werden, wenn ein Taskswitch anliegt.

Natürlich ist dies oft viel zu langsam. Interrupts sollen es regeln. Auf ein äußeres Ereignis wird
möglichst schnell mit dem Aufruf einer Interrupt-Routine reagiert, die alle laufenden Prozesse
unterbricht. Das nützt aber nichts, wenn diese Interrupt-Routine nur einen Task mit höherer Priorität
auf READY schaltet,
denn auch dann dauert es bis zu 1 ms, bis der Scheduler auf diesen Task umschaltet.
Die Interrupt-Routine muss vielmehr in der Lage sein, auch einen sofortigen Taskswitch zu
veranlassen.
Das bringt aber den Scheduler aus dem Takt - jetzt wird er nicht mehr nur jede ms aufgerufen,
sondern - bedingt durch den Interrupt - öfter. Eine Task-Umschaltung genau an der ms-Grenze ist
nicht mehr gewährleistet, denn es kann ein Interrupt dazwischenkommen.
Auch die Interrupt-Routine kann sich nicht mehr darauf verlassen, immer exakt nach gleicher Zeit
aufgerufen zu werden: Der Scheduler könnte gerade einen Taskswitch vornehmen und dabei
zwangsweise - wenn auf kurzzeitig - den Intrerrupt verbieten.
Weiterhin gibt es viele Tasks, die nicht im Stück eine Zeit von 1 ms für ihre Aufgabe benötigen.
Sie geben vorzeitig zurück an den Scheduler mit z. Bsp. einem freiwillig ausgelösten
- kooperativen - YIELD.
Auch das führt zu nicht im ursprünglichen Konzept vorgesehen gleichmässigem Taskwechsel.

In der Tat ist es in der Praxis inzwischen oft so, dass ein RTOS in einem System NIE einen
Taskwechsel im ms Takt ausführt, weil es ständig auf die beschriebene Weise zu vorzeitigen
- kooperativen - Taskwechseln veranlasst wird.
So gesehen verschwimmt der Untersschied zwischen einem kooperativen und preemptiven
System.
Das RTOS garantiert zwar weiterhin, dass eine Taskwechsel unter allen Umständen nach 1 ms
passieren WÜRDE, wenn ein Task die CPU nicht rechtzeitig freiwillig abgibt. Das ist aber der
einzige Unterschied.
Und ein kooperatives System kann Vorkehrungen treffen, um ähnliches zu erreichen.

Der oft beschriebene Gegensatz zwischen "cooperative" und "preemtive" ist in der Praxis meist
nicht mehr auszumachen!

Wenn kooperatives Verhalten aber nicht - wie bei RTOS - ein Option ist, sondern die einzige
Möglichkeit, einen Taskswitch herbeizuführen, dann ergeben sich auch viele Vorteile.
Denn anders als eine Task in einem RTOS kann ein Task in einem kooperativem System davon
ausgehen, dass er auf alle gemeinsamen Variablen zugreifen kann, da er nicht unfreiwillig
unterbrochen wird.

Eine Ausnahme ist auch hier natürlich die Interrupt-Routine.
Deshalb kann es sehr vorteilhaft sein, nicht für alle Ereignisse eine Interrupt -Routine zu benutzen,
sondern nur eine einzige, die besonders häufig aufgerufen wird und dabei ALLE äußeren Ereignisse
registriert und dem Scheduler nur mitteilt, welcher Task (welche Tasks) zur Bearbeitung der
Ereignisse ausgeführt werden soll - mit höchster Priorität.

Eine solche Reaktion wird - so ist im Extremfall anzunehmen - um die längste Zeit verzögert, die
ein Task für sich zwischen kooperativen Taskwechseln für sich in Ansprúch nimmt!
Daher ist es in einem wirchlich kooperativen System notwendig, diese Zeit so klein wie möglich zu
halten.
Viele Bibliotheksfunktionen wie ein printf mit mit Formatierung und Umleitung auf z. Bsp. eine
serielle Schnittstelle nicht einsetzbar und müssen durch eigene schnelle und kooperativen Routinen
ersetzt werden.

Dies scheint eine erhebliche Einschränkung darzustellen - ist aber bei einem schnellen RTOS
genauso.
Denn sonst kommt man dort auch wieder auf Verzögerungen, die nur noch durch Interrupts
unterbrochen werden können.
Und bei einem RTOS kommt es zu weiteren Schwierigkeiten:

1)                 Vorkehrungen für "reentrent"
Was passiert, wenn ein Task eine (Bibliotheks-)Funktion F aufruft, diese aber unterbrochen wird und
ein anderer Task ruft ebenfalls F auf, bevor das F des ersten Tasks beendet wurde?
Das kommt beim kooperativen System nicht vor.

2)                 Vorkehrungen beim Zugriff auf Resourcen
Der Fall: 2 Tasks geben Informationen auf dem Bildschirm aus. Damit die Ausgabe nicht zerstückelt
wird,
müssen sich bei einem RTOS die zwei Tasks darüber einigen, wer wann Ausgaben machen darf. Sie
"reservieren" also jeweils den Bildschirm für sich, um ungestört eine Ausgabe zu machen. Der
2. Task wartet ggf. darauf, dass diese Resource wieder frei geschaltet wurde.
Bei einem kooperativen System "weiß" jeder Task, dass er bei der Ausgabe auf den Bildschirm die
vollständige Kontrolle hat. Kein anderer Task kann ihn unterbrechen. Er gibt die Kontrolle erst an den
Scheduler zurück, wenn er seine Ausgabe beendet hat. Besondere Reservierungen sind nicht nötig.
Der "Kampf um die Resourcen" kann bei einem RTOS einen erheblichen Teil beim Programmier-
aufwand
und auch bei der Ausführung der Tasks darstellen!



3                 Vorkehrungen für schnelle zeitliche Abläufe

Nehmen wir als Beispiel einen handelüblichen Distanzsensor per Ultraschall. Dieser wird zu Messung
von einem Impuls von 5µs Länge angestossen. Das Echo löst eine Pegelwechsel auf der Antwort-
leitung aus.
1ms bedeutet entspricht dabei einer Wegstrecke von 330 mm - is also als Zeittakt ungeeignet.
Bei einem RTOS habe ich bereits die Schwierigkeit beim Start: ein Delay(5µs) ist mit Taskwechsel
nicht vorgesehen.
Es entsteht also bei jeder Messung eine Pause von 5µs, in der jeder Interrupt - also auch der Timer,
der für Taskwechsel zuständig ist!
Bei dem hier vorgestellten Demo können in dieser Zeit andere Tasks aktiv werden. Bei bis zu 6
Taskswitches pro µs kann in dieser Zeit also durchaus etwas erledigt werden. Die Granularität bei
der Zeitmessung beträgt 200ns.
Dann die Messung des Echos:
Wird bei einem RTOS einen Intrrupt auslösen müssen. Die Interrupt-Latenzzeit muss berücksichtigt
werden.
Fehler ergeben sich, wenn dieser Echo-Interrupt vom Timer-Interrupt des RTOS unterbrochen oder
verzögert werden kann.

Als häufiger Unterschied wird oft dargestellt, dass ein RTOS mehr Sicherheit bietet, weil bei jedem
Taskwitch auch der komplette Kontext, wie Stackpointer, CPU-Register usw. umgeschaltet wird -
bei einem kooperativem System nicht.
Das kann, aber muss kein Unterschied sein! Auch ein kooperatives System kann durchaus, wenn
gewünscht, mit dieser Kontext-Umschaltung arbeiten! Im Gegensatz zu einem RTOS geht es aber
auch ohne.
Auch bei einem RTOS wir die vollständige Sicherheit nur mit dem Einsatz einer MMU gewährleistet.
Auch das ist bei einem kooperativem System möglich.

Die heute immer noch als Referenz angesehene Taktzeit von 1ms ist ebenfalls ein alter Zopf. Er ist
traditionell bedingt.
In alten langsamen Mikrocontrollern belastete die Kontextumschaltung den Prozessor sehr.
Moderne Microcontroller wie der Raspberry Pi 3+ schaffen - auch bei einem RTOS -
6 Kontextumschaltungen pro Mikrosekunde. Es ist also an der Zeit, sich von dem 1ms Zeittakt
zu verabschieden.

Das hier besprochene Demo-Programm wurde mit und ohne Kontext-Umschaltung ausprobiert!
Mit Kontext-Umschaltung ist es zwar etwas langsamer, aber es geht vielleicht um 30% verlängerte
Reaktionszeiten - mehr nicht!


In dem hier vorgestellten CoopOS-System gibt es keine Interrupts ausser dem alle 500ns
stattfindendem FIQ-Interrupt (Fast Interrupt). Dieser erkennt das Echo mit einer Genauigkeit von
500ns, ohne dass dafür ein extra Interrupt ausgelöst werden müsste. Die festgestellte Laufzeit wird
an einen normalen Task übergeben.
Wenn dies mit einem Signal geschieht, sogar von maximal von 1 Mikrosekunde!
Der Code für diese Aufgabe sieht dann so aus:
Startsignal Ultraschallsensor setzen, Startzeit merken
taskWait(5µs)
Startsignal Ultraschallsensor löschen
A - Mit "WaitSignal" au Echo warten, (Task gestoppt, Scheduler macht weiter)
FIQ erkennt Echo-Signal
FIQ informiert Scheduler
Scheduler ruft den Ultraschalltask wieder auf (1µs nach Ankuft des Echos)
Ein vorher geblockter "Rechen-Task" wird gestartet
Dieser berechnet aktuelle Zeit - Startzeit und errechnet Laufzeit / Abstand

Signale werden immer mit höchster Priorität bearbeitet. Selbst zwei gleichzeitige Signale werden,
wenn das System gut organisiert ist.

Bei einem kooperativen Betriebssystem ( CoopOS ) einscheidet jeder Tasks selbst, wann er bereit
ist, zwischenzeitlich auf die CPU zu verzichten, um einen Taskswitch zuzulassen.

Methode 1 (ohne Kontextumschaltung)

Dabei speichert der Task selbst die Zeilennummer des Codes ab, bei dem er den Taskswitch erlaubt
hat, um dann später dann genau dort weiterzumachen.
Jeder Task arbeitet also als eine Art State-Machine!

Vorteil:
Schneller geht es nicht!
Nachteile:
Jeder Task muss seine lokalen Variablen als static anlegen.
Jeder Task ( wenn er mehrfach aufgerufen werden soll) benötigt die Konstruktion

int Task ( void *Parameter) {

beginTask()
while(1) {
...
taskSwitch(); (oder: taskDelay(µs), taskWaitSignal(signal), ...)
...
taskSwitch(); (oder: taskDelay(µs), taskWaitSignal(signal), ...)
...
}
endTask();
}

Zuvor muss jeder Task initialisiert werden:

InitTask(Name, Function, State, Delay_us, Priority, WaitSignal, Parameter);


Methode 2 (Mit Kontextumschaltung)


Hier speichert der Scheduler alle Informationen. Ein Task selbst benötigt keine weiteren
Vorkehrungen. Selbstverständlich kann auch ein kooperatives System wie ein RTOS alle
Parameter wie Stacklocation, Stacksize, Register usw. vor einem Taskswitch sichern.

Der einzige Unterschied zu einer "normalen" Funktion:
Er muss lediglich über den Scheduler gestartet worden sein.
Hier hat ein Task "echte" lokale Variablen.

Ganz formal entspricht dieses Vorgehen einem RTOS. CoopOS-Tasks und RTOS-Tasks sind so leicht
ineinander zu transformieren. Ein RTOS, dass aus Zeitgründen oft genug eineYIELD() aufruft ist
weder im Programmtext noch in seiner Funktionalität von einem CoopOS-Task zu unterscheiden.
Bis auf feine Unterschiede:
delay(5µs) lösen bei einem RTOS keinen Context-Switch aus - bei einem CoopOS schon.


Das Hauptargument gegen kooperatives Multitasking ist, dass ein Task, der sich nicht kooperativ
verhält, das ganze System anhalten kann. Das stimmt!
Aber anders, als bei einem Betriebssystem, wie Windows oder Linux, werden auf einem
Microcontroller nicht irgendwelche Programme aus fremden Quellen laufen. Das System ist eine
black box mit genau definierten Aufgaben, die durch einzelne vordefinierte Tasks erledigt werden.
Der (die) Programmierer ist (sind) bei CoopOS in der Tat dafür verantwortlich, die Taskswitches
häufig genug einzuleiten.

Als weiteres Argument wird oft genannt, dass ein kooperatives System in seinen Ergebnissen
weniger deterministisch (vorhersehbar) ist. das ist falsch, wie das vorliegende
Demonstrationsprogramm beweist! Bei sauberer Programmierung und zeitlicher Kontrolle gilt eher
das Gegenteil!



2.2                Ein- / Ausgabe

Bei einem RTOS ist die Ausgabe auf z.Bsp. eine serielle Schnittstelle - aus Sicht des
Programmieres - unproblematisch. Die Befehle wie printf(...) können bei einem Taskswitch
unterbrochen werden.
Bei CoopOS muss die Ausgabe anders gestaltet werden. Im Ergebnis müssen alle
Ausgaberoutinen in Einheiten zerlegt werden, die in kurzer Zeit (angestrebt: weniger als 1
Mikrosekunde) abzuarbeiten 3333333333333333333333333333sind.
Dies ist ein wesentlicher Bestandteil von CoopOS! Eine spezielle Routine zur Umwandlung von
Zahlen in Strings wird ebenfalls benötigt.
Die gepufferten Ausgaben werden in einem speziellen CoopTask in einen gleichmässigen
Zeichenstrom verwandelt.
Es wird mit 500000 Baud gearbeitet. 50 Zeichen pro Millisekunde sind also möglich.
CoopOS sendet - wenn vorhanden - alle 100µs ein Zeichen - also vergleichsweise langsam. Die
Zeichen können direkt in die serielle Schnittstelle geschrieben werden, ohne auf Überlauf prüfen zu
müssen.
Dies macht den Ausgabetask extrem schnell.
Vorher wurden alle Augaben zwischengepuffert.
So kann garantiert werden, dass alle Ausgaben nie mehr als 1µs im Stück in Anspruch nehmen.
Beispiel: Die umwandlung einer Zahl in einen Text, der ausgegeben werden kann, dauert 160ns.



2.3                Interrupts / FIQ

Der Fast Interrupt Handler (FIQ) wird alle 500ns ausgelöst. Er ist nicht nur die zentrale
Zeitreferenz, sondern überwacht auch Input-Pins oder Variablen des Systems und sendet ein
"Signal" an den Scheduler. De facto versetzt er den Zustand eines auf ein Signal wartenden Tasks
direkt in READY - ohne die Mitwirkung des Schedulers.
Der Scheduler erkennt beim nächsten Aufruf, dass der entsprechende Task mit höchster Priorität
zu starten ist.
Der Fast-Interrupt (FIQ) trägt seinen Namen zu Recht! Vom Zustandswechsel eines Pins bis zur
Reaktion innerhalb eines "normalen" Interrupts beträgt die Latenzzeit gut 500 ns.
Der FIQ erreicht den gleichen Zustand in 125ns.
Vorteile:

• Schnellere Reaktion möglich - fast schon vergleichbar mit TTL-Logik!
• Mehrere Pins parallel abfragen und adäquat reagieren.
• Es kann mit immer gleichen Unterbrechungszeiten gerechnet werden.

Auf ein Signal reagiert CoopOS zuverlässig innerhalb einer Mikrosekunde - wenn dafür gesorgt wird,
dass keine Task länger als eine Mikrosekunde arbeitet, was bei diesem Demo-Programm üer viele
Stunden nachgewiesen wurde.

Der FIQ kann leicht 50% oder noch mehr der CPU-Zeit beanspruchen. In der Tat ist dafür zu
sorgen, dass der FIQ nicht überlastet wird. Hierfür sind Messungen z. Bsp. mit einem Oszilloskop /
Logic Analyzer oder genaue Berechnungen notwendig!
Zeigt es sich, dass alle Aufgaben des FIQ nicht in einer Zeit von 500ns zu erledigen sind, so gibt es
zwei Möglichkeiten:
Pro FIQ nur einen Teil der Aufgaben erledigen. Etwa nur die Hälfte. Dann würde je Aufgabe im FIQ
"nur" jede Mikrosekunde eine Abarbeitung stattfinden.
- oder -
gleich die Taktzeit des FIQ auf z. Bsp. 1µs "herabsetzen". Aber eine Million Interrupts pro Sekunde
ist auch noch sehr brauchbar!

Diese Herangehensweise der "virtuellen" Interrupts gegenüber herkömmlichen Interrupts ausgelöst
durch den FIQ hat sich als schneller, zuverlässiger und besser skalierbar herausgestellt.
Linux preempt_RT geht ähliche Wege.

Im vorliegenden Beispiel beträgt die Verweildauer im FIQ etwa 235 ns (von 500ns pro Zyklus).



2.4                Scheduler


Funktion des Schedulers:

images/19-3.png


Der Scheduler ist die zentrale Instanz, die die Tasks aufruft und zu der die Tasks freiwillig
zurückkehren. Sehr häufig geben dies Tasks an, wann bzw.
nach welcher Zeit sie wieder geweckt werden möchten. Diese Zeiten werden vom Scheduler
verwaltet. Der Scheduler läuft in einer Schleife und ist völlig unabhängig vom FIQ.
Er benötigt allerdings eine sehr genaue Zeitmessung für seine Aufgaben. Dies kann eine beliebige
Zeitquelle sein.
Hier wird ein interner Timer benutzt, der alle 100ns inkrementiert. Der Scheduler schafft bis zu
10.000.000 Taskswitches pro Sekunde.

Nach der Umstellung von CISC auf RISK Prozessoren konnten manche Aufgaben besser und
schneller durchgeführt werden.
Hier ist es - auf Software bezogen - ähnlich. Statt eines komplexen RTOS-Schedulers ist der von
CoopOS verwendete Scheduler geradezu lächerlich einfach.
Er wurde mit der Prämisse entwickelt: Schnell, schnell, schnell - und hochgradig deterministisch.

Der Scheduler kennt zwei mögliche Zustände, zwischen denen bei der Programm-Entwicklung
entschieden werden muss :



2.4.1                 ROUND_ROBIN

Der Scheduler geht alle vorhandenen Tasks durch und schaut nach, ob ein Task READY geschaltet
werden muss oder bereits ist. Dieser Task wird gestartet.
Dann setzt der Scheduler die suche fort, bis alle vorhandene Tasks geprüft wurden.
Alle Tasks sind so gleichberechtigt.




2.4.2                START_NEW_AFTER_EXECUTE

Auch hier beginnt der Scheduler mit dem ersten initialisierten Task. Wird ein Task - z. Bsp.
Task 3 - ausgeführt, weil er READY war oder jetzt gesetzt wurde, so beginnt der Scheduler
bei der Suche wieder bei Task 1.
Die zuerst initialisierten Task haben also höhere Priorität. Sie könnten dafür sorge, dass weitere
Tasks nie ausgeführt werden, obwohl sie READY sind.
Sind bei "Round Robin" alle Tasks READY, so werden sie nacheinander ausgeführt. Bei "High
Priority".



2.4.3                SIGNALE

Tasks, die auf Signale warten, werden in beiden Fällen mit höchster Priorität bearbeitet, sowie
die Signale gesetzt werden. Es werden vom Scheduler diese Tasks vor allen anderen gestartet.
Sie sind in erster Linie zur schnellen Weiterleitung von Ereignissen, die in Interrupt-Routinen erfasst
wurden, an den Scheduler gedacht, der mit höchster Priorität "Pseudo-Interrupt-Tasks" aufruft.


2.4.4                IDLE

Sehr zeitkritische Abfragen - z. Bsp. von extern verursachten Pegeländerungen an bis zu 54 Pins
werden in FIQ abgefragt. FIQ setzt Signale, auf die innrehalb einer Mikrosekunde reagiert werden
kann.
Allerdings reicht es oft, wenn die Reaktion innerhalb einiger Mikirosekunden erfolgt.
Idle() wird vom Scheduler immer dann aufgerufen, wenn kein Task READY ist und arbeiten kann.
Idle() ist, wenn man so will, das Zeitgrab für Zeiten ohne Aufgaben.
Idle() wird bis zu 3000.000x pro Sekunde aufgerufen - allerdings im vorliegenden Beispiel mit Pausen
von bis zu 30 Mikrosekunden, weil eben doch etwas zu tun war.
Aber für viele Vorgänge ist es schnell genug, denn Idle() verbraucht keine kostbare Taskzeit, die im
Scheduler verwaltet werden muss!

Selbst bei 1.500.000 von FIQ an den Scheduler ausgesandten Signalen - jeweil mit Aufruf von einem
Task - kommt das System noch auf 1.600.000 Idle-Calls in 5 Sekunden, also im Schnitt auf einen
Idle-Call alle 3µs.

Idle() kann ebenfalls die Input-Pins überwachen.

Ein Beispiel:
Direkte Ansteuerung eines Displays und Eingaben von einer externen Tastatur.

Auch bei Auslastung des Systems erfolgt die Erkennung nach spätestens einer Zeit X, die hier bei
ca. max 30 Mikrosekunden für 31 zu überwachede Pins.
In der Praxis konnten 10000 Zustandswechsel problemlos und garantiert ohne Verluste erkannt
werden.

Wohlgemerkt ohne Interrupts und ohne Belastung des laufenden Systems!



3                 Demo 2 - Taskswitching mit Stackumschaltung

Mit einem kooperativen Betriebssystem kann ohne die bei einem RTOS notwendig Taskswitch-
Prozedur, bei der der komplette CPU-Inhalt vor dem Taskswitch gespeichert wird, gearbeitet werden.
Gerade das macht das System so schnell.
Aber natürlich ist es auch möglich, kooperativ das Taskswitching mit einem Speichern aller Register
zu verbinden.
"circle" einhält bereits als addon die die Klasse für Tasks und einen kooperativen Scheduler mit
Stackumschaltung bereit - eine gute Grundlage, um CoopOS auch damit auszuprobieren.

Ein weit verbreteter Irrtum ist es, dass ein Task, der sich nicht kooperativ verhält, zwangsweise das
ganz System anhält.
Das muss nicht sein!
Im FIQ kann alle 500ns geprüft werden, ob sich das System normal verhält. Wenn nicht - z. Bsp. weil
die Scheduler-calls nicht mehr hochgezählt werden, kann FIQ - ohne Scheduler! - einen Watchdog-
Task starten.
Dieser Watchdog-Task kann

• den fehlerhaften Task stoppen
• den Scheduler wieder starten
• einen weiteren Task aufrufen, der eine genaue Analyse des Fehlverhaltens vornimmt

In Demo 2 feuert der Watchdog-Task alle 20 Sekunden, um die Auswirkung des Watchdog zu testen.



Da auch hier viel mehr als eine Million Taskswitches durchführbar sind, wurde der Timer so
eingestellt, dass er 5 Ticks pro Mikrosekunde liefert, also alle 200ns.

Die Aufgaben:
FIQ bekommt 2000000 Interrupts per externem Signal.
FIQ soll:
• Jeden 4. Interrupt über den Scheduler an einen passenden Task per Signal weiterleiten
(500000 pro Sekunde),
der diese Interrupts dann im CoopOS-System verarbeitet.
• An einem weiteren Pin eine Zustandsänderungen erkennen und an einen passenden Task per
Signal weiterleiten.

• Diese Zustandswechsel erfolgen alle 2µs, also ebenfalls 500000 pro Sekunde.
Von einem Task soll der Systemzustand jede Sekunde erfasst und ausgegeben werden.
Ein Ausgabe-Task übernimmt die zwischengepufferten Ausgaben zeichenweise.

• Ein Stress-Task sorgt in einem Loop für zusätzliche Arbeit. Dieser Task inkrementiert einen
Zähle führt dann einen Taskswitch (Yield) mit einer Pause von 1 Mikrossekunde aus - startet
also erwartungsgemäß alle 2 Mikrosekunden.



Also: 2.000.000 ausgelöste Echte Interrupts, 1.000.000 durch FIQ angestoßene Aufrufe von Tasks,
Ausgabe auf den Bildschirm, "Stress-Task"

Ergebnis:

images/19-7.png
Bild: Watchdog feuert alle 20 Sekunden und schaltet zwangsweise auf den nächsten Task.
Das System läuft problemlos weiter und hält die Zeitvorgaben ein!

TaskSwitches Jeder Taskwechsel wird gezählt - ca. 1,6 Millionen Taskswitches pro Sekunde
Real FIQ IRQs Anzahl der von aussen erkannten FIQ Interrupts zur Zeit "at" in Mikrosekunden
FiqCount: Erkannte weiterzuleitende Interrupts in FIQ (Soll)
FiqIrqCount: Tatsächlich erfolgreich durchgeführte Task-Calls dafür
IrqCount: Erkannte weiterzuleitende Interrupts am 2. Pin in FIQ (Soll)
IrqIrqCount: Tatsächlich erfolgreich durchgeführte Task-Calls dafür
StressCount: Task, der ca. alle 2µs aufgerufen wird.
Idle -calls: ca. 400000 pro Sekunde.
Der Watchdog-Timer erzwingt einen Taskwechsel alle 20 Sekunden

Die Drift der auf eine Mikrosekunde genaue Ausgabe ist minimal.

Die Latenz zwischen äußerem low/high-Wechsel am den dem FIQ-Intrerrupthändler
zugeordneten Pin beträgt ca. 100ns, die Verweildauer im FIQ-Interrupt-Handler beträgt
ebenfalls ca. 100 ns.

Der Start eines Tasks - initiert durch den FIQ-Handler - durch den Scheduler geschieht in weniger
als 1 Mikrosekunde.


Also auch mit Sicherung der Register und Stackumschaltung dürfte hier die System-Performace alle
bisherigen handelsüblichen Microcontroller in den Schatten stellen!

Besonders hervorzuheben ist die 64-Bit Arbeitsweise, die z. Bsp. dafür sorgt, dass die 100ns-Ticks
mehr als 8000 JAHRE gezählt werden können, ohne dass es einen Überlauf gibt.



4             Demo 3 - Serielle Ausgabe mit 115200 Baud mit 10 Tasks

Ausgangsbedingungen:
Es gibt immer noch 2 Millionen echte Interrupts pro Sekunde - die im FIQ behandelt werden.
FIQ dekodiert immer noch 500000 "Interrupts" auf einem 2. Input Pin.
Es laufen zur CPU Belastung:
1) Der Stress-Task
2) Ein Task zur Primzahlen-Berechnung als Langzeit-Task

Natürlich laufen auch die Tasks des Ringpuffers, der Task für die zeichenweise Ausgabe usw.

Das besondere hier:
Es wird eine serielle Ausgabe an einem Pin mit 115200 Baud vorgenommen - ohne Interrupts, ohne
CPU-Zeit fressende Loops.

Vielmehr gibt es 10 Tasks, in Wartestellung sind und auf Signale warten. Jeder Task ist genau für
ein serielles Bit zuständig.

void StartBit::Run() {
while(1) {
SCHED.WaitSignal(10);
Digital_Write_Clr(GPIO_OUTPUT_PIN);
SCHED.usSleepHigh(33);
SCHED.SetSignal(11);
}
}

void Bit1::Run() {
while(1) {
SCHED.WaitSignal(11);
(Byte&(1<<0)) ? Digital_Write_Set(GPIO_OUTPUT_PIN) : Digital_Write_Clr(GPIO_OUTPUT_PIN);
SCHED.usSleepHigh(33);
SCHED.SetSignal(12);
}
}

.....
µsSleep ist etwas irreführend, denn es geht um 100ns Ticks, also 10 pro µs. Bei 115200 Baud
benötit ein Bit 8.6µs - es ist also mit einer Granularität von 1µ gar nicht herzustellen!
Die Zeit für SetSignal ist zusätzlich (empirisch ermittelt) zu berücksichtigen.

Die Sende-Routine sendet die Zeichen mit 20µs Pause nach dem Stopbit neu:

Byte=' ';
while(1) {
SCHED.usSleep(10*20); // Wait 20 µs
Byte++;
SCHED.SetSignal(SEND_BYTE); // Send Byte
SCHED.WaitSignal(BYTE_SENT); // Wait Byte sent
if (Byte=='z') Byte=' ';
}


Es geht dabei nicht in erster Linie um das die Ausgabe von Zeichen auf einem extra Pin, sondern um
herauszufinden, ob das System auch unter erheblicher Belastung alleine durch das Senden von
Signalen in der Lage, zuverlässig Tasks im Abstand von 8,7 µs zu starten.
Serielle Empfänger-Bausteine sind sehr empfindlich, was das Timing betrifft. Abweichungen zeigen
sich sofort mit der Ausgabe von falschen Zeichen, wenn das Timing nicht stimmt.

Hier das Ergebnis:
(Ausgabe "normaler" serieller Port)
images/19-8.png
Also wie erwartet 2000000 FIQ-Interrupts durch externes Signal, 500000 "Interrupts" (durch FIQ
dekodiert) auf dem 2. Irq-Pin. Der Stress-Task wurde ca 118000 mal pro Sekunde aufgerufen
und mehr als 9000 Zeichen/s , also ca. 90000 Bits über die neu geschaffene serielle Ausgabeleitung
gesendet.

Die Ausgabe ist fehlerfrei:
images/19-9.png



Bei 115200 Baud müssen alle Interrupt-Antworten in FIQ selbst ausgeführt werden - der weiterhin
2 Millionen Mal aufgerufen wird. Das Durchschleifen von Signalen durch den Scheduler, um Interrupt-
Tasks aufzurufen, ist so nicht mehr möglich.

Aber mit einer Reduzierung auf 57600 Baud und einer äußeren Interrupt-Quelle von 500000 Interrupts
funktioniert auch das Aufrufen einers Intrerrupt-Tasks zuverlässing.

In beiden Fällen läuft der Task "Stress" mit niedrigster Priorität und wird im 2. Fall immerhin noch
80000 Mal pro Sekunde aufgerufen.
Weiterhin läuft ein Programm "Primzahlen" im Hintergrund.
Der Idle-Task wird 2.500.000 Mal pro Sekunde aufgerufen - ideal, um nahezu verzögerungsfrei Daten
in ein Schieberegister zu bringen, das wie ein virtueller Port behandelt werden kann.
In weniger als 10 µs ist so eine Speicherstelle des Microcontrollers im Schieberegister gespiegelt,
ohne dass ein Task dafür etwas tun muss. Für viele Steueraufgaben ist das ausreichend.


Noch einmal: Die serielle Ausgabe besteht aus 10 Tasks, die sich nacheinander durch Signale
aufrufen! In der Wartezeiten innerhalb einer Bitlänge verbrauchen die Tasks keine Prozessor-
zeit.

Die Simulation der seriellen Ausgabeleitung ist nur ein "proof of concept", um zu belegen, dass mit
den neu vorgestellten Methoden reproduzierbar Vorgänge durch kommunizierende Tasks bis hin
zu Bruchteilen von Mikrosekunden sauber und deterministisch dargestellt werden können - jenseits
aller in der Literatur zu findenden Ergebnisse.

Die Ausgabe von seriellen Zeichen ist ein sehr probates Mittel, um das Timing des Systems zu
überprüfen, da Fehler sofort als falsche Zeichen vom Betrachter identifiziert werden!


images/19-10.png






Anwendungen:

Dieses Beispiel demonstriert auch, mit welcher reproduzierbaren Genauigkeit z. Bsp. Sensordaten
im CAN-Bus oder bei sonstigen industriellen Systemen erfasst und weitergereicht werden können.

Bei Audio-Systemen hat sich eine Sample-Rate von 44,1kHz etabliert, das entspricht einer Taktzeit
von 22,68 µs.
Dieses System sollte das - einen entsprechend schnellen AD- bzw. DA-Wandler vorausgesetzt -
zuverlässig entsprechende Aufnahmen bzw. Wiedergaben leisten können!

Bei ein EKG-Ableitung sollen - getriggert durch das Signal - jeweils 2000 Punkte im Abstand von 1ms
(2 Sekunden) erfasst werden, um mit einer Korrelationsfunktion das typische gemittelte Signal alle
10 Sekunden auszugeben.
Dieses System verfügt über genügend Kapazitäten, um das zu leisten.



5                 Ergebnisse

Das hier vorgestellte System - Raspberry 3+ bare metal + CoopOS erfüllt Anforderungen und
Aufgaben, wie sie bisher weltweit mit keiner Kombination von Microcontrollern/RTOS auch nur
annähernd zu erfüllen wären!
Das vorgestellte System verhält sich in mancherlei Hinsicht eher wie ein FPGA, als ein
Softwareprodukt .

Die Reaktion auf die Änderung einer Variablen - was auch alle externen Signale betrifft, die ja im
Speicher an einer Adresse parallel abzulesen sind - innerhalb spätestens einer halben Mikrosekunde
unter allen Bedingungen ist besonders hervorzuheben.


Deshalb ist es nicht nur möglich, Änderungen an bis zu 32 (mit zwei Zugriffen bis 56) externen Pins
mit einem Zugriff zu erlkennen, sondern auch darauf mit priorisierten "Interrupts" zu reagieren - auf
den jeweils höchst priorisierten Interrupt bereits nach einer Latenz bis herunter zu 200ns - 300ns.
Dabei können im Raspberry bis zu 56 Leitungen parallel überwacht werden.


Im Demoprogramm-1:

Vom Starten des externen FIQ-Interrupt Pulses bis zum Start der FIQ-Interrupt-Routine vergehen
ca. 120 ns.
Die Bearbeitung innerhalb des FIQ dauert dann bei sehr begrenzten Aufgabenn bis herunter zu
165ns .

Der Prozessor ist also gut 50% seiner Zeit in der FIQ-Interrupt-Routine, die alle 500ns gestartet wird.

Es können alle Input-Pins parallel gelesen werden. Eine Reaktion kann direkt im FIQ ausgelöst
werden (2000000/s) oder in einem (oder mehreren) dazu passenden Tasks (bis zu
1000000/s).
Umgekehrt können auch alle Output-Pins gleichzeitig angesprochen werden.


Im Demoprogramm-2:

Auch in einem kooperativen System kann beim Taskswitch der Inhalt der CPU-Register, Stackpointer
usw. gespeichert werden, wie dieses Demo beweist.
Auch in einem kooperativen System kann ein Watchdog einen Task, der sich nicht kooperativ
verhält, zu einem Taskswitch zwingen!


Im Demoprogramm-3:

Selbst unter Stress ist das sehr genaue Scheduling von Tasks auf Bruchteile von Mikrosekunden
genau möglich!

Zwischen einem RTOS, dessen Tasks sich aus Zeitgründen kooperativ verhalten und dem CoopOS
ist in der Praxis an den Programmen kaum ein Unterschied zu erkennen, bis auf:
CoopOS ist schneller!


RTOSs haben sich als zuverlässig herausgestellt und haben heute eine große Bedeutung. Aber
es gibt Anforderungen, die damit nicht zu lösen sind - wenn sie in ihrem Verhalten nicht viel
kooperative gestaltet werden.

Und für den Raspberry 3+ gibt es keinen wirklich funktionierenden Port eines RTOS, so dass direkte
Vergleiche nicht möglich sind.

Aber selbst, wenn es einen gäbe, könnten damit die gestellten Anforderungen keinesfalls auch
nur annähernd erfüllt werden.
Hier kann "bare metal "CoopOS ein probates Hilfsmittel darstellen, um Unmögliches möglich zu
machen!

CoopOS ist aber auch ein sehr gutes Hilfsmittel, um Methoden zu testen, die dann auch für ein RTOS
übernommen werden können. Denn die Anforderung, jeden Task schneller als in einer Mikrosekunde
kooperativ zu beenden zwingt zu neuem Denken:

In einem RTOS würde man Bildschimausgaben, die sich gegenseitig stören könnten, durch einen
durch Semaphoren geschützten Zugriff auf den Bildschirm regeln.
Welche Probleme damit verbunden sein können, zeigte das peinliche Versagen des Bordcomputers
bei einer Mars-Path-Finder Mission: Prioritätsinversion durch einen solchen Schutz!

Bei CoopOS erzeugt jeder Task seine Ausgabestrings in einem lokalen Bereich. Pointer auf diese
Strings werden an einen Ringpuffer übergeben. Da die Tasks nicht unterbrochen werden können,
ist das ohne Schutz möglich.
Der Ausgabe-Task holt sich die Pointer aus dem Ringpuffer und gibt die Strings getaktet
Zeichenweise aus.
Auch dieser Task benötigt keine Kommunikation mit den anderen Tasks per Semaphoren, da er
ebenfalls nicht willkürlich unterbrochen werden kann.
Formatierungs-Routine müssen allerdings neu geschrieben werden, um kooperativ zu sein.
Aber so ist es möglich, auch bei grösseren Ausgabemengen das Kriterium - kleiner, als eine
Mikrosekunde Unterbrechung - für alle Ausgaben zu gewähren!

Ein RTOS ist gezwungen, immer wieder auch externe Interrupts zu verbieten, um Probleme z. Bsp.
beim Taskswitching zu vermeiden. Dies zusammen mit evt. mehreren unterschiedlich priorisierten
Interrupts machen es unmöglich, Reraktionszeiten von garantierten 200ns auf ein äußeres Signal
zu gewährleisten.

Und aus den Ergebnissen dieser CoopOS-Methoden lassen sich auch Vorhersagen für die zukünftige
Entwicklung von RTOS-basierten Systemen machen - zumindest, wenn es um kürzeste
Reaktionszeiten geht:

• Ein Task, der ohne YIELD seine Rechenzeit (üblicherweise 1ms) ausnutzt und es völlig dem RTOS
Scheduler überläßt, ihn zu unterbrechen, ist "poor design". Das gilt insbesondere für Ein- /
Ausgaben.

• Das Konzept von vielen verschiedenen Interrupt-Quellen, die den RTOS-Scheduler unterbrechen,
wird durch EINE Highspeed-Interrupt-Quelle (wie hier der FIQ) ersetzt werden, um die Interrupt-
Verarbeitung zu beschleunigen und deterministischer gestalten zu können. Auch
Linux preempt_RT geht ähnliche Wege.

• Die Umschaltung auf "Schattenregister" in von außen erzeugten Interrupts wird die Norm werden,
um die Interrupt- Reaktionszeiten deutlich zu reduzieren.

• Bei den schnellen Microcontrollern heute sind Tickzeiten von 1ms viel zu langsam, um den
wachsenden Anforderungen der Industrie gerecht zu werden.

• 64 Bit Datenbreite hat sich bei PCs längst etabliert. Auch bei Mirocontrollern wird sich diese
Entwicklung schnell wiederholen.


Ein Beispiel: In einer Speicherstelle wird ein "virtueller" Ausgabeport mit 64 Leitungen vorgehalten.
FIQ schiebt diesen Wert in Paketen von 8 Bit in ein Schieberegister mit Latch. Das dauert 160ns
pro FIQ,
also mit einem Takt von 50 MHz im 8-Bit-Burst. Für die schnellsten Schieberegister 74HC595 werden
maximale Taktraten von 100MHz angegeben. Es ist also umsetzbar - wenn auch schon an der Grenze
des heute machbaren.
Alle 64 Bit stehen in insgesamt 4µs zur Verfügung.
Tasks können direkt in den virtuellen Port schreiben, da das nur ein Speicherzugriff ist, der vom FIQ-
Interrupt nicht unterbrochen wird.
Mit diesen 64 Leitungen sollen 16 Roboter-Motoren gesteuert werden - also jeweils mit 4 Leitungen.
Jeder Motor soll im Takt von einer Millisekunde bewegt werden.
Dies ist mit den vorgestellten Methoden ohne weiteres möglich - mit einem maximalen "Jitter" von
4 Mikrosekunden, die sich allerdings nicht akkumulieren, sondern im nächsten Step automatisch
berücksichtigt (abgezogen) werden, denn der virtuelle Port wird beim Scheiberegister im genauen
Takt von 4 Mikrosekunden gespiegelt.

Eine solche Steuerung von 16 Motoren gleichzeitig im Takt von einer Millisekunde - also jeder Motor
bekommt jede Millisekunde neue Steuerpulse - dürfte sich ohne allzu große Belastung der CPU
machen lassen.

Ein weiters Beispiel wäre eine Kennfeldzündung. Wenn ein Motor mit 6000 R/M dreht, dauert eine
Umdrehung 10ms.
Ein Grad der Drehung ca. 28µs. Die hier vorgestellten Methoden wären sehr wohl geeignet, die
Zündung - abhängig von Drehzahl und diversen weiteren Sensorwerten für 6 Zylinder mit der
notwendigen Genauigkeit durchzuführen.

Es gibt heute eine Menge von Anforderungen, bei denen es auf höchste Geschwindigkeit ankommt.
Der Raspberry 3+ unter bare metal Programmierung mit 64 Bit und CoopOS ist nach meinen
Ermittlungen der derzeit weltweit schnellste Microcontroller.



6                 Ist CoopOS ein Ersatz für ein RTOS?

In manchen Fällen schon. Nämlich dann, wenn es um höchste Geschwindigkeit und Präzision geht
und die Zahl der Tasks überschaubar ist ~ max 20 Tasks. Wenn die Aufgaben der Tasks sehr schnell
zu erledigen sind oder sich in zeitlich sehr kleine Abschnitte zerlegen lassen.

Es ist aber besonders ein methodisches Hilfsmittel, um auch RTOS-Tasks besser zu organisieren und
zu verstehen.
Denn auch in einem modernen Einsatz von RTOS hat sich die Sicht geändert. Es geht kaum noch
darum, irgendwelche Tasks ohne kooperatives Verhalten in einer while(1)-Schleife laufen zu lassen
in der Annahme, der RTOS-Scheduler wird ja diesen Task alle ms unterbrechen und Tasks mit
höherer Priorität starten. Es ist zwar gut, diese Gewissheit zu haben, aber in der Praxis ist dieses
Vorgehen nicht wünschenswert, denn es greift in der Regel nut dann, wenn der Programmierer nicht
ausreichend kooperativ gedacht hat.

Auch in einem RTOS sollte ein Task mit z. Bsp. TaskYield() die Kontrolle freiwillig an den Scheduler
zurückgeben und damit einen vorzeitigen Taskswitch zu ermöglichen, wenn eine Teilaufgabe
erledigt ist.
Und der Aufwand der Synchronisation der Tasks und der Zugriff auf gemeinsam genutzte Resourcen
sollten auch da minimiert werden.

Ein Beispiel ist die Ausgabe auf den Bildschirm. RTOS-Tasks könnten den Bildschirm als Resource
reservieren, bevor sie auf den Bildschirm schreiben und dann die Resource wieder freigeben, damit
die Ausgaben nicht durcheinander geraten - ein Task weiss ja nicht, wann er unterbrochen wird.
Aber dieser "Kampf" verschiedener Tasks um die Resource Bildschirm ist unnötig.
In CoopOS schreibt jeder Task in seinen eigenern Puffer. und überträgt dann diesen Puffer an einen
Ringpuffer, der einzelne zusammengehörende Ausgaben zeilenweise speichert. Ein spezieller Task
sendet dann Zeichen für Zechen eines Strings aus dem Ringpuffer. Ist eine Zeile ausgegeben, wird
mit der nächsten gespeicherten Zeile im Ringpuffer fortgefahren.
In einem RTOS ist dieses Vorgehen natürlich genauso möglich. Damit müsste nur der Ringpuffer als
Recource kurzeitig reserviert werden. Das ist aber extrem schnell und gegenseitige Blockaden der
Task ausgeschlossen.

Bei RTOS-Systemen wird gerne die Hardware-Interrupt-Latenzzeit angegeben - die Zeit also, die das
System benötigt, um auf ein äußeres Signal mit seiner Interrupt-Routine zu antworten. Um diese
Zeit kurz zu halten, darf der Scheduler die Interrupts nur sehr kurzzeitig verbieten.
Viel wichtiger aber ist es oft, wie lange es dann dauert, um einen zum Interrupt entsprechenden
Task aufzuwecken. Und hier scheint es bisher nicht erreichbar, diese Zeit auf weniger als eine
Mikrosekunde garantiert zu begrenzen.
2 Millionen Taskswitches/s sind bei den bisherigen Microcontrollern unter irgend einem RTOS noch
Träume.

Aber CoopOS zeigt methodische Wege, um auch bei einem RTOS die Ansätze zu verbessern.

CoopOS zwingt dazu, anders zu denken. Aber die CoopOS-Programme lassen sich leicht in ein RTOS
-Programm umwandeln - wie auch umgekehrt. Ein schnelles RTOS kann die Taskswitches schnell
erledigen.
Häufigere Taskswitches als die 1000 pro Sekunde, von denen immer noch häufig ausgegangen wird,
sind inzwischen für viele Aufgaben zwingend - um viele Interrupt-Quellen zu vermeiden.

Hersteller von Microcontrollern erkennen das. So bietet RENESAS Microcontroller an, die das
Taskswitching durch entsprechende Hardware sehr beschleunigen.

CoopOS ist sehr minimalistisch und benötigt nur (auch sehr altes) ANSI-C sowie eine Methode, um
den Ablauf der Zeit in Mikrosekunden (oder kleiner) zu erfassen. Damit läuft es auf jedem modernen
Mikrocontroller bis herunter zum 8 Bit Controller mit sehr begrenztem Speicherplatz. Hier ist es oft
die einzige Möglichkeit für ein schnelles Multitasking, wenn ein RTOS viel zu speicherintensiv ist.

Das vorgestellte System ist in der Tat extrem schnell und auch skalierbar. Es verarbeitet
viele Tasks parallel und führt auf die Mikrosekunde genau zu deterministischen
Ergebnissen mit bisher unerreichter
                                                                        Präzision.












The fastest microcontroller system in the world


> 3.000.000 Interrupts/s


10.000.000 Taskswitches/s


4 Cores

1400 Mhz

1 GB RAM

ARM v8 Prozessor

64 Bit



abstract: What you can do with an 1/4 Raspberry Pi
 


In an RTOS, usually the tasks are changed every 1 ms.
Fast-to-detect operations are managed by interrupts.

The system presented here is based on cooperative multitasking with priority set to
fast and deterministic reactions.


This happens in this system in 1 ms for comparison (logic analyzer):


Only the output of characters to the serial interface every 250 us can be recognized.

More can be detected in 100 μs:


 
But you only get a real impression with a time window of 10 μs:



2 µs:


Statement:
FIQ
  An external interrupt that appears every 500 ns and is processed.
 
IRQs
  Further interrupts every 2 microseconds are detected by the FIQ and directed to a separate interrupt routine.
  Sense: detection of external interrupts and - staggered by priority - call appropriate
  Interrupt routines.
  FIQ and IRQ signals are timer controlled at 2 pins (without using the CPU)
  They are routed to the outside to two input pins that trigger the interrupts.

Task-calls
  Each pulse shows a task switch with processing of a task
 
Idle-calls
  Called if no task is READY otherwise. Can be used.
 
Variable Change
  A task changes a value every microsecond. This is checked in the FIQ every 500 ns and
  a signal is sent to a second task. Sense: checking internal signal latency,
  called MaxLat (s.b.)
 
Char Output
  Every 250 μs a character is sent, if available.

Not shown, but done in additional tasks:
Generation of a tone of 440 Hertz (digital) for acoustic control of the system
Generation of a tone of 432 hertz
Receipt (and execution) of commands via serial interface
Updating the screen

The Raspberry has 4 cores. The presented tasks run on one core, accordingly
25% of the total power of the processor.

4 MHz (with 2. core)
For testing, a second core generates a low -> high edge on a pin (output).
It is connected to another pin (input) at the outside. The second core then registers
the edge change low-high and resets the first pin back to low - a kind of handshake.
The results are sent regularly to CoopOS running in Core 1.
The consequence is more than 4000000 detected flanks per second - so in the distance of 250 ns.
This can be significantly increased with external - not self-generated - signals.



With up to 6 million task changes per second each task can be started exactly to the microsecond.


2 cores = 50% of the computing power has not yet been used!