Motivation

Die neueren Boards für den ESP32-μC sind mit RGB-LEDs vom Typ WS2812(B) ausgerüstet. Diese lassen sich durch eine einfache Pegeländerung an einem GPIO-Pin nicht mehr ansteuern. Meist wird dazu die Adafruit-Neopixel-Bibliothek benutzt. Dies ist eine sehr umfangreiche Bibliothek, die viele μC- und RGB-LED-Typen ansteuern kann. Für mein ESP32-C6-Dev-Modul erzeugt dieser Code zufällige Farben auf der eingebauten WS2812B (Download s.u.):

#include <Adafruit_NeoPixel.h>

constexpr uint8_t LED_PIN  = 8;    // BUILTIN_LED liefert nicht den passenden Wert
constexpr uint8_t NUM_LEDS = 1;
constexpr uint8_t BRIGHTNESS = 64;

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);

void setup() {
   pixels.begin();
}

void loop() {
   pixels.setPixelColor(0, pixels.Color(random(BRIGHTNESS), random(BRIGHTNESS), random(BRIGHTNESS)));
   pixels.show(); // Farbwerte übertragen.
   delay(500); 
}

Für den ESP32 basiert die Bibliothek auf der Remote Control Bibliothek (RMT). Diese Peripherieeinheit wurde ursprünglich als Infrarot-Transceiver entwickelt, kann jedoch auch zum Erzeugen oder Empfangen vieler anderer Arten von digitalen Signalen mit präziser Zeitsteuerung verwendet werden.

Das Datenblatt der WS2812B gibt Auskunft darüber, wie die LED angesteuert werden muss.

Zur Ansteuerung einer einzelnen WS2812 ist die Adafruit-Bibliothek vielleicht etwas oversized. Ein guter Grund, sich einmal mit dem ESP32-RMT auseinander zu setzen.

Ein paar Hintergrundinformationen schaden außerdem nicht, wenn man Probleme beheben muss.


In­halts­ver­zeich­nis

Abstraktionsebenen

Das ESP32 Remote Control Modul (RMT)

Initialisierung (rmtInit)

Daten versenden (rmtWrite)

Beispiel: WS2812 ansteuern (HAL-Ebene)

WS2812-Protokoll

Bespielcode

Weitere Funktionen

Allgemein

rmtDeinit

rmtSetEOT

Übertragung

rmtWriteAsync

rmtTransmitCompleted

rmtWriteRepeated

rmtWriteLooping

Modulation

rmtSetCarrier

Empfang

rmtRead

rmtReadAsync

rmtReceiveCompleted

rmtSetRxMinThreshold

rmtSetRxMaxThreshold

RMT_SYMBOLS_OF

Beispiel IR-Fernbedienungssignale empfangen

Beispiel: Frequenzmessung

Beispiel: IR-Decoder

Klasse RmtDecoder

Konkrete Decoder-Klasse (RmtToshibaSER0336Decoder)

Download

Abstraktionsebenen

Die Adafruit-Neopixel-Bibliothek ist speziell für die Ansteuerung von RGB-LEDs wie der WS2812 ausgelegt. Sie kann auf diversen μC eingesetzt und für verschiede LED-Typen verwendet werden. Bei den ESP32-Typen werden die Funktionen der Espressif Remote Control-Bibliothek weitergeleitet.

Die Espressif Remote Control-Bibliothek bietet Funktionen auf der HAL-Ebene (Hardware Abstraction Layer). Diese Peripherieeinheit kann zum Erzeugen oder Empfangen vieler Arten von digitalen Signalen mit präziser Zeitsteuerung verwendet werden. Neben der Ansteuerung der WS2612-LED-Typen können Signale zur IR-Fernsteuerung erzeugt und empfangen werden. Diese Bibliothek erlaubt u.a. den asynchronen Versand und Empfang von Signalen. Ebenfalls ist die Modulation einer Trägerfrequenz möglich, wie sie für die IR-Fernsteuerung von Geräten der Unterhaltungselektronik üblich ist. Diese Bibliothek funktioniert auf jedem ESP32-Typ.

Noch feiner lassen sich die Dinge mit Hilfe der Remote Control Transceiver-Bibliothek regeln. Diese Bibliothek ist vom ESP32-Typ anhängig. Der API-Umfang ist riesig und Bedarf eines ausführlichen Studiums.

Das Technical Reference Manual z.B. für den ESP32: ESP32 Technical Reference Manual Version 5.6, Kapitel 30 Remote Control Peripheral (RMT) beschreibt die Funktionen des RMT auf der Registerebene.

Diese Seite beschäftigt sich mit den Funktionen auf der HAL-Ebene. Die Angaben und Bespiele beziehen sich auf das Arduino-Framework in der Version 3.3.4 und dem ESP-IDF in der Version 5.5.2. Die Programme wurden auf einem EPS32-C6-Dev-Modul getestet. Sollten aber auf jedem anderen ESP32 ebenfalls funktionieren. Das ist der Vorteil der HAL-ebene.

ESP32-C6 Dev-Modul

Das ESP32 Remote Control Modul (RMT)

Das Remote Control Modul ist von Espressif ausführlich dokumentiert. Dort gibt es auch Beispiele, wie eine WS2812 angesteuert werden kann. Das Modul agiert auf der HAL-Ebene (Hardware Abstraction Layer). Es gibt noch eine darunter liegende Schicht LL (Low Level), die noch hardwarenäher und ggf. prozessorspezifisch programmiert ist.

Die Funktionen des RMT-HAL-Moduls sind in der Datei esp32-hal-rmt.h deklariert. Es reicht die Namensangabe in der include-Anweisung, weil ein Include-Pfad für die HAL-Dateien definiert ist. Der Ordner mit den HAL-Modulen findet man im Verzeichnis C:\Users\<user>\AppData\Local\arduino15\packages\esp32\hardware\esp32\3.3.4\cores\esp32

Die verschiedenen ESP32-Systeme besitzen eine unterschiedliche Anzahl von RMT-Kanälen. Bei ESP32-C6 sind es je zwei Kanäle zum Senden und zum Empfangen.

Initialisierung (rmtInit)

Das RMT-Modul wird über die Funktion RMTInit initialisiert.

bool rmtInit(int pin, rmt_ch_dir_t channel_direction, rmt_reserve_memsize_t memsize, uint32_t frequency_Hz);

pin ist die GPIO-Nummer, an die die WS2812 angeschlossen ist. channel_direction gibt an, ob über den Kanal gesendet oder empfangen werden soll. Vordefinierte Werte sind RMT_RX_MODE und RMT_TX_MODE. Die Bezeichnung des Parameters memsize ist etwas irreführend. Hier ist die zu benutzende Kanalnummer anzugeben. Beim ESP32-C6 gibt es zwei Kanäle. Mögliche Angaben sind deshalb RMT_MEM_NUM_BLOCKS_1 oder RMT_MEM_NUM_BLOCKS_2. Der letzte Parameter frequency_Hz gibt die Basisfrequenz an. Taktzeiten sind dann auf Basis dieser Frequenz anzugeben. Mögliche Angaben sind Werte zwischen 312.5 kHz und 80 MHz. Für die Ansteuerung von WS2812-LEDs ist 10 MHz (100 ns Tick) geeignet. Gut geeignet für die meisten IR-Protokolle ist 1 MHz (1 µs Tick). Diese Funktion gibt true zurück, wenn die Initialisierung erfolgreich war, andernfalls false.

Für das ESP32-C6-Modul ist zur Ansteuerung der internen WS2812 folgende Initialisierung passend:

rmtInit(LED_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 10'000'000);

Daten versenden (rmtWrite)

Vor der Übertragung muss für jedes Bit ein Strukturelement gefüllt werden, das die Pulsform bestimmt:

typedef union {
  struct {
    uint32_t duration0 : 15;
    uint32_t level0    : 1;
    uint32_t duration1 : 15;
    uint32_t level1    : 1;
  };
  uint32_t val;
} rmt_data_t;

Um den Puls für eine WS2812 zu generieren eignen sich diese Anweisungen:

rmt_data_t pulse = { 8, 1, 4, 0 }; // Für eine 1

rmt_data_t pulse = { 4, 1, 8, 0 }; // Für eine 0

Oder etwas ausführlicher

rmt_data_t pulse = { // Für eine 1
    .duration0 = 8,
    .level0 = 1,
    .duration1 = 4,
    .level1 = 0
};

Das Versenden der Farbcodes erledigt die Funktion rmtWrite:

bool rmtWrite(int pin, rmt_data_t *data, size_t num_rmt_symbols, uint32_t timeout_ms);

pin ist wieder die GPIO-Nummer, an die die WS2812 angeschlossen ist. data ist ein Zeiger auf ein Feld mit Elementen vom Typ rmt_data_t. Für jedes Bit ist ein Feldelement notwendig, für eine WS2812 als 12: rmt_data_t led_data[24]. num_rmt_symbols gibt an, wie viele Symbole erzeugt, Puls übertragen werden müssen. timeout_ms gibt das Timeout in Millisekunden an. RMT_WAIT_FOR_EVER für unbegrenztes Warten. Die Funktion kehrt erst nach dem Senden aller Daten oder nach Ablauf der Zeitüberschreitung zurück. Die Funktion gibt true zurück, wenn die Übertragung erfolgreich war, oder false bei einem Fehler oder einer Zeitüberschreitung.

Beispiel: WS2812 ansteuern (HAL-Ebene)

WS2812-Protokoll

Bevor näher auf das RMT eingegangen wird, soll zunächst dargestellt werden, welche Anforderungen zur Ansteuerung einer WS2812 erfüllt sein müssen.

Die WS2812 unterscheidet sich nur intern von der WS2812B. Die WS2812B hat besitzt einen integrierten Verpolungsschutz, bietet eine höhere Helligkeit und eine bessere Farbkonsistenz.

Zur Festlegung der Farben müssen drei RGB-Bytes in der Reihenfolge G-R-B übertragen werden. Das höchstwertige Bit wird zuerst gesendet:

Bit-Reigenfolge WS2812

Die WS2812 können kaskadiert werden. Ab dem 25.-Bit werden die Bits an die folgenden WS2812 weiter geleitet:

WS2812-Kaskade

Ein einzelnes Bit wird als 1-0-Puls übertragen. Der logische Wert wird durch das Pulsverhältnis festgelegt:

WS2812-Einzel-Bit-Codes

Zwischen Farbwertfolgen muss eine Pause von länger als 50 μs erfolgen.

Bespielcode

In setup werden der RMT-Block initialisiert. Die Funktion xmitColor überträgt den RGB-Wert an die interne WS2812. Der komplette Code kann im Abschnitt Download herunter geladen werden.

constexpr uint8_t LED_PIN = 8;     // BUILTIN_LED liefert nicht den passenden Wert
constexpr uint8_t NUM_LEDS = 1;    // Anzahl WS2812
constexpr uint8_t BRIGHTNESS = 64; // Max. Helligkeit, 1..255
constexpr uint8_t NUMBEROFBITS = NUM_LEDS * 24; // Anzahl Bits, die übertragen werden müssen

...

 if (!rmtInit(LED_PIN, RMT_TX_MODE, RMT_MEM_NUM_BLOCKS_1, 10'000'000)) {
    Serial.println("Initialisierung des Senders fehlgeschlagen\n");
}

...

// Überträgt eine RGB-Kombination an die WS2812
void xmitColor(uint8_t R, uint8_t G, uint8_t B) {
   rmt_data_t led_data[NUMBEROFBITS]; // 24 Bit für eine WS2812
   int color[] = { G, R, B };// Green Red Blue values
   int i = 0;

   for (int colIndex = 0; colIndex < 3; colIndex++) {
      for (int bit = 0; bit < 8; bit++) {
         if (color[colIndex] & (1 << (7 - bit)))  // 0 oder 1?
            led_data[i] = { 8, 1, 4, 0 };
         else
            led_data[i] = { 4, 1, 8, 0 };
         i++;
      }
   }

   // Send the data and wait until it is done
   rmtWrite(LED_PIN, led_data, NUMBEROFBITS, RMT_WAIT_FOR_EVER);
}

Weitere Funktionen

Allgemein

rmtDeinit

Wenn der Pin oder der RMT-Kanal nicht andauernd für das RMT-Modul zur Verfügung stehen kann, kann der Pin / der Kanal durch die Funktion rmtDeinit wieder freigegeben werden:

bool rmtDeinit(int pin);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). Die Funktion gibt true zurück, wenn die Deinitialisierung erfolgreich war, andernfalls false.

rmtSetEOT

rmtSetEOT legt den End of Transmission (EOT)-Pegel für den RMT-Pin fest, wenn die Übertragung beendet ist.

bool rmtSetEOT(int pin, uint8_t EOT_Level);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). EOT_Level legt den Spannungswert fest, der nach beendeter Übertragung angelegt wird. Die Voreinstellung ist LOW (0). Ein Wert ungleich 0, legt HIGH für den Endwert fest. Die Funktion gibt true zurück, wenn die Deinitialisierung erfolgreich war, andernfalls false.

Hinweis: Dies betrifft nur den Übertragungsprozess. Der Leerlaufpegel vor der Übertragung kann manuell mit digitalWrite(pin, level) eingestellt werden.

Übertragung

Es stehen ergänzende Übertragungsfunktionen für spezielle Anwendungen zur Verfügung.

rmtWriteAsync

rmtWriteAsync sendet RMT-Daten im nicht blockierenden (asynchronen) Modus. Die Funktion kehrt sofort nach Beginn der Übertragung zurück.

bool rmtWriteAsync(int pin, rmt_data_t *data, size_t num_rmt_symbols);

Parameter analog zu rmtWrite. pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). data ist ein Zeiger auf ein Feld mit Elementen vom Typ rmt_data_t. Für jedes Bit ist ein Feldelement notwendig. num_rmt_symbols gibt an, wie viele Symbole erzeugt, Puls übertragen werden müssen. Die Funktion gibt bei erfolgreicher Ausführung true zurück, andernfalls false.

Hinweis: Wenn rmtWriteAsync aufgerufen wird, während eine vorherige Übertragung noch läuft, gibt es sofort false zurück, um einen Fehler anzuzeigen. Es wird nicht gewartet, bis die vorherige Übertragung abgeschlossen ist.

rmtTransmitCompleted

rmtTransmitCompleted überprüft, ob die RMT-Übertragung abgeschlossen ist und der Kanal für die Übertragung neuer Daten bereit ist.

bool rmtTransmitCompleted(int pin);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit).

Diese Funktion gibt true zurück, wenn alle Daten gesendet wurden und der Kanal für eine neue Übertragung bereit ist, andernfalls false.

Hinweis: Wenn rmtWrite eine Zeitüberschreitung verursacht hat oder rmtWriteAsync aufgerufen wurde, gibt diese Funktion false zurück, bis alle Daten gesendet wurden. rmtTransmitCompleted gibt immer true zurück, wenn rmtWriteLooping aktiv ist, da es in diesem Fall keine Auswirkung hat.

rmtWriteRepeated

rmtWriteRepeated versendet eine Bitfolge mehrfach.

bool rmtWriteRepeated(int pin, rmt_data_t *data, size_t num_rmt_symbols, uint32_t loop_count);

Parameter analog zu rmtWrite. loop_count gibt die Anzahl an Wiederholungen an. Die Funktion gibt bei erfolgreicher Ausführung true zurück, andernfalls false.

rmtWriteLooping

rmtWriteLooping versendet die Bitfolge in einer Endlosschleife. Der Schleifenmodus benötigt ein mit Null endendes Datensymbol {0, 0, 0, 0}, um das Ende der Daten zu markieren.

bool rmtWriteLooping(int pin, rmt_data_t *data, size_t num_rmt_symbols);

Parameter analog zu rmtWrite. Die Funktion gibt bei erfolgreicher Ausführung true zurück, andernfalls false.

Um die Schleife zu beenden, muss rmtWrite oder rmtWriteAsync mit neuen Daten oder Sie rmtWriteLooping mit NULL-Daten oder einer Größe von Null aufgerufen werden.

Modulation

Wenn die Bibliothek zum Verwenden von IR-Fernbedienungssignalen verwendet werden soll, muss eine Trägerfrequenz moduliert werden. Dies wird von der Bibliothek unterstützt.Modulation

rmtSetCarrier

Legt die Trägerfrequenzmodulation/-demodulation für den RMT-Sende- oder Empfangskanal fest.

bool rmtSetCarrier(int pin, bool carrier_en, bool carrier_level, uint32_t frequency_Hz, float duty_percent);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). carrier_en: Trägermodulation (TX) oder Demodulation (RX) aktivieren/deaktivieren. carrier_level legt die Trägerpolarität fest: true: Positive Polarität (aktiv hoch), false: Negative Polarität (aktiv niedrig). frequency_Hz: Trägerfrequenz in Hz (z. B. 38000 für 38 kHz IR-Träger). duty_percent: Tastverhältnis als Float von 0,0 bis 1,0 (z. B. 0,33 für 33 % Tastverhältnis, 0,5 für 50 % Rechteckwelle). Diese Funktion gibt true zurück, wenn der Träger erfolgreich eingestellt wurde, andernfalls false.

Beispiel: 38,5 kHz Trägerfrequenz mit 50 % Tastverhältnis: rmtSetCarrier(Pin, true, true, 38500, 0.55)

Empfang

Es gibt ebenfalls Funktionen zum Empfang von Daten. Diese Funktionen sind relevant, wenn z.B. Daten von einer IR-Fernsteuerung empfangen und ausgewertet werden sollen.

rmtRead

Initiiert den blockierenden Empfangsvorgang. Liest RMT-Daten und speichert sie im bereitgestellten Puffer. Die Funktion wartet, bis Daten empfangen wurden oder eine Zeitüberschreitung auftrat.

bool rmtRead(int pin, rmt_data_t *data, size_t *num_rmt_symbols, uint32_t timeout_ms);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). data ist ein Zeiger auf den Puffer, in dem empfangene RMT-Symbole gespeichert werden sollen. num_rmt_symbols ist ein Zeiger auf eine Variable, die die maximale Anzahl der zu lesenden Symbole enthält. Bei der Rückkehr aus der Funktion enthält diese Variable die tatsächlich gelesene Anzahl von Symbolen. timeout_ms legt die Zeitüberschreitung in Millisekunden fest. RMT_WAIT_FOR_EVER steht für unbegrenztes Warten.

Wenn der Lesevorgang zeitlich begrenzt ist, ändert sich num_rmt_symbols nicht und rmtReceiveCompleted kann später verwendet werden, um zu überprüfen, ob Daten verfügbar sind.

Die Funktion gibt true zurück, wenn die Daten erfolgreich gelesen wurden, und false bei einem Fehler oder Zeitüberschreitung.

rmtReadAsync

rmtReadAsync startet einen nicht blockierenden (asynchronen) Empfangsvorgang. Kehrt unmittelbar nach dem Start des Empfangsprozesses zurück.

bool rmtReadAsync(int pin, rmt_data_t *data, size_t *num_rmt_symbols);

Parameter analog zu rmtRead. Die Funktion gibt true zurück, wenn die Daten erfolgreich gelesen wurden, und false bei einem Fehler. Mit rmtReceiveCompleted kann überprüft werden, ob Daten verfügbar sind.

rmtReceiveCompleted

Überprüft, ob der Empfang der RMT-Daten abgeschlossen ist und neue Daten zur Verarbeitung verfügbar sind.

bool rmtReceiveCompleted(int pin);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). Diese Funktion gibt true zurück, wenn Daten empfangen wurden und im Puffer verfügbar sind, andernfalls false.

rmtSetRxMinThreshold

Legt den Mindestimpulsbreiten-Schwellenwert für den RX-Kanal fest. Impulse, die kleiner als dieser Schwellenwert sind, werden als Rauschen ignoriert.

bool rmtSetRxMinThreshold(int pin, uint8_t filter_pulse_ticks);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). filter_pulse_ticks: Minimale Impulsbreite in RMT-Ticks. Impulse (hoch oder niedrig), die kleiner als dieser Wert sind, werden herausgefiltert. 0 deaktiviert den Filter. Der Schwellenwert wird in RMT-Ticks angegeben, die von der während rmtInit festgelegten RMT-Frequenz abhängen.

Hinweis: In der aktuellen Version 5.5.2 des ESP-IDF darf der Wert nicht größer als 3187 ns sein. Wenn also in rmtInit die Frequenz mit 1 MHz festgelegt wird, darf der Wert nicht größer als 3 sein. Ansonsten wird beim Aufruf von rmtRead dieser Fehlertext ausgegeben: E (..) rmt: rmt_receive(396): signal_range_min_ns too big, should be less than 3187 ns.

Diese Funktion gibt true zurück, wenn der Filter-Schwellenwert erfolgreich festgelegt wurde, andernfalls false.

rmtSetRxMaxThreshold

Legt den maximalen Leerlaufschwellenwert für den RX-Kanal fest. Wenn länger als dieser Schwellenwert keine Flanke erkannt wird, wird der Empfangsprozess beendet.

bool rmtSetRxMaxThreshold(int pin, uint16_t idle_thres_ticks);

pin ist die GPIO-Nummer, mit der der Kanal initialisiert wurde (s. rmtInit). idle_thres_ticks legt die maximale Leerlaufzeit in RMT-Ticks fest. Wenn länger als diese Zeit keine Flanke erkannt wird, wird der Empfang beendet. Dieser Schwellenwert definiert auch, wie viele Low/High Bits am Ende der empfangenen Daten gelesen werden.

Diese Funktion gibt true zurück, wenn der Filter-Schwellenwert erfolgreich festgelegt wurde, andernfalls false.

RMT_SYMBOLS_OF

Liefert die Anzahl der Elemente in einem Array von rmt_data_t.

#define RMT_SYMBOLS_OF(x) (sizeof(x) / sizeof(rmt_data_t))

Beispiel IR-Fernbedienungssignale empfangen

Mit einem kleinen IR-Empfänger kann man die Codes ermitteln, die von einer IR-Fernbedienung gesendet werden. Man benötigt einen Empfänger, der auf die Trägerfrequenz des Signals abgestimmt ist. Die meisten Fernbedienungen modulieren ein 38 kHz Rechtecksignal. Er muss mit 3,3 V betrieben werden können. Die Empfänger benötigen als Versorgungsspannung 2,5-5,5 V. Der Empfänger kann ohne weitere Bauelemente an einen ESP32 angeschlossen werden (GND, 3V3, GPIO). Das Ruhesignal (Idle-Level) liefert HIGH am GPIO.

IR-Empfänger

Beim Programmstart wird das RMT-Modul initialisiert (der ausführliche Code kann im Anschnitt Download herunter geladen werden):

constexpr uint8_t RMT_GPIO = 10; // Pin, an den der IR-Empfänger angeschlossen ist

rmt_data_t data[50];             // Feld zur Aufnahme der empfangenen Symbole
size_t data_symbols = RMT_SYMBOLS_OF(data); // Größe des Datenfeldes

constexpr uint16_t rxMaxThreshold = 32'767; // Maximal möglicher Wert (RMT_LL_MAX_IDLE_VALUE in rmt_ll.h)
constexpr uint16_t rxMinThreshold = 3;      // Max. Wert => 3'000 ns (Max ist 3187 ns)


void setup() {

   ...

   rmtInit(RMT_GPIO, RMT_RX_MODE, RMT_MEM_NUM_BLOCKS_1, 1'000'000));

   rmtSetRxMaxThreshold(RMT_GPIO, rxMaxThreshold); 

   rmtSetRxMinThreshold(RMT_GPIO, rxMinThreshold);

In loop wird auf den Empfang der Daten gewartet:

void loop() {
   data_symbols = RMT_SYMBOLS_OF(data); // data_symbols wurde beim Lesen verändert

   // Wenn Daten gelesen wurden, enthält data_symbols die Anzahl der tatsächlich gelesenen RMT - Symbole.
   // Um zu überprüfen, ob etwas gelesen wurde oder zu einer Zeitüberschreitung gekommen ist,
   // kann rmtReceiveCompleted() verwendet werden.
   rmtRead(RMT_GPIO, data, &data_symbols, 500);

   // Wenn etwas gelesen wurde, Daten verarbeiten.
   if (rmtReceiveCompleted(RMT_GPIO)) {
      Serial.printf("%i symbols\n", data_symbols);
      for (size_t i = 0; i < data_symbols; i++)
         Serial.printf("%4i\t%4i\n", data[i].duration0, data[i].duration1);
   }

   // Wegen Bug (?) im Modul ist nach jedem Lesen eine neue Initialisierung notwendig. Sonst:
   // E (28028) rmt: rmt_receive(401): channel not in enable state
   rmtDeinit(RMT_GPIO);
   rmtInit(RMT_GPIO, RMT_RX_MODE, RMT_MEM_NUM_BLOCKS_1, 1'000'000);
   rmtSetRxMaxThreshold(RMT_GPIO, rxMaxThreshold);
   rmtSetRxMinThreshold(RMT_GPIO, rxMinThreshold);
}

Dies ist der Output von einer Toshiba SE-R0336:

IR Output Toshiba SE-0336   Toshiba SE-R0336

Beispiel: Frequenzmessung

Das Programm ist prinzipiell auch geeignet, Frequenzen zu messen. Schließt man z.B. einen Fototransistor an, lässt sich z.B. die Trägerfrequenz ermitteln, mit der eine IR-Fernbedienung sendet.

Anschluss eines Phototransistors

Der Erwartungswert für die Pulsdauer bei einer Fernbedienung, die mit 38 kHz sendet, ist 26,3 μs. Die regelmäßigen Pulslängen werden mit 26 oder 27 μs erkannt. Die größeren Werte sind die Pausenzeiten die durch die Modulation der Trägerfrequenz entstehen.

Beispiel: IR-Decoder

Das folgende Beispiel liest Signale einer IR-Fernbedienung ein und dekodiert das Signal in eine 32-Bit-Zahl. Der Übersichtlichkeit wegen, sind die dazu notwendigen Funktionen in eine Klasse ausgelagert worden.

Klasse RmtDecoder

Die abstrakte Klasse RmtDecoder dient als Interface für spezielle Decoder-Klassen. Sie deklariert die virtuelle Funktion decode. Ihr wird die eingelesene Symbolfolge übergeben.

// Basisklasse für IR-Decoder
// Die Klasse dient als Interface
class RmtDecoder {
public:
   RmtDecoder() {}
   virtual DecoderResult decode(rmt_data_t* data, size_t data_symbols) = 0;
   virtual ~RmtDecoder() {}
};

decode liefert ein Objekt vom Typ DecoderResult zurück:

// Rückegabewert des Decoders
class DecoderResult {
public:
   DecoderErrorCodes ReturnCode; 
   uint32_t IrCode; // Ermittelter Code
   const char* Type; // Typ der Fernbedienung

   DecoderResult(DecoderErrorCodes ReturnCode, uint32_t IrCode, const char * Type) : ReturnCode(ReturnCode), IrCode(IrCode), Type(Type) {}

   // Liefert true, wenn die Decodierung erfolgreich war
   operator bool() const { return ReturnCode == DecoderErrorCodes::Success; }
};

Die DecoderErrorCodes sind in einer enum class zusasmmengefasst:

// Mögliche Fehlercodes des IR-Decoders
enum class DecoderErrorCodes {
   Success,                // Die Symbolfolge konnte erfolgreich dekodiert werden
   InvalidNumberOfSymbols, // Die erwartete Anzahl an Symbolen stimmt nicht
   InvalidSymbolDuration,  // Die erwartete Dauer der Pulse stimmt nicht
};

Konkrete Decoder-Klasse (RmtToshibaSER0336Decoder)

Die Klasse RmtToshibaSER0336Decoder versucht die eingelesene Symbolfolge als Codes einer Toschiba-SE-R0336-IR-Fernbedienung zu enschlüsseln.

class RmtToshibaSER0336Decoder : public RmtDecoder {
public:
   const	size_t expectedSymbols = 34;
   const char* Type = "Toshiba SE-R0336";

   virtual DecoderResult decode(rmt_data_t* data, size_t data_symbols);
};

expectedSymbols enthält die Anzahl der erwarteten Symbole für diese Fernbedienung.

Die Funktion decode prüft zunächst, ob die erwartete Anzahl an Symbolen korrekt ist. Danach, ob das Kopfsymbol die erwartete Länge aufweist. Anschließend werden die restlichen Symbole in eine Binärzahl decodiert. Dabei wird wieder die Pulsdauer überprüft.

DecoderResult RmtToshibaSER0336Decoder::decode(rmt_data_t* data, size_t data_symbols) {
   uint32_t code = 0;

   if (data_symbols != expectedSymbols)
      return DecoderResult(DecoderErrorCodes::InvalidNumberOfSymbols, 0, Type);

   if (data[0].duration0 < 8900 || data[0].duration0 > 9500)
      return DecoderResult(DecoderErrorCodes::InvalidSymbolDuration, 0, Type);
   if (data[0].duration1 < 4000 || data[0].duration1 > 5000)
      return DecoderResult(DecoderErrorCodes::InvalidSymbolDuration, 0, Type);

   for (int i = 1; i < data_symbols - 1; i++) {
      if (data[i].duration0 < 600 || data[i].duration0 > 700)
         return DecoderResult(DecoderErrorCodes::InvalidSymbolDuration, 0, Type);
      code = code << 1;
      if (data[i].duration1 < 1000) // Mark
         code += 1;
      else
         ; // Space
   }
   return DecoderResult(DecoderErrorCodes::Success, code, Type);
}

Der Rahmen Hauptprogramm funktioniert anlog zum vorherigen Beispiel. Die Auswertung der Symbolfolge geschieht jedoch durch die Decoder:

RmtDecoder* decoders[] = { new RmtMetzRM18Decoder, new RmtToshibaSER0336Decoder };
constexpr int decoderCount = sizeof(decoders) / sizeof(decoders[0]);

...

void loop() {
   data_symbols = RMT_SYMBOLS_OF(data); // data_symbols wurde beim Lesen verändert

   // Wenn Daten gelesen wurden, enthält data_symbols die Anzahl der tatsächlich gelesenen RMT - Symbole.
   // Um zu überprüfen, ob etwas gelesen wurde oder zu einer Zeitüberschreitung gekommen ist,
   // kann rmtReceiveCompleted() verwendet werden.
   rmtRead(RMT_GPIO, data, &data_symbols, 500);

   // Wenn etwas gelesen wurde, Daten verarbeiten.
   if (rmtReceiveCompleted(RMT_GPIO)) {
      int decoderIndex = 0;

      while (true) {
         DecoderResult rtc = decoders[decoderIndex]->decode(data, data_symbols);

         if (rtc) {
            Serial.printf("%.4X %.4X  %s \n", (unsigned int) (rtc.IrCode >> 16), (unsigned int) (rtc.IrCode & 0xFFFF), rtc.Type);
            break;
         }
         else {
            if (++decoderIndex == decoderCount)
               break;
            // Versuch mit nächstem Decoder
         }
      }
   }
}

Die bekannten Decoder werden in einem Array zusammengefasst und über dieses wird iteriert, bis ein Decoder ein positives Ergebnis liefert.

Weitere Hinweise zu den IR-Codes findet man u.a. bei https://ithub.com/crankyoldgit/IRremoteESP8266, insbesondere hier: https://github.com/crankyoldgit/IRremoteESP8266/tree/master/src

Download

Das ZIP-Archiv für Bibliothek ESP32-WS2812-Blink zum Download. Es enthält die Visual Studio / Visual Micro Projekte

Benutzer der Arduino-IDE verwenden nur die Code-Dateien (Doppelklick auf die .ino-Dateien).