nRF24L01 sensor network with RF24Network on an Arduino Uno
Two nRF24L01 sensor nodes send DS18B20 temperatures to a base node every 2 seconds, and the base shows each one on an LCD, or no report when a node goes quiet. The whole build, an Arduino Uno and 6 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced About 45 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $94. On Mokxi they're free.
What you'll learn
- Radio networking with RF24Network and octal node addresses
- SPI wiring for the nRF24L01+: CE, CSN, SCK, MOSI and MISO
- Why the radio must run on 3.3 V, never 5 V
- Sending a small struct as a message
- Noticing a silent node with a timeout
- Difficulty: Advanced
- Time: About 45 minutes
- Board: Arduino Uno R3
- Parts: 12
How it works
Three Uno boards each carry an nRF24L01+ radio. Two of them are sensor nodes, 01 and 02. Every 2 seconds each node reads its DS18B20 temperature sensor on pin 4 and sends the reading to the base. RF24Network gives each radio an octal address, and the base is 00.
The base listens, remembers the last reading from each node, and shows both on a 16x2 LCD every 500 ms. The network layer acknowledges each message, so a node can print whether the base got it.
If a node has not reported for 10 seconds, the base shows "no report" for it.
Parts list
12 parts, plus the jumper wires. They retail for about $94; on Mokxi they're free.
- 3 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 3 × 2.4 GHz radio, nRF24L01+
- 1 × Character LCD with I2C backpackIn Claw Machine Kit, Vending Machine Kit, Rhythm Game Kit.
- 3 × Power, 5 V
- 2 × Temperature sensor, DS18B20
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| 2.4 GHz radio, nRF24L01+ (1) pin GND | Arduino Uno R3 (1) pin GND |
| 2.4 GHz radio, nRF24L01+ (1) pin SCK | Arduino Uno R3 (1) pin 13 |
| 2.4 GHz radio, nRF24L01+ (1) pin MISO | Arduino Uno R3 (1) pin 12 |
| 2.4 GHz radio, nRF24L01+ (1) pin MOSI | Arduino Uno R3 (1) pin 11 |
| 2.4 GHz radio, nRF24L01+ (1) pin CSN | Arduino Uno R3 (1) pin 8 |
| 2.4 GHz radio, nRF24L01+ (1) pin CE | Arduino Uno R3 (1) pin 7 |
| 2.4 GHz radio, nRF24L01+ (1) pin VCC | Arduino Uno R3 (1) pin 3V3 |
| Character LCD with I2C backpack pin SDA | Arduino Uno R3 (1) pin A4 |
| Character LCD with I2C backpack pin SCL | Arduino Uno R3 (1) pin A5 |
| Character LCD with I2C backpack pin GND | Ground |
| Character LCD with I2C backpack pin VCC | 5 V |
| 2.4 GHz radio, nRF24L01+ (2) pin GND | Arduino Uno R3 (2) pin GND |
| 2.4 GHz radio, nRF24L01+ (2) pin SCK | Arduino Uno R3 (2) pin 13 |
| 2.4 GHz radio, nRF24L01+ (2) pin MISO | Arduino Uno R3 (2) pin 12 |
| 2.4 GHz radio, nRF24L01+ (2) pin MOSI | Arduino Uno R3 (2) pin 11 |
| 2.4 GHz radio, nRF24L01+ (2) pin CSN | Arduino Uno R3 (2) pin 8 |
| 2.4 GHz radio, nRF24L01+ (2) pin CE | Arduino Uno R3 (2) pin 7 |
| Temperature sensor, DS18B20 (1) pin DQ | Arduino Uno R3 (2) pin 4 |
| 2.4 GHz radio, nRF24L01+ (2) pin VCC | Arduino Uno R3 (2) pin 3V3 |
| Temperature sensor, DS18B20 (1) pin GND | Ground |
| Temperature sensor, DS18B20 (1) pin VDD | 5 V |
| 2.4 GHz radio, nRF24L01+ (3) pin GND | Arduino Uno R3 (3) pin GND |
| 2.4 GHz radio, nRF24L01+ (3) pin SCK | Arduino Uno R3 (3) pin 13 |
| 2.4 GHz radio, nRF24L01+ (3) pin MISO | Arduino Uno R3 (3) pin 12 |
| 2.4 GHz radio, nRF24L01+ (3) pin MOSI | Arduino Uno R3 (3) pin 11 |
| 2.4 GHz radio, nRF24L01+ (3) pin CSN | Arduino Uno R3 (3) pin 8 |
| 2.4 GHz radio, nRF24L01+ (3) pin CE | Arduino Uno R3 (3) pin 7 |
| Temperature sensor, DS18B20 (2) pin DQ | Arduino Uno R3 (3) pin 4 |
| 2.4 GHz radio, nRF24L01+ (3) pin VCC | Arduino Uno R3 (3) pin 3V3 |
| Temperature sensor, DS18B20 (2) pin GND | Ground |
| Temperature sensor, DS18B20 (2) pin VDD | 5 V |
The note on the canvas
Word for word, as it sits beside the circuit in the editor.
Try it
1. Press Run. Within a few seconds the base's LCD shows both nodes' temperatures.
2. Drag the thermometer on node 01 or 02: the base shows the new reading on its next report, every 2 seconds.
3. Each node's serial monitor says "sent: ok" when the base acknowledged the reading.
4. Hold a node's reset button for 10 seconds: the base shows "no report" for it. Let go and it comes back.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// nRF24L01 sensor network, the base: node 00 of an RF24Network. Two sensor
// nodes (01 and 02) each send their DS18B20's temperature every 2 seconds,
// and this board shows the last reading from each on a 16x2 I2C LCD. A node
// that has not reported for 10 seconds reads "no report".
//
// nRF24L01 CE -> 7 CSN -> 8 SCK -> 13 MOSI -> 11 MISO -> 12
// VCC -> 3.3V (never 5 V) GND -> GND
// LCD SDA -> A4 SCL -> A5 VCC -> 5V GND -> GND
//
// RF24Network gives each radio an octal address in a tree: 00 is the base,
// 01 to 05 its children. The network layer acknowledges each message, so a
// node knows whether the base got it. On the bench, put a 10 uF capacitor
// across the radio's VCC and GND: the module draws its current in bursts.
//
// Things to try. Drag a node's thermometer and watch its line change.
// Change SILENT_MS to notice a quiet node sooner.
#include <SPI.h>
#include <RF24.h>
#include <RF24Network.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
RF24 radio(7, 8); // CE, CSN
RF24Network network(radio);
LiquidCrystal_I2C lcd(0x27, 16, 2);
const uint16_t THIS_NODE = 00; // the base, in octal
const uint8_t CHANNEL = 90; // every node must use the same channel
const unsigned long SILENT_MS = 10000;
// What a node sends: hundredths of a degree and a running count.
struct Reading {
int16_t centiC;
uint16_t count;
};
struct Node {
bool heard;
bool silent;
unsigned long at;
Reading last;
};
Node nodes[2];
unsigned long lastDraw = 0;
void printTemp(Print &out, int16_t centiC) {
out.print(centiC / 100.0, 1);
out.print(" C");
}
void draw() {
for (uint8_t i = 0; i < 2; i++) {
lcd.setCursor(0, i);
lcd.print("Node 0");
lcd.print(i + 1);
lcd.print(": ");
if (!nodes[i].heard) {
lcd.print("waiting ");
} else if (nodes[i].silent) {
lcd.print("no report");
} else {
printTemp(lcd, nodes[i].last.centiC);
lcd.print(" ");
}
}
}
void setup() {
Serial.begin(9600);
lcd.init();
lcd.backlight();
if (!radio.begin()) {
Serial.println("radio not responding: check CE, CSN and the 3.3 V supply");
lcd.print("No radio");
while (1) {}
}
radio.setChannel(CHANNEL);
network.begin(THIS_NODE);
Serial.println("base 00 listening");
draw();
}
void loop() {
network.update();
while (network.available()) {
RF24NetworkHeader header;
Reading r;
network.read(header, &r, sizeof(r));
uint16_t from = header.from_node;
if (header.type != 'T' || from < 1 || from > 2) continue;
Node &n = nodes[from - 1];
n.heard = true;
n.silent = false;
n.at = millis();
n.last = r;
Serial.print("node 0");
Serial.print(from);
Serial.print(": ");
printTemp(Serial, r.centiC);
Serial.print(" (reading ");
Serial.print(r.count);
Serial.println(")");
}
for (uint8_t i = 0; i < 2; i++) {
if (nodes[i].heard && !nodes[i].silent && millis() - nodes[i].at > SILENT_MS) {
nodes[i].silent = true;
Serial.print("node 0");
Serial.print(i + 1);
Serial.println(": no report for 10 s");
}
}
if (millis() - lastDraw >= 500) {
lastDraw = millis();
draw();
}
}
// nRF24L01 sensor network, a sensor node: node 01 of an RF24Network. Every
// 2 seconds it reads its DS18B20 and sends the temperature to the base,
// node 00, which shows it on an LCD. Node 02 runs the same sketch with
// THIS_NODE = 02.
//
// nRF24L01 CE -> 7 CSN -> 8 SCK -> 13 MOSI -> 11 MISO -> 12
// VCC -> 3.3V (never 5 V) GND -> GND
// DS18B20 DQ -> 4, with a 4.7 kohm pull-up to 5V (on the module)
//
// network.write() returns true once the base has acknowledged the message,
// so the serial monitor says whether each reading got through.
//
// Things to try. Change THIS_NODE to 03 and the base ignores it: it only
// shows nodes 01 and 02. Change INTERVAL_MS to report more often.
#include <SPI.h>
#include <RF24.h>
#include <RF24Network.h>
#include <OneWire.h>
#include <DallasTemperature.h>
RF24 radio(7, 8); // CE, CSN
RF24Network network(radio);
OneWire oneWire(4);
DallasTemperature sensors(&oneWire);
const uint16_t THIS_NODE = 01; // this node, in octal
const uint16_t BASE_NODE = 00;
const uint8_t CHANNEL = 90;
const unsigned long INTERVAL_MS = 2000;
struct Reading {
int16_t centiC;
uint16_t count;
};
unsigned long lastSent = 0;
uint16_t count = 0;
void setup() {
Serial.begin(9600);
sensors.begin();
if (!radio.begin()) {
Serial.println("radio not responding: check CE, CSN and the 3.3 V supply");
while (1) {}
}
radio.setChannel(CHANNEL);
network.begin(THIS_NODE);
Serial.print("node 0");
Serial.print(THIS_NODE);
Serial.println(" sending to the base");
}
void loop() {
network.update();
if (millis() - lastSent < INTERVAL_MS) return;
lastSent = millis();
sensors.requestTemperatures();
float c = sensors.getTempCByIndex(0);
if (c == DEVICE_DISCONNECTED_C) {
Serial.println("no DS18B20 on pin 4");
return;
}
Reading r = {(int16_t)lround(c * 100), ++count};
RF24NetworkHeader header(BASE_NODE, 'T');
bool ok = network.write(header, &r, sizeof(r));
Serial.print(c, 2);
Serial.print(" C sent: ");
Serial.println(ok ? "ok" : "no answer from the base");
}
// nRF24L01 sensor network, a sensor node: node 02 of an RF24Network. Every
// 2 seconds it reads its DS18B20 and sends the temperature to the base,
// node 00, which shows it on an LCD. Node 01 runs the same sketch with
// THIS_NODE = 01.
//
// nRF24L01 CE -> 7 CSN -> 8 SCK -> 13 MOSI -> 11 MISO -> 12
// VCC -> 3.3V (never 5 V) GND -> GND
// DS18B20 DQ -> 4, with a 4.7 kohm pull-up to 5V (on the module)
//
// network.write() returns true once the base has acknowledged the message,
// so the serial monitor says whether each reading got through.
//
// Things to try. Change THIS_NODE to 03 and the base ignores it: it only
// shows nodes 01 and 02. Change INTERVAL_MS to report more often.
#include <SPI.h>
#include <RF24.h>
#include <RF24Network.h>
#include <OneWire.h>
#include <DallasTemperature.h>
RF24 radio(7, 8); // CE, CSN
RF24Network network(radio);
OneWire oneWire(4);
DallasTemperature sensors(&oneWire);
const uint16_t THIS_NODE = 02; // this node, in octal
const uint16_t BASE_NODE = 00;
const uint8_t CHANNEL = 90;
const unsigned long INTERVAL_MS = 2000;
struct Reading {
int16_t centiC;
uint16_t count;
};
unsigned long lastSent = 0;
uint16_t count = 0;
void setup() {
Serial.begin(9600);
sensors.begin();
if (!radio.begin()) {
Serial.println("radio not responding: check CE, CSN and the 3.3 V supply");
while (1) {}
}
radio.setChannel(CHANNEL);
network.begin(THIS_NODE);
Serial.print("node 0");
Serial.print(THIS_NODE);
Serial.println(" sending to the base");
}
void loop() {
network.update();
if (millis() - lastSent < INTERVAL_MS) return;
lastSent = millis();
sensors.requestTemperatures();
float c = sensors.getTempCByIndex(0);
if (c == DEVICE_DISCONNECTED_C) {
Serial.println("no DS18B20 on pin 4");
return;
}
Reading r = {(int16_t)lround(c * 100), ++count};
RF24NetworkHeader header(BASE_NODE, 'T');
bool ok = network.write(header, &r, sizeof(r));
Serial.print(c, 2);
Serial.print(" C sent: ");
Serial.println(ok ? "ok" : "no answer from the base");
}
Code walkthrough
The key functions of the sketch, and what each one does.
setup sketch.ino
Starts the radio and stops with an error message if it does not answer. Then it sets CHANNEL and calls network.begin with the node's own address. Every node must use the same channel. The base also starts the LCD, and the sensor nodes also start the temperature sensor.
void setup() {
Serial.begin(9600);
lcd.init();
lcd.backlight();
if (!radio.begin()) {
Serial.println("radio not responding: check CE, CSN and the 3.3 V supply");
lcd.print("No radio");
while (1) {}
}
radio.setChannel(CHANNEL);
network.begin(THIS_NODE);
Serial.println("base 00 listening");
draw();
}loop sketch.ino
On the base, it calls network.update, reads every waiting message, and stores the reading for node 01 or 02 with the time it arrived. It marks a node silent after SILENT_MS. On a sensor node, it sends a reading every INTERVAL_MS and prints whether the base answered.
void loop() {
network.update();
while (network.available()) {
RF24NetworkHeader header;
Reading r;
network.read(header, &r, sizeof(r));
uint16_t from = header.from_node;
if (header.type != 'T' || from < 1 || from > 2) continue;
Node &n = nodes[from - 1];
n.heard = true;
n.silent = false;
n.at = millis();
n.last = r;
Serial.print("node 0");
Serial.print(from);
Serial.print(": ");
printTemp(Serial, r.centiC);
Serial.print(" (reading ");
Serial.print(r.count);
Serial.println(")");
}
for (uint8_t i = 0; i < 2; i++) {
if (nodes[i].heard && !nodes[i].silent && millis() - nodes[i].at > SILENT_MS) {
nodes[i].silent = true;
Serial.print("node 0");
Serial.print(i + 1);
Serial.println(": no report for 10 s");
// ... 8 more lines in the editordraw sketch.ino
Runs on the base. For each of the two nodes it writes one LCD line: waiting if nothing has arrived yet, no report if the node went quiet, or the last temperature otherwise.
void draw() {
for (uint8_t i = 0; i < 2; i++) {
lcd.setCursor(0, i);
lcd.print("Node 0");
lcd.print(i + 1);
lcd.print(": ");
if (!nodes[i].heard) {
lcd.print("waiting ");
} else if (nodes[i].silent) {
lcd.print("no report");
} else {
printTemp(lcd, nodes[i].last.centiC);
lcd.print(" ");
}
}
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The serial monitor says radio not responding This project
Check CE on pin 7, CSN on pin 8 and the SPI pins 11, 12 and 13. Power the radio from 3.3 V, never 5 V. - A node sends but the base shows waiting This project
Make sure every board sets the sameCHANNEL. The base only shows nodes 01 and 02, so a differentTHIS_NODEis ignored. - A node prints no DS18B20 on pin 4 This project
Check that the sensor's DQ pin goes to pin 4 and that it has power and ground. - The module resets, drops packets or never works on 5 V. 2.4 GHz radio
Power it from 3.3 V, never 5 V. Add a 10 uF capacitor across VCC and GND close to the module, since the Uno 3.3 V pin is weak. - Two radios do not hear each other. 2.4 GHz radio
Both need the same channel, address, data rate and payload size. One writes, the other listens. Check CE and CSN pins and the SPI wires (SCK, MOSI, MISO). - The screen stays blank or shows only a row of blocks. Character LCD with I2C backpack
The I2C address is usually 0x27 or 0x3F. Run an I2C scanner to find yours. Then turn the contrast trimmer on the backpack slowly until text appears. - The backlight is on but there is no text, or text is garbled. Character LCD with I2C backpack
Wire SDA and SCL to the right pins (A4 and A5 on an Uno), power it from 5 V, and call lcd.init() and the backlight call in setup. - The sensor reads -127 C or no device is found. Temperature sensor
Put a 4.7 k resistor between the data line and VCC. Check the pinout, since swapping VCC and GND can overheat the sensor. Then confirm the data pin matches your code.
Questions and answers
How does RF24Network address the nodes?
Each radio gets an octal address in a tree: 00 is the base and 01 to 05 are its children. Every node calls network.begin() with its own address on the same channel, 90 here.
How does a node know its reading arrived?
The network layer acknowledges each message, so network.write() returns true once the base has it. The node's serial monitor prints sent: ok or no answer from the base.
What does no report mean on the LCD?
The base keeps the time of each node's last reading. After 10 seconds of silence it shows no report for that node. The limit is SILENT_MS in the base sketch.
Can I power the nRF24L01 from 5 V?
No. Its VCC is 3.3 V, so use the Uno's 3.3V pin; its data pins tolerate 5 V. On the bench, put a 10 uF capacitor across VCC and GND, because the module draws current in bursts.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.