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 for state (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) >> 2u) /\* 16384 (π/2 phase shift) \*/ |
| define | LO_LUT_SIZE (1u << [**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).
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).
Parameters:
norm_freqNormalised frequency in cycles per sample. Any real value; only the fractional part matters.
Returns:
Heap-allocated state, or NULL on allocation failure.
function lo_destroy¶
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.
>>> 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.
>>> 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).
function lo_get_state¶
Serialize state's mutable state intoblob (>= lo_state_bytes).
function lo_init¶
Initialise an LO in place (no allocation).
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:
stateLO state to initialise in place. Must be non-NULL.norm_freqNormalised frequency in cycles per sample (fractional part only).
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.
>>> 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¶
function lo_set_phase¶
function lo_set_state¶
Restore mutable state from 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).
function lo_step¶
Emit the current CF32 phasor, then advance the accumulator.
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:
stateLO 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.
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:
stateLO state. Must be non-NULL with phase/phase_inc set.ctrlFrequency 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.
Parameters:
stateLO state returned by lo_create().nNumber of phasors to generate.outOutput 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:
stateLO state returned by lo_create().ctrlFloat32 array of per-sample normalised-frequency deviations. Only the fractional part of each element contributes.ctrl_lenNumber of elements in ctrl; equals output length.outOutput 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¶
function lo_steps_max_out¶
Maximum samples per call (determines pre-allocated buffer size).
Macro Definition Documentation¶
define LO_LUT_BITS¶
define LO_LUT_QTR¶
define LO_LUT_SIZE¶
define LO_STATE_MAGIC¶
define LO_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/lo/lo_core.h