NRF24L01 Tutorial

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Einleitung

Das Tutorial soll den Einstieg, eine Funkstrecke mit dem nRF24L01+ aufzubauen, erleichtern.

Beim nRF24L01+ handelt es sich um einen single Chip Transceiver von Nordic Semiconductor. Dieser Chip kann also sowohl als Sender (TX) als auch als Empfänger (RX) arbeiten und bietet eine Reihe von sehr praktischen Features wie Ultra Low Power (nur 900nA in Power Down Modus), 6 Data Pipes beim Betrieb als MultiCeiver, Enhanced Shockburst (dazu später mehr), Automatic Packet Handling, SPI Schnittstelle, 5V tolerante Dateneingänge. VCC ist 1,9V - 3,6V. Achtung: 5V an VCC zerstören den Chip.

Um mit dem nRF24L01+ eine Funkstrecke zu realisieren braucht man nicht mehr als einen µC sowie ein nRF24L01+ Funkmodul. Das ganze natürlich zwei mal. In dem Tutorial wird der AVR Atmega8 verwendet sowie nRF24L01+ Module die sehr günstig über das Internet (Ebay derzeit unter 2€) in verschiedenen Ausführungen bezogen werden können. Für Testaufbauten reichen einfache Steckbretter um die Hardware aufzubauen. Bei der Hand sollte unbedingt das Datenblatt des nRF24L01+ sein und natürlich das Datenblatt des jeweils verwendeten µC. Das Datenblatt des nRF24L01+ kann von Nordic Semiconductor runtergeladen werden:

nRF24L01 Datenblatt

Vieles in diesem Tutorial basiert auf der Vorarbeit von Stefan Engelke und Brennan Ball denen mein Dank dafür gilt.

Das nRF24L01+ Modul

Ein Funkmodul mit dem nRF24L01+ hat zumindest 8 Anschlüsse:

Handelsübliche nRF24L01+ Module

Vcc, GND, IRQ, CE, und die vier SPI bezogenen Anschlüsse CSN, SCK, MISO, und MOSI.

Je nach Datenblatt des verwendeten Moduls ist die Spannung zu wählen, manche Module verfügen über eigene Spannungsregler, der nRF24L01+ selbst verträgt Spannungen von 1,9V bis 3,6V. Auf dem von mir verwendeten Modul befindet sich kein weiterer Spannungsregler somit muß man aufpassen den Chip nicht durch Überspannung zu ruinieren. Als Spannung habe ich 3,3V verwendet.

Die Anschlüsse SCK, MISO, und MOSI werden mit den entsprechenden Pins des µC verbunden. Der CSN (chip select not) Pin ist active low und normalerweise auf high-Level gehalten. Wenn CSN low geschaltet wird beginnt der nRF24L01+ am SPI Port zu horchen oder anders gesagt, jedes Kommando an den Chip wird mit einem high to low schalten von CSN eingeleitet. Im Falle des Atmega8 werden SCK mit SCK, MISO mit MISO und MOSI mit MOSI verbunden. Für den CSN wurde Pin PB1 gewählt.

Stromlaufplan für den Anschluß an einen ATmega8

Was uns noch bleibt ist CE und IRQ. CE wird im RX und TX Modus verwendet für die Datenübertragung. In dem Beispiel an Pin PB0 angeschlossen. IRQ ist der Interrupt-Pin des Funkmoduls und ist active-low. Es gibt drei interne Interrupts im nRF24L01+ die den IRQ Pin low schalten wobei man sämtliche Interrupts ausmaskieren kann wenn man möchte. Am Atmega8 wurde IRQ mit Pin PD2 (INT0) verbunden um auf die externen Interrupts reagieren zu können.

Die SPI-Routine

Als SPI-Routine habe ich die Routine von Stefan Engelke unverändert übernommen die er dankenswerter Weise zur allgemeinen Verwendung zur Verfügung gestellt hat. siehe Tinkerer.eu

spi.h

#ifndef _SPI_H_
#define _SPI_H_

#include <avr/io.h>


extern void spi_init();
extern void spi_transfer_sync (uint8_t * dataout, uint8_t * datain, uint8_t len);
extern void spi_transmit_sync (uint8_t * dataout, uint8_t len);
extern uint8_t spi_fast_shift (uint8_t data);


#endif

spi.c

#include "spi.h"

#include <avr/io.h>
#include <avr/interrupt.h>

#define PORT_SPI    PORTB
#define DDR_SPI     DDRB
#define DD_MISO     DDB4
#define DD_MOSI     DDB3
#define DD_SS       DDB2
#define DD_SCK      DDB5


void spi_init()
// Initialize pins for spi communication
{
    DDR_SPI &= ~((1<<DD_MOSI)|(1<<DD_MISO)|(1<<DD_SS)|(1<<DD_SCK));
    // Define the following pins as output
    DDR_SPI |= ((1<<DD_MOSI)|(1<<DD_SS)|(1<<DD_SCK));

    
    SPCR = ((1<<SPE)|               // SPI Enable
            (0<<SPIE)|              // SPI Interrupt Enable
            (0<<DORD)|              // Data Order (0:MSB first / 1:LSB first)
            (1<<MSTR)|              // Master/Slave select   
            (0<<SPR1)|(1<<SPR0)|    // SPI Clock Rate
            (0<<CPOL)|              // Clock Polarity (0:SCK low / 1:SCK hi when idle)
            (0<<CPHA));             // Clock Phase (0:leading / 1:trailing edge sampling)

    SPSR = (1<<SPI2X);              // Double Clock Rate
    
}

void spi_transfer_sync (uint8_t * dataout, uint8_t * datain, uint8_t len)
// Shift full array through target device
{
       uint8_t i;      
       for (i = 0; i < len; i++) {
             SPDR = dataout[i];
             while((SPSR & (1<<SPIF))==0);
             datain[i] = SPDR;
       }
}

void spi_transmit_sync (uint8_t * dataout, uint8_t len)
// Shift full array to target device without receiving any byte
{
       uint8_t i;      
       for (i = 0; i < len; i++) {
             SPDR = dataout[i];
             while((SPSR & (1<<SPIF))==0);
       }
}

uint8_t spi_fast_shift (uint8_t data)
// Clocks only one byte to target device and returns the received one
{
    SPDR = data;
    while((SPSR & (1<<SPIF))==0);
    return SPDR;
}


Der SPI Befehlssatz

Command name Command word (binary) # Data bytes Operation
R_REGISTER 000A AAAA 1 to 5
LSByte first
Read command and STATUS registers. AAAAA = 5 bit Register Map Address
W_REGISTER 001A AAAA 1 to 5
LSByte first
Write command and status registers. AAAAA = 5 bit Register Map Address
Executable in power down or standby modes only.
R_RX_PAYLOAD 0110 0001 1 to 32
LSByte first
Read RX-payload: 1 – 32 bytes. A read operation always starts at byte 0. Payload is deleted from FIFO after it is read. Used in RX mode.
W_TX_PAYLOAD 1010 0000 1 to 32
LSByte first
Write TX-payload: 1 – 32 bytes. A write operation always starts at byte 0 used in TX payload.
FLUSH_TX 1110 0001 0 Flush TX FIFO, used in TX mode
FLUSH_RX 1110 0010 0 Flush RX FIFO, used in RX mode
Should not be executed during transmission of acknowledge, that is, acknowledge package will not be completed.
REUSE_TX_PL 1110 0011 0 Used for a PTX device

Reuse last transmitted payload.
TX payload reuse is active until W_TX_PAYLOAD or FLUSH TX is executed. TX payload reuse must not be activated or deactivated during package transmission.

R_RX_PL_WID* 0110 0000 1 Read RX payload width for the top R_RX_PAYLOAD in the RX FIFO.
Note: Flush RX FIFO if the read value is larger than 32 bytes.
W_ACK_PAYLOAD* 1010 1PPP 1 to 32
LSByte first
Used in RX mode.
Write Payload to be transmitted together with ACK packet on PIPE PPP. (PPP valid in the range from 000 to 101). Maximum three ACK packet payloads can be pending. Payloads with same PPP are handled using first in - first out principle. Write payload: 1– 32 bytes. A write operation always starts at byte 0.
W_TX_PAYLOAD_NOACK* 1011 0000 1 to 32
LSByte first
Used in TX mode. Disables AUTOACK on this specific packet.
NOP 1111 1111 0 No Operation. Might be used to read the STATUS register

*=The bits in the FEATURE register shown in Table 28. on page 63 have to be set.

Zu finden natürlich auch im Datasheet auf Seite 51.

Es gibt ein paar Dinge die zu beachten sind. Bevor die Kommunikation mit dem Chip begonnen wird muß der CSN Pin high sein. Zum Start der Kommunikation wird der CSN Pin auf Low geschaltet und muß auch Low bleiben während der gesamten Dauer der Kommunikation.

Dann wird der Befehl gesendet. Wenn man Daten vom nRF24L01+ empfangen möchte muß für jedes Byte an Daten auch ein Byte gesendet werden. Wenn man nur seine eigenen Daten an den Chip senden möchte kümmert man sich nicht weiter darum was der Chip zurücksendet. Auf jeden Fall wird parallel zu den Daten die man schickt der STATUS des Chips zurückgegeben. Ist die Unterhaltung (mit dem nRF24L01+ - es geht noch nicht um die Funkstrecke) abgeschlossen, wird CSN wieder auf High geschaltet.

Als Beispiel: Wir wollen den R_REGISTER Befehl senden um den Inhalt des Registers TX_ADDR zu lesen. Das TX_ADDR Register enthält maximal 5 Bytes an Daten/Information. Als erstes CSN auf Low. Dann "0001 0000" an den Chip senden. Das weist den nRF24L01+ an das Register TX_ADDR auszugeben. Dann werden fünf Dummy-Bytes gesendet und für jedes Dummy-Byte erhält man ein Byte aus dem TX_ADDR Register zurück. (Der Inhalt der Dummy-Bytes ist ganz egal) Danach CSN wieder auf High. Insgesamt erhält man vom Chip sechs Bytes: Egal welches Command Byte man sendet antwortet der Chip mit dem STATUS-Register.

Mit dem R_REGISTER können also alle Registerinhalte gelesen werden die zwischen 1 Byte und 5 Byte groß sind.

Mit dem W_REGISTER können wir beliebig unsere Werte in die Register schreiben. Das Prinzip ist das gleiche, es wird der Befehl 001AAAAA gesendet wobei AAAAA für die Adresse des Registers steht. Danach werden - je nach Register - bis zu fünf Bytes an Daten gesendet die in das Register geschrieben werden sollen.

R_RX_PAYLOAD ist einer der beiden Befehle die mit dem FIFO zu tun haben. R_RX_PAYLOAD liest den Inhalt des FIFO sollten Daten empfangen werden. Der Empfang von Daten wird mit dem RX_DR Interrupt angezeigt. Bei diesem Befehl ist das Vorgehen ein wenig anders. Wenn man Daten per Funkstrecke empfängt ist CE high. Sobald man ein Paket empfangen hat muß man CE low schalten um den Empfang auszuschalten und danach kann R_RX_PAYLOAD ausgeführt werden. Gefolgt von so vielen Dummy-Bytes wie Payload definiert wurde. Gelesene Pakete werden automatisch aus dem FIFO gelöscht. Sollten weitere Pakete im FIFO warten sollten die auch gleich gelesen werden. Sobald der FIFO leer ist wird der RX_DR Interrupt gelöscht und CE wieder auf High geschaltet. Der FIFO kann maximal 3 Pakete (PAYLOADS) halten. Ein Paket ist maximal 32 Bytes groß.

Der zweite dieser Befehle ist W_TX_PAYLOAD. Er wird verwendet wenn man sich im TX-Modus befindet und Daten senden möchte. Im TX-Modus ist CE low. Nachdem man den Befehl gesendet hat werden so viele Bytes ins FIFO geladen wie man für den Empfänger auch defniniert hat. D. h. Payload Größe muß beim Sender und Empfänger gleich groß sein solange man das FEATURE-Register nicht verwendet. Nachdem die Daten in den FIFO geladen wurden muß man den CE Pin toggeln (mind. 10µs high) damit die Daten gesendet werden. Man kann auch zuerst drei Pakete in den FIFO laden und dann erst wegschicken.

FLUSH_TX und FLUSH_RX löschen die Daten im TX_FIFO bzw. RX_FIFO.

NOP kann sehr gut dafür verwendet werden das STATUS Register zu lesen da es - wie der Name schon sagt - sonst nichts tut.

Die nRF24L01+ Register

Hier ist es nötig, das Datenblatt ab Seite 57 zur Hand zu nehmen da die Menge an Registern das Tutorial sprengen würde, würden alle hier besprochen werden.

Mit Hilfe der Register wird das Modul per SPI konfiguriert und auch kontrolliert. Die meisten Register sind 1Byte groß, in vielen Registern werden aber nicht alle Bits verwendet. Eine Ausnahme bilden die Register RX_ADDR_P0, RX_ADDR_P1 sowie TX_ADDR die jeweils bis zu 5 Byte groß sind. (Abhängig von der Konfiguration). Die drei 32Byte großen Register werden mit eigenen SPI Befehlen gesteuert. Sie dienen dazu die Datenpakete in den nRF24L01+ zu schreiben bzw. empfangene auszulesen.

Innerhalb der Register belegen die Informationen eine unterschiedliche Anzahl an Bits. Als Beispiel das STATUS Register. Bit 1 bis 3 geben hier zum Beispiel Auskunft über die verwendete Pipe. Während alle anderen Informationen nur 1 Bit groß sind in dem Register. Es muß also immer entsprechend maskiert werden um die gewünschten Infos zu lesen oder auch zu schreiben.

Die ersten Schritte

So, nach dem kurzen theoretischen Überblick ist es an der Zeit die ersten Schritte zu wagen. Sollte es noch nicht der Fall sein sollten spätestens jetzt die beiden nRF24L01+ Module mit dem µC verkabelt sein. Der ATmega8 sollte für dieses Beispiel mit mind. 8Mhz laufen, ob externer Quarz oder interner Takt ist dabei egal. Nochmals zur Erinnerung: Das nRF-Modul darf mit maximal 3,6V betrieben werden, die Eingänge verkraften aber 5V Signale solange die Versorgungsspannung zwischen 2,7V und 3,3V liegt. Werft sicherheitshalber einen Blick ins Datenblatt betreffend dieser Dinge.

In der Vergangenheit gab es bei Arduino-Boards Fälle, in denen eine Spitzenspannung von mehr als 3,3V zu sehen war. Bei der Stromversorgung des Moduls direkt von einem Arduino-Board sollte daher ein 10µF Kondensator oder mehr parallel zu den VCC- und GND-Pins angeschlossen werden.[1]

Worüber man sich auch Gedanken machen muß ist, wie man die Funktion bzw. die Daten die man hin und her schickt, visualisiert. Ohne Visualisierung kann es natürlich auch funktionieren doch für das Tutorial wollen wir sicher sehen ob, und wenn ja, welche Daten hin und hergeschickt werden oder auch mal einen Registerinhalt anzeigen. In welcher Form das passiert bleibt jedem selbst überlassen. Man kann die Daten auf einem LCD ausgeben oder auch per UART an den PC liefern lassen. Hier sollte jeder das wählen womit er am Besten zurecht kommt. Theoretisch könnte man auch das Auslangen mit LEDs finden und sich die Registerinhalte oder Datenpakete mittels 8 LEDs anzeigen lassen. Allerdings wäre das nicht sehr komfortabel.

Konfigurieren des nRF24L01+

Als erstes müssen wir das Modul mal initialisieren bzw. die verwendeten Anschlüsse des µC einrichten. Am besten packen wir das ganze in eine Routine die ziemlich zeitig beim Start des µC aufgerufen wird. Weiter unten findet sich dann die Header-Datei die hier verwendet wird.

Als erstes setzen wir die beiden verwendeten Pins als Ausgänge und schalten sie in den default-Status.

void wl_module_init() 
{
    DDRB |= ((1<<CSN)|(1<<CE));
    wl_module_CE_lo;
    wl_module_CSN_hi;

Dann werden die beiden (eigentlich reicht INT0) externen Interrupteingänge des ATmega8 auf fallende Flanke gestellt und der Interrupt für INT0 aktiviert.

    MCUCR = ((1<<ISC11)|(0<<ISC10)|(1<<ISC01)|(0<<ISC00));
    GICR  = ((0<<INT1)|(1<<INT0));

und zum Schluß noch SPI (siehe oben unter SPI) initialisiert:

	    
    spi_init();
}

Anbei noch die Header-Datei für das Modul: Datei:Wl module.h (aus irgendeinem Grund forciert das Wiki hier große Anfangsbuchstaben, die Datei sollte als "wl_module.h" abgespeichert werden.)

In dieser Header-Datei werden die wesentlichen Zuordnungen und Einstellungen getroffen wie Pin-Definitionen, Größe des Payloads, Interruptmaskierung für das Modul, CRC-Größe, etc. Außderdem die public functions deklariert. Im Moment noch nicht schrecken, es sind eine ganze Reihe von Funktionen die in einer wl_module.c Datei zusammengefaßt sind. Aufmerksame Leser werden feststellen, daß auch obige Funktion, die wl_module_init() dort erstellt wurde.

In dem Projekt wird noch eine weitere Header-Datei für das Modul verwendet in dem die Mnemonics des Moduls abgebildet sind. Doch dazu gleich.

Grundsätzliche Konfiguration

Konfigurieren läßt sich eine ganze Menge beim nRF24L01+. Aber wir haben das Glück, daß es auch funktioniert wenn man wenig konfiguriert und die Standardwerte eingestellt läßt.

Für unsere ersten Tests stellen wir nur das notwendigste ein. Um uns die Arbeit mit den vielen Registern zu erleichtern ist es jetzt Zeit die schon angesprochene Header-Datei vorzustellen und in weiterer Folge einzubinden.

Datei:NRF24L01.h

In dieser Header-Datei finden wir alle Mnemonics um auf die entsprechenden Befehle, Register und Registerinhalte zuzugreifen. Ja, es ist eine ziemlich lange Liste aber davon nicht schrecken lassen.

Gut, dann setzen wir mal die ersten Werte im nRF24L01+. Als erstes wollen wir den Funkkanal einstellen. Dazu haben wir im wl_module.h schon wl_module_CH definiert. Ein Blick ins Datenblatt verrät uns, daß wir das Kommando W_REGISTER brauchen und der Kanal im Register RF_CH eingestellt wird. Die grundsätzliche Funktionsweise der Kommandos wurde schon weiter oben erklärt doch wie setzen wir das jetzt um?

Wir bedienen uns dazu einer Funktion die genau ein Byte in ein gewähltes Register schreibt. reg ist das gewählte Register und value der Wert der geschrieben werden soll. Mit dieser Funktion können wir schon fast alle Register des Moduls beschreiben.

void wl_module_config_register(uint8_t reg, uint8_t value)
// Clocks only one byte into the given wl-module register
{
    wl_module_CSN_lo;
    spi_fast_shift(W_REGISTER | (REGISTER_MASK & reg));
    spi_fast_shift(value);
    wl_module_CSN_hi;
}

Um den Kanal zu setzen wird

// Set RF channel
    wl_module_config_register(RF_CH,wl_module_CH);

aufgerufen.

Fein, damit ist es uns ein leichtes den Payload zu konfigurieren. Der Payload sind die Anzahl an Bytes die auf einmal übertragen werden. Der Sender muß (solange nicht die dynamic payload length aktiviert ist) genau so viele Bytes senden wie am Empfänger als Payload-Länge eingestellt ist.

// Set length of incoming payload 
wl_module_config_register(RX_PW_P0, wl_module_PAYLOAD);

Für die ersten Tests werden wir uns um keine Pipes kümmern daher reicht es erstmal wenn wir nur den Payload für die Pipe0 festlegen.

Ich mag das Augenmerk auf folgende Zeile in der wl_config.h legen:

#define wl_module_CONFIG		( (1<<MASK_RX_DR) | (1<<EN_CRC) | (0<<CRCO) )

In der Zeile wird einiges für das CONFIG Register eingestellt. Im CONFIG Register können etwa die drei Interrupts maskiert werden, es wird eingestellt ob CRC verwendet wird und ob CRC aus einem oder zwei Bytes besteht. Und dann gibt es noch zwei ganz wesentliche Dinge darin. Einerseits PWR_UP und dann auch noch PRIM_RX. PRIM_RX legt fest ob sich das Modul im TX oder im RX Modus befindet.

Über

// Defines for setting the wl_module registers for transmitting or receiving mode
#define TX_POWERUP wl_module_config_register(CONFIG, wl_module_CONFIG | ( (1<<PWR_UP) | (0<<PRIM_RX) ) )
#define RX_POWERUP wl_module_config_register(CONFIG, wl_module_CONFIG | ( (1<<PWR_UP) | (1<<PRIM_RX) ) )

können wir das Modul entweder im TX oder RX Modus aktivieren.

Fürs erste können wir die restlichen Register so lassen wie eingestellt.

Wie schaut es jetzt aus wenn wir ein Register beschreiben wollen das mehr als ein Byte an Daten enthält? Als Paradebeispiel ist dafür natürlich das schreiben des Payloads in den TX-FIFO geeignet da wir hier bis zu 32 Bytes schreiben.

Aber auch andere Register brauchen mehr als 1 Byte, so zum Beispiel die Adressregister die bis zu 5 Bytes breit sind.

Auch dafür können wir uns einer kleinen aber feinen Routine bedienen:

void wl_module_write_register(uint8_t reg, uint8_t * value, uint8_t len) 
// Writes an array of bytes into inte the wl-module registers.
{
    wl_module_CSN_lo;
    spi_fast_shift(W_REGISTER | (REGISTER_MASK & reg));
    spi_transmit_sync(value,len);
    wl_module_CSN_hi;
}

An die Routine übergeben wird das Register das beschrieben werden soll, ein Zeiger auf das Array mit den Daten und die Größe dieses Arrays.

Register wieder auslesen

Eine Frage die sich sehr bald stellen wird: Wie lese ich Register wieder aus? Sei es um die Einstellungen zu kontrollieren oder auf Bits die darin automatisch gesetzt werden zu reagieren.

Wie wir wissen reagiert der nRF24L01+ auf jeden Befehl den wir schicken automatisch und parallel auf unseren Befehl mit der Antwort des STATUS Registers. Das STATUS Register ist ein wichtiges weil es einiges an wesentlichen Informationen enthält auf die wir reagieren müssen. So werden alle drei Interrupts in dem Register durch setzen eines entsprechenden Bits abgebildet. Dieses Bit muß durch schreiben einer 1 auch wieder gelöscht werden. Es läßt sich außerdem die Pipe auslesen die die Daten gesendet hat sowie, auch ganz wichtig, das TX_FULL Flag auslesen. Dieses Flag wird gesetzt wenn der Sende-Fifo voll ist.

Um den Status auszulesen und sonst nix weiter zu machen können wir eine kleine Funktion einsetzen:

//return the value of the status register
extern uint8_t wl_module_get_status()
{
	return wl_module_get_one_byte(NOP);
}

Aber nicht vergessen, egal was wir an den nRF24L01+ schicken, er antwortet jedes mal mit dem Inhalt des Status-Registers. Das können wir uns später zu nutze machen.

In dem Beispiel wird die Funktion wl_module_get_one_byte() verwendet:

extern uint8_t wl_module_get_one_byte(uint8_t command)
{
uint8_t status;

wl_module_CSN_lo;
status = spi_fast_shift(command);
wl_module_CSN_hi;

return status;

}

Genau betrachtet liefert die Funktion wl_module_get_one_byte() schon den Inhalt des Status Registers. Nachdem die Funktion aber die Grundlage für den Zugriff auf einzelne Teile des Status-Registers ist die in weiteren Funktionen verwendet wird wurde für die bessere Übersichtlichkeit eine eigene Status-Abfrage Funktion implementiert.

Wenn andere Inhalte als der Status abgefragt werden sollen etwa der Inhalt des FIFO wo ja unsere empfangenen Daten liegen bedienen wir uns einer weiteren Funktion.

void wl_module_read_register(uint8_t reg, uint8_t * value, uint8_t len)
// Reads an array of bytes from the given start position in the wl-module registers.
{
    wl_module_CSN_lo;
    spi_fast_shift(R_REGISTER | (REGISTER_MASK & reg));
    spi_transfer_sync(value,value,len);
    wl_module_CSN_hi;
}

Diese Funktion leistet uns für einige weitere Funktionen die darauf zugreifen gute Dienste. Wollen wir als Beispiel den gewählten Funkkanal auslesen so können wir das mit dieser Funktion:

//returns the current RF channel in RF_CH register
extern uint8_t wl_module_get_rf_ch()
{
	uint8_t data;
	
	wl_module_read_register(RF_CH, &data, 1);
	
	return data;
}

Wie man sieht greift die Funktion wl_module_get_rf_ch() auf die Funktion wl_module_read_register zu, übergibt die notwendigen Parameter und liefert uns dann das Byte zurück das in RF_CH gespeichert ist.

Erster praktischer Test

So, genug der Grundlagen, schauen wir uns einen ersten praktischen Test mit den Modulen an.

Der Aufbau ist verdrahtet und mit den richtigen Spannungen versorgt, der µC läuft mit mind. 8 MHz (niedrigere Frequenzen gehen auch aber wurden von mir nicht getestet, achtet auf den passenden SPI-Vorteiler), als Optimierungsmethode ist -Os eingestellt und dem Compiler wurde auch die richtige Frequenz mitgeteilt.

Als erstes werden wir beim Sender einen Zähler hochzählen und die Zahlen an den Empfänger übermitteln. Dadurch sehen wir, ob die Funkstrecke funktioniert. Welche Bytes tatsächlich übertragen werden ist schlußendlich egal.

Der Sender

Hier die kurze Main-Routine für den Sender, verwendete Funktionen werden im Anschluß besprochen. Nach belieben könnt ihr an freie Pins noch LED anschließen um bestimmte Ereignisse optisch zu überwachen. Im Code selbst wird darauf verzichtet.

/*
 * nRF24L01_Tutorial_Sender.c
 *
 * Created: 06.01.2012 20:15:04
 *  Author: Ernst Buchmann
 */ 

#ifndef F_CPU				//Define F_CPU if not done 
#define F_CPU 8000000UL
#endif

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdlib.h>
#include "spi.h"
#include "wl_module.h"
#include "nRF24L01.h"

volatile uint8_t timercounter;

int main(void)
{
	uint8_t payload[wl_module_PAYLOAD];		//Array for Payload
	uint8_t maincounter =0;
	uint8_t k;
	
	wl_module_init();	//initialise nRF24L01+ Module
	_delay_ms(50);		//wait for nRF24L01+ Module
	sei();
	
	wl_module_tx_config(wl_module_TX_NR_0);		//Config Module
		
	//Timer aktivieren ATMEGA8
	#if defined(__AVR_ATmega8__)
	TCCR0 |= ( (1<<CS02) | (1<<CS00));		//Prescaler auf 1024 ATMEGA8
	TIMSK |= ( (1<<TOIE0));					//enable TOVF ATMEGA8
	#endif // __AVR_ATmega8__
	
	//Timer aktivieren ATMEGA88A
	#if defined(__AVR_ATmega88A__)
	TCCR0B |= ( (1<<CS02) | (1<<CS00));
	TIMSK0 |= ( (1<<TOIE0));
	#endif // __AVR_ATmega88A__
		
    while(1)
    {
		if (timercounter >= 30)			//30 entspricht ~1 Sekunde bei 8MHz
			{
				timercounter = 0;
			
				
				for (k=0; k<=wl_module_PAYLOAD-1; k++)
				{
					payload[k] = k;
				}
			
				payload[0] = maincounter;
				payload[1] = maincounter+1;				
			
				wl_module_send(payload,wl_module_PAYLOAD);
				
				maincounter++;
				if (maincounter >250)
				{
					maincounter = 0; 
				}
			}
    }
}

ISR(TIMER0_OVF_vect)
{
	timercounter++;
}


//Unterscheidung je nach verwendeten µC
#if defined(__AVR_ATmega8__)
ISR(INT0_vect)
#endif // __AVR_ATmega8__
#if defined(__AVR_ATmega88A__)
ISR(INT0_vect)
#endif // __AVR_ATmega88A__
#if defined(__AVR_ATmega168__)
ISR(PCINT2_vect) 
#endif // __AVR_ATmega168__  
// Interrupt handler 
{
    uint8_t status;   
    
        // Read wl_module status 
        wl_module_CSN_lo;                               // Pull down chip select
        status = spi_fast_shift(NOP);					// Read status register
        wl_module_CSN_hi;                               // Pull up chip select
		
		
		if (status & (1<<TX_DS))							// IRQ: Package has been sent
		{
			wl_module_config_register(STATUS, (1<<TX_DS));	//Clear Interrupt Bit
			PTX=0;
		}
		
		if (status & (1<<MAX_RT))							// IRQ: Package has not been sent, send again
		{
			wl_module_config_register(STATUS, (1<<MAX_RT));	// Clear Interrupt Bit
			wl_module_CE_hi;								// Start transmission
			_delay_us(10);								
			wl_module_CE_lo;
		}
		
		if (status & (1<<TX_FULL))							//TX_FIFO Full <-- this is not an IRQ
		{
			wl_module_CSN_lo;                               // Pull down chip select
			spi_fast_shift(FLUSH_TX);						// Flush TX-FIFO
			wl_module_CSN_hi;                               // Pull up chip select
		}
		
}

So, Teile wie Timer oder Timerinterrupt sollten bekannt sein. Schauen wir uns die Funktionen an die aufgerufen werden.

wl_module_init();

ist die bereits bekannte Initialisierungsfunktion wie oben besprochen.

wl_module_tx_config(wl_module_TX_NR_0);

Müssen wir uns genauer ansehen. In dieser Funktion werden alle wesentlichen Register eingestellt die wir brauchen um das Modul als Sender zu nutzen. Alle verwendeten Funktionen sollten in einer wl_module.c Datei zusammengefasst werden. Obwohl wir es in diesem Beispiel noch nicht nutzen übergeben wir an wl_module_tx_config als Argument die Pipe auf der der Sender senden soll. Das Argument wird dazu verwendet die entsprechende Adresse für den Sender festzulegen. Diese Routine ist also schon dafür eingerichtet um später einen Multi-Ceiver Betrieb mit 6 Sendern zu ermöglichen. (Beim Empfänger weiter unten wird eine einfache Konfigurationsroutine verwendet mit der kein Multi-Ceiver Betrieb möglich ist!)

Die Variable PTX wird als globale Variable definiert und dazu verwendet um festzustellen ob gerade gesendet wird. Wird gesendet wird sie im Code auf 1 geschaltet, sonst auf 0.

Zum Schluß wird mittels TX_POWERUP (siehe oben) das Modul als Sender aktiviert.

extern void wl_module_tx_config(uint8_t tx_nr) 
{
    uint8_t tx_addr[5];
	
    // Set RF channel
    wl_module_config_register(RF_CH,wl_module_CH);
    // Set data speed & Output Power configured in wl_module.h
    wl_module_config_register(RF_SETUP,wl_module_RF_SETUP);
    //Config the CONFIG Register (Mask IRQ, CRC, etc)
    wl_module_config_register(CONFIG, wl_module_CONFIG);
    
    wl_module_config_register(SETUP_RETR,(SETUP_RETR_ARD_750 | SETUP_RETR_ARC_15));
	
	//set the TX address for the pipe with the same number as the iteration
			switch(tx_nr)			
			{
				case 0: //setup TX address as default RX address for pipe 0 (E7:E7:E7:E7:E7)
					tx_addr[0] = tx_addr[1] = tx_addr[2] = tx_addr[3] = tx_addr[4] = RX_ADDR_P0_B0_DEFAULT_VAL;
					wl_module_set_TADDR(tx_addr);
					wl_module_set_RADDR(tx_addr);
					break;
				case 1: //setup TX address as default RX address for pipe 1 (C2:C2:C2:C2:C2)
					tx_addr[0] = tx_addr[1] = tx_addr[2] = tx_addr[3] = tx_addr[4] = RX_ADDR_P1_B0_DEFAULT_VAL;
					wl_module_set_TADDR(tx_addr);
					wl_module_set_RADDR(tx_addr);
					break;
				case 2: //setup TX address as default RX address for pipe 2 (C2:C2:C2:C2:C3)
					tx_addr[1] = tx_addr[2] = tx_addr[3] = tx_addr[4] = RX_ADDR_P1_B0_DEFAULT_VAL;
					tx_addr[0] = RX_ADDR_P2_DEFAULT_VAL;
					wl_module_set_TADDR(tx_addr);
					wl_module_set_RADDR(tx_addr);
					break;
				case 3: //setup TX address as default RX address for pipe 3 (C2:C2:C2:C2:C4)
					tx_addr[1] = tx_addr[2] = tx_addr[3] = tx_addr[4] = RX_ADDR_P1_B0_DEFAULT_VAL;
					tx_addr[0] = RX_ADDR_P3_DEFAULT_VAL;
					wl_module_set_TADDR(tx_addr);
					wl_module_set_RADDR(tx_addr);
					break;
				case 4: //setup TX address as default RX address for pipe 4 (C2:C2:C2:C2:C5)
					tx_addr[1] = tx_addr[2] = tx_addr[3] = tx_addr[4] = RX_ADDR_P1_B0_DEFAULT_VAL;
					tx_addr[0] = RX_ADDR_P4_DEFAULT_VAL;
					wl_module_set_TADDR(tx_addr);
					wl_module_set_RADDR(tx_addr);
					break;
				case 5: //setup TX address as default RX address for pipe 5 (C2:C2:C2:C2:C6)
					tx_addr[1] = tx_addr[2] = tx_addr[3] = tx_addr[4] = RX_ADDR_P1_B0_DEFAULT_VAL;
					tx_addr[0] = RX_ADDR_P5_DEFAULT_VAL;
					wl_module_set_TADDR(tx_addr);
					wl_module_set_RADDR(tx_addr);
					break;
			}
	
	PTX =0;
	TX_POWERUP;
}

In der Main-Routine wird ein Timer dazu verwendet ca. jede Sekunde einen Zähler (maincounter) hinauf zu zählen (bis max. 250) und im Array payload wird das erste Element mit dem Wert des maincounters belegt.

payload[0] = maincounter;

Die Schleife dient nur dazu das Array mit irgendwelchen Werten zu belegen die am Empfänger auch abgerufen werden können um damit zu "spielen". Dann wird die Funktion aufgerufen die das Array ins Modul überträgt und die Sendung der Daten beginnt.

wl_module_send(payload,wl_module_PAYLOAD); kümmert sich darum.

void wl_module_send(uint8_t * value, uint8_t len) 
// Sends a data package to the default address. Be sure to send the correct
// amount of bytes as configured as payload on the receiver.
{
    while (PTX) {}                  // Wait until last paket is send

    wl_module_CE_lo;

    PTX = 1;							// Set to transmitter mode
    TX_POWERUP;							// Power up
    
    wl_module_CSN_lo;                   // Pull down chip select
    spi_fast_shift( FLUSH_TX );			// Write cmd to flush tx fifo
    wl_module_CSN_hi;                   // Pull up chip select
    
    wl_module_CSN_lo;                   // Pull down chip select
    spi_fast_shift( W_TX_PAYLOAD );		// Write cmd to write payload
    spi_transmit_sync(value,len);		// Write payload
    wl_module_CSN_hi;                   // Pull up chip select
    
    wl_module_CE_hi;                    // Start transmission
	_delay_us(10);						// Grünes Modul funktioniert nicht mit 10µs delay
	wl_module_CE_lo;
}

Zum Schluß werfen wir noch einen Blick auf die Interrupt-Routine ISR(INT0_vect). Wir wissen, daß es drei Zustände gibt die einen Interrupt auslösen können: RX_DR der Auftritt wenn neue Daten empfangen wurden, TX_DS der Auftritt wenn Daten gesendet wurden und MAX_RT der Auftritt wenn die maximale Anzahl an Sendungswiederholungen ohne Erfolg stattgefunden haben. Sobald einer der Zustände auftritt wird auch das passende Bit im STATUS-Register gesetzt. Um das Bit zu löschen muß eine 1 geschrieben werden. Jeder dieser drei Zustände kann ausmaskiert werden und löst dann keinen Interrupt aus. Man kann so entweder per Interrupt-Routine auf ein Ereignis reagieren oder aber auch mittels polling auf das interessierende Bit auf ein Ereignis reagieren.

In der ISR(INT0_vect) wird zum Beispiel auf TX_DS reagiert und auf MAX_RT. Soblad das Paket gesendet wurde wird TX_DS wieder zurückgesetzt und die Variable PTX auf 0 gesetzt. MAX_RT wird genutzt um die Daten noch einmal zu senden. Der TX-FIFO Speicher wird ja nicht gelöscht wenn eine Datenübertragung fehlgeschlagen hat.

Der Empfänger

So, ein nRF24L01+ Modul sollte mal funken. Um das zu kontrollieren brauchen wir jetzt unser zweites nRF24L01+ Modul mit dem wir die Daten empfangen. Ich verwende in dem Beispiel eine LCD-Anzeige um die Daten anzuzeigen auf das aber nicht weiter eingegangen wird. Wie die Daten angezeigt werden ist jedem selbst überlassen, genau so gut kann man mittels UART die Daten am PC anzeigen lassen oder was immer Euch zur Verfügung steht. Auch beim Empfänger nicht darauf vergessen die üblichen Stolpersteine wie -Os, µC-Frequenz etc. einzustellen.

Wie schon angekündigt können empfangene Daten mittels Polling abgefragt werden oder über IRQ. In dem Beispiel wird mittels Polling kontrolliert ob neue Daten angekommen sind.

/*
 * nRF24L01_Tutorial_RX.c
 *
 * Created: 06.01.2012 22:51:57
 *  Author: Ernst Buchmann
 */ 

#ifndef F_CPU				//define F_CPU if not done 
#define F_CPU 20000000UL
#endif

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdlib.h>
#include <string.h>
#include "spi.h"
#include "lcd-routines.h"
#include "wl_module.h"
#include "nRF24L01.h"

//Variablen
volatile uint8_t PTX;			//Global Variable
char itoabuffer[20];

int main(void)
{
	uint8_t payload[wl_module_PAYLOAD];		//holds the payload
	uint8_t nRF_status;						//STATUS information of nRF24L01+
	uint8_t zaehler = 0;
	
	lcd_init();
	lcd_clear();
	wl_module_init();		//Init nRF Module
	_delay_ms(50);			//wait for Module
	sei();					//activate Interrupts
	wl_module_config();		//config nRF as RX Module, simple Version
	
    while(1)
    {
		while (!wl_module_data_ready());			//waits for RX_DR Flag in STATUS
		nRF_status = wl_module_get_data(payload);	//reads the incoming Data to Array payload
		zaehler = payload[0];
		lcd_clear();
		lcd_home();
		itoa(zaehler, itoabuffer, 10);				//conversion into String
		lcd_string(itoabuffer);
		
    }
}

Das ist die ziemlich kurze main-Routine zum Abfragen der Daten die im Empfänger angekommen sind. Geübten Augen wird sofort auffallen, daß die Abfrage über Polling in diesem Beispiel den µC solange in einer while-Schleife fest hält bis neue Daten angekommen sind. Das ist natürlich nicht praktikabel wenn der µC noch andere Dinge zu erledigen hätte. Für ein erstes Übungsbeispiel wo der Fokus nur darauf liegt die Funkstrecke einzurichten soll uns das nicht stören. Schauen wir und das Programm im Detail an:

Achtung: F_CPU an die eigenen Gegebenheiten anpassen!

wl_module_init();

kennen wir schon von den Sender Einstellungen.

wl_module_config

Konfiguriert das nRF Modul mit den wesentlichen Registerinhalten und stellt das Modul auf Empfang. Es wurde hier absichtlich sehr kurz gehalten und nur die wesentlichsten Einstellungen getroffen.

void wl_module_config() 
// Sets the important registers in the wl-module and powers the module
// in receiving mode
{
    // Set RF channel
    wl_module_config_register(RF_CH,wl_module_CH);
	// Set data speed & Output Power configured in wl_module.h
	wl_module_config_register(RF_SETUP,wl_module_RF_SETUP);
	// Set length of incoming payload 
    wl_module_config_register(RX_PW_P0, wl_module_PAYLOAD);
	
    // Start receiver 
    PTX = 0;        // Start in receiving mode
    RX_POWERUP;     // Power up in receiving mode
    wl_module_CE_hi;     // Listening for pakets
}

Wie man sieht wurde hier nur die Payload-Größe für Pipe0 eingestellt und keine weiteren Empfangsadressen gesetzt. Das funktioniert weil beim Sender zwar Adressen konfiguriert wurden aber die Default-Adressen verwendet wurden. Man könnte sich für dieses Beispiel die Definition der Adressen beim Sender sparen und auch dort eine sehr verkürzte Konfiguration verwenden. Als Empfangskanal und -Geschwindigkeit müssen natürlich die gleichen Werte verwendet werden wie beim Sender. Auch die Payload-Größe muß im Empfänger zu der Anzahl an Bytes passen die der Sender schickt.

In dieser Zeile while (!wl_module_data_ready()); prüft das Programm ob im Empfangsmodul Daten angekommen sind die ausgelesen werden können und bleibt solange in dieser while-Schleife bis RX_DR gesetzt wurde.

Diese Funktion macht nichts weiter als das STATUS Register auszulesen und den Wert von RX_DR zurück zu geben:

extern uint8_t wl_module_data_ready() 
// Checks if data is available for reading
{
    if (PTX) return 0;
    uint8_t status;
    // Read wl_module status 
    wl_module_CSN_lo;                                // Pull down chip select
    status = spi_fast_shift(NOP);               // Read status register
    wl_module_CSN_hi;                                // Pull up chip select
    return status & (1<<RX_DR);
}

Sobald RX_DR 1 ist und damit anzeigt, daß Daten angekommen sind wird die while Schleife verlassen und

nRF_status = wl_module_get_data(payload);

ausgeführt.

Diese Funktion liest den Inhalt des RX-FIFO Speichers aus und gibt den Inhalt des STATUS Registers zurück. Anschließend wird noch das RX_DR Bit gelöscht um den nächsten Datenempfang detektieren zu können:

extern uint8_t wl_module_get_data(uint8_t * data) 
// Reads wl_module_PAYLOAD bytes into data array
{
	uint8_t status;
    wl_module_CSN_lo;                               // Pull down chip select
    status = spi_fast_shift( R_RX_PAYLOAD );            // Send cmd to read rx payload
    spi_transfer_sync(data,data,wl_module_PAYLOAD); // Read payload
    wl_module_CSN_hi;                               // Pull up chip select
    wl_module_config_register(STATUS,(1<<RX_DR));   // Reset status register
	return status;
}

Der Rest der main-Routine kümmert sich dann nur noch um die Ausgabe der Daten.

Zum Schluß noch eine kleine Aufgabe: Warum wird beim Empfänger keine Interruptfunktion ISR(INT0_vect) verwendet? Und löst RX_DR überhaupt einen Interrupt aus?

Beispieldateien

Hier finden sich die Dateien die in diesem Beispiel verwendet wurden:

Datei:Nrf24L01 Tutorial.zip

In wl_module.c finden sich alle Routinen die im Tutorial verwendet wurden und auch noch viel, viel mehr. Es sind alle Routinen vorhanden um sehr schnell einen Multi-Ceiver mit 6 Data-Pipes aufzubauen oder verschiedene interessante Daten auszulesen. Das Prinzip sollte nach diesem Tutorial, so hoffe ich, klar sein um diese Routinen zu durchschauen und selbst anzuwenden oder anzupassen.

Zusammenfassung

Noch ein paar wesentliche Punkte zu den Funkmodulen und ersten Schritten hier im Tutorial:

1. Achtung mit der Spannung, die Module vertragen wirklich keine höhere Spannung als im Datenblatt angegeben wie ich leider feststellen durfte.

2. Wie schon angesprochen eignet sich die Polling-Variante im Empfänger-Beispiel nur bedingt für die Umsetzung in eigenen Projekten da sie den µC bis zum nächsten Dateneingang lahm legt. Und sollten keine Daten kommen, dann ist der µC in dieser Zeile ad infinitum gefangen.

3. Es kann sein, wenn die Daten sehr schnell kommen, daß man ein RX_DR versäumt. Der FIFO hat ja Platz für 3xPayload. Man sollte also durchaus auch abfragen ob noch weitere Daten im FIFO vorhanden sind. Insbesondere wenn mit Multi-Ceivern gearbeitet wird.

4. Für kurze Distanzen funktioniert die Datenübertragung mit 2Mbps sehr gut. Wenn längere Distanzen überwunden werden sollen, dann sollte man auf 250kbps zurückgehen, das erhöht die Empfindlichkeit des Empfängers. Der Sender sollte mit maximaler Leistung senden. Außerdem sollte der Payload nur so groß sein wie auch wirklich benötigt. Je weniger Bytes übertragen werden desto größer die Chance auf erfolgreiche Datenübertragung. Bei Bedarf gibt es auch Module die den Anschluß einer externen Antenne ermöglichen.

5. Man sollte die Möglichkeiten nutzen die Enhanced Shockburst bietet. Das TX_DS Bit wird dann nur gesetzt wenn auch wirklich ein ACK vom Empfänger eingetroffen ist. (Im Tutorial wird übrigens Enhanced Shockburst verwendet. Details dazu hätten aber das Tutorial gesprengt)

6. Beim Multi-Ceiver Betrieb sollte für jeden Sender ein, sich von den anderen Sendern unterscheidendes, AUTO RETRANSMIT DELAY (ARD) eingestellt werden. Das erhöht die Chancen, daß sich die Sender nicht gegenseitig stören.

7. Aufpassen beim AUTO RETRANSMIT DELAY wenn mit 250kpbs gesendet wird. Der Default-Wert von 250µS ist zu kurz um eine funktionierende Datenübertragung zu gewährleisten. Für Details siehe Datenblatt.

8. Die Adressen der Pipes können nicht beliebig gewählt werden, wie die Erfahrunng zeigt, sondern müssen ausreichend Bitwechsel haben, andernfalls kommt es zu häufigen Fehlübertragungen oder "false positives", d.h. es wird zwar eine Payload erkannt und das Flag gesetzt aber diese ist nur Datenmüll. Adressen wie 0xaa oder 0x55 sind nicht zu empfehlen.

Nachrichten an den Autor: PuraVida

Einzelnachweise