A Frame You Built, Generated¶
wfmgen frames a waveform two ways, and they are not two representations.
--acq-code, --sync and --crc spell one frame, the common
[preamble × reps | sync | payload | crc]. Everything else — a field of
your own at a position you choose, a coding stage, a CCSDS CADU — is a
wfm_frame_desc_t: fields in wire order, stages with the
span each covers.
wfm_source_t.frame is the way in. Point a source at a description and that
description is the frame. wfmgen --frame FILE and a scene's "frame"
key reach the same member.
The whole thing, in one program¶
A 16-bit header of the caller's own bits, a payload, and a CRC-16 over a span named rather than counted — then the samples, checked back against the description that produced them.
#include <complex.h>
#include <stdio.h>
#include <string.h>
#include <doppler/wfm/wfm_compose.h>
#include <doppler/wfm/wfm_frame.h>
#include <doppler/wfm_synth/wfm_synth_core.h>
#define NBITS 56u /* 16 header + 24 payload + 16 CRC */
#define SPS 4u
#define NS (NBITS * SPS)
int
main (void)
{
/* Bits the caller owns. A description BORROWS them, so they must outlive
every compose call that reads it. */
static uint8_t hdr[16], payload[24];
for (unsigned i = 0; i < 16u; i++)
hdr[i] = (uint8_t)((0x5C5Cu >> (15u - i)) & 1u);
for (unsigned i = 0; i < 24u; i++)
payload[i] = (uint8_t)((i * 7u + 1u) & 1u);
wfm_seq_t h = { 0 }, p = { 0 };
h.kind = WFM_SEQ_LITERAL, h.bits = hdr, h.len = 16u;
p.kind = WFM_SEQ_LITERAL, p.bits = payload, p.len = 24u;
/* Fields in WIRE order; the stage names the span it covers. The cover
reaches the derived field, which is what wires that field's producer --
so a CRC's position and the fact that a CRC produces it are one
declaration rather than two that can disagree. */
wfm_frame_desc_t d;
memset (&d, 0, sizeof d);
if (dp_wfm_frame_add_field (&d, "hdr", &h, 0u) < 0
|| dp_wfm_frame_add_field (&d, "payload", &p, 0u) < 0
|| dp_wfm_frame_add_derived (&d, "crc", WFM_FRAME_CRC_BITS) < 0
|| dp_wfm_frame_add_stage (&d, WFM_STAGE_CRC16, "payload", "crc") < 0)
return 1;
/* One source, carrying it. The description is the whole frame -- fixed
bits here, sent once. A payload that changes frame to frame is a
data:LEN field instead, filled from the source's `data`. */
wfm_source_t src = { 0 };
src.type = WFM_SYNTH_BITS;
src.modulation = 1; /* bpsk */
src.sps = (int)SPS;
src.snr = WFM_SYNTH_SNR_CLEAN; /* no AWGN, so the checks are equalities */
src.snr_mode = 1;
src.frame = &d;
if (dp_wfm_source_error (&src) != NULL)
return 1;
wfm_segment_t seg = { 0 };
seg.sources = &src;
seg.n_sources = 1u;
seg.fs = 1.0e6; /* no num_samples: the frame sets the run, NS samples */
dp_wfm_compose_state_t *c = dp_wfm_compose_create (&seg, 1u, 0, 0);
if (!c)
return 1;
float complex out[NS];
size_t n = 0, got_n;
while (n < NS && (got_n = dp_wfm_compose_execute (c, out + n, NS - n)) > 0)
n += got_n;
dp_wfm_compose_destroy (c);
/* The samples carry the DESCRIPTION's bits. `dp_wfm_frame_assemble` builds
them independently; dp_bpsk_map's convention is 0 -> +1, 1 -> -1, so with a
clean rectangular source at zero offset the sign is the bit. */
uint8_t want[NBITS], got[NBITS];
if (dp_wfm_frame_assemble (&d, NULL, want, NBITS) != NBITS)
return 1;
for (unsigned i = 0; i < NBITS; i++)
got[i] = crealf (out[i * SPS]) < 0.0f ? 1u : 0u;
printf ("%zu samples, %u frame bits, match: %s\n", n, NBITS,
memcmp (got, want, NBITS) == 0 ? "yes" : "NO");
return memcmp (got, want, NBITS) == 0 ? 0 : 1;
}
What the description bought¶
Three things, none of which a flag offers:
A field at a position you choose. The flat fields offer a preamble, a
sync word and a payload, in that order. hdr above is neither of the first
two and sits ahead of the payload because the description says so.
A cover that is named, not counted. dp_wfm_frame_add_stage(&d, WFM_STAGE_CRC16, "payload", "crc") says what three integers used to. The
header is deliberately outside the CRC's cover — a receiver has to find the
header before it can check anything — and that choice is one argument, not an
offset arithmetic exercise.
A kind that is open. wfm_stage_kind_t stops at WFM_STAGE_USER = 0x1000; above that the kinds are yours, and the kernel comes in through
wfm_frame_ops_t. A transform doppler has never heard of is a stage, not a
pull request against a header — and the table extends the built-ins, so
supplying one kind does not mean restating the CRC.
That is the claim this page rested most weight on and demonstrated least, so
§6 of the example below now runs it: a whitener at WFM_STAGE_USER + 1, its
kernel supplied through a one-entry table. Without the kernel the assembly is
refused — never a silent skip, because a stage that quietly did not run
produces a frame that still assembles, still decodes against itself, and syncs
to nothing. With it, the frame assembles and reverses through the same open
lookup, and dp_wfm_frame_check reports both stages reversed.
The common frame is a description too¶
--acq-code, --sync and --crc go through one function,
dp_wfm_frame_fixed(), which builds the common frame as an ordinary
description with fields called "preamble", "sync", "payload" and
"crc". Hand that description to a second source and the two compose
byte-identically — which is what makes "one frame representation" a
measurement rather than a claim.
The worked version of that check, and of every claim above, is
native/examples/wfmgen_frame_demo.c:
it composes framed against unframed, demodulates the frame back to the
description's own bits, shows one description making a three-frame record
from three chunks of data -- its payload a data:LEN field, each frame the
next chunk with its own CRC -- proves the equivalence above, and ends on the
caller's own stage kind. It self-validates and exits non-zero if any of it stops holding — which
is why the sections are numbered here rather than counted.
From Python, where there is no ops table by design, the same end is
reached by running the transform in the caller and handing the wire the
result — src/doppler/examples/frame_own_stage_demo.py, which describes the
same generic frame, whitens it with its own kernel and checks the recovered
bits against the same description. The trade is the one
#1125 chose: once per
frame in Python, rather than per stage inside the assembler.
Reaching it from the other interfaces¶
The C struct is the primary interface, and the other two carry the same
description rather than reimplementing it — see
Waveforms: in a scene, and from Python
for the scene JSON's frame key and what Python does with it.
Related pages¶
- A CCSDS CADU — the description on its own: what a real standard's fields and covers look like, and why the marker is covered by the inner code and by neither the outer code nor the randomiser.
- Name Your Own Code — an outer and an inner code that are nobody's standard, run end to end.
- DsssBurstReceiver — a spread frame from a description, transmitted on all three wfmgen faces and checked against the same description on receive.