The Spektrum AR8000 RX is a long range RC aircraft receiver with eight channels and a “satellite” module that acts as a redundant receiver. At first, I thought the satellite was some kind of RF signal booster or redundant antenna, but it is, in fact, a full redundant receiver. The satellite can be disconnected from the main RX and used as an easy-to-use receiver in microcontroller projects -- Arduino, PIC, et al. -- essentially anything with a serial interface. This post will describe how to interface with it, using an ATMega32 project in Atmel Studio as an example.
I had set a goal to interface with a Spektrum receiver so that I could control my 6WD rover with a Spektrum transmitter. Initially, I had expected to be interfacing with the servo output. This would have been much harder as I would have to do some hardware multiplexing to share the channels with a limited number of external interrupt pins, and I’d have to determine the values with a fairly sensitive timing algorithm.
In this video I talk about integrating the Spektrum Satellite into my 6WD project, but it does not cover technical details about the protocol and electronics.
My good friend Richard van Slooten suggested it may be possible to tap into the receiver in such a way where the multiplexing was done for us. Alas, the underlying signal is serial PPM so it stands to reason that such a tap might exist (and in fact the radio protocol itself is rooted in this protocol for historical reasons as he explains). We then found that someone had been able to just read serial directly from a satellite from an OrangeRX receiver, so I set out to find out if this was possible on the Spektrum. I hooked it up to an oscilloscope and saw right away that it was a serial signal. It turns out, the idea was basically the same with the OrangeRX, but there were differences in the protocol and electronics. There are equal write-ups about the OrangeRX if you want a cheaper route.
First, before you can use the satellite as a standalone receiver, you must first bind it to the transmitter using the standard binding procedure with the satellite connected to the main RX. It’s pretty clear to me that this could also be done while standalone, but I did not pursue this strategy as it is easy enough to swap the satellite over to bind, and you only have to do it once until, for whatever reason, you lose the binding.
There are three leads to the satellite: orange for +3V, black for ground, and grey for signal. Power-wise, it tolerates up to some voltage, but not 5V. The OrangeRX had been reported to handle up to 5V, so I started there. However, after some use, the satellite failed, so I ended up putting a 68 ohm resistor in series. This has worked fine for a couple of months and I have concluded this is a safe way to power it if your supply is 5V.
The grey signal wire is connected to the USART RX pin on the ATMega32 through a 100k ohm resistor. The resistor may or not be necessary, but there’s no reason not to be on the safe side. The serial baud rate is fixed at 115,200, meaning you will definitely need an external crystal to bump up the clock frequency from the internal 1Mhz clock. Mine runs at 16Mhz, but I believe it will also work with 8Mhz -- meaning it may be possible to implement this with an Atmel microcontroller that caps out at 8Mhz. If you’re on an Arduino, don’t worry about this as the Arduino board does this for you already.
Baud: 115200 Parity: None Data Bits: 8 Stop Bits: 1 bit Hardware Flow Control: None
Other than the baud rate, this configuration is generally default for Arduino and terminal applications.
The fun part was reverse engineering the protocol. There’s what I call a header, for synchronizing the signal, a two-byte error counter, and then a series of two-byte integers representing each channel. The integers are therefore 16 bit, and I figured out through trial and error that they are unsigned. The annoying part is that they do not range cleanly between fixed values; each channel’s lower and upper bounds are all different, so I had to manually inspect these values while playing with the controller. So, even if you are a smarty pants and already figured that you can do all this, I’ll save you some time by providing those values.
The header starts with 0x3B and is then followed by nine 0xFFs. The full header is:
3BFF FFFF FFFF FFFF FFFF
Immediately following the header is an unsigned 16 bit integer error counter. This will start at 0x0000 and presumably max out at 0xFFFF. An “error” is some kind of miss on the data that I don’t know the details of, but you can get a count like this if you interface with the RX through the USB interface (and maybe with Spektrum’s telemetry packages?). Obviously, longer radio ranges and interference make this error rate increase.
Next, is a list of the following channels. Each subsequent two byte chunk represents a channel as an unsigned 16 bit integer. The sequence of channels is always the same, and the first one is ailerons, which I call channel number 1. The integer ranges are all different, so I’m listing those values. Some channels are inherently centering, like ailerons and elevator; the throttle channel does not center; and some channels are on switches -- in either two or three positions switches. Therefore, I formatted the ranges in such a way that they make sense for what the channel does. Some of the values in the sequence didn’t make much sense to me, either always being 0xFFFFs or what looked like counters. The 0xFFFFs are probably placeholders for compatibility in the protocol with radios that have more channels.
1: aileron Left: 3750 Center: 3057 Right: 2391 2: flaps pos 0: 11946 pos 1: 11264 pos 2: 10582 3: elevator Up (stick): 5801 Center: 5075 Down: 4439 4: rudder Left: 7849 Center: 7196 Right: 6487 5: Aux 2 (heli throttle) Bottom: 13994 Top: 12630 6: always 0xFF 7: always 0xFF 8: counter of some kind! Always the same as error counter? 9: gear Up (stick): 41302 Down: 42666 10: throttle Down: 342 Up: 1706
Do you see why this is annoying? You have to map these ranges to make them portable. What I ended up doing was mapping centering channels from (float) -1.0 to 1.0, non-centering channels from 0.0 to 1.0, and so on. However, you only have to solve this problem once, and provide the ranges in a data structure. In fact, you only have to solve it zero times, because here’s the C code:
There are two basic versions: Non-centering:
float map_range(float input, float in_low, float in_high, float out_low, float out_high){ return out_low + (((input - in_low) / (in_high - in_low)) * (out_high - out_low)); }
And Centering:
float map_range_with_center(float input, float in_low, float in_high, float out_low, float out_high, float in_center){ float out_center = out_low + (out_high - out_low) / 2; if (input > in_center) return map_range(input, in_center, in_high, out_center, out_high); else return map_range(input, in_low, in_center, out_low, out_center); }
If these are not self-explanatory, input is the channel value, in_low is the smaller integer in the mapping table, in_high is the larger integer in the table, and out_low and out_high are the floating point lower and upper bound you want to convert it to. I defined my mapping table as a multi-dimensional array with type, sequence number, and ranges, and simply fed that to those functions to get my nice clean -1.0, 0.0, 1.0 ranges.
The parsing algorithm is a little state machine. If the value is 0x3B, I then switch to looking for 0xFFs, and once those exhaust, I switch to collecting values. The operation to collect values is blocking in my example, and will block for somewhere around 10ms, depending on where in the sequence you jump in. This was fine for my application because it runs at a fixed 20ms interval and spent most of that time waiting. You could also write this to run in parallel by triggering on the USART interrupt, but I determined this would interfere with another highly time-sensitive operation elsewhere in this particular application.
It’s worth noting why the ranges are all different. They don’t overlap, which means an alternative way of parsing the data would be to look at the value and work backwards from the range to figure out the channel. However, you still have to worry about single byte offsets, so you still have to do a synchronization step. I didn’t see that it would save me much work, but there are obviously many ways to parse.
So, there you have it. I’m curious if anyone else has done this and if they chose to parse a different way. I’m also interested to see if this post was any help to anyone and if they were able to do it. This is such an ideal way to add Spektrum remote control to anything with serial. The satellite is teeny tiny, weighs almost nothing, and uses very little power. It’s perfect for many applications.
Here's the Spektrum library in its entirety: spectrum.c