Tuesday, April 16, 2013

RF24 Performance Improvement w/SPI

The Up Side

The RF24 (nrf24l01, nrf24l01p, nrf24l01+) radios use the SPI protocol to communicate with its master device. The speed at which the devices communicate is controlled by the master's clock speed. Here, the Arduino is the master device. Accordingly, we can control the Arduino's SPI bus speed. On the Arduino, this clock speed is set by means of a divisor based on the speed on the CPU. For a standard Arduino, the CPU runs at 16Mhz.

To reduce the chance of communication errors, a compromise was made in the RF24 driver to not run at maximum bus speed. By default of the driver, the divisor value is SPI_CLOCK_DIV4. This divides the 16Mhz clock by 4. Which drives the SPI bus at (16/4) 4Mhz.

To further improve performance, it is possible to use a divisor of 2 (SPI_CLOCK_DIV2), allowing for an 8Mhz SPI bus speed. That's twice the performance of the default setting. This translates into half the latency for SPI bus communication. If you're pinched for maximum performance, this may well be a change worthy of consideration.

To make this change, look at RF24.cpp. Find the line containing "SPI_CLOCK_DIV4". Change that value to one of the other supported constants to adjust the SPI bus speed accordingly. Just remember, its a divisor, so the larger the number, the slower the SPI bus speed. Inversely the lower the number, the higher SPI bus speed.

The Down Side

There are down sides to running at faster speeds. And this is why a lower value was picked for a default. At faster speeds noise becomes much more noticeable and communication between devices becomes more easily corrupted.  Corrupted communications means packet loss, an increase in communication errors, and/or corrupted packets at the receiving end of things. This is important because, should a packet become corrupted during SPI transfer, it will be delivered in its corrupted form at the receiving radio. In spite of corruption, it will still pass a CRC check because it was received exactly as was transmitted - corrupted. That's not to say these things will happen. But they can happen. So caution is advised.

For breadboard use, when jumper leads are long and noise is likely, I strongly encourage you to maintain a slower SPI bus speed of 4Mhz. But, once you're ready for production use, where you have everything connected properly with decoupling capacitors and short leads, a clock divisor of 2 (SPI_CLOCK_DIV2), providing for an (16/2) 8Mhz SPI bus speed, may well give your performance a tiny, extra boost.


As happens with many Arduino projects, you may find you've simplified things by converting to a simple AVR project. Many users who do this deviate from the 16Mhz clock speed to something slower or faster, as dictated for the given project. As such, if you find you've increased your clock speed to 20Mhz or even decreased to 1Mhz, don't forget you can tweak these settings as needed. Always remember, SPI bus speed is determined by the speed of your CPU. If you change the speed of your CPU, you might re-evaluate your SPI bus divisor.

History & Summary

When the original SPI bus speed was selected in Maniacbug's code, I did observe errors when running at 8Mhz on a breadboard. Infrequent yet present. This is why I picked a 4Mhz speed as a default value. That, however, doesn't mean you can't try faster.

Be mindful of your decision should you decide to run at a faster SPI bus speed. Use leads as short as possible. Use decoupling capacitors as close to the radio as possible. If you're unsure, leave it where its at. Worst case, you now know you have some options to squeak every last bit of performance out of your project.


  1. And as people move on to products like the Due, they deal with the extra clock speed and the "Extended SPI" calls in the IDE. Argh.

  2. That's a good point. As far the RF24 library is concerned, not front and center. Regardless, I'm hoping this article will get people to think about durations spent in various calls which communicate to the outside world. For example, a simple Serial.print adds latency. And lower baud rates contribute to higher latencies. That's why one of the first things I do is, Serial.begin(115200). I don't have time for 9600, or even 57600. I honestly can't think of a good reason to ever use those lower baud rates on modern computers...especially since the interface is usually USB.

    Same for for SPI. No point in wasting time in SPI calls (directly or indirectly). Its time lost to your application. Its time which might complicate debugging...or even artificially impose latency bugs.

    If people walk away with anything, I hope they learn its okay to tweak libraries for their own needs...and stop blindly accepting defaults which may exist for full spectrum compatibility at the cost of performance.