Forum: Mikrocontroller und Digitale Elektronik Programmaufbau / Verarbeitung vieler I/Os


von Programmierer (Gast)


Lesenswert?

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
int main () {
2
  setupTimer (); setupADC (); setupCAN ();
3
}
4
5
int canWert1;
6
void CAN_Receive_Interrupt () 
7
  canWert1 = getFromCANMessage ();
8
}
9
10
bool zustand_AN = false;
11
uint32_t counter = 0;
12
13
void TimerInterrupt () {
14
  sampleADC ();
15
16
  // Eingabewerte einlesen
17
  bool knopf1 = getGPIO_Knopf1 ();
18
  bool knopf2 = getGPIO_Knopf2 ();
19
  bool input = getGPIO_Input ();
20
  uint16_t adc1 = getADC_Wert1 ();
21
  uint16_t adc2 = getADC_Wert2 ();
22
  
23
  // Zähler für Delay an "input"
24
  bool inputDelayed = 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
  } else counter = 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
36
  zustand_AN = (zustand_AN && !knopf2) || (knopf1 && adc1 <= 600);
37
  
38
  setGPIO_bla (knopf1 && knopf2 && adc <= 600);
39
  setGPIO_blubb (adc2 >= 300 && canWert1 == 42 && inputDelayed);
40
}

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?

von stefanus (Gast)


Lesenswert?

Lies Dich mal zum Thema Zustandsautomat ein.

von Programmierer (Gast)


Lesenswert?

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.

von dunno.. (Gast)


Lesenswert?

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...)

von Karl H. (kbuchegg)


Lesenswert?

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.

von Programmierer (Gast)


Lesenswert?

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.

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.