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:
genug Platz noch im Buffer, kein Problem, ich kann die Daten in den Buffer geben zum Versand
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.
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.
... 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?
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?
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.
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.
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.
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.
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.
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 ;-)
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.
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 ;-)
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."
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?
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.
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.
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.
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.
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:
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?
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.
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.
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 . . .
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.
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.
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.
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
intmain(){
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.
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.
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.
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).
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.
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!
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.
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).
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.
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.
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...
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.
Ich bin überwältigt von der Fülle an Informationen, die ich da erhalten habe. Daher nur soviel:
ich werde nicht mehr reagieren, weil ich die von mir gewünschten Informationen bereits erhalten habe.
Ich bedanke mich für die Hinweise auf die vom Compiler gebotenen Möglichkeiten, atomare Operationen zu machen.
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 ... ;-)
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 ...
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.
"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 ;-)
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.
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).
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...
"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.
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...
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.