nRF24L01 – 2.4 GHz Funkmodule

Über den Beitrag

Der nRF24L01 bzw. nRF24L01+ ist ein leistungsfähiger Transceiver (Transmitter und Receiver) für das 2.4 GHz ISM Frequenzband, den ihr mithilfe von Arduino Boards oder anderen Mikrocontrollern und der Bibliothek RF24 programmieren könnt. Die Bibliothek hat gute Beispielsketche, die aber insbesondere für Anfänger unter Umständen etwas schwer verdaulich sind. Ich habe diesen Beitrag geschrieben, um den Einstieg – hoffentlich – etwas zu erleichtern.

Das kommt auf euch zu:

Technische Eigenschaften der nRF24L01 Module

Die nRFL01 Serie stammt von der Firma Nordic Semiconductors. Ein Datenblatt für den nRF24L01(+) gibt es auf der Herstellerseite nicht oder nicht mehr. Als Dokumentation scheint lediglich eine vorläufige Produktspezifikation im Umlauf zu sein, zum Beispiel hier auf GitHub.

Die meisten von euch werden den nRFL24L01 nicht als IC, sondern als Modul einsetzen. Davon gibt es viele verschiedene Ausführungen. Besonders weit verbreitet sind diese Modelle:

Zwei nRF24L01 Module
Zwei nRF24L01 Module

Das obere, große Modul mit SMA Antenne wird meistens als „nRF24L01 + PA + LNA“ angeboten, wobei PA für Power Amplifier und LNA für Low Noise Amplifier steht. Es soll eine Reichweite von bis zu 800 Metern erreichen. Dafür verbraucht es in der Spitze stolze 115 Milliampere im Sende- und bis zu 45 Milliampere im Empfangsbetrieb.

Die „nRF24L01 + PA + LNA“ Module sind empfindlich gegenüber elektromagnetischer Strahlung. Wenn sie nicht funktionieren, dann versucht sie abzuschirmen. Ideal wäre ein Metallgehäuse. Es geht aber auch mit einer Schicht Alufolie, nur müsst ihr dann vorher eine Schicht Isoliermaterial auflegen, um Kurzschlüsse zu vermeiden. Siehe auch hier. Bei mir haben die Teile tatsächlich erst nach der Abschirmung funktioniert. Ich hätte sie schon beinahe entsorgt, bis ich den Hinweis mit der Abschirmung gefunden habe.

Die kleinen Module sind unproblematischer. Sie brauchen maximal um die 14 Milliampere. Dafür liegt ihre Reichweite bei 100 Metern (Verkäuferangabe!). Dazu weiter unten mehr. Eine Kombination der verschiedenen Module funktioniert auch.

Weitere technische Daten

  • Betriebsspannung: 1.9 – 3.6 Volt
  • Spannungstoleranz der I/O Pins: 5 Volt
  • Kanäle: 125 (2.400 – 2.525 GHz)
  • Datenrate: 250 kbit/s, 1 Mbit/s, 2 Mbit/s
  • Max. Ausgangsleistung: 0 dBm
  • Standby Stromverbrauch: 26 µA
  • FIFO (First In, First OUT) Puffer: 3 x 32 Bit
  • Kommunikation: per SPI

Pinout / Anschlüsse

Die nRF24L01 Module haben 8 Pins:

  • MISO/MOSI/SCK: SPI Anschlüsse
  • GND/VCC: Spannungsversorgung
  • CE: Chip Enable
  • CSN: Chip Select
  • IRQ: Interrupt Pin

Die Anordnung der Pins und die fehlende Beschriftung sind etwas unkomfortabel für Versuchsschaltungen. Wenn ihr es etwas bequemer haben wollt, dann besorgt ihr euch für wenig Geld Adapterplatinen. Diese besitzen, neben vernünftig beschrifteten Ausgängen, noch einen AMS1117-3.3 Volt Spannungsregler und Kondensatoren zur Spannungsstabilisierung. Zum Betrieb sind damit mindestens 4.6 Volt notwendig.

Falls ihr – so wie ich – die Bauteile lieber fest ins Breadboard stecken möchtet, könnt ihr alternativ einen ESP8266-ESP01 Adapter missbrauchen. Ihr müsst dann aber den Kondensator (ESD) entfernen und die Ausgänge umbenennen.

nRF24L01 Adapter – links: Selbstbau, rechts: kommerziell erhältlicher Adapter

Vorbereitungen

Anschluss an den Mikrocontroller

Für die Beispielsketche kam die folgende Schaltung zum Einsatz:

Schaltung für das nRF24L01 Modul
Schaltung für das nRF24L01 Modul

Als Kondensator kam ein 10 µF Elko zum Einsatz. Er kann aber durchaus noch größer ausfallen.

Lediglich für einen Sketch habe ich zusätzlich noch den Interruptpin IRQ mit dem Arduino Pin 2 verbunden.

Falls ihr ein „nRF24L01 + PA + LNA“ Modul verwendet, dann wird es auf der Transmitterseite mit der Stromversorgung über den 3.3 Volt Pin eng. Der Arduino UNO und der Nano liefern nur 50 Milliampere. Für den Testbetrieb über kurze Distanz könnt ihr die Sendeleistung herunterregeln, ansonsten solltet ihr eine potentere Stromquelle wählen.

Installation der RF24 Bibliothek

Ihr findet die RF24 Bibliothek über die Bibliotheksverwaltung der Arduino IDE. Sucht dort nach RF24:

RF24 Bibliothek
RF24 Bibliothek

Alternativ ladet ihr die Bibliothek hier von GitHub herunter.

Hinweise zu den Sketchen

Die RF24 Bibliothek verfügt über eine Reihe von Beispielsketchen, die die Funktionen des nRF24L01 veranschaulichen. Allerdings können die Beispiele auf den ersten Blick ein wenig verwirrend sein, da die Autoren den Transmitter- und den Receiverteil jeweils in einem Sketch zusammengefasst haben. Erst nach dem Programmstart wird dem jeweiligen Modul seine Rolle über den seriellen Monitor zugeordnet. Das ist eigentlich ziemlich cool, aber, wie zuvor erwähnt, macht es den Einstieg etwas schwerer. Ich habe deshalb für den Transmitter und den Receiver jeweils separate Sketche verfasst.

Einige meiner Beispielsketche erzeugen sowohl auf der Transmitter-, als auch auf der Receiverseite Ausgaben auf dem seriellen Monitor. Man kann natürlich zwischen den Ports hin- und herwechseln, einfacher ist es jedoch zwei Instanzen der Arduino IDE zu erzeugen. Das heißt, ihr ruft die Arduino IDE zweimal auf, ordnet jeder einen eigenen Port zu und könnt so gleichzeitig zwei serielle Monitore öffnen.

Minimalsketch

Im ersten Beispiel kommen zwei nRF24L01 Module zum Einsatz. Der eine übernimmt die Rolle als Transmitter, der andere dient als Receiver. Wenn der Receiver eine Nachricht erhält, schickt er eine Empfangsbestätigung (Acknowledgement) an den Transmitter zurück. Im ersten Beispiel merken wir nichts davon, ich wollte diesen Aspekt aber nicht unterschlagen.

Ein nRF24L01 Transmitter-Receiver Paar
Ein nRF24L01 Transmitter-Receiver Paar

Minimum Transmittersketch

Wir beginnen mit dem Transmittersketch:

#include <SPI.h>
#include <RF24.h>

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "1RF24"; // address / identifier

void setup() {
  radio.begin();
  radio.openWritingPipe(address); // set the address
  radio.stopListening(); // set as transmitter
}
void loop() {
  const char text[] = "Hi Receiver"; // max. 32 bytes
  radio.write(&text, sizeof(text));
  
  delay(2000);
}

 

Erklärungen zum Transmittersketch – Pipes und Adressen

Achtung – jetzt wird es vielleicht etwas verwirrend: Die Datenübertragung der nRF24L01 Module erfolgt über sogenannte Pipes (= Rohr / Leitung). Dabei handelt es sich um eine Art Kommunikationskanal. Allerdings sind die Pipes nicht zu verwechseln mit den „Channels“, zu denen wir noch kommen. Der nRF24L01 hat eine Pipe zum Schreiben (bzw. Senden). Zum Lesen (bzw. Empfangen) könnt ihr bis zu sechs Pipes einrichten.

Damit zwei nRF24L01 Module miteinander kommunizieren können, muss die Pipe des Transmitters („writing pipe“) und die Pipe – bzw. eine der Pipes – des Empfängers („reading pipe“) dieselbe Adresse haben. Diese besteht aus einer Kombination von 5 Bytes. Die Bezeichnung „Adresse“ ist vielleicht etwas irreführend, ich wollte aber von der Nomenklatur der Bibliothek bzw. des Datenblattes nicht abweichen. Ihr könnt euch die Adresse auch einfach als Kennung (Identifier) vorstellen. Nehmt das erst einmal so hin. Später wird es – hoffentlich – klarer.

Weitere Erklärungen zum Transmittersketch

Ihr bindet zunächst die notwendigen Bibliotheken ein und erzeugt dann mit RF24 radio(7, 8) ein RF24 Objekt mit dem Namen „radio“. Dabei legt ihr auch die CE und CSN Pins fest. Zusätzlich könntet ihr die SPI Taktrate übergeben – schaut dazu hier in die Dokumentation der Klasse. 

Mit radio.begin() wird das nRF24L01 Modul initialisiert. Mit openWritingPipe(address) öffnet ihr den Schreibkanal und legt die Adresse fest. Die Funktion stopListening() bringt das Modul in den Transmittermodus.

Eure Nachrichten versendet ihr in Häppchen von maximal 32 Byte. Ihr definiert sie als Character Array und übergebt sie der Funktion write(). Um Arbeitsspeicher zu sparen, übergebt ihr das Character Array als Referenz. Dazu stellt ihr dem Variablennamen den Adressoperator & voran.

Minimum Receiversketch

Kommen wir zum Receiversketch:

#include <SPI.h>
#include "RF24.h"

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "1RF24"; // address / identifier

void setup() {
  Serial.begin(115200);
  radio.begin();
  radio.openReadingPipe(0,address); // set the address for pipe 0
  radio.startListening(); // set as receiver
}

void loop() {
  if(radio.available()){
    char text[33] = {0}; 
    radio.read(&text, sizeof(text)-1);
    Serial.println(text);
  }
}

Die wesentlichen Unterschiede zum Transmittersketch sind:

  • openReadingPipe(0, address) öffnet den Lesekanal 0 und legt die Adresse fest. Da es mehr als einen Lesekanal gibt, müsst ihr ihn spezifizieren. Die möglichen Werte sind 0 bis 5. Aus Gründen, die erst später klar werden, könntet ihr in diesem Beispiel nur 0 oder 1 wählen.
  • startListening() bringt den nRF24L01 in den Receivermodus.
  • available() prüft, ob eine Nachricht empfangen wurde.
  • read() liest die Nachricht. Ihr müsst die Variable übergeben, in der die Nachricht gespeichert wird und die Anzahl der zu lesenden Bytes.
    • Die Variable text ist ein Character Array. Character Arrays enden mit dem unsichtbaren Nullzeichen '\0'. Deshalb hat text eine Länge von 33 und nicht 32.

Die Ausgabe des Receiversketches ist keine große Überraschung:

nRF24L01 - Output minimum_receiver_sketch.ino
Output minimum_receiver_sketch.ino

Erweiterte Sketche

Die folgenden zwei Sketche bewirken im Prinzip dasselbe wie die Minimalsketche. Nachrichten werden vom Transmitter zum Receiver gesendet und dort ausgegeben. Die Sketche sind aber um diverse Funktionen erweitert. Einige dieser Funktionen ändern die Standardeinstellungen, andere bieten mehr Sicherheit.

Erweiterter Transmittersketch

Hier zunächst der Transmittersketch:

#include <SPI.h>
#include <RF24.h>

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "1RF24"; // address / identifier

void setup() {
  Serial.begin(115200);
  if(!radio.begin()){
    Serial.println("nRF24L01 module not connected!");
    while(1){}
  }
  else 
    Serial.println("nRF24L01 module connected!");
  
  /* Set the data rate:
   * RF24_250KBPS: 250 kbit per second
   * RF24_1MBPS:   1 megabit per second (default)
   * RF24_2MBPS:   2 megabit per second
   */
  radio.setDataRate(RF24_2MBPS);

  /* Set the power amplifier level rate:
   * RF24_PA_MIN:   -18 dBm
   * RF24_PA_LOW:   -12 dBm
   * RF24_PA_HIGH:   -6 dBm
   * RF24_PA_MAX:     0 dBm (default)
   */
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side

  /* Set the channel x with x = 0...125 => 2400 MHz + x MHz 
   * Default: 76 => Frequency = 2476 MHz
   * use getChannel to query the channel
   */
  radio.setChannel(0);
  
  radio.openWritingPipe(address); // set the address
  radio.stopListening(); // set as transmitter

  /* You can choose if acknowlegdements shall be requested (true = default) or not (false) */
  radio.setAutoAck(true);
 
  /* with this you are able to choose if an acknowledgement is requested for 
   * INDIVIDUAL messages.
   */
  radio.enableDynamicAck(); 

  /* setRetries(byte delay, byte count) sets the number of retries until the message is
   * successfully sent. 
   * Delay time = 250 µs + delay * 250 µs. Default delay = 5 => 1500 µs. Max delay = 15.
   * Count: number of retries. Default = Max = 15. 
   */
  radio.setRetries(5,15);

  /* The default payload size is 32. You can set a fixed payload size which must be the
   * same on both the transmitter (TX) and receiver (RX)side. Alternatively, you can use 
   * dynamic payloads, which need to be enabled on RX and TX. 
   */
  //radio.setPayloadSize(11);
  radio.enableDynamicPayloads();
}
void loop() {
  const char text[] = "Hi Receiver";
  Serial.println(sizeof(text));  
  if(radio.write(&text, sizeof(text)-1, 0)){ // 0: acknowledgement request, 1: no ack request
    Serial.println("Message successfully sent");
  }
  delay(2000);
}

 

Dazu einige Erklärungen:

  • begin() kennt ihr schon. Aber hier prüfen wir mit dem Rückgabewert (true / false), ob das nRF24L01 Modul verbunden ist. 
  • Mit setDataRate() legt ihr die Datenübertragungsrate fest. Es stehen drei Optionen zur Wahl: 250 kbit/s, 1 Mbit/s oder 2 Mbit/s. Zu beachten: Transmitter und Receiver müssen dieselbe Einstellung haben!
  • setPALevel() bestimmt die Verstärkungsstufe. Ihr könnt -18, -12, -6 oder 0 dBm einstellen. Je größer der Wert, desto größer die Reichweite. Dasselbe gilt aber auch für den Strombedarf.
  • Die Funktion setChannel(channel) eröffnet euch die Möglichkeit, die Sendefrequenz zu ändern. Die Frequenz ist 2400 MHz + channel * 1 MHz mit channel = 0 bis 125. Sender und Empfänger müssen auf dieselbe Frequenz eingestellt sein. Voreinstellung ist 76.
  • Mit setAutoAck(true/false) legt ihr fest, ob der Transmitter eine Empfangsbestätigung anfordern soll oder nicht. Voreinstellung ist „true“.
  • enableDynamicAck() ermöglicht euch, für jeden write() Befehl individuell festzulegen, ob eine Empfangsbestätigung geschickt werden soll oder nicht.
  • Wird eine Empfangsbestätigung angefordert, aber sie kommt nicht, sendet der nRF24L01 die Nachricht erneut. Die Zahl der Wiederholungen (count) und die Zeit zwischen den Wiederholungen (delay) steuert ihr mit setRetries(delay, count). Dabei ist delay eine Zahl zwischen 0 und 15 und die resultierende Verzögerung ist 250 µs + delay * 250 µs. Die Voreinstellung ist setRetries(5, 15).
  • setPayloadSize(size) legt die Länge (size) der Nachricht (die/der Payload) fest. Voreingestellt ist size = 32. Ihr müsst auf der Sender- und Empfängerseite denselben Wert einstellen.
  • Alternativ haltet ihr die Länge der Nachricht mit enableDynamicPayloads() variabel.
  • write() kennt ihr schon, aber:
    • Mit dem Parameter sizeof(text)-1 übergeben wir das Character Array ohne den abschließendes Nullzeichen. Auf der Receiverseite können wir ihn wieder anhängen. Ihr könnt den Nullstring natürlich auch mit übergeben – aber wozu?
    • Wir übergeben einen dritten Parameter, der steuert, ob eine Empfangsbestätigung erfolgen soll. 0 ist mit, 1 ist ohne Empfangsbestätigung. Genau andersherum, als man es erwarten würde.
    • Wir nutzen den Rückgabewert von write(), um zu prüfen, ob die Datenübertragung erfolgreich abgeschlossen wurde. Das funktioniert natürlich nur, wenn auch eine Empfangsbestätigung angefordert wird.

Erweiteter Receiversketch

Und hier der Receiver Sketch:

#include <SPI.h>
#include "RF24.h"

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "1RF24"; // address / identifier
void setup() {
  Serial.begin(115200);
  if(!radio.begin()){
    Serial.println("nRF24L01 module not connected!");
    while(1){}
  }
  else 
    Serial.println("nRF24L01 module connected!");

  /* Set the data rate:
   * RF24_250KBPS: 250 kbit per second
   * RF24_1MBPS:   1 megabit per second
   * RF24_2MBPS:   2 megabit per second
   */
  radio.setDataRate(RF24_2MBPS);

  /* Set the power amplifier level rate:
   * RF24_PA_MIN:   -18 dBm
   * RF24_PA_LOW:   -12 dBm
   * RF24_PA_HIGH:   -6 dBm
   * RF24_PA_MAX:     0 dBm (default)
   */
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side 
   
   /* Set the channel x with x = 0...125 => 2400 MHz + x MHz 
   * Default: 76 => Frequency = 2476 MHz
   * use getChannel to query the channel
   */
  radio.setChannel(0);
  
  radio.openReadingPipe(0,address); // set the address
  radio.startListening(); // set as receiver

  /* The default payload size is 32. You can set a fixed payload size which 
   * must be the same on both the transmitter (TX) and receiver (RX)side. 
   * Alternatively, you can use dynamic payloads, which need to be enabled 
   * on RX and TX. 
   */
  //radio.setPayloadSize(11);
  radio.enableDynamicPayloads();
}

void loop() {
  if(radio.available()){
    byte len = radio.getDynamicPayloadSize();
    Serial.println(len); //just for information
    char text[len+1] = {0}; 
    radio.read(&text, len);
    Serial.println(text);
  }
}

 

Zum Receiversketch gibt es weniger zu sagen:

  • Die Einstellungen für die Payload müssen mit denen des Transmitters übereinstimmen.
  • getDynamicPayloadSize() fragt die Größe der Payload in Bytes ab.
  • Da wir das Character Array ohne Nullzeichen übergeben haben, fügen wir ihn durch die Definition char text[len+1] = {0} wieder hinzu.

Und hier die Ausgabe. Wie erwartet wurden 11 Bytes empfangen:

Output receiver_extended.ino
Output receiver_extended.ino

Der nRF24L01 als MultiCeiver™

Als Receiver kann der nRF24L01 gleichzeitig auf sechs Transmitter „hören“. Allerdings gilt die Einschränkung, dass immer nur eine Nachricht zu einem gegebenen Zeitpunkt erhalten werden kann. Hier kommen die Wiederholungen ins Spiel.

Ein Receiver ("Multiceiver") für 6 Transmitter
Ein Receiver („Multiceiver“) für 6 Transmitter

Wir beginnen diesmal mit der Receiverseite. Der nRF24L01 nutzt hier seine sechs Pipes. Jede der Pipes bekommt eine individuelle Reading Pipe Adresse, die mit der Writing Pipe Adresse des korrespondierenden Transmitters übereinstimmen muss. 

Jetzt wird es noch einmal etwas verwirrend: Nur die Adresse der Pipe 0 besteht aus individuellen 5 Bytes. Bei den Adressen der Pipes 1 bis 5 unterscheidet sich nur das Byte 0. Diese Adressen „teilen“ sich die Bytes 1 bis 4 der Adresse von Pipe 1. Das Schema lautet also:

  • Adresse 0 ist „abcde“.
  • Adresse 1 bis 5 ist: „fghij“, „kghij“, „lghij“, „mghij“, „nhij“.
  • Dabei stehen a bis n für jedes druckbare Zeichen.

Und dieses System begründet auch, warum ihr beim minimum_receiver_sketch nur die Reading Pipe 0 oder 1 wählen konntet (ihr erinnert euch?): Wenn die Adresse der Pipe 1 nicht definiert ist, sind es die Bytes 1 – 4 der Adressen der Pipes 2 – 5 auch nicht.

Hier nun der Receiversketch:

#include <SPI.h>
#include "RF24.h"

RF24 radio(7, 8); // (CE, CSN)

const byte address[][6] = {"0Base", "1RF24", "2RF24", "3RF24", "4RF24", "5RF24"};

void setup() {
  Serial.begin(115200);
  radio.begin();
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side 

  for(int i=0; i<6; i++){
    radio.openReadingPipe(i, address[i]);
  }
  radio.startListening(); // set as receiver
}

void loop() {
  byte pipe; 
  if(radio.available(&pipe)){
    char receivedText[33] = {0}; 
    radio.read(&receivedText, sizeof(receivedText));
    Serial.print("Received on pipe ");
    Serial.print(pipe);
    Serial.print(": ");
    Serial.println(receivedText);
  }
}

Neu ist bei diesem Sketch, dass der Funktion available() die Variable pipe (als Referenz) übergeben wird. Das erlaubt uns zu prüfen, über welche Pipe die Nachricht eingetroffen ist. Alle anderen Funktionen wurden schon besprochen.

Und nun zum Transmittersketch für das Modul 0. Um ihn für die anderen fünf Transmittermodule anzupassen, müsst ihr lediglich in Zeile 8 die Transmitter ID tx_id abändern.

#include <SPI.h>
#include <RF24.h>

RF24 radio(7, 8); // (CE, CSN)

const byte address[][6] = {"0Base", "1RF24", "2RF24", "3RF24", "4RF24", "5RF24"};

byte tx_id = 0; // max. 6 TX: 0...5
                        
void setup() {
  Serial.begin(115200);
  radio.begin();
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side 

  radio.openWritingPipe(address[tx_id]); // set the address
  radio.stopListening(); 
  radio.setRetries(((tx_id * 3) % 12) + 3, 15);
}
void loop() {
  char text[32] = {0};
  strcpy(text, "Message from TX ");
  char buf[2] ={0};
  itoa(tx_id, buf, 10);
  strcat(text, buf);
  Serial.println(text);
  
  if(radio.write(&text, sizeof(text))){
    Serial.println("Message successfully sent");
  }
  delay(3000);
}

Noch ein paar Anmerkungen. Die eigentümliche Konstruktion für das Retry-Delay in Zeile 17 soll sicherstellen, dass die Wiederholungen von den Modulen in unterschiedlichen Abständen gesendet werden. Dadurch werden Kollisionen vermieden (genauer gesagt: wiederholte Kollisionen).

Der größte Teil des Codes in loop() dient der Zusammensetzung der Nachricht:

  • strcpy(text, "Message from TX ") kopiert „Message from TX “ in text.
  • itoa(tx_id, buf, 10)  kopiert den Integerwert tx_id als Zeichenkette in das Character Array buf unter Verwendung des Dezimalsystems.
  • strcat(text, buf) hängt buf an das Ende von text.

Wenn ihr Floats versenden wollt, dann könntet ihr die Funktion dtostrf() verwenden:

  • dtostrf(float_value, min_width, num_digits_after_decimal, target). Dabei ist min_width die Mindestbreite (>=4), num_digits_after_decimal ist die Anzahl der Stellen hinter dem Komma und target ist das Character Array, in das ihr euren Wert schreibt.
nRF24L01 - Output multiceiver_receiver.ino
Output multiceiver_receiver.ino

Anmerkung zum MultiCeiver Beispielsketch der Bibliothek

Wenn ihr euch den Beispielsketch MulticeiverDemo.ino der RF24 Bibliothek anschaut, dann werdet ihr dort vielleicht über die Adress-Definition stolpern:

uint64_t address[6] = { 0x7878787878LL,
                        0xB3B4B5B6F1LL,
                        0xB3B4B5B6CDLL,
                        0xB3B4B5B6A3LL,
                        0xB3B4B5B60FLL,
                        0xB3B4B5B605LL };

Die Adressen werden in diesem Sketch als Array von 6 Ganzzahlen mit einer Größe von je 8 Byte definiert. Das nimmt etwas mehr Platz ein als die sechs Arrays von je 6 Byte. Das ist aber noch nicht der Punkt. Vielmehr könnte die Frage aufkommen, warum sich bei den Adressen 1 bis 5 das rechts stehende Byte unterscheidet und nicht das links stehende. Die Antwort ist, dass das Byte 0 einer Ganzzahl ganz rechts steht, wohingegen sich das 0te Element eines Arrays links befindet.

Die Rolle des nRF24L01 wechseln

Die Rolle des nRF24L01 als Transmitter oder Receiver könnt ihr während des laufenden Programmes wechseln. Der folgende Sketch macht aus dem nRF24L01 alle zwei Sekunden einen Transmitter, der eine Nachricht versendet. Wenn die Nachricht erfolgreich gesendet wurde, gibt es eine entsprechende Erfolgsmeldung und der Transmitter wird zum Receiver. Wenn der nRF24L01 als Receiver eine Nachricht erhalten hat, dann wird diese ausgegeben und er wird wieder zum Transmitter.

Das bedeutet, dass das Modul, welches mit diesem Sketch läuft, den Takt angibt. Deshalb habe ich den Sketch „leading_transceiver.ino“ genannt:

#include <SPI.h>
#include <RF24.h>

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "_no_1"; // address / identifier

void setup() {
  Serial.begin(115200);
  radio.begin();
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side 

  radio.openWritingPipe(address); // set the address
  radio.openReadingPipe(1,address);
}
void loop() {
  unsigned long int sendingPeriod = 2000;
  static unsigned long int lastSend = 0;
  const char sendText[] = "Hi Follower";

  if((millis()-lastSend)>sendingPeriod){
    radio.stopListening(); // set as transmitter
    lastSend = millis();
    if(radio.write(&sendText, sizeof(sendText))){
      Serial.println("Message successfully sent");
      radio.startListening();
    }
  }
  if(radio.available()){
    char receivedText[33] = {0}; 
    radio.read(&receivedText, sizeof(receivedText));
    Serial.print("Message received: ");
    Serial.println(receivedText);
    radio.stopListening();
  }
}

 

Das Modul auf der anderen Seite startet als Receiver. Empfängt es eine Nachricht, wird sie ausgegeben und das Modul wird zum Transmitter. Als Transmitter sendet es ein Nachricht zurück und wird wieder zum Receiver.

Wenn es dumm läuft, könnte der Fall eintreten, dass beide Module bis in alle Ewigkeit aufeinander warten, ohne dass etwas passiert. Dafür habe ich die Zeilen 18 – 20 eingefügt. Einmal pro Sekunde wird das Modul daran erinnert, dass es die Receiverrolle übernehmen soll.

#include <SPI.h>
#include "RF24.h"

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "_no_1"; // address / identifier
void setup() {
  Serial.begin(115200);
  radio.begin();
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side 
   
  radio.openReadingPipe(0,address); // set the address
  radio.openWritingPipe(address);
  radio.startListening(); // set as receiver
}

void loop() {
  if((millis()%1000) == 0){
    radio.startListening();  // gentle reminder to listen
  }
  if(radio.available()){
    char receivedText[33] = {0}; 
    radio.read(&receivedText, sizeof(receivedText));
    Serial.print("Message received: ");
    Serial.println(receivedText);
    
    radio.stopListening();
    const char sendText[] = "Hi Leader";
    if(radio.write(&sendText, sizeof(sendText))){
      Serial.println("Message successfully sent");
      radio.startListening();
    }
  }
}

 

Erweiterte Acknowledgements

Im vorherigen Beispiel war das eine Modul vorwiegend Transmitter und das andere vorwiegend Receiver. Der Receiver sollte nur dann eine Nachricht zurücksenden, wenn er zuvor eine Nachricht vom Transmitter erhalten hatte. Für diese Konstellation gibt es eine einfachere Lösung, und zwar könnt ihr Daten mit dem Acknowledgement versenden. Huckepack, sozusagen. Der Vorteil ist, dass ihr die Rolle der Module nicht wechseln müsst.

Die Funktion, die dieses Feature aktiviert, ist enableAckPayload(). Sie muss sowohl auf der Transmitter-, als auch auf der Receiverseite aufgerufen werden. Ob eine Antwort des Receivers vorliegt, prüft ihr mit available(). Ist das der Fall, dann könnt ihr die Nachricht wie gewohnt mit read() lesen.

#include <SPI.h>
#include <RF24.h>

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "_no_1"; // address / identifier

void setup() {
  Serial.begin(115200);
  radio.begin();
  
  radio.openWritingPipe(address); // set the address for writing
  radio.openReadingPipe(1, address);  // set the address for reading
  radio.stopListening(); // set as transmitter
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side 

  radio.enableDynamicPayloads();
  radio.enableAckPayload();
}
void loop() {
  unsigned long int sendingPeriod = 2000;
  static unsigned long int lastSend = 0;
  const char text[] = "Hi Receiver";

  if((millis()-lastSend)>sendingPeriod){
    lastSend = millis();
    if(radio.write(&text, sizeof(text))){ 
      Serial.println("Message successfully sent");
    }
  }
  if(radio.available()){
    Serial.print("Received: ");
    byte len= radio.getDynamicPayloadSize();
    char ackPayload[len] = {0};
    radio.read(&ackPayload, len);
    Serial.println(ackPayload);
  }
}

 

Und so sieht die Receiverseite aus:

#include <SPI.h>
#include "RF24.h"

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "_no_1"; // address / identifier

void setup() {
  Serial.begin(115200);
  radio.begin();
  
  radio.openReadingPipe(0,address); // set the address for writing
  radio.openWritingPipe(address); // set the address for reading
  radio.startListening(); // set as receiver
  radio.setPALevel(RF24_PA_LOW); // sufficient for tests side by side
 
  radio.enableDynamicPayloads();
  radio.enableAckPayload();
  char ackPayload[] = "First Answer";
  radio.writeAckPayload(0, &ackPayload, sizeof(ackPayload));
}

void loop() {
  static float counter = 0.0;
  if(radio.available()){
    byte len = radio.getDynamicPayloadSize();
    char text[len+1] = {0}; 
    radio.read(&text, sizeof(text));
    Serial.println(text);
    
    //Acknowledgement Payload:
    char ackPayload[16];
    strcpy(ackPayload, "counter ");
    char number[12] = {0};
    dtostrf(counter, 4, 2, number);
    Serial.println(number);
    strcat(ackPayload, number);
    Serial.println(ackPayload);
    counter += 1.0;
    radio.writeAckPayload(0, &ackPayload, sizeof(ackPayload));
  }
}

 

Die Antwort an den Transmitter schreibt ihr mit writeAckPayload(pipe, text, size) in den FIFO. Sie wird nach Erhalt der Nachricht sofort gesendet. D.h. ihr müsst die Antwort formulieren, bevor ihr die Nachricht bekommt. Wenn die Antwort zum Beispiel den Messwert eines Sensors beinhaltet, dann ist dieser nicht ganz aktuell. Entweder ihr lebt damit, oder:

  • Ihr schreibt regelmäßig ein Update in den FIFO.
  • Der Transmitter sendet seine Nachricht zweimal kurz hintereinander und ihr verwerft jeweils die erste Antwort.
  • Ihr geht doch zur Methode zurück, bei der ihr die Rollen wechselt.

Damit es nicht so langweilig ist und immer dieselbe Receiverantwort kommt, sendet der Receiver in diesem Beispiel einen Zählerstand zurück. Hier kommt auch dtostrf() zum Einsatz.

Das ist die Ausgabe der Transmitterseite:

Output transmitter_ack_payloads.ino
Output transmitter_ack_payloads.ino

Interrupts des nRF24L01 nutzen

Der nRF24L01 kann drei Ereignisse durch einen Interrupt melden:

  • Daten wurden versendet.
  • Die Datenübertragung ist schiefgegangen.
  • Es liegen Daten zum Abruf aus dem FIFO bereit.

Um die Interrupts ein- oder auszuschalten, verwendet ihr die Funktion maskIRQ(data_sent, data_fail, data_ready). Eine „0“ für data_send, data_fail oder data_ready aktiviert den Interrupt, eine „1“ maskiert ihn. 

Der IRQ Pin ist LOW-aktiv. Das heißt, ihr solltet den Interrupt am Arduino auf FALLING einstellen.

Falls ihr mehrere der drei Interruptauslöser aktiviert habt, könnt ihr mit whatHappened(data_sent, data_fail, data_ready) herausfinden, welcher den letzten Interrupt verursacht hat. Der „schuldige“ Parameter hat den Wert 1.

Der folgende Sketch nutzt den Data Ready Interrupt, um anzuzeigen, dass Daten empfangen wurden. Entsprechend kann auf die Abfrage if radio.available() verzichtet werden. Dieses spezielle Beispiel bringt keinen wirklichen Vorteil, sondern dient nur der Anschauung. Eine sinnvollere Nutzung des Data Ready Interrupts wäre beispielsweise, den Mikrocontroller aus dem Schlaf zu wecken.

#include <SPI.h>
#include "RF24.h"
#define IRQ_PIN 2 
volatile bool event = false;

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "_no_1"; // address / identifier

void setup() {
  Serial.begin(115200);
  radio.begin();
  radio.openReadingPipe(0,address); // set the address
  radio.startListening(); // set as receiver
  pinMode(IRQ_PIN, INPUT);
  attachInterrupt(digitalPinToInterrupt(IRQ_PIN), interruptHandler, FALLING);
  // let IRQ pin only trigger on "data ready" event in RX mode
  radio.maskIRQ(1, 1, 0);  // args = "data_sent", "data_fail", "data_ready"
}

void loop() {
  if(event){
    //if(radio.available()){
      char text[33] = {0}; 
      radio.read(&text, sizeof(text)-1);
      Serial.println(text);
    //}
    event = false;
  }
}

void interruptHandler(){
  event = true;
  bool tx_ds, tx_df, rx_dr;                 // declare variables for IRQ masks
  radio.whatHappened(tx_ds, tx_df, rx_dr);  // get values for IRQ masks

  Serial.print("Data_sent: ");
  Serial.print(tx_ds);  // print "data sent" mask state
  Serial.print(", Data_fail: ");
  Serial.print(tx_df);  // print "data fail" mask state
  Serial.print(", Data_ready: ");
  Serial.println(rx_dr);  // print "data ready" mask state
}

 

Und dies ist die Ausgabe:

Output receiver_IRQ.ino

Weitere Funktionen

Ich habe in meinen Beispielsketchen noch nicht alle Funktionen abgedeckt. Auf ein paar der fehlenden Funktionen möchte ich noch aufmerksam machen, da ich sie für besonders sinnvoll halte. Ich gehe aber nur oberflächlich auf sie ein.

  • writeFast() ähnelt write(). write() schreibt die Nachricht in den FIFO und wartet, bis sie versendet ist. writeFast() hingegen wartet nicht, sondern weitere Nachrichten können in den FIFO Puffer geschrieben werden, bis alle drei FIFOs voll sind. Erst dann blockiert das Programm, bis wieder FIFO-Kapazität frei ist. Bei großen Datenmengen gibt das einen gewissen Geschwindigkeitsvorteil.
  • powerDown() versetzt euren nRF24L01 in den Tiefschlaf, in welchem er nur 0.9 µA Strom verbraucht. powerUp() ist das Gegenstück dazu.
  • flush_rx() und flush_tx() löschen den FIFO des Receivers bzw. des Transmitters.
  • printDetails(), printPrettyDetails() und sprintfPrettyDetails() sind nützlich für das Debugging. Die Funktionen zeigen Status und Einstellungen des nRF24L01 an.

Weitere Informationen zu diesen und weiteren Funktionen findet ihr in der Klassendokumentation oder in den Beispielsketchen der Bibliothek.

Indoor-Reichweitentest

Ich habe einen Reichweitentest bei mir im Haus durchgeführt. Ohne Barriere mag man die maximalen Reichweiten vielleicht erreichen, drinnen geht das wegen der Wände natürlich nicht. Auch stört unter Umständen Fremdstrahlung wie das WLAN.

Ich habe für den Test den maximalen PA-Level gewählt und die niedrigste Datenübertragungsrate. Sowohl auf der Transmitter-, als auch auf der Receiverseiter kamen jeweils zwei Spannungsversorgungen zum Einsatz. Der Arduino Nano wurde mit einer 9 Volt Blockbatterie über VIN versorgt. Das nRF24L01 Modul habe ich mit einem Lithium-Ionen-Akku betrieben. Letzteres ist nicht generell zu empfehlen, da der Akku frisch geladen bis 4.2 Volt liefert. Meine Akkus waren schon etwas entladen. Mit ~3.8 Volt lagen sie ein wenig über der Spezifikationsgrenze von 3.6 Volt. Für kurze Zeit ist das vertretbar. Der Spannungsversorgung des nRF24L01 habe ich noch einen fetten Elko mit 470 µF spendiert.

Stellvertretend ist hier der Receiversketch:

#include <SPI.h>
#include "RF24.h"
const int ledPin = 6;

RF24 radio(7, 8); // (CE, CSN)

const byte address[6] = "_no_1"; // address / identifier

void setup() {
  pinMode(ledPin, OUTPUT);
  radio.begin();
  radio.openReadingPipe(0,address); // set the address
  radio.startListening(); // set as receiver
  radio.setPALevel(RF24_PA_MAX);
  radio.setDataRate(RF24_250KBPS);
}

void loop() {
  if(radio.available()){
    char text[33] = {0}; 
    radio.read(&text, sizeof(text)-1);
    String textString = String(text);
    if(textString == "Hi Receiver"){
      digitalWrite(ledPin, HIGH);
      delay(300);
      digitalWrite(ledPin, LOW);
    }
  }
}

Den Transmitter habe ich in einer Ecke des Hauses positioniert. Er sendete alle zwei Sekunden die Nachricht „Hi Receiver“. Bei korrekter Übertragung blinkte die LED auf der Receiverseite kurz auf. So bin ich dann mit dem Receiver durchs Haus gelaufen und habe geprüft, wo die LED blinkte und wo nicht (was meine Familie kopfschüttelnd zur Kenntnis nahm).

Ergebnis für das Standardmodul

Im Nebenraum war der Empfang problemlos. Noch einen Raum weiter gab es nur Empfang in der Nähe der Tür. Wie groß die Reichweite in Innenräumen ist, hängt natürlich in hohem Maße von der Bausubstanz des Hauses ab. Ein besseres Maß ist vielleicht das folgende: Die Reichweite war in etwa vergleichbar mit der Reichweite des 2.4 GHz WLAN Netzes meines Routers (Fritz!Box 7590). Dieses dringt auch nicht durch das ganze Haus und muss durch einen Repeater unterstützt werden. Wenn ihr also quer durch eure Wohnung oder euer Haus funken wollt, dann geht nicht unbedingt davon aus, dass das mit den Standardmodulen funktioniert.

Ergebnis für das „nRF24L01 + PA + LNA“ Modul

Wie eingangs erwähnt, musste ich die „nRF24L01 + PA + LNA“ Module erst einmal mit Alufolie abschirmen, damit sie überhaupt funktionierten (ja, hier nützen Aluhüte etwas 😉 ). Dann aber war die Reichweite sehr viel besser als die der Standardmodule. Probleme gab es erst beim Senden aus dem Keller zum Dachboden.

6 thoughts on “nRF24L01 – 2.4 GHz Funkmodule

  1. Hallo Wolfgang,
    vielen Dank für diesen sehr schönen Beitrag! Obwohl ich die nRF24-Module schon in mehreren Projekten verwendet habe, muss ich zugeben, mich noch nicht mit allen möglichen Konfigurationen die der nRF24 in Zusammenarbeit mit der Library von TMRh20 bietet auseinandergesetzt zu haben.
    Das Interessante an diesem Modul ist die Kombination aus geringem Schlafstrom, hohe Geschwindigkeit beim Aufwachen, Senden, und wieder schlafen gehen, sowie der moderate Strombedarf beim senden, so daß man Projekte mit einer Knopfzelle über Jahre zum laufen bekommt.
    https://www.arduinoforum.de/arduino-Thread-SensEgg1-Temperatursensor-im-%C3%9C-Ei-ATtiny84-nRF24-NTC
    https://www.arduinoforum.de/arduino-Thread-SensEgg3-FunkSensor-im-%C3%9C-Ei-ATmega168PA-nRF24-BME280-NTC
    https://www.arduinoforum.de/arduino-Thread-Door-Sens-Low-Power-Funk-ReedSensor

    Ich hoffe Du hast nichts gegen die Lings meiner Projekte, ansonsten schmeiße die einfach raus!

    Gruß André

    1. Hallo André,

      vielen Dank für deine Einschätzung und die Links, die ich gerne teile!

      VG, Wolfgang

  2. Hallo Wolfgang

    wie immer sehr ausführlich und umfangreich erklärt – vielen Dank.

    Ich frage mich allerdings, welche Vorteile habe ich mit diesen Modulen gegenüber ESP-NOW?
    Link: https://randomnerdtutorials.com/esp-now-esp32-arduino-ide/

    Beides sendet auf 2,4GHz, die Sendeleistung ist in etwa gleich, mit dem Unterschied, daß ich mit ESP-NOW kein zusätzliches Funkmodul benötige und mehr als sechs Kommunikationspartner einrichten kann.

    Zudem Frage ich mich auch noch, ob das 2,4GHz Netz mit WiFi und Bluetooth – die ja in den Haushalten immer mehr werden – nicht langsam zu voll wird.
    Wäre es nicht sinnvoller wieder auf 433MHz oder 868MHz zu gehen?

    Gruß
    Robert

    1. Hallo Robert,

      danke für den Kommentar und die Frage. Nur, weil ich über bestimmte Bauteile berichte, heißt das nicht unbedingt, dass ich ein großer Freund von ihnen bin oder sage, dass es nichts Besseres gäbe. Ich selbst bin eher ein Fan von 433 MHz Modulen, insbesondere dem HC-12 Modul. ESP-NOW ist in der Tat interessant, vielleicht berichte ich darüber auch noch einmal.

      VG, Wolfgang

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert