SPI Interface to the FlySky/Turnigy 9x
by angryfeet in Circuits > Arduino
10554 Views, 14 Favorites, 0 Comments
SPI Interface to the FlySky/Turnigy 9x
Interfacing a RC radio to a microcontroller is a bit of a pain, especially if you want a lot of channels, because you have to time each channel's output individually.
An AVR only has one 16 bit timer with two compare channels, so either you can only use two channels at full resolution or you have to waste lots of cycles sampling it.
The other hassle is having loads of wires running from the rx to the micro.
The receiver on the FlySky 9x uses SPI to send data from the radio chip to the chip that actually generates the timings on each output channel.
If we tap off the SPI lines we can listen directly to the serial data the radio is spewing out... Luckily its super easy to understand!
An AVR only has one 16 bit timer with two compare channels, so either you can only use two channels at full resolution or you have to waste lots of cycles sampling it.
The other hassle is having loads of wires running from the rx to the micro.
The receiver on the FlySky 9x uses SPI to send data from the radio chip to the chip that actually generates the timings on each output channel.
If we tap off the SPI lines we can listen directly to the serial data the radio is spewing out... Luckily its super easy to understand!
Cables
Break off 4 pins of SIL 0.1" header. Plug in some jumper leads to get the sizing, then solder on some ribbon cable. About 15cm will do the trick.
I'd recommend 80 way ATA hard drive cable (the thin stuff, not like this)
I'd recommend 80 way ATA hard drive cable (the thin stuff, not like this)
Cutting
Cut out a hole in the side of the box, and glue the header down so the pins are flush with the outside of the case
Power
Solder the power leads to the conveniently provided pads. The other side isn't quite so easy...
The edge one is +ve, the middle is -ve.
The edge one is +ve, the middle is -ve.
Data
Get some very thin wire (speaker wire or 80 way HDD cable)
We're soldering onto the MOSI and SCK pins shown in the picture.
We're soldering onto the MOSI and SCK pins shown in the picture.
Connecting
Now we just connect the header to the chip through some 330 ohm resistors for safety.
Close It Up
That's it!
But Wait... There's More!
The radio sends a data frame of 27 bytes over about 0.7ms every 1.7ms. (ie a 1ms gap between frames)
The frame starts with 8 bytes, I don't know what they are for.
- The first three numbers change when you switch off the radio, maybe they are related to power levels?
- Bytes 4-8 seem to stay constant, so I assume they are an ID?
The next 16 bytes are the channel data, simply 8 big endian words. (bits 0-7 in the first byte, 8-15 in the second byte)
The last byte changes all the time, but stays a multiple of 5. I'm guessing its channel hopping info, or maybe a checksum (but then why always a multiple of 5?)
The channels are encoded as microsecond values, from 1000 to 2000
Here's a dump of the output:
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 252, 5, 247, 3, 254, 5, 253, 5, 252, 5, 253, 5, 253, 5, 175, 15, 115,
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 252, 5, 247, 3, 254, 5, 252, 5, 252, 5, 253, 5, 253, 5, 175, 15, 55,
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 251, 5, 247, 3, 254, 5, 253, 5, 252, 5, 253, 5, 253, 5, 175, 15, 155,
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 250, 5, 247, 3, 253, 5, 252, 5, 253, 5, 253, 5, 252, 5, 175, 15, 15,
The frame starts with 8 bytes, I don't know what they are for.
- The first three numbers change when you switch off the radio, maybe they are related to power levels?
- Bytes 4-8 seem to stay constant, so I assume they are an ID?
The next 16 bytes are the channel data, simply 8 big endian words. (bits 0-7 in the first byte, 8-15 in the second byte)
The last byte changes all the time, but stays a multiple of 5. I'm guessing its channel hopping info, or maybe a checksum (but then why always a multiple of 5?)
The channels are encoded as microsecond values, from 1000 to 2000
Here's a dump of the output:
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 252, 5, 247, 3, 254, 5, 253, 5, 252, 5, 253, 5, 253, 5, 175, 15, 115,
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 252, 5, 247, 3, 254, 5, 252, 5, 252, 5, 253, 5, 253, 5, 175, 15, 55,
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 251, 5, 247, 3, 254, 5, 253, 5, 252, 5, 253, 5, 253, 5, 175, 15, 155,
64, 24, 69, 85, 73, 7, 0, 0, 253, 5, 250, 5, 247, 3, 253, 5, 252, 5, 253, 5, 253, 5, 252, 5, 175, 15, 15,
Obligatory Arduino
Here's how I implemented it in Arduino:
Manually set the SPI to slave mode
Enable timer2 on its slowest prescaler
Set timer2 compare A to about 500us
When a byte is received, reset TCNT2
The timer2 compare interrupt should fire halfway through the delay period between frames. I check if there are enough bytes, copy received bytes into channel values, then reset the SPI peripheral to make sure the bytes stay synced and aligned.
If timer2 overflows then the transmitter is off
Manually set the SPI to slave mode
Enable timer2 on its slowest prescaler
Set timer2 compare A to about 500us
When a byte is received, reset TCNT2
The timer2 compare interrupt should fire halfway through the delay period between frames. I check if there are enough bytes, copy received bytes into channel values, then reset the SPI peripheral to make sure the bytes stay synced and aligned.
If timer2 overflows then the transmitter is off
Some Sketchy Arduino Code
//This code comes with all the usual disclaimers...
#include <stdio.h>
#include "pins_arduino.h"
static FILE uart = {0};
#define RADIO_MAX_BYTES 27
#define RADIO_CHANNELS 9
#define RADIO_IDLE 0xFF
byte radio_bytes[RADIO_MAX_BYTES];
byte radio_len;
byte radio_ok;
unsigned short radio_channels[RADIO_CHANNELS];
byte debug_bytes[RADIO_MAX_BYTES];
byte debug_len;
// the setup routine runs once when you press reset:
void setup() {
// initialize the digital pin as an output.
//pinMode(led, OUTPUT);
debug_len = 0;
radio_ok = 0;
debug_len = 0;
noInterrupts();
Serial.begin(9600);
TCCR2A = 0; //no outputs from T2
TCCR2B = 5; // div1024, 64uS, 16ms timeout
OCR2A = 8; // SPI gets reset 512uS after last byte received
TIMSK2 |= _BV(OCIE2A);
TIMSK2 |= _BV(TOIE2);
TCCR1A = 0;
TCCR1B = 5; //Standard timer, Ck=/1024 = 64uS resolution, 4s timeout
TCCR1C = 0;
pinMode(MISO, INPUT);
pinMode(MOSI, INPUT);
pinMode(SCK , INPUT);
pinMode(SS, INPUT);
//Enable SPI slave mode
SPCR |= _BV(SPE);
//Enable spi interrupts
SPCR |= _BV(SPIE);
interrupts();
}
//SPI Data interrupt
ISR (SPI_STC_vect)
{
byte c = SPDR;
if (radio_len < RADIO_MAX_BYTES)
{
radio_bytes[radio_len] = c;
radio_len = radio_len + 1;
}
//reset TIMER2 so that it doesnt timeout and reset the SPI
TCNT2 = 0;
}
//Approx 0.5ms after last SPI byte received
ISR (TIMER2_COMPA_vect)
{
SPCR &= ~_BV(SPE);
if (radio_len == RADIO_MAX_BYTES)
{
for (byte b=0; b<RADIO_CHANNELS; b++)
{
radio_channels[b] = (radio_bytes[(b<<1)+8+1] << 8) + radio_bytes[(b<<1)+8];
}
//Copy out SPI frame to variables...
radio_ok = 1;
}
debug_len = radio_len;
for (byte b=0; b<radio_len; b++)
{
debug_bytes[b] = radio_bytes[b];
}
radio_len = 0;
SPCR |= _BV(SPE);
}
//Approx 16ms after last SPI byte received
ISR (TIMER2_OVF_vect)
{
//Radio is offline!
radio_ok = 0;
}
// the loop routine runs over and over again forever:
void loop()
{
byte c = 0;
int n = 0;
delay(200);
if (radio_ok == 1)
{
for (n=0; n<8; n++)
{
//Serial.print(debug_bytes[n], DEC);
Serial.print(radio_channels[n], DEC);
Serial.print(", ");
}
}
else
{
Serial.print("NO DATA");
}
Serial.println("");
}
#include <stdio.h>
#include "pins_arduino.h"
static FILE uart = {0};
#define RADIO_MAX_BYTES 27
#define RADIO_CHANNELS 9
#define RADIO_IDLE 0xFF
byte radio_bytes[RADIO_MAX_BYTES];
byte radio_len;
byte radio_ok;
unsigned short radio_channels[RADIO_CHANNELS];
byte debug_bytes[RADIO_MAX_BYTES];
byte debug_len;
// the setup routine runs once when you press reset:
void setup() {
// initialize the digital pin as an output.
//pinMode(led, OUTPUT);
debug_len = 0;
radio_ok = 0;
debug_len = 0;
noInterrupts();
Serial.begin(9600);
TCCR2A = 0; //no outputs from T2
TCCR2B = 5; // div1024, 64uS, 16ms timeout
OCR2A = 8; // SPI gets reset 512uS after last byte received
TIMSK2 |= _BV(OCIE2A);
TIMSK2 |= _BV(TOIE2);
TCCR1A = 0;
TCCR1B = 5; //Standard timer, Ck=/1024 = 64uS resolution, 4s timeout
TCCR1C = 0;
pinMode(MISO, INPUT);
pinMode(MOSI, INPUT);
pinMode(SCK , INPUT);
pinMode(SS, INPUT);
//Enable SPI slave mode
SPCR |= _BV(SPE);
//Enable spi interrupts
SPCR |= _BV(SPIE);
interrupts();
}
//SPI Data interrupt
ISR (SPI_STC_vect)
{
byte c = SPDR;
if (radio_len < RADIO_MAX_BYTES)
{
radio_bytes[radio_len] = c;
radio_len = radio_len + 1;
}
//reset TIMER2 so that it doesnt timeout and reset the SPI
TCNT2 = 0;
}
//Approx 0.5ms after last SPI byte received
ISR (TIMER2_COMPA_vect)
{
SPCR &= ~_BV(SPE);
if (radio_len == RADIO_MAX_BYTES)
{
for (byte b=0; b<RADIO_CHANNELS; b++)
{
radio_channels[b] = (radio_bytes[(b<<1)+8+1] << 8) + radio_bytes[(b<<1)+8];
}
//Copy out SPI frame to variables...
radio_ok = 1;
}
debug_len = radio_len;
for (byte b=0; b<radio_len; b++)
{
debug_bytes[b] = radio_bytes[b];
}
radio_len = 0;
SPCR |= _BV(SPE);
}
//Approx 16ms after last SPI byte received
ISR (TIMER2_OVF_vect)
{
//Radio is offline!
radio_ok = 0;
}
// the loop routine runs over and over again forever:
void loop()
{
byte c = 0;
int n = 0;
delay(200);
if (radio_ok == 1)
{
for (n=0; n<8; n++)
{
//Serial.print(debug_bytes[n], DEC);
Serial.print(radio_channels[n], DEC);
Serial.print(", ");
}
}
else
{
Serial.print("NO DATA");
}
Serial.println("");
}