TL;DR - I can't figure out why I'm failing to replicate the Quadcopter's remote control with, say, Arduino and NRF24L01 after eavesdropping the SPI commands on the remote control.
So I got this cheap quadcopter, actually 3 different models (this, this and this, and the principle is the same with all three) All models use the BK2423 radio, which should be air-compatible with the NRF24l01 - which is what I have.
So since I have found (surprisingly) very little information on this matter online, I put some probes between the remote control's MPU and it's BK2423 and used Saleae Logic Analyzer to read the communication running on the SPI bus between the two. What I have find out is it has 3 main stages: Config, Search/Bind and Command, where in between there are some tweaks that the MCU performs, as I described below.
On the Config phase, some registers are being set, like the pipe number for Rx and the address, RF channel and more. Here is part of the config capture:
So the initial Tx (and Rx) address is 5 bytes: 0xCCCCCCCCCC. And the initial RF Channel is 0x2D. The default address length for the two chips is 5 bytes and since the remote control does not change it, the length should stay 5 bytes.
Then comes the search phase. In that phase, the remote sends basically the same data over and over, and only changes the RF channel. Here is the last search packet:
So after many transmissions with different RF channels, something weird happens. The remote hits the correct RF channel for the quadcopter, but I can't figure out how he understands that, since the Status register (on the MISO channel) stays the same on the last search packets. Here is where the remote control MCU tells the BK2423 to change the RF Channel, with no evidence to anything else around:
Then the MCU tells the BK2423 to change the address and RF channel to the right ones. It must be the right ones since the quadcopter reacts properly:
From this point and on, the remote control sends command packets and the quadcopter happily fly around. I couldn't manage to properly pass the search phase.
I used some code from this project (taken from this page) and changed HCD.cpp to match what I sniffed. Here are only the two methods I changed (rest of the code within that file stays the same):
void HCD::bind(unsigned char *ID)
{
for(unsigned char i=0;i<4;i++)
noteID[i]=ID[i];
state=HS_CONNECT;
radio.offline();
radio.txbuf[0] = 0x3F;
radio.txbuf[1] = 0x4C;
radio.txbuf[2] = 0x84;
radio.txbuf[3] = 0x6F;
radio.txbuf[4] = 0x9C;
radio.txbuf[5] = 0x20;
radio.sendBuffer(6);
radio.txbuf[0] = 0x3E;
radio.txbuf[1] = 0xC9;
radio.txbuf[2] = 0x9A;
radio.txbuf[3] = 0xB0;
radio.txbuf[4] = 0x61;
radio.txbuf[5] = 0xBB;
radio.txbuf[6] = 0xAB;
radio.txbuf[7] = 0x9C;
radio.sendBuffer(8);
radio.txbuf[0] = 0x39;
radio.txbuf[1] = 0x0B;
radio.txbuf[2] = 0xDF;
radio.txbuf[3] = 0xC4;
radio.txbuf[4] = 0xA7;
radio.txbuf[5] = 0x03;
radio.sendBuffer(6);
radio.txbuf[0] = 0x30;
radio.txbuf[1] = 0xCC;
radio.txbuf[2] = 0xCC;
radio.txbuf[3] = 0xCC;
radio.txbuf[4] = 0xCC;
radio.txbuf[5] = 0xCC;
radio.sendBuffer(6);
radio.txbuf[0] = 0x2A;
radio.txbuf[1] = 0xCC;
radio.txbuf[2] = 0xCC;
radio.txbuf[3] = 0xCC;
radio.txbuf[4] = 0xCC;
radio.txbuf[5] = 0xCC;
radio.sendBuffer(6);
radio.txbuf[0] = 0xE1;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
radio.txbuf[0] = 0xE2;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
radio.txbuf[0] = 0x27;
radio.txbuf[1] = 0x70;
radio.sendBuffer(2);
radio.txbuf[0] = 0x21;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
radio.txbuf[0] = 0x22;
radio.txbuf[1] = 0x01;
radio.sendBuffer(2);
radio.txbuf[0] = 0x23;
radio.txbuf[1] = 0x03;
radio.sendBuffer(2);
radio.txbuf[0] = 0x25;
radio.txbuf[1] = 0x2D;
radio.sendBuffer(2);
radio.txbuf[0] = 0x24;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
radio.txbuf[0] = 0x31;
radio.txbuf[1] = 0x09;
radio.sendBuffer(2);
radio.txbuf[0] = 0x26;
radio.txbuf[1] = 0x07;
radio.sendBuffer(2);
radio.txbuf[0] = 0x50;
radio.txbuf[1] = 0x73;
radio.sendBuffer(2);
radio.txbuf[0] = 0x3C;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
radio.txbuf[0] = 0x3D;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
radio.txbuf[0] = 0x31;
radio.txbuf[1] = 0x09;
radio.sendBuffer(2);
radio.txbuf[0] = 0x1D;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
}
void HCD::update(unsigned char *payload)
{
Serial.print("Sending, state: ");
Serial.print(state);
Serial.print(" Channel: ");
Serial.println(channel);
unsigned char c=0;
switch(state)
{
case HS_CONNECT:
radio.offline();
radio.txbuf[0] = 0x27;
radio.txbuf[1] = 0x70;
radio.sendBuffer(2);
radio.txbuf[0] = 0xE1;
radio.txbuf[1] = 0x00;
radio.sendBuffer(2);
radio.txbuf[0]=0x25;
radio.txbuf[1]= channel; //0x2A;
radio.sendBuffer(2); //-Chan
//radio.block(baseAddress);
//radio.block(flushTX);
radio.txbuf[0]=0xA0;
radio.txbuf[1]=0x20;
radio.txbuf[2]=0x14;
radio.txbuf[3]=0x03;
radio.txbuf[4]=0x25;
radio.txbuf[5]=0x1F; //noteID[0];
radio.txbuf[6]=0xC3; //noteID[1];
radio.txbuf[7]=0x00; //noteID[2];
radio.txbuf[8]=0xE2; //noteID[3];
radio.txbuf[8]=0xAA; //noteID[3];
radio.sendBuffer(9); //-tx data
//radio.block(PTX);
radio.online();
while(!c)
c=radio.getStatus();
radio.clrStatus();
Serial.print("Status: " );
Serial.println(c);
if(c&0x10) //MRT
{
state=HS_UNINITIALIZED;
Serial.println("Uninitializing :( Connecting");
}
if(c&0x20) //ACK
{
Serial.println("ACK Connecting");
state=HS_CONNECTED;
}
break;
case HS_CONNECTED:
radio.offline();
radio.txbuf[0]=0x25;
radio.txbuf[1]=channel;
radio.sendBuffer(2); //-Chan
radio.txbuf[0]=0x2a;
radio.txbuf[1]=noteID[0];
radio.txbuf[2]=noteID[1];
radio.txbuf[3]=noteID[2];
radio.txbuf[4]=noteID[3];
radio.txbuf[5]=0xCC;
radio.sendBuffer(6); //-tx data
radio.txbuf[0]=0x30;
radio.txbuf[1]=noteID[0];
radio.txbuf[2]=noteID[1];
radio.txbuf[3]=noteID[2];
radio.txbuf[4]=noteID[3];
radio.txbuf[5]=0xAA;
radio.sendBuffer(6); //-tx data
radio.online();
radio.block(flushTX);
radio.txbuf[0]=0xA0;
radio.txbuf[1]=payload[0];
radio.txbuf[2]=payload[1];
radio.txbuf[3]=payload[2];
radio.txbuf[4]=payload[3];
radio.txbuf[5]=payload[4];
radio.txbuf[6]=payload[5];
radio.txbuf[7]=payload[6];
radio.txbuf[8]=payload[7];
radio.sendBuffer(9); //-tx data
radio.txbuf[0]=0x20;
radio.txbuf[1]=0x7A; //-TX
radio.sendBuffer(2);
while(!c)
c=radio.getStatus();
radio.clrStatus();
if(c&0x10) //MRT
{
Serial.println("MRT - connected");
}
if(c&0x20) //ACK
{
Serial.println("ACK - connected");
}
break;
}
}
And I'm using a fairly simple Arduino sketch to invoke these methods (which is a modified version of the INO file form the project mentioned above):
#include <HCD.h>
HCD drone0;
unsigned char ID0[]={
0x1F, 0xC3, 0x00, 0xCC};
unsigned char payload0[]={ 0x0, 0x20, 0x14, 0x03, 0x25, 0x1F, 0xC3, 0x00 };
unsigned char payload1[]={
0x00,0x70,0x70,0x1E,0x1E,0x00,0x10,0x7E};
void setup()
{
Serial.begin(19200);
Serial.println("ON");
Serial.println("Connecting a");
drone0.bind(ID0);
drone0.setChannel(0x25);
}
unsigned long timer=0;
unsigned char chs[] = { 0x2A, 0x20, 0x34, 0x16, 0x3E, 0x0C, 0x48};
void loop()
{
for (int i=0; i<128; i++)
{
drone0.setChannel(i);
drone0.update(payload0);
delay(10);
}
delay(2000);
drone0.setChannel(0x2);
drone0.update(payload1);
delay(2000);
}
Yet the Quadcopter seems to gently ignore this code and many variations of it.
If anyone got to this line in my question, I would appreciate any help he/she can provide.