In diesem Artikel beschäftigen wir uns mit NRF24L01+ Funkmodulen. Diese kleinen Funkmodule sind bereits für weniger als 2 Euro (inkl. Versand) bei eBay erhältlich. Wer die dann lange Lieferzeit nicht in Kauf nehmen möchte, da kann für 2…3 Euro mehr die Module auch bei Händlern aus Deutschland beziehen. Bei den Funkmodulen handelt es sich um 2,4 GHz Sende- und Empfangsmodule in einem Chip. Das bedeutet, dass jedes Modul sowohl zum Senden als auch zum Empfangen von Daten verwendet werden kann.
Einleitung
Bevor wir beginnen, solltet Ihr euch zumindest schon einmal das Datenblatt des Funkmoduls herunterladen. Dieses ist recht ausführlich und gut geschrieben.
Die beiden Fotos, siehe oben, zeigen handelsübliche NRF24L01+ Module, wie sie z.B. bei eBay zu beziehen sind. Wir möchten uns in diesem Artikel damit beschäftigen eine komplette Funkstrecke, bestehend aus Sender und Empfänger mit Hilfe der Module aufzubauen. Dabei lehnt sich der Artikel stark an das Tutorial an, das auf Mikrocontroller.net zu finden ist. Dieses Tutorial ist lediglich an PIC-Mikrocontroller angepasst und bietet eine aufgeräumte Variante der Ansteuerungs-Routinen zur Verfügung.
Hardwareaufbau
Das Wichtigste zuerst: Die Module vertragen keine 5 Volt. Ihr Arbeitsbereich liegt zwischen 1,9V und 3,6V. Ich betreibe die Module mit 3,3V. Die Ansteuerung der Module wird mit dem SPI-Bus realisiert. Zusätzlich zu den SPI üblichen Signalen (MOSI, MISO, CLK und CSN*) haben die Funkmodule noch einen IRQ-Ausgang und einen CE-Eingang. Alle sechs Signalleitungen sowie Vcc und GND müssen verschaltet werden. Eine Beschaltung der Funkmodule ist dem folgenden Schaltplan zu entnehmen. * CSN: Chip select not – Es handelt sich um die normale low aktive Chip-Select Leitung des SPI-Bus
Pinbelegung der Stiftleiste auf dem Modul:
Software
Nachdem nun zwei mal der oben gezeigte Aufbau realisiert wurde (zum Beispiel auf einem Steckbrett), können wir uns mit der Software auseinandersetzen. Auch wenn es im Grunde genommen selbstverständlich ist, möchte ich dennoch kurz anmerken, dass man sich die Möglichkeit schaffen sollte die Funkstrecke zu verifizieren. Das geht zum Beispiel mit einem LC-Display am Empfänger. Es hat sich als hilfreich erwiesen auch den Sender entsprechend zu prüfen. Ich habe dies zum Beispiel mit einer seriellen Schnittstelle und einem Terminal-Programm am PC gelöst. So sieht man stets was sich im Sender und Empfänger abspielt und kann entsprechend reagieren.
Ich möchte weiterhin anhand zweier PIC18F45K22 die Funkstrecke aufbauen. Also der oben beschriebene Testaufbau liegt zwei mal vor und wir haben LC-Displays oder ähnliches installiert, so dass diese für das Anzeigen der Pakete zur Verfügung stehen.
An dieser Stelle möchte ich erwähnen, dass ich nicht so sehr in die Tiefe gehen werde, was die Funkmodule an sich betrifft. Wer detailliertere Information sucht oder wünscht, der sei auf das Datenblatt verwiesen. Dieses ist wie schon gesagt sehr ausführlich und genau geschrieben.
Der Sender
Ich möchte damit beginnen den Sender zu realisieren. Zunächst teilen wir der Bibliothek, die für die Ansteuerung des Funkmoduls zuständig ist mit, an welchen IO-Pins wir die Signale CSN und CE angeschlossen haben. Das wird in der Datei wl_module.h in diesen Zeilen erledigt:
// Pin definitions for chip select and chip enabled #define CE LATAbits.LA7 #define CSN LATAbits.LA0
Diese beiden IO-Pins müssen dann natürlich auch als Ausgänge definiert werden indem die zugehörigens TRIS-Bits, zum Beispiel in der main-Routine gelöscht werden. Weiter geht es mit dem SPI-Modul. Dieses muss natürlich konfiguriert werden, damit die Routinen für das Funkmodul das SPI-Interface überhaupt erst benutzen können. Folgende Funktionen müssen hierzu geschrieben werden:
- Initialisierung des SPI-Moduls
- senden und empfangen eines Bytes
- senden und empfangen mehrerer Bytes
- senden ohne empfangen mehrerer Bytes
Diese Funktionen habe ich natürlich alle schon geschrieben und ich stelle sie zusammen mit allen anderen Dateien am Ende dieses Artikels zum Download zur Verfügung. Innerhalb der wl_module.h werden noch weitere Einstellungen für das Funkmodul getroffen. So wird zum Beispiel mit der folgenden Zeile die Anzahl an Bytes eingestellt, die pro Übertragung übertragen werden soll:
#define wl_module_PAYLOAD 1
In dem hier beschriebenen Beispiel sind bereits alle Einstellungen für die Testübertragung korrekt eingestellt. Es wird lediglich jeweils ein Byte übertragen. Das übertragene Byte wird einen Zählerwert enthalten, der alle 2 Sekunden um eins inkrementiert wird. Beim Zählerstand von 255 beginnt der Wert wieder bei 0. Diesen Zählerwert wollen wir dann im Empfänger, wie auch immer, zur Anzeige bringen um die erfolgreich aufgebaute Funkstrecke zu verifizieren.
Die Main-Routine des Senders:
void main (void) { uint8_t k = 0; uint8_t payload[wl_module_PAYLOAD]; // Array for Payload char buf[10]; /*Alle Initialisierungen durchführen*/ initAll(); wl_module_init(); // initialise nRF24L01+ Modul delayMS(50); // wait for nRF24L01+ Module wl_module_tx_config(wl_module_TX_NR_0); // config Module /*Endlosschleife*/ while(1) { delayMS(2000); wl_module_send(payload,wl_module_PAYLOAD); k++; payload[0] = k; } }
In der Main-Routine werden zunächst sämtliche Einstellungen die den PIC betreffen durchgeführt (IO-Konfigurationen, SPI-Initialisierung, …). Im Anschluss wird die Funktion wl_module_init aufgerufen, die das Funkmodul initialisiert (Pegel von CE und CSN setzen sowie Interrupt Einstellungen). Die wichtige Zeile ist die darauf folgende mit dem Aufruf der Funktion wl_module_tx_config. Diese Funktion konfiguriert nun unser Funkmodul als Sender. Wir können nun mit dem Senden von Daten beginnen. In der Endlosschleife wird lediglich alle 2 Sekunden eine Variable (k) inkrementiert und gesendet. Der Sendefunktion wird lediglich der Sendebuffer sowie dessen Länge übergeben.
Beim Sender arbeiten wir des Weiteren mit Interrupts. Aus diesem Grund muss der externe Interrupt INT0 an RB0 entsprechend konfiguriert werden. Die IRQ-Leitung des Funkmoduls ist low-aktiv. Sobald es zu einer fallenden Flanke am Pin RB0 kommt wird der Interrupt ausgelöst. Innerhalb der ISR, die infolge dessen abgearbeitet wird, wird überprüft was den Interrupt ausgelöst hat. Grundsätzlich existieren hierfür folgende Quellen/Gründe:
- Daten empfangen, Bit RX_DR
- Daten gesendet, Bit TX_DS
- Sendeversuch(e) fehlgeschlagen, Bit MAX_RT
Die Flags können jeweils wieder zurückgesetzt werden indem eine ‘1’ in das entsprechende Bit geschrieben wird. Hier noch die Interrupt-Service-Routine (die ebenfalls im Download enthalten ist):
/*Interrupt mit hoher Priorität*/ void interrupt highPrio(void) { uint8_t status; if(INTCONbits.INT0IF) { INTCONbits.INT0IF = 0; // Read wl_module status wl_module_CSN_lo; // Pull down chip select status = sendSPI(WL_NOP); // Read status register wl_module_CSN_hi; // Pull up chip select if (status & (1<<TX_DS)) // IRQ: Package has been sent { // Clear Interrupt Bit wl_module_config_register(STATUS, (1<<TX_DS)); PTX = 0; } // IRQ: Package has not been sent, send again if (status & (1<<MAX_RT)) { // Clear Interrupt Bit wl_module_config_register(STATUS, (1<<MAX_RT)); wl_module_CE_hi; // Start transmission __delay_us(10); wl_module_CE_lo; } if (status & (1<<TX_FULL)) // TX_FIFO Full < this isnt an IRQ { wl_module_CSN_lo; // Pull down chip select sendSPI(FLUSH_TX); // Flush TX-FIFO wl_module_CSN_hi; // Pull up chip select } } }
Der Empfänger
So wir haben nun dafür gesorgt, dass der Sender alle 2 Sekunden eine Übertragung anstößt. Übertragen wird ein einfacher Zählerwert (8 Bit / 1 Byte). Dieses Paket möchten wir nun mit einem identisch aufgebauten Empfänger empfangen. Wir schauen uns direkt die Main-Routine des Empfängers an:
void main (void) { uint8_t payload[wl_module_PAYLOAD]; // holds the payload uint8_t nRF_status; // STATUS information of nRF24L01+ /*Alle Initialisierungen durchführen*/ initAll(); wl_module_init(); // init nRF Module msDelay(50); // wait for Module wl_module_config(); // config nRF as RX Module, simple Version while(1) { // waits for RX_DR Flag in STATUS while ( !wl_module_data_ready() ); // reads the incoming Data to Array payload nRF_status = wl_module_get_data(payload); /* and here Display the value in payload[0] */ } }
Im Gegensatz zum Sender wird hier beim Empfänger kein Interrupt eingesetzt. Es wird mittels Polling überprüft ob neue Daten empfangen wurden. Die Funktion wl_module_data_ready gibt eine ‘1’ zurück, sobald das der Fall ist. Mit wl_module_get_data können die empfangenen Daten dann aus dem Modul ausgelesen werden. Im Anschluss sollte eine Art Kontrolle stattfinden ob die Daten den Erwartungen entsprechen.
Download
Im Download befinden sich für Sender und Empfänger je ein MPLABX-Projekt. Der Quellcode ist auch in meinem Repository auf GitHub einsehbar.
Software Reset
Ich habe das Funkmodul zuletzt mit einem ARM Cortex M4 eingesetzt und bin dabei auf ein Problem gestoßen. In dem Projekt wurde das Modul für eine bidirektionale Funkverbindung eingesetzt. Dabei ist einer der Teilnehmer ein autarkes System, das mit Batterie betrieben wird und sich zum Stromsparen immer wieder “ausschaltet”, es geht in den sogenannten Hybernation Mode. Wenn der ARM Controller wieder aus diesem Zustand erwacht, funktionierte die Funkverbindung nicht mehr. Natürlich wurde das Modul erneut Initialisiert, doch nach dem Aufwachen aus dem Hybernation Mode war keine Funkverbindung mehr möglich. Erst ein einfaches trennen der Versorgungsspannung des Funkmoduls half.
Um jetzt aber auf z.B. einen FET-Transistor in der Versorgungszuleitung des Moduls zu verzichten habe ich einen anderen Weg gesucht, der das Modul wieder zum Leben erweckt. Die Kurzfassung, die zum gewünschten Ergebnis führte:
- der Controller wacht aus dem Hybernation Mode wieder auf
- der Controller ruft normal die Funktion wl_module_init() auf
- nun wird dem Modul etwas Zeit eingeräumt (~50 ms)
- jetzt wird das Modul in den Power down Modus gesetzt
- das Data-Ready und Data-Send-Flag werden gelöscht
- der RX- und TX-Buffer werden gelöscht
- das Status-Register wird auf den Wert 0x0E gesetzt
Als Programmcode könnte das dann zum Beispiel so in etwa aussehen:
void wl_module_reset (void) { uint8_t tmp_wl_status = 0x00; // power down the mpdule wl_module_power_down(); // clear data ready and data send flag (of STATUS Register) wl_module_read_register(STATUS, &tmp_wl_status, 1); tmp_wl_status &= (~STATUS_RX_DR); tmp_wl_status &= (~STATUS_TX_DS); wl_module_write_register(STATUS, &tmp_wl_status, 1); // flush RX- and TX-Buffer wl_module_CSN_lo; // Pull down chip select sendSPI(FLUSH_TX); wl_module_CSN_hi; // Pull up chip select wl_module_CSN_lo; // Pull down chip select sendSPI(FLUSH_RX); wl_module_CSN_hi; // Pull up chip select // write 0x0E to STATUS-Register tmp_wl_status = 0x0E; wl_module_write_register(STATUS, &tmp_wl_status, 1); }