Nochmals weitere Optimierung der Datenübertragung per Laser zwischen Arduino und STM32

Der erste Versuchsaufbau zur Übertragung von Daten via Laserstrahl zwischen zwei Arduinos war ja mehr eine Machbarkeitsstudie, das mit dem Ergebnis endete, dass eine Datenübertragung mit Signallängen mit 10 bzw. 20 ms pro Bit sicher möglich ist. Dabei kam ein ungeschirmter Fotowiderstand zum Einsatz.

Mit der Optimierung durch Einsatz von Fotodioden wurde die Übertragungsgeschwindigkeit auf 500/1000 µs gesteigert.

Danach ging es an die Optimierung der Empfänger-Software: es wurden Interrupts eingesetzt und das Programm wurde auf Performance getrimmt. Dadurch waren Übertragungsgeschwindigkeiten mit einem Arduino Uno von 64/128 µs möglich, bei dem der Text zwar ein paar Fehler, aber noch lesbar war.

Heute wollen wir versuchen, noch ein wenig mehr Geschwindigkeit herauszukitzeln, indem wir ein STM32-Board einsetzen, dass eine höhere Performance (z. B. 72 statt 20 MHz Takt) aufweist. Dabei verlassen wir nicht unsere gewohnte Arduino-Entwicklungsumgebung und übernehmen auch den Code soweit es geht, um so auch einen Vergleich zwischen der Performance eines ATmega 328P und eines STM32F103 - Mirkocontrollers zu ziehen.


Versuchsaufbau


Der Versuchsaufbau vom Sende-Arduino wurde bereits im letzten Projekt beschrieben. Er bleibt unverändert. Auch die Software bleibt hier gleich.

Noch einmal zur Erinnerung die Warnung: der Laser hat eine Licht-Leistung von 5 Milliwatt und ist kein Spielzeug. Es darf niemals direkt in den Laserstrahl geschaut werden. Das kann zu ernsten Augenschädigungen führen. Wer sicher gehen will, benutzt eine Laserschutzbrille (auf den richtigen Wellenbereich, z. B. 650nm achten). Für den Empfänger kommt wieder die bewährte Fotodiode Osram SFH 203 zum Einsatz.

Statt eines Arduinos mit Multi Function Shield kommt jetzt allerdings ein potenteres Board mit STM32, auch "Blue Pill" genannt, zum Einsatz. Es hat mehr Speicher, einen höheren Takt und ist ein 32 Bit Mikroprozessor, sollte also schneller sein und auch kürzere Signale erkennen.

Da die Blue Pill eher das Format einen Arduino Nano hat, können wir das Uno-sized Multi Function Shield nicht benutzen und setzen kurzerhand drei Taster auf das 830er-Breadboard, die bei Druck die GPIO Pins PB9, PB8 und PB7 (die im Setup per Pullup standardmäßig auf High liegen) auf Low ziehen. Die Fotodiode schließen wir an PB6 an.


Erfreulicherweise sind fast alle STM32-Ports interruptfähig, so dass ich zuerst auch die Tastendrücke per Interrupt abfragte. Davon bin ich allerdings wieder weggekommen, da dies "zu gut" und zu schnell funktioniert und ein Prellen der Taster mit ausgeführt wurde, was zu Mehrfachausführungen der Funktionen führte. Darum frage ich die Taster jetzt im Loop-Teil traditionell mit 10ms Pause ab.

Die Interrupt-Funktionen für das Zusammensetzen der über die Fotodiode empfangenen Bits funktioniert genauso wie vorher. Der Code konnt eins zu eins übernommen werden.

Für die Taster habe ich folgende Funktionen programmiert: Den Code habe ich mit der Option "Fastest (-O3)" kompiliert und hochgeladen, denn wir wollen es ja schließlich schnell und nicht platzsparend. Außerdem ist genügend Flash-Speicher vorhanden.

Nun ging es ans Testen. Würde der STM32 schneller sein als der Arduino? Und wenn ja, wie kurz könnten die Pulslängen sein, bevor nichts mehr zu entziffern ist? Dieses Experiment habe ich in folgendem Video festgehalten. In einer Ecke ist dabei der Serial Monitor der STM32-Empfängers eingeblendet:



Ergebnis: 16/32 statt 64/128 ist nochmals ein Geschwindigkeitsgewinn um den Faktor vier. Das, was ich anfangs auch optimistisch geschätzt hatte.

Damit hat die Blue Pill für mich bewiesen, dass sie es drauf hat. Sie hat für mich einen tollen Funktionsumfang und ein super Preis-/Leistungsverhältnis. Auch wenn es nicht immer sinnvoll ist (Beispiel Taster) alles über Interrupts zu steuern, so kann es definitiv nicht schaden, mehr als 2 (siehe Arduino Uno) zu haben. Auch 128 KiB statt 32 KiB Programmspeicher und 20 KiB statt 2 KiB Variablenspeicher sind tolle Features.

Allein das Hochladen der Programme ist nicht so easy wie beim Arduino, aber das wird evtl. mit dem ST-Link, der bereits bestellt ist, noch einfacher. Doch davon ein ander mal.

Software

Das Datenübertragungsprotokoll bleibt wie zuvor und ist so simpel wie möglich: 1. Bit: 0 = kurz = 1 Einheit 1 = lang = 2 Einheiten Pause zwischen Bits der Länge kurz (1 Einh.) ... Wiederholung für 2. bis 8. Bit Byte-Ende-Markierung der Länge 3 * lang (=6 Einheiten)

Source-Code

Sender (Arduino) siehe hier.

Empfänger (Blue Pill): //////////////////////////////////////////////////////// // (C) 2019 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// #define PinFoto PB6 // wo ist der Foto-Widerstand (DIGITAL!) angeschlossen ? #define PinTaster1 PB9 #define PinTaster2 PB8 #define PinTaster3 PB7 //Globale Variablen unsigned long KURZ = 1024; // Standard-Zeiten unsigned long LANG = 2048; unsigned long laserStart=0; unsigned long laserStop=0; unsigned long laserDur=0; unsigned long laserPause=0; byte bitpos=0; byte bit[8]; int stelle=0; void setup() { pinMode (PinFoto, INPUT); pinMode (PinTaster1, INPUT_PULLUP); pinMode (PinTaster2, INPUT_PULLUP); pinMode (PinTaster3, INPUT_PULLUP); Serial.begin (115200); //Auf dem STM32 ist fast jeder Pin Interrupt-fähig attachInterrupt (digitalPinToInterrupt(PinFoto), irq_laser, CHANGE); } void loop() { // hier könnte jetzt der Buzzer mit dem Laserempfang beepen // ich habe aber gerade keinen Buzzer mehr zur Hand // Taster abfragen byte t1=digitalRead(PinTaster1); byte t2=digitalRead(PinTaster2); byte t3=digitalRead(PinTaster3); if (t1==LOW) { delay (10); while (digitalRead(PinTaster1)==LOW) delay (10); taster1(); } if (t2==LOW) { delay (10); while (digitalRead(PinTaster2)==LOW) delay (10); taster2(); } if (t3==LOW) { delay (10); while (digitalRead(PinTaster3)==LOW) delay (10); taster3(); } delay (10); } void taster1() { Serial.println("Taster 1 gedrückt!"); // Datenrate halbieren KURZ = KURZ /2; LANG = LANG /2; showRate(); } void taster2() { Serial.println("Taster 2 gedrückt!"); showRate(); // war: Beeper ein / aus. Da aber z. Zt. keiner verbaut ist, andere Funktion } void taster3() { Serial.println("Taster 3 gedrückt!"); // zurücksetzen auf 1024 / 2048 KURZ = 1024; LANG = 2048; showRate(); } void showRate() { Serial.print("Datenrate: "); Serial.print(KURZ); Serial.print("/"); Serial.print(LANG); Serial.println("\n"); } void irq_laser() { // Flanke an Pin hat sich geändert if (digitalRead(PinFoto)) { irq_laser_rising(); } else { irq_laser_falling(); } } void irq_laser_rising () { // Laser ist gerade auf HIGH gegangen, Zeit starten // global laserStart; global laserStop; global KURZ; global LANG; // Serial.print ("/"); laserStart=micros (); laserPause = laserStart - laserStop; if (laserPause > LANG*2) gotByte(); } void irq_laser_falling () { // Laser ist gerade auf LOW gegangen, Zeit stoppen // global laserStart; global laserStop; global KURZ; global LANG; // Serial.print ("\\"); laserStop=micros (); laserDur=laserStop - laserStart; // if (laserDur < 0) { +++ TODO +++ } Overflow der micros-Timer-Variablen hat stattgefunden if (laserDur > KURZ * 0.5 && laserDur < KURZ * 1.5) { gotBit(0); } else if (laserDur > LANG * 0.5 && laserDur < LANG * 1.5) { gotBit(1); } else { gotBit(99); } } void gotBit(byte bitt) { // global bit; global bitpos; //Serial.print(bitt); if (bitt > 1) { // weder 0 noch 1 erkannt, byte neu init. bitpos=0; } else { bit[bitpos]=bitt; bitpos++; } } void gotByte() { // global stelle; bit; bitpos; char c=0; for (int i=0; i < 8; i++) { c = c << 1; if (bit[i] >0) c = c | 1; } Serial.print(c); bitpos=0; stelle++; if (stelle > 100) { Serial.println(); stelle=0; } } Der Sketch verwendet 19572 Bytes (14%) des Programmspeicherplatzes. Das Maximum sind 131072 Bytes. Globale Variablen verwenden 2504 Bytes (12%) des dynamischen Speichers, 17976 Bytes für lokale Variablen verbleiben. Das Maximum sind 20480 Bytes.