I have managed to implement frequency hopping on the nrf24l01 for 2 way communication. I could not find very much information on this online (other than a Hackaday article using a serial connection to sync the timings - which is impractible). One of the benefits of these radios chips is there fast channel switching time (130us). I've put this here in case any one else decides to go down this same rabbit hole.
Short Version
- Make sure the frame times on both Microcontroller is as close to the same time as possible
- Make sure the main (Master) Microcontroller controlling the frequency hopping sends its packets at the same time every frame and advance consistently forward through the channel hopping scheme
- Use the interrupt pin on the other (Slave) Microcontroller to record a time stamp when a packet is received
- Sync the Slaves own frame clock start to this interrupt pin time stamp
- Sync the Slaves counts before the next hop with the Master
- Have the slave MC scan backwards through the frequency list to find the correct frequency
Long Version
There are 125 channels with each channel jumping in 1mhz of bandwidth. This gives a frequency range of 2.4 to 2.525ghz. The RF24 library simply calls these channels 0 to 124. Some countries do have laws regarding the maximum output power on the 2.4ghz frequency, with channel hopping allowing a higher output power. The 2.4ghz spektrum is pretty crowded, so doing constant channel hopping rather than a fixed channel will greatly improve communication.
In my implementation i do NOT use Auto-acks. With auto acks the receiving Nrf will send an acknowledgement if it has received a packet. If the sender does not receive an acknowledgement after a specified time it will resend for a set number of retries. I find auto acks create unnecessary delays. I have implemented my own acknowledgement system.
What we are trying to achieve is for each nrf24l01 to change there channel many times a second and to stay in sync. I'll refer to of the NRF as the Master and the other one as the slave. We need to have both Master and slaves run with the same frame times.
I am using 2 x Teensy 4.1. For the frame time on the Master i am using a hardware timer set to 120hz (8.3333ms).
For the frame time on the Slave i created a software timer using the Teensys clock cycle counter. The Teensy has a 32 bit clock register where you can count the current clock cycle. 120hz is 5,000,000 clock cycles for 1 frame at 600mhz cpu frequency. This counter overflows every 17 seconds. Its a cheap look up without requiring interrupts. The timer records the clock stamp of when the last frame started (Not the finish time for that frame) At the start of each frame we go into a while loop until the timer has triggered. We check the timer by getting the time difference from the current time, checked against the time from the start of the last loop. If this is more than our target time, we start the frame and we increase our last frame stamp by our target time.
This makes it easier to calculate for overflow as well as easier to sync the clock later on. Whilst we can lose some clock cycles on Polling its the best way i could implement it and have a variable timer. We need to set this frametime to be as close as possible to the Masters frame time. I don't have an oscilloscope but i managed to get the difference between Master and Slave +- 50 clock cycles by counting clock cycles.
Both Master and Slave need to keep an array of the same randomly spaced channels. I've gone for 60 channels. They also both need to keep there own counter on how many frames before they advance to the next channel. I advance every 5 frames (approx 41ms). We will call this channelHopCounter (Increments from 0 to 5)
Master
The Master always changes its channel on a set pattern. At the start of each frame it increments it channelHopCounter. If it reaches 5, it resets to 0 and advances to the next channel. It then sends its packet(s). It's important this packet is sent right at the start of every frame for timing. As is common, the first byte of each packet from Master to Slave is used as a packet Identifier. The first 5 bits in this byte can be used for that. The last 3 bits we are going to set to channelHopCounter. The current count before the Master is about to advance a channel.
At the end of every frame before the Master hits idle time it will read any packets in the NRF buffer. We then have to read(or use an interrupt if hardware timer) to set the next frame as ready to start. We have to wait at the start of our loop until this happens.
Slave
The Slave will be the one chasing the channel. We are trying to do 3 things here
Find the current channel the master is on.
Synchronise our hop counter.
Synchronise our frame to start at the same time as the Master
The Slave requires the IRQ pin on the NRF. We need to set the pin to trigger when data is received in the NRF buffer. We have an interrupt to trigger when this is falling. In our interrupt we are going to record the current local Clock Stamp on when this pin triggered. We know this current stamp will be always just slightly after the Master has started its frame.
To make things easier i have a variable which records the current state of the Slave. SCANNING or CONNECTED
We start in SCANNING mode. When scanning we need to make sure we are NOT sending any packets. When the NRF is sending it will not receive any packets. If we had both the Master and slave switched on close to the same time, there could be a conflict where they are both sending at the same time.
SCANNING
If we are in SCANNING mode we are still trying to find the correct channel. every 2 frames we are going to go backwards through our channelList and switch to that channel until we receive a packet. (With the Master advancing forward 1 channel every 5 frames - this doesn't take long) We keep our main loop still running as normal, the only difference is in scanning mode we do not send any packets. We know we have received a new packet when our interrupt goes high, or there is data available. If we have received a packet we will change to CONNECTED mode. We have now synched our channel with the Master.
CONNECTED
At the start of our new frame if we are in CONNECTED MODE we first increase our local channelHopCount. If it reaches 5 we will advance to the next channel.
The next thing we do in our new frame is receive the packet. If we have found a packet we have found the correct channel. We can now change to CONNECTED mode. The last 3 bits of the first byte were the channelHopCount the Master was on (How many frames before its next hop) We can synchronise our local copy of channelHopCount to equal the Masters. We have now synced our channel hop count (And will continue to sync it every frame - although it shouldn't require it)
Towards the end of the frame before we hit idle time the Slave will send its packets. This will ensure good timing between the Master and Slave. The Master is sending a packet at the start of the frame and receiving before its finished while the Slave is receiving just after the start of the frame and sending towards the end.
The next loop, when the timer is up we are ready to start our next frame. Rather than the last frame time to just be incremented by our set frame time (500,000,000 clock cycles) we can simply set our last frame time to be the clock stamp we have set in the interrupt when our packet was received. However we only want to set this if our channelHopCount is not 0. This is because if the Master channel hop count is 0, it had to do a channel switch before sending, which is about a 170us delay.
This is where with our software timer, having the timer check if the difference between our current timer and the start of the last frame is more than our target frame time is handy. We can simply adjust our last frame time to be the time the packet come in without having to try figure out differences.
We have now synched our starting frame time, our hop counter, as well as which channel we are on in our channel sequence.
However. If the Master was to turn off then back on, we need our Slave to go back into scanning mode to resynch the Slave to the Master. We can keep a current count of how many frames in a row we have missed a packet on our Slave. If this reaches a predetermined amount we will go back into scanning mode. Making sure scanning mode will not send any packets from the slave until we have resynched.