Skip to content

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.

enum u8_to_f32_mode_t {
    U8_TO_F32_SHIFT = 0,
    U8_TO_F32_MIDPOINT = 1
};

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.

dp_u8_to_f32_state_t * dp_u8_to_f32_create (
    int mode
) 

Parameters:

  • mode U8_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.

void dp_u8_to_f32_destroy (
    dp_u8_to_f32_state_t * state
) 

Parameters:

  • state May be NULL.

function dp_u8_to_f32_reset

No-op reset, provided only for lifecycle symmetry.

void dp_u8_to_f32_reset (
    dp_u8_to_f32_state_t * state
) 

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:

  • state Must 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.

JM_FORCEINLINE  JM_HOT float dp_u8_to_f32_step (
    const dp_u8_to_f32_state_t * state,
    uint8_t x
) 

Dispatches on the mode chosen at construction. For a block, steps() resolves the mode once and runs a branch-free loop instead.

Parameters:

  • state Must be non-NULL.
  • x Offset-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:

  • state Must be non-NULL.
  • input Input uint8 array; must contain at least n elements.
  • output Output float32 array; must contain at least n elements.
  • n Number 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) .

JM_FORCEINLINE float u8_to_f32_midpoint (
    const dp_u8_to_f32_state_t * state,
    uint8_t x
) 

The one definition of the unbiased path; step() and steps() both call it.

Parameters:

  • state Must be non-NULL (supplies the pre-computed reciprocal).
  • x Offset-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.

JM_FORCEINLINE float u8_to_f32_shift (
    uint8_t x
) 

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:

  • x Offset-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