Forum: Mikrocontroller und Digitale Elektronik STM32 Locking in C


von Stefan F. (Gast)


Lesenswert?

Hallo,
ich möchte verhindern, dass eine ISR Daten verändert, die ich gerade 
außerhalb der ISR benutze.

Bei AVR kenne ich dazu das Makro ATOMIC_BLOCK. Wie geht man bei STM32 in 
solchen Fällen überlicherweise vor?

Fehlerhaftes Beispiel:
1
volatile uint64_t counter;
2
3
void mein_isr_handler()
4
{
5
    counter++;
6
}
7
8
int main()
9
{
10
    while (1)
11
    {
12
        printf("Counter: %ul",counter);
13
    }
14
}

Dieser Code würde falsche Zahlen ausgeben, wenn die ISR dazwischen 
kommt.

von Lötlackl *. (pappnase) Benutzerseite


Lesenswert?

Ein Makro wie beim AVR ist mir nicht bekannt. Habe ich vor ein paar 
Jahren so gelöst.
1
///////////////////////////////////////////////////////////////////
2
//
3
// check if a key has been pressed. Each pressed key is reported
4
// only once
5
//
6
uint64_t get_key_press(uint64_t key_mask) {
7
  __disable_irq;
8
  key_mask &= key_press;                    // read key(s)
9
  key_press ^= key_mask;                    // clear key(s)
10
  __enable_irq;
11
  return key_mask;
12
}
Ist in der CMSIS in core_cm3.h zu finden.
Man kann es natürlich auch selber hinschreiben.
1
static __INLINE void __enable_irq()               { __ASM volatile ("cpsie i"); }
2
static __INLINE void __disable_irq()              { __ASM volatile ("cpsid i"); }

von g457 (Gast)


Lesenswert?


von Stefan F. (Gast)


Lesenswert?

g457 verwies im Beitrag #5601753 auf:

> Die korrekte Vorgehensweise ist:
> 1. Zustand der Interrupts in einer Variablen sichern
> 2. Interrupt deaktivieren (einen oder alle…)
> 3. Atomarer Block…
> 4. Ursprünglichen Zustand der Interrupts wieder herstellen!

> Im Definitive Guide to ARM® Cortex®-M3 and Cortex®-M4 Processors
> empfehlen sie die Verwendung von __get_PRIMASK() und __set_PRIMASK().

Das klingt vielversprechend, schaue ich mir mal an.

von Jim M. (turboj)


Lesenswert?

Stefanus F. schrieb:
> Dieser Code würde falsche Zahlen ausgeben, wenn die ISR dazwischen
> kommt.

Machs Dir nicht zu kompliziert:

volatile uint64_t counter;

uint64_t readCounter() {

uint64_t cval1;
uint64_t cval2;

do {
  cval1 = counter;
  cval2 = counter;
} while (cval1 != cval2);

return cval1;
}

Meistens will man nur sicherstellen dass der gelesene Wert einigermaßen 
valid ist.

von Dr. Sommer (Gast)


Lesenswert?

Jim M. schrieb:
> Meistens will man nur sicherstellen dass der gelesene Wert einigermaßen
> valid ist.

Was wenn bei jedem Zugriff ein Interrupt dazwischen kommt? Da ist 
Interrupts abschalten doch die deutlich bessere Lösung.

von Jim M. (turboj)


Lesenswert?

Dr. Sommer schrieb:
> Was wenn bei jedem Zugriff ein Interrupt dazwischen kommt? Da ist
> Interrupts abschalten doch die deutlich bessere Lösung.

Dann hast Du GANZ andere Probleme, denn dann läuft  main() nur noch im 
Schneckentempo.

Meistens ist das der Punkt wo man Interrupts verliert, z.B. Zeichen aus 
dem UART nicht mehr ankommen.

Zum Vergleich: Interrupt Entry und Exit allein sind bei Cortex-M3 
jeweils 12 Takte - das reicht hier für die komplette Funktion falls kein 
Interrupt dazwischen funkt.

Und wenn beim Cortex-M die Interrupt Last zu hoch wird, dann wird main() 
anders als bei AVR gar nicht mehr ausgeführt. Das merken aber schon 
Anfänger wenn man das Rücksetzen eines Flags im Handler vergisst.

von Stefan F. (Gast)


Lesenswert?

Jim M. schrieb:
> Machs Dir nicht zu kompliziert (und Lösungsvorschlag
> durch Wiederholtes Lesen).

Diese Variante nutze beim Lesen der RTC. Meine Frage zielte allerdings 
auf eine allgemeingültige Lösung basierend auf Locking die auch mit 
erheblich größeren Datenstrukturen und I/O Schnittstellen funktioniert.

Über deren Nachteile denke ich natürlich auch nach.

von Jim M. (turboj)


Lesenswert?

Stefanus F. schrieb:
> Meine Frage zielte allerdings
> auf eine allgemeingültige Lösung basierend auf Locking die auch mit
> erheblich größeren Datenstrukturen und I/O Schnittstellen funktioniert.

Und da erwartest Du ernsthaft eine allgemeingültige Lösung?

Ich nehme da gerne Ringpuffer, auch weil ich im Interrupt meistens nicht 
blockieren kann oder möchte.

Allgemeingültig ist schon deswegen schwierig weil sich die Frage stellt 
wer wann wie blockieren darf oder nicht. Siehe Informatik: 
Leser-Schreiber-Problem.

von Stefan F. (Gast)


Lesenswert?

Jim M. schrieb:
> Und da erwartest Du ernsthaft eine allgemeingültige Lösung?

Ich dachte, ich hätte die Aufgabe klar genug formuliert:

> ich möchte verhindern, dass eine ISR Daten verändert,
> die ich gerade außerhalb der ISR benutze.

> Bei AVR kenne ich dazu das Makro ATOMIC_BLOCK. Wie geht man
> bei STM32 in solchen Fällen überlicherweise vor?

Der Gast g457 hat meine Frage zufriedenstellend beantwortet. Das in 
bestimmten (vermutlich sogar in den meisten) Fällen weniger heftige 
Sperren genügen, akzeptiere ich.

von Codix (Gast)


Lesenswert?

Ich löse das meist so, dass ich eine globale Variable benutze. In dieser 
setze ich in der ISR ein Bit (Flag) wenn neue Daten vorliegen, das ich 
in der main() abfrage, die entsprechende Funktion ausführe und danach 
das Flag zurücksetze. Die ISR selbst prüft dieses Flag und schreibt die 
Daten, falls das Flag nicht frei ist, in einen Puffer. Meist ist der 
Puffer nicht notwendig, speziell bei ISRs mit zeitlich langem Intervall.
Damit komme ich in fast allen Fällen ohne Interruptsperre aus.

That's my 2 cents.

von Stefan F. (Gast)


Lesenswert?

Guter Vorschlag. Danke

von Jan K. (jan_k)


Lesenswert?

Zumindest cortex m3 und aufwärts haben wunderbare Instruktionen, um 
variablen lockless atomar zu schreiben und zu lesen. Die heißen ldrex 
und strex, siehe z.b da: 
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/ch01s03s03.html

Gibt's natürlich auch als C Variante mit Compiler intrinsics.

von Ruediger A. (Firma: keine) (rac)


Lesenswert?

Jan K. schrieb:
> Zumindest cortex m3 und aufwärts haben wunderbare Instruktionen, um
> variablen lockless atomar zu schreiben und zu lesen. Die heißen ldrex
> und strex, siehe z.b da:
> 
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/ch01s03s03.html
>
> Gibt's natürlich auch als C Variante mit Compiler intrinsics.

Jein. Der Exclusive Access Monitor (EAM) ist zur Synchronisation 
zwischen Applikation(en) und ISRs nicht geeignet. Der Begriff "atomar" 
ist auch etwas ungünstig in diesem Zusammenhang gewählt. Der EAM erlaubt 
es im Prinzip, kooperierenden Handlungssträngen die Möglichkeit zu 
geben, zu "pollen," ob ein Anderer Handlungsstrang nebenläufig Zugriff 
auf die Variable bansprucht. Das ist natürlich fatal, wenn ein ISR in 
einer Schleife sitzt und darauf wartet, dass eine Task fertig ist. Geht 
sozusagen gar nicht.

Der asymmetrische Fall (ISRs vs. Tasks), auf den Stefan aus ist, lässt 
sich eigentlich wie in jeder Prozessorarchitektur nur mit Ausmaskieren 
von Interrupts in den Griff kriegen. Das lässt sich allerdings beim 
Cortex recht fein granulieren (wenn man z.B, weiss, welcher ISR 
konkurriert, lässt sich genau dieser Interrupt ausmaskieren, und Andere 
Interrupts höherer und niedriger Priorität können fröhlich 
weiterlaufen).

Nebenbei bemerkt: cpsie() und cpsid() arbeiten unabhängig vom PRIMASK 
register, d.h. durch cpsie() und cpsid() lässt sich ein vorher durch 
PRIMASK gesetzter Interruptlevel kurzzeitig "global" hochsetzen. Das ist 
Anders als bei vielen Anderen Prozessorarchitekturen.

von Jan K. (jan_k)


Lesenswert?

Bin verwirrt, warum sollte das nicht gehen? Wir reden von Single core 
Prozessoren. Dh, entweder die ISR oder der App "thread" läuft. Haut der 
Interrupt zwischen dem read modify write der App rein, schlägt der 
exklusive Zugriff fehl. Spätestens beim 2. Zugriff (in der do while 
schleife, beim Schreiben der variablen in der ist) sollte es aber 
funktionieren.
Nach Ende der isr schlägt der Check in der App auch fehl und die Daten 
werden neu aus dem RAM geladen. Kann sein, dass man unter bestimmten 
Bedingungen noch eine memory barrier braucht.

Sehe momentan noch nicht den Grund, warum das so nicht geht :)

Bezüglich atomar hast du recht, es ist immer noch ein read modify write, 
nur dass eben auf exklusiven Zugriff geachtet wird und man die schleife 
drumherum bauen muss.

von Ruediger A. (Firma: keine) (rac)


Lesenswert?

Da der ISR per Definition eine höhere Prio hat als der thread, wird er 
die loop niemals verlassen (wann sollte der thread drankommen, um seinen 
lock wieder freizugeben)? Oder willst Du den ISR erst verlassen und dann 
den thread seine Freigabe machen lassen? Wer würde in dem Fall den ISR 
nochmals feuern?

Grundsätzlich ist loopen mit nicht deterministischer Länge im ISR ein 
Nono, selbst wenn o.g. Szenario verhindert werden könnte. Da der 
Prozessor seinen Zustand wechselt, sobald ein ISR anfängt zu Laufen, ist 
die einzig gute Lösung, den Zustandswechsel während der kritischen Phase 
so kurz wie nötig zu unterdrücken (ausmaskieren).

von Mampf F. (mampf) Benutzerseite


Lesenswert?

Jan K. schrieb:
> Zumindest cortex m3 und aufwärts haben wunderbare Instruktionen,
> um
> variablen lockless atomar zu schreiben und zu lesen. Die heißen ldrex
> und strex, siehe z.b da:
> http://infocenter.arm.com/help/index.jsp?topic=/co...
>
> Gibt's natürlich auch als C Variante mit Compiler intrinsics.

Die richtige Lösung wurde schon von Jan K. genannt.

Alles andere ist Murks.

von Jan K. (jan_k)


Lesenswert?

Hi,

Das passiert so nicht. Der exklusive Monitor schätzt afaik nur möglichst 
pessimistisch, ob im selben Moment ein Speicherzugriff auf die selbe 
Adresse stattfindet.
Klar hat die ISR Priorität. Aber beim 2. Versuch in der isr kann kein 
paralleler Zugriff mehr stattfinden, da wir ja nur in der isr sind.

Ich habe das so hier implementiert, und zähle zum Spaß mit, wie häufig 
der das lesen / schreiben wiederholt werden musste. Glaube war so 
1/10000, muss aber nochmal nachgucken. Deadlocks gab es keine.

Versuche auch nochmal zu nachzulesen, wann genau der Monitor gecleart 
wird.

PS, es geht hier nur um das Inkrementieren einer Zählvariablen, vllt hat 
das Missverständnisse verursacht?

Schöne Grüße!

von Dr. Sommer (Gast)


Lesenswert?

Ruediger A. schrieb:
> Da der ISR per Definition eine höhere Prio hat als der thread, wird er
> die loop niemals verlassen (wann sollte der thread drankommen, um seinen
> lock wieder freizugeben)?

Die ISR muss natürlich am Anfang einmal die CLREX-Instruktion ausführen, 
um alle Locks zu löschen, genau wie der Dispatcher bei einem "echten" 
Multithreading-System. Das signalisiert dem "Thread" (main-Loop), dass 
zwischen dessen LDREX-STREX-Paar ein Zugriff stattgefunden hat, sodass 
die main es nochmal probieren muss.

Ruediger A. schrieb:
> Grundsätzlich ist loopen mit nicht deterministischer Länge im ISR ein
> Nono, selbst wenn o.g. Szenario verhindert werden könnte

Du brauchst ja auch nicht Loopen, es sei denn du verwendest 
verschachtelte Interrupts und die ISR könnte selbst wieder unterbrochen 
werden. Ansonsten reicht CLREX-LDR-STR, es kann ja nichts dazwischen 
kommen.

Jan K. schrieb:
> Gibt's natürlich auch als C Variante mit Compiler intrinsics.
Die braucht man aber gar nicht - zumindest beim GCC kann man auch 
std::atomic bzw. _Atomic nutzen, das nutzt dann automatisch die -EX 
Instruktionen.

von Ruediger A. (Firma: keine) (rac)


Lesenswert?

Ja, stimmt, ich hatte hier nicht die genaue Semantik berücksichtigt. Da 
das Ganze nicht einfach zu durchschauen ist, hier die Funktionsweise in 
einer Nussschale:

Der EAM koppelt Lese-und Schreibzugriffe auf eine Speicheradresse. Ein 
strex() auf eine Adresse xyz wird fehlschlagen*, wenn nicht vorher auf 
die Adresse mindestens(!) ein ldrex() passiert ist. Mit dem strex() wird 
die Markierung aufgehoben, so dass ein zweites strex() nichts bewirkt. 
Der Nettoeffekt ist, dass jede durch Scheduler und/oder Interruptfolge 
generierte Sequenz

1 <Strang 1 liest xyz>
2 <Strang 2 liest xyz>
3 <Strang 2 schreibt xyz>
4 <Strang 1 schreibt xyz>

keinen Schaden anrichtet, weil step 4 ins Leere läuft, als keine 
inkonsistenten Daten erzeugt.

Im ISR Fall sind wir tatsächlich auf der sicheren Seite, weil der ISR 
durch seine höhere Priorität mindestens das erste, ggf. das zweite 
ldrex() durchführt und damit immer der erste Handlungsstrang ist, dessen 
Schreiben erfolgreich ist. Damit ist der unterbrochene Handlungsstrang 
derjenige, der seinen Schreibvorgang nochmal auslösen muss.

Im ursprünglichen Beispiel gibt es übrigens hier kein Problem, weil der 
eine Handlungsstrang (main loop) nur liest und nicht schreibt. Es könnte 
dann ein Problem geben, wenn der Wert von counter an zwei Stellen 
gebraucht wird und nicht klar ist, dass der faktische Wert sich in der 
Zwischenzeit geändert haben könnte (s.u. bei *).

N.B. Es ist wichtig hier zu sehen, dass es verschiedene 
Abstraktionsebenen gibt. Für die EAM Befehle gibt es hicht nur Lib 
spezifische Macros als Abstraktionen, sondern auch C Wrappers 
(__sync_sub_and_fetch etc), die sich aber dadurch unterscheiden, dass 
sie tatsächlich loopen, bis das strex() erfolgreich gewesen ist. Das 
kann im Fall von verschachtelten Interrupts tatsächlich zu subtilen 
Timingphänomenen führen.


* fehlschlagen heisst, das der Schreibzugriff nicht ausgeführt und der 
fehlgeschlagene Status dem Aufrufer mitgeteilt wird. Der Aufrufer kann 
das Ergebnis ignorieren, hat damit dann aber ggf. selber eine inkorrekte 
Annahme über den Wert von xyz - falls der neu berechnete Wert also 
weiter verwendet wird, ist das Ergebnis abhängig davon, ob xyz 
tatsächlich wieder gelesen oder

von Dr. Sommer (Gast)


Lesenswert?

Ruediger A. schrieb:
> Im ursprünglichen Beispiel gibt es übrigens hier kein Problem, weil der
> eine Handlungsstrang (main loop) nur liest

Es ist aber ein 64bit-Wert, und die können nicht atomar gelesen werden. 
Außerdem:
"LDREXD and STREXD are not supported in ARMv7-M." (ARM-v7M ArchRef, S. 
65). Daher ist eine Interruptsperre hier wirklich erforderlich, wenn man 
sich nicht auf "wird schon klappen" verlassen möchte:

Jim M. schrieb:
> Dann hast Du GANZ andere Probleme, denn dann läuft  main() nur noch im
> Schneckentempo.

Stefanus F. schrieb:
> Diese Variante nutze beim Lesen der RTC.
Da ist das auch ok, weil hier garantiert ist dass die RTC die Werte 
regelmäßig und langsam verändert.

von W.S. (Gast)


Lesenswert?

Stefanus F. schrieb:
> ich möchte verhindern, dass eine ISR Daten verändert, die ich gerade
> außerhalb der ISR benutze.
>
> Bei AVR kenne ich dazu das Makro ATOMIC_BLOCK. Wie geht man bei STM32 in
> solchen Fällen überlicherweise vor?

Das ist eigentlich unabhängig von der Architektur.

Du hast zwei Instanzen, die sich um eine Ressource katzbalgen. Entweder 
du schaffst dir eine Möglichkeit, daß beide sich irgendwie verständigen 
oder es geht irgendwann mal schief.

Was soll denn deine Interruptroutine tun, wenn sie mitkriegt, daß sie 
die Ressource nicht aktualisieren darf? Solange warten, bis die andere 
Seite da raus ist, GEHT NICHT. Den Interrupt abwürgen, solange der 
Andere drin herumgrabscht, würde eine vielleicht wichtige Reaktion des 
Systems zur rechten Zeit(!!!) verhindern.

Vorschlag 1: Du packst in der ISR die Daten in einen Ringpuffer und 
übermittelst dem Rest der Welt lediglich eine Botschaft, die besagt, man 
möge sich den Datenblock gefälligst aus selbigem wieder herausklauben. 
Falls der Ringpuffer jemals überlaufen sollte, ist dein µC sowieso 
überlastet.

Vorschlag 2: Du richtest dir eine direkte Synchronisation zwischen ISR 
und Rest ein, z.B. so, daß jede Instanz einen popligen Zähler hat, den 
nur sie selbst inkrementiert. Die ISR speichert den Wert nur dann in den 
allgemeinen Puffer, wenn beide Zähler gleich sind - und der "Rest" liest 
nur dann den Wert aus, wenn beide Zähler ungleich sind. Das ist 
sozusagen der Ringpuffer mit nur einem Platz darinnen.

W.S.

von Stefan F. (Gast)


Lesenswert?

Eigentlich wollte ich dieses mal nur Interrupts temporär sperren. Die 
anderen Lösungsansätze kenne ich wie gesagt.

von W.S. (Gast)


Lesenswert?

Stefanus F. schrieb:
> Eigentlich wollte ich dieses mal nur Interrupts temporär sperren.

Sowas hatten wir damals beim Z80 gemacht.

Aber heutzutage sollte man so etwas erst garnicht mehr in Erwägung 
ziehen. Ja, bei den derzeitigen Cortexen stecken derartige Dinge noch 
immer in den CMSIS-Bibliotheken, aber schon vorher bei den ARM7TDMI sah 
es ganz anders aus, da hatte man diverse Stacks und wenn die gewöhnliche 
App-Schicht der Firmware im Usermodus läuft, gehen solche Spielereien 
garnicht. Ich halte das temporäre Abwürgen von Interrupts auch ganz 
generell für schlechten Stil. Mit einem vernünftigen Systementwurf 
braucht man sowas auch nicht wirklich.

W.S.

von Gerd E. (robberknight)


Lesenswert?

W.S. schrieb:
> Ich halte das temporäre Abwürgen von Interrupts auch ganz
> generell für schlechten Stil. Mit einem vernünftigen Systementwurf
> braucht man sowas auch nicht wirklich.

Lass mal die Kirche im Dorf. Es hängt ganz davon ab wie lange die 
Sequenz ist unter der die Interrupts gesperrt sind.

Sagen wir mal das sind nur 4 Takte oder so. Alleine um in den IRQ zu 
kommen, braucht der Cortex-M3 aber schon 12 Takte. Wenn es bei dem 
Verhältnis jetzt wirklich auf diese 4 Takte ankommt daß Du Deine 
geforderte Antwortzeit einhalten kannst, fährst Du die Hardware so nah 
am Limit, daß ich sagen würde das Konzept oder der verwendete Controller 
passt nicht.

von Dr. Sommer (Gast)


Lesenswert?

W.S. schrieb:
> die besagt, man möge sich den Datenblock gefälligst aus selbigem wieder
> herausklauben.

Und wie holt man Daten aus einem Ringpuffer, ohne die Interrupts zu 
sperren? Die diversen Metadaten (Lesezeiger, Füllstand, ...) atomar zu 
ändern ist sehr kompliziert. Atomare nicht-blockierende Queues sind ein 
Forschungsgebiet...

von Mampf F. (mampf) Benutzerseite


Lesenswert?

W.S. schrieb:
> Aber heutzutage sollte man so etwas erst garnicht mehr in Erwägung
> ziehen.

Aus ästhetischen Gründen, weil man das halt nicht mehr macht oder wie? 
;-)

Wenn nicht dagegen spricht, die Interrupts zu sperren, spricht nichts 
dagegen.

Warum umständlich irgendwelche Ringpuffer für ein (wahrscheinlich) 
triviales Problem bauen?

Der TE muss entscheiden, ob es für ihn ein Problem ist oder nicht, die 
Ints zu sperren.

Ein gutes Pferd springt nur so hoch es muss ;-)

: Bearbeitet durch User
von Ruediger A. (Firma: keine) (rac)


Lesenswert?

Dr. Sommer schrieb:
> W.S. schrieb:
>> die besagt, man möge sich den Datenblock gefälligst aus selbigem wieder
>> herausklauben.
>
> Und wie holt man Daten aus einem Ringpuffer, ohne die Interrupts zu
> sperren? Die diversen Metadaten (Lesezeiger, Füllstand, ...) atomar zu
> ändern ist sehr kompliziert. Atomare nicht-blockierende Queues sind ein
> Forschungsgebiet...

Da muss ich mal für W.S. in die Bresche springen... da in dieser 
speziellen Situation die Rollen klar verteilt sind - ISR schreibt, App 
liest - gibt es einen möglichen Konflikt nur in dem Fall, dass der 
Ringpuffer überläuft. Ansonsten ist es der lesenden App egal, ob der ISR 
in der Zwischenzeit ein weiteres Zeichen einpflegt oder nicht; je nach 
Timing wird das /werden die neue/n Zeichen noch in diesem Zyklus 
verarbeitet oder im nächsten. Die Vor- und Nachlaufzeiger sind ja bis 
auf das "Berührungsszenario" voneinander unabhängig, so dass der 
Vorlaufzeiger vom ISR und der Nachlaufzeiger von der App verwaltet wird 
(dabei hilft, dass beide immer in die selbe Richtung laufen).

Zumindestens auf der Rx Seite habe ich schon viele Anwendungen gesehen, 
die problemlos und stabil so laufen (insbesonders bei HD Protokollen).

Die Pauschalaussage von W.S. kann auch ich aber nicht so stehen lassen; 
es gibt noch genügend reale Fälle, in denen Interruptsperren 
unvermeidbar sind (z.B. beim RTOS, wo es Codesabschnitte gibt, die 
einfach nicht unterbrochen werden dürfen, zumindestens nicht von ISRs, 
die ebenfalls RTOS Services benutzen). Eine "globale" Sperre ALLER ISRs 
ist aber in der Regel mit Kanonen auf Spatzen geschossen; die meisten 
Prozessorarchitekturen erlauben recht fein granulierbare Teilsperren.

von Dr. Sommer (Gast)


Lesenswert?

Ruediger A. schrieb:
> Ansonsten ist es der lesenden App egal, ob der ISR
> in der Zwischenzeit ein weiteres Zeichen einpflegt oder nicht;

Kannst du dafür mal ein Beispiel zeigen? Ein typischer Ringpuffer hat 
ja:
- Lesezeiger
- Schreibzeiger
- Füllstand (für wenn Lesezeiger=Schreibzeiger, um zwischen ganz voll 
und ganz leer zu unterscheiden)
- Reduzierte Größe - Wird genutzt falls ein Datenblock nicht mehr ans 
Ende des Puffers passt, man stattdessen wieder am Anfang anfängt, und 
daher den Puffer temporär als geschrumpft angibt

Die alle konsistent zu lesen und zu verändern, sodass bei 
Unterbrechungen nichts passieren kann, finde ich nicht so einfach. 
Besonders interessant ist es natürlich, wenn von mehreren ISRs, die sich 
gegenseitig unterbrechen können, geschrieben werden kann.

von Nils P. (torus)


Lesenswert?

Typischer Ringbuffer für Application und Interrupts? Gern. Hab ich 
zufällig am Samstag geschrieben:

  https://pastebin.com/La3t0Phx

Das funktioniert lock-less, wenn der Interrupt lediglich RBGet und die 
Application RBPut aufruft (bzw. natürlich auch, wenn die Rollen 
vertauscht sind).

von Peter D. (peda)


Lesenswert?

Ruediger A. schrieb:
> Eine "globale" Sperre ALLER ISRs
> ist aber in der Regel mit Kanonen auf Spatzen geschossen; die meisten
> Prozessorarchitekturen erlauben recht fein granulierbare Teilsperren.

Ist es nicht, denn damit ist die maximale Zeit der Interruptsperre klar 
definiert. Sie ist immer auf den reinen Read-Modify-Write Zugriff 
begrenzt.

Sperre ich dagegen nur den Interrupt, mit dem es Konflikte gibt, können 
sich alle anderen Interrupts niederer Priorität dazwischen drängeln und 
der kritische Zeitraum kann sich um ein Vielfaches verlängern. Dadurch 
entstehende Probleme sind außerdem extrem schwer zu debuggen.

Die globale Sperre hat daher deutlich weniger Seiteneffekte und 
Fallstricke als jede andere Lösung.
Mir fällt jetzt auch keine Konstellation ein, wo ein Programm Fehler 
macht, wenn mal ein Interrupt 4 CPU-Zyklen später ausgeführt wird.

von Dr. Sommer (Gast)


Lesenswert?

Nils P. schrieb:
> Typischer Ringbuffer für Application und Interrupts? Gern. Hab ich
> zufällig am Samstag geschrieben:

Vielen Dank!
1
static inline bool RBIsFull(const ringBuffer * r)
2
{
3
    size_t next = (r->head+1 == r->len) ? 0: r->head+1;
4
    return next == r->tail;
5
}
Das bedeutet der RB ist voll wenn noch 1 Byte frei ist?

Deine RBPut und RBGet Funktionen können jeweils nur 1 Byte bearbeiten. 
Die für jedes Byte einzeln aufzurufen wäre ziemlich langsam. Was machst 
du in dieser Situation:

len=10
head=7
tail=5

Es sind also 8 Plätze frei. Man möchte einen Datenblock von 5 Bytes 
schreiben, kann aber nicht weil am Ende nur 3 Bytes frei sind. Den Block 
in der Mitte zu teilen wäre ineffizient, weil man dann Zugriffe 
byteweise machen muss.

von Nils P. (torus)


Lesenswert?

Dr. Sommer schrieb:
> Das bedeutet der RB ist voll wenn noch 1 Byte frei ist?

Ja, das ist Absicht so. Du hast ansonsten immer das Problem, das Du 
Buffer voll und Buffer leer nicht direkt unterscheiden kannst. Natürlich 
kannst Du das mit zusätzlichen Variablen erledigen, aber die kosten auch 
mindestens ein Byte und machen es zudem komplizierter. Es ist also netto 
unter dem Strich egal.


> Deine RBPut und RBGet Funktionen können jeweils nur 1 Byte bearbeiten.
> Die für jedes Byte einzeln aufzurufen wäre ziemlich langsam.

In dieser Simpel Variante ist das in der Tat so, aber niemand kann mich 
davon abhalten statt Bytes z.B gleich ganze USB oder DMA Frames zu 
verwalten.

Geschrieben habe ich das ganze für simples RS232, da fällt die byteweise 
Verarbeitung nicht weiter ins Gewicht.

von Dr. Sommer (Gast)


Lesenswert?

Nils P. schrieb:
> In dieser Simpel Variante ist das in der Tat so, aber niemand kann mich
> davon abhalten statt Bytes z.B gleich ganze USB oder DMA Frames zu
> verwalten.

Kannst du auch dafür ein Beispiel zeigen, natürlich für variable 
Frame-Größe und ohne Interrupt-Sperre? Für einzelne Bytes ist das ja 
uninteressant.

von Vincent H. (vinci)


Lesenswert?

Dr. Sommer schrieb:
> Ruediger A. schrieb:
>> Ansonsten ist es der lesenden App egal, ob der ISR
>> in der Zwischenzeit ein weiteres Zeichen einpflegt oder nicht;
>
> Kannst du dafür mal ein Beispiel zeigen? Ein typischer Ringpuffer hat
> ja:
> - Lesezeiger
> - Schreibzeiger
> - Füllstand (für wenn Lesezeiger=Schreibzeiger, um zwischen ganz voll
> und ganz leer zu unterscheiden)
> - Reduzierte Größe - Wird genutzt falls ein Datenblock nicht mehr ans
> Ende des Puffers passt, man stattdessen wieder am Anfang anfängt, und
> daher den Puffer temporär als geschrumpft angibt
>
> Die alle konsistent zu lesen und zu verändern, sodass bei
> Unterbrechungen nichts passieren kann, finde ich nicht so einfach.
> Besonders interessant ist es natürlich, wenn von mehreren ISRs, die sich
> gegenseitig unterbrechen können, geschrieben werden kann.


Grundsätzlich stimme ich dir zu. Einen allgemein gültigen Ringbuffer zu 
schreiben ist keine leichte Aufgabe. Der springende Punkt ist in meinen 
Augen jedoch, dass man das auf einem STM32 ohnehin nicht braucht.

Ein Großteil aller Use-Cases für embedded Gschichtln enthält ja ziemlich 
strenge Constraints:
- push_back, pop_front, sprich ein Ende lesend, ein Ende schreibend
- maximal in 1 ISR genutzt oder zumindest kein preemptive möglich
- single threaded

Geht man jetzt noch davon aus, dass man nicht an die Performance Grenze 
des Chips geht und die Interrupts schnell genug bedienen kann, dann 
braucht man sich um "locking" eigentlich gar nicht mehr kümmern...

von Ruediger A. (Firma: keine) (rac)


Lesenswert?

Dr. Sommer schrieb:
> Ruediger A. schrieb:
>> Ansonsten ist es der lesenden App egal, ob der ISR
>> in der Zwischenzeit ein weiteres Zeichen einpflegt oder nicht;
>
> Kannst du dafür mal ein Beispiel zeigen? Ein typischer Ringpuffer hat
> ja:
> - Lesezeiger
> - Schreibzeiger

ja, ich benutze dafür i.d.Regel die Begriffe Nachlaufzeiger und 
Voralufzeiger, aber das ist ja egal.

> - Füllstand (für wenn Lesezeiger=Schreibzeiger, um zwischen ganz voll
> und ganz leer zu unterscheiden)
> - Reduzierte Größe - Wird genutzt falls ein Datenblock nicht mehr ans
> Ende des Puffers passt, man stattdessen wieder am Anfang anfängt, und
> daher den Puffer temporär als geschrumpft angibt
>

Meinem Verständnis nach brauchst Du die beiden nicht, solange der 
Vorlaufzeiger (Schreibzeiger, bei Rx im ISR) immer sicher stellt, dass 
er den Nachlaufzeiger nicht überholt. Ich visualisiere mir das immer 
gerne so, dass der Wertebereich der beiden Zeiger 2* die Puffergrösse 
einnehmen kann, der Zugriff auf den Puffer dann aber Modulo der 
Puffergrösse erfolgt. Es ist also im Prinzp kein Unterschied zwischen 
einem Wraparound oder nicht warparound, solange der Vorlaufzeiger immer 
vor dem Nachlaufzeiger steht. Bei Blocktransfers (z.B. FIFO USARTs) ist 
das natürlich ein Problem, wie schon in der Zwischenzeit eingeworfen 
wurde, aber ein Überlauf ist ein Überlauf und somit immer ein Problem, 
für das es i.d. Regel keine saubere Lösung gibt, das muss auf höheren 
Protokollschichten durch retransmissions etc. abgefangen werden. Man 
könnte also in dem Fall "einfach" auf das Maximum des freien Puffers vor 
dem Nachlaufzeiger und dem empfangenen Datenblock auffüllen. Verlorene 
Daten sind verloren, das ist halt so...

> Die alle konsistent zu lesen und zu verändern, sodass bei
> Unterbrechungen nichts passieren kann, finde ich nicht so einfach.

Naja, was in unserem Job ist einfach, wenn man nach der bestmöglichen 
Lösung sucht? ;-)

> Besonders interessant ist es natürlich, wenn von mehreren ISRs, die sich
> gegenseitig unterbrechen können, geschrieben werden kann.

Uhm, Du meinst mehrere nicht synchronisierte 
Kommunikationsschnittstellen, die denselben linearen Buffer beschreiben? 
Woher soll die Wegverarbeitung dann die Quellen voneinander 
unterscheiden können bzw. gibt es dafür einen use case?

Das wäre in der Tat spannend, beträfe aber eigentlich nur das korrekte 
Führen des Schreibzeigers. Dem Lesezeiger wäer das meiner ersten groben 
Einschätzung nach egal, oder?

von Dr. Sommer (Gast)


Lesenswert?

Ruediger A. schrieb:
> ja, ich benutze dafür i.d.Regel die Begriffe Nachlaufzeiger und
> Voralufzeiger, aber das ist ja egal.

Klingt nach Klempnern :D

Ruediger A. schrieb:
> aber ein Überlauf ist ein Überlauf

Die gezeigte Situation ist kein Überlauf - es ist genug Platz da - man 
kann ihn nur nicht nutzen:

Dr. Sommer schrieb:
> len=10
> head=7
> tail=5

Ruediger A. schrieb:
> Woher soll die Wegverarbeitung dann die Quellen voneinander
> unterscheiden können bzw. gibt es dafür einen use case?
z.B. aus einem Header im Paket in der Queue. Das könnten mehrere 
Netzwerk-Schnittstellen sein, die Pakete in den IP-Stack werfen. 
Ereignisse aus verschiedenen Schnittstellen, die aber zusammen 
verarbeitet werden müssen. Nachrichten im Aktor-Modell.

Ruediger A. schrieb:
> Das wäre in der Tat spannend, beträfe aber eigentlich nur das korrekte
> Führen des Schreibzeigers.
Vermutlich... Das Problem ist sogar so kompliziert dass bestehende 
Lösungen einfach mehrere Queues nutzen:

http://moodycamel.com/blog/2014/a-fast-general-purpose-lock-free-queue-for-c++

The less threads fight over the same data, the better. So, instead of 
using a single data structure that linearizes all operations, a set of 
sub-queues is used instead -- one for each producer thread.

von Ruediger A. (Firma: keine) (rac)


Lesenswert?

Peter D. schrieb:
> Ruediger A. schrieb:
>> Eine "globale" Sperre ALLER ISRs
>> ist aber in der Regel mit Kanonen auf Spatzen geschossen; die meisten
>> Prozessorarchitekturen erlauben recht fein granulierbare Teilsperren.
>
> Ist es nicht, denn damit ist die maximale Zeit der Interruptsperre klar
> definiert. Sie ist immer auf den reinen Read-Modify-Write Zugriff
> begrenzt.
>
> Sperre ich dagegen nur den Interrupt, mit dem es Konflikte gibt, können
> sich alle anderen Interrupts niederer Priorität dazwischen drängeln und
> der kritische Zeitraum kann sich um ein Vielfaches verlängern. Dadurch
> entstehende Probleme sind außerdem extrem schwer zu debuggen.
>

Es ging mir dabei nicht unbedingt um das chirurgische Sperren nur der 
von dem Synchronisation betroffenen ISRs (obwohl sich dafür auch use 
cases finden lassen), sondern auch um das Sperren Aller ISRs BIS ZU dem 
betroffenen level (Bsp. configMAX_SYSCALL_INTERRUPT_PRIORITY in 
FreeRTOS). Natürlich werden auch dann höher priorisierte hereinkommende 
Interrupts den Umlauf verzögern, aber das ist ja gewollt (deswegen haben 
die ISRs ja die höhere Priorität). Das globale Sperren ALLER Interrupts 
(cpsid beim Cortex) würde ich wenn möglich nur auf deterministisch 
extrem kurze Zeiten beschränken, in denen tatsächlich ein 
"Systemstillstand" unvermeidbar ist.

> Die globale Sperre hat daher deutlich weniger Seiteneffekte und
> Fallstricke als jede andere Lösung.
> Mir fällt jetzt auch keine Konstellation ein, wo ein Programm Fehler
> macht, wenn mal ein Interrupt 4 CPU-Zyklen später ausgeführt wird.

von Jack (Gast)


Lesenswert?

Nils P. schrieb:
> Typischer Ringbuffer für Application und Interrupts? Gern. Hab ich
> zufällig am Samstag geschrieben:
>
>   https://pastebin.com/La3t0Phx
>
> Das funktioniert lock-less, wenn der Interrupt lediglich RBGet und die
> Application RBPut aufruft (bzw. natürlich auch, wenn die Rollen
> vertauscht sind).

Die kritischen Teile sind
1
static inline bool RBIsEmpty(const ringBuffer * r)
2
{
3
    return (r->head == r->tail);
4
}
5
6
7
static inline bool RBIsFull(const ringBuffer * r)
8
{
9
    size_t next = (r->head+1 == r->len) ? 0: r->head+1;
10
    return next == r->tail;
11
}

Z.B. Ein paralleles Schreiben von r->head in RBPut() sprengt den 
Vergleich in RBIsEmpty() wenn Schreiben und Lesen von r->head nicht 
atomar ist.

Das muss die Plattform bzw. der Kontext in dem die Funktionen aufgerufen 
werden, garantieren. Wer den Code verwendet muss schon genau hinsehen 
und wissen was seine Plattform macht, sonst gibt es Tränen.

von W.S. (Gast)


Lesenswert?

Mampf F. schrieb:
> Aus ästhetischen Gründen, weil man das halt nicht mehr macht oder wie?

Nein. Nicht aus ästhetischen Gründen.

Den Fall, daß der Usercode auch nur unter Userbedingungen ausgeführt 
wird und derartige Sperrungen schlichtweg wegen fehlendem Privileg-Level 
nicht drin sind, schließe ich hier mal aus. Das ist bei kleinen µC eher 
nicht Mode.

Aber:
Wo soll denn die Sperrung und die korrekte Entsperrung stattfinden? In 
der Grundschleife in main oder in irgendeinem aufgerufenen Treiber oder 
einer Funktion? Ja? Und wenn irgend eine andere Funktion ebenfalls (aber 
aus anderen Gründen) die Int's sperrt und wieder freigibt, dann kommt es 
zusätzlich noch zu einem Konflikt zwischen mehreren Funktionen.

Ich habe selber sowas schon erlebt, deswegen weiß ich, daß man dann das 
Sperren und Entsperren kellern muß, also einen Zähler beim Sperren 
raufzählen und beim Entsperren wieder herunterzählen und nur dann, wenn 
er Null wird, tatsächlich wieder entsperren.

Das ist ein elendiglicher Streß, der im Prinzip völlig unnötig ist, wenn 
man sich der Methoden bedient, die ich geschildert habe.


Dr. Sommer schrieb:
> Und wie holt man Daten aus einem Ringpuffer, ohne die Interrupts zu
> sperren? Die diversen Metadaten (Lesezeiger, Füllstand, ...) atomar zu
> ändern ist sehr kompliziert.

Dann laß dir dein Lehrgeld zurückzahlen. Jeder Ringpuffer ist im Grunde 
EASY. Es gibt einen Schreibzeiger und einen Lesezeiger, die atomar les- 
und schreibbar sind - egal wie umfänglich die gepufferten Elemente sind 
und jede Instanz verwaltet nur den eigenen Zeiger. Den anderen darf sie 
lesen, aber nicht schreiben. Das ist ALLES.


Ruediger A. schrieb:
> Die Pauschalaussage von W.S. kann auch ich aber nicht so stehen lassen;
> es gibt noch genügend reale Fälle, in denen Interruptsperren
> unvermeidbar sind (z.B. beim RTOS, wo es Codesabschnitte gibt, die
> einfach nicht unterbrochen werden dürfen

Gerade bei RTOS auf ARM bzw. Cortex sieht das jedoch anders aus, da 
macht man so etwas per SVC. Ach ja, GCC-Leute kennen das ja nicht. Also 
der Supervisor-Call ist sowas wie ein Software-Interrupt mit eingebauter 
Möglichkeit, Zusatzinformationen zu transferieren.

Der so aufgerufene Supervisor läuft dann üblichermaßen auf einer höheren 
Privileg-Stufe als alles, was ansonsten als unterbrechbar gilt. 
Hochrangige Interrupts, die in jedem Falle durchkommen müssen, sind 
davon ausgenommen, sofern man sie auf einer entspechend höheren 
Vorrangstufe einrichtet.

Bei früheren ARM's haben die verschiedenen Level auch getrennte Stacks, 
so daß der Stack des Usercodes (beginnend ab main()) ein anderer Stack 
ist als der für den Supervisor. Wer damals einmal in die Lernbetty 
geschaut hat, hätte diese Dinge dort sehen und ggf. begreifen können.

W.S.

von W.S. (Gast)


Lesenswert?

Dr. Sommer schrieb:
> static inline bool RBIsFull(const ringBuffer * r)
> {
>     size_t next = (r->head+1 == r->len) ? 0: r->head+1;
>     return next == r->tail;
> }Das bedeutet der RB ist voll wenn noch 1 Byte frei ist?

Erstens trifft das auf Elemente beliebiger Art zu und zweitens ist das 
Testen auf voll oder leer recht simpel:

mytype Ringpuffer[soundsoviel];
int rd, wr;

bool IsPufferLeer(void) {return rd==wr;}
bool IsPufferVoll(void) {return rd==next(wr);}

wobei next eben der Index bzw. Zeiger ist, der sich ergeben würde, wenn 
man noch ein Element in den Puffer tun würde.

Eigentlich ganz einfach: wr darf rd nicht überholen. Und wenn wr direkt 
hinter rd zeigt, dann ist der Puffer eben voll. Dabei ist es EGAL, ob 
die gepufferten Daten nun aus Bytes oder aus fetten struct's bestehen.

W.S.

von Dr. Sommer (Gast)


Lesenswert?

W.S. schrieb:
> Dann laß dir dein Lehrgeld zurückzahlen. Jeder Ringpuffer ist im Grunde
> EASY.

Dann erleuchte mich und zeig mal einen lock-freien Ringpuffer her, 
welcher ganze und unterschiedlich große Pakete verwalten kann. Einzelne 
Bytes sind uninteressant.

W.S. schrieb:
> Das ist ALLES.

Wie bereits erwähnt ist eine solche Umsetzung ineffizient.

W.S. schrieb:
> da macht man so etwas per SVC. Ach ja, GCC-Leute kennen das ja nicht

Gerade heute habe ich einen syscall-Mechanismus per SVC mit dem GCC 
implementiert. Nur weil du das nicht hinbekommst heißt das nicht dass 
das nicht geht. Wie das geht findet man übrigens auch ganz schlicht per 
Google.

W.S. schrieb:
> Bei früheren ARM's haben die verschiedenen Level auch getrennte Stacks,

Aktuelle Cortex-M (deaktivierbar) und -A (immer) auch; der SP ist bei 
letzteren gebankt zwischen den verschiedenen Modi (ABT, USR/SYS, IRQ, 
FIQ, UND, SVC).

W.S. schrieb:
> Erstens trifft das auf Elemente beliebiger Art zu und zweitens ist das
> Testen auf voll oder leer recht simpel:

Ja, ich hab's jetzt auch gesehen. Ich hab es halt immer anders rum 
gemacht. Hat auch den netten Nebeneffekt, dass man immer direkt den 
freien Platz parat hat.

von Nils P. (torus)


Lesenswert?

Dr. Sommer schrieb:
> Dann erleuchte mich und zeig mal einen lock-freien Ringpuffer her,
> welcher ganze und unterschiedlich große Pakete verwalten kann. Einzelne
> Bytes sind uninteressant.

Das wollte der OT aber gar nicht. Du reitest hier Requirements rum, die 
gar nicht gefragt waren und gehst vom Hundersten ins Tausendste.

Das nützt niemandem etwas, insbesondere dem OT nicht, der eine einfache 
Frage beantwortet haben wollte. Und für den hab ich eine Antwort 
geliefert mit der er weiterabeiten kann.

Du kommst von der Seite, grätscht hier rein aber bringst dich nicht 
Produktiv ein.



In diesem Sinne: Zeig deinen Ringbuffer, der deinen Ansprüchen genügt.

von Dr. Sommer (Gast)


Lesenswert?

Nils P. schrieb:
> In diesem Sinne: Zeig deinen Ringbuffer, der deinen Ansprüchen genügt

Die Aussage war, dass ein praxistauglicher Lock-Freier Ringpuffer 
ziemlich kompliziert ist (ich habe auch keinen) und daher simples 
Sperren der Interrupts sinnvoller ist - genau das, was der OP haben 
wollte.

von Dr. Sommer (Gast)


Lesenswert?

W.S. schrieb:
> Cortex sieht das jedoch anders aus, da
> macht man so etwas per SVC. Ach ja, GCC-Leute kennen das ja nicht.

Und wie funktioniert das deiner Meinung nach unter ARM-Linux? Wird da 
die C-Library extra mit ARMCC kompiliert?

Daher hier extra nochmal für dich: So geht SVC mit dem GCC und mit Clang 
auf Cortex-A:
1
__attribute__((always_inline)) inline void sleep (uint32_t time) {
2
  register uint32_t r0 __asm__ ("r0") = time;
3
  __asm__ volatile ("svc #1": "+r" (r0) : : "memory", "r1", "r2", "r3", "r12");
4
  return r0;
5
}

Der Wert "time" wird per r0 übergeben. Es wird "SVC #1" ausgeführt. Über 
die Clobber-Liste wird angegeben, dass alle Caller-Save-Register (r1-r3, 
r12) überschrieben werden - genau wie bei einem Funktionsaufruf. Da der 
Cortex-A im Gegensatz zum -M diese Register nicht automatisch beim 
Exception-Eintritt sichert, müsste die Exception dies selbst tun, oder 
eben wie hier dem Aufrufer überlassen. Zudem wird angegeben, dass "r0" 
selbst auch überschrieben werden kann. Somit kann man im 
Exception-Handler, der erstmal in Assembler sein muss, eine gewöhnliche 
C-Funktion aufrufen. Letztlich verhält sich der Aufruf so wie ein 
normaler Funktionsaufruf, aber eben mit "svc" statt "bl". Man kann so 
keine Parameter über den Stack übergeben - das wäre aber ohnehin nicht 
sehr sinnvoll, weil der SVC-Exception-Handler ohnehin einen eigenen 
Stack hat und man nicht so direkt auf den User/System-Stack zugreifen 
kann. Da muss man also mit Pointern arbeiten. Die Rückgabe erfolgt über 
r0, was hier als In-Out-Register angegeben wird.

Alternativ macht man es einfach so, dass man in C(++) nur einen 
Funktionsprototypen erstellt:
1
#ifdef __cplusplus
2
extern "C"
3
#endif
4
uint32_t sleep (uint32_t time);
Und dann eine Assembler-Datei hinzulinkt mit
1
.type sleep, %function
2
sleep:
3
  svc #1
So hat man im Exception-Handler automatisch das selbe ABI wie bei 
Funktionen (Übergabe in r0-r3, Rückgabe in r0, r12 beliebig nutzbar), 
aber die Funktion kann nicht geinlined werden, d.h. man hat immer einen 
Sprung.

von Peter D. (peda)


Lesenswert?

W.S. schrieb:
> Ich habe selber sowas schon erlebt, deswegen weiß ich, daß man dann das
> Sperren und Entsperren kellern muß, also einen Zähler beim Sperren
> raufzählen und beim Entsperren wieder herunterzählen und nur dann, wenn
> er Null wird, tatsächlich wieder entsperren.

Ich habe sowas noch nie erlebt.
Die Sperre erfolgt natürlich nur für den konkreten atomaren Zugriff.
Wer Interrupts nested über mehrere Funktionsebenen hinweg sperrt, der 
hat das Prinzip nicht verstanden: Die Sperre darf nur so kurz wie nötig 
sein!

Das gleiche gilt für volatile. Das verteilt man ja auch nicht mit der 
Schöpfkelle, sondern benutzt es immer nur dann, wenn nötig.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

W.S. schrieb:
> Dann laß dir dein Lehrgeld zurückzahlen. Jeder Ringpuffer ist im Grunde
> EASY. Es gibt einen Schreibzeiger und einen Lesezeiger, die atomar les-
> und schreibbar sind

Nun und genau das muß man mit Interruptsperre absichern. Zeiger sind nur 
dann von Haus aus atomar zugreifbar, wenn ihre Größe der Bitbreite der 
CPU entspricht. Beim hier oft verwendeten AVR ist das nicht der Fall.

Man kann aber beim AVR die Sperre umgehen, wenn man nicht Pointer 
sondern Indexe speichert und die FIFO <256Byte groß ist. Dann reicht ein 
uint8_t zum speichern, d.h. der Index ist atomar zugreifbar.

Ich benutze für FIFOs grundsätzlich Indexe, weil das fehlersicherer ist. 
Einen Index teste ich auf Überlauf, d.h. er kann niemals in den Wald 
zeigen, wie ein Pointer es kann und mir dann andere Daten zerstören.
Das nennt sich defensive Programmierung.

: Bearbeitet durch User
von W.S. (Gast)


Lesenswert?

Peter D. schrieb:
> Nun und genau das muß man mit Interruptsperre absichern. Zeiger sind nur
> dann von Haus aus atomar zugreifbar, wenn ihre Größe der Bitbreite der
> CPU entspricht. Beim hier oft verwendeten AVR ist das nicht der Fall.

Wie immer denkst du mal wieder grundfalsch.

Was meinst du denn, wie groß ein Ringpuffer zur sicheren Datenübergabe 
zwischen ISR und Grundprogramm sein muß?

Normalerweise reicht dafür ein Ringpuffer von 4..8 Plätzen aus, weil 
dann, wenn dieser tatsächlich überlaufen sollte, der Prozessor ohnehin 
hoffnungslos überlastet ist.

Und dafür reicht als Zeiger bzw. Index im Puffer jeweils ein Byte 
problemlos aus. Und sowas ist auch in jedem AVR atomar. Selbst auf einem 
steinalten 4 Bit Controller von NEC würde das Nibble ausgereicht haben.

Hast du das jetzt?
Warum polterst du so oft los, ohne die Sache hinreichend überdacht zu 
haben?

W.S.

von Dr. Sommer (Gast)


Lesenswert?

W.S. schrieb:
> Normalerweise reicht dafür ein Ringpuffer von 4..8 Plätzen aus, weil
> dann, wenn dieser tatsächlich überlaufen sollte, der Prozessor ohnehin
> hoffnungslos überlastet ist.

Und wie legt man da z.B. Ethernet-Pakete drin ab?

Im Ringpuffer-Beispiel sollte man die Indices über _Atomic 
implementieren. So wird man das Problem mit den Zugriffen portabel los 
(ok, AVR-GCC kann das bestimmt nicht), und kann auch dieses hässliche
1
asm volatile("": : :"memory");
weglassen indem man die korrekte memory order angibt.

von Jim M. (turboj)


Lesenswert?

Dr. Sommer schrieb:
> Deine RBPut und RBGet Funktionen können jeweils nur 1 Byte bearbeiten.
> Die für jedes Byte einzeln aufzurufen wäre ziemlich langsam. Was machst
> du in dieser Situation:

Ringpuffer füllen sind maximal 2x memcpy, deren Grenzen sich trivial 
berechnen lassen.

Und damit das 1-Byte-Memcpy die Performance beeinflussen kann, müsste 
die Interrupt Quelle oder Senke schon relativ schnell arbeiten.

Dr. Sommer schrieb:
> Und wie legt man da z.B. Ethernet-Pakete drin ab?

Gar nicht. Für sowas nimmt man Scatter-Gather DMA, denn ansonsten wären 
die notwendigen memcpy() zu teuer für einen kleinen µC.


Man erinnere sich: Die alten Realtek 8139 PCI erreichten nur ab PII/400 
die vollen 100MBit/s, weil da wegen Alignment Problemen zuätzliches 
memcpy() nötig war. Mit Intel Karten erreichte man 100MBit/s schon ab 
ca. 100MHz CPU Takt.

Dr. Sommer schrieb:
> Im Ringpuffer-Beispiel sollte man die Indices über _Atomic
> implementieren

Der Witz ist dass du den Ringpuffer mit <=256 Einträgen baust, damit 
sind die Indizes nur noch Bytes und so automagisch Atomar.

Größere Ringpuffer braucht man nur relativ selten - insbesondere da die 
8-Bit AVR mit RAM knausern. Auf 32-Bit ARM sind 32-Bit Zugriffe Atomar.

von Dr. Sommer (Gast)


Lesenswert?

Jim M. schrieb:
> Ringpuffer füllen sind maximal 2x memcpy, deren Grenzen sich trivial
> berechnen lassen.
Ich würde Zero-Copy bevorzugen.

Jim M. schrieb:
> Und damit das 1-Byte-Memcpy die Performance beeinflussen kann, müsste
> die Interrupt Quelle oder Senke schon relativ schnell arbeiten.
Soll vorkommen.

Jim M. schrieb:
> Gar nicht. Für sowas nimmt man Scatter-Gather DMA, denn ansonsten wären
> die notwendigen memcpy() zu teuer für einen kleinen µC.
Und was macht man auf Mikrocontrollern die nur DMA ohne Scatter-Gather 
haben?

Jim M. schrieb:
> Größere Ringpuffer braucht man nur relativ selten - insbesondere da die
> 8-Bit AVR mit RAM knausern. Auf 32-Bit ARM sind 32-Bit Zugriffe Atomar.
Das _Atomic ersetzt aber auch auf 32-Bittern das 
Assembler-Speicher-Optimierungs-Barrier.

von The other Peter (Gast)


Lesenswert?

W.S. schrieb:
> Peter D. schrieb:
>> Nun und genau das muß man mit Interruptsperre absichern. Zeiger sind nur
>> dann von Haus aus atomar zugreifbar, wenn ihre Größe der Bitbreite der
>> CPU entspricht. Beim hier oft verwendeten AVR ist das nicht der Fall.
>
> Wie immer denkst du mal wieder grundfalsch.
>
Meine Güte, deine Selbstherrlichkeit stinkt zum Himmel!

> Was meinst du denn, wie groß ein Ringpuffer zur sicheren Datenübergabe
> zwischen ISR und Grundprogramm sein muß?
>
> Normalerweise reicht dafür ein Ringpuffer von 4..8 Plätzen aus, weil
> dann, wenn dieser tatsächlich überlaufen sollte, der Prozessor ohnehin
> hoffnungslos überlastet ist.
>

Inkompetenter Blödsinn. Je nachdem was "das Grundprogramm" bzw. die an 
der Abarbeitung betiligten tasks so alles zu tun haben ist die 
Umlaufzeit der WEGarbeitung komplett indeterministisch. Eine 
Pauschalaussage ist hier absolut fehl am Platz. Es reicht schon eine 
serielle Kommunikation mit 76800 Baud mit kurzen bursts von (in deinem 
Beispiel 9 bytes), dann ist die Mathematik dahinter einfach, wie lange 
(kurz) die Verarbeitungstask maximal Latenz haben darf um die Daten 
wegzuschaufeln. Von komplexeren Systemen scheinst Du Dich immer fern 
gehaletn zu haben!

> Und dafür reicht als Zeiger bzw. Index im Puffer jeweils ein Byte
> problemlos aus. Und sowas ist auch in jedem AVR atomar. Selbst auf einem
> steinalten 4 Bit Controller von NEC würde das Nibble ausgereicht haben.
>
> Hast du das jetzt?
> Warum polterst du so oft los, ohne die Sache hinreichend überdacht zu
> haben?
>

Das Kompliment darf man gerne zurück geben.

> W.S.

von Jim M. (turboj)


Lesenswert?

The other Peter schrieb:
> Inkompetenter Blödsinn. Je nachdem was "das Grundprogramm" bzw. die an
> der Abarbeitung betiligten tasks so alles zu tun haben ist die
> Umlaufzeit der WEGarbeitung komplett indeterministisch.

Dann ist das Programm aber Müll, weil es abstürzen kann. Du brauchst 
eine obere Schranke für die Abarbeitung, schon alleine wenn man einen 
Watchdog korrekt benutzen möchte.
Mit der oberen Schranke kann man dann auch den Ringpuffer 
dimensionieren.

BTW: Bei 76800 Baud und 8Byte Ringpuffer muss ich nur etwas schneller 
als 1x pro ms (0.9ms) den Ringpuffer in der Hauptschleife leeren. Das 
passen bei einem hoch getaktetem STM32 recht viele Zyklen dazwischen...

von The other Peter (Gast)


Lesenswert?

Jim M. schrieb:
> The other Peter schrieb:
>> Inkompetenter Blödsinn. Je nachdem was "das Grundprogramm" bzw. die an
>> der Abarbeitung betiligten tasks so alles zu tun haben ist die
>> Umlaufzeit der WEGarbeitung komplett indeterministisch.
>
> Dann ist das Programm aber Müll, weil es abstürzen kann. Du brauchst
> eine obere Schranke für die Abarbeitung, schon alleine wenn man einen
> Watchdog korrekt benutzen möchte.
> Mit der oberen Schranke kann man dann auch den Ringpuffer
> dimensionieren.

Sorry, auch das ist Unfug. Jedes fehlerhafte Programm kann abstürzen, 
aber ein gut geschriebenes Programm i.Z. mit einem fehlertoleranten 
Protokoll über die serielle Schnittstelle hat mit Überläufen keinerlei 
Probleme, jedenfalls kann es auch nicht mehr oder weniger abstürzen als 
Andere Programme.

Und was das mit einem WatchDog zu tun haben soll, hätte ich auch gerne 
erklärt bekommen.

>
> BTW: Bei 76800 Baud und 8Byte Ringpuffer muss ich nur etwas schneller
> als 1x pro ms (0.9ms) den Ringpuffer in der Hauptschleife leeren. Das
> passen bei einem hoch getaktetem STM32 recht viele Zyklen dazwischen...

Nun nimm aber mal eine Software, die mehrere Schnittstellen parallel 
bedient und dann vielleicht noch ein Hostinterface über Ethernet bedient 
und nebenbei auch etwas mit den Daten tun soll, die da immer fleissig 
von A nach B geschoben werden. Da kommt man bald an den Punkt, wo auf 
einer Schnittstelle mal das eine oder Andere Zeichen verloren gehen kann 
- ohne dass der Prozessor "hoffnungslos überlastet" wäre (wie es der 
Herr Oberlehrer ausdrückt).

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.