Thursday, April 4, 2013

RF24 - Avoiding RX Pipe 0 For Enhanced Reliability

RX Pipe 0 Is Special


The NRF24L01(+) radios have six receiving hardware pipes as part of its MultiCeiver design. These pipes, zero through five [0-5], each can have their own address. This sounds great, but reality is, pipe 0 is special.

Pipe 0 is special because, whenever you transmit, RX pipe 0 is changed to that of the writing pipe's address. The fact this occurs is obscured, by design, by the RF24 driver. This is because the RF24 driver has specific logic for pipe 0. This is primarily why the startListening() method exists. Every time a call to startListening() is made, the RX pipe 0 address is shuffled back into the radio. Its shuffled in because whenever the radio transmits, the RX pipe 0 address is internally replaced by the radio.

While not explicitly declared in the data sheet, I believe I understand why this behaviour takes place. When you enable auto-acknowledgement, the receiving radio needs to reliably inform the transmitter of its ACK/NAK status. In turn, potentially returning an ACK payload. However, the receiver doesn't directly know who transmitted the message. Its not part of the message. In order for the receiver to reply, the transmitter must be prepared to listen for a reply back from the receiving radio. As such, if the radio simply listens for a reply using the destination address, it should always match and filter properly. This is a clever idea to prevent transmitting source addresses.

That's fine and all, and is rather clever, but there is a problem. The RF24 driver, in its attempt to hide this detail, creates an opportunity for lost and/or missed packets. Its a classic race condition. This is a race condition because, should a transmitter send a message before the application's call to startListening() completes, the radio will completely ignore the message. Even if received, it will be silently filtered out and ignored. That means all messages destined for RX pipe 0's address will be silently ignored until the reloading of RX pipe's address completes. That completion only takes place with a call to startListening().

This race condition is potentially compounded by the fact applications are free to have any amount of logic between the end of a write() or startWrite() call and the completion of a startListening() call. For almost the entire duration between [write()/startWrite()] and startListening() calls, the radio will ignore all messages addressed to pipe 0's address.

The solution? Well, There really isn't a neat and clean solution. While many applications won't have issue with using RX pipe 0, and the associated message loss, high traffic networks are likely to suffer from periodic message loss and potential packet loss without full use of all retry attempts. For this second case, imagine the radio starts listening into 14-retries out of 15. That means all but one of the available retry opportunities have been lost simply because the radio was ignoring those messages. In turn, this would also drive up latency on the transmitter's side.

Long story short, if you want a reliable network, don't use RX pipe 0. For small networks, use is unlikely to cause significant issue. But for a better option, just pretend you only have RX pipes one through five (1-5); for a total of five, rather than six. But if you insist on using RX pipe 0, always ensure your [write()/startWrite()] calls are as close as is possible to your startListening() call, so as to minimize the window of potential lost packets.

3 comments:

  1. I find it confusing to translate "Avoiding RX Pipe 0" into codes/config..

    Here is my codes, am I configuring them correctly to avoid pipe0 RX ?


    hub (rpi-huh.cpp) :-
    const uint64_t pipes[6] = { 0x7365727631LL, 0xF0F0F0F0E1LL, 0xF0F0F0F0E2LL, 0xF0F0F0F0E3LL, 0xF0F0F0F0E4, 0xF0F0F0F0E5 };

    radio.openWritingPipe(pipes[0]);
    radio.openReadingPipe(1,pipes[1]);
    radio.openReadingPipe(2,pipes[2]);
    radio.openReadingPipe(3,pipes[3]);
    radio.openReadingPipe(4,pipes[4]);
    radio.openReadingPipe(5,pipes[5]);

    RX_ADDR_P0-1 = 0x7365727631 0xf0f0f0f0e1
    RX_ADDR_P2-5 = 0xe2 0xe3 0xe4 0xe5
    TX_ADDR = 0x7365727631

    *** hub will RX on P1 to P5 and TX on P0

    node (sendto_hub.cpp) :-
    const uint64_t pipes[2] = { 0xF0F0F0F0E3L, 0x7365727631LL };

    radio.openWritingPipe(pipes[0]);
    radio.openReadingPipe(1,pipes[1]);

    RX_ADDR_P0-1 = 0xf0f0f0f0e3 0x7365727631
    RX_ADDR_P2-5 = 0xe2 0xe3 0xe4 0xe5
    TX_ADDR = 0xf0f0f0f0e3

    This node will TX on pipe0 and RX on pipe1...

    ReplyDelete
  2. The RF24 has SEVEN pipelines. One TX pipeline and six RX pipelines. In your code, you are only using five of the six RX pipelines.

    Basically, if you have a line that reads something like, "radio.openReadingPipe( 0, 0xf0f0f0f0e3 ) ;", then you'll have to deal with potential pipe 0 issues. Notice the, "0", for the pipe number.

    In your code, you're only using five reading pipes, plus one writing pipe. Your first reading pipe is 1, not 0. In your code, you're good. Technically the TX's pipe and RX pipe 0 are different pipes. And if, for example, you were a read-only node (never transmitted), you could open RX pipe 0 without an issue - ever.

    As an aside, in the code above, I have noticed you are using 'F' nibbles. Please check out my article on bad RF24 addresses. I would encourage you to not use 0x0 or 0xF nibbles in your addresses. Of the two, 0x0 is likely safer of the two. And even then I would encourage you to not use either within the first octet of your address...preferably not within the first two octets. As the data sheets says, 0xF (0b1111) is far more suitable to RF noise corruption.

    ReplyDelete
  3. Yes...thanks for the suggestions... I will change the node addressing...

    ReplyDelete