In einem Kurs mit dem ESP32 kam die Frage, ob man einen Sender für eine Infrarot-Fernbedienung selbst bauen kann. Ja, das geht. Es ist eine schöne Anwendung für den ESP32.
Zielvorgaben
Worum geht es also? Die Idee war, Signale von einer vorhandenen Infrarot-Fernbedienung einzulesen und dann gesteuert durch den Mikrocontroller zu reproduzieren. Der Controller kann z.B. zu einer bestimmten Zeit das Radio einschalten. Oder man kann einen kleinen Web-Server programmieren, der vom Smartphone gesteuert Licht oder Musik ein- oder ausschaltet. Geräte lassen sich automatisch steuern, ohne dass irgendein Eingriff in das Gerät notwendig ist. Vieles ist denkbar.
Um die Komplexität in Grenzen zu halten, nehmen wir für den Anfang nur drei Kanäle. Es soll also 3 Tasten geben, die jeweils ein angelerntes Signal abspielen können. Eine weitere Taste wird gebraucht, um das kleine Gerät in den Lernmodus zu versetzen. Wenn die Anlern-Taste für eine bestimmte Zeit (hier 3 Sekunden) gedrückt wird, soll das Gerät bereit sein, Signale von einer IR-Fernbedienung zu empfangen und aufzuzeichnen. Schließlich möchten wir noch Leuchtdioden haben, die den aktuellen Status anzeigen. Und die angelernten Daten sollen natürlich dauerhaft über das Stromabschalten hinaus erhalten bleiben. Das ist dann eigentlich schon alles.
IR Signal-Übertragung
Wie funktioniert die IR-Signalübertragung? Die üblichen IR-Sender produzieren Lichtsignale, die mit einer festen Frequenz zwischen 36 und 40 kHz moduliert sind, also sehr schnell ein- und ausgeschaltet werden. Dieser Frequenzbereich wurde gewählt, um den Signalabstand zu Störungen möglichst groß zu halten, denn schließlich ist Infrarotlicht allgegenwärtig, sei es als natürliches Licht von der Sonne oder als gepulstes Licht von diversen Lampen. Die eigentliche Information (z.B. „Radio einschalten“) befindet sich digital kodiert in der zeitlichen Abfolge von kurzen und langen Pulsen („bursts“) mit z.B. 38 kHz. Ein typisches Signal von einer Fernbedienung kann z.B. 30 Millisekunden lang sein und 30, 40 oder 50 Pulse von kurzer oder langer Dauer enthalten.
Soweit die Theorie. Zum Glück ist in diesem Fall die Praxis nicht weit: Man kann mit einem Oszilloskop das Signal einer IR-Fernbedienung an einer Infrarot-Photodiode gut beobachten.
Für unsere Anwendung müssen wir das Signal zum Glück nicht „verstehen“. Aber es muss möglichst exakt aufgezeichnet und wieder abgespielt werden, damit das empfangende Gerät entsprechend reagiert.
Hardware
Es ist relativ einfach, IR-Signale mit einem Mikrocontroller zu generieren. Mit einem Timer wird eine Rechteckschwingung von 38 kHz erzeugt und auf einen der Ports gegeben. Typische IR-Leuchtdioden vertragen Ströme von bis zu 100 mA, was den Port überfordern würde. Deshalb kommt ein einfacher Transistor hinzu, z.B. ein BC337, der das Signal verstärkt. Um die 3.3V Board-Spannung des ESP32 nicht zu sehr durch die 38 kHz Pulse von 100mA zu belasten, wird die Infrarot-LED mit einem 33 Ohm Widerstand nach +5V verschaltet. Damit ist der Strom auf etwas unter 100 mA begrenzt.
Auf der Empfangsseite wird eine Schaltung benötigt, die möglichst empfindlich und gleichzeitig selektiv 38 kHz-IR-Signale herausfiltert und diese dann als digitale Zustände Ein (= Signal vorhanden) oder Aus (= kein Signal vorhanden) verfügbar macht. Das könnte aufwendig werden, wenn es dafür nicht fertige und preisgünstige Komponenten gäbe. Ich verwende den TSOP1138. Dieser oder ähnliche Empfänger mit guter Selektivität und hoher Empfindlichkeit sind in der Arduino-Welt verbreitet und z.B. in Funduino-Bausätzen enthalten. Der TSOP1138 hat drei Anschlüsse: Ground, Spannungsversorgung Vs und Signalausgang. Das Datenblatt sagt, dass die Betriebsspannung 5V betragen sollte. Tatsächlich habe ich sehr gute Ergebnisse mit den 3.3V des ESP32 erreicht. Es besteht also kein Bedarf für Pegelumsetzer.
Schließlich kommen noch 4 Taster und 3 Leuchtdioden dazu. Damit ist die Hardware komplett. Die Schaltung ist einfach und kann gut auf einem Steckbrett aufgebaut werden.
Wenn der Schaltplan klar ist, kann man die entsprechenden GPIO-Nummern im Sketch bereitstellen. Das geschieht im Programm ganz am Anfang als globale Konstanten. Damit sind die Definitionen an einem Ort und können bei Bedarf schnell angepasst werden.
1 2 3 4 5 |
// pin assignments const int bt_learn = 16; const int bt_play[] = {17, 5, 18}; const int ir_receiver = 34, ir_transmitter = 33; const int led[] = {27, 26, 25}; |
Software
Der eigentlich spannende Teil des Projekts ist die Software. Natürlich gibt es ausgefeilte Bibliotheken für die Bearbeitung von Infrarot-Signalen mit einem Arduino (z.B. die multi-protocol-infrared-remote-library von Ken Shirriff). Diese sind aber nicht unbedingt für den ESP32 geeignet. Außerdem fand ich es interessanter, die notwendige Funktionalität mit Board-Mitteln zu programmieren.
System-Takt in Mikrosekunden
Grundsätzlich gibt es zwei mögliche Ansätze, um Zeitverläufe von Signalen aufzuzeichnen:
- Man kann mit einem regelmäßigen, möglichst engen Abstand den Zustand des Eingangs-Port testen und den jeweils aktuellen Wert als 0 oder 1 abspeichern. Üblich ist z.B. eine Abtastrate von 50 Mikrosekunden. Der Nachteil dieser Methode besteht darin, dass die Abtastrate unweigerlich einen möglichen zeitlichen Fehler (hier +/- 25 Mikrosekunden) mitbringt.
- Alternativ kann der Eingangs-Port ständig mit hoher Geschwindigkeit abgefragt („Polling“) und jeweils der Zeitpunkt der Umschaltung aufgezeichnet werden. Wenn das Polling ausreichend schnell geschieht, ist die zeitliche Auflösung dieser Methode besser. Dieser Ansatz setzt allerdings eine möglichst exakte System-Uhr im Bereich von Mikrosekunden voraus.
Glücklicherweise verfügt der ESP32 über genau das: Eine Timer-gesteuerte System-Uhr, die die Anzahl der Mikrosekunden seit dem Einschalten zur Verfügung stellt: esp_timer_get_time() (siehe ESP Referenz). Die Ausführung der Funktion benötigt selbst weniger als eine Mikrosekunde, stellt also keine besondere Systemlast dar.
Die Zeitpunkte (Mikrosekunden) des Anfangs (Einschalten) und Ende (Ausschaltens) der einzelnen Bursts werden in Arrays abgelegt. Da wir mit 3 Kanälen arbeiten, gibt es ein zwedimensionales Array, um die Signal-Zeitpunkte abzuspeichern: ir_data[3][250] . Es enthält drei Zeilen, wobei jede Zeile bis zu 250 Datenpunkte aufnehmen kann. In meinen bisherigen Versuchen waren in der Regel nie mehr als 100 Datenpunkte für ein IR-Signal notwendig – aber es kann nicht schaden, Luft nach oben zu lassen.
Zusätzlich benötigen wir noch jeweils eine Integer-Variable, die die tatsächliche Anzahl der Einträge in den Arrays abspeichert. Ich nenne sie ir_data_len[3], ebenfalls ein Array mit 3 Werten für drei Kanäle. ir_data[][] und ir_data_len[] werden als globale Variablen definiert, also im Programmablauf vor allen Funktionen.
1 2 3 4 5 6 7 8 9 |
// global constants const int channel_cnt = 3; // this version supports 3 channels (maximum is 10) const uint16_t ir_data_size = 250; // size of ir data sequence // global variables Preferences prefs; // access to non volatile storage (NVS) uint32_t ir_data[channel_cnt][ir_data_size]; // IR data set, 3 rows of up to 250 data items each int ir_data_len[channel_cnt] = {0, 0, 0}; bool learn_bt_pressed = false; |
Funktion read_ir_data()
Damit wird die Funktion zum Einlesen der IR-Signale read_ir_data() unkompliziert. Die Funktion übernimmt einen Zeiger auf das Daten-Array, in dem die Daten abgelegt werden, und einen Zeiger auf die zugehörige Zähler-Variable. Am Anfang des Lesevorgangs wartet die Software auf das erste Signal von der IR-Fernbedienung und erfasst die aktuelle Startzeit (start_time). Ab jetzt wird bei jedem Wechsel des Eingangs-Ports die aktuelle Zeit gelesen, die Differenz zur Startzeit errechnet (time_stamp) und diese im Array gespeichert. Die boolesche Variable edge sorgt für die Unterscheidung zwischen dem Wechsel von LOW -> HIGH oder HIGH -> LOW. Nach dem Ablauf der voreingestellten Zeit, hier 250 Millisekunden, wird die Erfassung beendet. Die Arrays mit den Umschalt-Zeitpunkten bilden die Datenbasis für eine exakte Reproduktion des Signals.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/* read_ir_data --------------------------------------------------------------------------- Reads a sequence of ir data via the IR receiver. Captures the start- and stop time points of each burst and stores them to the array ir_data[]. Data reading continues until either the data storage is exceeded or reading timeout is reached. Flashes the corresponding LED during the reading process. Input: channel -> channel number (0 … 2) Output: array ir_data -> ir data points int *ir_data_len -> number of data points stored in the array */ void read_ir_data(int channel, uint32_t ir_data[], int *ir_data_len) { long start_time; uint32_t time_stamp; bool edge = true; // false for falling edge, true for rising edge ledcAttachPin(led[channel], led_pwm_channel); // flash LED ledcWrite(led_pwm_channel, 128); *ir_data_len = 0; // reset array index while (digitalRead(ir_receiver) == HIGH); // wait for falling edge start_time = esp_timer_get_time(); // capture start time do { time_stamp = (uint32_t) (esp_timer_get_time() - start_time); // refresh time stamp if (edge) { if (digitalRead(ir_receiver) == HIGH) { // waiting for rising edge ir_data[*ir_data_len] = time_stamp; // capture time stamp ++*ir_data_len; edge = false; // switch to falling edge }; } else { if (digitalRead(ir_receiver) == LOW) { // waiting for falling edge ir_data[*ir_data_len] = time_stamp; // capture time stamp ++*ir_data_len; edge = true; // switch to rising edge }; }; } while ((time_stamp < ir_read_timeout) && (*ir_data_len < ir_data_size)); ledcDetachPin(led[channel]); // clear flashing LED digitalWrite(led[channel], HIGH); // switch off LED } |
Um die Arbeitsweise zu kontrollieren, gibt es eine Funktion print_ir_data(), die die eingelesenen Daten auf dem seriellen Monitor ausgibt. Hier die Daten für „Einschalten“ bei der „Bose Wave“ Anlage. Das Signal wird zum Zeitpunkt 0 eingeschaltet, nach 1047 Mikrosekunden aus, dann nach 2499 Mikrosekunden wieder ein, nach 3040 Mikrosekunden aus, usw.
1 2 3 4 5 6 7 8 |
Channel 0 IR data (71): 1047, 2499, 3040, 3521, 4041, 4491, 5032, 6490, 7034, 8489, 9038, 9512, 10029, 10507, 11030, 12480, 13023, 13503, 14024, 15475, 16016, 17475, 18019, 18497, 19010, 19466, 20011, 21465, 22015, 23465, 24007, 24486, 25009, 26452, 27001, 77482, 78523, 79984, 80526, 81005, 81527, 82002, 82518, 83975, 84521, 85974, 86524, 86992, 87515, 87992, 88516, 89964, 90508, 90987, 91510, 92960, 93502, 94960, 95505, 95981, 96496, 96950, 97498, 98949, 99500, 100949, 101493, 101971, 102494, 103937, 104486 |
PWM zur Erzeugung des 38 kHz-Signals
Zum Abspielen der aufgezeichneten Signale muss ein 38 kHz-Signal erzeugt und entsprechend ein- und ausgeschaltet werden. Dazu eignet sich die PWM-Funktion der Arduinos. Diese unterscheidet sich beim ESP32 von den 8-Bit-Arduinos, die über die Funktion analogWrite() verfügen. Der ESP32 hat mehr Hardware-Möglichkeiten, insbesondere mehr PWM-Kanäle. Hier wird die PWM mit drei Low Level-Funktionen gesteuert.
1 2 3 |
1) ledcSetup(<channel>, <frequency>, <bit resolution>); 2) ledcAttachPin(<GPIO no>, <channel>); 3) ledcWrite(<channel>, <duty cycle>); |
Mit ledcSetup() wird der gewünschte PWM-Kanals konfiguriert. Die Frequenz ist in unserem Fall 38 kHz. Die Funktion ledcAttachPin() bindet den PWM-Kanal an einen der Ausgabe-Ports (hier der Port für die Infrarot-Leuchtdiode). Schließlich setzt ledcWrite() den gewünschten Duty-Cycle. Wir arbeiten mit einer Bit Resolution von 8, so dass der Wert für Duty Cycle zwischen 0 (entspricht dauerhaft Aus) und 255 (entspricht dauerhaft Ein) liegen darf.
Eine gute Zusammenfassung der PWM beim ESP32 gibt es hier: EPS32 Arduino LED PWM Fading.
Der entsprechende Code im setup()-Block sieht so aus:
1 2 3 4 |
// setup PWM for IR transmitter ledcSetup(ir_pwm_channel, ir_pwm_frequency, 8); // pwm runs with 8 bit resolution ledcAttachPin(ir_transmitter, ir_pwm_channel); ledcWrite(ir_pwm_channel, 0); // switch off the IR transmitter for now |
Funktion play_ir_data()
Im Programm übernimmt die Funktion play_ir_data() die Aufgabe, das IR-Signal – basierend auf den angelernten Daten – zu reproduzieren. Dabei kommt wieder die System-Uhr zur Hilfe, um im richtigen Moment die jeweiligen Bursts ein- oder auszuschalten. Einschalten heißt, den Duty-Cycle auf die Hälfte des Maximalwertes, also 128, zu setzen, so dass das PWM ein möglichst ausgeprägtes 38 kHz Signal erzeugt. Ausschalten heißt, den Duty-Cycle auf 0 zu setzen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/* play_ir_data --------------------------------------------------------------------------- Plays an IR data sequence Input: array ir_data -> holds the start- and stop time points for the bursts int ir_data_len -> number of data points in the array */ void play_ir_data(uint32_t ir_data[], uint16_t ir_data_len) { long start_time; uint32_t time_stamp; bool edge = true; // true for active, false for pause int i = 0; if (ir_data_len == 0) return; // if data set is empty, then nothing to do start_time = esp_timer_get_time(); // capture start time ledcWrite(ir_pwm_channel, 128); // activate IR transmitter do { time_stamp = esp_timer_get_time() - start_time; if (time_stamp >= ir_data[i]) { if (edge) { ledcWrite(ir_pwm_channel, 0); // stop IR transmitter edge = false; } else { ledcWrite(ir_pwm_channel, 128); // activate IR transmitter edge = true; }; ++i; } } while (i < ir_data_len); ledcWrite(ir_pwm_channel, 0); // ensure that IR transmitter is stopped } |
Das Oszilloskop zeigt, dass das ursprüngliche Signal mit guter Genauigkeit reproduziert wird.
Daten langfristig sichern: Preferences
Schließlich gibt es noch die Anforderung, dass der ESP32 die Daten der gelernten Signale über das Ausschalten hinaus im Speicher behalten soll. Beim 8-Bit Arduino gibt es das EEPROM zum dauerhaften Abspeichern von Daten. Leider ist der Speicherplatz des EEPROMS begrenzt und die Programmierung dazu etwas umständlich.
Der ESP32 bietet die Möglichkeit, aus dem Programm heraus den Flash-Speicher, in der ESP-Terminologie non-volatile storage (NVS), zu lesen und zu beschreiben. Der hat natürlich sehr viel Platz. Im Arduino-Framework gibt es das Preference-Objekt, mit dem der Flash-Speicher erreichbar ist. Dazu wird ein globales Objekt prefs vom Typ Preferences erzeugt.
1 2 |
#include <Preferences.h> Preferences prefs; |
Dort kann man Bereiche einrichten, die zum Abspeichern beliebiger Daten zur Verfügung stehen. Der Bereich bekommt einen Namen, der als String übergeben wird, und wird mit Begin geöffnet. Der Name kann willkürlich gewählt werden, hier ir_nvs.
1 |
prefs.begin("ir_nvs", false); |
False bedeutet, dass der Bereich sowohl gelesen als auch beschrieben werden darf. Zum Lesen und Schreiben gibt es die Objekt-Methoden get…() und put…(), jeweils für die üblichen Datentypen, z.B.
1 |
prefs.putUShort(<name_string>, <int value>); |
zum Schreiben eines 16-Bit Integer-Wertes ohne Vorzeichen. Der name_string kann wieder beliebig gewählt werden, muss aber eindeutig sein und dient zur Identifikation der Variable. Eine gute Praxis ist, dafür den Namen der entsprechenden Variable im RAM zu verwenden.
Für Arrays und andere größere Objekte kann man auf die Funktion
1 |
prefs.putBytes(<name_string>, <pointer>, <size>); |
zurückgreifen, die den entsprechenden Speicherbereich ins Flash kopiert und so dauerhaft verfügbar macht.
Als Gegenstück kann mit
1 |
<short_value> = prefs.getUShort(<name_string>); |
der gespeicherte Wert gelesen werden. Wenn man auf einen bisher noch unbeschrieben Namen zugreift, bekommt man den Wert 0.
Im Programm werden nach dem Einschalten im setup()-Block die Anzahl der abgespeicherten Daten für die drei Kanäle aus dem Flash-Speicher in das Array ir_data_len[] gelesen. Wenn die Werte größer als 0 sind, dann gibt es tatsächlich abgelegte IR-Daten, die dann in das Array ir_data[][] im RAM kopiert werden und für die Funktion play_ir_data() zur Verfügung stehen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// read IR data from NVS char ir_len_str[10] = "ir_len_0", ir_data_str[10] = "ir_data_0"; prefs.begin("ir_nvs", false); Serial.println(); Serial.println("Reading NVS ir_data "); for (int i = 0; i < channel_cnt; ++i) { ir_len_str[7] = '0' + i; ir_data_str[8] = '0' + i; ir_data_len[i] = prefs.getUShort(ir_len_str); Serial.print("Channel " + String(i) + ": "); Serial.println(String(ir_data_len[i]) + " Entries"); if (ir_data_len[i] > 0) prefs.getBytes(ir_data_str, ir_data[i], ir_data_len[i] * sizeof(uint32_t)); } Serial.println(); |
Entsprechend werden nach dem Anlernen von IR-Signalen die Daten in den Flash-Speicher geschrieben, so dass sie dauerhaft erhalten bleiben und beim nächsten Einschalten des Geräts gelesen werden können. Das übernimmt die Funktion learn_sequence(), die nacheinander für alle 3 Kanäle die Funktion read_ir_data() aufruft und die Daten dann in den Flash-Speicher schreibt.
1 2 3 4 5 6 7 8 9 10 11 12 |
/* -------------------------------------------------------------------------------------*/ void learn_sequence(void) { char ir_len_str[10] = "ir_len_0", ir_data_str[10] = "ir_data_0"; for (int i = 0; i < channel_cnt; ++i) { read_ir_data(i, ir_data[i], &ir_data_len[i]); print_ir_data(i, ir_data[i], ir_data_len[i]); ir_len_str[7] = '0' + i; ir_data_str[8] = '0' + i; prefs.putUShort(ir_len_str, ir_data_len[i]); prefs.putBytes(ir_data_str, ir_data[i], ir_data_len[i] * sizeof(uint32_t)); }; } |
Alle Komponenten zusammen setzen
Damit sind die wesentlichen Komponenten für das System vorhanden. Die Schleife loop() fragt die Buttons ab und verzweigt in die jeweiligen Funktionen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
/* -------------------------------------------------------------------------------------*/ void loop() { uint8_t learn_pressed_cnt = 0; // check whether the learn button is pressed for at least 3 seconds while (digitalRead(bt_learn) == LOW) { delay(200); ++learn_pressed_cnt; if (learn_pressed_cnt >= 15) { learn_sequence(); learn_pressed_cnt = 0; } } // check the play buttons and play correspoding sequence for (int i = 0; i < channel_cnt; ++i) { if (digitalRead(bt_play[i]) == LOW) { digitalWrite(led[i], LOW); play_ir_data(ir_data[i], ir_data_len[i]); digitalWrite(led[i], HIGH); delay(250); } } |
Die Bedienung geschieht folgendermaßen:
- Nach dem Einschalten (Power On) werden die drei Leuchtdioden der Reihe nach kurz durchgeschaltet, um zu zeigen, dass das System aktiv ist.
- Das Gerät kann drei verschiedene Signale aufzeichnen und abspielen. Dazu dienen die drei Taster. Ein Druck auf einen der Taster bewirkt das Aussenden des entsprechenden Signals. Dabei leuchtet die zugehörige Leuchtdiode kurz auf.
- Um in den Lernmodus zu gelangen, muss die Anlern-Taste für mindestens 3 Sekunden gedrückt werden. Dann blinkt die Leuchtdiode des ersten Kanals in einem schnellen Rhythmus, um anzuzeigen, dass das Gerät auf ein Signal zum Anlernen wartet. Man sollte jetzt die Fernbedienung auf den IR-Empfänger richten und kurz (!) die gewünschte Funktion drücken. Nachdem das passiert ist, wiederholt sich der Vorgang für Kanal 2 und 3. Damit ist das Gerät programmiert und bereit zum Einsatz.
Hier das ganze Programm.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
/* ir_remote_control This scetch records and reproduces IR signals from any IR remote control. The IR data is stored in NVS memory and therefore retained beyond power off. This version supports 3 channels. It can easily be extended to more channels. Pin assignments: bt_learn input pullup GPIO16 learn button, push for at least 3 seconds bt_play input pullup GPIO17, 5, 18 buttons to send signal ir receiver input GPIO34 IR receiver chip, e.g.TSOP1138 ir transmitter output GPIO33 IR transmitter diode, connected via a transistor led output GPIO27, 26, 25 control leds SLW 10-Sep-2019 */ #include <Arduino.h> #include <Preferences.h> // pin assignments const int bt_learn = 16; const int bt_play[] = {17, 5, 18}; const int ir_receiver = 34, ir_transmitter = 33; const int led[] = {27, 26, 25}; // global constants const int channel_cnt = 3; // this version supports 3 channels (maximum is 10) const uint16_t ir_data_size = 250; // size of ir data sequence const uint16_t ir_pwm_frequency = 38000; // ir signal sends at 38kHz const uint8_t ir_pwm_channel = 0; // first pwm channel for ir signal const uint8_t led_pwm_channel = 2; // second pwm channel for LEDs const uint16_t led_pwm_frequency = 8; // frequency for flashing LEDs const uint32_t ir_read_timeout = 250000; // max duration is 0.2 sec // global variables Preferences prefs; // access to non volatile storage (NVS) uint32_t ir_data[channel_cnt][ir_data_size]; // IR data set, 3 rows of up to 250 data items each int ir_data_len[channel_cnt] = {0, 0, 0}; bool learn_bt_pressed = false; /* startup_msg --------------------------------------------------------------------------- Flashes the LEDs at startup, just to show that the system is active. */ void startup_msg(void) { for (int i = 0; i < channel_cnt; ++i) { digitalWrite(led[i], LOW); delay(500); digitalWrite(led[i], HIGH); } } /* read_ir_data --------------------------------------------------------------------------- Reads a sequence of ir data via the IR receiver. Captures the start- and stop time points of each burst and stores them to the array ir_data[]. Data reading continues until either the data storage is exceeded or reading timeout is reached. Flashes the corresponding led during the reading process. Input: channel number Output: array ir_data -> ir data points int *ir_data_len -> number of data points stored in the array */ void read_ir_data(int channel, uint32_t ir_data[], int *ir_data_len) { long start_time; uint32_t time_stamp; bool edge = true; // false for falling edge, true for rising edge ledcAttachPin(led[channel], led_pwm_channel); // flash LED ledcWrite(led_pwm_channel, 128); *ir_data_len = 0; // reset array index while (digitalRead(ir_receiver) == HIGH); // wait for falling edge start_time = esp_timer_get_time(); // capture start time do { time_stamp = (uint32_t) (esp_timer_get_time() - start_time); // refresh time stamp if (edge) { if (digitalRead(ir_receiver) == HIGH) { // waiting for rising edge ir_data[*ir_data_len] = time_stamp; // capture time stamp ++*ir_data_len; edge = false; // switch to falling edge }; } else { if (digitalRead(ir_receiver) == LOW) { // waiting for falling edge ir_data[*ir_data_len] = time_stamp; // capture time stamp ++*ir_data_len; edge = true; // switch to rising edge }; }; } while ((time_stamp < ir_read_timeout) && (*ir_data_len < ir_data_size)); ledcDetachPin(led[channel]); // clear flashing LED digitalWrite(led[channel], HIGH); // switch off LED } /* play_ir_data --------------------------------------------------------------------------- Plays an IR data sequence Input: array ir_data -> holds the start- and stop time points for the bursts int ir_data_len -> number of data points in the array */ void play_ir_data(uint32_t ir_data[], uint16_t ir_data_len) { long start_time; uint32_t time_stamp; bool edge = true; // true for active, false for pause int i = 0; if (ir_data_len == 0) return; // if data set is empty, then nothing to do start_time = esp_timer_get_time(); // capture start time ledcWrite(ir_pwm_channel, 128); // activate IR transmitter do { time_stamp = esp_timer_get_time() - start_time; if (time_stamp >= ir_data[i]) { if (edge) { ledcWrite(ir_pwm_channel, 0); // stop IR transmitter edge = false; } else { ledcWrite(ir_pwm_channel, 128); // activate IR transmitter edge = true; }; ++i; } } while (i < ir_data_len); ledcWrite(ir_pwm_channel, 0); // ensure that IR transmitter is stopped } /* print_ir_data --------------------------------------------------------------------------- Prints ir data to the serial monitor, for control purpose only */ void print_ir_data(int channel, uint32_t ir_data[], uint16_t ir_data_len) { int line_cnt = 0; Serial.println(); Serial.print("Channel " + String(channel) + " IR data ("); Serial.print(ir_data_len); Serial.println("):"); for (int i = 0; i < ir_data_len; ++i) { Serial.print(ir_data[i]); if (i < ir_data_len - 1) Serial.print(", "); ++line_cnt; if (line_cnt > 10) { Serial.println(); line_cnt = 0; } } Serial.println(); } /* -------------------------------------------------------------------------------------*/ void learn_sequence(void) { char ir_len_str[10] = "ir_len_0", ir_data_str[10] = "ir_data_0"; for (int i = 0; i < channel_cnt; ++i) { read_ir_data(i, ir_data[i], &ir_data_len[i]); print_ir_data(i, ir_data[i], ir_data_len[i]); ir_len_str[7] = '0' + i; ir_data_str[8] = '0' + i; prefs.putUShort(ir_len_str, ir_data_len[i]); prefs.putBytes(ir_data_str, ir_data[i], ir_data_len[i] * sizeof(uint32_t)); }; } /* -------------------------------------------------------------------------------------*/ void setup() { char ir_len_str[10] = "ir_len_0", ir_data_str[10] = "ir_data_0"; Serial.begin(115200); // read IR data from NVS prefs.begin("ir_nvs", false); Serial.println(); Serial.println("Reading NVS ir_data "); for (int i = 0; i < channel_cnt; ++i) { ir_len_str[7] = '0' + i; ir_data_str[8] = '0' + i; ir_data_len[i] = prefs.getUShort(ir_len_str); Serial.print("Channel " + String(i) + ": "); Serial.println(String(ir_data_len[i]) + " Entries"); if (ir_data_len[i] > 0) prefs.getBytes(ir_data_str, ir_data[i], ir_data_len[i] * sizeof(uint32_t)); } Serial.println(); // set pin modes for (int i = 0; i < channel_cnt; ++i) { pinMode(bt_play[i], INPUT_PULLUP); // define ports for buttons pinMode(led[i], OUTPUT); // define ports for LEDs digitalWrite(led[i], HIGH); // switch off the LEDs }; pinMode(bt_learn, INPUT_PULLUP); pinMode(ir_receiver, INPUT); // setup PWM for IR transmitter ledcSetup(ir_pwm_channel, ir_pwm_frequency, 8); // pwm runs with 8 bit resolution ledcAttachPin(ir_transmitter, ir_pwm_channel); ledcWrite(ir_pwm_channel, 0); // switch off the IR transmitter, for now // setup PWM for flashing LEDs ledcSetup(led_pwm_channel, led_pwm_frequency, 8); // show welcome message startup_msg(); } /* -------------------------------------------------------------------------------------*/ void loop() { uint8_t learn_pressed_cnt = 0; // check whether the learn button is pressed for at least 3 seconds while (digitalRead(bt_learn) == LOW) { delay(200); ++learn_pressed_cnt; if (learn_pressed_cnt >= 15) { learn_sequence(); learn_pressed_cnt = 0; } } // check the play buttons and play correspoding sequence for (int i = 0; i < channel_cnt; ++i) { if (digitalRead(bt_play[i]) == LOW) { digitalWrite(led[i], LOW); play_ir_data(ir_data[i], ir_data_len[i]); digitalWrite(led[i], HIGH); delay(250); } } } |
Fazit
Eine lernfähige IR-Fernbedienung kann mit dem ESP32 relativ schnell entwickelt werden. Das Gerät arbeitet zuverlässig und hat sich bei verschiedenen Anwendungen bewährt. Die Genauigkeit der Reproduktion ist sehr hoch. Bei mir haben die Zielgeräte bisher klaglos die Signale der „fremden“ Fernbedienung akzeptiert.
Bei Bedarf kann die Anzahl der Kanäle weiter erhöht werden, solange GPIOs für Tasten und Leuchtdioden vorhanden sind.
Bleibt die Frage, ob der ESP32 für diese Anwendung Vorteile im Vergleich zum 8-Bit Arduino bietet. Natürlich kann man ein ähnliches Ergebnis auch mit eine Arduino Nano erreichen. Dabei würde es aber im RAM knapp. 3 * 250 Datenpunkte von jeweils 32 Bit belegen mehr als 2 kB und würden das verfügbare RAM des ATmega328 bereits überfordern. Außerdem macht sich beim ESP32 der schnelle Systemtakt für eine höhere Genauigkeit der reproduzierten Signale und der der große Flash-Speicher zum dauerhaften Ablegen der Sequenzen nützlich. Spätestens wenn mehr Kanäle notwendig werden, wird der 8-Bitter nicht ausreichen. Und als Erweiterung kann der ESp32 die Bedienung über ein Web-Interface ermöglichen, was ein weiterer Schritt in Richtung Smart-Home wäre.
Downloads
Hallo, eine Frage: Warum ist die Anzahl der Kanäle auf 10 beschränkt?
Hallo Max, die maximale Zahl der Kanäle wurde willkürlich gewählt. Jeder Kanal sollte möglichst einfach über die Tasten 0 bis 9 aufrufbar sein. Es ist aber durchaus möglich, mehr Kanäle zu verwenden. Das Programm kann einfach angepasst werden.
Viele Grüsse,
Stephan Laage-Witt
Welche IR-Sende-Diode empfiehlst Du?
Hey Stephan,
ich habe mich wahnsinnig über Dein(e Kreation) Programm gefreut. Endlich ein Code der auch mit Sony und anderen Fernbedienungen super zurecht kommt. Leider hatte ich mit diversen für Uno geschriebenen Programmen weniger Erfolg.
Ich bin leider erst Anfänger und kann Genies wie Dich (noch nur bewundern, aber ich arbeite dran.
Die Erweiterung auf 5 Tasten mit 10 Kanälen ist mir gelungen.
Daher nochmal vielen Dank!
Viele Grüße,
Christian