use super::bpsk::BpskDecider;
use super::qam::{Qam16Decider, Qam64Decider, Qam256Decider, QamDecider};
use super::qpsk::QpskDecider;
use crate::core::{Block, WorkReport};
use crate::modulate::qam::{axis_scale, build_axis_table};
use crate::modulate::{ConstellationOrder, OfdmConfig};
use crate::multicarrier::{CarrierGrid, GridExtract, SymbolFft};
use crate::sync::ofdm_sync::training_symbol_freq_pattern;
use num_complex::Complex32 as C32;
pub struct OfdmDemod {
samples_per_symbol: usize,
num_data_carriers: usize,
gain: f32,
symbol_fft: SymbolFft,
grid_extract: GridExtract,
}
impl OfdmDemod {
pub fn new(cfg: &OfdmConfig) -> Self {
let grid = CarrierGrid::from_plan(&cfg.carrier_plan);
let n_fft = cfg.carrier_plan.n_fft();
let cp_len = cfg.carrier_plan.cp_len();
let num_data_carriers = grid.num_data_carriers();
Self {
samples_per_symbol: cfg.samples_per_ofdm_symbol(),
num_data_carriers,
gain: 1.0,
symbol_fft: SymbolFft::new(n_fft, cp_len).with_window_backoff(cfg.rx_window_backoff),
grid_extract: GridExtract::new(grid),
}
}
pub fn set_gain(&mut self, g: f32) {
self.gain = g;
}
pub fn num_data_carriers(&self) -> usize {
self.num_data_carriers
}
pub fn samples_per_symbol(&self) -> usize {
self.samples_per_symbol
}
}
impl Block for OfdmDemod {
type In = C32;
type Out = C32;
fn process(&mut self, input: &[C32], output: &mut [C32]) -> WorkReport {
if input.len() < self.samples_per_symbol || output.len() < self.num_data_carriers {
return WorkReport::default();
}
let freq = match self
.symbol_fft
.demod_symbol(&input[..self.samples_per_symbol])
{
Some(f) => f,
None => return WorkReport::default(),
};
let grid_wr = self.grid_extract.process(freq, output);
debug_assert_eq!(grid_wr.out_written, self.num_data_carriers);
let g = self.gain;
if (g - 1.0).abs() > f32::EPSILON {
for s in output[..self.num_data_carriers].iter_mut() {
*s = C32::new(g * s.re, g * s.im);
}
}
WorkReport {
in_read: self.samples_per_symbol,
out_written: self.num_data_carriers,
}
}
}
enum DeciderKind {
Bpsk(BpskDecider),
Qpsk(QpskDecider),
Qam16(Qam16Decider),
Qam64(Qam64Decider),
Qam256(Qam256Decider),
}
impl DeciderKind {
fn new(order: ConstellationOrder) -> Self {
match order {
ConstellationOrder::Bpsk => DeciderKind::Bpsk(BpskDecider::new()),
ConstellationOrder::Qpsk => DeciderKind::Qpsk(QpskDecider::new()),
ConstellationOrder::Qam16 => DeciderKind::Qam16(QamDecider::new()),
ConstellationOrder::Qam64 => DeciderKind::Qam64(QamDecider::new()),
ConstellationOrder::Qam256 => DeciderKind::Qam256(QamDecider::new()),
}
}
#[inline(always)]
fn process(&mut self, input: &[C32], output: &mut [u8]) -> WorkReport {
match self {
DeciderKind::Bpsk(d) => d.process(input, output),
DeciderKind::Qpsk(d) => d.process(input, output),
DeciderKind::Qam16(d) => d.process(input, output),
DeciderKind::Qam64(d) => d.process(input, output),
DeciderKind::Qam256(d) => d.process(input, output),
}
}
}
pub struct OfdmDecider {
num_data_carriers: usize,
bits_per_ofdm_symbol: usize,
decider: DeciderKind,
}
impl OfdmDecider {
pub fn new(cfg: &OfdmConfig) -> Self {
Self {
num_data_carriers: cfg.carrier_plan.data_carriers().len(),
bits_per_ofdm_symbol: cfg.bits_per_ofdm_symbol(),
decider: DeciderKind::new(cfg.constellation),
}
}
}
impl Block for OfdmDecider {
type In = C32;
type Out = u8;
fn process(&mut self, input: &[C32], output: &mut [u8]) -> WorkReport {
if input.len() < self.num_data_carriers || output.len() < self.bits_per_ofdm_symbol {
return WorkReport::default();
}
self.decider.process(
&input[..self.num_data_carriers],
&mut output[..self.bits_per_ofdm_symbol],
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OfdmRxFrame {
pub bits: Vec<u8>,
pub num_symbols: usize,
pub evm_db: Option<f32>,
pub cfo_hz: Option<f32>,
pub timing_offset_samples: Option<i32>,
pub channel_mse: Option<f32>,
pub sync_score: Option<f32>,
pub channel_estimate: Option<Vec<C32>>,
pub inner_fec_ok: Option<bool>,
pub outer_fec_ok: Option<bool>,
pub channel_ber: Option<f32>,
pub inner_ber: Option<f32>,
}
pub fn build_ofdm_rx_frame(cfg: &OfdmConfig, soft_symbols: &[C32], bits: Vec<u8>) -> OfdmRxFrame {
let num_data_carriers = cfg.carrier_plan.data_carriers().len();
let num_symbols = soft_symbols
.len()
.checked_div(num_data_carriers)
.unwrap_or(0);
let evm_db = evm_db(cfg, soft_symbols, &bits, num_symbols);
OfdmRxFrame {
bits,
num_symbols,
evm_db,
cfo_hz: None,
timing_offset_samples: None,
channel_mse: None,
sync_score: None,
channel_estimate: None,
inner_fec_ok: None,
outer_fec_ok: None,
channel_ber: None,
inner_ber: None,
}
}
pub(crate) fn evm_db(
cfg: &OfdmConfig,
soft_symbols: &[C32],
bits: &[u8],
num_symbols: usize,
) -> Option<f32> {
if num_symbols == 0 || soft_symbols.is_empty() {
return None;
}
let mut mapper = crate::modulate::ofdm::ideal_symbol_mapper(cfg.constellation);
let mut ideal = vec![C32::default(); soft_symbols.len()];
let wr = mapper.process(bits, &mut ideal);
if wr.out_written != soft_symbols.len() {
return None;
}
let mut err_energy = 0.0f64;
let mut ref_energy = 0.0f64;
for (s, r) in soft_symbols.iter().zip(ideal.iter()) {
let e = s - r;
err_energy += (e.re * e.re + e.im * e.im) as f64;
ref_energy += (r.re * r.re + r.im * r.im) as f64;
}
if ref_energy <= 0.0 {
return None;
}
Some((10.0 * (err_energy / ref_energy).log10()) as f32)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EqualizerMethod {
#[default]
TrainingSymbolHold,
PerSymbolPilotInterp,
}
pub struct OfdmEqualizer {
method: EqualizerMethod,
n_fft: usize,
estimate: Vec<C32>,
weight: Vec<C32>,
default_layout: PilotLayout,
layouts: Vec<(usize, PilotLayout)>,
active_phase: Option<usize>,
}
struct PilotLayout {
pilot_bins: Vec<(usize, C32)>,
brackets: Vec<(usize, Bracket)>,
}
#[derive(Clone, Copy)]
struct Bracket {
left_bin: usize,
right_bin: usize,
t: f32,
}
impl PilotLayout {
fn build(pilots: &[(usize, C32)], data_bins: &[usize]) -> Self {
let mut pilot_bins = pilots.to_vec();
pilot_bins.sort_by_key(|&(bin, _)| bin);
let mut sorted_data = data_bins.to_vec();
sorted_data.sort_unstable();
let mut brackets = Vec::with_capacity(sorted_data.len());
if !pilot_bins.is_empty() {
let mut pi = 0usize;
for db in sorted_data {
while pi + 1 < pilot_bins.len() && pilot_bins[pi + 1].0 <= db {
pi += 1;
}
let lb = pilot_bins[pi].0;
let bracket = if lb >= db || pi + 1 == pilot_bins.len() {
Bracket {
left_bin: lb,
right_bin: lb,
t: 0.0,
}
} else {
let ub = pilot_bins[pi + 1].0;
let t = (db - lb) as f32 / (ub - lb) as f32;
Bracket {
left_bin: lb,
right_bin: ub,
t,
}
};
brackets.push((db, bracket));
}
}
Self {
pilot_bins,
brackets,
}
}
}
const EQUALIZER_FLOOR: f32 = 1e-6;
impl OfdmEqualizer {
pub fn new(cfg: &OfdmConfig, method: EqualizerMethod) -> Self {
let grid = CarrierGrid::from_plan(&cfg.carrier_plan);
let n_fft = cfg.carrier_plan.n_fft();
Self {
method,
n_fft,
estimate: vec![C32::new(1.0, 0.0); n_fft],
weight: vec![C32::new(1.0, 0.0); n_fft],
default_layout: PilotLayout::build(grid.pilot_bins(), grid.data_bins()),
layouts: Vec::new(),
active_phase: None,
}
}
pub fn method(&self) -> EqualizerMethod {
self.method
}
pub fn set_pilot_bins(&mut self, phase: usize, pilots: &[(usize, C32)], data_bins: &[usize]) {
if !self.layouts.iter().any(|&(p, _)| p == phase) {
self.layouts
.push((phase, PilotLayout::build(pilots, data_bins)));
}
self.active_phase = Some(phase);
}
pub fn estimate_from_training_symbol(&mut self, received_freq: &[C32]) {
if self.method != EqualizerMethod::TrainingSymbolHold || received_freq.len() < self.n_fft {
return;
}
let known = training_symbol_freq_pattern(self.n_fft);
for bin in 0..self.n_fft {
self.set_channel(bin, received_freq[bin] / known[bin]);
}
}
#[inline]
fn set_channel(&mut self, bin: usize, h: C32) {
self.weight[bin] = equalizer_weight(h);
}
fn interpolate_from_pilots(&mut self, received_freq: &[C32]) {
let layout = match self.active_phase {
Some(phase) => match self.layouts.iter().find(|&&(p, _)| p == phase) {
Some((_, layout)) => layout,
None => return,
},
None => &self.default_layout,
};
if layout.pilot_bins.is_empty() {
return;
}
for &(bin, known) in &layout.pilot_bins {
self.estimate[bin] = received_freq[bin] / known;
}
for &(data_bin, br) in &layout.brackets {
let l = self.estimate[br.left_bin];
let r = self.estimate[br.right_bin];
self.estimate[data_bin] = l + (r - l) * br.t;
}
}
}
#[inline(always)]
fn equalizer_weight(h: C32) -> C32 {
let mag_sq = h.norm_sqr();
if mag_sq >= EQUALIZER_FLOOR {
h.conj() / mag_sq
} else {
C32::default()
}
}
impl Block for OfdmEqualizer {
type In = C32;
type Out = C32;
fn process(&mut self, input: &[C32], output: &mut [C32]) -> WorkReport {
if input.len() < self.n_fft || output.len() < self.n_fft {
return WorkReport::default();
}
if self.method == EqualizerMethod::PerSymbolPilotInterp {
self.interpolate_from_pilots(&input[..self.n_fft]);
for bin in 0..self.n_fft {
output[bin] = input[bin] * equalizer_weight(self.estimate[bin]);
}
} else {
for bin in 0..self.n_fft {
output[bin] = input[bin] * self.weight[bin];
}
}
WorkReport {
in_read: self.n_fft,
out_written: self.n_fft,
}
}
}
#[inline]
pub fn bpsk_soft_llr(v: C32) -> f32 {
4.0 * v.re
}
#[inline]
pub fn qpsk_soft_llr(v: C32) -> [f32; 2] {
let scale = 4.0 * std::f32::consts::SQRT_2;
[scale * v.re, scale * v.im]
}
pub fn qam_axis_soft_llr<const BITS: usize>(v: f32, out: &mut [f32]) {
let k = BITS / 2;
let m = 1usize << k;
let table = build_axis_table(BITS, axis_scale(BITS));
for (b, slot) in out.iter_mut().enumerate().take(k) {
let bit_shift = k - 1 - b;
let mut d0_sq = f32::INFINITY;
let mut d1_sq = f32::INFINITY;
for (gray, &level) in table.iter().enumerate().take(m) {
let d_sq = (v - level) * (v - level);
if (gray >> bit_shift) & 1 == 0 {
d0_sq = d0_sq.min(d_sq);
} else {
d1_sq = d1_sq.min(d_sq);
}
}
*slot = d1_sq - d0_sq;
}
}
pub fn qam_soft_llr<const BITS: usize>(v: C32) -> [f32; 8] {
let k = BITS / 2;
let mut out = [0.0f32; 8];
qam_axis_soft_llr::<BITS>(v.re, &mut out[..k]);
qam_axis_soft_llr::<BITS>(v.im, &mut out[k..2 * k]);
out
}
enum SoftKind {
Bpsk,
Qpsk,
Qam16,
Qam64,
Qam256,
}
impl SoftKind {
fn new(order: ConstellationOrder) -> Self {
match order {
ConstellationOrder::Bpsk => SoftKind::Bpsk,
ConstellationOrder::Qpsk => SoftKind::Qpsk,
ConstellationOrder::Qam16 => SoftKind::Qam16,
ConstellationOrder::Qam64 => SoftKind::Qam64,
ConstellationOrder::Qam256 => SoftKind::Qam256,
}
}
#[inline]
fn llrs_per_symbol(&self) -> usize {
match self {
SoftKind::Bpsk => 1,
SoftKind::Qpsk => 2,
SoftKind::Qam16 => 4,
SoftKind::Qam64 => 6,
SoftKind::Qam256 => 8,
}
}
#[inline]
fn extract(&self, v: C32, out: &mut [f32]) {
match self {
SoftKind::Bpsk => out[0] = bpsk_soft_llr(v),
SoftKind::Qpsk => out[..2].copy_from_slice(&qpsk_soft_llr(v)),
SoftKind::Qam16 => out[..4].copy_from_slice(&qam_soft_llr::<4>(v)[..4]),
SoftKind::Qam64 => out[..6].copy_from_slice(&qam_soft_llr::<6>(v)[..6]),
SoftKind::Qam256 => out[..8].copy_from_slice(&qam_soft_llr::<8>(v)[..8]),
}
}
}
pub struct OfdmSoftDemod {
num_data_carriers: usize,
bits_per_ofdm_symbol: usize,
kind: SoftKind,
}
impl OfdmSoftDemod {
pub fn new(cfg: &OfdmConfig) -> Self {
Self {
num_data_carriers: cfg.carrier_plan.data_carriers().len(),
bits_per_ofdm_symbol: cfg.bits_per_ofdm_symbol(),
kind: SoftKind::new(cfg.constellation),
}
}
}
impl Block for OfdmSoftDemod {
type In = C32;
type Out = f32;
fn process(&mut self, input: &[C32], output: &mut [f32]) -> WorkReport {
if input.len() < self.num_data_carriers || output.len() < self.bits_per_ofdm_symbol {
return WorkReport::default();
}
let llrs_per_symbol = self.kind.llrs_per_symbol();
for (k, &v) in input[..self.num_data_carriers].iter().enumerate() {
self.kind.extract(
v,
&mut output[k * llrs_per_symbol..(k + 1) * llrs_per_symbol],
);
}
WorkReport {
in_read: self.num_data_carriers,
out_written: self.bits_per_ofdm_symbol,
}
}
}