Forum: Mikrocontroller und Digitale Elektronik cli() und sei()


von Erwin U. (erwin_u)


Lesenswert?

Ich programmiere gerade serielle Routinen, die interruptgesteuert Daten 
vom Arduino zum PC schicken sollen, d.h. im ATMEGA8 gibt es einen 
Buffer, der mittels Interrupt USART_UDRE_vect "abgearbeitet" wird. Wenn 
der Transmitter meldet, dass das Zeichen gesendet worden ist, wird 
mittels Interrupt das nächste Zeichen zum Versand gegeben.
Das funktioniert auch schon ganz gut. Ich habe jetzt im Hauptprogramm 
(also nicht in der Interruptroutine) 2 Fälle:
1. genug Platz noch im Buffer, kein Problem, ich kann die Daten in den 
Buffer geben zum Versand
2. der Buffer ist voll. In dem Fall muss ich wohl warten, bis die 
Interruptroutine meldet, dass sie fertig ist, damit ich das nächste 
Zeichen senden kann.

Das ist alles klar. Beim Warten muss ich natürlich immer wieder den 
Buffer abfragen, ob schon Platz ist, daher muss ich hier kurzfristig die 
Interrupts sperren, schauen, ob Platz ist, und natürlich die Interrupts 
wieder freigeben, damit der Interrupt passieren kann.

Und jetzt endlich die Frage: in dieser Schleife hatte ich den Code in 
etwa
1
while (buffer_voll)
2
{
3
  sei();
4
  cli();
5
}
Und jetzt das Problem: Hier kommt offensichtlich der Interrupt nicht 
immer durch. Es funktioniert, wenn ich schreibe
1
while (buffer_voll)
2
{
3
  sei();
4
  _delay_us(1);
5
  cli();
6
}
Heisst das, dass nach einem sei() immer ein anderer Befehl gemacht 
werden muss, damit die Interrupts verlässlich durchkommen? Oder gibt es 
hier einen anderen Effekt? Wie gesagt, es funktioniert manchmal, und 
manchmal bleibt er hängen, ich vermute, er läuft in dem Fall endlos in 
der Schleife.

ps.: volatile ist in Verwendung, das ist es nicht.

: Bearbeitet durch User
von Michael H. (mha1)


Lesenswert?

Dein Fall ist extrem ungewöhnlich. Warum sei() und cli() direkt 
hintereinander? Damit hat die CPU quasi keine Zeit für einen Interrupt.

Interrupts schaltet man nur ab, wenn es unbedingt notwendig ist. Die 
Pufferverwaltung bei der seriellen Übertragung gehört normalerweise 
nicht dazu. Das geht problemlos bei eingeschalteten Interrupts.

Deine while-Schleife implementiert ein Busy-Wait und verbraucht viel CPU 
Zeit und damit auch Strom. Das delay ist aus diesen Gründen durchaus 
sinnvoll. Aber 1µs ist extrem kurz. So schnell wird deine serielle 
Übertragung nicht sein.

Ich benutze für sowas normalerweise einen Puffer mit z.B. 16 Zeichen und 
einem Lese- und einem Schreib-Zeiger (beide volatile). Damit sind beide 
Seiten voneinander unabhängig.

Wenn der Puffer der seriellen Schnittstelle voll ist und nichts anderes 
zu tun ist, würde ich den Arduino in den Sleep Mode schicken. UART und 
andere Peripherie können weiterlaufen. Aus dem Sleep Mode kann der 
Arduino bei einem Interrupt oder nach einem Timeout automatisch 
aufwachen.

von Rainer W. (rawi)


Lesenswert?

Erwin U. schrieb:
> ... daher muss ich hier kurzfristig die Interrupts sperren, schauen, ob Platz 
ist, ...

Warum musst du dazu den Interrupt sperren? Wie stellst du fest, dass 
Platz in deinem Puffer ist? Es kann dir doch egal sein, ob du den freien 
Pufferplatz in der aktuellen oder in der nächsten Abfrage feststellst. 
Was ist dein eigentliches Problem, dass du mit dem Sperren und wieder 
Freigeben der Interrupts lösen möchtest?

von Peter (pittyj)


Lesenswert?

So etwas bizarres habe ich selten gesehen.
Gerade auf einem Arduino ist doch genug Code vorhanden, der das schon 
macht.
Oder Code, den man sich anschauen kann, wie Profis das machen.

Warum meint jeder, dass er es besser kann, als die Generationen vor ihm?

von Peter D. (peda)


Lesenswert?

Erwin U. schrieb:
> Heisst das, dass nach einem sei() immer ein anderer Befehl gemacht
> werden muss, damit die Interrupts verlässlich durchkommen?

Ja, so steht es im Datenblatt.
Diese Arbeitsweise ist äußerst nützlich. Z.B. kommt dann immer noch das 
Main zum Zuge, wenn ein Interrupt nicht gelöscht wird. Oder es wird 
garantiert, daß ein Sleep vor dem Aufwachinterrupt ausgeführt wird.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Erwin U. schrieb:
> Heisst das, dass nach einem sei() immer ein anderer Befehl gemacht
> werden muss, damit die Interrupts verlässlich durchkommen?

Ein Sleep-Befehl passt da perfekt hin!

von Klaus S. (kseege)


Lesenswert?

Erwin U. schrieb:
> Das ist alles klar. Beim Warten muss ich natürlich immer wieder den
> Buffer abfragen, ob schon Platz ist, daher muss ich hier kurzfristig die
> Interrupts sperren, schauen, ob Platz ist, und natürlich die Interrupts
> wieder freigeben, damit der Interrupt passieren kann.

Seit mindestens 50 Jahren sind Ringpuffer erfunden worden, bei denen man 
auch ohne  Interruptsperre für die Abfragen auskommt. Stichwort 
Producer/Consumer-Schema.

Des Weiteren sind Warteschleifen, die einfach nur Zeit verballern, die 
Babysprache der Programmierung. Damit sollte man aber  keine Programme 
mit Interrupts selbst erstellen. Wenn man keine bessere Anleitung dafür 
hat, bietet selbst die vielgeschmähte Arduino-Umgebung zwei 
Blinkprogramme zur Anschauung, einmal in Babysprache und einmal in 
Erwachsenensprache, allerdings nur für den Umgang mit Timerinterrupts.

Gruß Klaus (der soundsovielte)

von Sebastian W. (wangnick)


Lesenswert?

Erwin U. schrieb:
> Ich programmiere gerade serielle Routinen, die interruptgesteuert Daten
> vom Arduino zum PC schicken sollen

Erwin U. schrieb:
> der Buffer ist voll. In dem Fall muss ich wohl warten

Es wäre äußerst ungewöhnlich wenn eine serielle Routine, die asynchron 
Daten verschicken soll, dann doch in Sonderfällen wartet.

Erwin U. schrieb:
> ATMEGA8

Welcher Arduino ist das denn?

Erwin U. schrieb:
> daher muss ich hier kurzfristig die Interrupts sperren, schauen, ob
> Platz ist

Michael H. schrieb:
> einem Lese- und einem Schreib-Zeiger (beide volatile)

Das ist beste Lösung, weil dadurch buffer_voll atomar wird.

LG, Sebastian

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Michael H. schrieb:
> Ich benutze für sowas normalerweise einen Puffer mit z.B. 16 Zeichen und
> einem Lese- und einem Schreib-Zeiger (beide volatile). Damit sind beide
> Seiten voneinander unabhängig.
So mache ich das auch:
- http://www.lothar-miller.de/s9y/categories/51-Fifo

von Erwin U. (erwin_u)


Lesenswert?

Zur Klarstellung:
1. die seriellen Routinen sind ein Beispiel für den Unterricht und 
sollen verdeutlichen, wie man mit einem Ringbuffer umgeht. Sie enthalten 
einen Schreib- und einen Lesezeiger, beide volatile.
2. Alle, die meinen, man müsste nicht warten, stelle ich mal die 
Gegenfrage: was machst du, wenn dein Buffer bereits voll ist und du im 
Programm weitere Daten an die Schnittstelle loswerden willst? Ich habe 
diesen Fall natürlich durch geringe Baudrate etc. provoziert, aber ich 
denke, jeder gute Programmierer sollte seinen Code ausprobieren.
3. Der Zugriff auf volatile Variable sollte immer nur mit gesperrtem 
Interrupt erfolgen, damit keine Laufzeiteffekte auftauchen. Wenn ich 
jetzt, nur in dem seltenen und unerwünschten Fall, dass mein Buffer voll 
ist, warten muss, dann ist eine Schleife, die einfach Interrupts 
freigibt und danach wieder sperrt, sinnvoll.

- Ich sehe den Kommentar mit dem sleep() als sehr sinnvoll, ich wollte 
aber im Unterricht keine zusätzliche Komplexität einführen.
- Ein Kommentar sagt, wenn ich sofort wieder sperre, dann hat die CPU 
quasi keine Zeit, einen Interrupt auszulösen. Ist das jetzt die Antwort 
auf meine Frage? Immerhin müsste der Interrupt ja gequeued sein, und 
sofort ausgelöst werden, oder macht Atmel dazu irgendeine Aussage?
- Auch vielen Dank für den Hinweis auf die Blinkprogramme, ich werde sie 
mir mal anschauen, ob sie mir bei der Beantwortung meines Problems 
helfen können ;-)

von Peter D. (peda)


Lesenswert?

Sebastian W. schrieb:
> Es wäre äußerst ungewöhnlich wenn eine serielle Routine, die asynchron
> Daten verschicken soll, dann doch in Sonderfällen wartet.

Es bleibt ihr schlichtweg nichts anderes übrig, will sie die Daten nicht 
verstümmeln. Daher machen es die Standardroutinen so.
Solange der FIFO ausreicht, ist alles fein. Will das Main aber längere 
Daten versenden, als man Puffer reserviert hat, muß es eben warten.

Man könnte höchstens mit malloc eine weiteres Häppchen dem FIFO 
hinzufügen, muß dann aber aufpassen, daß irgendwann der RAM auch wieder 
freigegeben wird und nicht überläuft.
Das Main zu verlangsamen ist daher allgemein die beste Lösung.

von Erwin U. (erwin_u)


Lesenswert?

Hallo!
Ich habe gerade gesehen, dass ich die Antwort bereits erhalten habe. 
Vielen Dank an Peter D. ich habe das Handbuch durchsucht, bin aber nicht 
fündig geworden. Ich nehme an, ich sollte alle 600 Seiten mal lesen und 
nicht nur suchen ;-)

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Ist etwas versteckt im Datenblatt des ATmega8.
Abschnitt "Reset and Interrupt Handling":

"When the AVR exits from an interrupt, it will always return to the main 
program and execute one more instruction before any pending interrupt is 
served."

"When using the SEI instruction to enable interrupts, the instruction 
following SEI will be executed before any pending interrupts, as shown 
in the following example."

von Εrnst B. (ernst)


Lesenswert?

Erwin U. schrieb:
> die seriellen Routinen sind ein Beispiel für den Unterricht

Gerade bei einer Lösung für den Unterricht (Ich nehme an als 
Lehrer/Dozent, nicht als Hausaufgabe) sollte man eine saubere, 
durchdachte, schöne Lösung präsentieren, und nicht irgendwas hinfrickeln 
und anschließend volatile und cli/sei drübersprinkeln bis es irgendwie 
funktioniert.

So als Denkanstoß:

Fang mit deinen Lese- und Schreib-Pointern oder Indices an. Sind die 8 
oder 16 Bit?
Wieviele Bit kann der µC atomar lesen, ohne dass eine IRQ-Sperre nötig 
ist?

Welcher Prozess schreibt welchen Pointer? Kann es vorkommen, dass sowohl 
main als auch die ISR denselben Pointer beschreiben wollen?

Warum meinst du "volatile" zu brauchen? Weil das irgendein Tutorial 
irgendwann mal behauptet hat? Wenn deine Schüler nach dem "volatile" 
fragen, antwortest du dann:
"Das macht man halt so, weil der 13-Jährige Timmy das in seinem 
TikTok-Video so gesagt hat"?

Ist's um Compiler-Optimierungen zu verhindern?

Braucht es das an allen Stellen, oder ist es z.B. in der ISR sinnfrei, 
weil während dem ISR-Lauf sowieso niemand anderes an deinen Pointern 
dreht?

Brauchst du es in main()? Brauchst du es in main an allen Stellen, oder 
nur an ausgewählten? Brauchst du es in main immer noch, wenn du weißt 
dass cli()/sei() eine memory barrier beinhalten, die das Neu-Lesen der 
Variable auch ohne "volatile" erzwingen?

Die "ATOMIC"-Helper aus der avr-libc sind bekannt?
https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html

von Sebastian W. (wangnick)


Lesenswert?

Peter D. schrieb:
> Es bleibt ihr schlichtweg nichts anderes übrig, will sie die Daten nicht
> verstümmeln.

Ich wollte darauf hinaus, dass es schlechtes Vertragsdesign ist, wenn in 
einer Routine, deren Zweck es ist, nicht zu warten, dann doch manchmal 
und unerwartet gewartet wird.

LG, Sebastian

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Erwin U. schrieb:
> Der Zugriff auf volatile Variable sollte immer nur mit gesperrtem
> Interrupt erfolgen, damit keine Laufzeiteffekte auftauchen.
Das ist nur nötig, wenn der Prozessor den Lese- und Schreibzugriff nicht 
auf die komplette Wortbreite atomar hinbekommt. Seit 1980 übliche 
Architekturen schaffen einen 8-Bit Schreib- und Lesezugriff aber 
garantiert atomar. Damit kann ich einen Buffer bis 256 Datensätze ohne 
jegliche Interruptsperre sicher und kollisionsfrei hinbekommen.

von Flunder (flunder)


Lesenswert?

Mal abgesehen von der nicht so ganz durchdachten Nutzung von sei/cli 
geht es hier also um die Frage, was tun, wenn Murks passiert. In diesem 
Fall, wenn mehr Daten anfallen als man weiter geben kann.

Auch wenn man sich immer sehr ungern von etwas trennt, wird man wohl 
früher oder später Daten wegwerfen müssen, weil
- kein Puffer unendlich groß ist
- während dem Warten auf Platz im Puffer die Auswertung stockt, und ihr 
dadurch Daten entgehen

Also mehr als zu erkennen, dass Mist passiert, und die Stelle für den 
Empfänger zu markieren ist vermutlich nicht drin.

Oder bist Du in der glücklichen Lage, dass keine Daten anfallen, wenn 
niemand fragt ? Dann musst Du nur die Rückmeldung "ich habe Platz im 
Puffer (gemacht)" vom Interrupt zum Hauptprogramm atomar machen. D.h. 
der µC kann es auf einmal erfassen, ohne dass ihn jemand dazwischen 
stören kann. Es reicht dabei ja schon ein Flag "habe gerade ein Datum 
aus dem Puffer genommen". Wenn Du dagegen mit einem 8-Biter zwei Zeiger 
zu je 32 Bit auswerten musst, um zu wissen, ob Platz im Puffer ist, wird 
es schwer mit atomar.

von Falk B. (falk)


Lesenswert?

Klaus S. schrieb:
> Seit mindestens 50 Jahren sind Ringpuffer erfunden worden, bei denen man
> auch ohne  Interruptsperre für die Abfragen auskommt. Stichwort
> Producer/Consumer-Schema.

Ganz sicher nicht. Wenn ein FIFO mit einem Interrupt arbeitet, 
müssen zumindest für die sehr kurzen Abfragen des Füllstandes die 
Interupts gesperrt werden.

von Falk B. (falk)


Lesenswert?

Erwin U. schrieb:
> Ich programmiere gerade serielle Routinen, die interruptgesteuert Daten
> vom Arduino zum PC schicken sollen, d.h. im ATMEGA8 gibt es einen
> Buffer, der mittels Interrupt USART_UDRE_vect "abgearbeitet" wird.

Nichts besonderes.

> Wenn
> der Transmitter meldet, dass das Zeichen gesendet worden ist, wird
> mittels Interrupt das nächste Zeichen zum Versand gegeben.

Nein. Wenn der Hardware Sendepuffer leer ist (UDRE). Das ist was 
anderes.

> Das funktioniert auch schon ganz gut. Ich habe jetzt im Hauptprogramm
> (also nicht in der Interruptroutine) 2 Fälle:
> 1. genug Platz noch im Buffer, kein Problem, ich kann die Daten in den
> Buffer geben zum Versand

Welcher Buffer? Der Hardwarebuffer des UARTs oder dein Software-Buffer?

> 2. der Buffer ist voll. In dem Fall muss ich wohl warten, bis die
> Interruptroutine meldet, dass sie fertig ist, damit ich das nächste
> Zeichen senden kann.

Nö, du musst warten, bis Platz im Buffer ist. Das passiert im 
Huntergrund durch den UDRE-Interrupt, welcher den Inhalt deines 
Software-FIFOs in den Hardware-UART schaufelt.

> Das ist alles klar. Beim Warten muss ich natürlich immer wieder den
> Buffer abfragen, ob schon Platz ist, daher muss ich hier kurzfristig die
> Interrupts sperren, schauen, ob Platz ist, und natürlich die Interrupts
> wieder freigeben, damit der Interrupt passieren kann.

Stimmt, dafür gibt es im avr gcc die Funktion ATOMIC_BLOCK(){}

> Und jetzt endlich die Frage: in dieser Schleife hatte ich den Code in
> etwawhile (buffer_voll)
> {
>   sei();
>   cli();
> }

Solche Fragmente sich Schrott. Zeig deinen REALEN Quelltext, siehe 
Netiquette.

> Und jetzt das Problem: Hier kommt offensichtlich der Interrupt nicht
> immer durch. Es funktioniert, wenn ich schreibewhile (buffer_voll)
> {
>   sei();
>   _delay_us(1);
>   cli();
> }

Ist Unsinn und ein Indiz für einen Programmier- oder Konzeptfehler.

> Heisst das, dass nach einem sei() immer ein anderer Befehl gemacht
> werden muss, damit die Interrupts verlässlich durchkommen?

Nein.

> Oder gibt es
> hier einen anderen Effekt?

Ja, siehe oben.

> Wie gesagt, es funktioniert manchmal, und
> manchmal bleibt er hängen, ich vermute, er läuft in dem Fall endlos in
> der Schleife.

Nicht "vermuten", TESTEN!

> ps.: volatile ist in Verwendung, das ist es nicht.

Auch immer mit atomarem Zugriff?

So geht's.

https://www.mikrocontroller.net/articles/FIFO#FIFO_als_Bibliothek

von Falk B. (falk)


Lesenswert?

Lothar M. schrieb:
> Damit kann ich einen Buffer bis 256 Datensätze ohne
> jegliche Interruptsperre sicher und kollisionsfrei hinbekommen.

Ja und? Was hast du dann gewonnen? Die handvoll Takte für die 
Interruptsperre für Daten >8 Bit sind in den allermeisten Fällen 
vollkommen unkritisch. Das Problem des OPs liegt sowieso woanders.

von Falk B. (falk)


Lesenswert?

Sebastian W. schrieb:
>> Es bleibt ihr schlichtweg nichts anderes übrig, will sie die Daten nicht
>> verstümmeln.
>
> Ich wollte darauf hinaus, dass es schlechtes Vertragsdesign ist, wenn in
> einer Routine, deren Zweck es ist, nicht zu warten, dann doch manchmal
> und unerwartet gewartet wird.

Stimmt, aber das ist bei einem FIFO eben manchmal der Fall. Und das muss 
auch klar definiert und kommuniziert werden.

. . . die Funktion wartet auf freien Speicher, wenn dieser nicht 
vorhanden ist  . . .

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Falk B. schrieb:
> Wenn ein FIFO mit einem Interrupt arbeitet, müssen zumindest für die sehr
> kurzen Abfragen des Füllstandes die Interupts gesperrt werden.
Für Fifos habe ich seit Jahrzehnten keinen Interrupt mehr gesperrt. Wenn 
wie gesagt die Pointer jeweils nur an 1 Stelle geschrieben und sonst nur 
gelesen werden und zudem Zugriffe auf die Pointer atomar sind, dann muss 
kein Interrupt gesperrt werden.

Und der Füllstand ist im schlimmsten Fall dann eben 1 Datensatz in die 
unkritische Richtung "falsch": der Sendefifo meldet "voll" obwohl 1 
Datensatz Platz hätte, oder der Empfangsfifo meldet "leer" obwohl grade 
1 Datensatz hereingekommen ist. Aber beim nächsten Durchlauf der 
"Hauptschleife" passt dann wieder alles, es gehen sicher keine Daten 
verloren.

Falk B. schrieb:
> Was hast du dann gewonnen? Die handvoll Takte für die Interruptsperre
> für Daten >8 Bit sind in den allermeisten Fällen vollkommen unkritisch.
Ffifos haben prinzipiell nichts mit Interrupts zu tun. Ich muss nicht 
mit Interrupts herumhampeln, wenn der Code so ist, dass es per 
Definition kein Problem mit Interrupts geben kann.

> in den allermeisten Fällen vollkommen unkritisch.
Genaus das: in ganz seltenen Fällen eben doch. Und dann kann man erst 
mal ein paar Tage herumsuchen. Denn je seltener ein Fehler auftritt, 
umso länger dauert die Suche.

: Bearbeitet durch Moderator
von Falk B. (falk)


Lesenswert?

Lothar M. schrieb:
> Falk B. schrieb:
>> Wenn ein FIFO mit einem Interrupt arbeitet, müssen zumindest für die sehr
>> kurzen Abfragen des Füllstandes die Interupts gesperrt werden.
> Für Fifos habe ich seit Jahrzehnten keinen Interrupt mehr gesperrt. Wenn
> wie gesagt die Pointer jeweils nur an 1 Stelle geschrieben und sonst nur
> gelesen werden und zudem Zugriffe auf die Pointer atomar sind, dann muss
> kein Interrupt gesperrt werden.

Das sind ne Menge Nebenbedingungen! Die Interruptsperre hat ja eben das 
Ziel, diese sicherzustellen! Wenn das dein (32)Bit Controller auch ohne 
Sperre kann, umso besser.

> Falk B. schrieb:
>> Was hast du dann gewonnen? Die handvoll Takte für die Interruptsperre
>> für Daten >8 Bit sind in den allermeisten Fällen vollkommen unkritisch.
> Ffifos haben prinzipiell nichts mit Interrupts zu tun.

Doch! Wenn eine Seite vom Hauptprogramm und die andere von einem 
Interrupt zugreift, was oft der Fall ist!

> Ich muss nicht
> mit Interrupts herumhampeln, wenn der Code so ist, dass ich per
> Definition kein Problem mit Interrupts habe.

Jaja, wir definieren Probleme weg.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Lothar M. schrieb:
> Aber beim nächsten Durchlauf der
> "Hauptschleife" passt dann wieder alles, es gehen sicher keine Daten
> verloren.

Was machst du in so einem Fall:
1
int main () {
2
  ...
3
  while (1) {
4
    ...
5
    if (fifoFill < max) {
6
      writeToFifo ();
7
    } else {
8
      sleep_enable ();
9
    }
10
  }
11
}

Was wenn der Fifo voll ist, dementsprechend der else-Fall betreten wird, 
aber direkt vor dem Betreten des Sleep-Mode der Interrupt kommt und den 
Fifo leert? Dann wird der Sleep-Mode trotzdem betreten aber ggf. nie 
wieder verlassen, weil der Interrupt nicht nochmal kommt.

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Niklas G. schrieb:
> Was machst du in so einem Fall:
Zeig mehr vom Code. Wo wird der ominöse "fifoFill" berechnet?

Falk B. schrieb:
> Jaja, wir definieren Probleme weg.
Wo kann da ein Problem auftreten, wenn ein Bytezugriff atomar ist:
1
#define BLEN (16)          // z.B. 16
2
#define BMSK (BLEN-1)      // z.B. 16-1 = 15 = binär 1111
3
typedef struct {
4
   volatile unsigned char w;
5
   volatile unsigned char r;
6
   unsigned char d[BLEN];   
7
} fifotyp;                 
8
fifotyp fifo;
9
10
   
11
   // Fifo schreiben
12
   if ((fifo.w - fifo.r)&BMSK==BMSK) {
13
       // Fehlerbehandlung: Fifo ist voll
14
   }
15
   else {
16
      // Zeichen eintragen
17
      fifo.d[fifo.w&BMSK] = din;    
18
      fifo.w++;
19
   }
20
21
22
   // Fifo lesen
23
   if (fifo.w != fifo.r) { // Fifo belegt?
24
      // Zeichen auslesen
25
      dout = fifo.d[fifo.r&BMSK];
26
      fifo.r++;
27
   }
Egal, welche der beiden Programmteile (lesen/schreiben) ich in eine ISR 
packe, die den anderen Programmteil unterbrechen kann: es geht nicht 
schief.

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


Lesenswert?

Lothar M. schrieb:
> Zeig mehr vom Code. Wo wird der ominöse "fofiFill" berechnet?

Das ist eine Möglichkeit einen Ringpuffer zu implementieren, um im Fall 
readPtr==writePtr zu unterscheiden ob der Puffer voll oder leer ist.

Aber man kann es auch in deinem Code demonstrieren:
1
int main () {
2
  ...
3
4
  while (1) {
5
    ...
6
    // Fifo schreiben
7
    if ((fifo.w - fifo.r)&BMSK==BMSK) {
8
      // -- Wenn genau jetzt ein Interrupt den Ringpuffer komplett leert, bleiben wir ggf. ewig im Sleep-Modus --
9
      sleep_enable ();
10
    }
11
    else {
12
      // Zeichen eintragen
13
      fifo.d[fifo.w&BMSK] = din;    
14
      fifo.w++;
15
    }
16
  }
17
}

von Steve van de Grens (roehrmond)


Lesenswert?

Erwin U. schrieb:
> Der Zugriff auf volatile Variable sollte immer nur mit gesperrtem
> Interrupt erfolgen

Das ist nur bei Variablen hilfreich, die so groß sind, dass sie nicht 
atomar gelesen/geschrieben werden können. Also bei AVR alle Variablen, 
die größer als 8 Bit sind.

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


Lesenswert?

Steve van de Grens schrieb:
> Das ist nur bei Variablen hilfreich, die aus mehr als einem Byte
> bestehen.

Oder wenn sowohl main-Schleife als auch Interrupt auf der Variablen ein 
Read-Modify-Write machen.

von Steve van de Grens (roehrmond)


Lesenswert?

Falk B. schrieb:
> Ja und? Was hast du dann gewonnen? Die handvoll Takte für die
> Interruptsperre für Daten >8 Bit sind in den allermeisten Fällen
> vollkommen unkritisch.

Ja schon. Aber solange man sei(), cli(), volatile, und ATOMIC_BLOCK noch 
nicht durchschaut, ist dieses Wissen hilfreich, denn derart kleine 
Puffer kann man (einfach) ganz ohne diese Maßnahmen implementieren.

: Bearbeitet durch User
von Foobar (asdfasd)


Lesenswert?

Niklas schrieb:
> [Beispiel]
> Was wenn der Fifo voll ist, dementsprechend der else-Fall betreten wird,

Ganz einfach: der Code ist broken ;-)  Dieses Test-und-Sleep muss 
sorgfältig programmiert werden, damit keine Race-Conditions entstehen.

Bei mir steckt das in den Macros sleep_unitl/sleep_while und sähe so 
aus:
1
volatile uint8_t fifoFill;
2
int main () {
3
    ...
4
    while (1) {
5
      ...
6
      sleep_until(fifoFill < max);
7
      writeToFifo ();
8
  }
9
}

Die passenden (low-level) Makros:
1
// sleeping
2
//   tries /exp/ first with ints enabled,
3
//   but then with ints disabled before actually sleeping.
4
//   Note: you got something wrong if /exp/ does not evaluate a volatile!
5
#define _sei()          asm volatile("sei")
6
#define _cli()          asm volatile("cli")
7
#define cli_or_sleep()  asm volatile("brie 1f\n\tsei\n\tsleep\n1:\tcli")
8
#define sleep_until(exp) \
9
    do { \
10
        if (exp) { _sei(); break; } \
11
        cli_or_sleep(); \
12
    } while (1)
13
#define sleep_while(exp) sleep_until(!(exp))

Dazu kommt noch die globale Entscheidung, ob überhaupt geschlafen werden 
darf.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Foobar schrieb:
> Bei mir steckt das in den Macros sleep_unitl/sleep_while und sähe so
> aus:

Hätte ich auch so gemacht, aber es wurde behauptet:

Lothar M. schrieb:
> Für Fifos habe ich seit Jahrzehnten keinen Interrupt mehr gesperrt

Über genau das Problem bin ich letztens beim ARM gestolpert:

Beitrag "Atomisches "__enable_irq () + WFI" / condition variablen für ARM"

von Foobar (asdfasd)


Lesenswert?

Niklas schrieb:
> Hätte ich auch so gemacht, aber es wurde behauptet:
>
> Lothar M. schrieb:
>> Für Fifos habe ich seit Jahrzehnten keinen Interrupt mehr gesperrt

Die Fifo-Routinen selbst brauchen ja auch keine Interruptsperren.  Es 
ist das Schlafen, was sie benötigt (atomic test-and-sleep).

Solange man in dem Beispiel nicht zu schnell sendet, werden auch hier 
nie Interrupts gesperrt - das passiert nur, wenn feststellt wird, dass 
der Buffer voll ist (irqs sperren, nochmal testen, wenn immer noch voll 
dann sleep, irqs freigeben).

> Über genau das Problem bin ich letztens beim ARM gestolpert:

Jo, das WFI scheint etwas gutmütiger zu sein (schläft nur, wenn kein irq 
pending ist).

: Bearbeitet durch User
von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Foobar schrieb:
> Die Fifo-Routinen selbst brauchen ja auch keine Interruptsperren.  Es
> ist das Schlafen, was sie benötigt (atomic test-and-sleep).
Genau das ist es. Wenn ich einen sleep() verwenden will, muss ich mir 
anschauen, welche Wechselwirkungen der hat.

Aber das mit dem Schlafen kam erst im zweiten Post dazu. Der TO will 
nichts in den Schlaf versetzen, sondern das Prinzip eines Fifos mit 
einem Ringpuffer vermitteln. Und dazu braucht er keinen Interrupt 
sperren.

von 900ss (900ss)


Lesenswert?

Falk B. schrieb:
> Wenn das dein (32)Bit Controller auch ohne Sperre kann, umso besser.

Also mein SPARC V8 kann das :)

von Falk B. (falk)


Lesenswert?

Steve van de Grens schrieb:
>> Ja und? Was hast du dann gewonnen? Die handvoll Takte für die
>> Interruptsperre für Daten >8 Bit sind in den allermeisten Fällen
>> vollkommen unkritisch.
>
> Ja schon. Aber solange man sei(), cli(), volatile, und ATOMIC_BLOCK noch
> nicht durchschaut, ist dieses Wissen hilfreich, denn derart kleine
> Puffer kann man (einfach) ganz ohne diese Maßnahmen implementieren.

Das würde ich genau anders herum sehen. Man sollte die Grundlagen von 
volatile, atomarem Zugriff und Interrupts erstmal verstehen und dann den 
FIFO bauen. Denn sonst könnte der irrtümliche Eindruck entstehen, daß 
man da nichts beachten muss. Das funktioniert in einigen Fällen 
vielleicht, dann aber oft durch Glück und Zufall. Eben so wie der Lothar 
das alles als easy und null Problemo darstellt. Das ist FALSCH! Das ist 
es NUR, wenn man GENAU weiß was man tut und wo man bestimmte Dinge wie 
cli()/sei() weglassen kann. Allgemeingültig ist das keine Sekunde!

von Falk B. (falk)


Lesenswert?

Lothar M. schrieb:
> Und dazu braucht er keinen Interrupt
> sperren.

Schon wieder so eine vollkommen falsche und gefährliche 
Verallgemeinerung! Das gilt nur unter bestimmten Bedingungen!
Und was ist plötzlich an einer sehr kurzen Interruptsperre so kritisch? 
Was soll die Hysterie? Eine handvoll sehr kurze ATOMIC_BLOCK() tut 
keinem weh, macht die Sache im Zweifel aber sicher.

von Rainer W. (rawi)


Lesenswert?

Falk B. schrieb:
> Eine handvoll sehr kurze ATOMIC_BLOCK() tut keinem weh, macht die Sache
> im Zweifel aber sicher.

Solche, mehr oder weniger blind in den Code gestreute 
Sicherungsmaßnahmen zeugen nur von mangelndem Überblick über die 
ablaufenden Vorgänge.

von Falk B. (falk)


Lesenswert?

Rainer W. schrieb:
>> Eine handvoll sehr kurze ATOMIC_BLOCK() tut keinem weh, macht die Sache
>> im Zweifel aber sicher.
>
> Solche, mehr oder weniger blind in den Code gestreute
> Sicherungsmaßnahmen zeugen nur von mangelndem Überblick über die
> ablaufenden Vorgänge.

Jaja, noch so ein Schlaumeier. Von blind hingestreut war nie die Rede! 
Thema verfehlt, 6!

von Rolf (rolf22)


Lesenswert?

Ich erzähl euch mal, wie wir früher™ serielle Ausgaben programmiert 
haben. Da hatten wir manchmal Interfaces, die gar keinen Interrupt 
erzeugen konnten. Wir mussten aber puffern, damit die Ausgabe nicht 
stotterte und das Hauptprogramm nicht durch unnötiges Warten gebremst 
wurde. Da haben wir einen periodischen Timer-Interrupt hergenommen und 
in der ISR den Pufferfüllstand UND den Interface-Status "Frei zur 
Ausgabe" abgefragt und dann ggf. ein Zeichen ausgegeben. Natürlich 
entstand da eine kleine Latenz, aber dafür war das Ganze interrupt-mäßig 
absolut zuverlässig. Wenn der UART einen kleinen Puffer hatte, konnte 
man in der ISR auch gleich mehrere Zeichen in einem Rutsch ausgeben und 
so die Latenz mildern.

Statt eines Timers konnte man ggf. auch einen anderen periodischen 
Interrupt nehmen, der von irgendeinem Signal kam (z. B. Umdrehungszähler 
eines Motors).

von Lothar M. (Firma: Titel) (lkmiller) (Moderator) Benutzerseite


Lesenswert?

Falk B. schrieb:
> Das ist es NUR, wenn man GENAU weiß was man tut und wo man bestimmte
> Dinge wie cli()/sei() weglassen kann.
Ich sehe es genau andersrum: cli() und sei() gehören nur dorthin, wo sie 
tatsächlich nötig sind.

Aber seins durm: es darf jeder gerne die Interrupts seines Controllers 
sperren und freigeben, wie er will. Er hat ihn bezahlt und kann ihn auf 
jede ihm beliebte Art und Weise langsam machen. Aber bitte verschont 
mich vor solchen Geräten, wo das dann "einfach so zur Sicherheit" 
gemacht wird. Denn wer da keinen Plan hat, ob es nötig ist oder nicht, 
der hat auch sonst noch irgendwo Macken im Design.


Erwin U. schrieb:
> Ich habe gerade gesehen, dass ich die Antwort bereits erhalten habe.
Das ist zwar die Antwort auf deine Frage, aber eben nicht die Lösung des 
ursächlichen Problems. Und gerade, weil du diesen Fifo im Unterricht 
zeigen willst, sollte der sauber implementiert sein, sonst lernen die 
Schüler noch irgendeine umständliche Implementierung ganz im Sinne von 
"Überall, wo du nicht ganz sicher bist, was passiert, sperrst du im 
Zweifelsfall besser mal die Interrupts!"

Und wie gesagt: bei geeigneter Implementioerung besteht absolut keine 
Notwendigkeit da irgendwo die Interrupts zu sperren.

Erwin U. schrieb:
> Wie gesagt, es funktioniert manchmal, und manchmal bleibt er hängen
Genau diese Race-Conditions kommen gerne vom Nicht-Verwenden von 
volatile, wo es hingehört. Das Problem liegt hier nicht im geposteten 
Code. Sondern das, was du gepostet hast, ist nur ein Work-Around um das 
eigentlich Problem.

Zeig mal den gesamten Code dieses Fifos. Sooo arg viele Zeilen sollte 
der ja nicht haben...

von Frank O. (fop)


Lesenswert?

Statt mit der Keule alle Interrupts zu sperren, könnte man ja auch nur 
die Freigabe des problamtischen Interrupts wegnehmen. Erspart einem 
Ärger mit dem Kollegen, der einen Regler mit kritischem Timing in einem 
anderen Teil der Software realisiert hat.

Die Verallgemeinerung, bei volatile musst Du immer Interrupts sperren 
ist auch Humbug. SFRs sind auch als volatile deklariert und da bringt es 
mir null zu glauben, dass sich der Wert der Digitaleingänge oder eines 
Timers bei gesperrten Interrupts nicht ändert.

Dann hätten wir da auch noch Multicore-Architekturen, wo die Software 
eines anderen Kerns die Daten trotz gesperrtem Interrupt aufmischen 
kann.

Es hilft wohl echt nur ein paar mal auf die Schnauze gefallen zu sein, 
um ein Augenmerk dafür zu entwickeln, wo die Party abgeht, wenn dies mal 
genau zwischen dem da und jenem statt findet.

von Erwin U. (erwin_u)


Lesenswert?

Hallo liebe Leute!

Ich bin überwältigt von der Fülle an Informationen, die ich da erhalten 
habe. Daher nur soviel:
1. ich werde nicht mehr reagieren, weil ich die von mir gewünschten 
Informationen bereits erhalten habe.
2. Ich bedanke mich für die Hinweise auf die vom Compiler gebotenen 
Möglichkeiten, atomare Operationen zu machen.
3. Ebenso dafür, dass man FIFO´s programmieren kann, ohne Sperren.

Mein Lehrziel ist eigentlich, dass meine Schüler verstehen, dass 
Interruptroutinen zwar äusserst nützlich, aber auch gefährlich sind, 
wenn man nicht aufpasst. Und da werde ich sicher nicht als erstes auf 
die Probleme hinweisen, und dann im 2. Schritt alles zu erklären, warum 
sie diese Dinge erst jetzt nicht brauchen. Allerdings werde ich auf 
Punkt 2. und 3. hinweisen, ebenso, dass sie meine Implementierung ja nie 
als Referenz verwenden sollen. Vielleicht sollte auch vom Level der 
Kenntnisse her klar sein, dass die Schüler einige Wochenstunden 
Programmierung haben, seit ca. 1 Jahr sich mit C befassen, und seit 
einigen Monaten mit Mikrocontrollern. Ich nehme an, viele der Leute im 
Forum haben 30 Jahre Programmiererfahrung und mehr. Diejenigen bitte 
ich, mal zurückzudenken an die Zeit, als sie begonnen haben, mit 
Mikrocontrollern zu arbeiten ...

Allen Leuten mit "verschont mich mit solchem Code" oder ähnlichen 
Aussagen:
Wie kauft ihr eigentlich ein neues Auto oder andere technische Dinge? In 
jedem steckt in der Zwischenzeit ein Mikrocontroller, beim Auto 
wahrscheinlich hundert. Macht ihr da Codereview von allen Programmen, 
bevor ihr "zuschlagt"? Oder habt ihr Foren, wo darüber diskutiert wird, 
wie Scheisse die Automarke xy ist, weil sie immer wieder bei 130 km/h 
kleinere Probleme hat, weil einer der Controller da schlecht 
programmiert ist? Oder kauft ihr einfach kein Auto? Wobei 
Mikrocontroller heute überall drinnen sind. Denkt daran, wenn ihr zur 
Maus greift, um mir böse zu antworten, da drinnen steckt auch ein 
Controller und ihr wisst nicht, wie der programmiert ist ;-)

Ich habe auch schon Programme gefunden (public domain, 
Amateurfunkbereich), da hatten die Programmierer keine Ahnung von 
volatile und "Aufpassen bei solchen Zugriffen", da wäre es mir lieber 
gewesen, die hätten mal meine Warnungen gehört.

Ich habe auch versucht, den 13-jährigen Timmy zu finden, der blöde 
Anweisungen gibt, um meine Schüler vor ihm zu warnen. Leider habe ich 
kein TikTok, aber im Youtube habe ich keinen gefunden. Und last not 
least habe ich mir inzwischen die Blinkprogramme angeschaut, deren Code 
hat mir aber leider überhaupt nicht weitergeholfen ... ;-)

In dem Sinne: danke für eure Hilfe!

von Gerhard H. (hauptmann)


Lesenswert?

Falk B. schrieb:
> Und was ist plötzlich an einer sehr kurzen Interruptsperre so kritisch?

Sie ist kein Zeugnis optimaler Code-Durchdachtheit sondern immer nur 
gedankliche WildWest Notlösung wenn man sonst nicht mehr weiter weiß. 
Und wenn man nicht aufpasst verliert man damit noch mehr Durchblick, 
weil es die Abläufe schlicht verkompliziert. Meine Erfahrungen früherer 
Jahre- später war da nämlich kein cli mehr nötig ...

Rolf schrieb:
> Da haben wir einen periodischen Timer-Interrupt hergenommen

Ein ganz wichtiges Mittel um Aufgaben systematisch voneinander zu 
trennen und eine Art zuverlässiger Service-Struktur zu entwickeln wo 
eines konfliktfrei aufs andere aufbaut.

Erwin U. schrieb:
> dass
> Interruptroutinen zwar äusserst nützlich, aber auch gefährlich sind,
> wenn man nicht aufpasst.

Es gibt gewisse Regeln zu beachten.
So wie überall.

: Bearbeitet durch User
von Rainer W. (rawi)


Lesenswert?

Falk B. schrieb:
> Von blind hingestreut war nie die Rede!

"tut keinem weh" als Argument kannst du getrost mit "da ich nicht genau 
drüber nachdenken will, was da passiert, baue ich es zur Sicherheit ein" 
übersetzen.
Wenn nicht Blindheit, dann ist es zumindest Faulheit, die den Code 
aufbläht.
Ein "es ist nötig, weil ... " wäre IHMO das einzig ernstzunehmende 
Argument.
Ein Zusatz wie "... und ich keine andere Lösung gefunden habe" würde 
eventuell noch Lernwilligkeit und Ansatzpunkte für weitere Verfeinerung 
offenbaren ;-)

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Frank O. schrieb:
> Statt mit der Keule alle Interrupts zu sperren, könnte man ja auch nur
> die Freigabe des problamtischen Interrupts wegnehmen. Erspart einem
> Ärger mit dem Kollegen, der einen Regler mit kritischem Timing in einem
> anderen Teil der Software realisiert hat.

Nö, damit wird man erst recht auf die Schnauze fallen.
Ein atomarer Zugriff kostet vielleicht 10 CPU Zyklen und tut niemandem 
weh.
Nur den betreffenden Interrupt zu sperren, kann aber deutlich länger 
dauern, bis alle anderen abgearbeitet sind.
Es kann sogar zur Prioritätsinversion kommen, d.h. jeder noch so 
unwichtige Interrupt kriegt plötzlich Vorrang.

Z.B. ein schneller 8Bit-Timer muß innerhalb 128 Zyklen behandelt werden, 
um einen Übertrag eindeutig zu erkennen. Sperrt man nur diesen 
Interrupt, kann dann ein langsamer UART-Interrupt dazwischen grätschen 
und der Timer geht nach dem Mond. Da dieser Konflikt vielleicht nur 
selten auftritt, debugt man sich dann dumm und dämlich.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Peter D. schrieb:
> Sperrt man nur diesen
> Interrupt, kann dann ein langsamer UART-Interrupt dazwischen grätschen

Praktisch wenn man einen Prozessor mit Interrupt-Prioritäten hat, und 
man einfach Interrupts bis zu einer bestimmten Priorität sperren kann 
(z.B. Cortex-M).

von Gerhard H. (hauptmann)


Lesenswert?

Niklas G. schrieb:
> Praktisch wenn man einen Prozessor mit Interrupt-Prioritäten hat

Verschiedene Interrupt-Prioritätslevel sollte man vermeiden wo immer 
möglich.
Die reinste Nebel-Maschine gegen klaren Durchblick. Glücklicherweise 
haben AVRs nur maximal zwei...

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Niklas G. schrieb:
> Praktisch wenn man einen Prozessor mit Interrupt-Prioritäten hat

Erwin U. schrieb:
> Ich programmiere gerade serielle Routinen, die interruptgesteuert Daten
> vom Arduino zum PC schicken sollen, d.h. im ATMEGA8 ...

ATmega8 != Cortex-M

von Rolf (rolf22)


Lesenswert?

Rainer W. schrieb:
> "tut keinem weh" als Argument kannst du getrost mit "da ich nicht genau
> drüber nachdenken will, was da passiert, baue ich es zur Sicherheit ein"
> übersetzen.

Da fällt mir ein, was ich vor Jahrzehnten in einem Buch über 
Software-Projektleitung gelesen habe. Das ging dem Sinn nach so:
"Programmierer Kevin meint, dass der Befehl "Lade das Spezialregister X 
mit dem Wert Y" manchmal nicht funktioniert. Deshalb schreibt er den 
Befehl zur Sicherheit immer zweimal hintereinander. Die Frage ist nun: 
Wie erkennt man so einen Kevin im Team?"

> die den Code aufbläht

Dass es den Code aufbläht, ist ein schlechtes Argument, obwohl es 
stimmt. Ein paar Bytes kosten wenig und der Zeitverlust ist oft 
irrelevant. Wenn du eine richtige Sache mit schlechten Argumenten 
verteidigst, schadet das, weil das auf deine gesamte restliche 
Argumentation abfärbt. "Der erzählt nur Käse, z. B. will er unbedingt 3 
Byte sparen und so was." – so denken viele die anderen dann.

Nein, der wichtige Punkt ist, dass man un- oder halbverstandene Sachen 
selbst dann nicht einbauen soll, wenn sie anscheinend nach Tests 
funktionieren. Die stehen dann nämlich für ewig im Code drin und 
verursachen womöglich später nach Änderungen der Bedingungen oder des 
Codes oder nach Übernahme von Teilen in völlig andere Projekte 
"unerklärliche" Probleme. Es sind Zeitbomben.

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


Lesenswert?

Gerhard H. schrieb:

> Verschiedene Interrupt-Prioritätslevel sollte man vermeiden wo immer
> möglich.

Natürlich. Aber manchmal ist es halt doch hilfreich.

> Die reinste Nebel-Maschine gegen klaren Durchblick

Wenn man die Konzepte von "parallelem" Code wirklich verstanden hat, 
bringte einen auch ein Interruptsystem mit mehreren Prioritäten nicht 
in's Schleudern.

Das gab's übrigens auch bei den Classic-AVR schon, wenn man das wollte 
und brauchte. Musste halt mangels HW-Unterstützung in Software gelöst 
werden. Und ja: man muss schon wirklich verstehen, was da passiert, um 
es richtig umsetzen zu können...

von Oliver S. (oliverso)


Lesenswert?

Erwin U. schrieb:
> Mein Lehrziel ist eigentlich, dass meine Schüler verstehen, dass
> Interruptroutinen zwar äusserst nützlich, aber auch gefährlich sind,
> wenn man nicht aufpasst.

Gefährlich werden die erst, wenn jemand, der davon keine Ahnung hat, 
damit eine Passagierflugzeugsteuerung programmiert.

Ansonsten sind die weder gefährlich noch sonst irgend etwas. Die sind 
ein elementarer Bestandteil des Konzepts „Mikrocontroller“, und daher 
muß man sich damit befassen.

Oliver

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.