Was soll es da für Schwierigkeiten geben? Das ist das Gleiche wie mehrere Threads auf dem PC zu verwenden. Man muss sich halt im Klaren sein, das gemeinsam benutzte Ressourcen entsprechend gegen konkurrierende Zugriffe abgesichert werden. Und ja ich benutze den zweiten Kern, regelmäßig.
uint16_t val = analogRead(A0);
float frequency = val;
DelayTime_us = 1 / frequency * 1e6;
Was macht die IEEE754 Implementation wenn val == 0 ist?
Und warum uint -> float -> uint?
Wichtiger aber, der Zugriff auf DelayTime_us von beiden Kernen aus ist — da 32Bit noch atomar gehandhabt werden – zwar möglich. Wenn du aber später ernsthafte Anwendungen schreiben möchtest, dann brauchst du einen Semaphore Mechanismus. Davon bringt der Pico praktischerweise gleich mehrere Hände voll in Hardware mit sich. Ebenso Pipelines für Interprozess-Kommunikation.
Was soll es da für Schwierigkeiten geben? Das ist das Gleiche wie
mehrere Threads auf dem PC zu verwenden
Wie implementierst du _Atomic auf dem PiPico damit man wie auf dem PC mit Threads arbeiten kann?
Aber grundsätzlich ist es sogar einfacher als auf dem PC, weil der Cortex-M0 ja keinen Cache und keine keine OoO-Execution hat und man keine Cache Maintenance Operations braucht.
Wie implementierst du _Atomic auf dem PiPico damit man wie auf dem PC
mit Threads arbeiten kann?
Das hab ich auch auf dem PC noch nie genutzt. Threads gabs schon vor 2011. Zugriffe auf "int" sind übrigens immer atomar, auch über Core Grenzen hinweg... Das reicht vollkommen für Synchronisationsprimitive.
Ja, und meines Wissens basierte die Implementation von Threads & Mutexen auf Kernel-Ebene auf x86 SMP schon immer auf Atomics. Da wurde dann natürlich nicht das viel später definierte C-Sprachkonstrukt _Atomic genutzt sondern (Inline) Assembler für die Instruktionen XADD und CMPXCHG, aber das Ergebnis ist das Selbe.
Atomics direkt im Code selber zu nutzen hat in diversen Situationen große Performance-Vorteile, als immer gleich einen Mutex zu nutzen und/oder Interrupts zu sperren.
Zugriffe auf "int" sind übrigens immer atomar, auch über Core
Grenzen hinweg...
Wo ist das definiert? Meines Wissens nach garantiert C da überhaupt nichts. Bei allen (?) ARM-Compilern ist int aber 32bit, und einzelne (!!) 32bit-Zugriffe sind bei ARM atomisch, wenn sie korrekt aligned sind. Damit kann man aber z.B. noch keinen Lock-Counter atomisch inkrementieren, denn dafür braucht man eben atomische read-modify-Write Mechanismen.
x86 macht das mit XADD und dem "LOCK" Prefix etc., und ARMv7 und drüber macht dies über die STREX/LDREX Instruktionen. Der Cortex-M0 ist aber ARMv6M und hat diese nicht, und auch kein Äquivalent.
Daher die Frage: Wie geht das beim PiPico mit Cortex-M0 (beim -M33 ist's klar, der hat STREX etc)?
Zugriffe auf "int" sind übrigens immer atomar, auch über Core
Grenzen hinweg
Für den rp2350 stimme ich im Großen und Ganzen zu.
Aber beim rp2040 wird explizit geraten die Spinlocks zu benutzen.
»…which can** be used to manage mutually-exclusive access to shared software
resources…«
und
»…If both cores try to claim the same lock on the same clock cycle, core 0 succeeds…«
** Wenn man sich die Sektion durchliest wird klar, dass das deutlich mehr als nur ein Vorschlag ist.
Okay, bisschen ungünstig dass die dann PiPico-spezifisch und nicht portabel sind. Kann man Mithilfe dieser Register beliebig viele Spinlocks implementieren, oder begrenzt die Hardware das? Bei ARMv7 und x86 kann ja jedes einzelne Byte im Speicher ein eigener Atomic sein.
AVR-gcc, Interrupt?
Norbert:
So ein Pico ist da Busbreitentechnisch schon deutlich weiter als ein
armer AVR.
Upps, ich vergesse immer ein paar Sachen zu erwähnen: Normalerweise mache ich meine Programme so, dass sie auch auf dem kleinen AVR laufen. Im Falle der zwei Kerne des Picos ist das natürlich schwierig. Ich versuche das aber quasi zu "emulieren" indem der zweite Kern durch eine schnelle Interruptsteuerung emuliert wird. Schnelle Sounderzeugung im Interrupt, langsame Tonsteuerung sonst.
Wenn Du einen Lock verwendet, dann bekommst Du immer Verzögerungen. Dass liegt in der Sache der Natur, ganz unabhängig davon wie das implementiert ist
Mein Ansatz ist es, die Software möglichst so zu struktieren, dass ich gar keine Locks oder dergleichen brauche. D.h die Ressourcen werden auf die Threads/Cores aufgeteilt. Dazwischen nutze ich gerne FIFOs, oder einfach nur zwei Zähler für Ping/Pong.
Wer Locks braucht, aber kein Atomic hat kommt z.B. hiermit weiter:
Kann man Mithilfe dieser Register beliebig viele
Spinlocks implementieren, oder begrenzt die Hardware das?
Aber da wir über einen Nano- bis Mikrosekunden-Bereich der Benutzung reden.
TEST: Zwei Kerne, jeder durchläuft 100.000 Schleifen und führt fünf mal ein Inkrement einer globalen Variable aus. Der Zähler sollte zum Schluss also bei 1.000.000 stehen.
Mit vorhersehbarem Misserfolg ohne die Nutzung von Spinlocks:
elapsed time: 175.0 ms (1.7 µs/loop)
Counter: 599997 <<< stark variabel
Unter Zuhilfenahme eines Spinlocks:
elapsed time: 258.7 ms (2.6 µs/loop)
Counter: 1000000
Ach ja, falls es interessiert, µPy. Mit C oder Thumb-Assembler geht's dann noch etwas flinker.
Bei Fifos inkrementiert der Schreiber den Schreibzähler und der
Leser den Lesezähler.
Nutzt du dann die Methode, immer min. 1 Platz freizulassen, um die Uneindeutigkeit im Fall Lesezähler=Schreinzähler zu vermeiden? Wenn nicht, und du einen separaten Zähler/Flag zur Unterscheidung dieser Fälle nutzt, wie synchronisierst du den Zugriff darauf? Du nutzt dann aber schon DMB zwischen den Zugriffen (bei _Atomic ja impliziert)?
Das funktioniert dann nur wenn man nur genau 1 Consumer und 1 Producer hat, was ja bei Dual-Core genau hinhaut.
Ja. Aber da die beiden Kerne ja nur sehr triviale Aufgaben erfüllen (ein wenig Inkrementieren) verbringen sie per Definition einen substanziellen Teil mit gegenseitigem warten. Das wäre bei jedweder Synchronisation, egal über welchen Mechanismus) gleich.
Und wenn man in einem realen Programm mehrere hunderttausend Mal pro Sekunde aus mehreren Ecken auf die selbe Resource lesend und schreibend hämmert, dann wäre es ein guter Zeitpunkt das Konzept zu überdenken.
Im Normalfall ist eine Resource ja nahezu immer verfügbar und Spinlocks dienen halt der Absicherung.
Der reine Acquire/Release Zyklus braucht übrigens nur 130ns. µPy!
Nutzt du dann die Methode, immer min. 1 Platz freizulassen, um die
Uneindeutigkeit im Fall Lesezähler=Schreinzähler zu vermeiden?
Nein, ich lasse die Zähler einfach weiterlaufen. Füllstand ist die Differenz. Bei 2er Potenzen in der FIFO Tiefe braucht man dann nur noch Modulo machen um den Zugriffsindex zu bekommen. Andere Tiefen lassen sich aber auch abbilden, man lässt den Zähler bis 2xTiefe-1 laufen und bricht dann auf 0 um. Braucht dann ein paar IF's für die Füllstände und Zugriffsindizes, ist aber immer noch schneller als Modulodivision durch nicht 2er Potenzen.
Da es mir um Geschwindigkeit geht, hier mal ein Test, bei dem ein Pin auf Core1 getoggled wird.
Erwartung: Frequenz im Megahertzbereich, absolut stabiles Signal weil der ganze Kern nur für das Toggling verwendet wird.
Beobachtung: ~2MHz, aber leichter Jitter auf Rechtecksignal
Der Jitter erscheint mir für die 150MHz des Pico2 zu groß.
Ein Jitter wird bei ARM Controllern immer auftreten da jede
I/O-Operation mit den Ports gepuffert wird und damit jede
Aktion erst mit dem I/O Clock auf den Core-Clock
synchronisiert werden muss. Absolute Core-Clock-synchrone
Ausgaben gibt es nicht, sondern nur Jitter-behaftet.
Das Arduino Framework funkt auch noch mit Interrupt dazwischen.
Mach mal im Vergleich:
Danke, eine sehr gute Idee.
Das Signal scheint so schnell, dass der Ausgangspin nicht mehr mitkommt.
Die Störungen im mitlaufenden UKW-Radio sind je nach Initialisierung völlig unterschiedlich sowie mit- und ohne while-Loop Kapselung.
Wenn ich mit pinMode statt mit gpio_init / set initialisiere, kann ich den UKW-Sender im Radio vollständig blockieren.
Lesepointer 7 und Schreibpointer 6 ist kein gültiger Zustand bei meinem Algo. Ich schrieb ja, das ich die "Pointer" - das ist der Falsche Begriff, es sind bei mir Zähler, bei 2xTiefe-1 Umbreche. D.h in für den Fall hier dass neun Elemente in der FIFO sind wäre der Lesezähler 7 und der Schreibzähler 16. Die jeweiligen Pointer für den Zugriff erhält man durch "x >= 10 ? (x-10) : x". Bei 2er Potenzen wirds noch einfacher, da reicht dann z.B. "x%16" was einfach nur ein "and" ist und man braucht nix umbrechen sondern inkrementiert die Zähler einfach nur.
Ich finde die weiter oben dargestellten Resultate ein wenig befremdlich. Und das ist noch stark euphemistisch formuliert.
Hier für die Interessierten mal eine Darstellung der real erzielbaren Werte.
Entschuldigt die Textdarstellung, für solchen Kleinkram reicht das jedoch allemal aus.
Auf Core 0 wird gemessen, auf Core 1 werden vier verschiedene Routinen gestartet, zweimal Python, zweimal Assembler. Jeweils für die schnellste symmetrische und die schnellste asymmetrische Wellenform.
Selbstverständlich jeweils von einem Hardware Reset eingeleitet.
CPU ist auf 100 MHz gebremst um die Skalierung angenehmer zu gestalten.
Gemessen wird an Pin 16, der war gerade so schön nahe.
Für die beiden Python Schleifen und die beiden Assembler Schleifen kommen wir
mit ein wenig zählen auf 6, 3, 4, 3 Taktzyklen pro Wellenform.
und mit ein wenig rechnen auf 16.667, 33.333, 25.000, 33.333 Mehahertz.
Insofern finde ich die 2 MHz mit Arduino gar nicht mal so schlecht.
Man sollte es vielleicht nur vermeiden, wenn man etwas präziser arbeiten möchte. ;-)
Und nein, bei Messung zwei und vier ist kein Fehler unterlaufen.
Python und Assembler sind dort vom Ergebnis tatsächlich identisch.
Hmmm, dachte ich hätte es genau genug dargelegt.
Im Grunde genommen lasse ich auf einem Pi Pico bzw. auch einem Pi Pico2 vier mal verschiedene Endlosschleifen laufen (auf Core 1) und messe das sich per Pin toggling ergebende Signal am Pin 16 mittels eines weiteren Programmes auf Core 0.
Es laufen:
Eine Schleife in Python welche ein symmetrisches (50:50) Ausgangssignal erzeugt. Damit erreicht man 16.667 MHz.
Eine Schleife in Python welche ein asymmetrisches (33:67) Ausgangssignal erzeugt, dafür jedoch nochmals schneller ist. Damit erreicht man 33.333 MHz.
Eine Schleife in Thumb-Assembler (innerhalb Python) welche ein symmetrisches (50:50) Ausgangssignal erzeugt. Damit erreicht man 25.000 MHz.
Eine Schleife in Thumb-Assembler (innerhalb Python) welche das schnellstmögliche, asymmetrisches (33:67) Ausgangssignal erzeugt. Damit erreicht man ebenfalls 33.333 MHz.
Zum Programm: Nein. Bei der hier im Forum nicht nur latent vorhandenen, sondern offen zur Schau gestellten Aggressivität und Feindseligkeit gegenüber Python (und auch allem anderen was nicht mit ›C‹ anfängt und am besten mit dem selben ›C‹ oder ›+‹ gleich wieder aufhört), habe ich für mich entschieden — wenn es erforderlich erscheint — bestenfalls nur noch kleinere Snippets zu zeigen.
Letzten Endes ging es darum zu zeigen, dass die vormals genannten 2 MHz mit Arduino noch nicht einmal annähernd in den Bereich des Möglichen reichen. Und ganz zweifellos können die Port Pins diese hohe Geschwindigkeit problemlos mitgehen.
Das ganze hat aber selbstverständlich keinerlei Bezug zu Bedürfnissen in der realen Welt. Da nutzt man für solche Dinge natürlich die eingebaute Hardware und erreicht damit 50 MHz.
Alle Angaben bezogen auf eine CPU welche mit 100 MHz getaktet ist.