Skip to content

File lo_core.h

FileList > inc > lo > lo_core.h

Go to the source code of this file

Local oscillator: NCO + 2^16 sin/cos LUT → CF32 phasors. More...

  • #include "clib_common.h"
  • #include "dp_state.h"
  • #include "jm_perf.h"
  • #include <math.h>

Classes

Type Name
struct lo_state_t
LO state.

Public Attributes

Type Name
float lo_sin_lut
Shared 2^16-entry sine LUT (read-only after init).

Public Functions

Type Name
lo_state_t * lo_create (double norm_freq)
Create an LO instance. Allocates state, sets phase to 0, and derives phase_inc from norm_freq. Initialises the shared 65536-entry float LUT on the first call (single-threaded concern: call lo_create() before spawning threads that share LO instances).
void lo_destroy (lo_state_t * state)
double lo_get_norm_freq (const lo_state_t * state)
Normalised frequency (read/write). Setting norm_freq recomputes phase_inc = floor(frac(v) × 2^32) and takes effect on the next lo_steps call; phase is NOT reset.
uint32_t lo_get_phase (const lo_state_t * state)
Current phase accumulator value (read/write). Returns the current integer phase in [0, 2^32) . Writing overrides the accumulator directly for phase-coherent frequency switching.
uint32_t lo_get_phase_inc (const lo_state_t * state)
Per-sample phase increment (read-only). Derived from norm_freq as floor(frac(norm_freq) × 2^32). A freq of 0.25 gives phase_inc = 1073741824 (0x40000000).
void lo_get_state (const lo_state_t * state, void * blob)
Serialize state's mutable state intoblob (>= lo_state_bytes).
void lo_init (lo_state_t * state, double norm_freq)
Initialise an LO in place (no allocation).
void lo_reset (lo_state_t * state)
Zero the phase accumulator. Sets phase to 0 so the next lo_steps call starts at angle 0 (1+0j). norm_freq and phase_inc are unchanged.
void lo_set_norm_freq (lo_state_t * state, double norm_freq)
void lo_set_phase (lo_state_t * state, uint32_t phase)
int lo_set_state (lo_state_t * state, const void * blob)
Restore mutable state from blob .
size_t lo_state_bytes (const lo_state_t * state)
Bytes lo_get_state() writes forstate (envelope + payload).
JM_FORCEINLINE JM_HOT float complex lo_step (lo_state_t * state)
Emit the current CF32 phasor, then advance the accumulator.
JM_FORCEINLINE JM_HOT float complex lo_step_ctrl (lo_state_t * state, double ctrl)
Emit the current CF32 phasor, then advance by phase_inc + control.
size_t lo_steps (lo_state_t * state, size_t n, float complex * out)
Generate n CF32 phasors at the current norm_freq. Each sample is cos(θ) + j·sin(θ) where θ is the phase BEFORE the accumulator is advanced, giving a unit-magnitude complex sinusoid via the 65536-entry LUT. SFDR ≈ 96 dBc. Returns n.
size_t lo_steps_ctrl (lo_state_t * state, const float * ctrl, size_t ctrl_len, float complex * out)
Generate CF32 phasors with per-sample FM deviation. For each sample i, ctrl[i] 's fractional part is converted to a delta phase-increment (delta = floor(frac(ctrl[i] ) × 2^32)) that is added on top of the base phase_inc for that one step only. The base norm_freq and phase_inc are NOT modified; the deviation is transient per sample, making this the natural API for FM synthesis and frequency-hopping. Output length equals ctrl_len. Returns ctrl_len.
size_t lo_steps_ctrl_max_out (lo_state_t * state)
size_t lo_steps_max_out (lo_state_t * state)
Maximum samples per call (determines pre-allocated buffer size).

Macros

Type Name
define LO_LUT_BITS 16u
define LO_LUT_QTR ([**LO\_LUT\_SIZE**](lo__core_8h.md#define-lo_lut_size) &gt;&gt; 2u) /\* 16384 (π/2 phase shift) \*/
define LO_LUT_SIZE (1u &lt;&lt; [**LO\_LUT\_BITS**](lo__core_8h.md#define-lo_lut_bits)) /\* 65536 \*/
define LO_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('L', 'O', '\_', '\_')
define LO_STATE_VERSION 1u

Detailed Description

Wraps the integer NCO in a CF32 phasor generator. The 32-bit phase accumulator drives a static 65536-entry float sine LUT; the top 16 bits of the phase select the LUT index, and a quarter-cycle offset (LUT_QTR = 16384) converts sin to cos without extra storage:

idx = phase >> 16 out(i) = cos(θ) + j·sin(θ) = lut((idx + LUT_QTR) & 0xFFFF) + j·lut(idx)

Output is emitted BEFORE the phase is incremented (same convention as NCO). The 16-bit phase truncation gives ~96 dBc SFDR.

The shared LUT is initialised lazily on the first lo_create() call.

Lifecycle: lo_create → (steps / steps_ctrl / reset)* → lo_destroy

lo_state_t *lo = lo_create(0.25);
float complex out[4];
lo_steps(lo, 4, out);
// out ≈ { 1+0j, 0+1j, -1+0j, 0-1j }
lo_destroy(lo);

Public Attributes Documentation

variable lo_sin_lut

Shared 2^16-entry sine LUT (read-only after init).

float lo_sin_lut[LO_LUT_SIZE];

Filled by the first lo_create()/lo_init(). Indexed by the top 16 bits of the phase accumulator; the quarter-cycle offset LO_LUT_QTR maps sin→cos. Do not write. Exposed only so lo_step() can be a header inline.


Public Functions Documentation

function lo_create

Create an LO instance. Allocates state, sets phase to 0, and derives phase_inc from norm_freq. Initialises the shared 65536-entry float LUT on the first call (single-threaded concern: call lo_create() before spawning threads that share LO instances).

lo_state_t * lo_create (
    double norm_freq
) 

Parameters:

  • norm_freq Normalised frequency in cycles per sample. Any real value; only the fractional part matters.

Returns:

Heap-allocated state, or NULL on allocation failure.

>>> from doppler.source import LO
>>> lo = LO(norm_freq=0.25)
>>> lo.phase_inc
1073741824


function lo_destroy

void lo_destroy (
    lo_state_t * state
) 

Free all resources. May be NULL (no-op).


function lo_get_norm_freq

Normalised frequency (read/write). Setting norm_freq recomputes phase_inc = floor(frac(v) × 2^32) and takes effect on the next lo_steps call; phase is NOT reset.

double lo_get_norm_freq (
    const lo_state_t * state
) 

>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> lo.norm_freq
0.25
>>> lo.norm_freq = 0.5
>>> lo.phase_inc
2147483648

function lo_get_phase

Current phase accumulator value (read/write). Returns the current integer phase in [0, 2^32) . Writing overrides the accumulator directly for phase-coherent frequency switching.

uint32_t lo_get_phase (
    const lo_state_t * state
) 

>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> lo.phase
0
>>> lo.phase = 1073741824
>>> lo.phase
1073741824

function lo_get_phase_inc

Per-sample phase increment (read-only). Derived from norm_freq as floor(frac(norm_freq) × 2^32). A freq of 0.25 gives phase_inc = 1073741824 (0x40000000).

uint32_t lo_get_phase_inc (
    const lo_state_t * state
) 

>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> lo.phase_inc
1073741824

function lo_get_state

Serialize state's mutable state intoblob (>= lo_state_bytes).

void lo_get_state (
    const lo_state_t * state,
    void * blob
) 


function lo_init

Initialise an LO in place (no allocation).

void lo_init (
    lo_state_t * state,
    double norm_freq
) 

The by-value counterpart to lo_create(): a tracking loop that embeds an lo_state_t initialises it with lo_init() instead of owning a heap pointer. Sets phase=0, derives phase_inc from norm_freq, and fills the shared LUT on first use (same single-threaded caveat as lo_create()).

Parameters:

  • state LO state to initialise in place. Must be non-NULL.
  • norm_freq Normalised frequency in cycles per sample (fractional part only).
    >>> from doppler.source import LO
    >>> lo = LO(0.25)            # the Python type uses lo_create internally
    >>> lo.phase_inc
    1073741824
    

function lo_reset

Zero the phase accumulator. Sets phase to 0 so the next lo_steps call starts at angle 0 (1+0j). norm_freq and phase_inc are unchanged.

void lo_reset (
    lo_state_t * state
) 

>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> _ = lo.steps(2)
>>> lo.phase
2147483648
>>> lo.reset()
>>> lo.phase
0
>>> lo.norm_freq
0.25

function lo_set_norm_freq

void lo_set_norm_freq (
    lo_state_t * state,
    double norm_freq
) 

function lo_set_phase

void lo_set_phase (
    lo_state_t * state,
    uint32_t phase
) 

function lo_set_state

Restore mutable state from blob .

int lo_set_state (
    lo_state_t * state,
    const void * blob
) 

Returns:

DP_OK, or DP_ERR_INVALID if the blob's envelope rejects.


function lo_state_bytes

Bytes lo_get_state() writes forstate (envelope + payload).

size_t lo_state_bytes (
    const lo_state_t * state
) 


function lo_step

Emit the current CF32 phasor, then advance the accumulator.

JM_FORCEINLINE  JM_HOT float complex lo_step (
    lo_state_t * state
) 

Single-sample form of lo_steps(), same emit-before-increment convention and bit-for-bit the same LUT math, suitable for inlining into a sample-by-sample loop (e.g. carrier wipe-off ahead of a matched filter). The caller must have run lo_create()/lo_init() so the LUT is populated.

Parameters:

  • state LO state. Must be non-NULL with phase/phase_inc set.

Returns:

cos(θ) + j·sin(θ) at the phase BEFORE the increment.

lo_state_t lo;            // embedded by value, no heap
lo_init (&lo, 0.25);
float complex s0 = lo_step (&lo);   // 1 + 0j
float complex s1 = lo_step (&lo);   // 0 + 1j


function lo_step_ctrl

Emit the current CF32 phasor, then advance by phase_inc + control.

JM_FORCEINLINE  JM_HOT float complex lo_step_ctrl (
    lo_state_t * state,
    double ctrl
) 

The NCO control port for a tracking loop: ctrl is a per-sample frequency control in normalized cycles/sample, added on top of the centre increment phase_inc for this step only (not persisted — the loop filter holds the integrator and supplies its full output as ctrl each sample). The LO owns the cycles→phase scaling, so the loop never touches the integer phase accumulator. Same emit-before-increment convention as lo_step(); with ctrl == 0 it is bit-identical to lo_step().

Parameters:

  • state LO state. Must be non-NULL with phase/phase_inc set.
  • ctrl Frequency control, normalized cycles/sample (any sign; the fractional cycle is taken, so it wraps correctly).

Returns:

cos(θ) + j·sin(θ) at the phase BEFORE the increment.

lo_state_t lo;
lo_init (&lo, 0.0);                 // centre at DC
float complex s = lo_step_ctrl (&lo, 0.01);  // step at +0.01 cyc/sample


function lo_steps

Generate n CF32 phasors at the current norm_freq. Each sample is cos(θ) + j·sin(θ) where θ is the phase BEFORE the accumulator is advanced, giving a unit-magnitude complex sinusoid via the 65536-entry LUT. SFDR ≈ 96 dBc. Returns n.

size_t lo_steps (
    lo_state_t * state,
    size_t n,
    float complex * out
) 

Parameters:

  • state LO state returned by lo_create().
  • n Number of phasors to generate.
  • out Output buffer; must hold at least n float complex values.

Returns:

n (always).

>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> out = lo.steps(4)
>>> out.dtype
dtype('complex64')
>>> out.shape
(4,)
>>> [round(float(abs(c)), 4) for c in out]
[1.0, 1.0, 1.0, 1.0]


function lo_steps_ctrl

Generate CF32 phasors with per-sample FM deviation. For each sample i, ctrl[i] 's fractional part is converted to a delta phase-increment (delta = floor(frac(ctrl[i] ) × 2^32)) that is added on top of the base phase_inc for that one step only. The base norm_freq and phase_inc are NOT modified; the deviation is transient per sample, making this the natural API for FM synthesis and frequency-hopping. Output length equals ctrl_len. Returns ctrl_len.

size_t lo_steps_ctrl (
    lo_state_t * state,
    const float * ctrl,
    size_t ctrl_len,
    float complex * out
) 

Parameters:

  • state LO state returned by lo_create().
  • ctrl Float32 array of per-sample normalised-frequency deviations. Only the fractional part of each element contributes.
  • ctrl_len Number of elements in ctrl; equals output length.
  • out Output buffer; must hold at least ctrl_len float complex values.

Returns:

ctrl_len (always).

>>> import numpy as np
>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> ctrl = np.zeros(4, dtype=np.float32)
>>> out = lo.steps_ctrl(ctrl)
>>> out.dtype
dtype('complex64')
>>> out.shape
(4,)
>>> [round(float(abs(c)), 4) for c in out]
[1.0, 1.0, 1.0, 1.0]


function lo_steps_ctrl_max_out

size_t lo_steps_ctrl_max_out (
    lo_state_t * state
) 

function lo_steps_max_out

Maximum samples per call (determines pre-allocated buffer size).

size_t lo_steps_max_out (
    lo_state_t * state
) 


Macro Definition Documentation

define LO_LUT_BITS

#define LO_LUT_BITS `16u`

define LO_LUT_QTR

#define LO_LUT_QTR `( LO_LUT_SIZE >> 2u)  /* 16384  (π/2 phase shift) */`

define LO_LUT_SIZE

#define LO_LUT_SIZE `(1u << LO_LUT_BITS ) /* 65536                    */`

define LO_STATE_MAGIC

#define LO_STATE_MAGIC `DP_FOURCC ('L', 'O', '_', '_')`

define LO_STATE_VERSION

#define LO_STATE_VERSION `1u`


The documentation for this class was generated from the following file native/inc/lo/lo_core.h