nRF24 walk through – Introduction

The Nordic nRF24 is a family of silicon integrated radio transceivers operating in the 2.4GHz band, the most popular one being the nRF24L01. This is the core element of some extremely cheap module boards available in online stores like eBay, Aliexpress and Banggood.

These boards do not provide WiFi (801.11) or Bluetooth connectivity (both in the 2.5GHz band), but they can be used to establish custom wireless networks between small electronic devices, including Arduino, RaspberryPi and Particle (formerly known as Spark).

Whenever we talk about networks you must take in account a few key aspects of networking, one of the most important being the network topology.

NetworkTopologies.svg

During this series we will aim to establish a star network between a series of Arduino based peripheral nodes and a central hub node, being either a RaspberryPi or a Particle Core/Photon: this represents a basic but invaluable configuration allowing for complex elaborations on remotely collected information, either on premise (RPi) or in the cloud (Particle).

If this series gets enough attention I might invest some more time and extend it to cover more complex topologies like tree and mesh, with the latter being my favorite and, IMHO, most valuable for inexpensive IoT projects.

The project

To keep things simple our peripheral nodes will be only collecting button presses, communicating to the central hub whenever a button gets pressed: the central hub will periodically (once every 30 seconds) print out the amount of button clicks it has received with a breakdown for each node; something like:

Received 14 clicks in the past 1 minute(s)
* 5 click(s) from node A
* 2 click(s) from node B
* 7 click(s) from node F

This will obviously represent just an example of what you will be able to do from the hub node; nothing prevents you, as an example, from pushing data into a database and generating graphs. You could aggregate the data differently or, more likely, collect other types of data from your sensor nodes: I’m not here to place constraints to your imagination!

Keep in mind though, the little radio transceivers we are using have a few limitations that are commonly misunderstood, which will be analyzed when we get there during the project.

The steps

This walk through will be split into the following posts:

nRF24 on Raspberry Pi

NOTE This post has been quite successful, so I decided to publish a complete series on nRF24 transceivers.

I’m working on a home automation project and I’m planning to use my Raspberry Pi as central node of a network of cheap nRF24 nodes.

First thing is to get the necessary compilation tools, something quite easy to achieve even on my RaspBMC installation:

$> apt-get install build-essential

With the compiler and the other tools at your hand you might want to get a library to get access to the nRF24 hardware, which is not a difficult step to achieve either:

$> git clone https://github.com/stanleyseow/RF24.git
$> cd RF24
$> cd librf24-rpi/librf24
// compile the files
$> make
// install the library
$> sudo make install
// check the library availability
$> sudo ldconfig -v | grep librf
      librf24.so.1 -> librf24.so.1.0

Now, let’s move to the wiring, but DO NOT attempt any connection while your Raspberry is powered up: even a brief short circuit made with a floating cable getting contact for a fraction of millisecond can ruin your day.

I’ve found some small difficulties here, mainly due to misleading information. Please refer to the pictures below and click on them to enlarge.

raspi-nrf24 raspi-nrf24-schema

Once you have everything in place you can power up your Pi and starting to get some fun!

All the code below is available as a Gist on GitHub for your convenience.

You can start with the examples contained within the library itself, but if you feel brave enough here is the code I’m executing on my Pi:

// file payload.h
#ifndef __VIRIDI_NEXU_PAYLOAD__
#define __VIRIDI_NEXU_PAYLOAD__

enum PayloadType {
    HERBA, METEO
};

typedef uint8_t vn_payload_type;
typedef uint8_t vn_payload_version;

typedef struct {
        int16_t humidity;
        int16_t temperature;
        int16_t pressure;
        int16_t altitude;
        int16_t luminosity;
} vn_meteo_t;

typedef struct {
        int16_t moisture;
        int16_t temperature;
} vn_plant_t;

struct Payload {
        vn_payload_type type;
        vn_payload_version version;

        union {
            vn_meteo_t meteo;
            vn_plant_t plant;
        } data;
};

#endif // __VIRIDI_NEXU_PAYLOAD__
// file receiver.cpp
#include <cstdlib>
#include <iostream>
#include "librf24/RF24.h"
#include "payload.h"
using namespace std;

//
// Hardware configuration
//
RF24 radio("/dev/spidev0.0", 8000000 , 25);  //spi device, speed and CSN,only CSN is NEEDED in RPI
const uint64_t pipes[2] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL };

Payload payload = Payload();

//
// Setup
//
void setup(void) {
  radio.begin();
  radio.setRetries(15, 15);
  radio.setDataRate(RF24_250KBPS);
  radio.setPALevel(RF24_PA_MAX);
  radio.setPayloadSize(sizeof(payload));
  radio.openWritingPipe(pipes[1]);
  radio.openReadingPipe(1, pipes[0]);
  radio.startListening();
  radio.printDetails();
}

//
// Loop
//
void loop(void) {
  if (radio.available()) {
    radio.read(&amp;amp;amp;amp;amp;payload, sizeof(payload));
    printf("packet %d %d %d %d %d \n", payload.data.meteo.temperature, payload.data.meteo.humidity, payload.data.meteo.luminosity, payload.data.meteo.altitude, payload.data.meteo.pressure);
  }
}

int main(int argc, char** argv){
        setup();
        while(1)
                loop();

        return 0;
}

To build the above code you just save the two files and execute gcc providing the necessary parameters: if you get an error regarding a missing header file you might have to adjust the librf/RF24.h include directive accordingly to your build location.

$> g++ -Wall -Ofast -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s -L../librf24/ -lrf24 receiver.cpp -o receiver

Remeber you must be have access to the /dev/spidev device to execute your program, the simplest way to get such permission is by running your receiver code as root:

$> sudo ./receiver

Now let me give you one last advice: always take in consideration differences in hardware architecture when you program/compile your communcation software!
In particular, if you send something from an Arduino (8 bit microcontroller) take in consideration there’s an important difference when the same data is read from the Raspberry (32 bit ARM processor):

  • float numbers are 4 bytes long on Arduino and 8 bytes on Raspberry
  • enumeration are 2 bytes on Arduino and 4 bytes on Raspberry
  • any struct on Raspberry is padded to even bytes
  • and so forth

What this means while developing for the nRF24 chips is that you need to take in consideration the architecture differences when transferring data and ensure the data types can be handled by both ends of the communication channel: my suggestion is to avoid floating points and stick to integers. If you need to transfer fractional values, switch the number to a higher scale: for example instead of transferring a temperature sensor reading of 25.12 C°  (Centigrade degrees), multiply the value by 1000 and transfer it as 25120 mC° (milli Centigrade degrees).

On the Arduino side there are many tutorials out there, but for the sake of completeness here is my wiring and the simple software sending out the payload for the Raspberry to read.

rf24_bbrf24_scheme

And here is the Arduino sketch broadcasting the data into the air

#include "payload.h"

#define SERIAL_DEBUG true
#include <SerialDebug.h>

#define LED_DEBUG true
#include <LedDebug.h>

#define SENSE_DELAY 2000
Payload payload = (Payload) { METEO };

#include <RF24Network.h>
#include <RF24.h>
#include <SPI.h>

uint32_t lastSense;
inline bool sense() {
	long now = millis();
	if (now < lastSense || now - lastSense > SENSE_DELAY) {
		payload.data.meteo.humidity = millis();
		payload.data.meteo.temperature = millis() * 50;
		payload.data.meteo.pressure = millis();
		payload.data.meteo.altitude = millis();
		payload.data.meteo.luminosity = map(analogRead(A0), 0, 1024, 100, 0);
		lastSense = now;
		return true;
	} else {
		return false;
	}
}

#define RADIO_CE_PIN 9
#define RADIO_CS_PIN 10
#define RADIO_RETRY_DELAY 15
#define RADIO_RETRY_COUNT 15
RF24 radio = RF24(RADIO_CE_PIN, RADIO_CS_PIN);
const uint64_t pipes[2] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL };

void setup() {
	SERIAL_DEBUG_SETUP(9600);
	pinMode(A0, INPUT);

	radio.begin();
	radio.setRetries(RADIO_RETRY_DELAY, RADIO_RETRY_COUNT);
	radio.setDataRate(RF24_250KBPS);
	radio.setPALevel(RF24_PA_MAX);
	radio.setPayloadSize(sizeof(payload));
	radio.openWritingPipe(pipes[0]);
	radio.openReadingPipe(1,pipes[1]);
}

void loop() {
	if (sense()) {
		radio.powerUp();
		if (!radio.write(&payload, sizeof(payload))) {
			PULSE(3,75);
		} else {
			PULSE(1,225);
		}
		radio.powerDown();

		DEBUG("payload", sizeof(payload), "enum", sizeof(payload.type));
	}
}