ImmersionRC Ghost protocol inside Betaflight

ImmersionRC recently pushed out the new product known as Ghost. It is based upon LoRaWAN which gave it some nice advantages in comparison to other existing radios. ImmersionRC also created a new lightweight protocol to improve latency and some new features. This post will be more about how it is integrated into Betaflight code. 

At the moment of the writing the code is located here:

If you figure out how this protocol works you can use it in your projects. With Ghost radio transmitter and Ghost receivers, you can communicate with any device you want at a distance of around 10 km with very low latency including telemetry data.

In the text below I'll describe how Ghost's frames look like. We are looking from the receiver's perspective (Betaflight flight controller).  I'll divide it into two sections, receiving frame and telemetry frame. 


ghost, irc, betaflight
Figure 1 - Ghost receiving frame (packet)

You can see that frame has consisted of a 1-byte address, length, type, variable payload up to 14 bytes, and at the end is 1 byte CRC. This is 18 bytes for full-frame size.

The physical layer used for Ghost is UART with parameters:

  • word length is 8 bits (defined in Betaflight UART driver)
  • the stop bit is 1
  • no parity bit
  • baud rate 420000
With the information above, we know that for one byte transported we need 10 bits at UART physical layer. 

Figure 2 - UART physical layer

Knowing that we can calculate [bytes/s] which is 42000. 

Now we can calculate the time that is needed for one frame to be sent. 

full_frame_time = 18 [bytes]/42000 [bytes/s] = 428.571 [µs]

This leads us to a defined minimal frame time which is set to 500 µs.

Frame rate (period) is defined with 45003CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22address%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%237ea6e0%3BfillColor%3D%23dae8fc%3BstrokeColor%3D%236c8ebf%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22920%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%223%22%20value%3D%22length%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%23b3b3b3%3BfillColor%3D%23f5f5f5%3BstrokeColor%3D%23666666%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221000%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%224%22%20value%3D%22type%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%2397d077%3BfillColor%3D%23d5e8d4%3BstrokeColor%3D%2382b366%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221080%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%225%22%20value%3D%22payload%26lt%3Bbr%26gt%3Bvariable%2C%20up%20to%2014%20bytes%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%23ffd966%3BfillColor%3D%23fff2cc%3BstrokeColor%3D%23d6b656%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221160%22%20y%3D%22320%22%20width%3D%22280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%226%22%20value%3D%22CRC%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%23ea6b66%3BfillColor%3D%23f8cecc%3BstrokeColor%3D%23b85450%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221440%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22address%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%237ea6e0%3BfillColor%3D%23dae8fc%3BstrokeColor%3D%236c8ebf%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22920%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%223%22%20value%3D%22length%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%23b3b3b3%3BfillColor%3D%23f5f5f5%3BstrokeColor%3D%23666666%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221000%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%224%22%20value%3D%22type%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%2397d077%3BfillColor%3D%23d5e8d4%3BstrokeColor%3D%2382b366%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221080%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%225%22%20value%3D%22payload%26lt%3Bbr%26gt%3Bvariable%2C%20up%20to%2014%20bytes%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%23ffd966%3BfillColor%3D%23fff2cc%3BstrokeColor%3D%23d6b656%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221160%22%20y%3D%22320%22%20width%3D%22280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%226%22%20value%3D%22CRC%26lt%3Bbr%26gt%3B1%20byte%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BgradientColor%3D%23ea6b66%3BfillColor%3D%23f8cecc%3BstrokeColor%3D%23b85450%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%221440%22%20y%3D%22320%22%20width%3D%2280%22%20height%3D%2280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E µs.

frame_frequency = 1/frame_period  = 1/4500[µs] = 222.22 [Hz]

Figure 3 - frames sequence

With this data, we can see that each frame has 4000 µs (4 ms) to be processed or this is enough time to send telemetry through the same UART port back to the sender. 

Here is how the data is processed in the Betaflight code. It goes through four steps. 

Figure 4 - Basic code diagram

Step 1) is interrupted from UART. It is the process where the bytes of the frames are collected. For each interrupt, one byte will be stored into ghstIncomingFrame.  At the process of collecting, the first couple of byte will be collected to determine what is the size of the full-frame. Once when full-frame has been collected the flag for the new available frame will be set and the ghstIncomingFrame will be copied to ghstValidatedFrame

Step 2) is where the task scheduler will call the next function and where CRC calculation is done. If CRC is ok,  and also if the address is for the flight controller (GHST_ADDR_FC) new flag for the validated frame will be set and the process request will be returned to Betaflight code. 

Step 3) is the next task in the row. Since the Ghost returned process is required the process function will be called. This is where most operations are done. Depending on the type of the frame different actions are taken.  Worth to mention is that Betaflight is expecting 11bit ch1to4 values and so Ghost will send those value in 12bit format but shifted for 1 bit left. This is why the code in figure 5 looks like this. 

Figure 5 - Code where 1 bit for ch1to4 is shifted

Ghost protocol supports up to 16 channels. With different types of frames, different channels are sent. Each frame contains 4 primary channels and could include additional 4 channels. To send all 16 channels 3 different frames need to be sent separately. All data are converted to channel data and stored in the ghstChannelData array

Finally, step 4) is where raw RC data is provided to Betaflight. The function call is from the same thread as step 3). The raw RC data is calculated from collected channel data inside the ghstChannelData array. 


The telemetry frame is sent through UART with the same setup as the receiving frame. The only difference is the baud rate. 

Telemetry supports two baud rates :

  • slow baud rate - 115200
  • fast baud rate -  400000

Figure 6 - Ghost telemetry frame (packet)

The telemetry frame is the same shape as the receiving frame but the payload size could be up to 20 bytes. 

Figure 7 - Betaflight's Ghost telemetry payload

At figure 7 it is shown what telemetry will Betaflight fill in. 

The telemetry frame is sent in a window between 1 to 2 ms after the receiving frame is received. 

Figure 8 - Receiving and telemetry frames

The protocol is still under development so check the code if anything changed. 

Igor Mišić


  1. Thanks for publishing this thorough writeup. Do you know anything about the VRx control that Ghost provides? What packet types are used to set the VRx Band and Channel through Ghost?

    1. hey Jonas, thanks for commenting. I never went through VRx protocol but this is what you can do:
      1) join this FB group and ask Tony directly
      2) connect logic analyzer at GHOST UART and try to decode it

    2. Hi Igor,
      Thanks for answering!

      Joining any facebook group is absolutely off limits for me. If the only documentation available is on facebook it doesn't exist.

      I'd be happy to use a logic analyzer, but I do not own Ghost hardware, which is actually the reason I was looking for docs. I want to implement a version of the ELRS-Backpack to control my Orqa goggles (which can be fitted with a Ghost Rx to mirror your quads channel).


Post a Comment

Popular posts from this blog

AUTOSAR and CRC calculation

Flashing/debugging/running code at external memory in the memory-mapped mode

Debugging EK-TM4C123GXL with Visual Studio Code on Linux