Forum: Mikrocontroller und Digitale Elektronik Intervall Handling mit einem TImer


von Julian (Gast)


Lesenswert?

Hallo alle,

komme nicht auf einen sinnvollen Ansatz die nachfolgende Anforderung zu 
implementieren (STM32 oder NRF52).
1) Es steht nur ein Timer, kann auch Systick sein, zur Verfügung.
2) Es existiert eine Liste von Kommandos (Ausgabe per UART) die zu 
unterschiedlichen Intervallen ausgegeben werden sollen. Beide Infos 
stehen in einem Array, welches unterschiedlich lang sein kann.
zb
[1,10ms, "Mache a"]
[2,30ms, "Mache b"]
[3,20ms, "Mache c"]
usw.

Wie implementiert man sowas am besten in C. Ich brauche jetzt kein Code, 
sondern einen Ansatz.

Eine Idee die ich habe, scheint aber nicht performant zu sein. Ich 
könnte das Array um einen zähler erweitern, den ich mit dem Timer 
inkrementiere.
Nach dem inkrementieren vergleiche ich in jedem index des Arrays den 
Zähler mit dem Sollwert. Aber das scheint mir nicht optimal zu sein.
Vielleicht hat hier jemand eine bessere Idee.

Vielen Danke und Grüße,
Julian

Beitrag #7238164 wurde von einem Moderator gelöscht.
von A. S. (Gast)


Lesenswert?

Julian schrieb:
> . Ich könnte das Array um einen zähler erweitern, den ich mit dem Timer
> inkrementiere.

Das ist der sogenannte SysTicker.

> Nach dem inkrementieren vergleiche ich in jedem index des Arrays den
> Zähler mit dem Sollwert.

Das ist als erste Implementierung OK.

Bei den meisten bleibt es auch so.

Je nach Aufgabe gibt es später leichte Anpassungen. Bis hin zur 
Anordnung der Timer in einer Liste nach Fälligkeit.

Aber das besuchst Du vermutlich nie.

von Walter T. (nicolas)


Lesenswert?

Die Frage ist: Ist das Array sortiert und ist jeder Zeitpunkt nur einmal 
drin? Wenn ja, brauchst Du immer nur eine Stelle auszuwerten.

Und wenn "mache a" eine abzählbare Anzahl an Operationen ist, geht das 
mit einem enum ganz gut.

Wenn nicht, kann das beliebig kompliziert werden. Dann solltest Du am 
besten eine Sprache suchen, die Dein Problem am besten beschreibt, und 
einen Interpreter dafür schreiben.

: Bearbeitet durch User
von Stefan F. (Gast)


Lesenswert?

Julian schrieb:
> Aber das scheint mir nicht optimal zu sein.

Mir gefällt die Idee. Man kann es vielleicht sparsamer implementieren, 
aber so ist der Code gut durchschaubar.

von Julian (Gast)


Lesenswert?

A. S. schrieb:
> Je nach Aufgabe gibt es später leichte Anpassungen. Bis hin zur
> Anordnung der Timer in einer Liste nach Fälligkeit.

Das hört sich interessant an mit der Liste. In dem Moment wo zur 
Laufzeit ein neues auszuführendes Kommando kommt, muss die Liste neu 
aufgebaut werden.

Walter T. schrieb:
> Die Frage ist: Ist das Array sortiert und ist jeder Zeitpunkt nur einmal
> drin? Wenn ja, brauchst Du immer nur eine Stelle auszuwerten.

Man kann sie ja sortieren.

Stefan F. schrieb:
> Mir gefällt die Idee. Man kann es vielleicht sparsamer implementieren,
> aber so ist der Code gut durchschaubar.
Es muss alles in eine Interrupt Routine passen und fertig werden bevor 
diese wieder befeuert wird, hier wären es alle 1ms

von Stefan F. (Gast)


Lesenswert?

Julian schrieb:
> Es muss alles in eine Interrupt Routine passen

Wieso das? Solche Aktionen macht man normalerweise aus der main-loop 
heraus. Interrupthandler sollten nicht arbeiten, sondern lediglich 
Ereignisse zur Kenntnis nehmen und I/O Puffer Übertragen falls die 
Hardware das nicht per DMA kann.

von Julian (Gast)


Lesenswert?

Stefan F. schrieb:
> Julian schrieb:
>> Es muss alles in eine Interrupt Routine passen
>
> Wieso das? Solche Aktionen macht man normalerweise aus der main-loop
> heraus. Interrupthandler sollten nicht arbeiten, sondern lediglich
> Ereignisse zur Kenntnis nehmen und I/O Puffer Übertragen falls die
> Hardware das nicht per DMA kann.

So ähnlich meinte ich es ja. Die Abarbeitung muss fertig werden, bevor 
der Interrupthandler den status für die Abarbeitung in der main loop 
setzt.
Eventuell lasse ich nur Intervalle >10ms zu, oder ich baue eine Liste 
auf, die A.S. beschrieb und arbeite diese einfach nur ab, weiß gerade 
nur nicht wie ich diese Liste ab besten ausbauen sollte, da diese 
unendlich lang, also im Ring, laufen müsste.

[1,10ms, "Mache a"]
[2,30ms, "Mache b"]
[3,20ms, "Mache c"]


++++++++++++++++++++++++++++++++++++++++++++++++++ (1ms inkrement)
          1         3;1         2;1         1;3    (auszulösende 
Kommandos)

von Stefan F. (Gast)


Lesenswert?

Programmiere das lieber so, dass verpasste Zeitpunkte ggf. nachträglich 
abgearbeitet werden. Manchmal zu spät kommen ist weniger schlimm, als 
manchmal gar nicht kommen.

von Walter T. (nicolas)


Lesenswert?

Du willst jetzt aber nicht einen Scheduler neu erfinden?

von Vorname N. (mcu32)


Lesenswert?

Stichwort RTOS.

Stefan F. schrieb:
> Manchmal zu spät kommen ist weniger schlimm, als
> manchmal gar nicht kommen.

Du Ferkel :)

von Grummler (Gast)


Lesenswert?

Walter T. schrieb:

> Du willst jetzt aber nicht einen Scheduler neu erfinden?

Ich kann nicht für den TO sprechen, aber ICH würde es
nicht wollen.

Nach meinem Verständnis verteilt ein Scheduler die Arbeit
zur LAUFZEIT; ich würde das -- soweit möglich -- zur
Compilezeit machen, und so hatte ich die Absicht des TO
auch verstanden.

von J. S. (jojos)


Lesenswert?

Mbed-os Ticker class angucken. Es gibt einen 1 MHz Timer und der Compare 
Int wird immer auf das nächste anstehende Ereignis gesetzt.

von Peter D. (peda)


Lesenswert?

Walter T. schrieb:
> Du willst jetzt aber nicht einen Scheduler neu erfinden?

Beitrag "Wartezeiten effektiv (Scheduler)"

von Julian (Gast)


Lesenswert?

Grummler schrieb:
> Nach meinem Verständnis verteilt ein Scheduler die Arbeit
> zur LAUFZEIT; ich würde das -- soweit möglich -- zur
> Compilezeit machen, und so hatte ich die Absicht des TO
> auch verstanden.

Ne, leider muss es zur Laufzeit geschehen, da die Liste der Tasks erst 
ind er Laufzeit aufgebaut werden können

von Julian (Gast)


Lesenswert?

Peter D. schrieb:
> Walter T. schrieb:
>> Du willst jetzt aber nicht einen Scheduler neu erfinden?
>
> Beitrag "Wartezeiten effektiv (Scheduler)"

Das geht in die richtige Richtung, danke!
Fast hätte ich etwas neu erfunden :-)

von W.S. (Gast)


Lesenswert?

Julian schrieb:
> komme nicht auf einen sinnvollen Ansatz die nachfolgende Anforderung zu
> implementieren...

Zu allererst brauchst du eine Uhr, um zu wissen, was es grad für eine 
Uhrzeit ist. Die kann relativ beliebig fein ticken, aber für die meisten 
Fälle braucht es nur Intervalle zu 1 ms oder 10 ms. Wenn du nur Text per 
UART ausgeben willst, dann reichen Intervalle zu 10 ms allemal.

So, die Textausgabe und die zeitliche Erinnerung daran, selbige zu 
senden (oder etwas anderes zu tun), sind zwei völlig verschiedene Dinge, 
also trenne sie auch voneinander. Und die Zeichenausgabe muß ja nun auch 
nicht auf die Mikrosekunde genau erfolgen, also kann man sowas auch mal 
ein paar Millisekunden später tun, Hauptsache ist, daß sich sowas nicht 
aufsummiert.

Kommen wir also nun zur Uhr und dem, was sie außer dem Zählen der 
Uhrzeit noch tun müßte: nämlich müßte sie eine Liste von Botschaften und 
ihrer Zeitintervalle und ihrer nächsten Zeitpunkte führen. Also sowas 
etwa:
struct Listenelement
begin
  Botschaft;     // 0=nix, alles andere=ne gültige Botschaft
  Intervall;
  NächsterZeitpunkt;
end;
Jaja, je mehr derartige Dinge da drin stehen sollen, desto länger wird 
auch die Bearbeitungszeit. Der Uhrtick (also die ISR des Timerticks 
o.ä.) müßte dann folgendes tun:
for jedes Element seiner Liste do
begin
  ist die Botschaft gültig?
  wenn ja then do
  begin
    ist NächsterZeitpunkt <= aktuelle Uhrzeit ?
    wenn ja then do
    begin
      NächsterZeitpunkt = NächsterZeitpunkt + Intervall;
      Werfe Botschaft in eine Ereignisliste;
    end
  end
end;

So etwa könnte das aussehen. Irgendwoanders in der Firmware muß dann die 
o.g. Ereignisliste angeschaut werden und wenn da was drinsteht, sollte 
es ausgeführt bzw. darauf reagiert werden.

Eigentlich ist das alles keine komplizierte Sache und logisch recht 
geradlinig. Du hättest also schon längst selber drauf kommen können. Man 
muß lediglich am Anfang mit Denken anfangen und nicht am Ende mit 
irgendwelchen Zeichenketten, die gesendet werden sollen. Ist wie beim 
Autofahren: zuerst das Ziel auswählen und dann erst losfahren.

W.S.

von Stefan F. (Gast)


Lesenswert?

W.S. schrieb:
> Eigentlich ist das alles keine komplizierte Sache und logisch recht
> geradlinig. Du hättest also schon längst selber drauf kommen können. Man
> muß lediglich am Anfang mit Denken anfangen

Sind wir heute wieder im Arschloch Modus?

Der TO hat bereits mit dem Denken angefangen und eine von mehreren Ideen 
genannt. Dazu wollte er unsere Meinung lesen, nicht zu der Frage ob er 
sein Gehirn benutzt.

von Peter D. (peda)


Lesenswert?

W.S. schrieb:
> Jaja, je mehr derartige Dinge da drin stehen sollen, desto länger wird
> auch die Bearbeitungszeit.

Das ist eben der Hauptunterschied zu einem Scheduler mit sortierter 
Liste.
Da muß nur einen einzige Variable getestet werden, ob sie schon 
abgelaufen ist. Erst dann werden die Callbacks ausgeführt und die Zeit 
bis zum nächsten Ereignis geladen.
Die CPU-Last des Schedulers ist also weitgehend unabhängig von der 
Anzahl der aktiven Einträge in der Liste.
Die CPU-Last der Callbacks wird natürlich durch ihre Aufrufhäufigkeit 
bestimmt.

: Bearbeitet durch User
von Stefan F. (Gast)


Lesenswert?

Peter D. schrieb:
> Das ist eben der Hauptunterschied zu einem Scheduler mit sortierter
> Liste.

Einen Tod muss man sterben. Wenn ich maximal 10 Einträge in der Liste 
erwarte, verschwende ich keine Zeit mit Sortieren. Bei 1000 Einträgen 
würde ich hingegen nicht auf die Sortierung verzichten.

von W.S. (Gast)


Lesenswert?

Peter D. schrieb:
> Das ist eben der Hauptunterschied zu einem Scheduler mit sortierter
> Liste.
> Da muß nur einen einzige Variable getestet werden,...

Dafür ist bei der Einrichtung ein Sortieraufwand notwendig und es kommt 
damit eine zeitliche Beziehung zwischen verschiedenen Ereignissen ins 
Spiel. Ersatzeshalber kann man dann bei jedem Ereignis neu sortieren, 
aber das ist nicht weniger aufwendig als das, was ich vorgeschlagen 
habe.

W.S.

von Euro (Gast)


Lesenswert?

Wahrscheinlich würde ich zuerst einmal zusehen, das die UART einen für 
mehrere Telegramme ausreichenden FIFO bekommt.
Oder ist das Umkopieren/Schreiben der Telegramminhalte in einen FIFO 
direkt im Interrupt oder per "AsyncDelay (siehe oben W.S.)" in Relation 
zur typischen UART-Geschwindigkeit hier so kritisch?

von W.S. (Gast)


Lesenswert?

Euro schrieb:
> Wahrscheinlich würde ich zuerst einmal zusehen, das die UART einen für
> mehrere Telegramme ausreichenden FIFO bekommt.

Also, du mußt dir erstmal darüber klar werden, daß über eine Serielle 
die Daten nicht schneller drübergehen, als sie drüber gehen.

Man würde die Übertragung und den gesamten Ablauf in der Firmware 
verstopfen, wenn man mehr Daten senden will, als über den seriellen 
Kanal tatsächlich drübergehen können.

Ein Fifo im zugehörigen Treiber dient nur zur Entkopplung der 
aufrufenden Funktion von der dann zeitlich folgenden 
interruptgesteuerten Übertragung der Daten. Zeitlich umgekehrt dasselbe 
in der Empfangsrichtung. Also damit der µC zwischenzeitlich sich um was 
Anderes kümmern kann.

Ähnlich verhält es sich mit Events, Botschaften oder wie man das nennen 
will in einer Warteschlange. Sowas wird irgendwo in der Firmware erzeugt 
und an ganz anderer Stelle und zu einer anderen Zeit (also NICHT 
zwingend sofort) zur Kenntnis genommen und irgendwie drauf reagiert 
(oder auch nicht, je nach sonstigen Umständen).

Begreife mal, daß es sinnvoll ist, nicht alle Dinge in der Firmware in 
einen Topf zu werfen, sondern sie stattdessen so gut es geht voneinander 
zu separieren. Also Treiber für I/O schreiben, die die Datenströme 
puffern, ohne daß man sich außerhalb des Treibers darum kümmern müßte. 
Oder eben das 'Stellen' und 'Klingeln-Lassen' des Weckers davon zu 
trennen, was nach dem Aufstehen getan werden soll.

Euro schrieb:
> Oder ist das Umkopieren/Schreiben der Telegramminhalte in einen FIFO
> direkt im Interrupt...

Eben nicht so. Das wäre das Werfen der Dinge in einen großen Topf und 
umrühren. Dem Timerinterrupt ist - und SOLL! - es egal sein, ob nach 15 
Millisekunden nun ein Text gesendet werden soll oder das Licht 
ausgemacht werden soll. Er soll nur nach 15 Millisekunden es klingeln 
lassen bzw. eine Botschaft an andere Firmwareteile erzeugen.

W.S.

von Peter D. (peda)


Lesenswert?

W.S. schrieb:
> Dafür ist bei der Einrichtung ein Sortieraufwand notwendig

Ich sehe da keinen großen Aufwand, da das Sortieren nur einmal erfolgt 
und nicht bei jedem Timertick nötig ist. Auch enthält das Sortieren 
keine komplizierten Berechnungen, nur Vergleiche und Subtraktionen.

von Walter T. (nicolas)


Lesenswert?

Warum gibt es bei der Standard-Aufgabe "Scheduler" eigentlich immer eine 
ellenlange Diskussion? Es ist ja nicht so, als gäbe es zu diesem Thema 
nicht massig Lesestoff.

von Euro (Gast)


Lesenswert?

W.S. schrieb:
> Also, du mußt dir erstmal darüber klar werden, daß über eine Serielle
> die Daten nicht schneller drübergehen, als sie drüber gehen.
Ach ehrlich?

W.S. schrieb:
> Eben nicht so.
Warum nicht? Es ist eigentlich völlig wumpe, ob Du die typischen 
20..50µs zum Befüllen des FIFOs im low level timer Interrupt oder in der 
main verballerst.
Schwierig wirds in der main, wenn Du nebenher noch den Turm von Brahma 
umstapeln möchtest. Das dauert in der Regel bei Turmhöhe > 20 schon mal 
etwas. :)

von Εrnst B. (ernst)


Lesenswert?

Peter D. schrieb:
> Ich sehe da keinen großen Aufwand, da das Sortieren nur einmal erfolgt
> und nicht bei jedem Timertick nötig ist. Auch enthält das Sortieren
> keine komplizierten Berechnungen, nur Vergleiche und Subtraktionen.

Die Liste muss auch nicht komplett sortiert werden, es reicht, wenn 
immer der am nächsten fällige Task am Anfang steht.

==> Datenstruktur "priority_queue".
https://en.cppreference.com/w/cpp/container/priority_queue
https://de.wikipedia.org/wiki/Vorrangwarteschlange

und schon wird aus dem bösen O(n log n) ein freundliches O(log n)...

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Die Liste muss auch nicht komplett sortiert werden, es reicht, wenn
> immer der am nächsten fällige Task am Anfang steht.

Es wird nicht die Liste sortiert, sondern die Elemente zeigen immer 
sortiert nach aufsteigender Zeit aufeinander.
Wenn ich eine Task einfüge, gehe ich die Liste der aktiven Tasks 
entlang, bis eine nicht früher ablaufende Task oder das Endekennzeichen 
gefunden sind. An dieser Stelle wird die neue Task eingereiht.

Εrnst B. schrieb:
> und schon wird aus dem bösen O(n log n) ein freundliches O(log n)...

Wo siehst Du dabei ein (n log n) oder (log n)?

von Εrnst B. (ernst)


Lesenswert?

Peter D. schrieb:
> Wo siehst Du dabei ein (n log n) oder (log n)?

Ok, es geht immer noch schlechter... Dein Algo wäre O(n²)
"n" mal Task einfügen, und für jeden "n" Vergleiche, um die 
Einfügestelle zu finden.

Wenn du nur einen einzelnen "Einfüge"-Vorgang betrachtest, ist dein O(n) 
immer noch eine ganze Kategorie schlechter als das O(log n) einer 
simplen Heap-basierten priority_queue.

von Grummler (Gast)


Lesenswert?

Walter T. schrieb:

> Warum gibt es bei der Standard-Aufgabe "Scheduler" eigentlich
> immer eine ellenlange Diskussion?

Warum gibt es hier über alles ellenlange Diskussionen?

Könnte das eventuell damit zusammenhängen, dass es sich
hierbei um ein Diskussionsforum handelt?

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Ok, es geht immer noch schlechter... Dein Algo wäre O(n²)
> "n" mal Task einfügen, und für jeden "n" Vergleiche, um die
> Einfügestelle zu finden.

Nö, der erste Eintrag in eine nicht aktive Liste erfolgt sofort, d.h. 
selbst im Worst Case max 1+2+3...
Abgesehen davon ist die Anzahl der Vergleiche nicht der Flaschenhals 
beim Scheduler. Jede Task, die eingefügt wurde, soll ja auch irgendwann 
mal laufen.

von Εrnst B. (ernst)


Lesenswert?

Peter D. schrieb:
> Abgesehen davon ist die Anzahl der Vergleiche nicht der Flaschenhals
> beim Scheduler. Jede Task, die eingefügt wurde, soll ja auch irgendwann
> mal laufen.

Das steht außer Frage.

Trotzdem kann man eine für diesen Anwendungsfall besser geeignete 
Datenstruktur wählen.

Nur weil eine Schraube auch mit dem Hammer in die Wand geht, darf man 
trotzdem besseres Werkzeug verwenden.

Peter D. schrieb:
> im Worst Case max 1+2+3

genau das ist es, was zu O(n²) führt...

: Bearbeitet durch User
von W.S. (Gast)


Lesenswert?

Grummler schrieb:
> Warum gibt es hier über alles ellenlange Diskussionen?
>
> Könnte das eventuell damit zusammenhängen, dass es sich
> hierbei um ein Diskussionsforum handelt?

Ähem, nö, das ist es nicht. Dieses Forum ist zwar ein Diskussionsforum, 
aber die Beiträge, wo tatsächlich diskutiert - also mit sachlichen 
Argumenten ein gemeinsames Ziel zu erreichen versucht wird -  kann man 
mit der Lupe suchen. Bei den meisten Beiträgen wird nur gegackert wie 
auf dem Hühnerhof und Argumente werden nicht zur Kenntnis genommen. 
Jetzt könnte man sagen (oder zumindest insgeheim meinen), dieses Forum 
sei ein Hühnerhof-Gacker-Forum, aber da würden sich wieder welche 
aufregen, weil sie Tatsachen als Beleidigungen empfinden.
Fazit: wie man's macht, ist es verkehrt. Also nicht aufregen.

W.S.

von W.S. (Gast)


Lesenswert?

Euro schrieb:
> W.S. schrieb:
>> Also, du mußt dir erstmal darüber klar werden, daß über eine Serielle
>> die Daten nicht schneller drübergehen, als sie drüber gehen.
> Ach ehrlich?
>
> W.S. schrieb:
>> Eben nicht so.
> Warum nicht? Es ist eigentlich völlig wumpe, ob Du die typischen
> 20..50µs zum Befüllen des FIFOs im low level timer Interrupt oder in der
> main verballerst.

Wieder so ein Gegacker ohne nachzudenken.
Es ist eben nicht egal, ob man in einer ISR wartet, bis der Fifo zum 
Senden wieder ausreichend geleert ist, oder ob man so etwas tunlichst 
aus einer ISR heraushält.
Es ist auch nicht egal, ob man Abläufe verschiedener Schichten in einer 
Firmware zusammenkippt oder lieber auseinanderhält. Das Warum hatte ich 
ja schon geschrieben, also lies es ruhig noch einmal und versuche 
diesmal, es zu vestehen.

W.S.

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Trotzdem kann man eine für diesen Anwendungsfall besser geeignete
> Datenstruktur wählen.

Ich benutze den Scheduler in fast allen Projekten. Er ist eine sehr 
bequeme und effiziente Methode, um Wartezeiten an die Mainloop 
abzugeben. Man muß sich nicht mehr um jede einzelne Task kümmern, die 
warten soll. Man muß nur festlegen, wieviel Tasks maximal gleichzeitig 
warten können. Man legt also einfach etwas Reserve fest und kann dann 
bei der Entwicklung weitere Tasks hinzufügen.

Die Idee mit der verketteten Liste ist aber nicht von mir. Ich hab ein 
älteres Projekt weiter pflegen müssen, da war er drin. Ich hab ihn dann 
nur noch etwas für kleine 8-Bitter (8051) optimiert. Ich finde die 
Lösung schon sehr elegant und universell.

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> genau das ist es, was zu O(n²) führt...

Ich weiß nicht, wo da das n² herkommen soll.
Wenn z.B. 10 Tasks gerade aktiv sind und ich will eine 11. hinzufügen, 
dann sind dafür max 10 Vergleiche bzw. im Mittel 5 nötig.

von Εrnst B. (ernst)


Lesenswert?

Peter D. schrieb:
> Ich weiß nicht, wo da das n² herkommen soll

Ich bin mir jetzt ehrlich nicht sicher, auf welchem Level ich das 
Erklären anfangen soll... Ist es die "O"-Notation generell?

Sieh's so: Der Binary-Heap macht es genauso wie dein Code, nur muss er 
beim  Hinzufügen vom 11. Task nicht maximal 10 Einträge vergleichen, 
sondern nur log₂(10).
Einfach nur weil die Einträge sinnvoller angeordnet sind.
Es ist also kein "Über den Haufen werfen" von deinem Konzept, sondern 
eine Verfeinerung.

Und nachdem es simpel zu implementieren ist (Wenn man die Idee hinter 
der Datenstruktur verstanden hat), gibt es eigentlich kaum einen Grund, 
darauf zu Verzichten, selbst wenn der eigentliche Vorteil vielleicht 
erst bei 100 oder 1000 Tasks deutlich sichtbar wird...

von Teo D. (teoderix)


Lesenswert?

Εrnst B. schrieb:
> Sieh's so: Der Binary-Heap macht es genauso wie dein Code, nur muss er
> beim  Hinzufügen vom 11. Task nicht maximal 10 Einträge vergleichen,
> sondern nur log₂(10).
> Einfach nur weil die Einträge sinnvoller angeordnet sind.
> Es ist also kein "Über den Haufen werfen" von deinem Konzept, sondern
> eine Verfeinerung.

-> "Binäre Suche" ... Sonst dauert das hier ja noch ewig. ;)

von Euro (Gast)


Lesenswert?

W.S. schrieb:
> Es ist eben nicht egal, ob man in einer ISR wartet, bis der Fifo zum
> Senden wieder ausreichend geleert ist, oder ob man so etwas tunlichst
> aus einer ISR heraushält.
Wer wartet denn in einer ISR?
Wenn ich das Telegramm aufgrund eines FIFO-Full nicht senden kann, wird 
es erst im nächsten Timer-Zyklus in den FIFO geschrieben.

Im Ergebnis kein Unterschied: Wenn Dein 10-Byte-Telegramm nicht mehr in 
den 20-Byte-Fifo passt, ist damit die UART für den nächsten 1ms-Zyklus 
bei 115kBaud ausgelastet. Es bringt also auch keinen Performancegewinn, 
x mal innerhalb der nächsten ms in der main den FIFO zu befragen, ob 
denn nun endlich das Telegramm geschrieben werden kann.

Naja, wenn man in der main sonst nix Besseres zu tun hat, ist es wohl 
auch eher eine philosophische Frage, ob dieses Overpolling eine 
gedankenlose Verschwendung von Ressourcen ist (oder ob man da nicht doch 
lieber den Turm von Brahma umstapelt).

von Grummler (Gast)


Lesenswert?

Εrnst B. schrieb:

> Peter D. schrieb:
>> Ich weiß nicht, wo da das n² herkommen soll
>
> Ich bin mir jetzt ehrlich nicht sicher, auf welchem
> Level ich das Erklären anfangen soll...

Vielleicht erstmal selber nachdenken.

Die Zeitkomplexität bezieht sich in der Regel auf
ALGORITHMEN , das sind "Vorschriften, die mit endlich
vielen Schritten zum Ergebnis" führen. Numerische
Simulationen sind ein typisches Beispiel dafür: Das
Programm ist fertig, wenn alle Berechnungen erledigt
sind.


Steuerungsanwendungen sind (als Ganzes!) aber keine
Algorithmen, denn sie terminieren nicht dann und dadurch,
dass ein "Ergebnis" "berechnet" wurde.
Eine Steuerung muss solange aktiv sein, wie es der reale
Prozess erfordert, der durch die Steuerung gesteuert wird.


Infolgedessen verliert der Begriff des "Gesamtaufwandes"
seinen Sinn -- man kann die Steuerung trotzdem nicht früher
abschalten, auch wenn der Prozessor schon lange fertig ist
mit Rechnen.


Für jeden Einzelschritt gelten natürlich Zeitgrenzen;
das ist ein anderes Thema, und das hat Peter ja auch so
formuliert.


> Und nachdem es simpel zu implementieren ist (Wenn man
> die Idee hinter der Datenstruktur verstanden hat), gibt
> es eigentlich kaum einen Grund, darauf zu Verzichten,
> selbst wenn der eigentliche Vorteil vielleicht erst
> bei 100 oder 1000 Tasks deutlich sichtbar wird...

Doch, kann es geben: KISS bzw. YAGNI.

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Sieh's so: Der Binary-Heap macht es genauso wie dein Code, nur muss er
> beim  Hinzufügen vom 11. Task nicht maximal 10 Einträge vergleichen,
> sondern nur log₂(10).

Das hast Du also gemeint. Das ist hier nicht anwendbar, da eben nicht 
wirklich sortiert wird, sondern die aktiven Einträge nur neu verkettet 
werden. Man kann also nicht die Mitte auswählen, weil diese nicht 
bekannt ist.

Εrnst B. schrieb:
> Und nachdem es simpel zu implementieren ist (Wenn man die Idee hinter
> der Datenstruktur verstanden hat), gibt es eigentlich kaum einen Grund,
> darauf zu Verzichten, selbst wenn der eigentliche Vorteil vielleicht
> erst bei 100 oder 1000 Tasks deutlich sichtbar wird...

Eine binäre Suche dürfte deutlich komplexer ausfallen. Man müßte 
entweder wirklich umsortieren oder zusätzliche Listen der aktiven und 
der freien Indexe pflegen. Es müssen also schon sehr viele Tasks 
gleichzeitig aktiv sein, damit sich der hohe Aufwand rechnet.
Die Liste hat bei meinen MC-Projekten oft nur 10-20 Einträge. In einem 
Projekt hat sie 30 Einträge für das automatische Senden von 
Monitorwerten. Man kann dann Werte im Fokus häufiger senden lassen und 
nur zu überwachende seltener.

: Bearbeitet durch User
von Εrnst B. (ernst)


Lesenswert?

Grummler schrieb:
> Vielleicht erstmal selber nachdenken.

Vielleicht erstmal selber lesen.

Grummler schrieb:
> Die Zeitkomplexität bezieht sich in der Regel auf
> ALGORITHMEN

Genau darum ging es. Datenstruktur für "Irgendwas" (Hier: Tasks), 
Element mit niedrigster Prio (Hier: "Due"-Zeitstempel) finden, und mit 
geänderter Prio (Hier: Zeitstempel+Interval) wieder einreihen.

Also beim Binary Heap/priority_queue eben die Algorithmen hinter den 
top() pop() push()-Operationen.
Oder eben Elementzugriff array[0] (Ohh! schwieriger Algorithmus) und ein 
"make_heap" danach.

Grummler schrieb:
> Doch, kann es geben: KISS

Wenn dein kisS-Prinzip Informatik-Erstsemester Algorithmen 
ausschließt, Sachen die knapp oberhalb vom Bubblesort-Urschleim liegen, 
muss das letzte "S" schon arg großgeschrieben sein.

Und hey: Wenn dir die paar Zeilen for-Schleife zu komplex sind: in der 
STL gibt's fix und fertige Implementationen als Template. Die darfst du 
sogar anwenden wenn du die Algorithmen dahinter nicht verstehst.

YAGNI kann es nicht sein, denn es ist ja schon klar, dass Tasks in der 
richtigen Reihenfolge ausgeführt werden sollen.

von Εrnst B. (ernst)


Lesenswert?

Peter D. schrieb:
> Eine binäre Suche dürfte deutlich komplexer ausfallen.

Man kann den Baum einfach auf ein Array mappen (hat dann aber eine fixe 
Maximallänge)
Knoten n hat Tochterknoten 2n+1 und 2n+2,
Knoten n hat Mutterknoten (n-1)/2
d.H. keine "Verpointerung" nötig, Indexberechnungen sind rein Bitshifts 
und Additionen.

Aktuell anliegender Task ist array[0].
dem kann man nach dem Ausführen einfach seinen fällig-Zeitstempel 
hochstellen, und dann den Baum hochwandern lassen, bis er an einer 
passenden Stelle liegt.
"MinHeapify(0)".
Diese Funktion wird zwar üblicherweise rekursiv definiert, es ist aber 
eine Tail-Rekursion, der Compiler wird da eine simple Schleife draus 
machen.

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Man kann den Baum einfach auf ein Array mappen (hat dann aber eine fixe
> Maximallänge)
> Knoten n hat Tochterknoten 2n+1 und 2n+2,
> Knoten n hat Mutterknoten (n-1)/2
> ...

Also ich verstehe da nur Bahnhof.
Hast Du mal eine Beispielimplementation, die ich nachvollziehen könnte, 
die also plain C und nicht C++ mit irgendwelchen super duper komplexen 
Templates ist?

Der Quelltext mit dem Scheduler, den ich damals geerbt hatte, machte den 
Eindruck, daß der Programmierer sehr erfahren war und die gesamte 
Herangehensweise an das Projekt hat mir gut gefallen. Wenn da mit 
binärer Suche was zu verbessern gewesen wäre, hätte er es bestimmt 
gemacht. Entwickelt hatte er das Programm zum Großteil unter Unix auf 
ner Sun und erst später durch den Keil C51 gejagt.

von Walter T. (nicolas)


Lesenswert?

Hier ist es in Farbe und bunt:

https://trstringer.com/complete-binary-tree/

Was ich allerdings noch nicht verstanden habe ist, wann es einen 
Geschwindigkeitsvorteil gegenüber Bisektionsverfahren auf einem 
sortierten Array hat. Vielleicht, wenn die Daten auf einem Band ohne 
wahlfreien Zugriff gespeichert sind?

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Walter T. schrieb:
> Hier ist es in Farbe und bunt:
>
> https://trstringer.com/complete-binary-tree/

Ja gut, das ist nur die Adressierungslogik.
Die Frage ist aber, wie hilft mir das beim Einsortieren einer Zeit, daß 
immer die kürzeste als erstes ausgeführt wird?

Es ist schwer, aus solchen Bildchen einen konkreten Code zu erstellen.
Die binäre Suche sieht doch erheblich aufwendiger aus, als das lineare 
Einsortieren.

von Walter T. (nicolas)


Lesenswert?

Naja, alles, was in Big-O-Größenordnungen toll ist, ist bei Zahlen in 
der Größenordnung von Fingern und Zehen aufwendiger als das 
Naheliegende.

von Εrnst B. (ernst)


Angehängte Dateien:

Lesenswert?

Peter D. schrieb:
> Also ich verstehe da nur Bahnhof.
> Hast Du mal eine Beispielimplementation, die ich nachvollziehen könnte,
> die also plain C und nicht C++ mit irgendwelchen super duper komplexen
> Templates ist?

Viel Magie steckt da nicht drinnen.
Wichtigste Funktion ist die hier:
1
#define LEFT(i) ((i)*2+1)
2
#define RIGHT(i) ((i)*2+2)
3
#define PARENT(i) ((i-1)/2)
4
// An Knoten "i" die Heap-Bedingung herstellen
5
// Laufzeit O(log n)
6
void heapify(int i) {
7
  // Wir suchen unter diesem und direkten Kindern den kleinsten Knoten
8
  int kleinster=i;
9
  int left=LEFT(i);
10
  int right=RIGHT(i);
11
  if (left < taskcount && tasks[left].due < tasks[kleinster].due)
12
    kleinster=left;
13
  if (right < taskcount && tasks[right].due < tasks[kleinster].due)
14
    kleinster=right;
15
  if (kleinster == i) return; // Passt schon
16
  // Ansonsten tauschen, kleinsten Knoten Richtung Wurzel wandern lassen.
17
  swap(i, kleinster);
18
  // Tail-rekursion
19
  heapify(kleinster);
20
}
und die sucht einfach nur den kleinsten von drei Werten, tauscht falls 
nötig den Kleinsten nach unten, und macht dasselbe dann rekursiv an dem 
Knoten, der gerade getauscht wurde. Aus der Rekursion macht der GCC ein 
einfaches "JMP".

Kompilierbarer Rest zum damit Spielen im Anhang, hoffentlich ausreichend 
kommentiert.

von Εrnst B. (ernst)


Lesenswert?

Walter T. schrieb:
> ist bei Zahlen in
> der Größenordnung von Fingern und Zehen aufwendiger als das
> Naheliegende.

ja, bei nur einer Handvoll Tasks lohnt weder das eine noch das andere, 
da packt man einfach per copy&paste sooft wie nötig sowas in den Code:
1
void loop() {
2
3
  static uint32_t lastrun_TaskX=0;
4
  if (millis() -  lastrun_TaskX >= periode_TaskX) {
5
     lastrun_TaskX=millis();
6
     taskX();
7
  }
8
...
9
10
}

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Aktuell anliegender Task ist array[0].
> dem kann man nach dem Ausführen einfach seinen fällig-Zeitstempel
> hochstellen, und dann den Baum hochwandern lassen, bis er an einer
> passenden Stelle liegt.

Eben, der Baum muß neu sortiert werden.
Der Binärbaum gilt ja nur für den einen Moment. Beim Einfügen oder 
Ausführen eines Eintrags müßte man ihn doch wieder umstellen.
Das Hoch- bzw. Runterwandern könnte auf größeren MCs per DMA erfolgen. 
Das könnte bei großen Arrays leicht etwas bringen.

Der Charme der verketteten Liste ist ja, daß ich beim Einfügen nur den 
Vorgänger korrigieren muß bzw. bei Ausführung nur den ersten Eintrag als 
frei markieren muß.

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> ja, bei nur einer Handvoll Tasks lohnt weder das eine noch das andere,
> da packt man einfach per copy&paste sooft wie nötig sowas in den Code:

Beachten muß man aber, daß in größeren Statemaschines an vielen Stellen 
gewartet werden könnte, aber nur an wenigen gleichzeitig.
Hat man z.B. 100 mögliche Stellen im Ablauf, wovon nur 10 aktiv warten, 
lohnt sich ein Scheduler durchaus.

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Kompilierbarer Rest zum damit Spielen im Anhang

Danke, muß ich mir erstmal ansehen.

von Εrnst B. (ernst)


Lesenswert?

Peter D. schrieb:
> Eben, der Baum muß neu sortiert werden.

Nicht der Baum, sondern nur ein Pfad von der Wurzel Richtung Blätter 
wird sortiert.
Also nur log₂(n), nicht n.
Und der auch nicht die ganze Strecke, sobald eine passende Position 
erreicht ist, wird (wie bei der Suche in der verketteten Liste) 
abgebrochen.

Peter D. schrieb:
> Beim Einfügen

als letztes Array-Element anhängen, den Pfad Blatt->Wurzel hochblubbern 
lassen. Also wieder nur ein Pfad, der angeschaut werden muss.

> oder
> Ausführen eines Eintrags müßte man ihn doch wieder umstellen.

wieder nur ein Pfad, den der Eintrag hochlaufen muss.

Peter D. schrieb:
> Das Hoch- bzw. Runterwandern könnte auf größeren MCs per DMA erfolgen

Da seh ich den Sinn von DMA nicht. Evtl. bei der "swap"-Operation zum 
memcopy-beschleunigen.
Aber: Sobald die einzelnen Task-Structs so groß werden, dass man da über 
memcopy+DMA nachdenkt, dann kann man auch ein Array mit pointern 
verwenden.

von Grummler (Gast)


Lesenswert?

Walter T. schrieb:

> https://trstringer.com/complete-binary-tree/
>
> Was ich allerdings noch nicht verstanden habe ist, wann es
> einen Geschwindigkeitsvorteil gegenüber Bisektionsverfahren
> auf einem sortierten Array hat.

Beim reinen Suchen hat es keinen Vorteil -- wohl aber beim
Einfügen.

Um in ein sortiertes Array (unter Erhalt der Ordnung)
ein Element einzufügen, muss man im Mittel n/2 Elemente
verschieben -- beim Heap sind es nur log(n) Stück.

Bei n=10 lohnt das noch nicht wirklich...

von Grummler (Gast)


Lesenswert?

Εrnst B. schrieb:

> Grummler schrieb:
>> Vielleicht erstmal selber nachdenken.
>
> Vielleicht erstmal selber lesen.

Habe ich.

Dort stand etwas von "O(n^2)", was nicht stimmt bzw.
praktisch nicht relevant ist.

O(n) versus O(log(n)) ist korrekt, aber für N=10
gleitet der Streit ins Lächerliche ab...


> Grummler schrieb:
>> Doch, kann es geben: KISS
>
> Wenn dein kisS-Prinzip Informatik-Erstsemester
> Algorithmen ausschließt, Sachen die knapp oberhalb
> vom Bubblesort-Urschleim liegen,

Ahh. Und weil das so ist, schrieb Peter: "Ich verstehe
nur Bahnnof".

Alles klar...

von Εrnst B. (ernst)


Lesenswert?

Grummler schrieb:
> Habe ich.
>
> Dort stand etwas von "O(n^2)", was nicht stimmt bzw.
> praktisch nicht relevant ist.

Das O(n²) bezog sich auf die Implementation mit der Liste, die sortiert 
aufgebaut wird indem nacheinander n Elemente eingefügt werden, und jedes 
Element sucht beim Einfügen mit O(n) seinen Platz.

Εrnst B. schrieb:
> "n" mal Task einfügen, und für jeden "n" Vergleiche, um die
> Einfügestelle zu finden.

Ja, das wäre mit "zuerst unsortiert sammeln", und dann einmal mit O(n 
log n) sortieren auch erledigt. So hat das Peter aber nicht beschrieben.


Überhaupt: Was ist so schlimm daran, neue und erprobte Algorithmen und 
Datenstrukturen kennenzulernen?
Wenn man sie kennt, kann man entscheiden, ob und wann man sie sinnvoll 
einsetzen kann.
Wenn man sie nicht kennt, hat man diese Möglichkeit nicht.

Tut es irgendwie weh, Neues zu lernen?

von Grummler (Gast)


Lesenswert?

Peter D. schrieb:

> Eben, der Baum muß neu sortiert werden.

Nee -- er ist ja gar nicht KOMPLETT sortiert.

Es wird nur gerade so das absolute Minimum an Ordnung
gewährleistet -- und das ist, dass die Wurzel jedes (!)
Teilbaumes kleiner ist als alle anderen Elemente dieses
Teilbaumes. Ein Vergleich "benachbarter" Teilbäume findet
nicht statt -- ist auch nicht notwendig.


> Der Binärbaum gilt ja nur für den einen Moment.

Ja... naja, jein.


> Beim Einfügen oder Ausführen eines Eintrags müßte man
> ihn doch wieder umstellen.

Immer nur einen kleinen Teil davon.
Man muss immer nur einen Pfad von der Wurzel zu einem
Blatt reparieren, und auch das nicht immer komplett.


> Der Charme der verketteten Liste ist ja, daß ich beim
> Einfügen nur den Vorgänger korrigieren muß bzw. bei
> Ausführung nur den ersten Eintrag als frei markieren muß.

Naja, bei 10 Elementen lohnt sich ein Heap m.E. noch nicht.
Bei 100 Stück sieht das schon anders aus...

von Grummler (Gast)


Lesenswert?

Εrnst B. schrieb:

> Grummler schrieb:
>> Habe ich.
>>
>> Dort stand etwas von "O(n^2)", was nicht stimmt bzw.
>> praktisch nicht relevant ist.
>
> Das O(n²) bezog sich auf die Implementation mit der Liste,
> die sortiert aufgebaut wird indem nacheinander n Elemente
> eingefügt werden, und jedes Element sucht beim Einfügen
> mit O(n) seinen Platz.

Ich weiss.
Genau das meinte ich mit "praktisch nicht relevant".

Die Zahl der Tasks mag erst zur Laufzeit feststehen oder
sich sogar von Zeit zu Zeit ändern -- aber dass permanent
die komplette Liste gelöscht und neu wieder aufgebaut
werden muss, halte ich doch für ziemlich exotisch. Wenn
DAS ein relevantes Problem wäre, hätte Peter das sicher
erwähnt...


> Überhaupt: Was ist so schlimm daran, neue und erprobte
> Algorithmen und Datenstrukturen kennenzulernen?

Der Zeitpunkt ist entscheidend.

Und die technisch bessere Lösung ist keineswegs immer
die für die jeweilige Aufgabe sinnvollere Entscheidung...

von Walter T. (nicolas)


Lesenswert?

Wobei der TO an keiner Stelle erwähnt hat, dass nicht eine einfache 
statische Planung ausreicht.

von Peter D. (peda)


Lesenswert?

Εrnst B. schrieb:
> Das O(n²) bezog sich auf die Implementation mit der Liste, die sortiert
> aufgebaut wird indem nacheinander n Elemente eingefügt werden, und jedes
> Element sucht beim Einfügen mit O(n) seinen Platz.

Nein, ein neues Element sucht nur über die bereits eingefügten.
Das 1. nimmt sich einen freien Platz (alle sind frei), das 2. vergleicht 
mit dem 1. usw.
Also 0+1+2+3+...+(n-1).

: Bearbeitet durch User
von Εrnst B. (ernst)


Lesenswert?

Peter D. schrieb:
> Also 0+1+2+3+...+(n-1).

Ja, genau das kürzt sich zu O(n²)

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.