2.4GHz RF Radio Transceivers and Library

nRF24L01 Test Pair Photo

I really want to communicate wirelessly between Arduinos for cheap. I love the idea of “The Internet of Things”, with everything I can look, see, touch all connected together. The problem is the cost. Zigbee modules seem to be the standard, but they are just too much to be a reasonable solution for puttingeverything on the Internet.

The Nordic nRF24L01, built into a small module and sold by mdfly.com for $6.50 is an excellent solution. It’s cheap, fast (2 Mbps), easy, reliable, and low-power. It entirely implements the Data Link Layer in hardware, handling addressing, collisions, and retry, saving us lots of work on the software side. Zigbee has the brand recognition, but this little guy puts it to shame.


Parts

Qty Vendor# Description Price Comment
2 MDFly RF-IS2401 2.4Ghz Wireless nRF24L01 Transceiver Module $6.50 Datasheet Schematic
1 511-LD1117V33 Low Dropout (LDO) Regulators 3.3V 0.8A Positive $0.68 Datasheet

Connections

Hooking this up to Arduino is relatively straightforward.

The first issue to confront is that the module’s physical interface is a 4×2-pin header. This doesn’t lend itself to an easy connection with the Arduino or with a breadboard. Fortunately for me, I have a handful of 5×2 breakout boards which I had already designed to break out a 5×2 box header onto a breadboard. The only slightly tricky thing for me is that the pinout has to be backwards, due to the way they fit together.

The second issues is that this module takes 3.3V power. Fortunately its inputs are 5V tolerant, and its outputs are sufficient to drive Arduino inputs high. So the only thing to remember is to get the thing 3.3V power. On a stock Arduino that’s no problem because it has a 3.3V pin, but not all Arduino-compatable boards have it (none of mine do), so sometimes I need to regulate 3.3V power for it.

Signal RF
Module
Breakout
Board
Arduino
GND 1 2 GND
VCC 2 1 3V3
CE 3 4 8
CSN 4 3 9
SCK 5 6 13
MOSI 6 5 11
MISO 7 8 12
IRQ 8 7 2*

*Note, I am not ever connecting the IRQ pin in my setup, but that is where it would go.

In the setup pictured above, I have a couple things going on. On the left, I have an Arduino Pro Mini (not pictured) connected to the breadboard via a 10-pin cable, using by 5×2 breakout. On that setup is a 3V3 regulator (without caps–shame on me!), and the RF module connected on the other side using another 5×2 breakout. In the middle is there to handle the 5V-to-3V3. It turns out I didn’t need them, so it’s not connected. The wires are there just to make the connections listed in the table just above.

On the right is a stock Arduino with a custom prototyping shield of mine. Since it has 3V3 built in, that’s an easier connection. Again, the radio is connected via a short 10-pin cable and little 5×2 breakout board.

Library

Get the library from github: github:RF24.

Because this part handles so much in hardware, the library is really simple. It just puts a pleasant face on the capabilities of the chip, and handles the SPI interface.

First, it’s worthwhile to look at the code that’s out there. The best I found is the nRF24L01 on Arduino Playground. This code was an invaluable help in bringing up the module–it would have taken hours longer without it. I especially appreciate the author’s simple examples.

Still, I decided to write my own library, because I had a number of goals that were not fulfilled by that library.

  • Use standard SPI library. Users will have an easier time of it if the library relies on the the official SPI libary included with the distribution. Otherwise there is an obvious point of confusion for users.
  • Protected internals. As a good rule of design, it’s important to keep the internals of the class hidden from the users, and focus on providing a rich external interface. This makes it easier to later refactor the library without breaking anyone who is currently using it.
  • Ready for complex topologies. This part can communicate with many devices at once, however this other library is designed for 1-to-1 communication. Ultimately I want to be able to build a Mesh Network out of this part, so I want the library to be ready.
  • Full compliance with data sheet. I wanted to make sure that the library behaved exactly as the data sheet intended.
  • Similar interface to packed-in libraries. To make it easy on users, I wanted the library to work as much like the packed in libraries as possible, yet without hiding the power of the chip.

Here is the documentation for the full public interface of the driver:

RF24

Constructor. Creates a new instance of this driver. Before using, you create an instance and send in the unique pins that this chip is connected to.

Parameters:

  • _cepin: The pin attached to Chip Enable on the RF module
  • _cspin: The pin attached to Chip Select

begin

Begin operation of the chip. Call this in setup(), before calling any other methods.

setChannel

Set RF communication channel.

Parameters:

  • channel: Which RF channel to communicate on, 0-127

setPayloadSize

Set Payload Size. This implementation uses a pre-stablished fixed payload size for all transmissions.

Parameters:

  • size: The number of bytes in the payload

getPayloadSize

Get Payload Size.

Returns:

  • The number of bytes in the payload

printDetails

Print a giant block of debugging information to stdout.

Warning: Does nothing if stdout is not defined. See fdevopen in stdio.h

startListening

Start listening on the pipes opened for reading. Be sure to open some pipes for reading first. Do not call ‘write’ while in this mode, without first calling ‘stopListening’.

stopListening

Stop listening for incoming messages. Necessary to do this before writing.

write

Write to the open writing pipe. This blocks until the message is successfully acknowledged by the receiver or the timeout/retransmit maxima are reached. In the current configuration, the max delay here is 60ms.

Parameters:

  • buf: Pointer to the data to be sent

Returns:

  • True if the payload was delivered successfully false if not

available

Test whether there are bytes available to be read.

Returns:

  • True if there is a payload available, false if none is

read

Read the payload. Return the last payload received

Parameters:

  • buf: Pointer to a buffer where the data should be written

Returns:

  • True if the payload was delivered successfully false if not

openWritingPipe

Open a pipe for writing. Addresses are 40-bit hex values, e.g.:

openWritingPipe(0xF0F0F0F0F0);

Parameters:

  • address: The 40-bit address of the pipe to open. This can be any value whatsoever, as long as you are the only one writing to it and only one other radio is listening to it. Coordinate these pipe addresses amongst nodes on the network.

openReadingPipe

Open a pipe for reading.

Warning: all 5 reading pipes should share the first 32 bits. Only the least significant byte should be unique, e.g.

openReadingPipe(0xF0F0F0F0AA);
openReadingPipe(0xF0F0F0F066);

Parameters:

  • number: Which pipe# to open, 1-5.
  • address: The 40-bit address of the pipe to open.


Example

Included with the library is the example I used to bring these guys up, the pingpair example.

Hardware Role Switch

One thing I decided to do is write a single piece of software for both the transmitter and receiver unit. This vastly simplifies logistics. Then I have a pin on the hardware that acts as a switch between the two types. I call these ‘roles’. There is a transmitter ‘tole’ and a receiver ‘role’. I used pin 7, connected to ground for one role, to power for another role.

//
// Role management
//
// Set up address & role.  This sketch uses the same software for all the nodes
// in this system.  Doing so greatly simplifies testing.  The hardware itself specifies
// which node it is.
//
// This is done through the addr_pin.  Set it low for address #0, high for #1.
//
 
// The various roles supported by this sketch
typedef enum { role_rx = 1, role_tx1, role_end } role_e;
 
// The debug-friendly names of those roles
const char* role_friendly_name[] = { "invalid""Receive""Transmit"};
 
// Which role is assumed by each of the possible hardware addresses
const role_e role_map[2] = { role_rx, role_tx1 };
 
// The role of the current running sketch
role_e role;
 
void setup(void)
{
  //
  // Address & Role
  //
  
  // set up the address pin
  pinMode(addr_pin, INPUT);
  digitalWrite(addr_pin,HIGH);
  delay(20); // Just to get a solid reading on the addr pin
  
  // read the address pin, establish our address and role
  node_address = digitalRead(addr_pin) ? 0 : 1;
  role = role_map[node_address];

Radio Setup

The sketch first sets up the radio…

  //
  // Setup and configure rf radio
  //
  
  radio.begin();
 
  // Set channel (optional)
  radio.setChannel(1);
  
  // Set size of payload (optional, but recommended)
  // The library uses a fixed-size payload, so if you don't set one, it will pick
  // one for you!
  radio.setPayloadSize(sizeof(unsigned long));
    
  //
  // Open pipes to other nodes for communication (required)
  //
  
  // This simple sketch opens two pipes for these two nodes to communicate
  // back and forth.
  
  // We will open 'our' pipe for writing
  radio.openWritingPipe(pipes[node_address]);
  
  // We open the 'other' pipe for reading, in position #1 (we can have up to 5 pipes open for reading)
  int other_node_address;
  if (node_address == 0)
    other_node_address = 1;
  else
    other_node_address = 0;
  radio.openReadingPipe(1,pipes[other_node_address]);
  
  //
  // Start listening
  //
  
  radio.startListening();
  
  //
  // Dump the configuration of the rf unit for debugging
  //
  
  radio.print_details();
}

Transmitter Role

The transmitter unit writes the current millis() time out to the other unit every second, listens for the response, measures the difference, and prints that.

void loop(void)
{
  //
  // Transmitter role. Repeatedly send the current time
  //
  
  if (role == role_tx1)
  {
    // First, stop listening so we can talk.
    radio.stopListening();
    
    // Take the time, and send it. This will block until complete
    unsigned long time = millis();
    printf("Now sending %lu...",time);
    bool ok = radio.write( &time ); 
    
    // Now, continue listening
    radio.startListening();
    
    // Wait here until we get a response, or timeout (250ms)
    unsigned long started_waiting_at = millis();
    bool timeout = false;
    while ( ! radio.available() && ! timeout )
      if (millis() - started_waiting_at > 250 )
        timeout = true;
    
    // Describe the results
    if ( timeout )
      printf("Failed, response timed out.\n\r");
    else
    {
      // Grab the response, compare, and send to debugging spew
      unsigned long got_time;
      radio.read( &got_time );
  
      // Spew it
      printf("Got response %lu, round-trip delay: %lu\n\r",got_time,millis()-got_time);
    }
    
    // Try again 1s later
    delay(1000);
  }
  

Receiver Role

…And the receiver does the opposite, receiving the packet, and mirroring it back out to the other guy.

  
  if ( role == role_rx )
  {
    // if there is data ready
    if ( radio.available() )
    {
      // Dump the payloads until we've gotten everything
      unsigned long got_time;
      boolean done = false;
      while (!done)
      {
        // Fetch the payload, and see if this was the last one.
        done = radio.read( &got_time );
  
        // Spew it
        printf("Got payload %lu...",got_time);
      }
      
      // First, stop listening so we can talk
      radio.stopListening();
            
      // Send the final one back.
      radio.write( &got_time ); 
      printf("Sent response.\n\r");
      
      // Now, resume listening so we catch the next packets.
      radio.startListening();
    }
  }
}

The Magic Makefile

The problem I had developing these initially is that even though the software was the same, it was a pain to switch back and forth in the IDE and remember to install it on both nodes. So, I posted to the Arduino forum: Is there an easy way to simultaneously deploy a sketch to multiple devices? Fortunately, the answer is yes! In that thread, Graynomad posted his Makefile for Arduino. With a little tweaking, I was able to set it up to compile my sketches on the command line and upload them to as many nodes as I have connected all at once. Yay!

Then, to make testing easier still, I created a little script that opens two xterm windows side-by-side and connects to each Arduino. Then I can watch the communication spew back and forth in real time. Makes it super easy to tell that I didn’t break something.

xterm -e "screen /dev/tty.usbserial-A1234ABc" &
xterm -e "screen /dev/tty.usbserial-A5678dEf" &