Architecture¶
Data flow¶
┌──────────── signals (what is transmitted) ─────────────┐
│ catalog/*.yaml + codes/ (spreading-code generators) │
└───────────────┬─────────────────────────────────────────┘
│
scenarios/*.yaml ──▶ sim ──▶ SigMF (samples + truth annotation) ──▶ rx (acquisition) ──▶ eval (compare with truth)
│ ▲
└─▶ playback file (int8 / sc16) │
│ │
▼ (a person transmits, cables only) │
generator ─▶ attenuators ─▶ ESP32 ──▶ SigMF (no truth)
frontend (device limits, frequency plan)
The acquisition code (rx) does not know whether a SigMF file came from the simulator or from
an ESP32. Both arrive in the same format. Files from the simulator also carry a truth
annotation, which eval uses for automatic checks.
Three layers kept apart¶
The layers are separated so that adding a new signal or a C-band front end changes one place only.
1. Signal definition (signals)¶
A YAML file per signal lists the carrier frequency, chip rate, code length, modulation, data
symbol rate and whether there is a pilot component. The spreading code is produced by the
generator registered under the file's code_family name.
Adding a signal takes one YAML file, one generator and one test that compares the generator
with values printed in the ICD (see .claude/commands/add-signal.md).
2. Front end and frequency plan (frontend)¶
Device files record hardware limits: sample rates, ADC bits, and the largest number of samples
one capture can hold. freqplan.py converts between the frequency at the antenna and the
frequency the receiver is tuned to, including an optional external mixer.
If the mixer's local oscillator (LO) is above the signal frequency, the spectrum is mirrored at
the intermediate frequency and a positive Doppler shift at the antenna appears as a negative
shift in the samples. FrequencyPlan.doppler_sign expresses this.
A scenario can carry a frequency plan (frequency_plan in the scenario YAML). The simulator
then mirrors the Doppler shift for a high-side LO, applies the external LO error
(lo_offset_ppm) separately from the receiver crystal error, and takes baseband_offset_hz
from the plan, so acquisition needs no other input. See decision D-013.
Receiver impairments in the simulator¶
Optional keys under receiver in a scenario YAML; without them the output of a scenario is
unchanged (a test compares the samples of every scenario in scenarios/ with stored hashes).
| Key | Meaning |
|---|---|
dc_offset: {i, q} |
Constant offset of each component as a fraction of ADC full scale (−0.5 on I with 10 bits is −256 counts). Each value must be in [−1, 1). Needs quantization_bits. |
spurs: [{offset_hz, power_db}] |
Fixed tones. offset_hz is the offset from the tuned frequency in the samples, and must be inside ±half of the rate at which samples are generated. power_db is the tone power divided by the total noise power per sample at the generation rate (noise has unit variance); it is not the height above the noise floor in a spectrum, where a tone of power ratio 1 stands 10·log10(N) dB above the floor of one bin in an N-point spectrum. |
Processing order: signal and noise at the generation rate, then spurs, analog low-pass,
output-band low-pass (decimation method ideal), decimation, and the ADC. With a generation rate
(generate_rate_hz), a spur outside the analog bandwidth is removed; without one, the analog
bandwidth has no effect and the spur stays. A spur outside the output band folds to an aliased frequency when
the decimation method is none. The DC offset is added in the ADC after the gain of the
automatic gain control (AGC) is set from the signal alone, so the offset is not part of the
level the AGC holds. The offset is constant over a snapshot; the drift of about 25 counts
within one 205 µs capture seen on an ESP32-C3 is not modelled. Spur phases come from a random
generator separate from the one for noise and data symbols. See decision D-016.
3. Data format (io)¶
Recordings use SigMF: a raw sample file plus a JSON metadata file. The simulator's truth is
stored in an annotation whose core:label is truth, under the key snappnt:truth.
Acquisition (rx/acquisition.py)¶
- An ESP32 at 80 MSa/s captures about 0.2 ms, shorter than one NavIC code period (1 ms). The snapshot is therefore correlated against a local code replica that is one code period plus one snapshot long. The correlation lag tells where in the code period the snapshot started. All lags are computed at once with FFTs.
- The carrier frequency is searched by repeating the correlation over a grid of frequency offsets. The default grid step is 1 / (2 × coherent integration time).
- With
n_blocksgreater than 1, the snapshot is split into blocks that are correlated separately and added in power (non-coherent integration). This tolerates data-bit sign changes and a coarse frequency grid, at some cost in sensitivity. - The detection threshold is set so that, with noise only, the probability of a false peak
anywhere in the search grid equals
pfa. It assumes independent search cells; issue #1 checks this assumption by measurement. - The C/N0 estimate ignores losses and therefore reads low. Issue #4 measures the bias.
- Code Doppler (option
code_doppler, command-line--code-doppler). By default one code replica at the nominal chip rate serves every frequency bin. In direct reception, one crystal drives both the LO and the ADC, so a carrier offset Δf (from satellite Doppler and from the receiver clock error together) comes with a change of the code rate, relative to the sample clock, of Δf / f_c, where f_c is the carrier frequency. Over a snapshot of length T the code drifts by Δf / f_c × R_c × T chips, where R_c is the nominal chip rate. For NavIC S-band at Δf = 25 kHz and T = 0.2 s this is about 2 chips, which spreads the correlation peak over several lags. With the option on, the frequency bins are split into groups of neighbouring bins, and each group uses a replica with chip rate R_c × (1 + f_g / f_c), where f_g is the middle of the group measured from the centre frequency of the search. A group is at most 2 × 0.1 × f_c / (R_c × T) wide, so the code drift left over within a group stays below 0.1 chip. For a snapshot of a few milliseconds all bins fall into one group. The groups are processed one at a time (with a Doppler-rate search, all rate hypotheses inside each group), so only one group's replica spectra are in memory. The code phase is converted from the lag with the chip rate of the group that holds the peak. The decision and the alternatives are in D-023 of the decision log. - The option assumes direct reception. With an external mixer the offset at the receiver also
contains the error of the external LO, which shifts the IF without changing the code rate.
snappnt acquireandsnappnt sweeptherefore stop with an error when the truth annotation or the scenario has a frequency plan withlo_hz. A recording without a truth annotation carries no frequency plan, so the check cannot be made, and the option is applied as for direct reception.
Terms used in this project¶
| Term | Meaning |
|---|---|
| Snapshot (capture) | One block of I/Q samples that a device writes to memory in one go. Not a continuous stream. |
| Code phase | The chip position in the spreading code at the first sample of the snapshot, in chips. |
| Frequency offset | How far the carrier is from the centre frequency set by the frequency plan (baseband_offset_hz). It is the sum of the Doppler shift and the receiver's crystal error. |
| Truth | The parameter values the simulator used to generate a snapshot, stored for comparison. |
| Conducted test | A test in which the generator is connected to the receiver through cables and attenuators, so no signal is radiated. |