All docsStellar LinkRF bench · by Stellar Systems v0.1.0

Signal Processing

Receiver Chain

The RX datapath block by block: ingress, DDC, AGC, matched filter, timing and carrier recovery, slicer, demodulators, deframer and FEC decoder, with their registers and lock indicators.

text
AD9361 ADC -> rx_ingress -> ddc_core -> rx_switch -> agc -> matched_filter
           -> timing_recovery -> carrier_recovery -> slicer
           -> demod (BPSK | QPSK | GMSK) -> deframer -> fec_rx -> DMA (bytes)

In PL loopback the input comes from the channel emulator instead of the ADC (the loopback mux sits in front of the RX chain). The DDC-side blocks live in the window at 0x4001_0000, the demodulator-side blocks at 0x4002_0000.

rx_ingress#

Packs the AD9361's 12-bit I/Q into the 16-bit datapath with a digital gain.

RegisterMeaning
rx_ddc.ingress.ctrl[0] enable (radio.rx_enabled), [1] clear counter, [2] clear status
rx_ddc.ingress.gainQ8.8 digital gain (0x0100 = 1.0); radio gain --domain digital
rx_ddc.ingress.status[0] ADC overflow (sticky), [1] post-gain saturation (sticky), [2] running
rx_ddc.ingress.smpl_cntSamples received

ddc_core#

A complex NCO mixer, a decimator by DECIM (a single-stage running-sum CIC), and a compensation FIR (identity by default).

RegisterMeaningSet by
rx_ddc.ddc.ctrl[0] enableProfile
rx_ddc.ddc.decimDecimation 1 to 32, always equal to the TX interpolationProfile
rx_ddc.ddc.nco_freqQ32 phase incrementTuning (LO offset), radio nco --rx-hz
rx_ddc.ddc.filter_sel, gain, statusFIR bank, gain, busy—

rx_ddc.rx_switch then routes the DDC output to the demodulator and to the monitor taps.

agc#

Estimates |x|², compares it with TARGET, and integrates the gain up (DECAY step) or down (ATTACK step).

RegisterMeaningProfile value
rx_demod.agc.ctrl[0] enable, [1] clearenabled
rx_demod.agc.targetTarget |A|² = I² + Q²0x0409C0C9: 12 dB below a full-scale QPSK
rx_demod.agc.attack, decayStep sizes0x0100 each
rx_demod.agc.gainCurrent gain, Q4.12 (read-only)→ telemetry agc_gain
rx_demod.agc.status[0] locked (error within 12.5 % of target)

The target sits 12 dB under full scale because the AGC acts before the matched filter, whose DC gain is about 2.8: at full scale the filter clipped most samples and blinded the carrier loop. A gain pinned at its Q4.12 ceiling (just under 16) means the input is too weak: raise the RX gain.

matched_filter#

A 33-tap complex FIR, root-raised-cosine α = 0.35 at 4 samples per symbol, built at synthesis time.

RegisterMeaning
rx_demod.matched_filter.ctrl[0] enable, [1] clear shift register
rx_demod.matched_filter.coeff_bankWrite a tap: index in [15:0], Q15 coefficient in [31:16]
rx_demod.matched_filter.spsInert: the kernel is fixed at 4 sps

timing_recovery#

Gardner timing recovery with a fractional (Farrow, piecewise-parabolic) interpolator: two interpolants per symbol, on-time and mid-symbol, driven by a PI loop.

RegisterMeaning
rx_demod.timing_recovery.ctrl[0] enable, [1] clear
rx_demod.timing_recovery.loop_bw[7:0] kp, [15:8] ki (profile: 0x0202)
rx_demod.timing_recovery.status[0] locked → telemetry lock
rx_demod.timing_recovery.phase_errNormalised Gardner error, signed 16-bit
rx_demod.timing_recovery.lock_cntSymbols spent under the lock threshold

The lock detector also requires a minimum input magnitude, so silence releases the lock.

carrier_recovery#

A second-order Costas loop, one update per symbol, with acquisition aids.

RegisterMeaning
rx_demod.carrier_recovery.ctrl[0] enable, [1] clear, [2] mode (0 QPSK, 1 BPSK), [3] blind sweep, [4] frequency-locked loop, [5] FLL always, [6] x4 frequency estimator, [7] hold the frequency while there is no signal
rx_demod.carrier_recovery.loop_bw[7:0] kp, [15:8] ki, [19:16] kp shift, [27:24] ki shift (profile: 0x04080420)
rx_demod.carrier_recovery.fllFLL gain: [7:0] kf, [11:8] kf shift
rx_demod.carrier_recovery.fdet_cfg, fdet_threshx4 estimator window and lock threshold
rx_demod.carrier_recovery.fdet_freq, fdet_statusResidual measured by the estimator, and its verdict
rx_demod.carrier_recovery.sweep_range, sweep_stepBlind sweep bounds (±3 kHz, sized from the symbol rate)
rx_demod.carrier_recovery.cfofreq_q, the loop's frequency: cfo_hz = freq_q · Rs / 2³²
rx_demod.carrier_recovery.phase_errPhase detector output (unitless)
rx_demod.carrier_recovery.status[0] locked → telemetry carrier_lock

What a profile arms depends on the path:

PathctrlWhy
PL loopback (radio.loopback: true)0x01There is no frequency to acquire; aids would only cost accuracy
RF0xD1: FLL, x4 estimator, silence holdThe FLL pulls the loop onto the offset; the x4 estimator owns the lock verdict above about 12 dB Es/N0 so a rotating constellation is not read as locked; the hold stops the frequency integrator drifting on noise during silences

The blind sweep (bit 3) is left off: it false-locked on frequency aliases in bench tests.

slicer and demodulators#

The slicer takes a hard, Gray-coded decision on the sign of I and Q and counts symbols (rx_demod.slicer.sym_cnt → telemetry rx_symbols). The demodulator matching the profile is enabled; the others are switched off.

BlockRegistersNotes
demod_bpskctrl, status, evmCoherent
demod_qpskctrl, status, evm, sig_pwr, err_pwr, win_cntCoherent. Signal and error powers over 1024-symbol windows give snr_db and mer_db
demod_gmskctrl, bt, statusNon-coherent FM discriminator, hard decision. No EVM

deframer#

Searches the sync marker, delimits frames and descrambles.

RegisterMeaning
rx_demod.deframer.ctrl[0] enable, [1] 2 bits per symbol (QPSK), [2] clear counters
rx_demod.deframer.sync_wordThe marker to search
rx_demod.deframer.frame_lenFrame length on the coded stream
rx_demod.deframer.scramble[0] enable, [15:8] seed (same layout as the TX scrambler)
rx_demod.deframer.status[0] locked, [2:1] state, [4:3] rotation of the last matched marker (0/90/180/270°)
rx_demod.deframer.frame_cntFrames emitted → telemetry rx_frames
rx_demod.deframer.err_cntMarkers matched with one wrong bit → telemetry rx_frame_errors, ber_asm

The marker is correlated against every rotation of the constellation the modulation allows (four for QPSK, two for BPSK and GMSK) with at most one bit of error. The matched rotation is undone on the payload before byte assembly and descrambling, so the bytes come out as sent whichever point the carrier loop settled on.

The deframer applies back-pressure: if nothing consumes its output (no RX block submitted), it parks with a byte pending and the jam propagates back to the transmitter. The reader must be armed before the feed; the API and CLI measurement commands do this for you.

fec_rx#

Viterbi decoder (64 states, hard decision) followed by the Reed-Solomon (255,223) decoder (syndromes, Berlekamp-Massey, Chien search, Forney).

RegisterMeaning
rx_demod.fec_rx.ctrl[0] enable, [1] bypass Viterbi, [2] bypass RS
rx_demod.fec_rx.corr_cntBytes corrected by RS
rx_demod.fec_rx.fail_cntUncorrectable codewords
rx_demod.fec_rx.rate, statusRate, busy

RS corrects up to 16 byte errors per codeword; beyond that it declares the failure rather than returning a wrong word silently, and the codeword is still passed on, errors included. The RS decoder is double-buffered, so a codeword is accepted while the previous one is corrected and the decoder closes its input for only about 20 cycles per codeword.

Reading the chain from the outside#

QuestionLook at
Is any signal reaching the receiver?part_rssi_db, rx_ddc.ingress.smpl_cnt, agc_gain below its ceiling
Are symbols being decided?rx_symbols moving
Is the constellation clean?evm, snr_db/mer_db, sig_pwr/err_pwr
Are frames found?rx_frames moving while tx_frames moves
How degraded is the channel?rx_frame_errors/ber_asm, RS corr_cnt, fail_cnt
Are the bytes right?Only a byte comparison (loopback measurement) or a transfer frame's FECF says

Stellar Link · v0.1.0

↑↓ to moveEnter to open