NodeSP Part I (Analog)

                                   oder

ein Beitrag zum Thema ‘Wieder versuchen./Wieder scheitern./Besser scheitern’ (Samuel Becket)

Motivation

Phail hat mir - temporär - ein ESP32-Dev-Board überlassen. Auf diesem ist ein FTDI-Chip zwecks Bereitstellung eines JTAG-Interfaces verbaut. Seit Sommer 2019 hat PlatformIO seinen Debugger für die Öffentlichkeit freigegeben. Beim Herumspielen in VSC zeigt sich, daß nicht nur Source-Level Debugging möglich ist, sondern auch auf die (Dis-)Assembler-Ebene gewechselt werden kann. Da ließe sich doch vielleicht etwas mit anfangen?

Showtime

Wie wäre es mit einer Fingerübung in ESP32-Assembler? Eine FFT hätte den Charme einer späteren Nutzbarkeit für verschiedenste Projekte und wäre doch ein überschaubarer Aufwand …

Planung

Wichtig ist zunächst einmal, einen brauchbaren Algorithmus zu finden. Wenn ein Schneller gefunden ist, könnte dieser als Re-Implementierung in ESP32-Assembler vermutlich alle anderen wegblasen?!

Anforderungen:

  • Der verwendete Algorithmus sollte hinreichend allgemein einsetzbar sein, d.h. die Meßwertblöcke sollten als 2er-Potenzen formuliert werden können.

  • Außerdem sollte mit echten Fließkommazahlen gearbeitet werden (der ESP32 unterstützt 32-Bit IEEE754 mit 16 Registern native).

  • Für die Meßwerte sollen keine Vorinformationen vorliegen (d.h. beliebige Daten sollen verarbeitet werden können).

  • Neben der Fourier-Transformation soll auch die Inverse berechnet werden können.

Ausführung Testprogramm FFT

Für die Geschwindigkeitsmessung habe ich zunächst ein Testprogramm in C(99) erstellt. Dies soll zunächst verschiedene Algorithmen bzw. deren Implementierungen vergleichen, später wird hiermit auch die Effizienz der eigenen Implementierung getestet. Es soll immer der gleiche Testmeßdatensatz verwendet werden, für den die korrekten Ergebnisse vorab bekannt sind. So wird die Korrektheit der Implementierung verifiziert.

Nachdem in meinem Vergleichsfeld ein ‘schnellster’ Algorithmus ermittelt ist (es liegen mehrere 10er-Potenzen - in [µs] gemessen - zwischen den verschiedenen Implementierungen), habe ich diesen in ’native’ ESP32 umgeschrieben (für mathematisch Interessierte: http://www.katjaas.nl/home/home.html ). Zunächst gilt es, die Parameterübergabe zu klären, d.h. welche Parameter werden in welchen Registern übergeben. Dann muß man die Dokumentation zur Inline-Assembly des GCC zusammensuchen und last (but not least) muß natürlich der Assembler-Guide ‘Xtensa® Instruction Set Architecture (ISA) Reference Manual’ durchgearbeitet werden ( https://0x04.net/~mwk/doc/xtensa.pdf ).

Im praktischen Teil zeigt sich, daß die aktuelle Kombination VSC/PlatformIO/Xtensa-Debugger noch reichlich ‘buggy’ ist. Beim Single-Stepping durch den Code kommt es immer wieder zum ‘Rausfliegen’ (Rücksprung in die übergeordnete Funktion). Außerdem klappt es mit dem Disassemblieren häufig nicht so recht, d.h. Opcodes werden falsch gelesen bzw. an ‘falschen’ Adressen mit der Disassembly begonnen (erkennbar häufig an der komplett sinnfreien Bedeutung des Codes, gelegentlich auch an eingefügten .byte-Instruktionen - die hier eigentlich nichts verloren haben …).

Trotz dieser Widrigkeiten habe ich schließlich eine funktionierende Version realisiert.

Ergebnis: Der Speedup sinkt sehr schnell von 2.2 (für 8 Meßwerte) auf 1.1 (ab 256 Meßwerte & mehr). Mmmh, nicht so überzeugend (nur 10%) für den Aufwand (ich hatte mit einem Speedup von 2-5 gerechnet!). Nach einigem Kopfkratzen bin ich zu folgenden Erkenntnissen gelangt:

  • Der limitierende Faktor ist vermutlich der Speicherzugriff. Der ESP32 hat keinen Cache, d.h. alle Speicherzugriffe bremsen unmittelbar. Bei größeren Meßwertmengen schlägt das überproportional durch (Speicherzugriffe von C<->Assembler sind aufgrund der leistungsfähigen Befehle des ‘Instruction Sets’ gleich schnell, d.h. der Compiler wählt bereits die optimalen Befehle).

  • Moderne Prozessoren (ESP32 gemäß Doku eine ‘post-RISC Architecture’) haben sehr leistungsfähige Befehle & vergleichsweise viele Register (hier 16 ‘universelle’ in einem Registerfile von 64 und 16 FP-Register - sowie diverse andere hier nicht so Relevante). Früher war das mal anders … (6510 et al.)! Letztlich kann dadurch der Compiler recht gut optimieren, der Gewinn durch ‘Metawissen’ seitens des Programmierers (also was man weglassen darf u.ä.) ist marginal.

  • Die speziellen Matrix-Support Operationen (multiply/add w/ storage pointer increment etc.) passten leider nicht zu dem von mir favorisierten Algorithmus (dumm gelaufen!).

Fazit: Nett, aber (weitgehend) sinnfrei! Eigentlich ist das zwar der Stand der Erkenntnisse (in der Informatik), aber schön, das wir das noch einmal überprüft haben …

Jetzt brauchen wir noch etwas ‘zum Anfassen’ …

Ausführung ‘Spectrum Analyzer’ (SP)

Naheliegend für einen Realitätscheck ist natürlich der Aufbau eines ‘Spectrum Analyzers’, so eine Art Equalizer wie man es von Audio-Apps, Hifi-Anlagen (oder dem Autoradio) kennt - nur ohne Eingriffsmöglichkeit.

Aufbaubild

Aus alten Beständen hatte ich noch die LED-Streifen (WS2812) aus einem Sonderangebot. Hier fällt etwas Lötarbeit an (& die Heißklebepistole kommt auch zu ihrem Recht).

Bohrschablone

Für das Gehäuse habe ich eine Bohrschablone mit FreeCAD erstellt, die wir auf der neuen ‘Großfräse’ der Innovationswerkstatt dann ausgeführt haben.

Neben dem Gehäuse habe ich eine Mikrofonbaugruppe mit eingebauter Verstärkung (40..60dB), einen Level-Shifter (3,3<->5V) sowie eine kleine Platine zusätzlich beschafft. Referenzen: Adafruit MAX9814-debo-amp-mic2, DEBO LEV SHIFTER

Da ich alle meine Projekte bis dato auf NodeMCUs (& esp-idf Framework) realisiert habe, muß jetzt ein passendes Modul (‘sampling’) in ‘C’ erstellt werden, die Scriptanbindung Richtung Lua gibt es gratis dazu (bei sachgerechter Anwendung … ;-).

Im Ergebnis kann man mit 44.6 kHz Daten sampeln (separate FreeRTOS Task, runtergebremst damit der belegte Core nicht komplett monopolisiert wird …). Als Blockgröße habe ich 2048 Meßwerte gewählt. Das Mikrofon leistet 50 Hz bis 20kHz lt. Datenblatt, d.h. ein Teil der Frequenzen kann unten & oben verworfen werden.

Damit das ganze hinreichend zügig abläuft, habe ich eine Verarbeitungspipeline aufgebaut: Datenerfassung(Blockgröße) -> FFT -> Binning(10 St.). Das Sampeln wird in Lua aktiviert, die Ergebnisse (aus zwei wechselseitig genutzten Ergebnispuffern à 10 ‘Bins’) werden per Timertask in Lua ausgelesen und an die LED-Kette ausgegeben.

Für die Verifizierung der Funktion habe ich zunächst mit folgender Webseite Testtöne mit definierter Frequenz erzeugt: http://onlinetonegenerator.com/432Hz.html . Leider zeigt sich, das die generierten Töne niedriger und vor allem der hohen Frequenzen nicht ’laut’ genug sind.

Es trifft sich glücklich, das wir im Space von hacKNology einen Funktionsgenerator zur Verfügung haben. Wenn wir den direkt auf den ADC schalten (ohne Mikro), können wir mit der Einstellung 2,2Vpp Testsignale für den ganzen Bereich sauber generieren (überflüssig, zu sagen, das wir den Funktionsgenerator seinerseits vorab mittels Oszi überprüft haben … ;).

Ein paar Meßergebnisse: Die Aufnahme der Meßwerte benötigt ca. 45ms, das Umrechnen per FFT ca. 0,8 ms! (Umso sinnfreier die Umsetzung in ESP32-Assembler …).

Da ich gerade keine Schalter zur Hand hatte, habe ich zum Feintuning eine Flutter-App per Multicast angebunden - die ich auf meinen ‘NodeSwarm Devices’ standardmäßig vorhalte.

NodeSP Android App

Die App sendet Konfigurationsdaten an die NodeMCU per Multicast, am wichtigsten natürlich für das Einstellen der Empfindlichkeit. Wenn die Umgebung sehr laut ist, sollte man runterregeln können … Die restlichen Parameter dienen der Konfiguration der LED-Ausgabe.

App picture

Showtime!

Hier mal ein (kurzes) Beispiel mit Musik:

Schwächen dieser Implementierung

Das Sampling ist nicht ‘sustained rate’ sondern intermittierend (wenn auch kaum spürbar). Es gibt lt. ‘ESP32 - Technical Reference Manual’ eine Betriebsart (LCD-Mode -> ADC/DAC-Mode) die hier Abhilfe schaffen könnte? Mal schaun … (ein Tipp von Wolfgang).

Eine verbesserte Version werde ich zu einem späteren Zeitpunkt mittels I2S-Mikrofon (ein Tip von Markus) nachreichen (der ESP32 unterstützt den I2S-Bus mit DMA-Transfer - da sollte was gehen … sagt auch das Internet :-).