File u8_to_f32_core.h¶
FileList > doppler > u8_to_f32 > u8_to_f32_core.h
Go to the source code of this file
Offset-binary uint8 to float converter — the RTL-SDR cu8 front end.More...
#include "doppler/clib_common.h"#include "doppler/jm_perf.h"
Classes¶
| Type | Name |
|---|---|
| struct | dp_u8_to_f32_state_t U8ToF32 state. |
Public Types¶
| Type | Name |
|---|---|
| enum | u8_to_f32_mode_t The two mappings, in the order of the Python mode string enum. |
Public Functions¶
| Type | Name |
|---|---|
| dp_u8_to_f32_state_t * | dp_u8_to_f32_create (int mode) Create a u8_to_f32 instance. |
| void | dp_u8_to_f32_destroy (dp_u8_to_f32_state_t * state) Destroy a u8_to_f32 instance and release all memory. |
| void | dp_u8_to_f32_reset (dp_u8_to_f32_state_t * state) No-op reset, provided only for lifecycle symmetry. |
| JM_FORCEINLINE JM_HOT float | dp_u8_to_f32_step (const dp_u8_to_f32_state_t * state, uint8_t x) Convert one offset-binary code to a normalised float. |
| void | dp_u8_to_f32_steps (dp_u8_to_f32_state_t * state, const uint8_t * input, float * output, size_t n) Convert a block of offset-binary codes to float32. |
| JM_FORCEINLINE float | u8_to_f32_midpoint (const dp_u8_to_f32_state_t * state, uint8_t x) The midpoint mapping of one code:(x - 127.5) * (1/127.5) . |
| JM_FORCEINLINE float | u8_to_f32_shift (uint8_t x) The shift mapping of one code:(x - 128) * 2^-7 , exactly. |
Detailed Description¶
An RTL-SDR (RTL2832U) streams interleaved I/Q as UNSIGNED 8-bit offset-binary samples, cu8: code 0 is the most negative level, 255 the most positive, and the analog zero sits between codes 127 and 128, at 127.5. This is not the signed-8 format of I8ToF32 (HackRF's cs8) — read as signed, every sample above 127 wraps to a large negative value.
The converter works element by element on the FLAT interleaved buffer, so 2N bytes of I,Q,I,Q,... become 2N floats, which is N complex samples in float _Complex layout. From C, pass the complex output buffer cast to float *; from Python, .view(np.complex64) the result.
Two mappings, chosen at construction by mode:
| mode | formula | range | cost |
|---|---|---|---|
shift |
(x - 128) * 2^-7 |
[-1, 127/128] |
integer subtract, convert, power-of-two scale |
midpoint |
(x - 127.5) * (1/127.5) |
[-1, +1] |
float subtract and multiply |
**shift (the default) is exact and the fast path.** Every step is exact in float: the integer subtract, the conversion of a value in [-128, 127], and the power-of-two scale. Both modes vectorise (GCC 14 on AArch64 emits NEON scvtf + fmul on four lanes; it does not fold the 2^-7 into the convert's fractional-bits operand, though the instruction has one), so the gap between them is small — 4% on a Cortex-A53-class core, 20% on a desktop x86 (bench_u8_to_f32_core). It is identical, bit for bit, to I8ToF32 at scale=128 applied to x ^ 0x80. The price is a DC bias: it centres on code 128 while the hardware centres on 127.5, so every sample reads 0.5/128 low — an analog zero dithers between codes 127 and 128, which map to -1/128 and 0 — a constant at about -48 dBFS. In a receiver that tunes the wanted signal off DC — the normal way to use an RTL-SDR, whose own DC spike sits at the same place — the downstream down-converter's channel filter removes it, so the fast path costs nothing that survives.
**midpoint is the opt-in unbiased mapping**: centred on 127.5 and symmetric, so a zero-mean input stays zero-mean and both rails map to exactly -1 and +1. Use it when DC matters: a zero-IF capture, a power measurement taken before any filtering, or a spectrum whose DC bin you intend to read. The scale is a pre-computed reciprocal, so a result can differ from the true quotient (x - 127.5)/127.5 in the last bit.
Stateless: nothing survives between calls, so there is no state to serialize and reset() is a no-op.
Lifecycle: create -> [step / steps / reset]* -> destroy
>>> from doppler.cvt import U8ToF32
>>> import numpy as np
>>> iq = np.array([0, 128, 255, 128], dtype=np.uint8) # I,Q,I,Q
>>> U8ToF32().steps(iq).tolist() # mode="shift"
[-1.0, 0.0, 0.9921875, 0.0]
>>> U8ToF32().steps(iq).view(np.complex64).tolist()
[(-1+0j), (0.9921875+0j)]
>>> U8ToF32(mode="midpoint").steps(iq)[[0, 2]].tolist() # both rails
[-1.0, 1.0]
Public Types Documentation¶
enum u8_to_f32_mode_t¶
The two mappings, in the order of the Python mode string enum.
The Python binding passes the index of "shift" / "midpoint", so these values ARE that order; a C caller may use either spelling.
Public Functions Documentation¶
function dp_u8_to_f32_create¶
Create a u8_to_f32 instance.
Parameters:
modeU8_TO_F32_SHIFT (0) or U8_TO_F32_MIDPOINT (1); from Python, the string"shift"(default) or"midpoint".
Returns:
Heap-allocated state, or NULL for an unknown mode. The allocation itself cannot fail visibly: it aborts on out-of-memory (dp_xcalloc), as every fixed-size internal allocation does.
Note:
Caller must call dp_u8_to_f32_destroy() when done.
function dp_u8_to_f32_destroy¶
Destroy a u8_to_f32 instance and release all memory.
Parameters:
stateMay be NULL.
function dp_u8_to_f32_reset¶
No-op reset, provided only for lifecycle symmetry.
The mode and its reciprocal are fixed at construction and nothing else is held, so there is nothing to clear; the method exists so every converter in the module presents the same create / step / reset / destroy lifecycle.
Parameters:
stateMust be non-NULL.
>>> from doppler.cvt import U8ToF32
>>> c = U8ToF32()
>>> c.reset() # stateless converter -> reset is a no-op
>>> c.step(0)
-1.0
function dp_u8_to_f32_step¶
Convert one offset-binary code to a normalised float.
Dispatches on the mode chosen at construction. For a block, steps() resolves the mode once and runs a branch-free loop instead.
Parameters:
stateMust be non-NULL.xOffset-binary code in[0, 255].
Returns:
The mapped sample (see the table at the top of this file).
>>> from doppler.cvt import U8ToF32
>>> c = U8ToF32() # mode="shift": (x - 128) / 128, exact
>>> c.step(0), c.step(128), c.step(192)
(-1.0, 0.0, 0.5)
>>> m = U8ToF32(mode="midpoint")
>>> m.step(0), m.step(255) # symmetric: both rails reach full scale
(-1.0, 1.0)
function dp_u8_to_f32_steps¶
Convert a block of offset-binary codes to float32.
void dp_u8_to_f32_steps (
dp_u8_to_f32_state_t * state,
const uint8_t * input,
float * output,
size_t n
)
The mode is resolved once for the block, then one branch-free loop runs, so the per-sample work is only the mapping itself. Feed it the flat interleaved I/Q buffer; the output is then complex samples in float _Complex layout.
Parameters:
stateMust be non-NULL.inputInput uint8 array; must contain at leastnelements.outputOutput float32 array; must contain at leastnelements.nNumber of samples (bytes) to convert.
>>> from doppler.cvt import U8ToF32
>>> import numpy as np
>>> cu8 = np.array([128, 0, 192, 64], dtype=np.uint8) # 2 I/Q pairs
>>> U8ToF32().steps(cu8).view(np.complex64).tolist()
[-1j, (0.5-0.5j)]
function u8_to_f32_midpoint¶
The midpoint mapping of one code:(x - 127.5) * (1/127.5) .
The one definition of the unbiased path; step() and steps() both call it.
Parameters:
stateMust be non-NULL (supplies the pre-computed reciprocal).xOffset-binary code in[0, 255].
Returns:
(x - 127.5) / 127.5 to within the last bit, in [-1, +1].
function u8_to_f32_shift¶
The shift mapping of one code:(x - 128) * 2^-7 , exactly.
The one definition of the fast path; step() and steps() both call it. The subtract is done in int32_t rather than by flipping the top bit into an int8_t, which gives the same value without C's implementation-defined narrowing conversion.
Parameters:
xOffset-binary code in[0, 255].
Returns:
(x - 128) / 128, in [-1, 127/128].
The documentation for this class was generated from the following file native/inc/doppler/u8_to_f32/u8_to_f32_core.h