Difference between revisions of "PIC32MX: High-speed Wireless Communication"

From Mech
Jump to navigationJump to search
 
(73 intermediate revisions by 3 users not shown)
Line 1: Line 1:
== Original Assignment ==
== Overview ==


[[Image:nrf-pic1.jpg|thumb|nrf24L01+ wireless chip with pins attatched]]
'''Do not erase this section!'''
This wiki describes how to wire a [http://www.sparkfun.com/commerce/product_info.php?products_id=691 nRF24L01+ chip] ("wireless chip") to a PIC32 board and includes annotated code describing the functions that facilitate the transmitting and receiving of wireless data. A [[media:Wireless-ppt.pdf|PowerPoint presentation]] covering much of the introductory information available on this page is also available.


To operate the system, you must properly connect the wireless chips to the PIC32s, load the transmitter code onto the PIC that you wish to act as a transmitter, load the receiver code onto the PIC that you wish to act as a receiver, properly connect an RS232 cable to the PIC and open a serial communication program (e.g. HyperTerminal or [http://chiark.greenend.org.uk/~sgtatham/putty/download.html putty]), and turn the NU32 boards on. If you use the c-files provided on this pages (and everything else is set up properly), the transmitting PIC will broadcast any characters received from the PC via RS232, and the receiving PIC will display what it receives from wireless link on the PC via RS232. The payload width can be adjusted using the "[" and "]" keys on both the transmitting and receiving PICs. Both wireless chips must be set to the same payload width in order to communicate. Payload widths may vary from 1 to 32. The transmitting PIC will not transmit until enough characters have been entered to fill a payload. Once enough characters have been entered into the transmitting PIC, the serial display will move to a new line to signal transmission.
Your assignment is to create the circuit and code that allows two PIC32s to use wireless communication at high speeds using the Transceiver nRF24L01+ Module with Chip Antenna from [http://www.sparkfun.com/commerce/product_info.php?products_id=691 Sparkfun].


==Chip Basics==
Read up on the nRF24L01+ Module commands and create functions to allow a PIC32 to initialize and send and receive data.
The nrf24L01+ Module is a breakout of the nrf24L01+ 2.4 GHz transceiver, with an on board antenna. In order to get a wireless link up and running, the following parameters must be consistent between a transmitter and receiver chip:


===Channels===
Test your code by sending 1000 bytes of data from one PIC32 to the other and echo the data back. How long does this transmission take? How far can you separate the PIC32s before the transmission is unreliable?
The channel of the nrf24L01+ chip is the frequency that it broadcasts and receives data from. The channel can range from 2.4 GHz to 2.525 GHz, at intervals no more than 1 MHz. The exact distance between channels is dependent on the on air data rate. Two wireless chips must be tuned to the same channel in order to communicate. See the Air Data Rate section for necessary channel spacing.


== Overview ==
===Air Data Rate===
The air data rate is the rate at which the chip transmits bits wirelessly (and the rate at which it looks for them). The nrf24L01+ chip has three selectable data rates: 250 kbps, 1 Mbps, and 2 Mbps. Faster data rates draw less current, but require a greater distance between channels. Slower data rates give you the option of more channels and better receiver sensitivity, for the obvious trade off of speed. Data rates of 250 kbps and 1 Mbps require channel spacing of at least 1 MHz (allowing for 125 unique channels), and a data rate of 2 Mbps requires spacing of 2 MHz between channels (allowing for 62 channels). Two wireless chips will not be able to communicate if they are not set to the same on air data rate.


===Payload Width===
'''The goal of this project is to transmit a byte of data (e.g. a character) from one PIC32/nRF24L01+ board to the other board, and display the received character on a PC screen.'''
The payload is the actual data you are trying to send over a wireless link, and the payload width is how many bytes are contained within each payload. The nfr24L01+ is capable of supporting payloads up to 32 bytes. A transmitter and receiver must be set to the same payload in order to communicate. Transmission speeds for different sizes payloads are analyzed below.


===Addresses & Pipes===
This wiki describes how to wire a [http://www.sparkfun.com/commerce/product_info.php?products_id=691 nRF24L01+ chip] ("wireless chip") to the transmitting PIC32 board ("transmitter") and receiving PIC32 board ("receiver"), and includes annotated code describing the functions that facilitate the wireless communication.
Every wireless transmission is preceded by the address of the intended receiver. The nrf24L01+ chip has 6 different receiving pipes, which means it may act as 6 receivers with 6 different addresses. Addresses may be three, four, or five bytes long. The addresses for pipe 0 and 1 may be completely different, but the addresses for pipes 2, 3, and 4 may only differ from the address of pipe 1 by their least significant byte. All active pipes feed into the same 3-packet-deep incoming transmission queue. Each pipe may also be set to a expect a different payload width.


== Circuit ==
== Circuit ==
Line 19: Line 25:
[[Image:Wireless-circuit.jpg|thumb|Schematic of the nRF24L01+ connected to the PIC32_NU32 board]]
[[Image:Wireless-circuit.jpg|thumb|Schematic of the nRF24L01+ connected to the PIC32_NU32 board]]


The wireless chip has 8 pins that need to be wired to the PIC:
The nrf24L01+ chip has 8 pins that need to be wired to the PIC (all I/O are relative to PIC):


(1) Vcc -> +3.3 Vdc supply<br>
*Vcc: +3.3 Vdc supply<br>
(2) CE -> uC pin PORTC.1, configured as GPIO/input<br>
*CE: uC pin C1 (input)<br>
(3) CSN -> uC pin PORTC.2, configured as GPIO/output<br>
*CSN: uC pin C2 (output) <br>
(4) SCK -> uC pin PORTC.3/SCK, configured as SCK(SPI)/output<br>
*SCK: uC pin D10, configured as SCK(SPI) (output)<br>
(5) MOSI -> uC pin PORTC.5/SDO, configured as SDO(SPI)/output<br>
*MOSI: uC pin D0, configured as SDO(SPI) (output) <br>
(6) MISO -> uC pin PORTC.4/SDI, configured as SDI(SPI)/input<br>
*MISO: uC pin C4, configured as SDI(SPI) (input)<br>
(7) IRQ -> uC pin PORTB.0, configured as GPIO/input<br>
*IRQ: uC pin B0 (input) <br>
(8) GND -> common digital ground<br>
*GND: common digital ground<br>


The +3.3V and ground are provided by the NU32 board. The term "uC" above stands for microcontroller, which is the PIC32 in our case. GPIO stands for General Purpose Input/Output.
The +3.3V and ground are provided by the NU32 board. The term "uC" above stands for microcontroller, which is the PIC32 in our case.


Descriptions of the wireless chip's pins can be found in Brennan Ball's DIYembedded [http://www.diyembedded.com/tutorials/nrf24l01_0/nrf24l01_tutorial_0.pdf Tutorial 0]. The SCK, MOSI, and MISO pins interface with the SPI on the PIC32 which allows for communication between the PIC and the wireless chip. The GPIO pins will be initialized as digital inputs or outputs in the code to enable/disable the wireless chip and check the wireless data transfer/receive stats.
Descriptions of the wireless chip's pins can be found in Brennan Ball's DIYembedded [http://www.diyembedded.com/tutorials/nrf24l01_0/nrf24l01_tutorial_0.pdf Tutorial 0]. The SCK, MOSI, and MISO pins interface with the SPI on the PIC32 which allows for communication between the PIC and the wireless chip. The GPIO pins will be initialized as digital inputs or outputs in the code to enable/disable the wireless chip and check the wireless data transfer/receive stats.
Line 36: Line 42:
In order to print information from the PIC to the PC, we need to wire an [[PIC_RS232]] cable's transfer and receive lines to the UART ports of the NU32 board (F4, F5).
In order to print information from the PIC to the PC, we need to wire an [[PIC_RS232]] cable's transfer and receive lines to the UART ports of the NU32 board (F4, F5).


== Code ==
==Code==


===Driver===
<math>
Besides the main C files for the transmitter and receiver, the project also needs the [[media:Wireless-files.zip|nRF24L01.h header file and C file]]. These driver files were created by Brennan Ball, and are explained in detail in his [http://blog.diyembedded.com/ tutorials]. The following things must be changed in the header file available on his website to make it compatible with the NU32 board:
/* **************************************************************
*pin registers and masks must be set to the appropriate values
*the "HardwareProfile.h" NU32 specific file must be included
WirelessTransmitter.c

Lab 5: High-speed Wireless Communication
These changes are accounted for the the driver available on this page. All of the SPI functions trace back to a single function to send and read a single byte, which must be defined in the main c file.
Jonathan Drake, Caitlin Ramsey

2010-02-16
This function is available here:

This code initializes the nRF24L01+ chip as a transmitter and
<code><pre>
continuously transmits a character every 100ms. The loop contains

code to both output the program's status to the PC via RS232
unsigned char spi_send_read_byte(unsigned char byte) {
cable and onboard LEDs.
unsigned short txData, rxData; // transmit, receive characters
int chn = 1; // SPI channel to use (1 or 2)
************************************************************** */
txData = byte; // take inputted byte and store into txData
SpiChnPutC(chn, txData); // send data
// Includes
rxData = SpiChnGetC(chn); // retreive over channel chn the received data into rxData
#include "HardwareProfile.h"
#include "nrf24l01.h"
return rxData;
}
// Function Declarations
</pre></code>
// Note: Descriptions of functions found after main() function
===Initialization Functions===
void Initialize(void);
In order to get a wireless link up and running, you will need to initialize both the wireless chip and SPI communication on the PIC.
void InitializeIO(void);
To intialize SPI on the PIC, create the following function in your main C file:
void SpiInitDevice(int chn, int isMaster, int frmEn, int frmMaster);
<code><pre>void SpiInitDevice(int chn, int isMaster, int frmEn, int frmMaster) {
void initUART2(int pbClk);
unsigned int config = SPI_CON_MODE8|SPI_CON_SMP|SPI_CON_ON; // SPI configuration word
unsigned char spi_send_read_byte(unsigned char byte);
if(isMaster)
void Delayms( unsigned t);
{
void Delayus( unsigned t);
config|=SPI_CON_MSTEN;
void Delaytus( unsigned t);
}
if(frmEn)
// Constants
{
#define DESIRED_BAUDRATE (19200) // The desired BaudRate for RS232 communication w/ UART
config|=SPI_CON_FRMEN;
if(!frmMaster) {
// Main Function
config|=SPI_CON_FRMSYNC;
int main(void)
}
{
}
Initialize(); // initialize onboard LEDs, IO pins, UART2, SPI1, set up nRF24L01+ as TX
SpiChnOpen(chn, config, 4); // divide fpb by 4, configure the I/O ports. Not using SS in this example
}
char RS232_Out_Buffer[64]; // buffer for printing data to the screen via UART
</pre></code>
unsigned char data = 'x'; // char to be transmitted
You can then call this function with the call <code> SpiInitDevice(1,1,0,0) </code> to initalize SPI channel 1 on the PIC.

// print char to screen
To initialize the nrf24L01+ chip itself, you have to options. You can either use <code> nrf24L01_initialize(..) </code> which gives you the opportunity to manually define every register in the chip right from the start. However, this function has 21 arguments, so it is not the most convenient to use if you don't have to. Instead, you can use <code> nrf24L01_initialize_debug(bool rx, int width, bool auto_ack_on) </code>, which gives you the option to specify whether or not the chip is a receiver or transmitter, what the payload width is, and whether or not to enable auto-acknowledge. However, it limits you to Pipe 0, an air data rate of 2 Mpbs, and RF Channel 2. However, these limitations can be adjusted easily after initialization using additional functions (see the library for all available functions). So, for example, to initialize your chip as a transmitter with a payload width of one byte and no auto-acknowledgement, you could call <code> nrf24L01_initialize_debug(0,1,0)</code>.
sprintf(RS232_Out_Buffer, "data to send = %c\r\n", data);

putsUART2(RS232_Out_Buffer);
If you wish to display information to the screen or receive information from the keyboard, you will have to initialize the RS232 connection as well. This can be done with the following code:

while(1)
<code><pre>
{
int pbClk = SYSTEMConfigPerformance(SYS_FREQ);
putsUART2("...\r"); // print ... to show transmission
initUART2(pbClk);
nrf24l01_irq_clear_all(); //clear all interrupts in the 24L01

nrf24l01_set_as_tx(); // force 24L01 to be a transmitter (this is a double-check)
void initUART2(int pbClk) {
#define config1 UART_EN | UART_IDLE_CON | UART_RX_TX | UART_DIS_WAKE | UART_DIS_LOOPBACK | UART_DIS_ABAUD | UART_NO_PAR_8BIT | UART_1STOPBIT | UART_IRDA_DIS | UART_DIS_BCLK_CTS_RTS| UART_NORMAL_RX | UART_BRGH_SIXTEEN
//transmit received char over RF, length = 1 byte, true = send away
#define config2 UART_TX_PIN_LOW | UART_RX_ENABLE | UART_TX_ENABLE | UART_INT_TX | UART_INT_RX_CHAR | UART_ADR_DETECT_DIS | UART_RX_OVERRUN_CLEAR
nrf24l01_write_tx_payload(&data, 1, true);
OpenUART2( config1, config2, pbClk/16/DESIRED_BAUDRATE-1); // calculate actual BAUD generate value.
ConfigIntUART2(UART_INT_PR2 | UART_RX_INT_EN);
//INTEnableSystemMultiVectoredInt(); //interrupts enabled?
}
</pre></code>

===Transmitter Code===

Wirelessly transmitting data requires three steps:

*initialize nrf24L01+ as transmitter
*transmit packet
*wait for payload to be sent

A basic outline for such a procedure would look like:
<code><pre>
char data = 'x'; //data to send
int width = 1; //width of payload

nrf24l01_initialize_debug(false, width, false); //initialize the 24L01 to the debug configuration as TX, 1 data byte, and auto-ack disabled

nrf24l01_write_tx_payload(&data, width, true); //add char to Tx queue, and transmit immediately (char * data, int width, bool transmit_now)

while(!(nrf24l01_irq_pin_active() && nrf24l01_irq_tx_ds_active())); //wait until IRQ status tells us that it is done transmitting

nrf24l01_irq_clear_all(); //clear all interrupts in the 24L01
</pre></code>

A fully functional [[media:nrf-tx.c|transmitter c-file]] is available. It is intended to work with the receiving code file, available below, and allows for a variable payload.

===Receiver Code===
Similarly, receiving data from a wireless link requires three steps:
*initialize nrf24L01+ as receiver
*wait for incoming packet
*write packet to memory

That would look like:
<pre>
char data; //variable to recieve incoming data
int width = 1; //width of payload

nrf24l01_initialize_debug(true, width, false); //initialize the 24L01 to the debug configuration as RX, 1 data byte, and auto-ack disabled

while(!(nrf24l01_irq_pin_active() && nrf24l01_irq_rx_dr_active())); //wait to receive a packet

nrf24l01_read_rx_payload(&data, width); //get the payload into data

nrf24l01_irq_clear_all(); //clear interrupts
</pre>

A fully functional [[media:nrf-rx.c|receiver c-file]] is available. It is intended to work with the transmitter code file, available above, and allows for a variable payload.

===Handling Multi-byte Payloads===
Transmitting and receiving payloads of widths larger than one byte are handled the same exact way as described as above, except that instead of having a a single char data variable, you would have an array of chars as your data variable. Then rather then pass in the memory of address of <code>data </code> you can simply pass in <code>data</code> itself, because in C, the variable representing an array is synonymous with the memory address of its first element. Examples of this are available in the attached C files.

===Bidirectional Communication===
While technically the nrf24l01+ chip can only be in one mode (transmitting/receiving) at a given instant, with some clever programming you can still get a bidirectional link established between two chips. The [[media:nrf-bidirectional.c| full code for bidirectional communication]] is available on this page. Basically, the chip sits in receiver mode, listening for any incoming packets. If it receives one, it displays it to the screen. However, there in the event that a key is pressed on the local terminal, the PIC switches the mode from receiving to transmitting, transmits the data received locally, and then goes back to receiver mode. It is not a perfect system because if a packet reaches a chip while the chip is in the middle of transmitting a packet itself, it will not pick up the packet in the air. However, the chance of this sort of situation happening would be rare, depending on the application of the system.

To establish a bidirectional link, both PICs should have the same bidirectional c-file loaded (but this is not necessary: e.g. one PIC could only be able to transmit, and it would still be a viable one-way link ). If the on air data rate is set to 1 Mbps, then this bidirectional c-file is compatible with the [http://www.sparkfun.com/commerce/product_info.php?products_id=9019 Nordic USB Serial Converter] (note that the Nordic USB-Serial converter is only compatible with a payload width of four bytes). The payload width of the bidirectional link can be adjusted with the "[" and "]" keys, exactly as with the above code.

==Latency and Data Rates==

Basic timed tests were done to find out how long it took to transmit and receive different sized payloads, as well as the latency involved with transmission.

To measure transmission latency, an oscilloscope was used to time how long it took before transmission was complete. For instance, something like this was used:
[[Image:nrf-latency.jpg|thumb|Plot of Transmission Latency]]
[[Image:bps-nrf24l01.jpg|thumb|Plot of Data Rates]]
<code><pre>
while(1){
if (!swUser){
PIN_D1=1; //raise test pin

nrf24l01_write_tx_payload(dataToSend, width, true); //transmit char array over RF
// is the 24L01 activated?
//wait until the packet has been sent
if (nrf24l01_irq_pin_active()) {
while(!(nrf24l01_irq_pin_active() && nrf24l01_irq_tx_ds_active()));

mLED_1_On();
PIN_D1=0; //lower test pin
} else {
nrf24l01_irq_clear_all(); //clear all interrupts in the 24L01
mLED_1_Off();
}
}
}
</pre></code>
// is the 24L01 an active transmitter?

if (nrf24l01_irq_tx_ds_active()) {
Three measurements of time to transmit were done for each payload size. It should be noted that these results are completely independent of whether or not the transmission successfully made it to a receiver; in fact, these tests were done without a receiving PIC. The results seem to indicate a latency of 178 microseconds.
mLED_2_On();

} else {
Throughput tests were done by sending a packet wirelessly, then timing how long it took to get it back. A pin was raised to high while waiting and then dropped when it was received. These periods were then measured on an oscilloscope, and then averaged. The times used were only those that had 100% return rate. However, the accuracy of these packets was not checked beyond the built in CRC error-checking within the packets themselves. The rates here are to go send out a transmission and then receive it back, so the rate should be doubled for only transmitting in one direction.
mLED_2_Off();
}
// wait until the packet has been sent or the maximum number of retries has been reached
while(!(nrf24l01_irq_pin_active() && nrf24l01_irq_tx_ds_active()));
// toggle onboard LED 3 to indicate loop iterating
mLED_3_Toggle();
Delayms(100);
}
}
// Initialize all initialize functions (function descriptions to follow)
void Initialize(void)
{
// initialize peripheral bus clock to SYS_FREQ (80 MHz)
int pbClk;
pbClk = SYSTEMConfigPerformance(SYS_FREQ);
// initialize UART
initUART2(pbClk);
// initialize input/output pins
InitializeIO();
// initialize the SPI channel 1 as master, no frame mode
SpiInitDevice(1, 1, 0, 0);
//initialize 24L01 to debug configuration as transfer, 1 byte data size, and auto-acknowledge mode disabled
nrf24l01_initialize_debug(false, 1, false);
// initialize all onboard LEDs on NU32
mInitAllLEDs();
// if program initialized correctly, indicate on PC & onboard LEDs
putsUART2("\r\n***** System Initialized *****\r\n");
mLED_0_On();
}
// initialize IO pins
void InitializeIO(void)
{
TRISBbits.TRISB0 = 1; // set IRQ pin as input
TRISC = 0xFFF9; // make CSN, CE digital outputs
PORTC = 0x0004; // set CSN bit active
}
// initialize UART2
void initUART2(int pbClk)
{
#define config1 UART_EN | UART_IDLE_CON | UART_RX_TX | UART_DIS_WAKE | UART_DIS_LOOPBACK | UART_DIS_ABAUD | UART_NO_PAR_8BIT | UART_1STOPBIT | UART_IRDA_DIS | UART_DIS_BCLK_CTS_RTS| UART_NORMAL_RX | UART_BRGH_SIXTEEN
#define config2 UART_TX_PIN_LOW | UART_RX_ENABLE | UART_TX_ENABLE | UART_INT_TX | UART_INT_RX_CHAR | UART_ADR_DETECT_DIS | UART_RX_OVERRUN_CLEAR
OpenUART2( config1, config2, pbClk/16/DESIRED_BAUDRATE-1); // calculate actual BAUD generate value.
ConfigIntUART2(UART_INT_PR2 | UART_RX_INT_EN);
}
// initialize SPI
void SpiInitDevice(int chn, int isMaster, int frmEn, int frmMaster)
{
unsigned int config = SPI_CON_MODE16|SPI_CON_SMP|SPI_CON_ON; // SPI configuration word
if(isMaster)
{
config|=SPI_CON_MSTEN;
}
if(frmEn)
{
config|=SPI_CON_FRMEN;
if(!frmMaster)
{
config|=SPI_CON_FRMSYNC;
}
}
SpiChnOpen(chn, config, 4); // divide fpb by 4, configure the I/O ports. Not using SS in this example
}
/* ******************************************************
Function: spi_send_read_byte
Inputs: unsigned char byte
Outputs: unsigned short rxData
This function is required by the 24L01 header file.
It takes the inputted byte, sends it over the chosen
SPI channel, then waits to retreive data back.
****************************************************** */
unsigned char spi_send_read_byte(unsigned char byte)
{
unsigned short txData, rxData; // transmit, receive characters
int chn = 1; // SPI channel to use (1 or 2)
txData = byte; // take inputted byte and store into txData
SpiChnPutC(chn, txData); // send data
rxData = SpiChnGetC(chn); // retreive over channel chn the received data into rxData
return rxData;
}
/* ******************************************************
Functions: Delayms, Delayus, Delaytus
Inputs: unsigned t
Outputs: none
These functions are required by the 24L01 header file.
They take an input of t (ms, us, us/10) and delay the
program.
****************************************************** */
void Delayms( unsigned t)
// This uses Timer 1, can be changed to another timer. Assumes FPB = SYS_FREQ
{
OpenTimer1(T1_ON | T1_PS_1_256, 0xFFFF);
while (t--)
{ // t x 1ms loop
WriteTimer1(0);
while (ReadTimer1() < SYS_FREQ/256/1000);
}
CloseTimer1();
} // Delayms
void Delayus( unsigned t)
{
OpenTimer1(T1_ON | T1_PS_1_256, 0xFFFF);
while (t--)
{ // t x 1ms loop
WriteTimer1(0);
while (ReadTimer1() < SYS_FREQ/256/1000000);
}
CloseTimer1();
}// Delayus
void Delaytus( unsigned t)
{
OpenTimer1(T1_ON | T1_PS_1_256, 0xFFFF);
while (t--)
{ // t x 1ms loop
WriteTimer1(0);
while (ReadTimer1() < SYS_FREQ/256/10000000);
}
CloseTimer1();
}// Delaytus
</math>

Latest revision as of 10:10, 17 September 2010

Overview

nrf24L01+ wireless chip with pins attatched

This wiki describes how to wire a nRF24L01+ chip ("wireless chip") to a PIC32 board and includes annotated code describing the functions that facilitate the transmitting and receiving of wireless data. A PowerPoint presentation covering much of the introductory information available on this page is also available.

To operate the system, you must properly connect the wireless chips to the PIC32s, load the transmitter code onto the PIC that you wish to act as a transmitter, load the receiver code onto the PIC that you wish to act as a receiver, properly connect an RS232 cable to the PIC and open a serial communication program (e.g. HyperTerminal or putty), and turn the NU32 boards on. If you use the c-files provided on this pages (and everything else is set up properly), the transmitting PIC will broadcast any characters received from the PC via RS232, and the receiving PIC will display what it receives from wireless link on the PC via RS232. The payload width can be adjusted using the "[" and "]" keys on both the transmitting and receiving PICs. Both wireless chips must be set to the same payload width in order to communicate. Payload widths may vary from 1 to 32. The transmitting PIC will not transmit until enough characters have been entered to fill a payload. Once enough characters have been entered into the transmitting PIC, the serial display will move to a new line to signal transmission.

Chip Basics

The nrf24L01+ Module is a breakout of the nrf24L01+ 2.4 GHz transceiver, with an on board antenna. In order to get a wireless link up and running, the following parameters must be consistent between a transmitter and receiver chip:

Channels

The channel of the nrf24L01+ chip is the frequency that it broadcasts and receives data from. The channel can range from 2.4 GHz to 2.525 GHz, at intervals no more than 1 MHz. The exact distance between channels is dependent on the on air data rate. Two wireless chips must be tuned to the same channel in order to communicate. See the Air Data Rate section for necessary channel spacing.

Air Data Rate

The air data rate is the rate at which the chip transmits bits wirelessly (and the rate at which it looks for them). The nrf24L01+ chip has three selectable data rates: 250 kbps, 1 Mbps, and 2 Mbps. Faster data rates draw less current, but require a greater distance between channels. Slower data rates give you the option of more channels and better receiver sensitivity, for the obvious trade off of speed. Data rates of 250 kbps and 1 Mbps require channel spacing of at least 1 MHz (allowing for 125 unique channels), and a data rate of 2 Mbps requires spacing of 2 MHz between channels (allowing for 62 channels). Two wireless chips will not be able to communicate if they are not set to the same on air data rate.

Payload Width

The payload is the actual data you are trying to send over a wireless link, and the payload width is how many bytes are contained within each payload. The nfr24L01+ is capable of supporting payloads up to 32 bytes. A transmitter and receiver must be set to the same payload in order to communicate. Transmission speeds for different sizes payloads are analyzed below.

Addresses & Pipes

Every wireless transmission is preceded by the address of the intended receiver. The nrf24L01+ chip has 6 different receiving pipes, which means it may act as 6 receivers with 6 different addresses. Addresses may be three, four, or five bytes long. The addresses for pipe 0 and 1 may be completely different, but the addresses for pipes 2, 3, and 4 may only differ from the address of pipe 1 by their least significant byte. All active pipes feed into the same 3-packet-deep incoming transmission queue. Each pipe may also be set to a expect a different payload width.

Circuit

Schematic of the nRF24L01+ connected to the PIC32_NU32 board

The nrf24L01+ chip has 8 pins that need to be wired to the PIC (all I/O are relative to PIC):

  • Vcc: +3.3 Vdc supply
  • CE: uC pin C1 (input)
  • CSN: uC pin C2 (output)
  • SCK: uC pin D10, configured as SCK(SPI) (output)
  • MOSI: uC pin D0, configured as SDO(SPI) (output)
  • MISO: uC pin C4, configured as SDI(SPI) (input)
  • IRQ: uC pin B0 (input)
  • GND: common digital ground

The +3.3V and ground are provided by the NU32 board. The term "uC" above stands for microcontroller, which is the PIC32 in our case.

Descriptions of the wireless chip's pins can be found in Brennan Ball's DIYembedded Tutorial 0. The SCK, MOSI, and MISO pins interface with the SPI on the PIC32 which allows for communication between the PIC and the wireless chip. The GPIO pins will be initialized as digital inputs or outputs in the code to enable/disable the wireless chip and check the wireless data transfer/receive stats.

In order to print information from the PIC to the PC, we need to wire an PIC_RS232 cable's transfer and receive lines to the UART ports of the NU32 board (F4, F5).

Code

Driver

Besides the main C files for the transmitter and receiver, the project also needs the nRF24L01.h header file and C file. These driver files were created by Brennan Ball, and are explained in detail in his tutorials. The following things must be changed in the header file available on his website to make it compatible with the NU32 board:

  • pin registers and masks must be set to the appropriate values
  • the "HardwareProfile.h" NU32 specific file must be included

These changes are accounted for the the driver available on this page. All of the SPI functions trace back to a single function to send and read a single byte, which must be defined in the main c file.

This function is available here:


 unsigned char spi_send_read_byte(unsigned char byte) {
  	 unsigned short	txData, rxData; // transmit, receive characters
   	 int chn = 1; // SPI channel to use (1 or 2)
  	 
   	 txData = byte; // take inputted byte and store into txData   
   	 SpiChnPutC(chn, txData);			// send data
 	 rxData = SpiChnGetC(chn);			// retreive over channel chn the received data into rxData
    
  	 return rxData; 
 }

Initialization Functions

In order to get a wireless link up and running, you will need to initialize both the wireless chip and SPI communication on the PIC. To intialize SPI on the PIC, create the following function in your main C file:

void SpiInitDevice(int chn, int isMaster, int frmEn, int frmMaster) {
   	 unsigned int config = SPI_CON_MODE8|SPI_CON_SMP|SPI_CON_ON;	// SPI configuration word
   	 if(isMaster)
   	 {
   		 config|=SPI_CON_MSTEN;
   	 }
   	 if(frmEn)
   	 {
   		 config|=SPI_CON_FRMEN;
   		 if(!frmMaster) {   
   			 config|=SPI_CON_FRMSYNC;
      	 }
   	 }
  	 SpiChnOpen(chn, config, 4);	// divide fpb by 4, configure the I/O ports. Not using SS in this example
 }

You can then call this function with the call SpiInitDevice(1,1,0,0) to initalize SPI channel 1 on the PIC.

To initialize the nrf24L01+ chip itself, you have to options. You can either use nrf24L01_initialize(..) which gives you the opportunity to manually define every register in the chip right from the start. However, this function has 21 arguments, so it is not the most convenient to use if you don't have to. Instead, you can use nrf24L01_initialize_debug(bool rx, int width, bool auto_ack_on) , which gives you the option to specify whether or not the chip is a receiver or transmitter, what the payload width is, and whether or not to enable auto-acknowledge. However, it limits you to Pipe 0, an air data rate of 2 Mpbs, and RF Channel 2. However, these limitations can be adjusted easily after initialization using additional functions (see the library for all available functions). So, for example, to initialize your chip as a transmitter with a payload width of one byte and no auto-acknowledgement, you could call nrf24L01_initialize_debug(0,1,0).

If you wish to display information to the screen or receive information from the keyboard, you will have to initialize the RS232 connection as well. This can be done with the following code:

int pbClk = SYSTEMConfigPerformance(SYS_FREQ);
initUART2(pbClk);

 void initUART2(int pbClk) {
  	 #define config1 	UART_EN | UART_IDLE_CON | UART_RX_TX | UART_DIS_WAKE | UART_DIS_LOOPBACK | UART_DIS_ABAUD | UART_NO_PAR_8BIT | UART_1STOPBIT | UART_IRDA_DIS | UART_DIS_BCLK_CTS_RTS| UART_NORMAL_RX | UART_BRGH_SIXTEEN
  	 #define config2		UART_TX_PIN_LOW | UART_RX_ENABLE | UART_TX_ENABLE | UART_INT_TX | UART_INT_RX_CHAR | UART_ADR_DETECT_DIS | UART_RX_OVERRUN_CLEAR	
   	 OpenUART2( config1, config2, pbClk/16/DESIRED_BAUDRATE-1);	// calculate actual BAUD generate value.
   	 ConfigIntUART2(UART_INT_PR2 | UART_RX_INT_EN);
   	 //INTEnableSystemMultiVectoredInt(); //interrupts enabled?
 }

Transmitter Code

Wirelessly transmitting data requires three steps:

  • initialize nrf24L01+ as transmitter
  • transmit packet
  • wait for payload to be sent

A basic outline for such a procedure would look like:

char data = 'x'; //data to send
int width = 1; //width of payload

nrf24l01_initialize_debug(false, width, false); //initialize the 24L01 to the debug configuration as TX, 1  data byte, and auto-ack disabled

nrf24l01_write_tx_payload(&data, width, true); //add char to Tx queue, and transmit immediately  (char * data, int width, bool transmit_now)

while(!(nrf24l01_irq_pin_active() && nrf24l01_irq_tx_ds_active())); //wait until IRQ status tells us that it is done transmitting

nrf24l01_irq_clear_all(); //clear all interrupts in the 24L01

A fully functional transmitter c-file is available. It is intended to work with the receiving code file, available below, and allows for a variable payload.

Receiver Code

Similarly, receiving data from a wireless link requires three steps:

  • initialize nrf24L01+ as receiver
  • wait for incoming packet
  • write packet to memory

That would look like:

char data; //variable to recieve incoming data
int width = 1; //width of payload

nrf24l01_initialize_debug(true, width, false); //initialize the 24L01 to the debug configuration as RX, 1  data byte, and auto-ack disabled

while(!(nrf24l01_irq_pin_active() && nrf24l01_irq_rx_dr_active())); //wait to receive a packet

nrf24l01_read_rx_payload(&data, width); //get the payload into data

nrf24l01_irq_clear_all(); //clear interrupts

A fully functional receiver c-file is available. It is intended to work with the transmitter code file, available above, and allows for a variable payload.

Handling Multi-byte Payloads

Transmitting and receiving payloads of widths larger than one byte are handled the same exact way as described as above, except that instead of having a a single char data variable, you would have an array of chars as your data variable. Then rather then pass in the memory of address of data you can simply pass in data itself, because in C, the variable representing an array is synonymous with the memory address of its first element. Examples of this are available in the attached C files.

Bidirectional Communication

While technically the nrf24l01+ chip can only be in one mode (transmitting/receiving) at a given instant, with some clever programming you can still get a bidirectional link established between two chips. The full code for bidirectional communication is available on this page. Basically, the chip sits in receiver mode, listening for any incoming packets. If it receives one, it displays it to the screen. However, there in the event that a key is pressed on the local terminal, the PIC switches the mode from receiving to transmitting, transmits the data received locally, and then goes back to receiver mode. It is not a perfect system because if a packet reaches a chip while the chip is in the middle of transmitting a packet itself, it will not pick up the packet in the air. However, the chance of this sort of situation happening would be rare, depending on the application of the system.

To establish a bidirectional link, both PICs should have the same bidirectional c-file loaded (but this is not necessary: e.g. one PIC could only be able to transmit, and it would still be a viable one-way link ). If the on air data rate is set to 1 Mbps, then this bidirectional c-file is compatible with the Nordic USB Serial Converter (note that the Nordic USB-Serial converter is only compatible with a payload width of four bytes). The payload width of the bidirectional link can be adjusted with the "[" and "]" keys, exactly as with the above code.

Latency and Data Rates

Basic timed tests were done to find out how long it took to transmit and receive different sized payloads, as well as the latency involved with transmission.

To measure transmission latency, an oscilloscope was used to time how long it took before transmission was complete. For instance, something like this was used:

Plot of Transmission Latency
Plot of Data Rates
while(1){
	if (!swUser){
			PIN_D1=1; //raise test pin

			nrf24l01_write_tx_payload(dataToSend, width, true); //transmit char array over RF
			
			//wait until the packet has been sent
			while(!(nrf24l01_irq_pin_active() && nrf24l01_irq_tx_ds_active()));

			PIN_D1=0; //lower test pin
			nrf24l01_irq_clear_all(); //clear all interrupts in the 24L01
          }
}

Three measurements of time to transmit were done for each payload size. It should be noted that these results are completely independent of whether or not the transmission successfully made it to a receiver; in fact, these tests were done without a receiving PIC. The results seem to indicate a latency of 178 microseconds.

Throughput tests were done by sending a packet wirelessly, then timing how long it took to get it back. A pin was raised to high while waiting and then dropped when it was received. These periods were then measured on an oscilloscope, and then averaged. The times used were only those that had 100% return rate. However, the accuracy of these packets was not checked beyond the built in CRC error-checking within the packets themselves. The rates here are to go send out a transmission and then receive it back, so the rate should be doubled for only transmitting in one direction.