Forum: Mikrocontroller und Digitale Elektronik Puffer in ISR füllen, in main mehrfach verarbeiten


von Jan K. (jan_k)


Lesenswert?

Hallo Leute :)

Ich habe ein Designproblem und hoffe, dass ihr mir etwas auf die Sprünge 
helfen könnt.

Folgende Ausgangssituation:

Der uC (stm32f100) liest in einer Timer ISR (alle 800us) einen Wert von 
extern ein. Es handelt sich um eine Position, die wirklich äquidistant 
abgetastet werden muss, da Geschwindigkeit, Beschleunigung etc daraus 
noch berechnet werden soll.

Der Wert wird in einen Puffer geschrieben, der so aussieht:
1
typedef struct {
2
  uint16_t raw[500]; 
3
  int32_t abs[500];
4
  uint16_t pos;  // AKTUELLE!! Position im Puffer, da liegt also der letzte Datensatz
5
  uint8_t  done; // flag zeigt an, ob Puffer bearbeitet wurde. 
6
  uint8_t  overrun; // flag zeigt an, ob overrun aufgetreten ist
7
  } positionBufferStruct;

Die ISR sieht so aus (Auszüge):
1
volatile positionBufferStruct counterBuffer = {0,0,0,0};
2
volatile uint8_t tim2Flag = 0;
3
...
4
void TIM2_IRQHandler(void)
5
{                        
6
  if(counterBuffer.pos == 500) // 500 gibt es nicht mehr! von vorne anfangen 
7
  {
8
    counterBuffer.pos = 0;
9
    counterBuffer.overrun = 1; // Überlauf passiert..
10
  }
11
  else if(counterBuffer.done == 1)// sollte Standardfall sein -> direkte Abarbeitung in main. z.B. wenn UART aus ist.
12
  {
13
    counterBuffer.pos = 0;
14
  }    
15
  else // Pos < 499
16
  {
17
    counterBuffer.pos++; // vorher erhöhen, denn pos gibt Position des letzten Datensates an.
18
  } 
19
  
20
  counterBuffer.raw[counterBuffer.pos] =  TIM2 -> CNT;
21
  tim2Flag = 1;  
22
  TIM2->SR &= ~0x0001;   // Pending Interrupt Bit löschen
23
}
In der main werden, wenn die tim2Flag gesetzt ist, ein paar Berechnungen 
angestellt und - falls aktiviert - ein UART Puffer erstellt (dieser 
enthält 1000 Positionswerte und wird dann blockweise geschickt).
Laut eurem Wiki sollten diese lange dauernden Prozeduren nicht in die 
ISR, deswegen die Flag.

Normalerweise dauert die main mit allem drum und dran weit weniger lang 
als 800us. Aber wenn der UART Puffer voll ist, dauert die Sendeprozedur 
(wird auch in der main aufgerufen) etwa 90ms. In dieser Zeit läuft der 
Timer Interrupt etwa 112 Mal.

Die struct ist jetzt nur eine Idee. Vorher war es so, dass einfach eine 
globale Variable mit TIM2->CNT in der Timer ISR erstellt wird.
Im oben angesprochenen Fall (UART Puffer soll gesendet werden) würden so 
111 Werte verworfen werden. Das ist nicht gut.
Daher die Idee mit der struct, in der wirklich alle Werte gespeichert 
werden, solange die UART sendet und dann, wenn die main Schleife wieder 
läuft abgearbeitet werden.
Es gibt drei Funktionen, die an die Werte müssen:
1
void positionCalc;
2
void doPID();
3
void fillLog();

positionCalc berechnet aus den Rohwerten des TIM2 die echte, absolute 
Position. Dieser Wert wird für doPID benötigt. doPID macht eine 
Positionsregelung.

fillLog soll sowohlauf die absoluten (in positionCalc berechneten), als 
auch auf die Rohwerte (TIM2->CNT aus der ISR) zugreifen können.

So sieht es momentan aus, ich hoffe das war ausführlich genug, aber 
nicht zu sehr geschwafelt.

Jetzt mein Problem:
In positionCalc wird also der Puffer von 0 bis counterBuffer.pos 
durchgegangen, die absolute Position berechnet und in die struct an den 
selben Positionsindex (also counterBuffer.abs[counterBuffer.pos]=...) 
geschrieben.

LogFill wird danach ausgeführt und möchte, je nach eingestelltem Log 
Modus die Rohdaten (counterBuffer.raw), die Absolutdaten 
(counterBuffer.abs) oder beides haben.
Also wird der Puffer erneut durchlaufen und die benötigten Werte werden 
ausgelesen.
Ist das vollständig passiert, wird die done Flag gesetzt.

In der ISR wird ja geprüft, ob der Puffer bearbeitet wurde (done Flag), 
und fängt dann wieder von vorne an.

Für mich ist die große Frage: was passiert, wenn zwischen den beiden 
Funktionen postitonCalc und logFill ein weiterer Interrupt auftritt. In 
der ISR wird die Pufferposition dann um 1 erhöht, ein neuer raw Wert 
wird reingeschrieben. Die Logfill funktion bekommt das dann noch mit und 
kann einen abs Wert in den Puffer schreiben. Dann wird allerdings die 
done Flag gesetzt -> logFill wird nie mitbekommen, dass zwischen den 
beiden Funktionen noch ein Wert dazu kam.
Anders herum - ich setze die done Flag bereits nach der Berechnung, 
sieht logFill nur einen einzigen Wert, weil ja der Puffer resettet 
wurde. Das wäre noch schlechter.

Ich dachte daran den Interrupt für die Zeit auszuschalten, aber es ist 
äußerst wichtig, dass die Position zu äquidistanten Zeitpunkten 
abgetastet wird, da sie sich unter Umständen ziemlich schnell ändern 
kann und ich somit große Fehler in der Geschwindigkeitsberechnung 
bekommen könnte.

Gibt es eine elegante Lösung für das Problem hier - und: wie wird das 
sonst gemacht? Es wird ja häufiger vorkommen, dass die main Schleife 
etwas länger dauert..

Ich hoffe, mein Anliegen wird klar. Ich würde mich über Hilfe sehr 
freuen!

Danke fürs Lesen :)

von Uwe (de0508)


Lesenswert?

Hallo Jan,

du willst die Daten asyn. verarbeiten, dazu verwendet man einen FiFo der 
als Ringpuffer implementiert wird.

Es gibt ein Array für die Daten und je ein Lese- und ein Schreibzeiger. 
In unserem Fall als Index auf die Position im Array.

Dann noch einige Funktionen, so solltest Du den Puffer auch 
strukturieren, zum:

Funktion: Lesen eines Datums aus dem Puffer, Lesezeiger erhöhen
Funktion: Schreiben eines Datums in den Puffer, Schreibzeiger erhöhen.
Funktion: Puffer leer (lesen), wenn (Schreibzeiger == Lesezeiger) ist.
Funktion: Puffer voll (schreiben, wenn (Schreibzeiger+1 == Lesezeiger) 
ist.


Hier noch ein Beispiel in Peters "Software UART mit FIFO":

Beitrag "Software UART mit FIFO"

Die Implementierung ist sehr schnell und einfach zu verstehen.


.

von Jan K. (jan_k)


Lesenswert?

Okay, danke für die Idee + den Link.

Würde ich mein Datenfeld dann zweidimensional machen (abs + raw Werte), 
oder zwei getrennte Puffer anlegen?

z.B. ein Puffer für die raw Daten: Schreib-Zeiger, der auf das als 
nächstes zu beschreibene Feld zeigt und ein Lese-Zeiger, der auf das 
nächste zu lesende Element zeigt. Aber die Daten werden ja mehrmals 
gelesen. Einmal in der positionCalc Funktion (da dann in einen zweiten 
Puffer schreiben?) und dann auch in der fillLog Funktion. Jetzt wird nur 
der Inhalt nicht gelöscht, sondern ringsrum geschrieben. Aber das 
Problem ist doch ähnlich wie oben?
Zuerst kommt positionCalc. Es wird der Puffer durchgegangen, zu jedem 
raw Wert der abs Wert berechnet und in einen zweiten Puffer geschrieben.
Dann kommt der Interrupt, ein zusätzlicher Wert kommt dazu. Kein 
Problem, der wird irgendwann später verarbeitet.
Aber jetzt kommt die FillLog Funktion. Die kann zwar jetzt den neuen 
Puffer mit den abs Werten auslesen (lese Zeiger), aber der Lese-Zeiger 
der raw Werte steht ja schon irgendwo. Aber ich muss ja auch noch die 
vorherigen Daten (nochmal) auslesen (zum zweiten Mal..).

Nimmt man dann einen zusätzlichen (lese-) Zeiger - für jede Funktion 
einen, oder speichert man die in positionCalc bereits ausgelesenen Daten 
in einem Array, das allerdings dann dynamisch erzeugt werden müsste, 
denn ich weiß ja nicht, wie viele Daten gelesen werden können. Dann 
bräuchte ich nur einen Lesezeiger.
Die zweite Möglichkeit ist sicherlich nicht sinnvoll.

Danke & schöne Grüße.

von Jan K. (jan_k)


Lesenswert?

Noch eine kleine Anmerkung:

Ich kann auch beides in eine Funktion packen, sodass nur einmal gelesen 
werden muss. Aber ich möchte eigentlich die Funktionen calc / logging 
voneinander trennen, um z.B. das logging auszuschalten.

von Narfie (Gast)


Lesenswert?

Hi,

da du einen STM32F100 benutzt kannst du den DMA für das versenden des 
UART Puffers benutzen, damit läuft die Übertragung im Hintergrund und du 
kannst normal weiterarbeiten. In dieser Zeit darfst du nat. nicht in den 
UART Puffer schreiben. Lösung u.U. zwei Puffer mit der halben Größe und 
dann jeweils abwechselnd beschreiben, musst nat. schauen wie du mit der 
Zeit hinkommst...

Viele Grüße,
Narfie

von Jan K. (jan_k)


Lesenswert?

Auch eine Möglichkeit, die ich in Betracht ziehen werde.

Momentan probiere ich aber einen FIFO Puffer mit einem Schreibzeiger und 
mehreren Lesezeiger zu implementieren, komme aber dank nicht vorhandenen 
Klassen arg durcheinander.

Es besteht in C nicht die Möglichkeit, eine Funktion einer struct 
zuzuordnen, so wie in C++ eine Klasse Funktionen enthält, oder?

von Karl H. (kbuchegg)


Lesenswert?

Jan K. schrieb:
> Auch eine Möglichkeit, die ich in Betracht ziehen werde.
>
> Momentan probiere ich aber einen FIFO Puffer mit einem Schreibzeiger und
> mehreren Lesezeiger zu implementieren, komme aber dank nicht vorhandenen
> Klassen arg durcheinander.

Hä?

Ein C++ Konstrukt
1
class A
2
{
3
  public:
4
    void foo();
5
6
  private:
7
    int member;
8
};
9
10
void A::foo()
11
{
12
  this->member = 2;
13
}


lässt sich in C schreiben als
1
struct A
2
{
3
  int member;
4
};
5
6
void foo( struct A* this )
7
{
8
  this->member = 2;
9
}

Die ersten C++ Compiler (und wohl auch einige heutige noch), haben genau 
nach solchen (bzw. ähnlichen) Schemata ein C++ Programm nach C 
übersetzt, um es dann dem bereits vorhandenem C Compiler vorzuwerfen.

Also das ist kein wirklich gutes Argument. Wenn dus mit Klassen 
schreiben kannst, kannst du es auch ohne. Klassen sind bei dieser 
Aufgabenstellung syntaktischer Zucker - mehr nicht.

von Jan K. (jan_k)


Lesenswert?

Danke für den Tip, wusste nicht, dass ich das in ansi C machen kann.

von Jan K. (jan_k)


Lesenswert?

Habe mich damit jetzt mal vertraut gemacht, bis auf die Übergabe von &A 
ist es ja wirklich gleich handzuhaben.

Jetzt ist allerdings die Frage, ob man das überhaupt so machen sollte, 
oder ob man das "einfach so" runterschreibt. Also einen zweiten 
Lesezeiger "manuell" erstellen und gut is.

Sollte man in C (speziell im embedded Bereich) trotzdem versuchen die 
Funktionen zu kapseln und Klassen mit structs nachzuahmen?

(sorry für die blöden Fragen, aber ich habe C++ gelernt, da wurde von 
Anfang an Richtung OOP gedacht..)

Danke + schöne Grüße!

von Karl H. (kbuchegg)


Lesenswert?

Jan K. schrieb:
> Habe mich damit jetzt mal vertraut gemacht, bis auf die Übergabe von &A
> ist es ja wirklich gleich handzuhaben.
>
> Jetzt ist allerdings die Frage, ob man das überhaupt so machen sollte,
> oder ob man das "einfach so" runterschreibt. Also einen zweiten
> Lesezeiger "manuell" erstellen und gut is.

Du brauchst sowieso einen 2-ten Lesezeiger.
Klassen helfen dir an dieser Stelle genau gar nichts. Niente, Nada, 
Nothing,

> Sollte man in C (speziell im embedded Bereich) trotzdem versuchen die
> Funktionen zu kapseln und Klassen mit structs nachzuahmen?

Es gibt im Embedded Bereich, auf den kleinen µC nur wenige Dinge, die 
von Klassen bzw. den damit dann zusammenhängenden Konzepten Vererbung 
und Polymorphie, tatsächlich guten Gebrauch machen können. Ganz im 
Gegenteil werden Verfahren dadurch oft aufgebläht, weil der Compiler 
dann nicht mehr auf einen spezifischen Port hin optimieren kann, sondern 
in Klassen dann vieles generisch gelöst wird. Templates helfen da zwar 
ein wenig, aber meistens sind die Programme nicht so umfangreich, dass 
sich das wirklich auszahlen würde, da ein großes Framework anzulegen.

von Gebhard R. (Firma: Raich Gerätebau & Entwicklung) (geb)


Lesenswert?

Mir kommt als Lösung dieses Problems die beliebte Doppelbuffer Strategie 
in den Sinn. In einen Buffer wird geschrieben, der andere verarbeitet. 
Einfach, Fehlerunanfällig und effizient.

Grüsse

von Jan K. (jan_k)


Lesenswert?

Ich muss diesen Thread nochmal hochholen.

Ich versuche gerade die ganze Puffer- bzw Uartgeschichte etwas zu 
vereinfachen, momentan sieht es so aus:

In der ISR wird ein aktueller Wert vom Encoder Eingang gelesen und in 
eine globale Variable gespeichert. Dann wird in der ISR die Funktion 
berechnePosition() aufgerufen, die die absolute Winkelposition bestimmt. 
Diese Position wird für die PID Regelung benötigt.

Der jeweils absolute Winkelwert wird in einen Ringpuffer gespeichert, 
bei jedem Aufruf von berechnePosition(). Da die Winkelposition 32Bit 
groß ist, der Puffer aber nur 16Bit (hat "historische" Gründe, habe 
vorher nur Halbwörter puffern müssen...) wird sie in zwei 16 Bit 
Ringpuffer geschrieben (hier werde ich nochmal überlegen, ob ich nicht 
32Bit Puffer verwende, und wenn ich 16 Bit Daten puffern möchte, jeweils 
zwei in ein 32 Bit Feld schreibe [oder macht der Compiler das 
möglicherweise sogar automatisch hinterher?]).

Falls aktiviert wird dann die PID Funktion aufgerufen. In dieser wird 
die Stellgröße zu Loggingzwecken in einen dritten Ringpuffer 
geschrieben.

Bisher gibt es also 3 Ringpuffer (stellgröße, absWinkelHigh, 
absWinkelLow), die ständig laufen. Die Größe beträgt jeweils 300 
Elemente, das reicht aus, um eventuell auftretende Verzögerungen der 
main (z.B. UART senden) auszugleichen.

Dann gibt es die Möglichkeit, das Logging einzuschalten. Wenn das 
passiert, wird in der main die logging Funktion ausgeführt, die prüft, 
ob die Puffer leer sind (bzw keine neuen Elemente hat). Ist das nicht 
der Fall, wird das eigentliche zu sendende Array erstellt. Das ist 1042 
Bytes groß, hat eine Checksumme, einen Anfangs- und einen Endframe zu 
Identifizierung. Dazwischen befinden sich die Daten aus den einzelnen 
Ringpuffern.
Ich verschicke das blockweise, damit ich nicht so viel Overhead habe. 
Das bedeutet aber, dass ich einen weiteren, noch größeren UART Puffer 
brauche (eben mit 1042 Elementen). Dieser wird dann, wenn er voll ist 
verschickt. Noch mit der unglücklichen "Wartemethode", die die main 
stilllegt. (aus diesem Thread : 
Beitrag "Regelalgorithmus in ISR?" )

Mir kommen diese vielen Puffer so umständlich vor. Wenn ich eine weitere 
Größe loggen wollte, müsste ich noch einen weiteren Ringpuffer benutzen.

Daher ist die Frage: Wie vereinfache ich diese Struktur?

Selbst, wenn ich die UART asynchron senden lasse, wie ihr mir das oben 
beschrieben habt, benötige ich dennoch die "kleinen" Ringpuffer, da es 
sein kann, dass die main trotzdem länger dauert, als eine 
Interruptperiode lang ist. Z.B. wenn auf Eingaben von außen reagiert 
werden soll.

"Baut" man überhaupt diese großen zu sendenden Arrays vorher zusammen 
und schickt sie dann erst?

Danke fürs Lesen und kommende Denkanstöße =)

von Snowowl (Gast)


Lesenswert?

Noch eine Anmerkung zum 1. Post:

counterBuffer.pos hat einen Wertebereich von 0-500 (und nicht von 0-499) 
da bei counterBuffer.pos!=500 (else Zweig) die Variable vor der 
Verwendung incrementiert wird.

von Jan K. (jan_k)


Lesenswert?

Danke für die Info, habe dort mittlreweile einen Ringpuffer eingebaut.

von W.S. (Gast)


Lesenswert?

Ich habe so den Eindruck, daß du deine Aufgabenstellung einfach noch 
nicht wirklich zu Ende gedacht und deswegen bislang nur Murks 
zusammengeschrieben hast.

Also: Einen Puffer (auch einen Ringpuffer) benutzt man dazu, irgendetwas 
zwischenzuspeichern, das zu einer Zeit hereinkommt, wo man nicht 
vorhersehen kann, ob man genau zu diesem Zeitpunkt auch Zeit dafür 
hat. Die vorhergesehene Verarbeitung ist also asynchron und SPÄTER.

Ein Interrupt-Programm benutzt man dafür, Dinge die genau jetzt und 
sofort bearbeitet werden müssen, zu erledigen.

Wenn du also irgendwelche Inputwerte verarbeiten willst und dazu 
innerhalb der Zeit, die sich eine Interrupt-Routine bei deiner Anwendung 
maximal gönnen darf, keine Gelegenheit hast, dann merke die Rohwerte 
eben in einem Ringpuffer und verarbeite sie später irgendwann.

Sowas wie
typedef struct {
  uint16_t raw[500];
  int32_t abs[500];
  ....etc.

sind kein Ringpuffer sondern Murks.
(btw: warum schreibst du die geschweifte Klammer hinter struct und nicht 
auf den Beginn der neuen Zeile? Du willst wohl partout nicht den Block 
optisch erkennen können?)

Also, ein Ringpuffer siehr eher so aus:

struct MyBlabla
 { int A;
   float B;
   bool C;
   etc. etc.
 };

struct MyBlabla Puf[512];
int Wrzeig;
int Rdzeig;

Mach dir lieber Gedanken über den Zeitfonds, also wie deine Anwendung 
sich verhalten soll wenn der Puffer überlaufen sollte. Das ist 
eigentlich das allerwichtigste an der ganzen Entwicklung. Das 
Gegenteil, also Puffer leer, sollte ja kein Problem sein.

W.S.

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.