A Frame You Built, Generated¶
wfmgen has always been able to frame a waveform. What it could not do was take a frame you described.
Framing reached the generator as flat flags — --sync, --acq-code,
--crc, --rs-depth, --asm, --conv — and between them they spell the
frames doppler already knows, at the positions doppler already puts them. A
layout outside that shape needed a new flag, which is one more spelling of
something wfm_frame_desc_t could already describe: 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; the flat fields stay, as sugar that builds one
of these, so every scene, flag and JSON key written before this keeps working
unchanged.
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 <wfm/wfm_compose.h>
#include <wfm/wfm_frame.h>
#include <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 (wfm_frame_add_field (&d, "hdr", &h, 0u) < 0
|| wfm_frame_add_field (&d, "payload", &p, 0u) < 0
|| wfm_frame_add_derived (&d, "crc", WFM_FRAME_CRC_BITS) < 0
|| wfm_frame_add_stage (&d, WFM_STAGE_CRC16, "payload", "crc") < 0)
return 1;
/* One source, carrying it. A frame needs an explicit payload, which is
what `type=bits` gives it. */
wfm_source_t src = { 0 };
src.type = WFM_SYNTH_BITS;
src.payload = p;
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 (wfm_source_frame_error (&src) != NULL)
return 1;
wfm_segment_t seg = { 0 };
seg.sources = &src;
seg.n_sources = 1u;
seg.fs = 1.0e6;
seg.num_samples = NS;
wfm_compose_state_t *c = 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 = wfm_compose_execute (c, out + n, NS - n)) > 0)
n += got_n;
wfm_compose_destroy (c);
/* The samples carry the DESCRIPTION's bits. `wfm_frame_assemble` builds
them independently; 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 (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. 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 wfm_frame_check reports both stages reversed.
The flags are sugar for exactly this¶
wfm_source_describe_frame() is the one place every consumer funnels
through, whichever way a source spelled its frame. Ask a flag-spelled source
for its description, hand that description to a second source, and the two
compose byte-identically — which is what makes "the flat fields are
sugar" 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 cycling to fill a three-frame
record, proves the sugar 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
Scenes: a frame the caller built
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.
- 5-Burst DSSS Link — the flag-spelled frame, through every wfmgen production path.