SatLink emulates a satellite channel in the FPGA fabric, between its own transmitter and its own
receiver. The emulator is the channel_impair block, driven from scenarios through the
channel.* fields.
Where the channel sits#
The channel emulator is only on the PL loopback path:
PL loopback (GLOBAL_CTRL[0] = 1):
tx chain -> channel_impair -> rx chain (AD9361 idle)
RF (air or device loopback):
tx chain -> AD9361 -> ... -> AD9361 -> rx chain (channel_impair bypassed)A scenario whose profile has radio.loopback: false applies its impairments to nothing: the
values are written and committed, and never reach the demodulator. Check the profile's
radio.loopback before believing any channel measurement.
The five operations#
Inside the block, samples go through five stages in this order:
NCO mixer -> attenuator -> AWGN -> burst-error gate -> mute gateThe mute is last on purpose: taking the link down removes the signal and the added noise, so the receiver sees true silence rather than pure noise.
| DSL field | Register | Conversion | Effect |
|---|---|---|---|
channel.doppler_hz + channel.freq_offset_hz | CHAN_NCO_FREQ | round(f · 2³² / fs), the two values summed | A frequency shift of the whole signal |
channel.path_loss_db | CHAN_ATTEN | round(10^(−dB/20) · 2¹⁵), Q15, clamped to 0..65535 | A linear attenuation |
channel.snr_db | CHAN_AWGN_AMP | round(10^(−dB/20) · 16384), clamped to 0..32767 | Additive Gaussian noise amplitude |
channel.burst_error_rate | CHAN_BURST_RATE | round(rate · 65536), rate clamped to 0..1 | Bursts of corrupted samples (64 cycles each) |
channel.link_state = "down" | CHAN_CTRL bit 0 | mute | No output at all |
fs for the frequency word is the modem's sample rate, baud × sps × interp, read back from the
PL after the profile apply (2.5 MSa/s for every rate from 19 531.25 to 625 000 baud).
The dB are relative#
Likewise, snr_db sets a noise amplitude relative to a fixed full-scale reference, not a ratio to
the signal actually present. At snr_db: 30 the noise amplitude is 518 counts, at 60 it is 16;
attenuating the signal with path_loss_db does not reduce the noise, because the noise is added
after the attenuator. The two therefore do not compose the way a link budget would. Use them
separately, and read the resulting quality from the demodulator (rx.snr_db, rx.mer_db,
rx.evm) rather than from the value you asked for.
Commits: why a channel change can be refused#
The channel registers are double-buffered. A write lands in a shadow bank; the datapath reads the active bank; the profile sequencer swaps them on a commit. The sequencer only commits at a safe point: 64 consecutive clock cycles with no sample moving on the TX or RX stream.
Consequences:
- Under a sustained, dense traffic feed, a commit can time out (
state=0x03, WAIT_SAFE). The scenario then records achannel_stagedevent, meaning the change was not applied, instead ofchannel_set. A windowed assertion onchan.*or on the receiver will show it. - Reading a channel register returns the active bank. A write that has not been committed reads back as the old value.
- Every profile apply resets the five registers to pass-through (attenuation 0x7FFF, everything
else 0) and commits. A scenario's
channel.initialblock is applied after its profile, then committed, so it is not lost.
The telemetry carries the five active-bank registers on every sample (chan_ctrl, chan_atten,
chan_awgn_amp, chan_nco_freq, chan_burst_rate), and assertions can target them as
chan.<name>. That is how a report proves an impairment actually reached the link.
Driving the channel from a scenario#
channel:
initial:
link_state: "up"
snr_db: 30
freq_offset_hz: 0
path_loss_db: 0
burst_error_rate: 0.0
timeline:
- id: "path_loss_ramp"
from: "4s"
to: "14s"
ramp:
channel.path_loss_db: { from: 0, to: 20 }
- id: "link_down"
at: "30s"
set:
channel.link_state: "down"
- id: "link_up"
at: "34s"
set:
channel.link_state: "up"set applies a value at one instant; ramp and curve sweep a value across their window, one
point every 500 ms (at most 120 points). Each point is committed. See
Scenarios for the full timeline syntax, and
dsl/scenarios/impairments/channel_operations_sweep_v1.yaml for all five operations exercised
with windowed assertions.
Order impairments from the gentlest to the most destructive in a single run. Burst errors reliably jam the receive chain, so nothing measured after them is attributable.
Frequency offset and the carrier loop#
On the PL loopback path, the carrier loop runs without its acquisition aids (the frequency detector, the x4 frequency estimator and the silence hold are armed only on RF paths, because a PL loopback has no frequency error of its own). A channel frequency offset of a few hundred hertz can therefore stop the link on this path. The carrier loop's pull-in range has not been characterised in general; do not write nominal scenarios that depend on it without measuring it first.
Modelling a LEO pass#
dsl/tools/leo_pass_profile.py derives a pass from its geometry and emits DSL curves:
python3 dsl/tools/leo_pass_profile.py --self-test
python3 dsl/tools/leo_pass_profile.py --altitude-km 500 --max-elevation-deg 60 \
--mask-deg 5 --carrier-hz 437.2e6 --aos-s 15| Option | Default | Meaning |
|---|---|---|
--altitude-km | 500 | Orbit altitude |
--max-elevation-deg | 60 | Elevation at closest approach |
--mask-deg | 5 | Elevation below which the link is down (AOS/LOS) |
--carrier-hz | 437.2e6 | Carrier frequency |
--retune-s | 5 | Ground-station Doppler retune cadence; the residual is the Doppler change between retunes |
--lo-ppm | 0.8 | Station reference error, emitted as a constant freq_offset_hz |
--aos-s | 15 | Scenario time of AOS, so emitted times are absolute |
--points | 18 | Curve points (even) |
--raw | off | Emit the uncompensated Doppler instead of the residual |
It emits the relative path loss (0 dB at closest approach) and, by default, the residual Doppler left after ground-station tracking (hundreds of hertz), not the raw shift (±10 kHz at UHF, half a symbol rate at 19 531 baud). Doppler is positive at AOS (the satellite approaches), crosses zero at closest approach and is negative at LOS, following an S-curve that does most of its work in the minute around closest approach.
dsl/scenarios/impairments/leo_doppler_pass_v1.yaml is a complete pass built this way, with a
continuous CSP downlink and telecommands during the pass.
Model-only metrics#
Assertions can name rf.ber, rf.frame_error_rate, rf.snr_db and rf.doppler_hz. These are
computed from the values the scenario itself asked for (rf.ber is a lookup on channel.snr_db),
never measured, and are always reported as not verified. Assert on rx.* and chan.*
instead (see Assertions and Reports).