So far I’ve shown you how to use nRF24L01 with polling and I’ve already started doing something with interrupts. More precisely, I used the data-receive interrupt so that incoming data is read from the chip instead of constantly polling the chip to see whether something has arrived. It may not seem like much, but it was still some wasted time on communicating with the chip. What if we used the full potential of interrupts?

I’d also like to remind you that you can buy nRF24 modules directly from me while supporting me at the same time.

The whole series about nRF24L01+:

Interrupts in nRF24

In our chip we have 3 interrupt sources available:

  • data reception completed – RX_DR
  • transfer (sending) completed – TX_DS
  • maximum number of retransmits exceeded during transmission – MAX_RT

I won’t be dealing with the number of retransmits. Everyone may have their own idea for how to handle it. Whether that’s throwing an error or continuing retransmission attempts. In the library I leave room (a callback) to implement handling of this interrupt.

So we’re left with two. One for data reception, which I already partially implemented. Partially, because I’m going to change the overall handling concept a bit.

The second one is for end of transmission. This interrupt will help us avoid waiting unnecessarily for the end of transfer via nRF24. Thanks to it I’ll free the MCU for other tasks. This will be very useful when I want to send more than 32 bytes “at once” and need to split the message into packets that fit in the nRF’s Payload.

How to handle interrupts better?

First of all, I had to better organize interrupt handling. Przemysław (dambo) rightly pointed out to me that I can have a conflict of interests when it comes to SPI access. Thank you so much for that, because I completely forgot about it!

The conflict would be that an interrupt could come in while SPI is “occupied” in the main program loop. In that interrupt I also needed access to SPI, so there’s a clash because two places in the code want the same SPI “at once”. There are at least two solutions:

  • Create a “mutex” for the SPI resource
  • Handle the interrupt in main

I chose option number two. How? In the EXTI interrupt handler I only set a flag that such an interrupt occurred.

void nRF24_IRQ_Handler(void)
{
	Nrf24InterruptFlag = 1;
}

And that’s it. Thanks to this flag, only in the event from nRF24 do I read from the chip’s registers which interrupt occurred and set another flag – now a specific one for the interrupt type.

void nRF24_IRQ_Read(void)
{
	if(Nrf24InterruptFlag == 1)
	{
		Nrf24InterruptFlag = 0;

		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);
	}
}

This way I’m sure that SPI will be free. Well, almost, because it could still be occupied by another chip via DMA… However, when using DMA, in my opinion it’s worth taking the entire interface exclusively for a single chip, so others won’t be able to squeeze in then 🙂

Now in the nRF24_Event function I can take care of these flags. At this stage it looks like this.

void nRF24_Event(void)
{
	nRF24_IRQ_Read(); // Check if there was any interrupt

	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;
	}
}

We have interrupt reading and reactions to them through callbacks.

Data management

You can now implement all handling in callbacks, but I’ll keep them as an additional layer. In the previous blog post I introduced you to the idea of a circular buffer. I wrote that I want to use it for nRF24 transmission. And that’s what I did.

I created two buffers – a receive buffer and a transmit buffer. From the names you can already guess what they’re for.

In the receive buffer, data will appear right after it is received, i.e. after the RX_DR interrupt occurs.

In the transmit buffer, on the other hand, we will write what we want to send. The library, or rather the event function, will check whether there is something to send and if the radio is free it will send what is in the buffer.

Thanks to such a solution, we don’t care what nRF24L01 is doing at the moment. We push data into and pull data out of the buffers whenever it’s convenient for us. What’s more! I’ll show you that you don’t have to worry about the 32-byte Payload limit! You can handle it automatically.

Ring buffers

I slightly modified the buffer I described previously. The educational implementation was very simple. Here I need something more advanced, but not necessarily full of unnecessary functions.

First, I dealt with buffer size. Instead of a fixed size for each buffer, I allowed myself to use something that in the C99 standard is called a Flexible array member. In short, the last element in a data structure can be a dynamic array. That will be my data buffer. Thanks to this I can have a small transmit buffer and a large receive buffer if needed.

Using FAM means the objects must be created dynamically using malloc. The structure definition looks like this.

typedef struct
{
	uint8_t Size;
	uint8_t Head;
	uint8_t Tail;
	uint8_t Elements;
	uint8_t Buffer[];	// Flexible Array Member
} RingBuffer;

As you can see, the last element is a pointer to an array. Interestingly, if you do sizeof(RingBuffer), it will return the value 4 as if that array wasn’t there at all 🙂

How do you create such a buffer now?

RB_Status RB_CreateBuffer(RingBuffer **Buffer, uint8_t Size)
{
	*Buffer = malloc(sizeof(RingBuffer) + (sizeof(uint8_t) * Size));

	if(Buffer == NULL)
	{
		return RB_NOTCREATED;
	}

	(*Buffer)->Size = Size;
	(*Buffer)->Head = 0;
	(*Buffer)->Tail = 0;
	(*Buffer)->Elements = 0;

	return RB_OK;
}

In malloc you provide the size of the structure plus the number of elements in the array. That’s it.

As you can see, in the structure I created a Size field. Now you have to remember what size the buffer is, because each one can be different. The Elements field will tell us how much data is currently stored.

For working with nRF24 I also created a function that returns exactly how many elements the buffer holds.

uint8_t RB_ElementsAvailable(RingBuffer *Buffer)
{
	return Buffer->Elements;
}

Buffers are created during nRF24 initialization and are visible only within its library.

Receiving data

Data reception happens when the nrf24_rx_flag flag is set. It is implemented similarly to the previous post, with the difference that I write the data into the internal ring buffer.

void nRF24_ReceiveData(void)
{
	uint8_t i, DataCounter;
	uint8_t RXPacket[32];
	do
	{
		nRF24_ReceivePacket(RXPacket, &DataCounter);

		for(i = 0; i < DataCounter; i++)
		{
			RB_WriteToBuffer(RXBuffer, RXPacket[i]);
		}

	}while(!nRF24_IsRxEmpty());
}

Now, to use this data, it’s enough to “read” it from nRF24. If the returned amount of data equals zero, it means nothing arrived. 

nRF24_ReadData(Message, &MessageLength);
if(MessageLength > 0)
{
	HAL_UART_Transmit(&huart2, Message, MessageLength, 1000);
}

The function returns as much as there is available data in the buffer – there is no Payload-related limitation.

nRF24_RX_Status nRF24_ReadData(uint8_t *Data, uint8_t *Size)
{
	uint8_t i = 0;
	*Size = 0;

	  if(nRF24_RXAvailable())
	  {
		while(RB_OK == RB_ReadFromBuffer(RXBuffer, &Data[i]))
		{
			i++;
		}
		*Size = i;
	  }
	if(*Size == 0)
	{
		return NRF24_NO_RECEIVED_PACKET;
	}

	return NRF24_RECEIVED_PACKET;
}

Meanwhile, checking whether there is something to read in the ring-buffer version is simply checking whether there is something in the buffer.

uint8_t nRF24_RXAvailable(void)
{
	return nRF24_IsSomtehingToRead();
}

Transmitting data

Now we operate the other way around. We write to the buffer, and the library checks whether there is something to send and sends it.

Writing to the nRF buffer happens – you could say – “normally”.

MessageLength = sprintf(Message, "abcdefghijklmnopqrstuwxyz1234567890\n\r" );
nRF24_SendData(Message, MessageLength);

This gets put into the transmit buffer.

nRF24_TX_Status nRF24_SendData(uint8_t* Data, uint8_t Size)
{
	uint8_t i = 0;

	while(Size > 0)
	{
		if(RB_OK == RB_WriteToBuffer(TXBuffer, Data[i++]))
		{
			Size--;
		}
		else
		{
			return NRF24_NO_TRANSMITTED_PACKET;
		}
	}
	return NRF24_TRANSMITTED_PACKET;
}

Then, when the nRF24 event comes, the library checks whether there is something to transmit. The radio must also be free, so I created another flag – Nrf24TXFreeFlag.

This flag is cleared according to its name, so zeroed means the radio is currently not free. Next I retrieve the number of bytes to send. If it’s larger than the maximum possible Payload, I cap it at 32 bytes. I read the required amount of data and send it to the chip.

void nRF24_CheckTXAndSend(void)
{
	uint8_t i, DataCounter;
	uint8_t TXPacket[32];

	if(nRF24_IsSomtehingToSend() && Nrf24TXFreeFlag)
	{
		nRF24_TX_Mode();

		Nrf24TXFreeFlag = 0;
		DataCounter = RB_ElementsAvailable(TXBuffer);
		if(DataCounter > 32)
		{
			DataCounter = 32; // Max Payload
		}

		for(i = 0; i < DataCounter; i++)
		{
			RB_ReadFromBuffer(TXBuffer, &TXPacket[i]);
		}

		nRF24_SendPacket(TXPacket, DataCounter);
		NRF24_CE_HIGH;
		nRF24_Delay_ms(1);
		NRF24_CE_LOW;
	}
}

You may notice there is one Delay here. In fact, it should last about 130 µs, because that’s how long the transition between nRF24 states takes, but the delay resolution in HAL is 1 ms. It’s a fairly small one, but you can insist on removing it. The idea for removing it came to me only while writing this text.

Ok, we sent data to the chip, and what next? After all, we have the Nrf24TXFreeFlag flag cleared, and to send more data we need to set it.

This is where the TX_DS interrupt rides in on a white horse, telling us that the radio transmission has finished. In the event, when the TX_DS flag appears, I set Nrf24TXFreeFlag, which means the next data can be sent. This may be the next part of a message if it was longer than 32 bytes.

How the library works and what it looks like

The current shape of the library is quite complex and unfortunately not very readable… All because I insisted that one set of files should handle three cases:

  1. Polling
  2. Receive on interrupt, send on polling
  3. Everything on interrupt

Several defines are used to configure the entire library

//
//	Configuration
//
#define NRF24_USE_INTERRUPT		1
#if (NRF24_USE_INTERRUPT == 1)
#define NRF24_USE_RINGBUFFER	1
#endif


#define NRF24_DYNAMIC_PAYLOAD	1

#if (NRF24_USE_RINGBUFFER == 1)
#define NRF24_RX_BUFFER_SIZE 32
#define NRF24_TX_BUFFER_SIZE 32
#endif

I think it’s easy to guess what they switch. In the article I omitted these different options in the functions for the sake of better readability of the ring-buffer part itself.

I wrote it all so that in all modes you use exactly the same functions for receiving and transmitting, so the final use of the library is quite simple. It always boils down to initialization and three basic functions.

#if (NRF24_USE_INTERRUPT == 1)
//
//	Main event for whole communication
//	Put it in main while(1) loop
//
void nRF24_Event(void);
#endif

//
// TRANSMITTING DATA
//
nRF24_TX_Status nRF24_SendData(uint8_t *Data, uint8_t Size);
nRF24_RX_Status nRF24_ReadData(uint8_t *Data, uint8_t *Size);

Summary

At this moment, in my opinion the library is finished. There are still a few minor things to improve, like that small delay during transmission, but it’s not that necessary. I tested transmitting and receiving quite “stressfully” by trying to send multiples of the Payload at once. On both the transmitter and receiver side everything worked perfectly, so I consider my library a success.

I’d be happy to hear your opinion about my code and suggestions for possible fixes or improvements. I’m open to discussion 🙂

For now, that’s all about nRF24L01+. In the future I’ll come back with range testing, but I have to build a measurement setup. Maybe we’ll do it together on a LIVE? 🙂

The whole series:

If you enjoyed the article, you can support me by buying something from me 🙂 https://sklep.msalamon.pl/

The full project along with the library can be found, 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 in accordance with the rules of the Polish language.

Podobne artykuły

.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *