File doppler_channel_core.h¶
FileList > doppler_channel > doppler_channel_core.h
Go to the source code of this file
Clock Doppler as a propagation impairment: dilate the time base and shift the carrier, coherently, from one physical parameter. More...
#include "clib_common.h"#include "dp_state.h"#include "jm_perf.h"#include "resamp/resamp_core.h"
Classes¶
| Type | Name |
|---|---|
| struct | doppler_channel_state_t DopplerChannel state. |
Public Functions¶
| Type | Name |
|---|---|
| doppler_channel_state_t * | doppler_channel_create (double fs, double carrier_hz, double doppler_ppm, double doppler_rate_ppm_s) Create a doppler_channel instance. |
| void | doppler_channel_destroy (doppler_channel_state_t * state) Destroy a doppler_channel instance and release all memory. |
| size_t | doppler_channel_execute (doppler_channel_state_t * state, const float _Complex * x, size_t x_len, float _Complex * out, size_t max_out) Apply clock Doppler to a block of complex baseband. |
| size_t | doppler_channel_execute_max_out (doppler_channel_state_t * state) Upper bound on the output of one execute() call. |
| double | doppler_channel_get_delay_samples (const doppler_channel_state_t * state) The resampler's group delay, in samples (10.5 for the built-in bank). |
| double | doppler_channel_get_elapsed_s (const doppler_channel_state_t * state) Receive time in seconds produced so far ( n_out/fs ). |
| double | doppler_channel_get_offset_hz (const doppler_channel_state_t * state) Instantaneous carrier offset fc*d(t) in Hz atelapsed_s . |
| void | doppler_channel_get_state (const doppler_channel_state_t * state, void * blob) Serialize the running state (both clocks + the resampler's). |
| void | doppler_channel_reset (doppler_channel_state_t * state) Reset DopplerChannel to its post-create state. |
| int | doppler_channel_set_state (doppler_channel_state_t * state, const void * blob) Restore a blob written by doppler_channel_get_state() . |
| size_t | doppler_channel_state_bytes (const doppler_channel_state_t * state) Bytes doppler_channel_get_state() writes (envelope + payload). |
Public Static Functions¶
| Type | Name |
|---|---|
| double | doppler_channel_excess (const doppler_channel_state_t * s, double t) Excess delay (seconds) accumulated by receive time t: __tau(t)-t . |
| double | doppler_channel_phase (const doppler_channel_state_t * s, double t) Carrier phase in CYCLES at receive time t: __fc * excess(t) . |
| double | doppler_channel_scale (const doppler_channel_state_t * s, double t) Instantaneous time-base scale 1 + d(t) at receive timet . |
Macros¶
| Type | Name |
|---|---|
| define | DOPPLER_CHANNEL_MAX_BLOCK 65536uLargest input block one doppler_channel_execute() call accepts. |
| define | DOPPLER_CHANNEL_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc)('D', 'P', 'C', 'H')State-blob magic ('DPCH') and layout version. |
| define | DOPPLER_CHANNEL_STATE_VERSION 1u |
Detailed Description¶
A real Doppler shift is not a frequency offset. Relative motion rescales the whole received time base, so every clock in the signal changes together — carrier, chip rate, symbol rate, frame rate. Modelling only the carrier is the classic unphysical shortcut, and it silently hides the code-rate error that a receiver's delay-lock loop exists to track.
This object takes any complex baseband stream and applies both halves of the effect from a single parameter, so they cannot disagree:
- Time-base dilation — the input is resampled at output/input ratio
1/(1+d), which makes a stream carryingRcchips/s andRssymbols/s come out atRc(1+d)andRs(1+d). One resampling on the whole stream, rather than a per-clock adjustment, is what keeps the clocks consistent. - Carrier offset — multiplication by
exp(j2*pi*fc*excess(t)), whose instantaneous frequency isfc*d(t).
Doppler is specified in ppm of the nominal time base, which makes it carrier-frequency agnostic: 20 ppm is +50 kHz at 2.5 GHz and +61.4 chip/s at 3.069 Mcps at the same time, and no caller converts between the two by hand. doppler_rate_ppm_s ramps it linearly for a pass-like geometry (0.2 ppm/s is 500 Hz/s at 2.5 GHz).
**carrier_hz is load-bearing, not metadata.** It is the only thing that converts a dimensionless ppm into a carrier offset in Hz. Leave it 0 and the clocks still dilate correctly but the carrier never moves — a physically inconsistent capture whose code rate runs fast while its carrier sits exactly on frequency. That combination is occasionally useful for isolating a code loop under test, so it is permitted rather than rejected, but it is not what a real channel does.
The dilation is resamp_execute_ctrl (see resamp_core.h), whose per-sample rate deviation tracks the ramp exactly instead of approximating it with a piecewise-constant ratio re-set once per block. No resampling math is implemented here.
The output is delayed by the resampler's group delay doppler_channel_get_delay_samples(), 10.5 samples for the built-in bank on top of the dilation. Output sample k, at receive time t = k/fs, carries the input at time t + excess(t) - delay/fs. A receiver started at the INPUT's phase is that far from the peak: at two samples per chip, five chips, outside a DLL's pull-in and onto a Gold code's sidelobe (doppler-dsp/doppler#1189). Subtract it, or start the loop there.
Lifecycle: create -> [execute / reset]* -> destroy
Example — a 2.5 GHz carrier seen at +20 ppm, ramping at 0.2 ppm/s:
doppler_channel_state_t *ch =
doppler_channel_create (6.138e6, 2.5e9, 20.0, 0.2);
size_t cap = doppler_channel_execute_max_out (ch);
float _Complex *out = malloc (cap * sizeof *out);
size_t n = doppler_channel_execute (ch, in, 65536, out, cap);
// n ~= 65536/(1+20e-6); doppler_channel_get_offset_hz (ch) ~= 50000.0
free (out);
doppler_channel_destroy (ch);
Public Functions Documentation¶
function doppler_channel_create¶
Create a doppler_channel instance.
doppler_channel_state_t * doppler_channel_create (
double fs,
double carrier_hz,
double doppler_ppm,
double doppler_rate_ppm_s
)
Parameters:
fsReceive sample rate in Hz (> 0).carrier_hzRF carrier in Hz (>= 0). Load-bearing: converts ppm into a carrier offset. 0 dilates the clocks but never moves the carrier (default: 0.0).doppler_ppmDoppler d0 in ppm of the nominal time base; positive is a closing range — clocks run fast, carrier shifts up (default: 0.0).doppler_rate_ppm_sLinear ramp of d in ppm per second (default: 0.0).
Returns:
Heap-allocated state, or NULL on allocation failure or fs <= 0.
Note:
Caller must call doppler_channel_destroy() when done.
function doppler_channel_destroy¶
Destroy a doppler_channel instance and release all memory.
Parameters:
stateMay be NULL.
function doppler_channel_execute¶
Apply clock Doppler to a block of complex baseband.
size_t doppler_channel_execute (
doppler_channel_state_t * state,
const float _Complex * x,
size_t x_len,
float _Complex * out,
size_t max_out
)
Resamples x by 1/(1+d(t)) and multiplies the result by the coherent carrier exp(j*2*pi*fc*excess(t)). State persists across calls, so feeding a stream in blocks gives the same samples as one large call (subject to DOPPLER_CHANNEL_MAX_BLOCK).
Output length is approximately x_len/(1+d) and varies by a sample from call to call as the fractional resampling accumulator crosses — that variation is the dilation itself, not a defect.
Parameters:
stateMust be non-NULL.xInput block.x_lenInput length in samples.outOutput buffer.max_outCapacity ofout; production stops there.
Returns:
Samples written to out.
>>> import numpy as np
>>> from doppler.impairment import DopplerChannel
>>> ch = DopplerChannel(fs=1e6, carrier_hz=2.5e9, doppler_ppm=20.0)
>>> y = ch.execute(np.ones(1000, dtype=np.complex64))
>>> y.shape # 20 ppm is 0.02 samples over this block
(1000,)
>>> round(ch.offset_hz, 1) # fc * d = 2.5e9 * 20e-6, in Hz
50000.0
function doppler_channel_execute_max_out¶
Upper bound on the output of one execute() call.
Assumes an input of at most DOPPLER_CHANNEL_MAX_BLOCK samples — see that macro for why the bound cannot depend on the actual input length.
function doppler_channel_get_delay_samples¶
The resampler's group delay, in samples (10.5 for the built-in bank).
Constant, and in addition to the dilation: output k at receive time t = k/fs carries the input at t + excess(t) - delay/fs (doppler_channel_excess()). Input and receive samples differ by the ppm of Doppler, so the unit is either to that precision. resamp_get_delay() for the derivation.
function doppler_channel_get_elapsed_s¶
Receive time in seconds produced so far ( n_out/fs ).
function doppler_channel_get_offset_hz¶
Instantaneous carrier offset fc*d(t) in Hz atelapsed_s .
function doppler_channel_get_state¶
Serialize the running state (both clocks + the resampler's).
function doppler_channel_reset¶
Reset DopplerChannel to its post-create state.
Zeroes both sample clocks (so elapsed_s and the carrier phase restart at zero) and clears the resampler's delay line and fractional accumulator. The configured fs/carrier_hz/doppler_ppm/doppler_rate_ppm_s are kept.
Parameters:
stateMust be non-NULL.>>> import numpy as np >>> from doppler.impairment import DopplerChannel >>> ch = DopplerChannel(fs=1e6, carrier_hz=2.5e9, doppler_ppm=20.0) >>> _ = ch.execute(np.ones(1000, dtype=np.complex64)) >>> round(ch.elapsed_s, 6) # receive time consumed: 1000 / 1e6 0.001 >>> ch.reset() # both sample clocks back to zero >>> ch.elapsed_s 0.0
function doppler_channel_set_state¶
Restore a blob written by doppler_channel_get_state() .
Returns:
DP_OK, or DP_ERR_INVALID if the envelope or a child blob is rejected.
function doppler_channel_state_bytes¶
Bytes doppler_channel_get_state() writes (envelope + payload).
Public Static Functions Documentation¶
function doppler_channel_excess¶
Excess delay (seconds) accumulated by receive time t: __tau(t)-t .
The one place the dilation integral is evaluated; the scale and the carrier phase are both derived from it, so the clocks cannot drift apart — there is only ever one number to drift.
tau(t) = integral_0^t (1 + d(u)) du, d(u) = (d0 + d_dot*u) * 1e-6 tau(t) - t = d0*t + 0.5*d_dot*t^2
Note this is the integral, not t*d(t): the latter double-counts the ramp and puts the instantaneous offset at fc*(d0 + 2*d_dot*t), exactly twice the intended Doppler rate.
Parameters:
schannel state.treceive time in seconds (>= 0).
Returns:
Excess delay in seconds (negative for an opening-range geometry).
function doppler_channel_phase¶
Carrier phase in CYCLES at receive time t: __fc * excess(t) .
Its derivative is fc * d(t), the instantaneous offset — so the carrier is driven by the same dilation the clocks are, not by a separately-specified frequency that could be set inconsistently with them.
Evaluated closed-form from t rather than accumulated per sample: an incremental accumulator would drift, and the closed form keeps a long capture phase-exact (a 1000 s run at 20 ppm on a 2.5 GHz carrier is ~5e7 cycles, ~1e-8 cycles of representation error in double).
Parameters:
schannel state.treceive time in seconds (>= 0).
Returns:
Phase in cycles; multiply by 2*pi for radians.
function doppler_channel_scale¶
Instantaneous time-base scale 1 + d(t) at receive timet .
The reciprocal of the resampler's output/input ratio: a stream resampled at 1/(1+d) comes out with every one of its clocks running (1+d) times faster.
Parameters:
schannel state.treceive time in seconds (>= 0).
Returns:
1 + d(t), a number very close to 1.
Macro Definition Documentation¶
define DOPPLER_CHANNEL_MAX_BLOCK¶
Largest input block one doppler_channel_execute() call accepts.
doppler_channel_execute_max_out() reports a bound for the output buffer, and the generated Python binding sizes its buffer from that alone — it never sees the input length. So the bound has to assume a worst-case input, and this is that assumption (the same convention, and the same value, as RateConverter_execute_max_out). Longer inputs are processed up to the caller's max_out and the remainder is not consumed; feed large streams in blocks of at most this many samples.
define DOPPLER_CHANNEL_STATE_MAGIC¶
State-blob magic ('DPCH') and layout version.
define DOPPLER_CHANNEL_STATE_VERSION¶
The documentation for this class was generated from the following file native/inc/doppler_channel/doppler_channel_core.h