A Framed Link, Checked by Its Description¶
One FrameDesc describes a frame — a sync word, a payload, a CRC-16 — and
that same description is used on both ends of the link. wfmgen builds the
waveform from it; MpskReceiver recovers the symbols; the description then
finds and judges every frame. Nothing about the frame is written twice: the
sync word is field 0, the frame length comes from the layout, and the verdict
comes from FrameDesc.check.
The example runs BPSK and QPSK at Es/N0 = 14 dB with a carrier offset the receiver has to remove, 200 frames of 48 payload bits each.
What it shows¶
Phase ambiguity is resolved by the sync word. A carrier loop settles on
one of M equivalent phases (two for BPSK, four for QPSK) and cannot tell
which. Each phase is tried, and the one whose bits repeat the description's
sync word at the frame period is kept — the way a real receiver resolves it.
Every frame after the lock transient passes, with the right payload. The
first frame or two are lost while the loops settle; the rest pass
FrameDesc.check, and each passed payload is one that was sent.
The check is a real detector. One flipped bit fails it. At 4 dB Es/N0 more than half the frames are damaged and fail, and none passes carrying a payload that was never sent.
The script asserts all of this and exits non-zero if any claim breaks. Its C
twin is native/examples/framed_link_demo.c.
The code¶
import numpy as np
from doppler.mpsk import mpsk_demap
from doppler.track import MpskReceiver
from doppler.wfm import (
STAGE_CRC16,
Composer,
FrameDesc,
Segment,
field_bits,
)
SYNC = field_bits("0x1ACFFC1D") # 32 bits: field 0 of the description
PAYLOAD = 48
SPS = 8
SETTLE = 12 # frames the carrier and timing loops may take to settle
d = FrameDesc()
d.add_field("sync", SYNC)
d.add_data("payload", PAYLOAD)
d.add_derived("crc", 16)
d.add_stage_over(STAGE_CRC16, "payload", "crc")
d.build()
NB = d.nbits # the frame's length, from the layout
POFF = d.field_off(d.field_index("payload")) # where the payload starts
def transmit(kind, esn0_db, frames, seed=4):
"""Frames of random payload -> (truth chunks, complex baseband)."""
rng = np.random.default_rng(seed)
data = rng.integers(0, 2, frames * PAYLOAD, dtype=np.uint8)
seg = Segment(
type=kind,
sps=SPS,
fs=1.0,
freq=0.0002, # a carrier offset the receiver must remove
pulse="rrc",
level=-6.0,
snr=esn0_db,
snr_mode="esno",
seed=seed,
frame=d,
data=data,
)
return data.reshape(-1, PAYLOAD), np.asarray(Composer([seg]).compose())
def receive(m, iq):
"""Matched-filter symbols from MpskReceiver."""
rx = MpskReceiver(
m=m,
sps=SPS,
m_out=SPS, # one matched-filter output per symbol
pulse="rrc",
rrc_beta=0.35,
rrc_span=8,
bn_carrier=0.005,
bn_timing=0.01,
lock_thresh=0.65,
)
return rx, np.asarray(rx.steps(iq))
def to_bits(sym, m, k):
"""Symbols -> bits for carrier-phase ambiguity ``k`` (MSB first)."""
bps = {2: 1, 4: 2}[m]
rot = (sym * np.exp(-2j * np.pi * k / m)).astype(np.complex64)
lab = mpsk_demap(rot, m)
shifts = np.arange(bps - 1, -1, -1)
return ((lab[:, None] >> shifts) & 1).astype(np.uint8).ravel()
# Retires when dp_syncword_search gets a Python face (#1803).
def sync_starts(bits):
"""Offsets where the description's sync word matches exactly."""
win = np.lib.stride_tricks.sliding_window_view(bits, SYNC.size)
return np.flatnonzero((win == SYNC).all(axis=1))
def slice_frames(sym, m):
"""Resolve the phase ambiguity by the sync word, then cut frames."""
best = None
for k in range(m):
bits = to_bits(sym, m, k)
st = sync_starts(bits)
ok = np.intersect1d(st, st + NB) # repeats at the frame period
if best is None or ok.size > best[2].size:
best = (k, bits, ok)
k, bits, ok = best
s0 = int(ok[0]) % NB if ok.size else 0
n = (bits.size - s0) // NB
return k, bits[s0 : s0 + n * NB].reshape(n, NB)
def link(kind, m, esn0_db, frames=200):
chunks, iq = transmit(kind, esn0_db, frames)
rx, sym = receive(m, iq)
k, got = slice_frames(sym, m)
verdicts = [d.check(f) for f in got]
# Which sent frame is got[0]? Frames are unique, so match a passing one.
first = next(
(
i
for i, v in enumerate(verdicts)
if v.passed
and any(
np.array_equal(got[i, POFF : POFF + PAYLOAD], c)
for c in chunks
)
),
None,
)
return rx, chunks, got, verdicts, k, first
Related¶
- M-PSK Receiver — the receiver used here.
FrameDesc— the description both ends share.