10
\$\begingroup\$

Some time ago I bought a simple and cheap little IR-controlled toy helicopter (same as this one - it's called "Diamond Gyro" or "Diamond Force"). For fun, I've been looking into controlling it via an Arduino.

Update: Got the protocol figured out; see answer

Others have already shared their results on hacking a different IR toy helicopter and decoding its IR protocol. Really cool, but unfortunately my helicopter uses a different protocol. One that I can't quite figure out. (I should add that electronics is purely a sometimes-hobby for me, so I may have overlooked something obvious).

Much like in the 2nd link above, I took the controller apart, located the IC pin controlling the LEDs (the IC's markings have been erased, by the way), and hooked up a logic analyzer.

Got lots of good data, but I still can't figure out the protocol. This site is a great resource, but none of the protocols listed seem to fit. And nothing else I've found seems to fit the signal I've captured either. I have to imagine, though, that it's a simple, off-the-shelf protocol, only because it's a cheap little toy.

So I'd appreciate any ideas you may have. Maybe I'm just looking at it wrong.
(More info below the image)

Samples from channel A

Signal/protocol characteristics

I captured this at 16MHz with the controller set to channel A; should be accurate, timing-wise. (There are 3 IR channels you can pick from, but using the two other channels don't change the characteristics, only parts of the packet itself.) The timings are very consistent (+/- 10µs max). Packets are repeated with varying intervals but at minimum they're about 100ms apart.

Carrier: 38kHz @ 50% duty-cycle

Lows:
- Short: 285µs
- Long: 795µs

Highs:
- Short: 275µs
- Long: 855µs

Always 17 highs per packet.

Controls/inputs

The heli's got 3 controls: "Throttle" (i.e. lift/rotor speed), pitch (forward/back), and yaw (rotation around the rotor axis) all controlled with 2 thumbsticks. They all have some kind of range (not just on/off) and are, as far as I can tell, all being transmitted in a single packet. The left/right inputs are only sent if something else is being sent, so I applied max throttle when sampling that. Throttle and pitch input on its own trigger packets being sent, as soon at you push the thumbsticks past some threshold/deadband (in the graph below the "min" label is for first packet sent when slowly pushing a control past its deadband).

It's also got buttons for trimming left & right, as the heli's not a precision instrument (at all) and tends to spin slowly otherwise. The left/right trim buttons unfortunately don't seem to send a signal that's incrementing/decrementing something for each press (which would be handy for figuring the protocol); it seems to just be single command, telling the helicopter to trim left/right, and then it keeps track of it.

\$\endgroup\$
  • \$\begingroup\$ Why not just use the signal traces you already have to write out the packets raw? \$\endgroup\$ – Ignacio Vazquez-Abrams Dec 22 '13 at 2:34
  • \$\begingroup\$ @IgnacioVazquez-Abrams You mean just replaying the recorded signals to the helicopter? \$\endgroup\$ – Flambino Dec 22 '13 at 2:41
  • \$\begingroup\$ Sure. It's not like the helicopter will be able to tell the difference... \$\endgroup\$ – Ignacio Vazquez-Abrams Dec 22 '13 at 2:45
  • \$\begingroup\$ @IgnacioVazquez-Abrams True but as far as I can tell, the packet contains all 3 controls (throttle/pitch/yaw) amd the heli's controls, are none are just on/off. To steer the thing by replaying, I'd have to capture every single configuration... Besides I want to understand the protocol \$\endgroup\$ – Flambino Dec 22 '13 at 2:48
  • \$\begingroup\$ @IgnacioVazquez-Abrams Oops, I somehow garbled my last comment. Meant to say: "... the packet contains all 3 controls (throttle/pitch/yaw) and none of them are just on/off". \$\endgroup\$ – Flambino Dec 22 '13 at 2:57
8
\$\begingroup\$

I'm taking the liberty of answering my own question as I got most of it figured out and this is a good way to share my findings. My thanks to Olin Lathrop for giving me a place to start and some ideas to try out, but ultimately, the protocol turned out quite different from Olin's guess, hence me posting this answer.


Update: I posted a follow-up question regarding the last 8 bits, which I didn't fully understand, and Dave Tweed figured it out. I'll include the details here, so this answer can work as full protocol spec, but do go check out Dave's answer.


I had to try some different things to get this figured out, but I'm pretty confident that I got it. Oddly, I haven't found anything resembling this protocol elsewhere, but it may very well be a common protocol that I just don't know about.

Anyway, here's what I've found:

Protocol/encoding

Both pulses and the spaces in between are used to encode the data. A long pulse/space is binary one (1), and a short pulse/space is binary zero (0). The pulses are sent using standard consumer infrared 38kHz modulation @ 50% duty-cycle.

The pulse/space timings are in the original question, but I'll repeat them here for completeness:

 Bit    Pulse     Space
-----+---------+---------
  0  |  275µs  |  285µs
  1  |  855µs  |  795µs

All ±10µs max., ±5µs typ.. This is based on samples captured with a logic analyzer at 16MHz; I don't have an oscilloscope, so I don't know the exact profile (i.e. rise/fall times).

Packets are repeated as long as the control inputs are applied and appear to be spaced a minimum of 100ms apart.

Packet transmission starts with a "pulse 1" preamble, which is fixed and not part of the data. The following space encodes the first data bit of packet, and the last pulse encodes the last bit.

Each packet is 32 bits long, and contains every input the remote control can provide. Values are read as little endian, i.e. MSB first.

Data structure

Below is the basic structure of the individual packets. The last 8 bits had me confused, but that's been figured out now (see below).

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
--+---------------------------+-----------+---+-------+-----------
 P|    Yaw    |   Throttle    |   Pitch   | T | Chan. |   Check

P: Preamble (always a pulse-1), T: Trim, Chan.: Channel

Bit    Length    Description (see note below)
-----------------------------------------------
0      1         Preamble. High 1
1-6    6         Yaw. Range 0-36 for left-right, 17 being neutral
7-14   8         Throttle. Range 0-134
15-20  6         Pitch. Range 0-38 for forward-back, 17 being neutral
21-22  2         Trim. Left = 1, right = 2, no trim = 0
23-26  4         Channel. A = 5, B = 2, C = 8
27-32  6         Check bits

Note: Ranges are based on the highest readings I got. The protocol is capable of larger ranges - up to 255 for throttle, 63 for pitch/yaw - but cap out at about half that.
The pitch value appears to have a deadband from 14-21 (inclusive); only values of above or below actually makes the helicopter react. I don't know if it's the same for the yaw (hard to tell, as the helicopter is unstable anyway, and may just spin slightly on its own).

Here it is in graphical terms (compare with the graphic in the original question)

packet structure

The 6 check bits are calculated by XOR'ing all the preceding values. Each value is treated as 6 bits. This means that the 2 MSBs of the 8-bit throttle value are simply ignored. I.e.

check = yaw ^ (throttle & 0x3F) ^ pitch ^ trim ^ channel

Practical notes

The signal timings and modulation doesn't need to be super accurate. Even my Arduino's not-at-all-accurate timing works fine despite dodgy modulation and a bit of hit and miss on the pulse/space durations compared to the real remote control.

I believe - but haven't tested - that the helicopter will simply latch on to the channel of the first signal it finds. If it's left without a signal for too long (couple of seconds), it appears to go back to its "search" mode, until it acquires a signal again.

The helicopter will ignore pitch and yaw values if the throttle is zero.

The trim commands are sent only once per button-press on the remote control. Presumably the trim value simply increments/decrements a value in the helicopter's own controller; it's not something the remote control keeps track of. So any implementation of this should probably stick to that scheme, and only send the occasional trim left/right value, but otherwise default to a zero trim value in the packets.

I recommend having a kill switch that simply sets throttle to zero. This will cause the helicopter to drop out of the sky, but it will sustain less damage when it's not spinning its motors. So if you're about to crash or hit something, hit the kill switch to avoid stripping the gears or breaking the blades.

The original remote control's IR LEDs appear to have a >900nm wavelength, but I've got no problems using a ~850nm LED.

The helicopter's IR receiver is ok, but not super sensitive, so the brighter your IR source, the better. The remote control uses 3 LEDs in series, sitting on the 9V rail instead of the 5V rail used by the logic. Haven't checked their current draw very precisely, but I'd wager it's 50mA.

Sample data

Here are a bunch of packets, for anyone interested (yes, I scripted a decoder; I didn't hand-decode all this). The channel A packets come from the same captures as the graphs in the original question.

Channel A                                                       
Yaw     Throttle  Pitch   Tr  Chan  Check     Description
-----------------------------------------------------------
000100  10000100  000000  00  0101  000101    Left Mid + Throttle
000000  10000110  010001  00  0101  010010    Left Max + Throttle 
100001  10000110  000000  00  0101  100010    Right Mid + Throttle 
100100  10000100  010001  00  0101  110100    Right Max + Throttle
010001  00000000  001011  00  0101  011111    Forward Min 
010001  00000000  000000  00  0101  010100    Forward Max 
010001  00000000  011000  00  0101  001100    Back Min 
010001  00000000  100101  00  0101  110001    Back Max
010001  00000000  010001  01  0101  010101    Left Trim 
010001  00000000  010001  10  0101  100101    Right Trim 
010001  00000011  010001  00  0101  000110    Throttle 01 (min)
010001  00010110  010001  00  0101  010011    Throttle 02
010001  00011111  010001  00  0101  011010    Throttle 03
010001  00101111  010001  00  0101  101010    Throttle 04
010001  00111110  010001  00  0101  111011    Throttle 05
010001  01010101  010001  00  0101  010000    Throttle 06
010001  01011111  010001  00  0101  011010    Throttle 07
010001  01101100  010001  00  0101  101001    Throttle 08
010001  01111010  010001  00  0101  111111    Throttle 09
010001  10000101  010001  00  0101  000000    Throttle 10 (max)

Channel B
Yaw     Throttle  Pitch   Tr  Chan  Check     Description
-----------------------------------------------------------
000000  10000110  010001  00  0010  010101    Left Max + Throttle 
100100  10000110  010001  00  0010  110001    Right Max + Throttle 
010001  00000000  001001  00  0010  011010    Forward Min 
010001  00000000  000000  00  0010  010011    Forward Max 
010001  00000000  010111  00  0010  000100    Back Min 
010001  00000000  100110  00  0010  110101    Back Max
010001  00000000  010001  01  0010  010010    Left Trim 
010001  00000000  010001  10  0010  100010    Right Trim 
010001  00000001  010001  00  0010  000011    Throttle Min 
010001  00110100  010001  00  0010  110110    Throttle Mid 
010001  01100111  010001  00  0010  100101    Throttle High 
010001  10001111  010001  00  0010  001101    Throttle Max 

Channel C
Yaw     Throttle  Pitch   Tr  Chan  Check     Description
-----------------------------------------------------------
000000  10000101  010001  00  1000  011100    Left Max + Throttle 
100100  10000101  010001  00  1000  111000    Right Max + Throttle 
010001  00000000  001010  00  1000  010011    Forward Min 
010001  00000000  000000  00  1000  011001    Forward Max 
010001  00000000  010111  00  1000  001110    Back Min 
010001  00000000  100110  00  1000  111111    Back Max
010001  00000000  010001  01  1000  011000    Left Trim 
010001  00000000  010001  10  1000  101000    Right Trim 
010001  00000001  010001  00  1000  001001    Throttle Min 
010001  00110100  010001  00  1000  111100    Throttle Mid 
010001  01100110  010001  00  1000  101110    Throttle High 
010001  10000101  010001  00  1000  001101    Throttle Max

As mentioned above, the last 8 bits have been figured out, but just for posterity, here are my original thoughts. Feel free to ignore it completely, as I was pretty much wrong in my guesses.

The last 8 bits

The last 8 bits of the packet are still a bit of mystery.

The 4 bits from bit 23 to 26 all appear to be entirely determined by the remote control's channel setting. Changing the channel on the remote control doesn't alter the protocol or modulation in any way; it only changes those 4 bits.

But 4 bits is double what's actually needed to encode the channel setting; there are only three channels, so 2 bits is plenty. Hence, in the structure description above, I've only labelled the first 2 bits as "Channel", and left the other two labelled as "X", but this is a guess.

Below is a sample of the relevant bits for each channel setting.

Chan.   Bits 23-26
-----+-------------
  A  |  0  1  0  1
  B  |  0  0  1  0
  C  |  1  0  0  0

Basically, there are 2 bits more than there needs to be to transmit the channel setting. Maybe the protocol has 4 bits set aside to allow for more channels later, or so the protocol can be used in entirely different toys, but I simply don't know. For the larger values, the protocol does use extra bits that could be left out (yaw/throttle/pitch could get by with a bit less each), but for trim - which also has 3 states - only 2 bits are used. So one could suspect that the channel is also just 2 bits, but that leaves the next 2 unaccounted for.

The other possibility is that the packet's checksum is 8 bits long, beginning with the "X bits", and - through the checksumming magic - they just happen to somehow always reflect the channel setting. But again: I don't know.

And speaking of: I have no idea how those check bits are formed. I mean, they are check bits, since they don't correspond to any single control input, and the helicopter doesn't seem to respond if I fiddle with them. I'm guessing it's a CRC of some kind, but I haven't been able to figure it out. The check is 6-8 bits long, depending on how you interpret the "X bits", so there are a lot of ways that could be put together.

\$\endgroup\$
6
\$\begingroup\$

This doesn't look so bad. First notice that all of the messages contain exactly 17 pulses. This immediately gives us a strong clue that short spaces within a message are irrelevant. It seems data is encoded by pulses being either short or long, and that some range of spacing between these pulses is acceptable.

Obviously, every message starts with a long pulse as a start bit. That leaves 16 data bits. Probably a few of the early bits are a opcode, possibly variable length. If I was doing this, a few of the ending bits would be a checksum. Figure the engineers that wrote the firmware wanted to keep things simple for themselves, so you can start by assuming there are 8 data bits in there somewhere. Now see if any of the messages make sense.

Let's call a long a 1 and a short a 0. It could be the other way around, but we have to start somewhere. Stripping off the start bit leaves:

1010001101011010  min throttle
1010011101011000  max throttle
1010000001011111  min forwward
1010000000011110  max forward
1010000011011101  max back
1010000100011010  min back
0000010101011100  max left + max throttle
0100010101011110  max right + max throttle
1010000101111111  trim left
1010000101011011  trim right

A few things pop out right away. Obviously bit 0 is a parity bit. Otherwise there appears to be a 3 bits field <15:13>, a 8 bit data value <12:5>, and another 4 bit field <4:1>.

It looks like the data value is being sent in low to high bit order, so it probably makes more sense to interpret the whole 16 bits flipped from what I show.

I don't feel like spending more time on this, but hopefully this has given you a start. I would proceed by re-writing the list above with the parity bit stripped off, the whole number flipped LSB to MSB, and each assumed field shown separately with a space between it and the abutting field. That may allow more to pop out at you. Also keep in mind that we may have the 1/0 sense of each bit backwards. Perhaps write out the new table each way and see if something makes more sense one way.

\$\endgroup\$
  • \$\begingroup\$ Thank you, this is excellent! I'll get right on it, and see what I find. After looking at other protocols, I did start to think that maybe the spaces were irrelevant, but since they had two very consistent timings, I wasn't so sure. I figured they'd vary more if they weren't important. Anyway, I'll try it out. Thanks again \$\endgroup\$ – Flambino Dec 22 '13 at 17:32
  • \$\begingroup\$ Huh... as far as I can tell, the spaces do matter. I focussed on the throttle, and captured some more samples at 10 different throttle positions. Excluding spaces gave me no meaningful numbers regardless of how I did the conversions. But including them as long=1, short=0 yields a smooth progression of the throttle value from 1 to 134 (little endian). Still working on the other parameters \$\endgroup\$ – Flambino Dec 23 '13 at 0:09
  • \$\begingroup\$ I've got the protocol almost entirely figured out, but there's a still a bit of mystery. Added a ton of stuff to my question, if you want to take a swing at it. Eitherway, thank you for you help so far! Got me working in the right direction. \$\endgroup\$ – Flambino Dec 23 '13 at 14:56
  • \$\begingroup\$ @Flambino: Looks like you're well ahead of what I did to start at, which turned out to be mostly wrong guesses in hindsight. I read your updated question but still don't understand how exactly the length of the spaces is used. Was it just conincidence that all the patterns you showed had exactly 17 pulses and that the last happened to indicate parity if only the pulses are taken to be 0 or 1? \$\endgroup\$ – Olin Lathrop Dec 23 '13 at 16:05
  • \$\begingroup\$ Honestly, it was mostly trial and error on my part. Since the 2 timings used for spaces are just as exact as the pulse timings, I figured they might be meaningful. And, when ignoring the spaces didn't yield useful binary data, I just assumed long pulse = 1 and long space = 1 (and short space/pulse = 0), which immediately gave me very useful data. So the first space after the preamble pulse is the first bit (the max right + max throttle graph shows a "space 1" as the first bit) followed by 16 pulses, with 15 more spaces in between; 32 bits. \$\endgroup\$ – Flambino Dec 23 '13 at 16:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.