DIY FlySky receivers

Part #3 of the "Roger Writes" series - June 2022 (Added ANT info June 2023)


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 AFHDS (aka AFHDS-1) is uni-directional, the Tx sends the packets, and hopefully the Rx gets them. The newer 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 only AFHDS-2A, some support both. There is also a newer AFHDS-3 which is completely different (uses a different RF chip, and over-air protocol). I'll only discuss AFHDS and AFHDS-2A here.
ModeRF subsystemPayload BytesPayload startchannel data
AFHDSA7105 with:
Reg 1F=0x0F-> CRC enabled, 4 byte ID, 4 byte preamble
Reg 20=0x1C-> Preamble detection=0 bits
215 Bytes
Type + 4xID
16 Bytes
8 channels
AFHDS-2AA7105 with:
Reg 1F=0x1E-> FEC enabled, CRC enabled, 4 byte ID, 2 byte preamble
Reg 20=0x1E-> Preamble detection=8 bits
379 Bytes
Type + 4xTx-ID + 4xRx-ID
28 Bytes
14 channels
AFHDS-3SX1280 based6...1279 Bytes
Type + 4xTx-ID + 4xRx-ID
6+ Bytes
3+ channels
ANTAMICCOM A7157 based??????

Packets format compared

Both AFHDS-1 and AFHDS-2A split the 2.4GHz ISM band (2400MHz...2483MHz) into 160 channels of 500kHz, using an over air rate of 500kbps, with a 16-bit payload CRC, a 32-bit packet ID . Every Rx and Tx packet uses a packet header ID of 0x54 0x75 0xC5 0x2A, this is more like a protocol ID, rather than a device ID (The Tx and Rx IDs are transferred in the data stream).
Main AFHDS-2A changes:

ModePreamblePacket IDPayloadCRCTotalOver air timePacket send rate
AFHDS32-bits32-bits20bytes=160-bits16-bits240-bits480µSecevery 1544µSec/650Hz
AFHDS-2A16-bits32-bits37bytes with FEC=518-bits16-bits+FEC=28-bits594-bits1188µSecevery 3850µSec/260Hz


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 of ADHDS-2A

FlySky have made this easy, as the FS-i6 Tx PCB has the important A7105 signals taken out to labelled 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)

Because the data received during the listen in step #1 isn't read from the A7105's FIFO until step #7, the data sent in step #5/#6 is calculated in step #2. So the data sent is based on the older frame, not the last one received (as that is still in the Rx FIFO). This explains the weird "1 packet delay" during the bind sequence, where we see that although the Tx has been sent 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./p>

Bind Phase #1

The channel sequence just toggles between the two bind channels (0x0D and 0x8C), with the Tx listening one channel down.
It's a bit confusing because the Tx sends its packet before it has processed the one just received.
The sequence is:
#1aTxBB32-bit Tx-IDFF FF FF FF00 0016 channelsThe Tx repeats these three packets on alternating channels
#1bTxBC32-bit Tx-IDFF FF FF FF00 0016 channels
#1cTxBC32-bit Tx-IDFF FF FF FF01 0016 channels
#2RxBC32-bit Tx-ID32-bit Rx ID01 00FF...At some point the Rx sees one of these packets.
It records the Tx-ID and the channel list, then responds with a 0xBC packet that includes its Rx-ID
#3TxBB/BC32-bit Tx-IDFF FF FF FF00 00/0116 channelsThe Tx sends the next packet in its BB/BC sequence (because it's not decoded the packet from #2 yet)
#4RxBC32-bit Tx-ID32-bit Rx ID01 00FF...Repeats #2
#5TxBC32-bit Tx-ID32-bit Rx ID02 00FF...Responds to #2, includes the Rx-ID, with sequence 0x02 (and 0xFF instead of the channel list)
#6RxBC32-bit Tx-ID32-bit Rx ID02 00FF...Responds with an 02 packet
#7TxBC32-bit Tx-ID32-bit Rx ID02 00FF...Repeats #5, then decodes #6 and drops into phase #2
#8RxBC32-bit Tx-ID32-bit Rx ID02 00FF...Repeats #6, then drops into phase #2
At this point both systems are in bind phase #2.

Bind Phase #2

The channel sequence follows the 16-channel list sent by the Tx in the early stages of the bind, with the Tx listening one channel down.
For about 500ms the Tx doesn't listen for responses (maybe so other Rx devices get warning that the new bind has happened).
The sequence is:
#1TxBC32-bit Tx-ID32-bit Rx ID02 00FF...Repeated over the 16-channelsThese steps repeat for about 500ms, until the Tx starts to listen
#2RxAA32-bit Tx-ID32-bit Rx IDNN NNFF...The Rx responds
#3TxBC32-bit Tx-ID32-bit Rx ID02 00FF...Repeated over the 16-channels
#4RxAA32-bit Tx-ID32-bit Rx ID(NN NN)+1FF...The Rx reponds
#5TxAA32-bit Tx-ID32-bit Rx IDNN NNFF...Responds to the packet from #2
#6RxAA32-bit Tx-ID32-bit Rx ID00 00FF...The Rx sees this and reponds with sequence 00 00, then drops into normal packet mode
#7TxAA32-bit Tx-ID32-bit Rx ID(NN NN)+1FF...Repeats #5, then decodes #6 and drops into normal packet mode
At this point the Tx/Rx have fully bound, and have both entered normal packet mode!

Normal packet mode

The channel sequence follows the 16-channel list sent by the Tx in the early stages of the bind, with the Tx listening one channel down.
Every 3.85ms, the Tx sends a 37 byte 0x58 packet, and then listens for a response.
5832-bit Tx-ID32-bit Rx-ID14 blocks of 16-bits stick position in µSec
(Least significant byte first, midpoint=1500µSec)

Every 15 packets the Rx responds with a 37 byte 0xAA packet containing status information. Each block is enough for upto 7 sensors. If there are more than 7 sensors, then multiple 0xAA packets are sent from the Rx, as sequental responses. With upto 6 blocks, the 15 packet repeat rate is kept, with 42 sensors/6 blocks, there is a gap of 7 blocks, with 98 sensors/14 blocks there is gap of 1. Beyond that there is a gap of 2 blocks, possibly to avoid repeating on a 16-block pattern. This means that if some channels are blocked by interference, then each data will cycle over all of the channels, so will (eventually) get through.
AA32-bit Tx-ID32-bit Rx-IDUpto 7 sensor data blocks, of {sensor-Type}{Sensor-Num}{16-bit sensor val, Least significant byte first}


For the Bind, the Tx alternates sending between channels 0x0D and 0x8C (listening for the Rx responses one channel lower). During the Bind, the Tx sends a preset list of 16-channels, here are the channels lists sent by 4 different Flysky remotes
Channel list, send by Tx
72 69 14 57 76 51 34 87 1A 39 2B 22 47 82 91 7C
6B 22 79 34 5F 71 14 2B 1A 7D 43 82 93 57 3B 0B
46 11 70 29 42 1A 36 74 7B 69 57 63 2F 3E 24 8F
82 3D 88 5B 6F 96 78 53 2A 61 44 17 34 0F 4E 2F
From these, the lowest channel used is 0x0B (11=2405.5MHz), the higest is 0x96 (150=2475.0MHz).
Generally they are chosen to avoid the Bind channels (so 0x0D=13, 0x8C=172), avoid the bottom 8 channels, and top 8 channels, cover at least 70% of the band (so max-min>=116 channels), and step at least 5 channels on each change.

FS-GT2E Update

I got a FS-GT2E Tx and FS-A3 Rx kit and tested the GT2E, but the over-air packets are massively simplified. The GT2E doesn't listen for any responses. So it just has bind mode (switched on with bind button held, until it's switched off), and normal mode (switched on, without bind button down).
The packets are:
Bind 1BB32-bit Tx-IDFF FF FF FF01 0016 channel listThe Tx repeats these three packets on alternating channels
Bind 2BC32-bit Tx-IDFF FF FF FF00 0016 channel list
Bind 3BC32-bit Tx-IDFF FF FF FF01 0016 channel list
Normal5832-bit Tx-IDFF FF FF FF14 blocks of 16-bits stick position (in µSec, Least significant byte first)
Channels 1 from Wheel, 2 from throttle, 3...14=Mid pos (1500µSec)

This is probably done to re-use older Tx-HW, as they don't need to switch between Tx/Rx (so can use a slower CPU, and if they use a Tx amplifier, then don't need to switch it's mode), they don't need to store the Rx ID (so don't need an EEPROM). So they can just do a SW update for the older boards, and sell them as AFHDS-2A!

For the Rx code to bind correctly, it now spots if the Tx is ignoring the Bind responses. If the code spots 100x 0xBC Bind packets, on alternate channels, with no 0xBB ones, and none missed, then it assumes it's a dumb-Tx, so stores the Tx-ID and channel details anyway, then drops into the normal mode.
For a dumb-Tx, then Rx code checks the Tx-ID and confirms the Rx ID is 0xFF FF FF FF, then everything works as expected.

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....

The Roger Writes series

I research / dabble with lots of things, and figured that if I write my notes here, I can quickly reference them, also, sometimes, they are useful to others!
Here is what I have so far:

This page was lasted updated on Sunday, 24-Mar-2024 12:53:11 GMT

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