Reverse Engineering a Tactic RC radio
I'm working on reverse engineering the Tactic TTX300, a simple RC radio meant for RC cars. This radio comes with RTR Axial RC cars and can be purchased individually on Amazon for $50.
I've been looking for a good way to control RC equipment from a PC. Most RC equipment uses all-in-one chips, which are hard to source, use and program. Luckily the Tactic radios use an Atmel microcontroller and a Nordic nRF24L01+, both of which are easy and cheap to source from online retailers. Having the RF separated from the digital controls means I can reverse engineer the radio without needing a 2.4 GHz spectrum analyzer or SDR.
After some poking around I was able to figure out which pins are the digital control signals between the MCU and the Nordic chip (SPI). A little soldering later, I had a logic analyzer listening in on RF messages.

I wrote a quick Python parser to analyze the data. There are three main elements: startup register initialization, first TX setup, and a periodic read/write.
Startup
On power-up the MCU writes the whole radio configuration in one burst. Checked against the nRF24L01+ datasheet, the values come out as:
| Register | Value | Meaning |
|---|---|---|
EN_AA | 0x00 | Auto-acknowledge off |
SETUP_RETR | 0x00 | No retransmits |
SETUP_AW | 0x02 | 4-byte addresses |
RF_SETUP | 0x24 | 250 kbps, -6 dBm |
CONFIG | 0x5E, then 0x3F | 2-byte CRC; powered up as a transmitter, then switched to receive |
RF_CH | 0x58 | Channel 88 |
RX_PW_P0 | 0x04 | 4-byte payload on pipe 0 |
RX_ADDR_P0 | C3 C3 AA 55 | Pipe 0 address, written and then read back |
Each command is two or more lines in the capture: the register being written, then its value.
Full startup capture
2.682745560000000,SPI,MOSI: 0x21; MISO: 0x0E-WRITE_REGISTER-EN_AA
2.682755320000000,SPI,MOSI: 0x00; MISO: 0x00
2.682774200000000,SPI,MOSI: 0x22; MISO: 0x0E-WRITE_REGISTER-EN_RXADDR
2.682783960000000,SPI,MOSI: 0x01; MISO: 0x00
2.682802800000000,SPI,MOSI: 0x23; MISO: 0x0E-WRITE_REGISTER-SETUP_AW
2.682812560000000,SPI,MOSI: 0x02; MISO: 0x00
2.682831440000000,SPI,MOSI: 0x24; MISO: 0x0E-WRITE_REGISTER-SETUP_RETR
2.682841200000000,SPI,MOSI: 0x00; MISO: 0x00
2.682860080000000,SPI,MOSI: 0x26; MISO: 0x0E-WRITE_REGISTER-RF_SETUP
2.682869840000000,SPI,MOSI: 0x24; MISO: 0x00
2.682888720000000,SPI,MOSI: 0x20; MISO: 0x0E-WRITE_REGISTER-CONFIG
2.682898440000000,SPI,MOSI: 0x5E; MISO: 0x00
2.682917360000000,SPI,MOSI: 0x31; MISO: 0x0E-WRITE_REGISTER-RX_PW_P0
2.682927080000000,SPI,MOSI: 0x04; MISO: 0x00
2.682946480000000,SPI,MOSI: 0x27; MISO: 0x0E-WRITE_REGISTER-STATUS
2.682956240000000,SPI,MOSI: 0xF0; MISO: 0x00
2.682975120000000,SPI,MOSI: 0x32; MISO: 0x0E-WRITE_REGISTER-RX_PW_P1
2.682984880000000,SPI,MOSI: 0x00; MISO: 0x00
2.683003760000000,SPI,MOSI: 0x33; MISO: 0x0E-WRITE_REGISTER-RX_PW_P2
2.683013520000000,SPI,MOSI: 0x00; MISO: 0x00
2.683032400000000,SPI,MOSI: 0x34; MISO: 0x0E-WRITE_REGISTER-RX_PW_P3
2.683042160000000,SPI,MOSI: 0x00; MISO: 0x00
2.683061080000000,SPI,MOSI: 0x35; MISO: 0x0E-WRITE_REGISTER-RX_PW_P4
2.683070840000000,SPI,MOSI: 0x00; MISO: 0x00
2.683089760000000,SPI,MOSI: 0x36; MISO: 0x0E-WRITE_REGISTER-RX_PW_P5
2.683099520000000,SPI,MOSI: 0x00; MISO: 0x00
2.683118400000000,SPI,MOSI: 0x25; MISO: 0x0E-WRITE_REGISTER-RF_CH
2.683128160000000,SPI,MOSI: 0x58; MISO: 0x00
2.683147080000000,SPI,MOSI: 0x20; MISO: 0x0E-WRITE_REGISTER-CONFIG
2.683156840000000,SPI,MOSI: 0x3F; MISO: 0x00
2.683175720000000,SPI,MOSI: 0x31; MISO: 0x0E-WRITE_REGISTER-RX_PW_P0
2.683185480000000,SPI,MOSI: 0x04; MISO: 0x00
2.683200000000000,SPI,MOSI: 0x2A; MISO: 0x0E-WRITE_REGISTER-RX_ADDR_P0
2.683209880000000,SPI,MOSI: 0xC3; MISO: 0x00
2.683219800000000,SPI,MOSI: 0xC3; MISO: 0x00
2.683229680000000,SPI,MOSI: 0xAA; MISO: 0x00
2.683239560000000,SPI,MOSI: 0x55; MISO: 0x00
2.683253320000000,SPI,MOSI: 0x0A; MISO: 0x0E-READ_REGISTER-RX_ADDR_P0
2.683263200000000,SPI,MOSI: 0xFF; MISO: 0xC3
2.683273480000000,SPI,MOSI: 0xFF; MISO: 0xC3-NOP-
2.683283760000000,SPI,MOSI: 0xFF; MISO: 0xAA-NOP-
2.683294040000000,SPI,MOSI: 0xFF; MISO: 0x55-NOP-
2.683310320000000,SPI,MOSI: 0xE2; MISO: 0x0E-FLUSH_RX-
2.683328440000000,SPI,MOSI: 0x27; MISO: 0x0E-WRITE_REGISTER-STATUS
2.683338200000000,SPI,MOSI: 0xF0; MISO: 0x00Periodic write
Each periodic write repeats the configuration from startup, then sets the channel and sends the packet. Here is the end of one, on channel 11 (0x0B):
2.741367680000000,SPI,MOSI: 0x25; MISO: 0x2E-WRITE_REGISTER-RF_CH
2.741377720000000,SPI,MOSI: 0x0B; MISO: 0x00-READ_REGISTER-RX_ADDR_P1
2.741392200000000,SPI,MOSI: 0xE1; MISO: 0x2E-FLUSH_TX-
2.741410360000000,SPI,MOSI: 0x27; MISO: 0x2E-WRITE_REGISTER-STATUS
2.741420160000000,SPI,MOSI: 0xF0; MISO: 0x00
2.741435160000000,SPI,MOSI: 0xA0; MISO: 0x0E-WRITE_TX_PAYLOAD-
2.741445200000000,SPI,MOSI: 0xF9; MISO: 0x00
2.741455200000000,SPI,MOSI: 0xFD; MISO: 0x00
2.741465240000000,SPI,MOSI: 0x90; MISO: 0x00
2.741475240000000,SPI,MOSI: 0xFF; MISO: 0x00-NOP-
2.741485280000000,SPI,MOSI: 0x75; MISO: 0x00
2.743103120000000,SPI,MOSI: 0xE1; MISO: 0x2E-FLUSH_TX-
2.743121240000000,SPI,MOSI: 0x27; MISO: 0x2E-WRITE_REGISTER-STATUS
2.743131000000000,SPI,MOSI: 0xF0; MISO: 0x00
2.743146040000000,SPI,MOSI: 0xA0; MISO: 0x0E-WRITE_TX_PAYLOAD-
2.743156040000000,SPI,MOSI: 0xF9; MISO: 0x00
2.743166040000000,SPI,MOSI: 0xFD; MISO: 0x00
2.743176080000000,SPI,MOSI: 0x90; MISO: 0x00
2.743186080000000,SPI,MOSI: 0xFF; MISO: 0x00-NOP-
2.743196080000000,SPI,MOSI: 0x75; MISO: 0x00The five bytes after WRITE_TX_PAYLOAD are the packet, and it goes out twice, about 1.7 ms apart. My parser labels every byte as if it were a command, so data bytes sometimes pick up a bogus name: the 0x0B is the channel number, not a register read, and the 0xFF is payload, not a NOP.
Full periodic write capture
2.741170640000000,SPI,MOSI: 0x21; MISO: 0x2E-WRITE_REGISTER-EN_AA
2.741180400000000,SPI,MOSI: 0x00; MISO: 0x00
2.741199280000000,SPI,MOSI: 0x22; MISO: 0x2E-WRITE_REGISTER-EN_RXADDR
2.741209080000000,SPI,MOSI: 0x01; MISO: 0x00
2.741227960000000,SPI,MOSI: 0x23; MISO: 0x2E-WRITE_REGISTER-SETUP_AW
2.741237720000000,SPI,MOSI: 0x02; MISO: 0x00
2.741256640000000,SPI,MOSI: 0x24; MISO: 0x2E-WRITE_REGISTER-SETUP_RETR
2.741266400000000,SPI,MOSI: 0x00; MISO: 0x00
2.741285320000000,SPI,MOSI: 0x26; MISO: 0x2E-WRITE_REGISTER-RF_SETUP
2.741295080000000,SPI,MOSI: 0x24; MISO: 0x00
2.741313960000000,SPI,MOSI: 0x20; MISO: 0x2E-WRITE_REGISTER-CONFIG
2.741323720000000,SPI,MOSI: 0x5E; MISO: 0x00
2.741342640000000,SPI,MOSI: 0x31; MISO: 0x2E-WRITE_REGISTER-RX_PW_P0
2.741352400000000,SPI,MOSI: 0x04; MISO: 0x00
2.741367680000000,SPI,MOSI: 0x25; MISO: 0x2E-WRITE_REGISTER-RF_CH
2.741377720000000,SPI,MOSI: 0x0B; MISO: 0x00-READ_REGISTER-RX_ADDR_P1
2.741392200000000,SPI,MOSI: 0xE1; MISO: 0x2E-FLUSH_TX-
2.741410360000000,SPI,MOSI: 0x27; MISO: 0x2E-WRITE_REGISTER-STATUS
2.741420160000000,SPI,MOSI: 0xF0; MISO: 0x00
2.741435160000000,SPI,MOSI: 0xA0; MISO: 0x0E-WRITE_TX_PAYLOAD-
2.741445200000000,SPI,MOSI: 0xF9; MISO: 0x00
2.741455200000000,SPI,MOSI: 0xFD; MISO: 0x00
2.741465240000000,SPI,MOSI: 0x90; MISO: 0x00
2.741475240000000,SPI,MOSI: 0xFF; MISO: 0x00-NOP-
2.741485280000000,SPI,MOSI: 0x75; MISO: 0x00
2.743103120000000,SPI,MOSI: 0xE1; MISO: 0x2E-FLUSH_TX-
2.743121240000000,SPI,MOSI: 0x27; MISO: 0x2E-WRITE_REGISTER-STATUS
2.743131000000000,SPI,MOSI: 0xF0; MISO: 0x00
2.743146040000000,SPI,MOSI: 0xA0; MISO: 0x0E-WRITE_TX_PAYLOAD-
2.743156040000000,SPI,MOSI: 0xF9; MISO: 0x00
2.743166040000000,SPI,MOSI: 0xFD; MISO: 0x00
2.743176080000000,SPI,MOSI: 0x90; MISO: 0x00
2.743186080000000,SPI,MOSI: 0xFF; MISO: 0x00-NOP-
2.743196080000000,SPI,MOSI: 0x75; MISO: 0x00Frequency hopping
Next up is to try and decipher the frequency hopping. Here are the frequency hops for my transmitter:
88 37 11 44 43 29 25 56 16 19 48 72 46 23 33
52 37 11 44 43 29 25 56 16 19 48 72 46 23 33
52 37 11 44 43 29 25 56 16 19 48 72 46 23 33
52 37 11 44 43 29 25 56 16 19 48 72 46 23 33
52 37 11 44 43 29 25 56 16 19 48 72 46 23 33
52 37 11 44 43 29 80 25 56 16 19 48 72 46 23
33 52 37 11 44 43 29 25 56 16 19 48 72 46 23
33 52 37 11 44 43 29 25 56 16 19 48 72 46 23
33 52 37 11 44 43 29 25 56 16 19 48 72 46 23
33 52 37 11 44 43 29 25 56 16 19 48 72 46 23
33 52 37 11 44 43 29 25 56 16 19 48 80 72 46
23 33 52 37 11 44 43 29 25 56 16 19 48 72 46
23 33 52 37 11 44 43 29 25 56 16 19 48 72 46
23 33 52 37 11 44 43 29 25 56 16 19 48 72 46
23 33 52 37 11 44 43 29 25 56 16 19 48 72 46These are decimal values from the register. The datasheet says the actual transmission frequency is 2400 + [RF_CH] MHz, so a decimal value of, say, 80 would transmit on 2480 MHz. The sequence looks like a repeating pattern of 15 frequencies with an "80" thrown in every 80 hops. Looking at the data for the "80" frequency:
frequency: 48
data: 0xf9,0xfd,0x90,0xff,0x75
data: 0xf9,0xfd,0x90,0xff,0x75
frequency: 80
data: 0x22,0x5a,0x83,0x47 (34,90,131,71)
frequency: 72
data: 0xf9,0xfd,0x90,0xff,0x75
data: 0xf9,0xfd,0x90,0xff,0x75
frequency: 46
data: 0xf9,0xfd,0x90,0xff,0x75
data: 0xf9,0xfd,0x90,0xff,0x75The data transmitted on 80 is always the same: 0x22, 0x5a, 0x83, 0x47. Looking at the register writes around the 80 transmission, the TX_ADDR is changed before and after the transmit. So I would guess that channel 80 is a general channel that all radios transmit on, perhaps for debugging or binding purposes. I should be able to use channel 80 as a heartbeat for figuring out the channels of an already bound transmitter. The goal here is to figure out the channels without soldering wires in and sniffing the SPI commands.
The RX_ADDR is 0x7E, 0xB8, 0x63, 0xA9. Listening on channel 80 with an address of 0xA9, 0x63, 0xB8, 0x7E resulted in the following packet being received: 0xFD 0x21 0xFE 0xEA.
Where this stands
That's as far as I've taken it. The SPI capture gave me the radio's configuration, its 15-channel hop sequence, and a fixed transmission on channel 80 that I was able to listen in on. The packet I received there doesn't match the bytes from the capture, so that is the first thing to run down. After that, what's left is decoding the five payload bytes and following the hop sequence, which should be enough to control a receiver from a PC.