All docsStellar LinkRF bench · by Stellar Systems v0.1.0

Radio Concepts

Channel Models

The PL channel emulator: where it sits, its five operations, how DSL values map to registers (relative dB), commits, and how to model a LEO pass.

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:

text
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:

text
NCO mixer -> attenuator -> AWGN -> burst-error gate -> mute gate

The 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 fieldRegisterConversionEffect
channel.doppler_hz + channel.freq_offset_hzCHAN_NCO_FREQround(f · 2³² / fs), the two values summedA frequency shift of the whole signal
channel.path_loss_dbCHAN_ATTENround(10^(−dB/20) · 2¹⁵), Q15, clamped to 0..65535A linear attenuation
channel.snr_dbCHAN_AWGN_AMPround(10^(−dB/20) · 16384), clamped to 0..32767Additive Gaussian noise amplitude
channel.burst_error_rateCHAN_BURST_RATEround(rate · 65536), rate clamped to 0..1Bursts of corrupted samples (64 cycles each)
channel.link_state = "down"CHAN_CTRL bit 0muteNo 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 a channel_staged event, meaning the change was not applied, instead of channel_set. A windowed assertion on chan.* 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.initial block 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#

YAML
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:

Shell
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
OptionDefaultMeaning
--altitude-km500Orbit altitude
--max-elevation-deg60Elevation at closest approach
--mask-deg5Elevation below which the link is down (AOS/LOS)
--carrier-hz437.2e6Carrier frequency
--retune-s5Ground-station Doppler retune cadence; the residual is the Doppler change between retunes
--lo-ppm0.8Station reference error, emitted as a constant freq_offset_hz
--aos-s15Scenario time of AOS, so emitted times are absolute
--points18Curve points (even)
--rawoffEmit 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).

Stellar Link · v0.1.0

↑↓ to moveEnter to open