DIY FlySky receivers

Part #3 of the "Roger Writes" series - June 2022


As a youngster I used to race radio controlled cars (10th scale electric indoor), many years later I got involved in Steampunk, and soon after found out about Teapot racing.

While many of the cheap kits are OK, they have been cost engineered to a point where things become a compromise. I've done a few low cost kits based on the SE8 (a cheap Chinese clone of the Nordic nRF24L01+) with a PIC MCU to decode and drive motor controllers.

For better racers and other devices (like the Teasmade and Little-luggage) I've used the FlySky system, this is a pretty decent, low-cost (around £50 for the Tx and about £10 per receiver). The iBus output from some of the receivers is easy to interface to MCUs (including Arduino and Raspberry-Pi), so is easy to interface to motor controllers and servos - See later for information about iBus frames.

AFHDS - Automatic Frequency Hopping Data System

The original version is uni-directional, the Tx sends the packets, and hopefully the Rx gets them. The newer version AFHDS-2A is bi-directional, the Tx sends packets, the Rx processes them, and occasionally returns status and information (battery voltages, propeller speeds, temperatures, etc.). Some FlySky transmitters only support AFHDS, some both AFHDS-2A, some support both. There is also a newer AFHDS-3 which is again completely different.
ModeA7105 optionsPayload BytesPayload startchannel data
AFHDS1F=0x0F-> CRC enabled, 4 byte ID, 4 byte preamble
20=0x1C-> Preamble detection=0 bits
215 Bytes
Type + 4xID
16 Bytes
8 channels
AFHDS-2A1F=0x1E-> FEC enabled, CRC enabled, 4 byte ID, 2 byte preamble
20=0x1E-> Preamble detection=8 bits
379 Bytes
Type + 4xTx-ID + 4xRx-ID
28 Bytes
14 channels
AFHDS-3??15+9 Bytes
Type + 4xTx-ID + 4xRx-ID
6+ Bytes
3+ channels
Note: During the bind, AFHDS-3 agrees the number of channels required, this sets the number of bytes per packet, and this determines the frame rate.

Packets format compared

Both AFHDS-1 and AFHDS-2A use 160 channels of 500kHz, and an over air rate of 500kbps, a 16-bit payload CRC, and the same 32-bit packet ID (0x54 0x75 0xC5 0x2A) (this is used for all Tx and Rx packets as a packet ID, and is always the same, the Tx-ID and for AFHDS-2A the Rx-ID, are after the type byte in the payload data)
AFHDS-2A uses a shorter preamble (2-byte instead of 4) and has FEC for the payload and CRC, so sends those nibbles as 7-bits (for 4-data), this should give much better packet protection (and range), but will mean that the longer packets and FEC add to the over-air time
PreamblePacket IDPayloadCRCTotalOver air timePacket send rate
32-bits32-bits20x8=160-bits16-bits240-bits480µSecevery 1544µSec
16-bits32-bits37x2x7=518-bits4x7=28-bits594-bits1188µSecevery 3850µSec


There has been some analysis of the packets for AFHDS and AFHDS-2A (not all of it correct!). Most of this has been for the Tx side of things (see Deviation forum), while there has been some Rx work (mainly Thierry Pébayle's PIC code and Thierry Pébayle's ARM code) but this is limited to AFHDS, not AFHDS-2A.

The A7105 (or the slightly upgraded A7106) is used in the FlySky devices, for both AFHDS and AFHDS-2A.

It's easy to get A7105 modules, (the XL7105 includes a Tx amp). They use a well documented 3-wire SPI interface (with MISO and MOSI muxed onto a single I/O pin). So starting with some AFHDS code, a cheap A7105 module, and a PIC, I fixed a few bugs and was decoding packets.

But the existing AFHDS-2A bind sequence documentation, is missing a number of steps, so while a FlySky Receiver can bind with the Open-source Tx, a real FlySky Tx won't bind with something that only impliments whose features! So creating a FlySky compatible AFHDS-2A receiver is going to need more information!

My analysis

Luckily the FlySky FS-i6 Tx PCB has the important A7105 signals taken out to test pads, so it's easy to connect wires to them (at least the SEN/SCLK/SIO/GPIO1/GPIO2 ones). With this it's simple to capture an actual Bind sequence. With the signals all connected, I captured the SPI sequence for a FlySky FS-i6 booting up in Bind mode, after a short while, I power up a FlySky FS-iA10B Rx in bind mode, so this capture includes the initial register settings, the bind search, and then the bind sequence when the Receiver is found.

An important thing to notice, is that the Tx uses overlapped send/receive operations to increase the processing time between sends. Waiting for the over air send and receive would take well over half of the frame time, so wouldn't leave much time for other things. Instead, the following sequence is repeated at 3.85ms intervals:

  1. A7105 is put into listen mode (but doesn't wait here)
  2. The next packet to send is prepared (reading the ADCs if the stick position data is required)
  3. Wait for the remainder of the frame time (gives a total listen time of around 2.1ms per 3.85ms frame)
  4. WTR state recorded (this indicates if a packet was received)
  5. Data to send is written to the FIFO
  6. A7105 starts sending over air (but doesn't wait here)
  7. While it's sending, if the WTR from step #4 indicated a packet was received, then the data is read from the FIFO
  8. If data was sucessfully read then this is processed
  9. Waits for the over air data to complete sending (takes about 1.5ms from the start of sending)

From this we can see that any data received during the listen in step #1, isn't read from the A7105's FIFO until step #7, and the data sent in step #5/#6 is calculated in step #2. So the data sent is based on the previously receive frame. This explains the weird "1 packet delay" during the bind sequence, where we see that although the Tx has received the Rx-ID, it doesn't include it until one packet later.

During each phase, the Tx switches between the sending channels with a 3.85ms period, after sending, it listens one channel down for the remainder of the period for a response.
The Rx switches between the channels at half the rate (so 2x3.85ms=7.7ms), until it syncronises with the Tx, then keeps stepping at 3.85ms even if a packet is missed, so that stays in sync with the Tx even if there is interference or other causes of missed packets

The bind sequence

The bind sequence happens in two phases:
During phase #1, the channel sequence just toggles between the two bind channels (0x0D and 0x8C), with the Tx listening on 0x0C and 0x8B for a response.

  1. The Tx sends a 0xBB packet (sequence=0x00), then a pair of 0xBC packets (with sequence 0x00 then 0x01), on alternating Bind channels, listening for a response
    (These packets contain the Tx ID, and the 16-channel frequency table)
  2. Soon after the Tx starts sending, the Rx should receive a packet from the Tx
  3. The Rx records the channel list, and responds with a 0xBC packet with sequence 0x01 including its Rx-ID (and 0xFF for most of the payload)
  4. The Tx sends the next packet in sequence (because it's not decoded the receiver's packet yet)
  5. The Rx responds to #4 with another 0xBC packet with sequence 0x01 including its Rx-ID (and 0xFF for most of the payload)
  6. The Tx responds to #3 with a 0xBC packet with sequence 0x02 and includes the Rx-ID (and 0xFF instead of the channel list)
  7. The Rx responds to #6 with a 0xBC packet with sequence 0x02 that includes its Rx-ID (and 0xFF for most of the payload)
  8. The Tx responds to #5 with another 0xBC packet with sequence 0x02 including the Rx-ID (it's not seen the response by the time it prepared this)
  9. The Rx responds to #8 with another 0xBC packet with sequence 0x02 that includes its Rx-ID (and 0xFF for most of the payload)
  10. The Tx finally decodes the first 0x02 sequence number from #7, so knows that the Rx has seen it's response, so moves onto phase #2
At this point both systems have switched to phase #2, and the channel list sent by the Tx, during the initial phase #1 of the bind is used (listening one channel down).
  1. The Tx sends 0xBC packets that include the Tx-ID and Rx-ID
  2. For about 500ms, the Tx ignores the responses, (probably so it can send multiple times on all channels, to warn any other Rx devices that the Tx has been re-bound)
  3. At some point the Rx and Tx end up on the same channel, the Rx receives a 0xBC packet from #1 and responds with an 0xAA packet which the Tx receives
  4. The Tx sends another 0xBC packet (as it's not decoded the 0xAA from #3 yet)
  5. The Rx doesn't know if the Tx has seen it yet, so sends another 0xAA packet
  6. After the Tx sees an 0xAA packet (from #3) it responds with an 0xAA packet, and drops into the normal packet sequence
  7. The Rx responds to #6 with a final 0xAA packet, and drops into the normal packet sequence
At this point the Tx/Rx have fully bound, and have both entered normal packet mode!

The normal packet sequence

The 16 data channels sent in the intial packets of phase #1 of the bind are used (listening for responses one channel down).
Every 3.85ms, the Tx sends a 37 byte 0x58 packet, and then listens for a response.
0x5832-bit Tx-ID32-bit Rx-ID14 blocks of 16-bits stick position (in µSec, Least significant byte first)

Every 15 packets the Rx responds with a 37 byte 0xAA packet containing status information
0xAA32-bit Tx-ID32-bit Rx-IDUpto 7 sensor data blocks, of {sensor-Type}{Sensor-Num}{16-bit sensor val, Least significant byte first}
If there are more than 7 sensors, then multiple 0xAA packets are sent from the Rx, as sequental responses, with the sensor blocks repeated every 15-packets, so 15x7=105 sensor maximum

Receiver outputs

Most receivers have seperate servo PWM outputs per channel (with a +ve pulse of 1000...2000 µSec repeated at about 20ms). This has been the industry standard for at least 50 years. While this was fine for a few channels, with 6+ channels you end up with a lot of wiring, so there have abeen a few alternative schemes.


This is essentially the inverted version of the old AM over-air signal, 400µSec lows, with the low+following high period being the PWM servo signal. Usually blocks of 8 servo positions (so 9 low pulses) repeated at 20ms intervals.
Here is an example PPM packet:


This is just the over air AFHDS-2A data with a header and checksum. It's sent as TTL level 115000 Baud serial, 1 start,8 data,2 stop, as 32-byte packets every 7.7ms (so every other received AFHDS-2A frame).
It starts with a length byte (0x20), then a type byte (0x40), then 14 channels of stick position as 16-bit µSec values, Least significant byte first, then 2 bytes of checksum.
The checksum is only for the data payload, and is inverted (so 16-bit payload sum + 16-bit CSUM value=0xFFFF).
Here is an example iBus packet:

When I've finished my receiver I'll open source the code, but for now, I hope this helps....

This page was lasted updated on Sunday, 03-Jul-2022 09:45:03 BST

This content comes from a hidden element on this page.

The inline option preserves bound JavaScript events and changes, and it puts the content back where it came from when it is closed.

Click me, it will be preserved!

If you try to open a new Colorbox while it is already open, it will update itself with the new content.

Updating Content Example:
Click here to load new content