Reverse Engineering a Quadcopter RC, or: How to not miss the needle while throwing the haystack in the air (Part 2)

Part one of this series covered the first three steps towards reverse engineering a quadcopter RC. The steps were “check the documentation“, “listen to the radio” to get the right channel and “listen to that channel intensively” in promiscuous mode. The second part is covering steps 4 and 5, showing the interpretation of the received data and how to start eavesdropping on the SPI interface.

Step 4: Interpretation of the received data – Now that I know how it’s supposed to work, I finally understand the manual!

Collecting data is one thing, “reading” it is a totally different one. I have to admit that this step was not too successful at first and the information below evolved only with the subsequent steps. Nevertheless, here is what I got:

There was two phases to look at:

  • The pairing process
  • Normal operation

The pairing process as per manual goes like this:

  1. Turn on the RC
  2. Connect the quadcopter to the battery
  3. Wait for the quadcopter to stop blinking
  4. Move the throttle to max and back to min again for the RC to stop blinking

The RC would beep twice and the quadcopter should be paired and ready for departure.

So I started listening to the traffic on channel 60, turned on the RC and received the following messages:

Messages received from the RC

Messages received from the RC

Each of the 8 messages shown in the chart above is being sent for about 2 seconds at a rate of about 800 messages per second. This is a little strange, since the “retransmission” setting for both, the nRF24 and it’s cousins has a maximum of 15 retries with a minimum delay of 250us in between. So either my receiver software is not working correctly or there must be some other mistake creating the impression of receiving the very same message (same package number) about 1.600 times before the RC is switching to the next message.

In the image above, you can see the received raw bytes in the upper part and the deriving values in the lower part. In order to get to these values, you have to keep the 9(!) bit packet control in mind, which is located between the address and the actual payload (ref. nRF24L01 product specification v2.0 p.25) . The packet control part of the message is “stealing” the MSB of the first data byte, which has to be shifted 1 bit to the left and then steal the MSB of the next byte and so on. The mechanism should become a little clearer in the image below.

How to get to the actual values sent by the RC

How to get to the actual values sent by the RC

In order to make things more reproducible, I connected the RC and the quadcopter to an external power supply. I then turned them on one after another via simple transistors circuits controlled by the Arduino. It showed me that the pairing would only be successful during the transmissions of the 101 address messages, and even then only during half of them. For the other half, the RC would still indicate a successful pairing, but the quadcopter would not start flying.

Initially the results have not been as clear as shown above and a few factors had to be kept in mind:

  • There was a lot of noise between the messages
  • The trigger for any received message was again noise, thus
  • Many of the actual message will have been lost half way through the reception
  • The actual messages could have been hidden anywhere in the 32 bytes of payload
  • The serial output is quite counter productive when it comes to time critical operations, thus
  • The received data needs to be buffered well before making it available

So I had to move on to the next step:

Step 5: Eavesdropping on the SPI communication – Second haystack

What I thought might be the most difficult part was in fact the most simple one: Connecting an Arduino to the SPI port of the RC MCU and listen to the commands it is sending to the transceiver. I was lacking an oscilloscope to check the pinout, so I was just hoping that it would be the same as in the very similar looking transceiver Dzl was using in his post.

But before soldering any wires into the RC, I wanted to try the procedure on my nRF24 module, just to see if it is really working and also to reduce the unknowns in the equation if it wasn’t. The setup was quite simple:

  • The transceiver was controlled by the Arduino Uno, which did some very simple settings (turn on the transceiver, set the channel, set the data rate, set the auto acknowledge)
  • Each setting was followed by a 1 second pause
  • The Arduino Mega was connected just the same, except of the MISO, to prevent any interference with the messages coming from the transceiver as the actual slave.

When searching for similar projects, I found that there was quite some confusion about how to connect to the SPI. Some people would suggest crossing MOSI and MISO, confusing them with the TX / RX of a serial connection. In fact, the direction of communication is determined by the CSN line: Who ever pulls this line low is the master and thus should be sending on MOSI, while the slave should be replying on MISO at the same time, bit by bit. You still would receive data when connecting the MOSI of the spying board to the MISO of the other one, but in that case you would be getting the feedback of the slave instead of the commands sent out by the master.

The most comprehensive description I could find on the SPI interface was put together by Nick Gammon. He not only describes the interface itself down to the level of zeroes and ones using a logic analyzer, but also has a lot of examples for the Arduino. In fact, I used his Master / Slave example to analyze the issue I had with the LSB in the setup shown below.

Arduino Mega eavesdropping on the SPI of an Arduino Uno talking to a nRF24L01+

Connection scheme

For the Uno I used a very simple sketch, calling begin(), setChannel(), setDataRate() and setAutoAck() in the setup, each call followed by a 1000ms delay and then just idling in the main loop.

#include <RF24.h>

RF24 myRF24(9, 10);

void setup() {




} // void setup() {

void loop() {
    asm volatile("nop");
} // void loop() {

The code for the R24Slave can also be found on my github repository.

As hinted above, I experienced some issues with the messages I received from the SPI interface during the first tries. While the master / slave example Nick posted in his Forum was working fine, the data coming from the RF24 transmissions was just not as expected. It took a few hours to find the reason: The main difference between both setups (in terms of how the SPI library was being used) was the transmission speed. Nevertheless, reducing the speed in the RF24 library did not work either. I then found out that the setting for the speed did not have any effect, since the definition, that was necessary for it, was not effective. This was due to the fact, that it was set before including the SPI library, which contained an important prerequisite for the definition. Long story short:

In RF24_config.h, move lines 28..30 below line 102 to look like this (I just commented the lines 28-30 out and copied them in between the #endif in line 102 and the #ifdef in line 104):

  #define _SPI SPI

// Lines 28 - 30 moved here, since it was not working above
// RF24_SPI_TRANSACTIONS is only defined AFTER including spi.h!
    #if defined SPI_HAS_TRANSACTION && !defined SPI_UART && !defined SOFTSPI
        #define RF24_SPI_TRANSACTIONS

    #define IF_SERIAL_DEBUG(x) ({x;})

Change RF24_SPI_SPEED in line 64 (or wherever it might end up after moving the lines mentioned above) to 5000000

// RF modules support 10 Mhz SPI bus speed
//  const uint32_t RF24_SPI_SPEED = 10000000;
  const uint32_t RF24_SPI_SPEED = 5000000;

The code for the RF24SPISpy running on the Arduino Mega can be found on my github repository.

If you let the SPISpy run on the Arduino Mega and start the RF24Slave on the Aruino Uno, the result using printBytes() should look like this:

32 12
36 95
6 255
38 39
6 255
6 255
6 255
38 7
6 255
80 115
61 0
60 0
39 112
37 76
0 255
32 14
0 255
32 14
37 60
6 255
38 7
6 255
33 0

Which is a great success!

More on the interpretation of the data in part 3 of this series.

Pitfalls of listening to SPI – Second Haystack

As mentioned before, the serial output is interfering quite a bit with collecting data and I received only scrambled messages or even lost them at the beginning. To roughly estimate the size of the second haystack: From starting up the RC (with the quadcopter already waiting) until the first pairing, the RC MCU sends about 3.100 commands to the transceiver, using about 6.400 bytes within about 1 second. The haystack might not be as big as the first one, and 51,2 kb/s might not be that fast (according to its product specification, the nRF24L01 could go as fast as 8 Mb/s on the SPI), but collecting the straws, sorting and making them visible at the same time caused a bit of scratching my head. The following two techniques helped in the end:

  • Ring of buffers (two dimensional ring buffer)
  • Interrupts

The data collection itself is being triggered by the SPI interrupt, filling the active buffer. A rising CSN signal is then triggering an interrupt on PIN2, indicating the end of a transmission, which would then result in switching to the next buffer in the ring. In the main loop, the Arduino is looping through the ring, waiting for the “dirty” flag to be set on each buffer. The content would then be sent out on the serial interface, but this time without interfering with the data collection, since the latter is being handled with a higher priority due to the interrupts. To keep the serial output cycles short, an array is keeping track of the actual number of bytes used in a single buffer, sending out only as many bytes as necessary.

The magic lies in balancing the size of the buffers and keeping the action during interrupt handling as short and simple as possible. The SPI messages would usually contain only a one byte command and one byte of data, in some rare cases 8 bytes of data (e.g. when setting the RX / TX addresses) or even 32 bytes (e.g. when transmitting a 32 byte message), so the size of the buffer can be adjusted accordingly. During the interrupt handling, the tasks are simply “store the received byte” and “advance to the next buffer index” for the SPI interrupt and “set buffer dirty flag” and “advance to the next buffer in the ring” for the external interrupt on PIN2.

It took a few iterations to get there, but in the end the SPISpy seems to be working fairly well (as long as the speed is low enough). During my research and trials, I came accross quite a few articles discussing “interrupt vs. polling” when using SPI. I tried both, but they did not make much of a difference, neither on the speed issue I had with the RF24 library nor on the scrambled or lost messages. On the other hand, I am also using the overhead that comes with the Arduino language (uhm, wait… there is no Arduino “language”!), so I guess the difference would only be visible when going lower level on programming the AVR. Either way, I did not need to buy a bus pirate or logic analyzer(, yet,) to make it work(, but they are already pretty high up on my list).


About Michael Melchior

Husband, Father, computer scientist, commercial helicopter pilot. Full of ideas and open minded, I try to see challenges in what others call problems.
This entry was posted in Projects, QC-360-A1 and tagged , , , , , . Bookmark the permalink.

2 Responses to Reverse Engineering a Quadcopter RC, or: How to not miss the needle while throwing the haystack in the air (Part 2)

  1. Pingback: Reverse Engineering a Quadcopter RC, or: How to not miss the needle while throwing the haystack in the air (Part 3) | Michael Melchior

  2. Pingback: Reverse Engineering Quadcopter Protocols | Hackaday

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s