Forum: Mikrocontroller und Digitale Elektronik Wie Code modularisieren bei Zugriff auf Interrupts aus verschiednen Modulen


von Samuel (samuel_a654)


Lesenswert?

Versuche meinen Code besser zu modularisiren. Konkret habe ich einen 
STM32F303 und daran an einem I2C Bus zwei verschiedene Sensoren. 
(CubeIDE mit HAL)

Überlege mir nun wie ich das generell am besten löse wenn ich mehrere 
Module (je eines für die verschiedenen Sensoren) mache, aber am Schluss 
immer der gleiche Interupt ausgelöst wird wenn die Übertragung fertig 
ist.

Also wie kann ich nach dem Interrupt bestimmen welche Funktion nun die 
Daten braucht?

Die KI schlägt mir da einen Dispatcher vor der dann die entsprechende 
Funktion nach einer registrierung aufruft vor. Bin aber noch relativer 
Anfänger, ist das ein guter Ansatz oder wie macht man das üblicherweise?

Danke.

von Adam P. (adamap)


Lesenswert?

Wenn ich aus mehreren Modulen auf eine Peripherie zugreifen möchte, will 
ich natürlich auch verhindern, dass ein Modul X auf die Hardware 
zugreift, da Modul Y z.B. noch keine Antwort erhalten hat.

Ich habe mir dazu einen Job-Manager geschrieben.
Dieser nimmt Jobs entgegen, reiht diese ein und arbeitet diese ab.
Natürlich muss jedes Modul das einen Job übergibt auch eine Callback 
Funktion mitliefern.

Im Endeffekt, ruft die Hardware den Interrupt auf, dieser dann meinen 
Job-Manager und dort sind ja die jeweiligen Callbacks für den aktuellen 
und noch offene Jobs bekannt.

Ob diese Lösung auch für dich passt, kann ich nicht beurteilen.
Aber es gibt da bestimmt 100te Wege die ans Ziel führen.

Man könnte dies wohl als "Dispatcher" betrachten, so wie du es erwähnt 
hast.

: Bearbeitet durch User
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Samuel schrieb:
> CubeIDE mit HAL

Bei der HAL kannst du doch meistens einen Callback übergeben, was du 
zuerst in STM32CubeMX im Project Manager aktivieren musst. Dann ruft die 
HAL automatisch den richtigen Callback des Moduls auf, das eine Aktion 
angefordert hatte.

von Ob S. (Firma: 1984now) (observer)


Lesenswert?

Niklas G. schrieb:

> Bei der HAL kannst du doch meistens einen Callback übergeben, was du
> zuerst in STM32CubeMX im Project Manager aktivieren musst. Dann ruft die
> HAL automatisch den richtigen Callback des Moduls auf, das eine Aktion
> angefordert hatte.

Tja, ist leider ziemlich blöd gelöst, weil das Konzept von Hause aus nur 
einen Callback-"Konsumenten" erlaubt. Das ist gerade für Sachen wie ISP 
oder I2C mit vielen verschiedenen, ggf. sogar ziemlich komplexen Clients 
ziemlich schlecht für die Code-Modularisierung.

Und wenn deren Existenz dann auch noch "dynamisch" ist, also erst zur 
Laufzeit ermittelt werden kann, ob die zugehörige Hardware des Clients 
überhaupt im System ist, scheitert das Konzept endgültig.

Also für alles, was näherungsweise nach einem Bus-System aussieht, taugt 
der Ansatz von CubeMX leider nix. Läuft i.A. dann darauf hinaus, dass 
man eine eigene Zwischenschicht mit eigenem Client-API einziehen muss.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Ob S. schrieb:
> Tja, ist leider ziemlich blöd gelöst, weil das Konzept von Hause aus nur
> einen Callback-"Konsumenten" erlaubt

Bei I2C kann sowieso nur eine Kommunikation gleichzeitig statt finden, 
ich kann mir keinen Anwendungsfall vorstellen wo zwei Module 
gleichzeitig über I2C kommunizieren müssen - nur abwechselnd. Jedes 
Modul setzt direkt vor dem Start des Transfers den eigenen Callback via 
HAL_I2C_RegisterCallback.

von Samuel (samuel_a654)


Lesenswert?

Niklas G. schrieb:
> Jedes
> Modul setzt direkt vor dem Start des Transfers den eigenen Callback via
> HAL_I2C_RegisterCallback.

Also könnte ich damit folgendes machen:

1. HAL_I2C_RegisterCallback für Sensor A
2. Abfrage starten
3. Callback ruft meine Funktion in Modul A auf die den Sensor Wert 
verarbeitet.
4. HAL_I2C_RegisterCallback für Sensor B
2. Abfrage starten
3. Callback ruft meine Funktion in Modul B auf die den Sensor Wert 
verarbeitet.

Damit könnte ich ja relativ elegant die Funktionen zum 
Empfangen/Auswerten der Sensordaten in den entsprechenden Modulen 
lassen?

Wäre das der bevorzugte Weg um mehrere I2C Sensoren auszuwerten? Oder 
gibt es da bewährtere Wege?

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Samuel schrieb:
> Also könnte ich damit folgendes machen:

Ja ganz genau so.

Samuel schrieb:
> Oder gibt es da bewährtere Wege?

Im Prinzip funktionieren ja viele APIs/Frameworks aus allen möglichen 
Bereichen genau so - oft übergibt man den Callback direkt an die 
Funktion, welche die Operation startet, hier gibt es eine separate 
Funktion zum Setzen des Callbacks.

Ich glaub der verbreitetste Weg ist es ein RTOS zu nutzen, und 
synchrone/blockierende Aufrufe für I2C zu nehmen und gar keine Callbacks 
zu setzen. Hat auch seine Vor- und Nachteile.

Bei asynchroner Programmierung mit vielen Callbacks kommt man schnell 
mal in die "Callback Hell" - in anderen Programmiersprachen kann man das 
mit async/wait, Coroutinen, Futures, Reactive Flows strukturieren, bei 
Embedded hat man weniger Möglichkeiten. Dafür sind bei Embedded die 
Abläufe meist nicht so kompliziert. Daher ist die Verwendung von 
Callbacks IMO durchaus sinnvoll, wenn man den Code gut strukturiert.

In C++ kannst du als Funktionszeiger für den Callback ein Lambda ohne 
Captures übergeben, dann steht der Callback-Code immerhin in der Nähe 
des Starts der Operation.

von Nemopuk (nemopuk)


Lesenswert?

Je mehr Module diesen Sensor abfragen, umso mehr Kommunikation und 
Warten auf die Antwort findet statt. Das kann je nach Anwendungsfall 
schnell zum Flaschenhals führen, wenn nicht gar Deadlocks. Ich meine 
hier gelesen zu haben, daß eine beliebte RTC und ein beliebter 
Temperaturfühler bei zu vielen Anfragen sogar zu Fehlfunktionen neigen. 
Also ist es vielleicht besser, den Sensor in regelmäßigen Intervallen zu 
pollen und das Ergebnis über shared Memory (bzw. entsprechende Getter) 
den Modulen bereit zu stellen.

: Bearbeitet durch User
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Nemopuk schrieb:
> Also ist es vielleicht besser,
> den Sensor in regelmäßigen Intervallen zu pollen und das Ergebnis über
> shared Memory (bzw. entsprechende Getter) den Modulen bereit zu stellen.

Es sind doch

Samuel schrieb:
> zwei verschiedene Sensoren

an einem einzelnen I²C-Bus, und jedes Modul kann für sich sicherstellen, 
dass es "seinen" Sensor nicht zu oft abfragt. Außer jedes Modul fragt 
beide Sensoren ab...

von Nemopuk (nemopuk)


Lesenswert?

Ach so. Da habe ich wohl den Anwendungsfall missverstanden.

von Marc V. (Firma: Vescomp) (logarithmus)


Lesenswert?

Samuel schrieb:
> Versuche meinen Code besser zu modularisiren. Konkret habe ich einen
> STM32F303 und daran an einem I2C Bus zwei verschiedene Sensoren.
> (CubeIDE mit HAL)

Verstehe dein Problem nicht.
Sensoren können von sich aus keine Kommunikation starten.
Master fragt, Slave antwortet - was ist da zu modularisieren?

von Samuel (samuel_a654)


Lesenswert?

Marc V. schrieb:
> Master fragt, Slave antwortet - was ist da zu modularisieren?

Sorry für die späte Antwort, habe das übersehen.

Möchte mein Code so strukturieren, das ich ein Modul (Header und Source 
File) für Sensor A und Sensor B habe. Wo ich die komplette Auswertung 
und Konfiguration des Sensors habe.

Mein Problem ist nun das ich das abfragen starte (Sensor A) und dann 
einen Interrupt bekommen wenn die Daten/Antwort des Sensors bereit ist.
Nun weiss ich aber nicht wie ich die Daten verarbeite, da ich ja nicht 
weiss welches Modul das gestartet hat.

Das nicht wissen ist hier natürlich etwas konstruiert, bei diesem 
einfachen Fall kann man das sicher lösen. Möchte aber das ganze 
allgemein lösen, das ich in Zukunft belibige Sensoren immer gleich 
einbinden kann. Ich für den Sensor quasi nur noch das passende Header 
File einbinden muss und dann alles ohne weitere Logik im Hauptprogramm 
funktioniert.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Samuel schrieb:
> Nun weiss ich aber nicht wie ich die Daten verarbeite, da ich ja nicht
> weiss welches Modul das gestartet hat.

War die Frage denn jetzt nicht schon beantwortet? Hat es mit 
HAL_I2C_RegisterCallback nicht funktioniert?

von Samuel (samuel_a654)


Lesenswert?

Doch eigentlich schon, wollte nur auf Marc V. antworten. Vielleicht 
mache ich es ja einfach nur zu kompliziert.

Vielen Dank dir für deine Hilfe.

von Dirk F. (dirkf)


Lesenswert?

Mach doch einfach eine globale Variable:
Wert  0 = Nix zu tun
1 = Sensor 1 abfrage
2 = Sensor 1 hat geantwortet
3 = Sensor 2 abfragen
4 = Sensor 2 hat geantwortet
Ja, ich weiß, globale Variablen sind verpönt.
Aber es ist eine sehr gute Möglichkeit, um zwei  Tasks zu 
synchronisieren, bzw. Informationen auszutauschen.

: Bearbeitet durch User
von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Dirk F. schrieb:
> Ja, ich weiß, globale Variablen sind verpönt

ganz besonders dann, wenn man von mehreren Tasks gleichzeitig drauf 
zugreift! Absolutes "recipe for disaster".

von Bruno V. (bruno_v)


Lesenswert?

Samuel schrieb:
> Möchte aber das ganze
> allgemein lösen, das ich in Zukunft belibige Sensoren immer gleich
> einbinden kann. Ich für den Sensor quasi nur noch das passende Header
> File einbinden muss und dann alles ohne weitere Logik im Hauptprogramm
> funktioniert.

Auftrag, Job, Taskmanager, die Namen sind verschieden, doch es läuft 
m.e. auf Adams Lösung hinaus.

Adam P. schrieb:
> Ich habe mir dazu einen Job-Manager geschrieben.
> Dieser nimmt Jobs entgegen, reiht diese ein und arbeitet diese ab.

Bei i2c kann so ein Job ein einfaches Telegram mit Antwort sein. Also 
Adresse, Datenbyte(s), Platz für die Antwort. Optional Prio, 
Auftragsnummer, Wartezeit, Empfangsqueue .... was auch immer.

Die Ausgestaltung hängt von Deiner generellen Architektur ab. Arbeitest 
Du mit parallelen Tasks, kann die Funktionen blockierend sein
> n = I2Cjob(addr, cmd, *data)
Oder in einer Empfangs-Queue auflaufen

Details machen nur Sinn, wenn wir eine grobe Vorstellung Deines 
"Systems" haben, sonst hat jeder was anderes im Kopf.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Bruno V. schrieb:
> Arbeitest Du mit parallelen Tasks, kann die Funktionen blockierend sein

Soll sie ja nicht, er hat explizit nach Callbacks gefragt. Und genau 
dieses Problem löst so ein Task Scheduler erstmal nicht. Rein zufällig 
enthält die HAL aber bereits die Lösung in Form eines einstellbaren 
Callbacks. Könnte man auch als Observer-Pattern oder async-wait 
betrachten.
Der STM32F303 ist auch ein Single-Core, der kann nichts parallel 
ausführen.

von Hannes J. (pnuebergang)


Lesenswert?

Samuel schrieb:
> Mein Problem ist nun das ich das abfragen starte (Sensor A) und dann
> einen Interrupt bekommen wenn die Daten/Antwort des Sensors bereit ist.
> Nun weiss ich aber nicht wie ich die Daten verarbeite, da ich ja nicht
> weiss welches Modul das gestartet hat.

Doch weißt du, denn irgendwas - was du programmiert hast - ruft das 
Modul auf um den Sensor abzufragen. Die Module werden nicht durch 
Zauberhand aufgerufen. Da es keine gute Idee ist überlappende 
Abfragen/Antworten auf I2C zu haben (kann I2C nicht), ist mit Start der 
Abfrage klar von wem die nächste Antwort kommen muss.

Anders ausgedrückt, du musst den Buszugriff sowieso serialisieren. Das 
kannst du

* durch sorgfältig nacheinander erfolgende Aufrufen deiner Module 
machen. Dazu Nachverfolgen der Antworten und des Busstatus durch die 
Interrupts.

* Du kannst auch auf die Interrupts verzichten (keine so gute Idee) und 
direkt nach dem Start einer Abfrage auf die Antwort warten.

* Du kannst mit einem Puffer (FIFO), Code der den Puffer abarbeitet und 
einem Interrupthandler, der die Ergebnisse einreiht und den Busstatus 
verfolgt, arbeiten. Eher sinnvoll wenn du einen wilden Haufen von Slaves 
am Bus hast die durcheinander und zu vorher unbekannten Momenten 
abgefragt werden müssen. Ansonsten würde ich es lassen.


Für extra Spaß - eher für wenn zusätzliche Sicherheit benötigt wird - 
kannst du das Eintreffen der Interrupts mit einem Timer absichern. 
Bleibt ein Interrupt innerhalb einer gewissen Zeit aus stimmt irgendwas 
ganz und gar nicht. Dann einen Fehlercode irgendwo eintragen wo ein 
µC-Reset überlebt wird und den Bus zurücksetzen. Das Bus-Zurücksetzen 
muss man meist händisch Programmieren. Oder den µC zurücksetzen, 
anhalten oder was auch immer sinnvoll ist.

> Ich für den Sensor quasi nur noch das passende Header
> File einbinden muss und dann alles ohne weitere Logik im Hauptprogramm
> funktioniert.

Unmöglich. Etwas in deinem Code, zum Beispiel das Hauptprogramm, muss 
die Sensordaten anwendungsspezifisch interpretieren und entsprechende 
Aktionen (eventuell auch wieder über I2C) ausführen.

von Nick (b620ys)


Lesenswert?

Adam P. schrieb:
> Ich habe mir dazu einen Job-Manager geschrieben.
> Dieser nimmt Jobs entgegen, reiht diese ein und arbeitet diese ab.
> Natürlich muss jedes Modul das einen Job übergibt auch eine Callback
> Funktion mitliefern.

Ja, dafür bekommt man gleich mal einen Minuspunkt! Hier sind wirklich 
Idioten unterwegs.
Ich weiß natürlich nicht, wie der Jobmanager aussieht, aber das ist 
jedenfalls ein guter Ansatz.

Wie ich das machen würde:
1
Modul 1 behandelt Sensor A
2
Modul 2 behandelt Sensor B
3
Modul 3 behandelt die Schnittstelle.
Beim Aufruf von Modul 3 übergibt das aufrufende Modul (1 oder 2) wer der 
Aufrufer war. Das kann ein function-pointer sein. Oder etwas primitiver 
eine Zahl mit deren Hilfe dann eine Funktion in Modul 1 oder 2 
aufgerufen wird. Die Lösung ist aber wirklich schlampig, weil dann Modul 
3 internas von Modul 1 und 2 wissen muss. Modul 1 & 2 werden wohl sehr 
kurz und behandeln nur irgendwelche Skalierungen.

Sauber ist es, in Modul 3 ein typedef für die zu übergebende 
callback-function zu definieren. Die kennen somit Modul 1 und 2 
(#include "modul3.h") und müssen sich dran halten. Modul 3 muss nichts 
von den aufrufenden Modulen wissen und kann beliebig oft von Anderen 
benützt werden.

Vorsicht! Die callbacks NICHT innerhalb des INTs aufrufen. Modul 3 muss 
den INT komplett kapseln und bis zu Ende behandeln. Andernfalls handelt 
man sich ein, dass man den fehlerträchtigen code zwei mal implementieren 
muss. Man kommt also, ums einfach zu halten, nicht um ein polling durch 
Mod 1 & 2 rum.

Kapselung bedeutet auch, dass man den Anwender des Moduls vor allen 
Fallen schützt.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Nick schrieb:
> Beim Aufruf von Modul 3 übergibt das aufrufende Modul (1 oder 2) wer der
> Aufrufer war. Das kann ein function-pointer sein

Nick schrieb:
> Sauber ist es, in Modul 3 ein typedef für die zu übergebende
> callback-function zu definieren

Die STM32CubeHAL übernimmt schon ganz wunderbar die Funktion von Modul 
3, und implementiert das schon genau so mit Funktionszeiger+Callback.

Nick schrieb:
> Die callbacks NICHT innerhalb des INTs aufrufen. Modul 3 muss den INT
> komplett kapseln und bis zu Ende behandeln. Andernfalls handelt man sich
> ein, dass man den fehlerträchtigen code zwei mal implementieren muss

Das musst du mal genauer erläutern.

Nick schrieb:
> Kapselung bedeutet auch, dass man den Anwender des Moduls vor allen
> Fallen schützt.

Das ist kaum machbar. Man kann auch Punkte offen lassen und die "Fallen" 
dokumentieren und manche Verantwortung dem Aufrufer überlassen.

von Nick (b620ys)


Lesenswert?

Niklas G. schrieb:
> Nick schrieb:
>> Die callbacks NICHT innerhalb des INTs aufrufen. Modul 3 muss den INT
>> komplett kapseln und bis zu Ende behandeln. Andernfalls handelt man sich
>> ein, dass man den fehlerträchtigen code zwei mal implementieren muss
>
> Das musst du mal genauer erläutern.

Gerne.
Wenn man innerhalb eines INT eine callback-function aufruft, dann muss 
man sich klar darüber sein, dass diese function INT-sicher sein muss. 
Wenn also beispielsweise in der cbf ein uint32_t geändert wird und das 
nicht atomar ist (weil 16-Bit µC), dann KANN das schon unerwünschte 
Nebeneffekte haben. Noch schlimmer, wenn die cbf komplexeren code 
ausführt wie z.B. Displayroutine, komplexere Berechnungen die Daten 
rumschaufeln, etc. Man muss streng sicherstellen, dass die cbf bei 
nichts irgendwie bei irgendwas dazwischenfunkt. Und das geht nur, wenn 
die cbf erst nach Behandlung des INTs aufgerufen wird. Ja, das ist sehr 
dogmatisch, vermeidet aber seltsame Fehler wenn das Programm mal etwas 
komplexer wird. Bei einfachen Progrämmchen die nur mal schnell irnkwie 
was machen sollen kann man das ignorieren. Man sollte sich das aber 
garnicht erst angewöhnen.


> Nick schrieb:
>> Kapselung bedeutet auch, dass man den Anwender des Moduls vor allen
>> Fallen schützt.
>
> Das ist kaum machbar. Man kann auch Punkte offen lassen und die "Fallen"
> dokumentieren und manche Verantwortung dem Aufrufer überlassen.

Kann man machen. Aus Anwendersicht führt das aber zu kopierten code, 
kopierten code-Mustern. Immer ein Indiz für schlampigen code. Man kann 
solche Ausnahmen im implementierenden code (hier Modul 3) komplett 
kapseln und die Aufrufer müssen keine Klimmzüge machen, damit Modul 3 
richtig und wie erwartet arbeitet. Man steckt einmal Hirnschmalz in 
Modul 3 statt zig-mal in Modul X um die Unzulänglichkeit von Modul 3 
auszubügeln.

Ich schreib mein Zeug so, dass es wiederverwendbar ist, ohne 
Herrschaftswissen sondern mit einfachen, sicheren, klar definierten 
Schnittstellen. Ohne Fallen.

Modul, modular, ein in sich geschlossenes System. Das sollte das oberste 
Ziel sein. Auch wenns manchmal nicht ohne möglichst einfache 
Konventionen geht wie "Aufrufer muss den übergebenen pointer 
deallokieren".

Das ist ja auch die Kernfrage des TO. Sehr lobenswert sich da Gedanken 
drüber zu machen! Dafür geb ich ihm noch nachträglich ein "+". :-)

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Nick schrieb:
> dann muss man sich klar darüber sein, dass diese function INT-sicher
> sein muss

Das kann man ja beim Registrieren des Callbacks dokumentieren.

Nick schrieb:
> weil 16-Bit µC),

Die STM32 sind keine 16bit-uC.

Nick schrieb:
> Und das geht nur, wenn die cbf erst nach Behandlung des INTs aufgerufen
> wird.

Man kann auch
- Während der Bearbeitung kritischer Dinge die Interrupts sperren
- Andere kritische Dinge ebenfalls in Interrupts laufen lassen mit 
gleicher oder höherer Priorität
- Den Callback im Interrupt-Kontext aufrufen und im konkreten Callback 
erst bei Bedarf (!) einen "Task" in eine Warteschlange zur späteren 
Bearbeitung einreihen. Das ist auch im Linux-Kernel verbreitet und mit 
gängigen RTOSen auch möglich.

Nick schrieb:
> das ist sehr dogmatisch

In der Tat.

Nick schrieb:
> Bei einfachen Progrämmchen die nur mal schnell irnkwie was machen sollen
> kann man das ignorieren

Zählt der Linux-Kernel als "einfaches Progrämmchen" für dich?

Nick schrieb:
> Ich schreib mein Zeug so, dass es wiederverwendbar ist, ohne
> Herrschaftswissen sondern mit einfachen, sicheren, klar definierten
> Schnittstellen

Wie stellst du sicher dass die übergebenen Pointer garantiert gültig 
sind und nicht irgendwo ins Nirvana zeigen (nicht alles das ungleich 0 
ist ist gültig)? Wie stellst du sicher dass man deine Funktion nicht 
aufrufen kann wenn der Stack voll ist? Wie stellst du sicher dass die 
übergebenen I2C-Adresse garantiert korrekt ist?

Wie stellt Modul 3 sicher dass der Callback so schnell wie möglich 
aufgerufen werden kann, und auch andere, unwichtige Dinge unterbrechen 
kann? Das kann für Echtzeit-Systeme wichtig sein.

Nick schrieb:
> , damit Modul 3 richtig und wie erwartet arbeitet.

Was wenn ich erwarte dass mein Callback im Interrupt-Kontext aufgerufen 
wird, damit ich flexibel entscheiden kann ob ich sofort reagiere oder 
es auch später in einem "pending Task" passieren kann?

Nick schrieb:
> Modul, modular, ein in sich geschlossenes System

Das ist aber eben nicht modular. Wenn dein Modul 3 mit einem Task-System 
verdoppelt ist, kann man es nicht getrennt nutzen. I2C-Callbacks und 
Task Management sollten orthogonal sein. Der Aufrufer entscheidet ob er 
es zusammen verwenden möchte oder nicht.

von Nick (b620ys)


Lesenswert?

Niklas G. schrieb:
> Nick schrieb:
>> weil 16-Bit µC),
>
> Die STM32 sind keine 16bit-uC.

Das war ein Beispiel. Dann halt 64 Bit. Oder eine struct.

> Nick schrieb:
>> Und das geht nur, wenn die cbf erst nach Behandlung des INTs aufgerufen
>> wird.
>
> Man kann auch
> - Während der Bearbeitung kritischer Dinge die Interrupts sperren

Ja, damit verlagert man die Verantwortung in beliebig viele andere 
Teile. Oder pflastert alles mit locks und semaphoren zu. Ich seh das 
äusserst kritisch.

> - Den Callback im Interrupt-Kontext aufrufen und im konkreten Callback
> erst bei Bedarf (!) einen "Task" in eine Warteschlange zur späteren
> Bearbeitung einreihen. Das ist auch im Linux-Kernel verbreitet und mit
> gängigen RTOSen auch möglich.

Ich mach das auch so. Da brauchts auch kein RTOS dazu. Bei mir 
kommunizieren die Tasks über messages (und msg-queues). Den Aufwand 
wollte ich aber nicht vorschlagen.

> Nick schrieb:
>> das ist sehr dogmatisch
>
> In der Tat.

Sag ich ja. Macht man auch, wie du sagst, genauso dogmatisch im 
Linux-kernel.

> Nick schrieb:
>> Bei einfachen Progrämmchen die nur mal schnell irnkwie was machen sollen
>> kann man das ignorieren
>
> Zählt der Linux-Kernel als "einfaches Progrämmchen" für dich?

Nein. Willst du damit sagen, dass im Linux-Kernel eine sichere 
INT-Behandlung für alle möglichen Fälle quer durch den code verteilt 
ist? Eher nicht (ich kenn den nicht), aber Deine obige Aussage sagt 
"Nein".


> Wie stellst du sicher dass die übergebenen Pointer garantiert gültig
> sind und nicht irgendwo ins Nirvana zeigen (nicht alles das ungleich 0
> ist ist gültig)?
 rust kann das. :-)
Dass ein pointer nicht ins Nirvana zeigt ist Verantwortung des Moduls. 
Dass pointer-increments nicht ins nirvana zeigen ist Verantwortung des 
Anwenders oder der Programmiersprache (slices in rust). Deallokierte 
pointer werden natürlich immer auf NULL gesetzt, auch wenn es "unnötig" 
ist. Code kann sich nämlich verändern, Fallen bleiben bestehen. Und C 
bietet extrem viele Fallen an. Die muss man dann mit dogmatischen 
Vorgehen vermeiden. Ich kanns nicht grundlegend ändern. Wenn ich den 
code eines "Kollegen" anschaue (Java-Programmierer) der sich mal in C 
versucht hat und die Anwendung zuverlässig nach spätestens zwei Tagen 
abgestürzt ist, bestärkt mich das nur dogmatisch zu sein.
Oder der (bewusst dumme) Spruch eines Kollegen wenn das Programm 
kompilierte: "Formal ist es schon mal richtig". Ja, ich verwende auch 
lint, selbst wenn er manchmal wirklich nervt.


> Wie stellst du sicher dass man deine Funktion nicht
> aufrufen kann wenn der Stack voll ist?

Irgend einen Tod muss man sterben. Aber dafür gibt es auch tools die den 
Stackbedarf ermitteln.

> Wie stellst du sicher dass die
> übergebenen I2C-Adresse garantiert korrekt ist?

Fängt das Modul ab, das die Adresse verwendet. Defensive Programmierung. 
Z.B. asserts in der Test/Entwicklungsphase.

> Wie stellt Modul 3 sicher dass der Callback so schnell wie möglich
> aufgerufen werden kann, und auch andere, unwichtige Dinge unterbrechen
> kann? Das kann für Echtzeit-Systeme wichtig sein.

Das kann das Modul nicht sicherstellen. Das ist in der Verantwortung des 
Aufrufenden. Denn nur der weiß, wie eilig er es hat. Damit nichts 
rumliegt was noch nicht abgeschlossen ist, muss das Modul das handhaben. 
Entweder in einer queue oder die Annahme verweigern. Message-basiert ist 
das aber trivial zu lösen.

> Was wenn ich erwarte dass mein Callback im Interrupt-Kontext aufgerufen
> wird, damit ich flexibel entscheiden kann ob ich sofort reagiere oder
> es auch später in einem "pending Task" passieren kann?

Wenn Du das ausdrücklich so haben willst, dann musst Du eine zweite 
Funktion in dem Modul anbieten die genau das macht. Ich kanns nicht 
ändern, dass Du das so haben willst. Nur würde ich den einfacheren Weg 
primär umsetzen.

> Das ist aber eben nicht modular. Wenn dein Modul 3 mit einem Task-System
> verdoppelt ist, kann man es nicht getrennt nutzen. I2C-Callbacks und
> Task Management sollten orthogonal sein. Der Aufrufer entscheidet ob er
> es zusammen verwenden möchte oder nicht.

Wenn man ein Taskmanagement hat (ich hab nichts dagegen!), dann nimmt 
man das natürlich her. Wenn man verbissen genug ist, dann verlagert man 
das Task-Management in einen wrapper um das "Modul 3". Da kann man sich 
aber gerne drüber streiten. Meine Antwort wäre sehr stimmungsabhängig. 
:-)

von Frank K. (fchk)


Lesenswert?

Samuel schrieb:

> Das nicht wissen ist hier natürlich etwas konstruiert, bei diesem
> einfachen Fall kann man das sicher lösen. Möchte aber das ganze
> allgemein lösen, das ich in Zukunft belibige Sensoren immer gleich
> einbinden kann. Ich für den Sensor quasi nur noch das passende Header
> File einbinden muss und dann alles ohne weitere Logik im Hauptprogramm
> funktioniert.

Vielleicht schaust Du Dir mal NuttX an.
https://nuttx.apache.org/

Das ist ein RTOS, aber während das FreeRTOS, das auch bei CubeMX dabei 
ist, nur den Multitasking-Kernel liefert, enthält NuttX das komplette 
Paket, inkl Treiberlayer, Netzwerk- und USB Protokolle etc etc. Also 
quasi ein Linux in klein. Da hast Du auf der unteren Schicht Deine 
Bus-Treiber für I2C, SPI, GPIOs, ..., darüber dann die Chiptreiber für 
ADCs, DAC, PWM, GPIO-Externer etc, darüber dann die generischen 
Klassentreiber für GPIO, ADC, etc, und darauf Deine Applikation, die im 
Idealfall nur mit den generischen Klassentreibern spricht. Ob dann ein 
GPIO direkt am Prozessor dran ist, oder an einem SPI-GPIO-Extender ist 
dann egal.

Wenn Du ein fertiges generisches Framework suchst und das Rad nicht neu 
erfinden willst, wäre das einen Blick wert. Ein Alternativprodukt wäre 
Zephyr, das z.B. von Nordic verwendet wird. Während bei NuttX alles 
statisch gelinkt ist, arbeitet Zephyr mit Device Trees und dynamischem 
Linking, ist also noch etwas mehr high-level, aber durch auch etwas 
fetter.  NuttX ist relativ schlank und läuft problemlos auf TI TM4C und 
STM32F4/F7/H7.

fchk

von Joachim M. (jmlaser)


Lesenswert?

Wieso driftet das jetzt in ein RTOS ab?
Habe jetzt in der Ursprungsfrage nichts von RTOS oder Multitasking 
gelesen.
Ich kenne nun die Anwendung nicht, aber braucht es heute für jede 
Kleinigkeit gleich ein RTOS?

Samuel schrieb:
> einbinden kann. Ich für den Sensor quasi nur noch das passende Header
> File einbinden muss und dann alles ohne weitere Logik im Hauptprogramm
> funktioniert.

Irgendwie wirst Du die verschiedenen Sensoren ja eh auseinanderhalten 
müssen. Im einfachsten Fall durch eine Nummerierung bzw. aus der 
IIC-Adresse. Weitere Sensoren "automatisch" in der Anwendung zu 
"registrieren", indem man nur deren H-file benennt, ist schon etwas 
sportlicher.
Kleine Nebenfrage: Wieso gibt es in Windows eigentlich ein Registry?

Wenn Du z.B. spezielle Daten wie Kalibrierdaten oder Bereichsgrenzen 
oder Messwerteinheiten zusammen mit einem Funktionspointer für die 
Abarbeitungsroutine für jeden Sensor in eine Struktur packst und ein 
Array aus diesen Strukturen machst, kann beim Sensoraufruf mittels der 
Nummer bzw. Adresse auf den Strukturinhalt und den Funktionspointer 
zugegriffen werden.
Für 2 Sensoren jetzt vielleicht unnötig, aber wenn später mehrere dazu 
kommen, ist es flexibel.
Eventuell könnte Dein Hauptprogramm dieses Array dann aus den zur 
Verfügung stehenden Informationen der einzelnen h-files selbst 
zusammenbasteln.

Ich habe das mit dem Array bei einer komplexen Menüstruktur mit vielen 
Untermenüs gemacht. Da "weiß" die Anwendung außer einer eindeutigen 
Nummer des ausgewählten Menüpunkts zunächst auch nicht, was sie nun 
damit anfangen soll.
Es wird dem Hauptprogramm nur gemeldet dass ein Menüpunkt angewählt 
wurde.
Das kann ja auch ein Interrupt sein, wie in meinem Fall ein 
Encoderimpuls.
Da steht dann nur der aktuelle Zählerwert des Drehencoders.
Das Abarbeiten wird erst beim Zugriff auf die jeweilige Struktur für 
diesen einen Menüpunkt aus dem Array über den Zählerwert (Adresse) 
möglich, wo dann auch die Funktionsaufrufe über die Pointer ausgelöst 
werden, die entweder gleich sein können oder für jeden Punkt spezifisch, 
oder auch "mache nichts".

Der Vorteil ist, man kann für jeden Menüpunkt oder ein Deinem Fall für 
jeden Sensor noch spezifische Daten, z.B. Umrechnungsfaktoren o.ä. in 
die Struktur schreiben, die für jeden Sensor anders sein können.
In meinem Fall liegen da noch Min- bzw. Maxwerte drin, Zählinkremente 
für den Drehencoder, das Anzeigeformat Float, Dez oder Hex sowie Zeiger 
auf das jeweils übergeordnete Menü.

Im Sourcefile des jeweiligen Sensors würde die Auswertefunktion oder 
sonstige spezielle Funktionen für diesen einen Sensor stehen.
Im H-File des Sensors können dann die Deklarationen für die 
Funktionspointer und die sensorspezifischen Eigenschaften (weitere 
Strukturinhalte) stehen.
Irgendwo im Haupt- oder Initprogramm muss dann eben einmal das Array mit 
der Struktur für jeden Sensor in der korrekten Adressierung 
(Nummerierung) zusammengebaut werden.
Kommt ein Sensor dazu, wird der an das Array angehängt, NUM_SENSORS um 
eins erhöht und fertig.
Die Struktur muss eben für alle Sensoren passen.

Voraussetzung ist natürlich dass jeweils nur 1 Sensor oder in meinem 
Fall ein Menüpunkt ausgewählt wird. Geht ja technisch auch nicht anders.
Ich kann nicht 2 Menüpunkte gleichzeitig auswählen und auf dem IIC 
quatschen auch nicht mehrere Sensoren gleichzeitig.

"Modularisieren" ist natürlich immer gut, aber man kann gerade bei 
Beginn einer Entwicklung nicht immer alle nötigen Eigenschaften der 
Schnittstelle zwischen den Modulen eindeutig vorausplanen. SOLLTE, IST 
aber meistens nicht ;-)
Und übertreiben sollte man den Wunsch nach Modularisierung auch nicht, 
denn sonst kommt man irgendwann an den Punkt, sein eigenes OS entwickeln 
zu wollen.
Ich habe bei meinen Projekten oft erst später Teile nachträglich in 
Module gepackt und deren Schnittstellen optimiert, nachdem ich gesehen 
habe, dass ich diese auch wirklich öfters benutze. Die Mehrzahl wird 
aber meistens nur einmal bzw. selten benötigt, so dass es sich nicht 
lohnt, alles für alle Zeiten immer wieder für alle Projekte auf allen 
Systemen einsetzbar zu gestalten.
Ein Jahr später hat man meistens schon eine viel bessere Lösung für ein 
ähnliches Problem entwickelt und will das alte Zeug in der Art sowieso 
nicht mehr einsetzen. Und wenn man es will, gibt es garantiert 
irgendeine Kleinigkeit, warum das ach so perfekte Modul doch nicht passt 
und geändert bzw. eine Variante davon erstellt werden muss.

Gruß
Joachim

von Bruno V. (bruno_v)


Lesenswert?

Niklas G. schrieb:
> Bruno V. schrieb:
>> Arbeitest Du mit parallelen Tasks, kann die Funktionen blockierend sein
>
> Soll sie ja nicht, er hat explizit nach Callbacks gefragt. Und genau
> dieses Problem löst so ein Task Scheduler erstmal nicht.

Wen die Sensoren alle ihre eigene Task haben, dienen die Callbacks nur 
dazu, die Funktion quasi-blockierend zu machen.
 * Start_Auftrag und warte auf Flag
 * Callback löscht das Flag
 * und weiter gehts

Bei einer Task pro Sensor kann man das Datenblatt "so 
runterprogrammieren". Anfänger verwenden es daher gerne und fallen beim 
Callback auf RaceConditions rein. Blockierend ist da "idiotensicher". 
(ich bin von alledem aber kein Fan!)

: Bearbeitet durch User
von Norbert (der_norbert)


Lesenswert?

Man sollte sich zeitnah eine A380 zulegen, falls man mal wieder des 
Morgens Brötchen holen möchte.

von Samuel (samuel_a654)


Lesenswert?

Vielen Dank für die Diskussion. Gibt mir mal einige Gedankenanstösse.

Habe momentan nur zwei Sensoren. Das könnte ich wohl belibig simpel 
lösen und es würde funktionieren. Habe mir mehr gedacht wenn ich das 
jetzt "sauber" mache, kann ich das in Zukunft einfach erweitern und auch 
für andere Projekte benützten.
Ein RTOS wäre da für meine Anwendungsfall wohl etwas zu viel. Werde mal 
versuchen eine Version wie hier vorgeschlagen umzusetzen:

Nick schrieb:
> Wie ich das machen würde:Modul 1 behandelt Sensor A
> Modul 2 behandelt Sensor B
> Modul 3 behandelt die Schnittstelle.

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.