Let’s move to the 3rd part of the nRF24 walk through series before the excitement cools off, shall we?
Now that I have three boards wired and I’m certain only my code can break things up, it is time to create the sensors’ firmware.
It should be quite simple: all we need to do is getting the transceiver set up at the beginning and send the current sensor identifier every time the button is pressed.
As this series is intended to be a tutorial, I’ll expand a little on the basic requirement. To add a bit of salt on our sensors’ network communication, we will expect the hub to send back to the nodes 2 pieces of information in response to their click events:
- the total count of clicks received from all nodes is going to be part of this article and we’ll achieve that using the ack packet payload
- the number of clicks received from the specific node will be instead added in another post, so not to push too much info in one shot
Each device is going to require a unique identifier: as radio network address (think ofit like an IP address) and for the software to print out (like a computer name). I will use one byte stored in EEPROM
for both: this will limit the maximum amount of sensors for this network to 255
(0
is going to be reserved as the hub identifier/address).
That’s not such a low number after all, but we want our hub to print out the node identifier as a letter, so I will further limit the range to [1, 26]
to simplify the hub code.
NOTE The above limits to the number of nodes is very soft and has been introduced into this tutorial to simplify the code. The real hard limit to the number of unique nodes is much higher and in the order of 1 thousand billions (1
followed by 12
zeros)!
Setting things up
The radio transceiver configuration occurs, as you might expect, in the setup()
function, plus a global variable declaration and a few defines:
// Creates a new instance using SPI plus pins 9 and 10
RF24 radio(9, 10);
// nRF24 address family: all addresses will be in the format 0xFACEC0DE## with the last two
// bytes determined by the node identifier.
#define ADDR_FAMILY 0xFACEC0DE00LL
#define MAX_ID_VALUE 26
#define BUTTON_PIN 3
// This node unique identifier: 0 is used for the hub, anything above MAX_ID_VALUE is considered
// not valid
byte nodeId = 255;
void setup() {
SERIAL_DEBUG_SETUP(57600);
// Setup the push button
pinMode(BUTTON_PIN, INPUT_PULLUP);
// Read the address from EEPROM
byte reading = EEPROM.read(EEPROM_ADDR);
// If it is in a valid range for node addresses, it is our address
if (reading > 0 && reading <= MAX_ID_VALUE) {
nodeId = reading;
DEBUG("Node identifier is %c", nodeId + 64);
// Initialize the transceiver
radio.begin();
radio.setAutoAck(true); // Enables auto ack: this is true by default, but better to be explicit
radio.enableAckPayload(); // Enables payload in ack packets
radio.setRetries(1, 15); // Sets 15 retries, each every 0.5ms, useful with ack payload
radio.setPayloadSize(2); // Sets the payload size to 2 bytes
radio.setDataRate(RF24_2MBPS); // Sets fastest data rate, useful with ack payload
// Opens the first input pipe on this node address
radio.openReadingPipe(1, ADDR_FAMILY + nodeId);
// Opens the first input pipe on this node address
radio.openReadingPipe(2, ADDR_FAMILY + 254);
#if (SERIAL_DEBUG)
// Prints current configuration on serial
radio.printDetails();
#endif
} else {
DEBUG("Invalid node id %u found: use S## (a number between 1 and %u) to configure the board", reading, MAX_ID_VALUE);
}
}
There are a few details in the code related to the nRF24 module which might be valuable to explain:
- when I instantiate the module driver class I’m specifying only two out of the five pins used by module (not including the power line) because the
MISO
, MOSI
and SCK
pins are defined by the board SPI
interface
- I’m defining a network address class
40
bits long (0xFACEC0DE00
), not very different from IP addressing when you specify the network class (192.168.0.0
is the network class C
of an host having IP address 192.168.0.*
)
- I’m enabling auto acknowledge packets (useful to know if your transmission was successful) capable to carry payloads (more on this later)
- I’m instructing the library to auto retry packet transmission every half millisecond (
500 us
) up to 15
times, so to reduce transmission errors
- I’m setting a fixed payload size, which improves transmission reliability, of
2
bytes
- the data rate is set to highest possible value (
2Mb
per second), so to have the ack packets coming back in time for being recognized (more on this in the next article)
- one of the input pipes (the second if we count in pipe
0
) is set to this node address. This is not used to receive ack packets (pipe 0
is used for that), but it will come into play later on, when I’ll show how the sensors can receive data (other than ack packets) from the hub (or whoever wants to talk to us, for what it matters!)
IMPORTANT Reading pipes are not associated to addresses you talk to, but to addresses you want to listen for. If that sounds confusing, consider reading pipes like mailboxes: you can have multiple mailboxes, but that does not limit who can send you mail, actually anybody knowing at least one of them can.
This means you are not limited to receive data from maximum five nodes (as commonly and mistakenly known) and this walk through will demonstrate it!
NOTE If you want to know more about pipes and addressing, the best source of information is the datasheet, but to summarize:
- you have one writing pipe only, but you can change it’s value to whatever address you want to talk to
- there are six reading pipes, only two (
0
and 1
) can be associated with 40
bits addresses, with pipe 0
automatically associated to the writing address (to receive acks); this means pipes 1
to 5
share the same upper 32
bits, but you can change the reading pipes addresses
- almost every address is valid, only a few are excluded (like
0x0000000000
, 0xFFFFFFFFFF
and similar)
- address length is configurable to
40
(default), 32
or 24
bits, but the limitation on pipes 1
to 5
still applies as they keep sharing the upper 24
or 16
bits
For the sensors’ setup()
function to complete correctly we need to read a valid node id from EEPROM
which means we have to push something in the correct location: this is responsibility of another function called config()
. I’m not going to analyze that function in detail, but it should be clear its purpose is to allow to set the node id using a serial connection.
A looping life
Moving into the sensor loop()
function, the plan is even easier, if possible: check the button state and, if pressed, send the sensor node identifier to the hub.
To add value to this tutorial we will expect the hub to send back the total amount of received clicks and we will use ack packet as the carrier of this information.
NOTE Acknowledge (here abbreviated into ack) packets are used to verify data transmission has been successful. This is a common technique used in TCP/IP protocol as well: if you don’t feel comfortable with the concept just consider those as return receipts.
The protocol used by this chip allows to optionally attach some information to the return receipts: we refer to this info as the ack packet payload.
Ack packets maximum payload size depends on data rate and re-transmission speed and is explained in details in the nRF24 datasheet at paragraph 7.5.2.
I’m going to use some macros to perform a simple software debouncing on the push button, but apart from that, the code should be very easy to read:
// Button debouncing macros
#define DEBOUNCE 15
#define DMASK ((uint16_t)(1<<DEBOUNCE)-1)
#define DF (1<<(uint16_t)(DEBOUNCE-1))
// Macro for detection of falling edge and debouncing
#define DFE(signal, state) ((state=((state<<1)|(signal&1))&DMASK)==DF)
// Button debouncing status store
unsigned int buttonStatus;
void loop() {
// Checks if we are trying to configure the node identifier
config();
delay(1); // Slow down a bit the MCU
// Checks the push button: if we enter here the button has been pressed
if (DFE(digitalRead(BUTTON_PIN), buttonStatus)) {
// Sets the destination address for transmitted packets to the hub address
radio.openWritingPipe(ADDR_FAMILY);
// Put transceiver in transmit mode
radio.stopListening();
// Transmit this node id to the hub
bool write = radio.write(&nodeId, 1);
DEBUG("Send attempt from node %c was %s", nodeId + 64, write ? "successful" : "UNSUCCESSFUL");
// Get acknowledge packet payload from the hub
while (write && radio.available()) {
unsigned int count;
radio.read(&count, 2);
// This function shows how a node can receive data from the hub
// without using ack packets payload
receiveNodeCount();
DEBUG("Got response from hub: total click count is %u", count);
}
}
}
Once again, let’s analyze in detail the parts specific to the nRF24 module, which are activated whenever the push button state has gone LOW
for enough time:
- I set the transmission destination address using the
openWritingPipe()
function, providing the hub node identifier (that is the node we want to send data to)
- to put the module into transmit mode we need to call the
stopListening()
function
- since ack packets have been enabled, we can transmit reliably, so the result of the
write()
function (a boolean value) is going to be stored and checked
- the data I’m going to transmit (the sensor node identifier, stored into the
nodeId
global variable) is provided to the write()
function along with the number of bytes (only one) we are going to send. The actual payload sent on the air is going to be 2
bytes long, because we have set the packet size to a fixed value of 2
, which means the packet will be zero filled
- if the transmission was successful we expect to have an ack packet available. We have enabled ack packets payload, so we can pull out an
unsigned int
(2
bytes) into the count
variable: this represents the total amount of clicks the hub has received so far
This is a complete example of data exchange, but the fact we are using the ack packet payload has some caveats we will analyze when we will talk about the hub.
The whole picture
The complete sensor’s firmware is available on Github and its compilation requires the two libraries mentioned in the previous post: the RF24 lib from TMRh20 and my own MicroDebug lib.
Please ignore the receiveNodeCount()
function call and its implementation for now: we’ll get into that in another post. If you prefer, comment out line 105 to completely exclude that part of the firmware.
Naming the newborns
To avoid packet transmission conflicts we will have to initialize each sensor board with its own unique identifier: that’s the part handled into the config()
function.
Internally I will use numeric identifiers, but they will be translated into one character when printing, hence the limit to numerical values between 1
and 26
: that’s the number of letters in the latin charset.
Each board will initially have 255
as identifier, which is to be considered invalid, but you can change it via serial.
Once connected, type the S
character followed by the board identifier you want to assign (like S2
or s5
): the board will print the corresponding identifier as a character, store it in EEPROM
and self reconfigure.
IMPORTANT The communication relies entirely on assigned node identifiers. The hub has no other way than the node identifier to distinguish between the nodes.
If you configure two nodes with the same identifier they will be considered as one and their clicks will not be distinguished. You can leverage this in some situations and have two sensors appear as one, if that’s what you want.

Sensor node id configuration is very simple and can be done via serial console in a handful of seconds
Repeat the procedure on each board and you have your sensors setup!
From this point forward your sensors does not require to be hooked up to your computer, they will just need power. If you have enough USB ports available and decide to leave the sensors connected to your computer though, you will be able to receive some debug messages on the serial ports and enjoy the occurring communication… once we have the hub running!

The Arduino Eclipse Plugin supports multiple consoles and distinguishes among them using different colors: black text is from the hub, red, blue and green from the sensors!