2018年11月16日 星期五

Make Nordic Enhanced ShockBurst™ (ESB) Transceivers as Multiceiver and Reliable


    ※ This post focuses on nRF24LE1, if you are interested in how to use an independent microcontroller to control NRF24L01, please refer to my another post.

    ※ The multi-sending and multi-receiving code is here,  Receiver send back Acknowledge code is here.

      Nordic's Enhanced ShockBurst™ transceivers have been very widely adopted in IoT (Internet of things) industries or maker area, they feature affordable prices while being of mass deployment. Also the design references, documents, demonstration and example code are huge, tt is easy for users to use the transceivers and chips adapting their purposes.
      There are enormous example code of operating these transceivers, those code are on Arduno, STM32, MSP430...and so forth. In the most circumstances, the are ample for the users.
     But those code are few in mentioning "How to implement a RELIABLE multi-transmission". Even about the issue "how to operate a sender with designating a receiver",  the explanation is obscure. The lots discussion are written in words, the example code is lacked.
      The reliable transmission issue for are very essential for IoT purpose :  if the transmission is not reliable, it is difficult to plan a network topology.  If the mechanism of checking reliability does not be done in network-link layer, the mechanism must be implemented in application. It leads it is difficult to design such protocol, and the code be tedious.
      It is important also for the designated destination issue. If in network link layer, the sender is not able to specify which transceiver should receive.It  brings all device would receive the packets of the EACH sender, it results in, the firmware work be increased to filter out redundant data. Besides, if one transceiver receives too much packets simultaneously, it may be hanging.

    This post tries to demonstrate how to designated the receiver, and provide a example of the transmission with acknowledge.

  甲. Implement three firmware, one is able to multi-send with designating destination, another is able to multi-receive with designating source, the other is sender and receiver in one, but sending and receiving are from different address.

 乙. Implement a pair of firmware, the receiver is able to receive packet from six different addresses reliably.

 
   There are three famous Enhanced ShockBurst™ transceivers and chips in the market, I explain ahead:

壹. nRF24L01/nRF24L01+ : Pure transceiver, SPI communication with a microcontroller. (Note: nRF24L01 with plus does not support 250kb data rate)

貳. nRF24LE1  : An 8051 based microcontroller integrated Enhanced ShockBurst™ function, operate the communication via internal SPI.

參. nRF24LU1 : Similar with nRF24LE1, but this microcontroller has been integrated USB device function.

    My codes are based on nRF24LE1, but it is easy to port the code to the other microcontrollers.

一. implement a sender/receiver pair, which support multi-sending/receiving with designated destination and sources.


take look at  trscvr_para.h, those parameters must be the same for the transceivers which are  going to communicate each other. You should regard the ADDR number, those are adopted to set the pipe addresses:

#define ADDR_PIPE0_0        (0x10)
#define ADDR_PIPE0_1        (0x11)
#define ADDR_PIPE0_2        (0x12)
#define ADDR_PIPE0_3        (0x13)
#define ADDR_PIPE0_4        (0x14)


#define ADDR_PIPE1TO5_1        (0xE1)
#define ADDR_PIPE1TO5_2        (0xE2)
#define ADDR_PIPE1TO5_3        (0xE3)
#define ADDR_PIPE1TO5_4        (0xE4)

#define ADDR_PIPE1_0        (0x81)
#define ADDR_PIPE2_0        (0x82)
#define ADDR_PIPE3_0        (0x83)
#define ADDR_PIPE4_0        (0x84)
#define ADDR_PIPE5_0        (0x85)


The address structure for communication pipe, is as below (ref: nRF24L01+ Preliminary Product Specification v1.0, page 36):


PIPE0 ADDR_PIPE0_0 ADDR_PIPE0_1 ADDR_PIPE0_2 ADDR_PIPE0_3 ADDR_PIPE0_4
PIPE1 ADDR_PIPE1_0 ADDR_PIPE1TO5_1 ADDR_PIPE1TO5_2 ADDR_PIPE1TO5_3 ADDR_PIPE1TO5_4
PIPE2 ADDR_PIPE2_0 ADDR_PIPE1TO5_1 ADDR_PIPE1TO5_2 ADDR_PIPE1TO5_3 ADDR_PIPE1TO5_4
PIPE3 ADDR_PIPE3_0 ADDR_PIPE1TO5_1 ADDR_PIPE1TO5_2 ADDR_PIPE1TO5_3 ADDR_PIPE1TO5_4
PIPE4 ADDR_PIPE4_0 ADDR_PIPE1TO5_1 ADDR_PIPE1TO5_2 ADDR_PIPE1TO5_3 ADDR_PIPE1TO5_4
PIPE5 ADDR_PIPE5_0 ADDR_PIPE1TO5_1 ADDR_PIPE1TO5_2 ADDR_PIPE1TO5_3 ADDR_PIPE1TO5_4

Only pipe 0 have complete 5-byte address values ; the first to the fourth bytes of the others are the same, there are only zeroth bytes different.

The addressese should be the same for a pipe of a TX corresponding to a RX to make the communication work. (but the pipe numbers are not necessary to be the same)


The addresses are allocated to each pipe via below code(line 39 of esb_app_ptx_noack.c , line 36 of esb_app_txrx_noack.c and line and line ine 37 of esb_app_prx_noack.c ) :


{
 uint8_t i;
 for(i = HAL_NRF_PIPE0; i <= HAL_NRF_PIPE5; i++)
  hal_nrf_set_address(i, l_pipe_addr[i]);
}/*set all addresss*/


Specify a pipe as TX pipe (line 108 of esb_app_ptx_noack.c and line 128 of esb_app_txrx_noack.c) :
hal_nrf_set_address(HAL_NRF_TX, l_pipe_addr[tx_pipe_number]);


Specify the pipe(s) to receive packets(line 45 of esb_app_prx_noack.c and line 43 of esb_app_txrx_noack.c) :
hal_nrf_open_pipe(RX_PIPE, false); 

Open in here, does not mean "Enable the RF function", it is only to open the receiving function of that pipe; and the false in the argument, means to disable the auto-ack function.
If the pipe is only for sending data, it is not necessary to call the hal_nr_open_pipe function.


In my code, The PTX would send packet via pipe 5, and RX receive packet from pipe 5. UART, in baudrate 38400.

The UART outputs are :


PTX:
rf snd in pipe = 5, len = 6:: 05 da 00 20 eb bc
rf snd in pipe = 5, len = 6:: 05 db 00 20 ed b0
 2158 sec
rf snd in pipe = 5, len = 6:: 05 dc 00 20 ef a4
 2159 sec
rf snd in pipe = 5, len = 6:: 05 dd 00 20 f1 98
rf snd in pipe = 5, len = 6:: 05 de 00 20 f3 8c
 2160 sec
rf snd in pipe = 5, len = 6:: 05 df 00 20 f5 80

PRX:
rf rcv in pipe = 5, len = 6:: 05 be 00 20 b5 0c
rf rcv in pipe = 5, len = 6:: 05 bf 00 20 b7 00
 4645 sec
rf rcv in pipe = 5, len = 6:: 05 c0 00 20 b8 f4
rf rcv in pipe = 5, len = 6:: 05 c1 00 20 ba e8
 4646 sec
rf rcv in pipe = 5, len = 6:: 05 c2 00 20 bc dc

If you modify the RX_PIPE as HAL_NRF_PIPE0 or change the address value, and leave the TX codes intact, the output would be

 8 sec
 9 sec
 10 sec
 11 sec
 12 sec
 13 sec

That is, the PRX receiving nothing. Is is very natural, The pipe addresses of TX and RX  are not corresponding to each other.


If you add a device, which runs the TXRX code, and define the macro _RX_FOR_ALL_CHANNEL in PRX (in line 12 of  PRX\esb_app_prx_noack.h) and leave the PTX in working, the output would be :

TXRX :
rf snd in pipe = 3, len = 6:: 05 6c 00 00 d4 e4
rf rcv in pipe = 5, len = 6:: 05 cc 00 01 90 64
 55 sec
rf snd in pipe = 3, len = 6:: 05 6d 00 00 d6 d8
rf rcv in pipe = 5, len = 6:: 05 cd 00 01 92 58
rf snd in pipe = 3, len = 6:: 05 6e 00 00 d8 cc
rf rcv in pipe = 5, len = 6:: 05 ce 00 01 94 4c
 56 sec
rf snd in pipe = 3, len = 6:: 05 6f 00 00 da c0
rf rcv in pipe = 5, len = 6:: 05 cf 00 01 96 40

PRX:

rf rcv in pipe = 5, len = 6:: 05 4a 00 02 86 7c
rf rcv in pipe = 3, len = 6:: 05 ec 00 01 ce e4
rf rcv in pipe = 5, len = 6:: 05 4b 00 02 88 70
 76 sec
rf rcv in pipe = 3, len = 6:: 05 ed 00 01 d0 d8
rf rcv in pipe = 5, len = 6:: 05 4c 00 02 8a 64
rf rcv in pipe = 3, len = 6:: 05 ee 00 01 d2 cc
 77 sec
rf rcv in pipe = 3, len = 6:: 05 ef 00 01 d4 c0

The TXRX recive packet from pipe 5, and send the packets vis pipe 3. PRX receives the packets from the ALL pipes : the packets of PTX from pipe 5, the packets of TXRX from pipe 3.

     To here, you could try to think for a while for why Nordic calls communication pipe as pipe : it causes  a pipe, does not represent a device, it represents a communication channel of a corresponding sender/receiver.  If your topology needs, a pipe could be used for a group of sender/receiver (it is a bit like the "group" term in Zigbee).




二.  Make a sender/receiver pair, the communication in which is reliable : there sender would know if the receiver is indeed received the packet or not.


The key to enable auto-acknowledge mechanism, is to set the corresponding address as HAL_NRF_TX and HAL_NRF_PIPE0, and open the two pipe with auto-ack function(about line 46 of PTX/esb_app_ptx_ack.c).


 
 {
  uint8_t i;
  for(i = HAL_NRF_PIPE0; i <= HAL_NRF_PIPE5; i++)
   hal_nrf_set_address(i, l_pipe_addr[i]);
 }/*set all addresss*/

#ifndef _TX_FOR_ALL_CHANNEL  
 hal_nrf_set_address(HAL_NRF_PIPE0, l_pipe_addr[TX_PIPE]); 
 hal_nrf_set_address(HAL_NRF_TX, l_pipe_addr[TX_PIPE]);
 
 hal_nrf_open_pipe(HAL_NRF_PIPE0, true);
 hal_nrf_open_pipe(TX_PIPE, true);
#endif

it is necessary to open the pipe 0 is for receiving ack signal .


If you would like to append some data to the acknowledge packet from receiver side, you must call the 2 function (about line 65 of PTX/esb_app_ptx_ack.c) :

 hal_nrf_enable_dynamic_ack(true);
 hal_nrf_enable_ack_payload(true);

In sender's RF interrupt request (RF-IRQ) function, you could determine if it is ack-appending or not though which pipe receives. The determination I adopted in my code  about line 131 of PTX/esb_app_ptx_ack.c. Pay attention: the ack packets are from pipe 0 always :


if(irq_flags & (1<<HAL_NRF_RX_DR))
{ 
 //ack payload received
 uint8_t received_pipe; 
 if(false == hal_nrf_rx_fifo_empty())
 {
  uint16_t pipe_and_len;
  pipe_and_len = hal_nrf_read_rx_payload(&l_rx_payload[0]);
  
  received_pipe = (uint8_t)(pipe_and_len >> 8);
  l_rx_payload_len = (uint8_t)(0xff & pipe_and_len);
 }/*if */
 
 hal_nrf_flush_rx();
 if(HAL_NRF_PIPE0 == received_pipe)
  l_is_ack_reached_flag = 1;
  
}/*if */


The judgment is very important while you want to implement a device of sender/receiver as one (as the TXRX folder) .


In the receiver side, the ack-packet will be sendbacked via the function:

void hal_nrf_write_ack_payload(uint8_t pipe, const uint8_t *tx_pload, uint8_t length);

the pipe in argument must be as the same as the receiving pipe. You could refer to function esb_send_ack_data, in PRX/esb_app_prx_ack.c, line 147.


After preliminary explanation, I show their UART output for more detail clarification:

PTX :


rf snd in pipe = 3, len = 6:: 05 38 04 12 15 00
ack received ::  6a 6a 6a 6a
 2205 sec
rf snd in pipe = 3, len = 6:: 05 39 04 12 15 00
ack received ::  6b 6b 6b 6b
rf snd in pipe = 3, len = 6:: 05 3a 04 12 15 00
ack received ::  6c 6c 6c 6c
 2206 sec
rf snd in pipe = 3, len = 6:: 05 3b 04 12 15 00
ack received ::  6d 6d 6d 6d

PRX:

rf rcv in pipe = 3, len = 6:: 05 37 04 12 15 7d
ack len = 4::  6a 6a 6a 6a
 3000 sec
rf rcv in pipe = 3, len = 6:: 05 38 04 12 15 7d
ack len = 4::  6b 6b 6b 6b
rf rcv in pipe = 3, len = 6:: 05 39 04 12 15 7d
ack len = 4::  6c 6c 6c 6c
 3001 sec
rf rcv in pipe = 3, len = 6:: 05 3a 04 12 15 7d
ack len = 4::  6d 6d 6d 6d

The PTX and PRX are consisted with each other. PTX sends a serial number  to PRX, PRX acknowledges with another series of number.

If you turn off the PRX device, PTX would show the warning of retry_count_reached. It is, HAL_NRF_MAX_RT IRQ  has been triggered (about line 105 of PTX/esb_app_ptx_ack.c) :  The retry count has been reached but the packet does not been sent successfully yet.


 2986 sec
rf snd in pipe = 3, len = 6:: 05 53 04 12 15 00
rf_max_retry_count_reached!!
rf snd in pipe = 3, len = 6:: 05 54 04 12 15 00
rf_max_retry_count_reached!!
 2987 sec
rf snd in pipe = 3, len = 6:: 05 55 04 12 15 00
rf_max_retry_count_reached!!


But if HAL_NRF_PIPE0  do not be open with auto_ack (hal_nrf_open_pipe(HAL_NRF_PIPE0, true) ), this HAL_NRF_MAX_RT IRQ would never been triggered even the max-retry count has been reached.

Max retry count and trying-interval are able to be set via the function:


void hal_nrf_set_auto_retr(uint8_t retr, uint16_t delay);

I called it at line 7 of PTX/esb_app_tx_ack.c.


     In this example, I assume all sending event request reciever's ack signal. If your sender suddenly does not need the ack from the receiver, you could call the function for sending the packet:

void hal_nrf_write_tx_payload_noack(const uint8_t *tx_pload, uint8_t length);

Calling this function would set the no_ack flag of ESB packet as true. Once the  no_ack flag has been set, The receiver would not sendback the auto-ack signal to sender.
     (The dynamical ack function, hal_nrf_enable_dynamic_ack, must be configured before calling this function, as line65, PTX/esb_app_ptx_ack.c,  More detail about the ESB packet format, please refer to here)

     However, in the application layer, there is NO WAY for the receiver to query if the no_ack flag in received packet has been set or not. If your receiver sendbacks an ack_payload to a sender requesting no_ack,  your sender would not received that.
     A solution to this issue is to adopt different pipe specified for ack/no_ack transmission, which is Nordic's suggestion. The other solution I know, is to give a bit in payload to describe if the packet needs ack-payload or not.


NOTE :

For PTX, you could change macro TX_PIPE to the other pipe, or enable macro _TX_FOR_ALL_CHANNEL for sending each channel by turns. There is also macro PX_PIPE/_PX_FOR_ALL_CHANNEL for PRX, those functions are similar to PTX's.

Nordic's doc said the Constant ESB_MAX_ACK_PAYLOAD_LEN, is 32 bytes, but based on my test, the length is 4 bytes only. If you know how to effectively sendback a packet of 32 bytes appended with acknowledge, please tell me. But from my experience, 4 bytes is very enough for appending to acknowledge in a good IoT architecture.


沒有留言:

張貼留言