Help with or hints for decoding an IR protocol
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)



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 &amp; 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.