NRF24L01+ Driver : Part 6 : C-File Driver Functions

The moment you have been waiting for is here after  this post you will be ready to crowd even more the 2.4GHz radio space. 

First I will mention that in the code you will see lines such as: NRF.spiSend(data) 
Remember that NRF is an internal structure that will hold a pointer to the hardware specific functions. Obviously right now it still does not have those pointers passed to it. I will save that for last, in the init function. So for now just assume it will call you hardware specific functions and that your functions work haha.

Also note these functions are not presented in an order that they should be called. That will become clear in the init function as well as the main user application.
 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void NRF_cmd_write_entire_reg(uint8_t reg, uint8_t value)
{
	NRF.NRF_CSN_LOW();	
	
	NRF.spiSend(reg | W_REGISTER);       //write data register
	NRFSTATUS = NRF.spiRead();
	
	NRF.spiSend(value); 
	NRFSTATUS = NRF.spiRead();

	NRF.NRF_CSN_HIGH();
}
The function we use to write an entire 8 bit , 1 byte register. Not caring if we modify or change all the bits. For example when writing the desired address for Pipes 2 through 5, it requires an 8bit address so you just write all of it at once. 
In order to make the NRF start listening on its SPI lines we have to bring the slave select line CSN  to a LOW state, and only then can communication start. Once we are done the CSN has to go back HIGH. 

To WRITE to a register we have to OR the register address ( which is the register name in our defines) with the W_REGISTER command. When we send a command the NRF will reply with the status register contents, you can store that in a variable for NRFSTATUS if you want or not. 

After sending the write command you have to send the data you intend to write.

then bring CSN low to end SPI communication.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
uint8_t  NRF_cmd_read_single_byte_reg(uint8_t reg)
{
	NRF.NRF_CSN_LOW();
	
	NRF.spiSend(reg);    // send register name
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend(DUMMYBYTE);
   
	NRF.NRF_CSN_HIGH();
	return NRF.spiRead();
}
This function is used to read a single byte register. Again we bring the CSN low to start SPI communication. To send a read command you have to OR the register address with the R_REGISTER command which is actually zero, so OR will have no effect, essentially you just send the register address to read from it. 
Followed by a byte (DUMMYBYTE)  so that it can clock back the contents of the register you want to read. 
 After you send the DUMMYBYTE the data it replies with is the data you seek so you have the function return this value to the caller. Dont forget to set the CSN HIGH again before you return from the function.  I made a silly mistake where I have the CSN go HIGH after the RETURN and thus the CSN never went high again and my SPI communication was all messed up. 


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void NRF_cmd_modify_reg(uint8_t reg, uint8_t bit, uint8_t state)
{
	// thhis is a "read-modify-write" procedure 
	
	//READ
	uint8_t reg_value =  NRF_cmd_read_single_byte_reg(reg);
	
	//MODIFY
	if (state)
	{
		reg_value |= (1 << bit);
	}
	else
	{
		reg_value &= ~(1 << bit);
	}
		
	//WRITE
	NRF.NRF_CSN_LOW();	
	
	NRF.spiSend(reg | W_REGISTER);      //write data register
	NRFSTATUS = NRF.spiRead();
	
	NRF.spiSend(reg_value); 
	NRFSTATUS = NRF.spiRead();

	NRF.NRF_CSN_HIGH();
}
This function is to modify a bit an a register without touching the rest. So just like our MCU registers it involves a read-modify-write procedure.
First we read the register value by using the previous function and store the result in a variable reg_value.
Next we modify the bit we wasnt given the state we passed, where state means if we want the bit to be 0 or 1. 
Finally we start a write procedure and if your smart you will realize I could have just called the NRF_cmd_write_entire_reg function at this point.



These 3 previous functions are the generic and most used ones. I will give you 2 more functions that i used for debugging purposes. 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void NRF_cmd_read_multi_byte_reg(uint8_t reg, uint8_t numBytes, uint8_t *buff)
{
	NRF.NRF_CSN_LOW();
	
	NRF.spiSend(reg);   //read data register
	NRFSTATUS = NRF.spiRead();
	
	NRF.spiSend(DUMMYBYTE); 
	buff[0] = NRF.spiRead();
	
	NRF.spiSend(DUMMYBYTE); 
	buff[1] = NRF.spiRead();
	
	NRF.spiSend(DUMMYBYTE); 
	buff[2] = NRF.spiRead();
	
	NRF.spiSend(DUMMYBYTE); 
	buff[3] = NRF.spiRead();
	
	NRF.spiSend(DUMMYBYTE); 
	buff[4] = NRF.spiRead();
	
	//NRF.spiSendMultiDummy(numBytes, buff);   //could have used this too 	
	
	NRF.NRF_CSN_HIGH();
}
I used to make sure the value I was writing into the TX_ADDR was really going in there. As you can see I simply send a read command to a register and then send 5 DUMMYBYTE to get the data. It was originally in a for-loop hence the numbytes parameter 

Next up below is the antithesis of this function. It is used to write to a 5 byte register. I used this for debugging also , at the end of the day I made dedicated functions with names to write to the TX_ADDR and other 5 byte registers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void NRF_cmd_write_5byte_reg(uint8_t reg, uint8_t value)
{
	NRF.NRF_CSN_LOW();	
	
	NRF.spiSend(reg | W_REGISTER);        //write data register
	NRFSTATUS = NRF.spiRead();
	
	NRF.spiSend(value); 
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend(0x02); 
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend(0x03); 
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend(0x04); 
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend(0x05); 
	NRFSTATUS = NRF.spiRead();

	NRF.NRF_CSN_HIGH();
}

The values I send for this are for testing purposes only. I would call this function and then call the read multi function and expect to get these values back. You can omit or further develop this function if you should need it.

The functions that fallow are more specific to accomplish a certain task. Some of these are internally used and some are them pertain to the different commands that the user can use via the main structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void NRF_cmd_listen(void)
{
	NRF.NRF_CE_HIGH(); 
}
//---------------------------------------------
uint8_t NRF_cmd_get_status(void)
{
	return NRF_cmd_read_single_byte_reg(NRF_STATUS);
}
//--------------------------------------------
void NRF_cmd_clear_interrupts(void)
{
	NRF_cmd_write_entire_reg(NRF_STATUS, 0x70);
}
//--------------------------------------------
uint8_t NRF_cmd_get_pipe_current_pl(void)
{
	return NRF_cmd_read_single_byte_reg(RX_P_NO);
}
//--------------------------------------------
void NRF_cmd_FLUSH_TX(void)
{
	NRF.NRF_CSN_LOW();
	
	NRF.spiSend(FLUSH_TX);    
	NRFSTATUS = NRF.spiRead();	 
	
	NRF.NRF_CSN_HIGH();
}
//--------------------------------------------
void NRF_cmd_FLUSH_RX(void)
{
	NRF.NRF_CSN_LOW();
	
	NRF.spiSend(FLUSH_RX);       
	NRFSTATUS = NRF.spiRead();	 
	
	NRF.NRF_CSN_HIGH();
}
//--------------------------------------------
void NRF_cmd_act_as_RX(bool state)
{
	if (state)
	{
		NRF_cmd_modify_reg(NRF_CONFIG, PRIM_RX, 1); 
	}
		
	else
	{
		NRF_cmd_modify_reg(NRF_CONFIG, PRIM_RX, 0); 
	}
		
}

Refer to the post about the header file where I explain what all the elements do, this is exactly what the functions do, because the elements are just pointers to these functions. But I copy and paste it here so you can see. Note they are  out of order so just go by the name. Im feeling lazy.

  1. cmd_clear_interrupts : This is a global interrupt clearing command. It just clears all interrupts whether they are RX or TX related. (remember I am talking about clearing interrupts for the NRF not your MCU) This function  is accessible via the user struct in the main application.
  2. cmd_get_status : Returns the contents of the STATUS register, this is what will help you determine what interrupt fired, and take your action accordingly. Then after this you would call the previous command to clear it. This function  is accessible via the user struct in the main application.
  3. cmd_listen : Puts the NRF in a listening state when  acting as a receiver. It needs to be "listening" to receiver data, otherwise it will ignore the airways. This function  is accessible via the user struct in the main application.
  4. cmd_get_payload_width : Used to retrieve the width of the current received payload when using dynamic payload. This function  is accessible via the user struct in the main application.
  5. cmd_get_pipe_num_current_pl : this will return the pipe number for which the current received payload was intended for. This function  is accessible via the user struct in the main application.
  6. cmd_act_as_RX : This randomly placed command is the one that tells the NRF to act as a receiver (true)  or a transmitter (false) This function  is accessible via the user struct in the main application.
  7. cmd_flush_rx cmd_flush_tx : These two commands clear the FIFOs for rx and tx , any unread payload data is erased. This function  is accessible via the user struct in the main application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
uint8_t  NRF_cmd_read_dynamic_pl_width(void)
{
	return NRF_cmd_read_single_byte_reg(R_RX_PL_WID) ;
}
void NRF_cmd_setup_addr_width(uint8_t width) 
{	
	NRF_cmd_write_entire_reg( SETUP_AW, (width | 0x03) );
}
void NRF_cmd_activate(void)
{
	NRF.NRF_CSN_LOW();
	
	NRF.spiSend(ACTIVATE);    
	NRFSTATUS = NRF.spiRead();	
	NRF.spiSend(ACTIVATE_BYTE);    
	NRFSTATUS = NRF.spiRead();	
	
	NRF.NRF_CSN_HIGH();
}

The first two of the functions shown above are pretty self explanatory. The first is used to read the width of the current payload received when using dynamic payload.  That way you know how much data you have to read from you rx buffer.

Next is the setup address width function does what its name indicates. When you are a receiver or transmitter, you have to tell the NRF what size address width you want to use, 3 , 4 , or 5 bytes in length. The more the better for security but also that is more data you have to transmit.

Finally the activate function is used to activate features such as dynamic payload, no ack on current packet and sending a payload along with ack. I have described these features in previous posts about commands and registers. None of these are called directly from the user application they are accessed via the main struct.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
void NRF_set_tx_addr(uint32_t addr_high, uint8_t addr_low, bool auto_ack)
{	
	NRF.NRF_CSN_LOW(); //start SPI comms by a LOW on CSN

	NRF.spiSend(TX_ADDR | W_REGISTER); //send write command to ADDR
	NRFSTATUS = NRF.spiRead();

	/*  5 byte address is devided into a uint8_t low byte
	 *  and a uint32_t high byte
	 *  since the SPI can only send 1 byte at a time
	 *  we first send the low byte
	 *  and then extract the other bytes from the uint32 
	 *  and send them one by one LSB first
	 */
	
	NRF.spiSend(addr_low); 
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend(addr_high & 0xFF);
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend((addr_high >> 8) & 0xFF);
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend((addr_high >> 16) & 0xFF);
	NRFSTATUS = NRF.spiRead();
	NRF.spiSend((addr_high >> 24) & 0xFF);
	NRFSTATUS = NRF.spiRead();
	NRF.NRF_CSN_HIGH();
	
	/* If auto ack is enabled then the same address that was written 
	 * to TX_ADDR above must also be written to PIPE 0 because that
	 * is the pipe that it will receive the auto ack on. This cannot
	 * be changed it is hardwaired to receive acks on pipe 0 */
	
	if (auto_ack)
	{	
		NRF_cmd_modify_reg(EN_AA, ENAA_P0, 1); //enable auto ack on pipe 0	
		NRF.NRF_CSN_LOW();

		//write address into pipe 0
		NRF.spiSend(RX_ADDR_P0 | W_REGISTER);             
		NRFSTATUS = NRF.spiRead();

		NRF.spiSend(addr_low);
		NRF.spiSend(addr_high & 0xFF);
		NRF.spiSend((addr_high >> 8) & 0xFF);
		NRF.spiSend((addr_high >> 16) & 0xFF);
		NRF.spiSend((addr_high >> 24) & 0xFF);

		NRFSTATUS = NRF.spiRead();

		NRF.NRF_CSN_HIGH(); //end spi
	}	
}

This function is used to change  or set the address you are transmitting to. The NRF allows for address widths of 3 to 5 bytes. So I split those 5 bytes into a single byte wide uint8_t ,  and a 4 byte wide uint32_t . So when you call this function the low byte of the address you want to transmit get passed on through the addr_low parameter, and the renaming bytes where its 4 or 2 more bytes will get passed on through the addr_high parameter.  If this does not make sense it will when I show how to use the driver.
This function will be called in the init function but can also be called from the main structure to change the transmitting address anytime, so long as a transmission is not in process. This function  is not called directly by the user, but is called via the main struct using the cmd_set_tx_addr element



This function is used to set the address you want for a specific pipe. It will be called by the init function and you may also call it form the main structure from the main application, however I feel you will rarely need to change the address of a pipe at runtime, but the option is here nonetheless. 

First I check to see if the desired pipe is higher than 1, because PIPE 0 register  and PIPE 1 register are 5 bytes long, so all bytes can fit into it. If the function caller chooses a PIPE higher than 1, for example 2 - 5 , then that specific PIPE will get the low byte saved in its register, and PIPE 0 will contain the high bytes. This is not called directly by the user but is called via the cmd_set_rx_addr element in the main struct.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void NRF_cmd_read_RX_PAYLOAD(uint8_t *rx_buffer, uint8_t len)
{
	NRF.NRF_CE_LOW();
	
	NRF.NRF_CSN_LOW();
	
	NRF.spiSend(R_RX_PAYLOAD);
	NRFSTATUS = NRF.spiRead();
	
	//for some reason i get an empty byte at the beggning of rx payload so ill just read it here
	//so it doesnt use up a spot in my "len" count
	NRF.spiSend(DUMMYBYTE);
	NRFSTATUS = NRF.spiRead();

	NRF.spiSendMultiByte(dummy_array, len, rx_buffer);    
	
	NRF.NRF_CSN_HIGH();	
}

This function is responsible for retrieving our received payload and putting into a buffer, we must know the length of the data to be retrieved so that we know when to stop reading from the buffer, if we do not have a static length that means we are using dynamic payload and thus we should read the dynamic payload width and pass that value to this function. This function is not called directly by the user, but is called by using the cmd_read_payload element from the main structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void NRF_cmd_write_TX_PAYLOAD(uint8_t *data, uint8_t len)
{	

	NRF.NRF_CE_LOW();
	NRF.NRF_CSN_LOW();
	
	NRF.spiSend(W_TX_PAYLOAD);     
	NRFSTATUS = NRF.spiRead();
	NRF.spiSendMultiByte(data, len, rx_buff);     //faster than for loop with individual NRF.spiSend
	 
	NRF.NRF_CSN_HIGH();
	//CE set high to start transmition if in TX mode
	// must be held high for a bit then back low
	NRF.NRF_CE_HIGH();
	delayUS(50); //this was here for debugging purposes feel free to delete or insert your own
	NRF.NRF_CE_LOW();
}

This function is used to transmit our data, you pass it a pointer to your data and a value for the length of that data. This function is not accessed by the use directly but rather indirectly via the main struct.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
void NRF_init(CL_nrf24l01p_init_type *nrf_type)
{	
	//initialize NRF command functions
	nrf_type->cmd_clear_interrupts	= &NRF_cmd_clear_interrupts;
	nrf_type->cmd_get_status		= &NRF_cmd_get_status;
	nrf_type->cmd_set_rx_addr		= &NRF_set_rx_addr;
	nrf_type->cmd_set_tx_addr		= &NRF_set_tx_addr;
	nrf_type->cmd_listen			= &NRF_cmd_listen; 
	nrf_type->cmd_get_payload_width = &NRF_cmd_read_dynamic_pl_width;
	nrf_type->cmd_get_pipe_num_current_pl = &NRF_cmd_get_pipe_current_pl;
	nrf_type->cmd_read_payload		= &NRF_cmd_read_RX_PAYLOAD;
	nrf_type->cmd_transmit			= &NRF_cmd_write_TX_PAYLOAD;
	nrf_type->cmd_act_as_RX 		= &NRF_cmd_act_as_RX;
	nrf_type->cmd_flush_rx 			= &NRF_cmd_FLUSH_RX;
	nrf_type->cmd_flush_tx 			= &NRF_cmd_FLUSH_TX;
	NRF.spiSend				= *nrf_type->spi_spiSend;
	NRF.spiRead				= *nrf_type->spi_spiRead;
	NRF.spiSendMultiByte	= *nrf_type->spi_spiSendMultiByte;
	NRF.NRF_CE_HIGH			= *nrf_type->pin_CE_HIGH;
	NRF.NRF_CE_LOW			= *nrf_type->pin_CE_LOW;
	NRF.NRF_CSN_HIGH		= *nrf_type->pin_CSN_HIGH;
	NRF.NRF_CSN_LOW			= *nrf_type->pin_CSN_LOW;	
	
	
	NRF.NRF_CE_LOW();  //start SPI comms
	
	//common configurations	
	NRF_cmd_modify_reg(NRF_CONFIG, PWR_UP, 1);    // turn on 
	delayMS(100); //this was here for debugging purposes feel free to delete or insert your own
	NRF_cmd_modify_reg(NRF_CONFIG, CRCO, nrf_type->set_crc_scheme);      //set CRC scheme
	NRF_cmd_modify_reg(NRF_CONFIG, EN_CRC, nrf_type->set_enable_crc);    //turn on CRC	
	NRF_cmd_modify_reg(NRF_CONFIG, MASK_TX_DS, !(nrf_type->set_enable_tx_ds_interrupt));    //dsiable TX_DS interrupt on IRQ pin
	NRF_cmd_modify_reg(NRF_CONFIG, MASK_MAX_RT, !(nrf_type->set_enable_max_rt_interrupt));   //disable MAX_RT interrupt on IRQ pin
	NRF_cmd_modify_reg(NRF_CONFIG, MASK_RX_DR, !(nrf_type->set_enable_rx_dr_interrupt));   //enable RX_DR interrupt on IRQ pin
	NRF_cmd_write_entire_reg(RF_CH, nrf_type->set_rf_channel);  	//rf channel		
	NRF_cmd_write_entire_reg(EN_AA, 0x00);     //disable auto ack by derfault, might be enabled below if user wants
	NRF_cmd_write_entire_reg(NRF_STATUS, 0x70);      //clear any interrupts
    NRF_cmd_write_entire_reg(SETUP_AW, nrf_type->set_address_width);    //address width		
	

	// SET UP AS RECEIVER
	if (nrf_type->set_enable_rx_mode)
	{		
		NRF_cmd_modify_reg(EN_RXADDR, nrf_type->set_rx_pipe, 1);    //enable rx pipe 
		NRF_set_rx_addr(nrf_type->set_rx_pipe, nrf_type->set_rx_addr_byte_2_5, nrf_type->set_rx_addr_byte_1);			
		
		if (nrf_type->set_enable_dynamic_pl_width)
		{
			NRF_cmd_activate();
			NRF_cmd_modify_reg(FEATURE, EN_DPL, 1); //enable dynamic PL feature
			NRF_cmd_modify_reg(DYNPD, nrf_type->set_rx_pipe, 1); //enable dynamic PL for pipe
			NRF_cmd_modify_reg(DYNPD, PIPE_1, 1);  //enable dynamic PL for pipe
			//auto ack MUST to be enabled if using dynamic payload
			NRF_cmd_modify_reg(EN_AA, ENAA_P1, 1);//enable auto ack on pipe 1	
			NRF_cmd_modify_reg(EN_AA, nrf_type->set_rx_pipe, 1);     //enable auto ack on pipe 5	
		}
		else
		{
			NRF_cmd_write_entire_reg((nrf_type->set_rx_pipe + RX_PW_OFFSET), nrf_type->set_payload_width);   //write the static payload width
			
			//if using static PL width use can still use auto ack but now its optional
			if(nrf_type->set_enable_auto_ack)
			{	
				NRF_cmd_modify_reg(EN_AA, ENAA_P1, 1);       //enable auto ack on pipe 1	
				NRF_cmd_modify_reg(EN_AA, nrf_type->set_rx_pipe, 1);     //enable auto ack on pipe 5	
			}
		}	
	}
	// SET UP AS TRANSMITTER
	if(nrf_type->set_enable_tx_mode)
	{		
		NRF_cmd_write_entire_reg(SETUP_RETR, 0x2F);	
		
		NRF_set_tx_addr(nrf_type->set_tx_addr_byte_2_5, nrf_type->set_tx_addr_byte_1, nrf_type->set_enable_auto_ack);
		
		if (nrf_type->set_enable_dynamic_pl_width)
		{
			NRF_cmd_activate();
			NRF_cmd_modify_reg(FEATURE, EN_DPL, 1);    //enable dynamic PL feature
			NRF_cmd_modify_reg(DYNPD, PIPE_0, 1);		
		}			
	}
}

Finally this is the init function. Lets analyze it part by part. 

The first part which I commented as "initialize NRF command functions"  is where I assign functions to all the function pointers. The init functions takes a pointer to the main struct in the main application and passes to it all the driver internal functions that those specific commands/elements use. 
Then it takes all the hardware specific functions and passes them to the internal smaller struct called NRF. Now both internal and external structs have the information they need to work.

Then we begin SPI communication and start executing the different functions to pass the use settings to the NRF module. In each function we access the user specific setting by de-referencing the struct pointer that the user passed to the init function by using the  "->" notation but this is not a C tutorial so it is assumed you know that.

Next if "set_enable_rx_mode" is true then we pass all the RX required settings to the NRF module. But we do not put it into RX mode , we simply prepare the RX settings. We check if they want to enable dynamic payload and make execute the settings required for that. Or we execute the settings required for static payload width. 

Same steps are taken to set up the device as a transmitter, or rather prepare it to be a transmitter. You can have both RX and TX mode enabled at the same time because "enabled" in my driver just means that we have executed the necessary settings it needs to act as an RX or TX but it does not mean it is acting as either or just  yet.  

Remember to make it act as an RX you set the cmd_act_as_RX to true, in the main struct in the main  application. Or set it to false to act as a TX. 

Please take your time to read these functions. I think I have commented them pretty well and the readability of the code makes it very easy to understand what is going on. If you have any questions or spot a mistake feel free to reach out to me via the contact form on the right side of the website. 

In the last post of this series I will give you a usage sample. 


<< PREVIOUS | NEXT >>


Comments

Share your comments with me

Archive

Contact Form

Send