This page covers the instruments used to answer "does the link work, and how well". Every one of them has a way to be wrong; each section says what the instrument proves and what it does not.
The byte-exact loopback#
The one measurement that proves bytes cross the modem correctly: feed a known payload, capture what comes back, compare.
head -c 65536 /dev/urandom > p.bin # a raw le:u8/32 payload (see below)
satlinkctl chain up --target pl --profile pl_loopback
satlinkctl datapath disable # loopback claims the modem itself
satlinkctl loopback --payload p.bin --blocks 4 --warmup 1 -o rx.binloopback arms the reader first, feeds the payload continuously, collects --blocks blocks in a
row after discarding --warmup blocks, and compares. The same comparison runs offline:
satlinkctl compare rx.bin p.binThe payload file is in the raw DMA encoding: only the low byte of each 32-bit little-endian word is used. A file of random bytes is fine (three bytes in four are ignored); to send a specific byte sequence, place each byte in the low lane of a 32-bit word.
Reading the result#
The comparison is reported per block, never as a single figure: each block is aligned to the payload independently, because consecutive DMA blocks need not share an alignment.
| Verdict | Meaning |
|---|---|
BytePerfect | Every block matched entirely |
Degraded | Every block correlates with the payload, some bytes differ |
NoCorrelation | At least one block does not correlate at all: not a degraded link, no link |
A slip is flagged separately when the alignment between consecutive blocks does not advance by exactly one block (a byte lost or inserted at a block boundary). It is not a demodulation error.
How to interpret errors:
- Errors only in the first 16 bytes of a block are the DMA block-boundary artefact: the block boundary does not align with a frame. Not a modem defect.
- Errors concentrated in block 0 and thinning out are an acquisition transient. Use
--warmup 1; its effect varies a lot from run to run. - A short comparison window over-weights boundary errors (the same capture has scored 98.8 % over 1024 bytes and 99.71 % over 4096). Compare whole captures.
Make the measurement able to fail#
A green loopback is the easiest thing to fake. Two controls make it mean something:
- Check the path. On an RF measurement, confirm the PL loopback mux is off
(
satlinkctl chain statusshowspl-mux 0,device-loopback open,dac-source 2). A profile withradio.loopback: trueapplied after the mode switch silently turns an "RF" loopback into a PL one. - Turn it red once. Collapse the RF level and nothing else (for example TX attenuation from −20 to −89 dB): the capture must stop (504, no blocks). Restore it and it must come back.
Frame counters as instruments#
tx.framer.frame_cnt counts what really crossed the PL, whatever userspace believes it sent. It
is the reliable throughput instrument: read it over a timestamped interval during a continuous
feed. Do not measure throughput with dd on the IIO devices: the buffer absorbs several blocks
before anything crosses, and one dd count=1 per block only ever sends the first block.
rx_demod.deframer.frame_cnt moving at zero errors proves frame sync, not correct bytes: the
marker is inserted by the PL, downstream of the payload, so a receiver can lock on it while the
payload is wrong. Use a byte comparison or a transfer frame's FECF for payload correctness.
Characterising the RF path#
Before trusting any absolute level, measure the loss between TX1 and your instruments:
python3 pl/top/bringup/rf_path_gain.py --rx1 connected # or --rx1 openThe script sets the analyser's reference offset to zero, sweeps the TX attenuation, and derives
the path loss from the board's known output (P_board ≈ att + 15.8 dBm). It checks three
criteria before setting any offset: the loss must be constant across the sweep, the loss beyond
the pad must be a plausible splitter (3 to 7 dB), and 1 dB of commanded attenuation must move the
output by 1 dB. If any fails, it refuses to set an offset. --rx1 is required because
terminating the RX1 arm changes the splitter's loss (about 0.4 dB on the reference bench).
Re-run it whenever a connector, pad or splitter changes, or the carrier moves far: a pad's loss
is frequency dependent. The script needs the analyser on the network and a board with the
raw_tx profile available.
Sweeps as scenarios#
A sweep is a measurement: run it as a scenario so every point shares the run's clock, traffic and telemetry, rather than as a shell loop.
| Scenario | Measures |
|---|---|
bringup/rf_rx_gain_response.yaml | The receive chain's response to the AD9361 RX gain, 0 to 70 dB, under a constant transmitter |
bringup/rf_rx_port_sweep.yaml | Which AD9361 RX input carries the signal, with a no-signal window first |
bringup/rf_level_sweep.yaml | The receiver across transmit levels |
bringup/tone_sweep_reference.yaml | Pure tones at 0.1, 0.25 and 0.5 of DAC full scale, then a lower-sideband tone, for analyser calibration |
impairments/channel_operations_sweep_v1.yaml | The five channel operations with windowed assertions |
pl/top/bringup/read_port_sweep.py <board> <run-id> and read_gain_response.py read such runs,
taking every window boundary from the run's own events.
When reading a sweep, prefer slopes and internal states (a 1:1 response, an AGC at its ceiling, a port at its no-signal floor) over absolute levels: they do not depend on a calibration you may not have.
Throughput: satlink-bench#
satlink-bench (ps/satlink-bench) measures the sustained PS↔PL throughput on the board: one
IIO descriptor kept open, one timestamp per write, and the PL counters read in the same process at
the same instants. It reports a distribution, because the TX chain runs in bursts. --duplex runs
a reader alongside, armed first; feeding TX alone back-pressures the RX chain within milliseconds.
Other bring-up instruments#
| Tool | Purpose |
|---|---|
satlinkctl doctor | Bring-up health check by register name, including repeated reads of the same address |
satlink-check | Host-side cold-deploy check: daemon, services, PL registers, profiles and scenarios |
pl/top/bringup/power_meter_check.py | The PL power meter's response across amplitudes (input and output taps read in successive windows, so about 1 dB of run-to-run spread) |
pl/top/bringup/run-campaign.sh | Launch a long shell campaign under a transient systemd unit, logging to both the journal and a file |
| ILA captures | Integrated logic analyser builds for signals no register exposes (see Building the Bitstream) |
Before instrumenting a block, read the debug registers it already exposes: ADI's axi_dmac, for
example, publishes its reset-manager state in its DBG0 register (0x774 = all domains released).