Decoding the Falcon-9 upper stage

This is hardly news any longer. Since a few weeks ago, some people have been decoding the S-band telemetry from the Falcon-9 upper stage, which includes live video of the exterior of the spacecraft and the liquid oxygen tanks interior. This started with the work of, @aang254 and others, who around the second week of March managed to decode video from the telemetry for the first time. has published some information about the telemetry, @aang254 has added a decoder to SatDump, and Alexandre Rouma is adding a decoder to SDR++. In fact, the whole exercise of decoding the video has become quite popular, and more and more people are contributing to decode the latest launches.

On 2021-03-24 there was a launch of 60 Starlink satellites from Cape Canaveral, and Iban Cardona EB3FRN sent me the IQ recording he did on the first orbit, some 18 minutes after the launch. I decided to make a GNU Radio decoder flowgraph for this, since even though there are already several software decoders, I haven’t seen anyone using GNU Radio. I figured out that I could easily put together a flowgraph using some blocks from gr-satellites. This would make a useful and interesting example of using GNU Radio to decode digital signals.

As described by, the telemetry signal is 3.5714 Mbaud GMSK. The coding is CCSDS Reed-Solomon frames with a (255,239) code (rather than the more frequent (255,223) code) and 5 interleaved Reed-Solomon code words per frame, which gives an uncoded frame size of 1195 bytes.

The decoder flowgraph is shown below. It can be downloaded here. In the end I have spent some time trying to optimize the demodulator to improve the number of decoded frames. I am using an approach based on quadrature demodulation rather than power detection of the FSK tones, which is what I tried at first (and it seems to work worse).

Falcon-9 GMSK S-band telemetry GNU Radio decoder flowgraph

Since the recording is sampled at 6 Msps, which gives only 1.68 samples per symbol, the first step is to interpolate by a factor of 2. This step is optional, but it makes it easier to test different clock synchronization algorithms. However, upsampling is not a good idea from the point of view of CPU usage. As part of the upsampling process, we lowpass filter the signal to 4 MHz bandwidth (which is more or less the occupied bandwidth), to let less noise power into the non-linear quadrature demodulator.

Next we have the quadrature demodulator with a gain adjusted to the deviation of the GMSK signal. Then we use the Symbol Sync block to recover the symbol clock and perform some pulse shape filtering. For the pulse shaping I’m using a Gaussian shape pulse corresponding to a BT of 1. That’s correct: it’s just a Gaussian, rather than the convolution of a Gaussian and a rectangular window, which would be the typical pulse shape for GMSK. By experimenting with the pulse shape, I’ve found that a narrower shape tends to work better, and the Gaussian BT gives me good control over how narrow to set the pulse.

I’m using the maximum likelyhood TED, but I’ve found that the Gardner TED also works well. I’ve decided to set the damping factor to 0.7, which seems to work slightly better than the usual factor of 1.

After demodulation, the frames are converted into PDUs by using gr-satellites’ Sync and create PDU block, which detects the CCSDS syncword at the start of the frames. Then we have the CCSDS descrambler from gr-satellites.

After descrambling, we need to do Reed-Solomon decoding. The Reed-Solomon code employed by Falcon-9 uses the dual basis, as mandated by the CCSDS standard. Therefore, it is necessary to do the basis change before decoding. However, Phil Karn KA9Q’s libfec only implements the basis change for the usual (255,223) code. While it would be very easy to add it also for the (255,239) code, in order not to modify the code I have decided to use Map blocks to perform the transformations to the conventional basis and back. The transformation tables are given in the Taltab and Tal1tab arrays of libfec, and this can be copied directly into the Map blocks.

To save myself some time (and perhaps mistakes), I have copied the Reed-Solomon code parameters from SDR++, but it is also easy to deduce these from the documentation given in the CCSDS TM Synchronization and Channel Coding Blue Book. The field generator polynomial is 391, or 110000111 in binary, which encodes the polynomial \(x^8 + x^7 + x^2 + x + 1\) given in the standard. This is the same for the (255,223) and (255,239) codes. The primitive element is 11 for both codes, since \(\alpha^{11}\) is used as a primitive element in \(\operatorname{GF}(2^8)\). The first consecutive root is 128 – E, where E is the number of errors that the code can correct: E = 16 for the (255,223) code and E = 8 for the (255,239) code. This corresponds to the factorization of the code generator polynomial \(g\) as\[g(x) = \prod_{j = 128 – E}^{127+E} (x – \alpha^{11j}).\]

After Reed-Solomon decoding we undo the basis change and write the decoded frames to a file for later analysis in a Jupyter notebook.

It is good to remark that the FEC used in this telemetry signal is not designed to be very robust or to allow copy with a weaker signal. It is probably intended as a low overhead approach to correcting occasional errors. The following figure, taken from the CCSDS Green Book shows the performance for BPSK/QPSK over AWGN. We see that around 6.5 dB Eb/N0 is needed to copy. Here we are doing non-coherent demodulation of GMSK, so the Eb/N0 threshold is higher.

The frames follow an ad-hoc protocol that is described in’s documentation and in my Jupyter notebook. There are packets fragmented inside the frames, and the way that these are defragmented is very similar to how the CCSDS M_PDU protocol for encapsulating Space Packets into AOS frames.

All the frames have a 13 bit sequence counter, and some of the packets have a timestamp which is given as a 64 bit integer counting the nanoseconds elapsed since the GPS epoch. The figure below shows the timestamps and sequence counts of the decoded packets. Note the regularity of the segments where frames are missing. This is a consequence of periodic fading in the signal.

All the packets contain a 64 bit ID that gives the source or the kind of information transmitted in the packets. One of these sources contains the video, which is transmitted by encapsulating five 188-byte MPEG transport stream packets inside each packet.

The decoded video doesn’t play back very smoothly, due to the frequent signal dropouts, but below we show a few frames that illustrate its contents.

Interior of Falcon-9 liquid oxygen tank
Falcon-9 upper stage Merlin engine
View outside of the spacecraft, showing the satellites to be deployed

Only around 28% of the data transmitted corresponds to the video transport stream, so there is a large amount of telemetry besides the video. The transport stream uses the following PIDs:

PID 0 ratio 0.4%
PID 32 ratio 0.4%
PID 511 ratio 6.5%
PID 2748 ratio 11.8%
PID 4112 ratio 79.4%
PID 8191 ratio 1.4%

As usual, PID 0 is used to carry the program allocation table (PAT), and can be decoded with dvbsnoop by doing

dvbsnoop -if falcon9.ts -s ts -tssubdecode 0

This shows the following decode:

We see that there is only program 1, whose program map table (PMT) is transmitted in PID 32. We can also decode the PMT with dvbsnoop, obtaining

These shows that there are two streams in this program:

  • An H.264 video stream in PID 4112
  • A stream of PES packets with private KLV metadata in PID 2748

Additionally, the program clock reference (PCR) is carried in PID 511. Note that the PIDs of the streams are 0x1010 and 0x0abc in hex, so probably they have been chosen because they have “nice” hex values.

The packets from the PCR PID 511 only have the PCR as useful data:

The PCR timestamps start by 2:13:10, so the transport stream started almost 2 hours before launch. The figure below shows the comparison of the PCR timestamps and the GPS timestamps in the packets carrying the transport stream data.

This is how the KLV stream packets look like:

PID 4112 contains the H.264 video with a resolution of 640×360 and 29.97 frames per second.

Another of the packet sources is dedicated to the log of the on-board GPS receiver. By throwing away the first 25 and last 2 bytes from these packets, we obtain the log in ASCII. This can be shown below. Note that some lines are cut short due to missed packets.

Most of the information in this log is self-explanatory. Something that I don’t understand is the codes next to the SVNs in the Track list. Probably these codes indicate something about the quality of tracking, availability of ephemerides or usage in the navigation solution. The timestamps listed at the beginning of each line are nanoseconds since the GPS epoch.

This post was just a revisit to things that are already known. The rest of the telemetry is not well understood, so there are nice reverse engineering challenges there for anyone that’s interested. The files for this post are included in this folder included in my jupyter_notebooks repo. The decoded frames are stored with git annex, and can be downloaded as indicated in the README.


  1. Great Job Dani !
    What an adventure from simple CW 1975 decoding (yes I’m 65+..) to this elaborate piece of “scientific” software ! Hope un dia will be up to a level I can make GR-Satellite working ! Saludos Albert PA5OXW

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.