Hallo Forum,
wie macht man so etwas eigentlich typischerweise:
Ich habe ein µC-Programm für eine Art Maschinensteuerung das eine große
Anzahl an Eingabedaten hat (GPIO-Pins, ADC-Werte, Daten per
CAN-Nachricht), aus denen eine ebenfalls große Anzahl an Ausgabewerten
(GPIO-Pins, CAN) berechnet werden soll.
Diese Berechnung soll "kontinuierlich" geschehen, d.h. wenn sich ein
Eingabewert ändert sollen "sofort" (Verzögerung bis 20ms ist OK) die
Ausgabewerte entsprechend angepasst werden.
Einige der Eingaben erfordern "Delays", z.B. muss ein Pin 100ms auf '1'
sein damit das Programm ihn als '1' akzeptiert und reagiert.
Es gibt einen kleinen internen Zustandsraum, d.h. wenn die Eingabewerte
bestimmte Konstellationen annehmen, sollen ein paar Variablen geändert
werden, die sich dann auf die Ausgabewerte auswirken (ala Taster
gedrückt & kein Fehler gefunden -> Maschine an - bleibt an auch wenn
Knopf wieder aus).
Die GPIO und CAN Werte sind quasi asynchron, d.h. können sich jederzeit
ändern, während die ADC Werte regelmäßig gesamplet werden und dann
später abgefragt werden müssen.
Das ganze wird in C++ auf einem STM32F3 programmiert.
Ich habe das jetzt so gelöst dass ich einen Timer so eingestellt habe
dass alle 0.5ms ein Interrupt kommt, in dessen ISR dann die Verarbeitung
der Eingaben geschieht. Somit ist die Verzögerung immer max. 0.5ms. Um
die Verzögerung bei den Eingabewerten zu realisieren habe ich Zähler
eingebaut, es wird bei o.g. Eingang die Anzahl Zyklen gezählt die der
Eingang '1' ist, und wenn er bei 200 angekommen ist wird der Pin als '1'
erkannt). Der interne Zustand wird schlicht als globale Variable
gespeichert. Pseudocode:
1
intmain(){
2
setupTimer();setupADC();setupCAN();
3
}
4
5
intcanWert1;
6
voidCAN_Receive_Interrupt()
7
canWert1=getFromCANMessage();
8
}
9
10
boolzustand_AN=false;
11
uint32_tcounter=0;
12
13
voidTimerInterrupt(){
14
sampleADC();
15
16
// Eingabewerte einlesen
17
boolknopf1=getGPIO_Knopf1();
18
boolknopf2=getGPIO_Knopf2();
19
boolinput=getGPIO_Input();
20
uint16_tadc1=getADC_Wert1();
21
uint16_tadc2=getADC_Wert2();
22
23
// Zähler für Delay an "input"
24
boolinputDelayed=false;
25
if(input){
26
if(counter==200)
27
// Eingabe erst akzeptieren Wenn Zähler = 200 (= 100ms)
28
inputDelayed=true;
29
else
30
++counter;
31
}elsecounter=0;
32
33
// Verarbeitung der Eingabewerte und Berechnung der Ausgabewerte
34
35
// Internen Zustand ändern: Maschine einschalten wenn knopf gedrückt oder an lassen oder abschalten
Macht das so ungefähr Sinn? Wird das üblicherweise so gemacht? Man
könnte ja auch direkt auf Änderung der Werte reagieren per
Pin-Change-Interrupts, ADC-Interrupt, CAN-Interrupt - aber das wäre auch
kaum schneller oder?
Der Zähler für den verzögerten Eingang ist ja im Prinzip ein delay() das
aber das Programm nicht anhält, sondern gleichzeitig die Verarbeitung
anderer Eingaben zulässt. Macht das Sinn?
Angenommen ich würde ein RTOS mit Multi-Threading verwenden, gäbe es
Dinge die man hier sinnvollerweise auf mehrere Threads aufteilen könnte,
um den Code zu verschönern? z.B. die Eingabe-Verzögerung?
stefanus schrieb:> Lies Dich mal zum Thema Zustandsautomat ein.
Habe ich eine ganze Vorlesung zu gehabt, sollte reichen. Der interne
Zustandsraum ist im Endeffekt ein endlicher Automat, und die Bedingungen
in der Timer-ISR für die Änderung sind die Zustandsübergänge.
Programmierer schrieb:> Macht das so ungefähr Sinn? Wird das üblicherweise so gemacht?
Nein. Entweder du lagerst die Funktionseinheiten in einzelne Threads
aus, oder du verwendest Interrupts, zumindest für ADC/CAN.. Warum 1000
mal schauen ob sich was getan hat, wenn ich auch drauf hingewiesen
werden kann?
Programmierer schrieb:> Man könnte ja auch direkt auf Änderung der Werte reagieren per> Pin-Change-Interrupts, ADC-Interrupt, CAN-Interrupt - aber das wäre auch> kaum schneller oder?
Vielleicht ist es nicht unbedingt schneller, aber die Reaktion erfolgt
bei korrekter Programmierung immer innerhalb einer bestimmten
Zeitspanne, für die du dann Worst-Case werte angeben kannst, um bei
deiner Zeitanforderung zu bleiben.
Was passiert denn wenn du ein CAN-Telegramm in deiner Riesenschleife
verarbeitest? Das verzögert den Programmablauf deutlich.
Generell gilt: sowenig Anweisungen wie möglich im Interrupt. So selten
Interrupts wie möglich (auch dein Timer ist einer, berechne mal,
wieviele Anweisungen dein µC in 500µS schafft, und ob du damit
auskommst...)
Das wichtigste
Hier
> Einige der Eingaben erfordern "Delays", z.B. muss ein Pin 100ms auf> '1' sein damit das Programm ihn als '1' akzeptiert und reagiert.
darfst du nicht in Delay EInheiten im sinne es programmierten Delay
denken.
Wenn du die Änderung dtektierst, dann 'startest' du eine Art Uhr, die
protokolliert, wie lange das Signal auf Zb '1' ist. Erst nach Ablauf der
Mindestzeit wird dann diese Änderung an den Zustandsautomaten weiter
gegeben, bzw. wird diese Mindestzeit in den Automaten eingebaut.
Derartige Zeitstruerungen laufen praktisch immer auf das Zusammespiel
eines Timers mit einer Interrupt Routine hinaus. Der Timer realisiert
eine Uhr mit hoher Auflösung und die Interrupt Routine realisiert die
eigentliche Uhr, in der dann zb Variablen entsprechend hoch oder runter
gezählt werden.
Die Anbindung der Interrupt Routine an den Timer stellt dir quasi deinen
Basistakt in Form der kleinsten Zeiteinheit zur Verfügung. 20ms
abzuzählen ist dann ja nichts anders als ein Mitzählen, wie oft diese
Interrupt Routine aufgerufen wurde.
Persönlich nehme ich dafür gerne Down-Counter. Ich finde die etwas
einfacher zu handhaben, als umgekehrt. Irgendein Code (zb der Code, der
die Flanke detektiert) stellt die Variable auf einen entsprechenden
'Delay' Wert ein und in der Interrupt Routine wird dieser Wert bei jedem
Aufruf um 1 verringert. Erreicht die Variable den Wert 0, dann ist die
vorgesehene Zeit abgelaufen und die Interrupt Routine setzt ein Flag,
oder schaltet einen Ausgang, was aber in deinem Fall wohl nicht so
zielführend sein wird. Im Falle eines Zustandsautomaten stellt die
Interrupt Routine dann zb. den Zustand der Zustandsmaschine auf den
Nachfolgezustand des Wartezustands.
Wie das dann konkret implementiert wird ... da gibt es viele
Möglichkeiten. Wichtig ist jedoch, dass du keine Delay-Funktion benutzt,
die nur sinnlos Taktzyklen abarbeitet. Zeitsteuerungen bedingen
praktisch immer den Einsatz eines Timers. Dort liegt der Schlüssel zu
Programmen, die quasi gleichzeitig viele Aufgaben erledigen, selbst wenn
diese Aufgaben Wartezeiten beinhalten.
So gesehen hast du bis jetzt alles richtig gemacht, auch wenn man
darüber diskutieren kann, welche Aktionen in der ISR gemacht werden
sollen und welche nicht.
dunno.. schrieb:> Programmierer schrieb:>> Macht das so ungefähr Sinn? Wird das üblicherweise so gemacht?>> Nein. Entweder du lagerst die Funktionseinheiten in einzelne Threads> aus, oder du verwendest Interrupts, zumindest für ADC/CAN.
Mache ich auch, habe nur im Pseudocode den ADC-Int nicht hingeschrieben.
> Warum 1000> mal schauen ob sich was getan hat, wenn ich auch drauf hingewiesen> werden kann?
Und was wenn man nicht alle Pins auf einen Pin Change Interrupt legen
kann? Die STM32 können zB nur 16 so verarbeiten. Den Rest muss man wohl
oder übel manuell abfragen.
Die ADC-Werte kommen ja onehin ständig neu, da muss ich eh ständig neu
berechnen, da kann ich das auch gleich in einem Timer Interrupt machen
um etwas freier in der Frequenz zu sein (und nicht die "krumme"
ADC-Frequenz nehmen zu müssen).
> Programmierer schrieb:>> Man könnte ja auch direkt auf Änderung der Werte reagieren per>> Pin-Change-Interrupts, ADC-Interrupt, CAN-Interrupt - aber das wäre auch>> kaum schneller oder?>> Vielleicht ist es nicht unbedingt schneller, aber die Reaktion erfolgt> bei korrekter Programmierung immer innerhalb einer bestimmten> Zeitspanne, für die du dann Worst-Case werte angeben kannst, um bei> deiner Zeitanforderung zu bleiben.
Ist bei regelmäßiger Abarbeitung auch so, immer 0.5ms.
> Was passiert denn wenn du ein CAN-Telegramm in deiner Riesenschleife> verarbeitest? Das verzögert den Programmablauf deutlich.
Schleife? Da gibts nirgendwo eine Schleife. Die Abarbeitung der
CAN-Telegramme erfolgt wie gezeigt im CAN-Interrupt. Außerdem ist die
CAN-Verarbeitung nicht sonderlich kompliziert, einfach nur ein paar
Bytes aus Registern holen...
> Generell gilt: sowenig Anweisungen wie möglich im Interrupt.
Warum?
> So selten> Interrupts wie möglich (auch dein Timer ist einer, berechne mal,> wieviele Anweisungen dein µC in 500µS schafft, und ob du damit> auskommst...)
Bis jetzt ja. 36000 Takte Zeit habe ich da. Wo soll ich die Berechnung
sonst machen? In der main()-Schleife? Was ist der Vorteil gegenüber sie
im Timer-Interrupt zu machen?
Karl Heinz schrieb:> Wenn du die Änderung dtektierst, dann 'startest' du eine Art Uhr, die> protokolliert, wie lange das Signal auf Zb '1' ist. Erst nach Ablauf der> Mindestzeit wird dann diese Änderung an den Zustandsautomaten weiter> gegeben,
Genau das mache ich ja...
Karl Heinz schrieb:> dass du keine Delay-Funktion benutzt,> die nur sinnlos Taktzyklen abarbeitet.
Ja habe ich ja auch nicht gemacht.