In the previous post I showed you the simplest way to communicate between two Nucleo boards using the nRF24L01+. For sending and receiving data I used the simplest method, i.e. polling. While checking whether something arrived didn’t do much harm because I only read a single register in the chip, when transmitting I waited in vain until the transfer finished. On top of that, fixed-length data packets were being sent and received. This time I’ll show you how to send data of different lengths and how to receive messages in interrupt mode
I also remind you that you can buy modules like this directly from me, supporting my work at the same time.
The entire series about the nRF24L01+:
Dynamic Payload
In the previous part I mentioned the most important properties of the nRF24L01+ chips. One of the cool features these chips offer is Dynamic Payload. Payload is the part of the transmitted frame that contains our data that we send/receive. This is what we can influence, because the rest of the frame construction and decoding is handled by the chip itself.
To enable Dynamic Payload, it’s enough to set the EN_DPL bit in the FEATURE register to enable the feature itself, and then enable this feature in the DYNPD register for the channel where we want to use it, e.g. Pipe 0.
In my library I made a constant NRF24_DYNAMIC_PAYLOAD that decides whether to enable Dynamic Payload during initialization. It enables it for all pipes. The library will also behave accordingly.
Once you have Dynamic Payload enabled, all that remains is to use it.
On the transmitter side you don’t need to do anything special. Just write all the data into the nRF. The chip itself will calculate how many bytes it has to send and transmit the packet.
On the receiver side the chip will also calculate how much Payload data arrived, but here you must read from the chip how many bytes are available in the receiver’s FIFO queue. After all, you have to get this information from somewhere.
This is read with the R_RX_PL_WID command. In response, the chip returns how many bytes the first message in the queue has. Now it’s enough to read the known number of bytes with the R_RX_PAYLOAD. command.
Simple, right? Here’s what the functions that do this look like.
void nRF24_WriteTXPayload(uint8_t * data, uint8_t size)
{
nRF24_WriteRegisters(NRF24_CMD_W_TX_PAYLOAD, data, size);
}
uint8_t nRF24_GetDynamicPayloadSize(void)
{
uint8_t result = 0;
result = nRF24_ReadRegister(NRF24_CMD_R_RX_PL_WID);
if (result > 32) // Something went wrong :)
{
nRF24_FlushRX();
nRF24_Delay_ms(2);
return 0;
}
return result;
}
void nRF24_ReadRXPaylaod(uint8_t *data, uint8_t *size)
{
*size = nRF24_GetDynamicPayloadSize();
nRF24_ReadRegisters(NRF24_CMD_R_RX_PAYLOAD, data, *size);
nRF24_WriteRegister(NRF24_STATUS, (1<NRF24_RX_DR));
if(nRF24_ReadStatus() & (1<<NRF24_TX_DS))
nRF24_WriteRegister(NRF24_STATUS, (1<<NRF24_TX_DS));
}
Now I’m able to transmit messages of different lengths. It looks more or less like this.

Interrupt on receiving a frame
First I’ll discuss how I handled the receive interrupt. In a way, I already used this interrupt in polling mode. How? Well, “enabling” interrupts only enables their reflection on the IRQ pin. The registers, with the interrupt “disabled”, still behave such that the appropriate bits in the status register are set. That’s why, wanting to find out whether something arrived, I read the STATUS register, which contains information about interrupts, and checked whether the RX_DR bit, i.e. Receiver Data Ready, was set.
Such continuous polling wastes a bit of the microcontroller’s power. A bit, because it’s only one register, but it still takes some time during I²C or SPI transfer. It’s better to check, for example, a flag in RAM that will tell us whether an interrupt occurred recently.
By setting the RX_DR bit in the CONFIG register I will cause an active (low) state to appear on the IRQ pin every time the RX_DR bit is set in the STATUS register. This means that I will read this register only when there is some valuable information there for me. In this case it will be the readiness of data to be read after receiving.
I wrote a short interrupt handler that sets the appropriate flags in the library depending on what happened.
void nRF24_IRQ_Handler(void)
{
uint8_t status = nRF24_ReadStatus();
uint8_t ClearIrq = 0;
// RX FIFO Interrupt
if ((status & (1 << NRF24_RX_DR)))
{
nrf24_rx_flag = 1;
ClearIrq |= (1<<NRF24_RX_DR); // Interrupt flag clear
}
// TX Data Sent interrupt
if ((status & (1 << NRF24_TX_DS)))
{
nrf24_tx_flag = 1;
ClearIrq |= (1<<NRF24_TX_DS); // Interrupt flag clear
}
// Max Retransmits interrupt
if ((status & (1 << NRF24_MAX_RT)))
{
nrf24_mr_flag = 1;
ClearIrq |= (1<<NRF24_MAX_RT); // Interrupt flag clear
}
nRF24_WriteStatus(ClearIrq);
}
I then handle these flags appropriately in the code. This happens outside the interrupt, because interrupts generally cannot last too long.
For interrupt handling I wrote an event function and appropriate callbacks, modeled after those from HAL.
__weak void nRF24_EventRxCallback(void)
{
}
__weak void nRF24_EventTxCallback(void)
{
}
__weak void nRF24_EventMrCallback(void)
{
}
void nRF24_Event(void)
{
if(nrf24_rx_flag)
{
nRF24_EventRxCallback();
nrf24_rx_flag = 0;
}
if(nrf24_tx_flag)
{
nRF24_EventTxCallback();
nrf24_tx_flag = 0;
}
if(nrf24_mr_flag)
{
nRF24_EventMrCallback();
nrf24_mr_flag = 0;
}
}
As you can see, the callbacks are empty by default and have the __weak symbol. You need to override them in your program, which I also did in the main.c file.
You put the nRF24_Event function into the main loop of the program. Events, i.e. the appropriate callbacks inside, will execute only if one of the interrupt-related flags is set.
Also remember to enable the EXTI_GPIO interrupt on the IRQ pin and set the callback for this interrupt!
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(Pin == NRF24_IRQ_Pin)
{
nRF24_IRQ_Handler();
}
}

Read after an interrupt
OK, an interrupt occurred, the receive flag was set and the callback was called. What should you do in such a callback? If it’s a callback for received data then… receive that data 🙂
In main.c I wrote an overriding function like this.
void nRF24_EventRxCallback(void)
{
do
{
nRF24_ReceivePacket(Message, &MessageLength);
Message[MessageLength] = 0; // end of string
MessageLength = sprintf(Message, "%s\n\r", Message);
HAL_UART_Transmit(&huart2, Message, MessageLength, 1000);
}while(!nRF24_IsRxEmpty());
}
What am I doing here? I receive data via nRF24_ReceivePacket as long as the data is available. The nRF24_IsRxEmpty() function tells us about data availability.
In the example I simply print this data to UART, but in your program it may go, for example, into a circular buffer, and in the buffer event be parsed accordingly. You have freedom here, however you must keep the receiving and checking whether the FIFO is already empty.
Also remember that the nRF24_ReceivePacket function returns through the second argument how much data was fetched from the nRF (Dynamic Payload).
Summary
This is what handling Dynamic Payload and the receive interrupt would look like. The transmitted data is still blocking, because the library actively waits for the transmission by polling the chip to see if it has finished. An interrupt would also be useful for that, but I’ll approach it a bit differently.
In the next article I’ll add circular buffers for transmitting and receiving, which I will nicely use in non-blocking transmit handling.
The whole series:
If you liked the article, you can support me by buying something from me 🙂 https://sklep.msalamon.pl/
You can find the full project along with the library as usual on my GitHub: TRANSMITTER, RECEIVER
If you noticed an error, disagree with something, would like to add something important, or simply feel like you’d like to discuss this topic, write a comment. Remember that the discussion should be polite and consistent with the rules of the Polish language.



0 Comments