Mode A (Part 9/12) 🕊️

Paul Tagliamonte 2026-09-08 diy
🕊️ This post is part of a series called "Pigeon". If this is the first post you've found, it'd be worth reading the intro post first and then looking over all posts in the series.

If you're looking for it, the intro to the PHY (layer 1) that this belongs at a high level and listing of the parts that make it up (including this one) is on the phy post.

While developing Pigeon, I’ve called the group of all the configuration of the Layer 1 PHY parameters the “Mode”. I’ve experimented with a few different “modes”, but one in particular has been the most resilient to the innumerable mistakes and bugs i’ve wrought into existence – and that is the first mode I wrote down, “Mode A”. This is even (mostly) backwards compatible to my original Go implementation of Pigeon Mode A (back in 2022) over the air, and has largely withstood the problems I’ve thrown at it.

I’ve removed the bulk of the support I wrote out for other modes, but i’m likely to bring them back over time as I use pigeon to learn more (such as “Mode B” (QAM-16), “Mode C” (QAM-16 NUC), and “Mode AW” which is the exact same as Mode A, except 5MHz in bandwidth. More to come on those as I get further along – but for now let’s braindump the parameters i’ve picked out for Mode A:

AttributeValueDescription
Rate2.5 MHzSampling Rate / Bandwidth
LCG3149721335LCG "RNG" whitening constant (randomly selected)
Preambleseq=16, order=4, count=2(this is as-written in the preamble post)
ModulationQPSK/QAM-42 bits per data subcarrier
FFT Size64
Cyc Len16Cyclic Prefix length (16 IQ samples)
Symbols168Number of OFDM Symbols
LDPC Table802.3an(this is as-written in the ldpc post)
Raw Bits144481806 bytes (168 symbols, 86 data bits per symbol)
LDPC Count7Number of packed LDPC encoded messages
Data Bits120611507 bytes

The last bit to describe here is the Subcarrier Plan. The plan is ordered “negative first” (meaning the 0th bin in-memory is the most negative frequency domain bin of the fft), and within a Mode A OFDM symbol, there are 64 frequency domain bins (so, just to make it explicit: 64 ‘subcarrier usages’ that make up our Mode A ‘subcarrier plan’).

We’ll follow the same structure and conventions that we went through in the post all about OFDM Symbols – which means, we’ll need to place our guard bins, data bins, and pilot bins. I’ll include a copy-paste-able version of the images to follow at the end.

Guard Bins

First up, let’s place our guard bins. As we’ve already gone over, we’re looking to clear some space right up against the high and low end of the frequency range, so let’s go ahead and do that:

I gave up the center bin (0 Hz) and 8 of the 64 bits on each side (1/4 of the signal!) to give myself a bit of elbow room. This is perhaps definitely a bit overkill, but it’s been an extremely robust choice. If you multiply that through, this accounts for 312.5 kHz of frequency domain “padding” at the high and low end of the bandwidth, or 625.0 kHz of bandwidth which is not to be used.

Pilot Bins

Next up was the pilot bins. We’ve already gone over the purpose (and use) of our pilots, but I’ve found there to be an art to the placement of the pilots. Interpolation between pilot bins has turned out to be very reliable, but extrapolation, on the other hand, has been a major pain, for reasons I don’t fully understand yet.

My intent in placement was to pick out roughly even stretches of data bins bracketed between pilots, with as few data subcarriers as practical “outside” of a pilot (using extrapolation). I’ve played a bit with my AGWN simulator(s), as well as logging errors between two SDRs, and the configuration I have this in has been fairly resillant (for whatever reason), and withstood a few rounds of tweaking.

Data Bins

Almost as an afterthought – all of the remaining bins become data bins.

This puts the total number of data bins at 43, which, since Mode A carries data in QPSK/QAM-4 (two bits per data subcarrier), means we can carry 86 bits of data per OFDM symbol. That fairly modest capacity is largely due to the modulation scheme (or fft size, but increasing that has been … fraught) we’re using for Mode A – but I’ve made up for it by including 168 OFDM symbols in a single burst in order to have enough data to carry IP traffic without splitting the packet into two bursts.

With all that designed and on paper, we’re ready to start to tackle the next layer up – our Layer 2, named, creatively, “link”.

Let’s send some link layer data →


Following along at home? Nice! As promised, I've put the full plan copy-pasted from the pigeon source tree below so that no one has to feel the need to transcribe this from the images above. Unlike most of the things I've "left to the reader", manual transcription from images is not a particularly useful task for anyone to do.

The following table is Mode A’s OFDM Subcarrier Plan. This is in negative first ordering (meaning the 0th member is the most negative fft bin, and the Nth is the highest frequency fft bin).

SubcarrierPlan([
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
  Data,
  Data,
  Data,
  Pilot(iq!(-1.0, 0.0)),
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Pilot(iq!(1.0, 0.0)),
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Guard,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Pilot(iq!(0.0, -1.0)),
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Data,
  Pilot(iq!(0.0, 1.0)),
  Data,
  Data,
  Data,
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
  Guard,
])
Still following along at home? Nicer! I've put the preamble sequence promised previously copy-pasted from the pigeon source tree below in case that's actually important (I don't think it is, but...).

The following table is Mode A’s frequency-domain preamble. This is, as above, in negative first ordering. I don’t actually think these values matter much (at all)? – but in case they do, here’s what I have. I muck with these a lot and haven’t found many changes in quality of detection or frequency correction yet.

[
  IQ::new(0.0, 0.0),
  IQ::new(0.0, 0.0),
  IQ::polar((TAU / 13.0) * 2.0, 1.0),
  IQ::polar((TAU / 13.0) * 3.0, 1.0),
  IQ::polar((TAU / 13.0) * 4.0, 1.0),
  IQ::polar((TAU / 13.0) * 5.0, 1.0),
  IQ::polar((TAU / 13.0) * 6.0, 1.0),
  IQ::polar((TAU / 13.0) * 7.0, 1.0),
  IQ::polar((TAU / 13.0) * 8.0, 1.0),
  IQ::polar((TAU / 13.0) * 9.0, 1.0),
  IQ::polar((TAU / 13.0) * 10.0, 1.0),
  IQ::polar((TAU / 13.0) * 11.0, 1.0),
  IQ::polar((TAU / 13.0) * 12.0, 1.0),
  IQ::polar((TAU / 13.0) * 13.0, 1.0),
  IQ::new(0.0, 0.0),
  IQ::new(0.0, 0.0),
]

Let’s send some link layer data →