A Radio Frequency transceiver library: nRF24L01 and ChibiOS/RT

Cheapest and most popular RF transceiver than ever

The nRF24L01 is one of the most popular RF transceiver as it is very cheap and it could be found already configured on small PCBs with needed passive components. This means that only a MCU is needed to design a radio system with the nRF24L01.


The nRF24L01 is a highly integrated, ultra low power 2Mbps RF transceiver IC for the 2.4GHz ISM band. It provides an hardware protocol accelerator, the Enhanced ShockBurst, enabling the implementation of advanced and robust wireless connectivity with low cost MCUs.


By this article we want to provide a full library for nRF24L01 compatible with ChibiOS/RT 3.0 ad a demo. That requires, as always, a preliminary read of the device datasheet.

nRF24L01 Datasheet


Design and connections

nRF24L01 pin map
The pin map of a nRF24L01 transceiver.

With peak RX/TX currents lower than 14mA, a sub μA power down mode, advanced power management, and a 1.9 to 3.6V supply range, the nRF24L01 provides a true ULP solution enabling months to years of battery lifetime when running on coin cells or AA/AAA batteries. The nRF24L01 is configured and operated through the SPI and using this interface we can access to its register maps contains all configuration registers of this chip.

Still using SPI we can send command to the transceiver making it accessible in all its operation modes.The Enhanced ShockBurst is based on packet communication and supports various modes from manual operation to advanced autonomous protocol operation. Internal FIFOs ensure a smooth data flow between the radio front end and the system’s MCU. The radio front end uses GFSK modulation and has user configurable parameters like frequency channel, output power and air data rate.

Pin Description

Just few pin are required to use this transceiver:2 pin for power supply, 4 pin for SPI communication and 2 additional pins for IRQ and CE. Follows the detailed pin map:

  1. GND, connection to ground
  2. VCC, power supply 1.9~3.6V DC
  3. CE, Chip enable used to perform some operation
  4. CSN, SPI chip select
  5. SCK, SPI serial clock
  8. IRQ, interrupt request pin

Enhanced ShockBurst

Enhanced ShockBurst is a packet based data link layer. It features automatic packet assembly and timing, automatic acknowledgement and re-transmissions of packets. It enables the implementation of ultra low power, high performance communication with low cost host MCUs. The features enable significant improvements of power efficiency for bi-directional and uni-directional systems, without adding complexity on the host controller side. This data link layer is enough complex and a reader can found a whole chapter (Chapter 7) about this argument on nRF24L01 reference manual.

Anyway, from our point of view we just need to know few thinks:

  1. We can set-up Enhanced ShockBurst using some commands clocked thought the SPI;
  2. Every receiver and transmitter need for a unique address which lenght is 5 bytes;
  3. Enhanced ShockBurst allows multiceiving (receive from 6 different addresses simultaneously);
  4. Max payload lenght is 32 bytes;
  5. Receiver is able to auto-detect payload lenght;

In this first library version we will not consider the multiceiver option: this requires just some additional APIs, but testing and debuging part would be much more difficult.

SPI Operation mode

Reading the reference manual (Chapter 8) we can understand that nRF24L01 uses a standard SPI with a maximum data rate of 8 Mbps. The serial commands are shifted out in this way: for each command word(one byte) most significant bit must be sent first; if the command is longer than one word less significant byte must be sent first. Chip select must be lowered before start communication and should be pulled up only when every word is shifted out.

A SPI timing diagram (see Fig.2) found on manual give us all the still missing information to configure SPI properly (If you are not expert about uses of SPI, we suggest our quick tutorial Meeting SPI).

nRF24L01 SPI timing diagram
nRF24L01 SPI timing diagram.

Summarizing, we need to configure SPI clock frequency to be equal or less of 8 MHz, Data size should be 8 bit, CPOL as low and CPHA as low.

We can found in the same chapter a table containing all the commands available (Figure 3), note that every SPI exchange length depends on commands and that we can access to every feature thought that commands. Furthermore during the first exchange nRF24L01 shift out the status register map.

nRF24L01 SPI commands
nRF24L01 SPI commands.

Equally important is the register map table (Chapter 9) We don’t report it here since it is very long, anyway remember that some configuration could be set-up writing in some of these registers.

The interrupt request

The nRF24L01 has an active low IRQ pin. The IRQ pin has three sources: it is activated when TX_DS (Transmit Data Sent), RX_DR (Receive Data Ready) or MAX_RT (Max Retransmit) bit are set high by the Enhanced ShockBurst in the STATUS register. The IRQ pin resets when MCU writes ‘1’ in the STATUS register on the bit associated to the IRQ active source. As example, we can suppose that a MAX_RT events happens. So, we will detect an IRQ transition from high to low and checking STATUS register we will found that MAX_RT bit is high. So we should take necessary actions and than set MAX_RT to high in the STATUS register to clear IRQ.

To detect IRQ we can use EXT driver from ChibiOS/HAL. This driver launches a callback when detects a transition (on rising, falling or both edge according to its configuration), anyway, we will discuss this callback in detail later.

From finite state machine to driver design

Library FSM
The proposed FSM for this transceiver library.

Before starting, please allow me a little note. Not everyone knows that PLAY Embedded takes its name by an open-source library that we create some time ago. That library (a Pretty LAYer for ChibiOS aka PLAY for ChibiOS) is born to be an abstraction layer for most used devices in embedded applications and it already supports nRF24L01. The project is still active on sourceforge (PLAY) but from next versions it will support MEMS only. All this to say that code and ideas here presented are originating from that project.

Resuming, on chapter 6.1 we can found a finite state machine representing the modes the nRF24L01 can operate in and how they are accessed. Inspired by this diagram we made our driver FSM here represented (See Fig.4).

This is a good starting point for our driver design. About details on library organization we are confident that our loyal readers have already read articles like ChibiOS/HAL design and C Library design for embedded applications. Like for other libraries, this one has just a configuration header for user configuration or to disable driver, a main header rf.h and rf.c.

rf.h: typedefs and enumerations

As always, header contains a certain number of bit-mask and enumeration. We will just show important parts. First of all, we defined an RX packet and a TX packet. Note that Payload data structure is included into the frame and its length is fixed removing additional complications. The assumption considered during the design phase is that in the case of a communication using multiple packets it is expected that these have the same length. Anyway, payload length could be chosen thought userconf.h.

Staring from enumeration and defines we can create a configuration structure. Additionally we need for CE and IRQ information, a pointer to an SPI driver and its configuration and a pointer to the an EXT driver and its configuration. We need for SPI for communication and for EXT to manage external IRQ.

According to our diagram these are RF available states:

Before to introduce RF driver typedef let’s spend some words about callback that occurs on IRQ. This callback could be considered like an ISR and in Real Time systems these routines should be atomic and their lifetime should be deterministic. We said above that IRQ has many sources and that we need to read STATUS register to understand the source that generated the IRQ. The lifetime of an SPI operation is by its very nature non-deterministic and as a general rule a driver should not be used in ISR.

We can solve this problem using another kernel instruments: events! The ISR broadcast an event, in this way we can manage STATUS register out of kernel critical section. That explains why in driver typedef we have some variable related to events: we have an event sources and an event listener.

rf.c: needed local functions

A simple read\write register is not enough for this device. We need to implement some function to write register or send command to nRF24L01. We will not explain these function because they just perform some SPI exchange composing properly transmission buffer. Just remember that before call one of these functions the SPI interface must be initialized and the driver started.

APIs explained

Here the full list of our APIs with code-box and its explanation.


This function should be called by main() right after halInit() and chSysInit(). This function initializes every Driver enabled calling rfObjectInit() (In this case we have only RFD1 and it is always enabled).


This function is not an API. It is typically called by rfInit() receiving pointer to a driver which must be initialized. Note that in this function we update driver state to RF_STOP and initialize driver variables. In our case we need to initialize event sources and a mutexes that is used by some others APIs.


This API and must be called before use this driver and only after the rfInit(). The rfStart() receives in addition to the driver also a pointer to RFConfig: configuration structure should be declared by user in its application but is needed by driver when we start the driver.
This function updates configuration pointer in driver structure so from here on out functions will not require configuration pointer as parameter. It register the IRQ event on the driver event listener (from here on, the listener will be able to listen to events generated by the source called IRQ), then starts SPI and EXT driver, resets STATUS register, configures Enhanced ShockBurst according to RFConfig and pulls down CE pin. After that updates driver state from RF_STOP to RF_READY: now we are ready to perform transmission or to receive.


This API, as always, makes some checks and assertion then turns off the nRF24L01, stop SPI and EXT driver, unregisters the IRQ event and updates status to LCD_STOP


This API just read STATUS register and checks TX FIFO status returning TRUE if there are some empty spaces.


This API just read STATUS register and checks RX FIFO status returning TRUE if there re packets in it.


This API set nRF24L01 to receive packets. Operation performed here are described in Appendix A of the nRF24L01 reference manual. Anyway, here thinks are a little bit more complicated since we don’t want to transfer a single packet but n packets. The user must provide a pointer to an array of RX frames which lenght is n. To avoid unnecessary complications here it is supposed that:

  1. Packets arrive from the same transmitter;
  2. Transmitter sends at least n packet sequentially (or operation will go in timeout);
  3. Someone send an event when IRQ is active (actually we do this in EXT callback);

This function configures nRF24L01 for receive acting on CONFIG register, then set TX_ADDR and RX_ADDR according to address reported into the first RX frame (TX_ADDR is required to be the same of RX_ADDR by Enhanced ShockBurst to send out acknowledge). After that flush RX FIFO, reset a counter, updates driver state and set CE pin: from now on nRF24L01 is listening.

We now enter in a while that terminates when receive is completed or is prematurely interrupted for timeout or error occurrence. Entering into the loop we immediately wait for an event. If event goes timed out wait returns 0 so we update the driver state, pull down the CE and return a RF_TIMEOUT message. If events occurs we check STATUS register and RX FIFO and if everything is ok we get a payload, increment the counter, reset the IRQ and continue the loop, otherwise we terminate the loop with RF_ERROR message. If we terminate the loop without a return, communication succeeds and we can return an RF_OK message.


This function configures nRF24L01 for transmit acting on CONFIG register, then set TX_ADDR and RX_ADDR according to address reported into the first TX frame (like rfReceive(), RX_ADDR is required to be the same of TX_ADDR by Enhanced ShockBurst to receive a transmission acknowledge). After that makes simple operation to prepare transmission and enter into the loop.

Note that, unlike rfReceive() we have here a flag. We use it to ensure that we send a new packet only if previous has been delivered correctly.

Like rfReceive() loop terminates when transmission is completed or is prematurely interrupted for timeout or max retransmit occurrence. Entering into the loop we check for empty spaces in TX FIFO and if flag is TRUE (of course this condition is verified on first cycle), if this condition occurs we perform a transmission and we lower the flag. After that we wait for an event: on timeout we made the usual operations returning a RF_TIMEOUT message. Otherwise (if events occurs), we check STATUS register and we can have two conditions.

  1. TX_DS is high, that means that packet has been delivered. We can reset the IRQ, set flag as TRUE, update the counter and continue;
  2. MAX_RT is high, that means Enhanced ShockBurst tries to resend packet for Auto Retransmit Count times with a delay of Auto Retransmit Delay unsuccessfully and we terminate the loop with RF_ERROR message.

If we terminate the loop without a return, communication succeeds and we can return an RF_OK message.


This API receive a packet using rfReceive() and use payload to compose a string. User must provide a buffer which length is at least RF_PAYLEN + 1 to avoid buffer overflow.


This API compose a packet from a string and send out it using rfTransmit(). Max string lenght allowed is RF_PAYLEN, otherwise string will be partially transmitted.


I don’t kow if this function could be considered an API, anyway it must be used by user in EXTConf. This is the callback used by EXT when an IRQ occurs. It lock the system from ISR, update a counter, broadcasts the event flag and unlock the system. Note that cbcounter has debug purposes.


This API lock the driver mutex and it is used to gains exclusive access to the RF driver. It could be used when concurrent threads try to access the RF resource.


This is the dual of rfAcquireBus().

Proposed demo

Demo explained

The proposed demo uses two STM32 Nucleo-64 F4 (STM32F401RE) connected to two nRF24L01. We configure RF and use it to exchange strings. We have a receiver and a transmitter both using serial communication to provide a simple user interface.

To use this demo you need to connect nRF24L01 to your ST NUCLEO using these pins:

  1. GND -> GND
  2. VCC -> 3.3V
  3. CE -> PA9
  4. CSN -> PB6
  5. SCK -> PA5
  6. MOSI -> PA7
  7. MISO -> PA6
  8. IRQ -> PC7

You can choose other pin but you should edit main.c to make it working. Note that EXTConf is written to detect high to low transition on pin PC7: changing IRQ pin requires some edit to this configuration.

Note that a TRANSMITTER allow to switch part of code so you need to set this define TRUE for transmitter and FALSE for receiver. Remember to save and rebuild before flashing.

Now you have two MCUs with different code. Connect to them using two terminal instances and try to write something on transmitter terminal.

Project download

The attached demo has been tested under ChibiOS 17.6.x. Note that you can find a more recent version of this project int the Download Page. Note also that starting from version 20 all the demos from PLAY Embedded will be distributed together with ChibiStudio.


Replies to A Radio Frequency transceiver library: nRF24L01 and ChibiOS/RT

  • Hello I have been trying to port this example over to the stm32f103 on some of the blue pill board from ebay. However I am a bit stuck as the rf thread hungs on the chnGetTimeout as it waits for an interrupt indefinitely regardless of the data sent over on the serial connection. Some reading over the internet tells me that the thread in which an event is needed must be registered using the chEvtRegister function. However I don’t see this anywhere in the original code. So I am asking how is it that this code works? How does one use chnGetTimeout and how does it work?

    • I found the bug so I wanted to post this if anyone might have problem with that function. The bug is actually a hardware issue with the pins for the USART being set wrongly. In order to use the serial driver, the Tx pin should be set to alternate_function while the RX should be set to input. Setting RX to alternate function will keep the RX line low preventing anything from being read. Reading the RM is really a lifesaver…

    • First off all, I have to say that your examples are very clean and I am learning a lot from them. The bug I spoke of isn’t on the nucleo (its was for the stm32f103) though I will get a f401RE soon so to reduce future trouble as I will then be able can compare the actual results when I’m changing items in the code. After I got the serial terminal to work, I am noticed that the nrf24 is timing out. I tried examining the timings using one of the cheap logic analysers (these things are a godsend) and it looked like the transceiver was not responding. the only result I got from looking at the MISO line is that the transceiver is output 0xC0 which I suppose is the status register contents. I tested the transceivers with a sketch on MSP430 energia and they work. Didn’t have much of an idea why. However,the example is working now. I think it was something to do with either the logic analyser or mis-connected pins.

      Either way, one of my future projects would be to leverage the RF driver and add a network layer over it. I did a very basic one for a final year project but using the MSP430 bare bones to transmit water level data from remote points for a simple flood detection prediction. I also plan to use it to transmit data from inertial measurement units namely the MPU6050 and use it to control LEDs and such. There is also DSP and other items I want to implement so having a solid foundation when prototyping is a great timesaver. As such, I must thank you for providing the examples.The more I use Chibios, the more I am being to like it.

    • About my first comment, I wasn’t enough clear. On STM32 Nucleo F401RE which is the board used in demo UART2 RX and TX are already configured in board files so you haven’t to remap they as AF. In case of your board maybe board files do not expect that PINs as UART so you have to remap them ( I don’t know if this could be considered a BUG).

      My library uses events to manage external IRQ but it is in my plans a massive review of the design of this library. I suggest you to check all PIN used by RF and if they are configured properly. Also check if SPI configuration is adeguate to nRF24L01 requirements (max clock speed, CPOL, CPHA)…

    • I was using the mininalist stm32f103 board file so hardly anything is mapped to AF there except the LED on PC13 and one or two other things. The reason why I called the USART issue a bug ( I can’t call it as such as I went against what the reference manual indicated) is that most examples have RX set as AF but the manual dictates to set it as input_floating or input_pullup. It is the same for MISO which in the case of the MCU being set as a master means that the MISO should be set to an input accordiing to the RM.

      I also checked the requirements and they are good. I didn’t touch most of your code, I just read over most of it so I had not looked at that area. While I haven’t set up a receiver board, the MISO line looks ok as I compared it with the energia example that worked. As expected, I got Max_RT error which meant that the mcu and the nrf24L01 could talk noting the absence of the receiver. I know that not a final ok but it is close enough for now.

Leave a Reply