Frequency hopping experiments on the NRF24

Any old or new electronic projects on the go
User avatar
Posts: 371
Joined: 15 Feb 2018, 23:32

Frequency hopping experiments on the NRF24

Post by Phil_G » 10 Apr 2020, 10:49

Edit: If you want to skip the story, the code for Phil's simple but functional NRF FHSS set is here and here. As usual the thread charts the development of the set as a learning project, warts & all, from scratch to a practical FHSS system.

Ok I'm happy that the hybrid semi-hopping 'lockdown NRF24' project works really well in its current state, where the tx hops continuously but the rx only hops when necessary, but for interest it would be nice to implement full FHSS. Since its really another topic I've split the thread so the hybrid 'lockdown project' can be concluded.

Werner has demonstrated his own FHSS implementation on the NRF24L01, which is quite superb having been developed to or better than commercial standards - its clear that an awful lot of work has gone into it! Thanks again for your support Werner, my next goal is a proper interrupt-driven hopping thing :D


Posts: 14
Joined: 22 Jan 2020, 02:30

Re: Frequency hopping experiments on the NRF24

Post by WernerL » 11 Apr 2020, 00:27

Phil_G wrote:
10 Apr 2020, 10:49
Thanks again for your support Werner, I will try the full interrupt-driven hopping thing soon, are your 20 channels hard-coded, scanned quiet-channels, or chosen at random? I have a rough 'find-free-channels' routine worked out based on the < -64db flag.
The frequency hopping stuff is quite simple really.
Both transmitter and receiver use a pre-arranged list of 20 hop frequencies (exchanged through a one-time binding procedure, which I don't talk about for now to keep things simple).

On the transmitter side, a packet is sent every 5 ms. For each packet, the transmitter changes to the next hop frequency in the list, round-robin style. The 5ms timing is kept as precise as possible.

On the receiver side, the receiver stays on the first hop frequency until it catches a packet from the transmitter. (20 hops, one hop every 5 ms means after worst case within 100 ms the receiver should get a packet). Once the packet has been received, it starts a timer that is a bit larger than the 5 ms, and changes the receive channel to listen on next hop frequency. It listens now for the next packet on this new hop frequency. If the packet arrives, the 5+ ms timer is restarted and the next hop frequency is chosen, and so on.

If the packet got lost, the slightly-greater-than-5 ms timer expires. We then switch to the next hop channel and restart the time out, but this time with exactly 5 ms (because we don't want to drift away from the transmitter, we just want the timeout to be slightly behind the transmitter).

This continues until we either receive a packet, or if we miss multiple packets in a row (I think 6 in my case). If we miss 6 packets in a row, then the receiver assumes it is completely out of sync with the transmitter, and switches to the first hop frequency, where it listens until it receives a valid packet from the transmitter (should be less than 100 ms). Then the hopping restarts as described above.

Failsafe kicks in if there are no received packets within 600 ms.

With my system, the 20 frequencies and the "pipeOut" number (basically an address that have to match on the Tx and Rx for the Rx to receive a packet) are randomly generated every time I create a new model.
This information is stored alongside with all the mixer setup etc, so when I load a model into the transmitter then the RF values are being set correctly for that model as well.
This method has the positive side effect that you can't fly a model with the wrong mixer settings, and you can quickly switch transmitters without having to re-bind the receivers. And since the RF settings belong to the model, you can use different transmitters without having to re-bind receivers.

The RF protocol has a special binding mode, where infrequently special bind packets are transmitted on a fixed channel, with a fixed pipeOut number (address) and a lower transmit power. The transmitter sends those bind packets for the first 15 seconds after power-up, interleaved with the regular stick packets.
On the receiver there is a bind button. If I press it, the receiver switches to the bind frequency and listens on the fixed pipeOut number for bind packets. The bind packet holds the model pipeOut number and the list of 20 hop frequencies. Since this is too much payload to send in one packet, the data is actually split across multiple bind packets, with one byte indicating which part of the overall data is contained.

Attached is a full description of the reverse engineered protocol of a commercial nRF24 based RC system (, opens in any text editor).

cheers, Werner
(2.35 KiB) Downloaded 15 times

User avatar
Posts: 371
Joined: 15 Feb 2018, 23:32

Frequency hopping experiments on the NRF24

Post by Phil_G » 29 Apr 2020, 22:17

Thanks Werner :D
Well, my humble first attempt kinda works but as yet its not syncing properly. I'm posting this in the hopes that writing it down might kick my fuddled brain into gear.

The transmitter is fine, spot on 5ms packets sequenced over 16 FHSS channels.
The receiver code is giving me the runaround. I'm sure I have a flaw in the sequence, but I've worked through it a gazillion times and I cant see it. But tomorrows another day. Hopefully the answer might wake me up at 3:30am as these problems tend to do!

Basically I followed Werners text description in the previous post.
I've a 6ms timer interrupt which sets a timeout flag. With perfect reception the 6ms should never timeout. If it does, we've missed a packet so we switch to a 5ms timeout and carry on hopping.
If we miss 6 packets we restart from the first channel, and if we miss 200 it goes to failsafe.
Visually the set works fine, no servo jitters and instant response, failsafe works, but somethings not right...

For testing, I've put a bit-flip on D7 in the packet-receive section, for my scope, where I'd expect to see alternating 5ms high, 5ms low, with an occasional glitch expected when a packet was missed and the hopping was resyncing.
What I actually see is a 5ms high followed by 80ms of low, which indicates two packets received then it has a bit of a lie down, then two more...

I have it set up for 16 FHSS channels, which is where the 80ms is coming from, 5ms per channel. Which makes me think its either restarting constantly, or sitting on one channel and catching every 16th packet.
The servo drive is commented out in case its upsetting the timing - but makes no difference in or out.

Code: Select all

// Phil_G's lockdown project, March 2020.
// Simple 4 Channel R/C Receiver with frequency hopping and failsafe on all functions
// Use a 3v3 Promini, Vcc to NRF24 pos, 4-cell pack or BEC to Vraw and servo positives. All negs common.
// Servo signals on pins D2, D3, D4, D5.
// Set failsafe by linking pin 7, servo wiggles to confirm its stored.
// If failsafe is not set, loss of signal reverts to defaults - low throttle and neutral controls.
// Choose a unique 'pipeIn' value and use same value in tx, I suggest the last 5 digits of your phone number

#include <EEPROM.h>
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include <ServoTimer2.h>
#define failSafePin 7
#define failSafeGnd 8
#define failSafeTime 200  // failsafe if no signal for one full second 
#define lostpkTime 50      // try next channel if no packet received here within a couple of frametimes
#define thr_failsafe 1000

ServoTimer2 ch1, ch2, ch3, ch4;

struct Packet {
  unsigned int throttle = thr_failsafe;
  unsigned int elevator = 1500;
  unsigned int aileron  = 1500;
  unsigned int rudder   = 1500;

Packet data;
unsigned long newChannelTime = 0, lastRecvTime = 0, now = 0;
volatile bool timeout = 0, hop = 1;

/************* Radio config - make it unique! **********************************/
const byte pipeIn[] = "39964";   // last 5 digits of your phone number
const byte mychannels[] = {117, 99, 57, 71, 35, 13, 7, 17, 23, 27, 31, 41, 45, 123, 111, 3};  // 16 FHSS hopping channels

RF24 radio(9, 10);
int rfchan = 0, missedcounter = 0; // NRF channel index, number of packets missed

void setup()
  //  pinMode(failSafePin, INPUT_PULLUP); // momentarily link to gnd to set failsafe
  pinMode(failSafeGnd, OUTPUT); // momentarily link to gnd to set failsafe
  digitalWrite(failSafeGnd, 0); // convenient return for F/S link
  pinMode(A0, OUTPUT); // channel indicator led
  pinMode(A1, OUTPUT); // channel indicator led
  pinMode(A2, OUTPUT); // channel indicator led
  pinMode(A3, OUTPUT); // channel indicator led
  PORTC = 0;
  pinMode(7, OUTPUT); // scope

  // Servo pins

  //Configure the NRF24 module
  radio.begin(); delayMicroseconds(500);
  radio.openReadingPipe(1, pipeIn);  delayMicroseconds(500);
  radio.setAutoAck(false); delayMicroseconds(500);
  radio.setDataRate(RF24_250KBPS); delayMicroseconds(500);
  radio.setPALevel(RF24_PA_MIN); delayMicroseconds(500);
  radio.setChannel(mychannels[0]); delayMicroseconds(500);

  // set up 5ms interrupt
  TCCR1A = 0;        // reset Timer1 control reg A
  TCCR1B = 0;
  // set prescaler to /8 0b010
  TCCR1B &= ~(1 << CS10);   // clear CS10
  TCCR1B |=  (1 << CS11);   // set CS11
  TCCR1B &= ~(1 << CS12);   // clear CS12

  TCNT1 = 0;      // reset timer1 to zero and set compare value
  OCR1A = 12000; // 5ms x16mhz/8 = 10000; 6ms x16mhz/8 = 12000
  TIMSK1 = (1 << OCIE1A);   // enble timer1 compare interrupt
  sei();        // enable global interrupts

void loop()  {
  TCNT1 = 0; timeout = 0;      // reset timer1 to zero
  while (timeout == 0 || hop == 0) { // dont timeout if we're resyncing
    if (hop == 0) TCNT1 = 0;
    if (radio.available() ) {   // is there a packet on the current RF channel?
      TCNT1 = 0; OCR1A = 12000; timeout = 0;    // reset timer1 and set compare value to 6ms x16mhz/8 = 12000, sizeof(Packet));
      ch1.write(data.throttle); ch2.write(data.elevator); ch3.write(data.aileron); ch4.write(data.rudder);   // Servo outputs
      PORTD ^= (1 << PD7);    // flip D7 for scope, all being well every 5ms
      hop = 1;
      missedcounter = 0;
      rfchan = (rfchan + hop) % 16; radio.setChannel(mychannels[rfchan]); // cycle through mychannels[]
  // 6ms timeout
  if (++missedcounter < 7) {  // if we've missed 1 to 5 packets, hop on 5ms at a time until we see data
    TCNT1 = 0; OCR1A = 10000; timeout = 0;    // reset timer1 and set compare value to 5ms x16mhz/8 = 10000
    hop = 1;
    rfchan = (rfchan + hop) % 16; radio.setChannel(mychannels[rfchan]); // try next channel

  else {  // we missed six packets on the trot, restart from zero
    rfchan = 0; hop = 0;
    TCNT1 = 0; OCR1A = 12000; timeout = 0;    // reset timer1 and set compare value to 6ms x16mhz/8 = 12000

  if (missedcounter > 200) {  // failsafe if nothing heard for 1 second
    failSafe(); // Signal lost.. revert to failsafe values, either saved or default

// This function will write a 2 byte integer to the eeprom at the specified address and address + 1
void EEPROMWriteInt(int p_address, int p_value)
  byte lowByte = p_value % 256;
  byte highByte = p_value / 256;
  EEPROM.write(p_address, lowByte);
  EEPROM.write(p_address + 1, highByte);

//This function will read a 2 byte integer from the eeprom at the specified address and address + 1
unsigned int EEPROMReadInt(int p_address)
  byte lowByte =;
  byte highByte = + 1);
  return lowByte + highByte * 256;

void failSafe()
  rfchan = 0; hop = 0;

  if (EEPROMReadInt(0) == 0x5AA5) {   // if failsafe set
    data.throttle = EEPROMReadInt(2); // throttle
    data.elevator = EEPROMReadInt(4); // elevator
    data.aileron  = EEPROMReadInt(6); // ailerons
    data.rudder   = EEPROMReadInt(8); // rudder
  else {
    data.throttle = thr_failsafe; // Low throttle
    data.elevator = 1500; // Neutral
    data.aileron  = 1500; // Neutral
    data.rudder   = 1500; // Neutral
  ch1.write(data.throttle); ch2.write(data.elevator); ch3.write(data.aileron); ch4.write(data.rudder);   // Servo outputs

ISR(TIMER1_COMPA_vect) {   // timer, set to 6ms whilst in sync, 5ms when resyncing
  timeout = 1;

Fun & games eh.
Tomorrows plan is to slow the tx & rx right down to a visible hopping speed (just change the prescaler), maybe I'll see the flaw.

Posts: 14
Joined: 22 Jan 2020, 02:30

Re: Frequency hopping experiments on the NRF24

Post by WernerL » 29 Apr 2020, 23:44

Phil_G wrote:
29 Apr 2020, 22:17

What I actually see is a 5ms high followed by 80ms of low, which indicates two packets received then it has a bit of a lie down, then two more...
Ah! Fun memories of debugging the hopping ...
I too think that it currently may only listen to the first frequency.

I remember you previously had issues switching frequency with the nRF library you are using, having to reset a lot of settings? Maybe you are dealing with the same issue here?
Since you do receive a packet on the first channel every 80ms, maybe the receiver is never actually switching channel and always keep listening on the first hop frequency.

To verify this theory you could change all 16 hop frequencies on the TX to the same frequency, but keep the random hop frequency on the RX side. If you then see packets every 5ms then the receiver is clearly not changing channel properly.

It may also be that you are hopping slightly too late. The RX must already listen to the new frequency before the TX starts transmitting the packet. It is unlikely that this is a problem though, as it would also affect your 17th hop (=transmitting again on the 1st hop channel) after 80ms.

I am sure you will get it working soon, Werner

User avatar
Posts: 371
Joined: 15 Feb 2018, 23:32

Re: Frequency hopping experiments on the NRF24

Post by Phil_G » 02 May 2020, 12:36

Still struggling here, I've proven that the tx hops correctly and tx timing is spot on,
at the receiver side it picks up channel 0 ok then picks up channel 1 ok, then nothing until the 6-missed-packets counter forces a restart from zero, at which point it receives channels zero and one again...
I've spent two full days thinking and trying various things including (amongst many other experiments and much pondering):
Tried Werners suggestion of setting all 16 tx channels to RF channel 0, rx is hopping ok but not receiving after the first channel (0 ok, 1 times out, 2 times out... etc to 6 then it restarts on RF ch0 ok...) this and other tests has convinced me the rx is cycling through the RF channels ok.
changed the tx & rx timing by increasing the prescaler from /8 to /1024 which gives a packet every .64 seconds so its slow enough to see whats happening.
Set the aileron channel data so instead of stick position it sends the actual current RF channel number, this confirmed 0 & 1 are received ok then 2 and onwards are not.
Changed 'hop' from a bool to an int in case some weird casting was limiting channels to 0 & 1. Nope!
I'm sure its a logic flow problem but for the life of me I cant see it yet. The restart-on-6-lost-packets bit is working fine, as is failsafe (except where I've temporarily forced it to stay in the rx loop)
The "two good channels then 6 fails causing a restart from 0" is absolutely consistent, the same every time, so its not genuine lost packets.
With perfect reception it should never exit the while loop since there will never be a 6ms timeout (or 768ms when the prescaler is 1024)

The TIL311 has proven a very useful tool, as a direct register write to PORTC takes insignificant time.
I used it to show 'hop', rfchan, missedpackets etc at various stages :D

Posts: 347
Joined: 16 Feb 2018, 14:11
Location: Warwickshire

Re: Frequency hopping experiments on the NRF24

Post by Martin » 02 May 2020, 18:27

I'm working on a version that hops around all the legal channels, in a pseudo-random order that's calculated based on the supplied ID (which must be common for the transmitter and receiver sketch - I've not implemented any kind of 'binding' yet).

It doesn't use any of the nRF libraries - it hits the nRF24 hardware direct. I'm using the Shockburst feature to do the ID and CRC checking, but not the Enhanced Shockburst which relies on two-way communication.

The transmitter hops around all the channels in the pseudo-random order with fairly constant timing (done by a timer interrupt) but I've deliberately introduced a tiny bit of random jitter - partly so that two transmitters running similar code can't remain in transmit synch for long (this wouldn't really matter since they would rarely happen to clash on the same channel anyway) and partly to help test out the "receiver lock" performance.

When the receiver gets a valid packet, it knows which channel to listen on next - because it has the same pseudo-random order list that the transmitter is using.

The important thing is that the receiver starts listening before the transmitter transmits. When the receiver gets a valid packet it knows exactly when the next packet should arrive (plus or minus any 'random' jitter). If the receiver misses a packet, it still switches to the next frequency in its list based on a timer interrupt. The length of the listening time is longer than the transmitted packet time, and a valid received packet normally happens somewhere near the middle of the allotted 'listen time'. Even with a chain of ten missed packets, the small amount of deliberate jitter on the transmit spacing means that the packet still falls within the listening window.

If the receiver misses ten packets running, then it assumes it's lost sync and starts switching channels more slowly and listening for longer on each one. It still eventually switches around all the channels in its pseudo-random order list, but does so in the reverse order. Of course, as soon as it receives a valid packet it switches back into 'locked' mode.

The number of transmitted packets per second is pretty constant - because it's controlled by a timer with just the small amount of deliberate jitter. The receiver counts the number of packets it receives per second and sends that count (serially) to the IDE for debug purposes. When there is no interference and no bugs you expect the receiver to get the same packets per second as are being transmitted. The ratio of received packets per second to the (fairly constant) transmitted packets per second, gives a pretty good indication of how well the link is working.

The receiver debug code also keeps track of how many times it's lost sync. On a finished flying version, I intend to do a flashing LED (similar to Spektrum receivers) where counting the flashes after a flight allows the pilot to see how many times (if any) the sync was lost after first being acquired.

Still needs some debugging, and I've not written any code yet to encode/decode actual model-controlling data into and out of the transmitted data stream.

User avatar
Posts: 371
Joined: 15 Feb 2018, 23:32

Re: Frequency hopping experiments on the NRF24

Post by Phil_G » 02 May 2020, 18:46

That sounds spot on Martin, I think you've 'left no stone unturned' there, and avoiding the existing libraries is definitely a good call.
I really think with the right code these modules are a complete answer to the R/C module availability problem, and at a fraction of the cost. The only downside I can see is having to make receivers, though its simple enough and the RFnano has answered that one anyway :D

I found the NRF datasheet 'not the easiest' to follow, with much scrolling to and fro :D

Posts: 347
Joined: 16 Feb 2018, 14:11
Location: Warwickshire

Re: Frequency hopping experiments on the NRF24

Post by Martin » 02 May 2020, 22:47

Phil_G wrote:
02 May 2020, 18:46
I found the NRF datasheet 'not the easiest' to follow, with much scrolling to and fro :D
Me too. :? I found the nRF2401 datasheet for the older, simpler chip (without Enhanced Shockburst) helpful - the nRF24L01 datasheet concentrates on the Enhanced mode which, as far as I can see, is no use for our kind of frequency hopping. The L01 version is supposed to be backward compatible with the older, non-L version.

User avatar
Posts: 111
Joined: 17 Feb 2018, 01:09
Location: Sydney Australia

Re: Frequency hopping experiments on the NRF24

Post by _AL_ » 03 May 2020, 02:26

This is for the most part way over my head but a massively impressive project none the less..


User avatar
Posts: 371
Joined: 15 Feb 2018, 23:32

Re: Frequency hopping experiments on the NRF24

Post by Phil_G » 03 May 2020, 18:10

Eureka!!! (not the Swedish TV star Tobe) :D

Oh, I'm chuffed to bits, I'd almost given up on this but after hours and hours working through hoops & loops I believe I've found the problem - and its not the code! Some NRF24 library users have reported that the radio.available() function doesnt always clear as it should after data is read. Its purpose is to flag that a packet has been received and is ready to read, then you read it using which should clear the 'available' flag. As others have said, it didnt. Clearing it manually after every packet using radio.flush_rx() has fixed it. Obviously this cant be a problem with the NRF chip itself so I'm thinking it has to be the library again. The problem is masked in most sketches where is within a while (radio.available()) loop so it re-reads the same data into the same buffer for as long as it takes the radio.available() flag to eventually clear, which works but gives the function unpredictable timing.

It works! It does almost everything right, staying in sync even if a few packets are missed, then restarting from zero and resyncing if more are missed. 'Almost' in that there remains one funny - whilst everything is working, if you then power cycle the transmitter, the receiver doesnt relink, but that should be easily fixed. [Fixed!] I'm relieved to finally get a simple but real fhss scheme working but in view of Martin & Werners way, way superior code it is just a learning exercise for my own amusement :D

Phil_G wrote:
03 May 2020, 18:10
...there remains one funny - whilst everything is working, if you then power cycle the transmitter, the receiver doesnt relink, but that should be easily fixed.
Sorted, all behaving perfectly now, but lots of soak testing next... happy chappie ! :D
I've been twiddling sticks for over an hour with the NRF receiver surrounded by as many other transmitters as I could find, including two FASST, a few Frskys, two Oranges, and a Flysky, all on full power whilst the NRF transmitter is set to minimum RF power, and no glitches, stuttering or delays - perfect :D
The scope now shows packets every 5ms with an occasional one or two missing as you'd expect.
Range on the very lowest power setting seems to be about 30 - 40 yards, beyond that failsafe kicks in. Walking back it hooks up again at about the same distance. This is using the low-power integrated PCB antenna modules for both tx and rx, with a high power module I expect the range to be as good as any.

Thanks very much to Werner, whose description above is the basis for this FHSS project, I guessed 6ms as the "just over 5ms" value and that works fine, not sure if tweaking would improve anything. The only part I changed from your description is when several packets are lost and we restart from zero (ie the first of mychannels[]), rather than wait there for a good packet, it waits a while then moves on, just in case the first is being blocked by interference.

Honestly I'm chuffed to bits, a proper happy chappie :D
FHSS NRF24L01 transmitter
(16.2 KiB) Downloaded 6 times
FHSS NRF24L01 receiver with f/s
For RF-Nano and other 16mhz 5v chips
(5.79 KiB) Downloaded 5 times
FHSS NRF24L01 receiver with f/s
For 8mhz 3v3 Promini
(5.8 KiB) Downloaded 7 times

Post Reply