Weitere Optimierung der Datenübertragung per Laser zwischen zwei Arduinos

Der letzte 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.

Heute wollen wir versuchen, noch ein wenig mehr Geschwindigkeit herauszukitzeln, werden dafür u. a. auch Fotodioden einsetzen und in den Mikrosekundenbereich vorstoßen.


Versuchsaufbau


Um unseren bereits etwas optimierte Datenübertragung per Laser noch weiter zu optimieren, untersuchen wir, an welchen Schrauben wir noch drehen können, um noch mehr Geschwindigkeit herauszukitzeln.

Der Versuchsaufbau wurde bereits im ursprünglichen Projekt beschrieben. Unter obigem Link kann er nachgelesen und nachgebaut werden. Es wird dann jeweils nur das lichtsensitive Bauteil ausgetauscht.

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). Als beste Fotodiode hat sich bei der letzten Tests die Osram SFH 203 herausgestellt, die wir auch heute wieder benutzen wollen.

Das höchste Optimierungspotential bietet zur Zeit die Software: Zuerst will ich untersuchen, wie genau die Pulslängen des Laserstrahls sind, die der Sender aussendet.

Dazu schließe ich die Fotodiode einfach an mein günstiges DSO-138 Oszilloskop (Kurzvorstellung siehe hier) an und überprüfe, ob die Pulslängen auch den programmierten Länge entspricht.

Hierzu habe ich ein kleines Video aufgenommen. Leider spiegelt das Acrylglas des DSO-138 sehr und die Kamera hat Schwierigkeiten, richtig zu fokussieren. Ich bitte das zu entschuldigen.



Ergebnis: An der Sendesoftware brauchen wir also nichts zu ändern. Eine Kleinigkeit möchte ich dennoch ändern, und zwar, dass die Software mit den Pulslängen 1024 / 2048 µs starten. Das lässt sich besser halbieren.

Auf der Seite des Empfängers werde ich auch bei 1024/2048 µs starten und ebenfalls einen Knopf belegen, der die Pulslängen halbiert. So kann ich mir das ständige Übertragen einer neuen Datenrate sparen.

Die Taster haben darum die folgenden Funktionen: Taster Funktion Sender Funktion Emfänger 1 Pulslänge halbieren Pulslänge halbieren 2 Übertragung pausieren/weiter Ton an/aus 3 Übertragung Datenrate mit 20/40ms --- Die Software des Empfängers wird ordentlich ausgemistet. Alles was Zeit kostet, fliegt raus. Das Flackern des LEDs und das unlesbar schnelle Ändern der Siebensegmentanzeige hat bei diesen Geschwindigkeiten eh keinen Informationswert mehr.

Und wichtiger noch: Die Zeitmess-Routinen werden wir in Interrupt-Routinen packen. So werden sie genauer und unabhängig vom Hauptprogramm. Es wird sich herausstellen, dass dann sogar auch bei hohen Datenraten mitgehört werden kann, ohne dass dies einen negativen Einfluss auf die Zeichenerkennung hat.


Interrupts funktionen aber nur an Digitaleingängen. Die bisherige Methode mit dem Vergleich des Auslesewertes am Analogeingang können wir damit vergessen. Und wir müssen dafür sorgen, dass die Voltzahl hoch genug angehoben wird, damit diese an einem Digitaleingang zu einem HIGH führt. Dazu müssen wir der Fotodiode einen zusätzlichen Widerstand spendieren. Je höher dieses ist, je empfindlicher ist die Schaltung und je weniger Licht muss einfallen, damit der Eingang auf HIGH gezogen wird.

Ich habe mich dazu an dem Schaltplan rechts orientiert und einen 1 Megaohm-Widerstand verwendet. Auf den Kondensator habe ich verzichtet, denn ich will ja ein möglichst unverfälschtes Signal mit vielen Hügeln und Tälern.


Vielleicht zuerst einmal ein Video, wie sich die neue Empfangs-Software verhält, damit man sich eine Vorstellung machen kann.

Zum Vergleich: Mit der alten Software war eine sichere Übertragung bis zu 500 µs / lang 1000 µs möglich.



Ergebnis (jeweils Kurz-Werte, Lang doppelt so lang): 64 statt 500 µs ist ein satter Geschwindigkeitsgewinn um den Faktor acht. Möglich wurde das gemacht durch den Einsatz von Interrupt-Programmierung. Beim Arduino kann hierzu nur der Digital-Pin 2 oder 3 benutzt werden. Der Pin 3 ist durch den Buzzer auf dem Multi Function Shield bereits belegt, so dass nur noch der Pin 2 übrig bleibt, der glücklicherweise auch gleich herausgeführt ist in der mitteleren Steckerleiste.

Hier passen leider nicht die normalen, "dicken" Header-Pins, sondern nur dünnere Drähte wie etwa von LEDs herein. Entweder man verdrahtet mit Jumper-Kabeln oder man benutzt wie ich einen halben IC-Sockel (hier gehen nur die billigen, nicht die teuren, runden; die haben wir ja schon vorliegend) als Zwischenstück.

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) Durch die Zeile attachInterrupt (digitalPinToInterrupt(PinFoto), int_change, CHANGE); teilen wir dem Arduino mit, dass die Funktion int_change jedesmal aufgerufen wird, wenn sich am Pin 2 etwas ändert, er also von LOW auf HIGH oder umgekehrt von HIGH auf LOW springt.

In int_change wird dann geschaut, ob es bei dem Ereignis um eine fallende oder um eine steigende Flanke handelte und entsprechend wird int_falling bzw. int_rising aufgerufen.
In int_rising startet die Flanke gerade. Wir speichern die Zeit in laserStart.

In int_falling ist die Flanke beendet. Wir messen nun die Zeit, die seit laserStart vergangen ist und haben unsere Pulslänge. Ist diese innerhalb eines gewissenen Rahmens für die definierten Pulslängen, dann wird das entsprechende Bit (0 oder 1) erkannt und die Funktion gotBit aufgerufen, die das Bit einsammelt.

Erkennt int_rising, dass die letzte Pause nicht wie gewöhnlich zwischen den Bits 1*kurz war, sondern mehr als 2*lang, dann war das eine Pause zwischen zwei Bytes und die Funktion gotByte wird aufgerufen, die aus den Bits ein Zeichen zusammenbaut, das dann auf dem seriellen Monitor ausgegeben wird.

Bei der Interrupt-Programmierung muss man darauf achten, dass die aufgerufenen Funktionen möglichst schnell abgearbeitet sind. Sonst kann es passieren, dass ein neuer Interrupt stattfinden, obwohl der alten noch bearbeitet wird. Das kann dann natürlich nur im Chaos enden.

Source-Code

Sender:
sender.cpp (klicken, um diesen Abschnitt aufzuklappen)
//////////////////////////////////////////////////////// // (C) 2019 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// #define PinLaser 2 // wo ist der Laser KY-008 angeschlossen? #define BUTTON_1 11 #define BUTTON_2 10 #define BUTTON_3 9 //20ms/40ms ist Safe-Mode zur Übertragung der Übertragungsrate #define KURZ_SAFE 20000 // wieviele microseks lang ist ein kurzer Impuls (für 0) ? #define LANG_SAFE 40000 // wieviele microseks ist ein langer Impuls (für 1) ? void setup() { pinMode(PinLaser, OUTPUT); pinMode(BUTTON_1, INPUT_PULLUP); pinMode(BUTTON_2, INPUT_PULLUP); pinMode(BUTTON_3, INPUT_PULLUP); Serial.begin (115200); } void sendChar(char c, unsigned long kurz, unsigned long lang) { unsigned long dur; Serial.print(c); for (byte i=0; i<8;i++) { // 8 bits byte b = c & 128; // oberstes Bit if (b>0) { dur = lang; } else { dur = kurz; } digitalWrite(PinLaser, HIGH); if (dur <= 16383) { // delayMicroseconds hat nur unsigned int als Parameter delayMicroseconds (dur); } else { delay (dur/1000); } digitalWrite(PinLaser, LOW); if (kurz <= 16383) { delayMicroseconds (kurz); // Pause zwischen den Bits } else { delay (kurz/1000); } c = c << 1; // nächstes Bit } if (lang*3 <= 16383) { delayMicroseconds (lang*3); // Pause nach einem Byte } else { delay (lang*3/1000); } } void loop() { String text = "Dies ist ein Uebertragungstext der genau einhundert stellen hat fuer den test 0123456789 0123456789 "; unsigned long kurz = 1024; unsigned long lang = 2048; char msg[20]; boolean pause=false; while (1) { for (int i=0; i < text.length(); i++) { if (pause) { delay (10); i--; // text anhalten } else { sendChar (text[i], kurz, lang); } if (digitalRead(BUTTON_1) == LOW) { // Übertragungsrate verdoppeln while (digitalRead(BUTTON_1) == LOW) delay(10); //auf loslassen warten //pause=true; kurz=kurz/2; lang=lang/2; //dran denken: muss noch übertragen werden (Button 3) } if (digitalRead(BUTTON_2) == LOW) { // Übertragung pausieren while (digitalRead(BUTTON_2) == LOW) delay(10); //auf loslassen warten pause = !pause; } if (digitalRead(BUTTON_3) == LOW) { // auf SAFE Übertragungsrate die neue Übertragungsrate übertragen while (digitalRead(BUTTON_3) == LOW) delay(10); //auf loslassen warten sprintf (msg, "S=%ld L=%ld!\0", kurz, lang); //S=20000 L=40000! Serial.print ("\n\n*** Übertragung der neuen Übertragungsrate: "); Serial.print (msg); Serial.println(); for (int i=0; i < 20; i++) { if (msg[i] == 0) break; sendChar (msg[i], KURZ_SAFE, LANG_SAFE); } Serial.println(); } } Serial.println(); delay (500); } }

Empfänger:
empfaenger.ino (klicken, um diesen Abschnitt aufzuklappen)
//////////////////////////////////////////////////////// // (C) 2019 by Oliver Kuhlemann // // Bei Verwendung freue ich mich über Namensnennung, // // Quellenangabe und Verlinkung // // Quelle: http://cool-web.de/arduino/ // //////////////////////////////////////////////////////// #include <TimerOne.h> // für Timings und Interrupts #include <MultiFuncShield.h> // API für das Multi Function Shield #define PinFoto 2 // wo ist der Foto-Widerstand (DIGITAL!) angeschlossen ? //Interrupt geht auf dem UNO nur auf Pin 2 und 3 //20ms/40ms ist Safe-Mode zur Übertragung der Übertragungsrate #define KURZ_SAFE 20000 // wie lang ist ein kurzer Impuls (für 0) ? #define LANG_SAFE 40000 // wie lang ist ein langer Impuls (für 1) ? //Globale Variablen unsigned long KURZ = 1024; //KURZ_SAFE; unsigned long LANG = 2048; //LANG_SAFE; 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() { Timer1.initialize(); MFS.initialize(&Timer1); // initialize multi-function shield library pinMode (PinFoto, INPUT); Serial.begin (115200); //Interrupt geht auf dem UNO nur auf Pin 2 und 3 attachInterrupt (digitalPinToInterrupt(PinFoto), int_change, CHANGE); } void int_change() { // Pin hat sich geändert if (digitalRead(PinFoto)) { int_rising(); } else { int_falling(); } } void int_rising () { // Laser ist gerade auf HIGH gegangen, Zeit starten // global laserStart; global laserStop; global KURZ; global LANG; laserStart=micros (); laserPause = laserStart - laserStop; if (laserPause > LANG*2) gotByte(); } void int_falling () { // Laser ist gerade auf LOW gegangen, Zeit stoppen // global laserStart; global laserStop; global KURZ; global LANG; 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; 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; } } void loop() { boolean laser=false; boolean laserBefore=false; boolean silent=true; MFS.beep(1, 5, 2); // bereit MFS.write ((int) KURZ); // Test, ob eingesetzter Widerstand ausreicht, den digitalen Pin auf High zu ziehen // am besten funktionierte am SFH 203 bei mir ein 3.3 kOhm-Widerstand /* while (1) { int wert = digitalRead(PinFoto); digitalWrite(3,!wert); } */ while (1) { laser = digitalRead(PinFoto); int btn= MFS.getButton(); if (btn == BUTTON_2_PRESSED) { digitalWrite(3,HIGH); silent=!silent; } if (btn == BUTTON_1_PRESSED) { // Datenrate halbieren KURZ = KURZ /2; LANG = LANG /2; MFS.beep(1,5,3); MFS.write ((int) KURZ); } // akustische Rückmeldung, Beeper ist Pin 3 if (!silent) digitalWrite(3,!laser); laserBefore=laser; // pause, muss nur noch auf knopfdrücke reagieren, machen alles die interrupts // es sei denn, man hat den ton an // delay (10); } }

Nochmals optimiert

Mit dem Arduino geht nicht mehr mehr. Da ist Ende der Fahnenstange in Sachen Übertragungsgeschwindigkeit. Aber ist gibt ja schnellere Microcontroller wie die STM32 Bluepill. Damit schaffe ich im Nachfolge-Artikel Nochmals weitere Optimierung der Datenübertragung per Laser zwischen Arduino und STM32 noch einmal ein Stück schneller.