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 putting everything 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" &

28 Comments

Filed under Arduino, RF Radio

28 responses to “2.4GHz RF Radio Transceivers and Library

  1. Hi!
    I’ve using your library for some time with the SFE modules and I’ve found it really useful and very well written and complete. I really would like to thank you, you’ve saved me from a lot of from scratch work.
    However, there is something I don’t quite understand or I must be missing something from the datasheet or the library:
    In my application I’m sending data in packages of one byte at the time with something like:
    radio.write( &buttons, 1 ); //on the transmitter side and of course…
    radio.read( &buttons, 1 ); //on the receiver side.
    And it works ok, with some missing packages specially when something moves around one of the modules. So I thought maybe downloading the payload size I could achieve a better lower miss rate, so I used
    setPayloadSize(1); at the end of the begin() function in the library basing on the fact that I’m sending only 1 byte at the time.
    However it had the exact opposite effect! Suddenly all my packages went lost and only a few with some regularity showed up on the receiver.
    Do you have any idea why this would be?
    I had to set the payload size to 8 to achieve an almost perfect transmission, with nearly 100% hit rate.
    Afterward I decided to use the hardware interrupt pin to free the cpu from the querying and focus on some LCD task. When I did so, the hit rate lowered a bit, let’s say to a 90% approx, I find that curious too.

    Thanks,
    regards!
    Lino

    • Hi there, thanks for checking in. Glad to hear it’s helpful for you! For the payload size, no I do not know why that’s happening. It’s good to know your experience, though, it may help others.

      Regarding packet loss… There is definitely packet loss to be expected with these units. The app notes from Nordic mention that in home use for something like a TV remote, it can take FIVE transmissions to get through. One thing you can try is taking the channel up over 100, reduce the data rate, and increase the CRC width.

      Please do let me know any other interesting experiences you have with the library and/or other things you’d like to see from it.

  2. I will write my experiences with modules and the libraries at the arduino forum, as you say, it might help others. By the way, reducing the data rate to 1MB didn’t help either, as with the payload size, it had the opposite effect, it’s pretty weird you know? Right now I’m waiting for some PCB I had made about this very project I’m working on, as soon as I got them in here I’ll keep experimenting with the library and the modules.

    I think you should add some public functions to configure the number of retries and time interval between them, I know it’s just to write to some registers, but trying to following the clean code you’ve came up, I think it would be the best. Besides, if I don’t remember wrong, the write_register function is protected.

    Thanks,
    I’ll let you know how my project it’s going on.
    Regards

  3. abdullah saeed

    hi
    can any one tel me its approx communication range?
    thanks
    awaiting for a quick reply.

  4. isobell

    you mention a pin for two roles eg. pin 7 is this the miso pin you are referencing where one end is grounded and the other live

    • No, pin 7 is not MISO. Connect pin 7 to ground on one of the units, and leave it disconnected on the other. That way the sketch knows if its running on the sender or the receiver.

  5. alex

    have installed library to ide as per normall but when i try any off the examples it onlyprints out nonsence randomletters and symbols any thoughts that will resolve problem
    thanks alex

  6. alex

    thanks that sorted it

  7. zen

    hii..
    really impressed with ur library just want some help….
    actually i want to transmit the sensor data wirelessly from one mc to mc..not able to understand how to fix the package…..got stuck

  8. ejcoombs

    Since this is a transceiver, is it possible for it to transmit and receive data simultaneously? If not, can it be configured to switch the role of a receiver or a transmitter by flipping a switch? Thanks for all your awesome work!

  9. mbed

    I want to communicate wireless data transmission from mbed NXP LPC1768 to nRF24L01+ and reseived data is should be sent to another nRF24L01+ and then to mbed again and display the result….please help me…
    Thanks in advance….

  10. Hi maniacbug – first I want to thank you so much for your RF24 library.. I’m finding your demo’s clear and very well documented and its been a great help.
    But I also wanted to mention something I have discovered (the hard way after many hours).. that I’m thinking this might help others starting out.
    My project is peer to peer if you like.. both both peers transmitting and receiving at different times during the RF communication.

    From what I can tell .. After I receive some data (and before I start transmitting a response) I need at wait around 100ms ..
    I assume this is for the ACK to get sent back.. otherwise the sender often thinks the TX has failed (and RF.write returns false) ..

    For several days now I thought my $2 ebay (nRF24L01) specials were the problem.. but introducing the delay at the end of my read function has massively improved things.. I also assume its only a problem because I switching between receive and transmit mode so much.

    Apologies if you have mentioned this already.. and to be honest I have not looked at the chip reference (its probably mentioned somewhere in there).
    I just thought others that are trying to create similar projects (request / response) might find this useful.
    Thanks again..
    rgds
    Jason

    • Thanks, I’m glad you found it useful. I HIGHLY recommend spending a lot of time in the data sheet for this unit. There are plenty little timing issues to get right with RF communication in general, and this radio in particular.

  11. I should also thank you for your “printf” implementation.. totally awesome .. probably the single single most useful trick Ive ever discovered for arduino coding .. 🙂 .. no more need to use so many serial.print’s … 🙂 thanks again..

  12. Neeraj

    Excellent article…Superb…Keep going on….
    Also, you refered something about mesh network that you are going to create using your library and rf-tranciever. I am interested in it. Can you give any link which provide information about it ?

  13. Maaaan, I love this kind of pretty badass work! 🙂
    I don’t know how, I don’t know why, I don’t even know where, but you made and awsome piece of powerful library.
    Please, if you find something you would like to change..to make things clearer and better, do it!
    Great comments, pretty nice written..etc.

    Take care and keep ur head up.

  14. Well, I have a little question.
    Is there any way how to get the info about pipe? I mean, I use IRQ to handle all the communication, but I would like to know which pipe received the new data..or something like that.

  15. Hi maniacbug. I want to use this to triangulate a robot. Then i need to know the time the signal uses from master node to slave node. In diffenrent nodes.
    Is it possible to send to node1 and listen to get a answer from node 1, send to node2 and listen to get a answer from node 2, send to node3 and listen to get a answer from node 3.

    Is this possible???
    Best,
    André

Leave a comment