BER calculation and remove seed tones from SNR

This commit is contained in:
zenith 2026-03-10 13:09:38 -04:00
commit 30002af301
3 changed files with 166 additions and 33 deletions

View file

@ -698,7 +698,7 @@ private:
int level_update_counter = 0;
const int LEVEL_UPDATE_INTERVAL = 5;
auto deliver_to_clients = [this](const std::vector<uint8_t>& payload, float snr, bool was_reassembled) {
auto deliver_to_clients = [this](const std::vector<uint8_t>& payload, float snr, float ber_pct, bool was_reassembled) {
ui_log("RX: " + std::to_string(payload.size()) + " bytes, SNR=" +
std::to_string((int)snr) + "dB" + (was_reassembled ? " (reassembled)" : ""));
if (g_verbose) {
@ -707,7 +707,7 @@ private:
#ifdef WITH_UI
if (g_ui_state) {
g_ui_state->add_packet(false, payload.size(), snr);
g_ui_state->add_packet(false, payload.size(), snr, ber_pct);
}
#endif
@ -723,12 +723,17 @@ private:
set_tx_lockout(RX_LOCKOUT_SECONDS);
float snr = decoder_->get_last_snr();
float last_ber = decoder_->get_last_ber();
float ber_pct = (last_ber >= 0) ? last_ber * 100.0f : -1.0f;
float ber_ema = decoder_->get_ber_ema();
#ifdef WITH_UI
if (g_ui_state) {
g_ui_state->rx_frame_count++;
g_ui_state->receiving = false;
g_ui_state->last_rx_snr = snr;
if (ber_ema >= 0)
g_ui_state->last_rx_ber = ber_ema;
}
#endif
@ -750,10 +755,10 @@ private:
auto reassembled = reassembler_.process(payload);
if (!reassembled.empty()) {
ui_log("RX: Reassembled " + std::to_string(reassembled.size()) + " bytes from fragments");
deliver_to_clients(reassembled, snr, true);
deliver_to_clients(reassembled, snr, ber_pct, true);
}
} else {
deliver_to_clients(payload, snr, false);
deliver_to_clients(payload, snr, ber_pct, false);
}
};
@ -794,6 +799,8 @@ private:
decoder_->stats_preamble_errors = 0;
decoder_->stats_symbol_errors = 0;
decoder_->stats_crc_errors = 0;
decoder_->reset_ber();
g_ui_state->last_rx_ber = -1.0f;
}
g_ui_state->sync_count = decoder_->stats_sync_count;
g_ui_state->preamble_errors = decoder_->stats_preamble_errors;

105
modem.hh
View file

@ -452,6 +452,17 @@ public:
// Get current modulation bits
int get_mod_bits() const { return mod_bits; }
// Get last per-frame BER
value get_last_ber() const { return last_ber_; }
// Get smoothed BER via EMA
value get_ber_ema() const { return ber_ema_; }
void reset_ber() {
last_ber_ = -1;
ber_ema_ = -1;
}
// decode statistics
int stats_sync_count = 0; // corelator
int stats_preamble_errors = 0; // preamble decoding failed
@ -475,16 +486,23 @@ private:
SchmidlCox<value, cmplx, search_pos, symbol_len, guard_len>* correlator_ptr = nullptr;
CODE::HadamardDecoder<7> hadamard_decoder;
CODE::PolarListDecoder<mesg_type, code_max> polar_decoder;
CODE::PolarEncoder<int8_t> ber_encoder;
int8_t ber_mesg[bits_max], ber_code[bits_max];
DSP::Phasor<cmplx> osc;
mesg_type mesg[bits_max];
code_type code[bits_max], perm[bits_max];
cmplx demod[tone_count], chan[tone_count], tone[tone_count];
cmplx saved_demod[symbols_max * tone_count];
int saved_seed_off[symbols_max];
int fwd_perm_table[bits_max];
value index[tone_count], phase[tone_count];
value snr[symbols_max];
value cfo_rad;
int symbol_pos;
value last_avg_snr_ = 0;
value last_ber_ = -1;
value ber_ema_ = -1;
State state_ = State::SEARCHING;
size_t sample_count_ = 0;
@ -833,6 +851,30 @@ private:
return true;
}
void build_fwd_perm(int* table, int order) {
int len = 1 << order;
table[0] = 0;
if (order == 11) {
CODE::XorShiftMask<int, 11, 1, 3, 4, 1> seq;
for (int i = 1; i < len; ++i) table[i] = seq();
} else if (order == 12) {
CODE::XorShiftMask<int, 12, 1, 1, 4, 1> seq;
for (int i = 1; i < len; ++i) table[i] = seq();
} else if (order == 13) {
CODE::XorShiftMask<int, 13, 1, 1, 9, 1> seq;
for (int i = 1; i < len; ++i) table[i] = seq();
} else if (order == 14) {
CODE::XorShiftMask<int, 14, 1, 5, 10, 1> seq;
for (int i = 1; i < len; ++i) table[i] = seq();
} else if (order == 15) {
CODE::XorShiftMask<int, 15, 1, 1, 3, 1> seq;
for (int i = 1; i < len; ++i) table[i] = seq();
} else if (order == 16) {
CODE::XorShiftMask<int, 16, 1, 1, 14, 1> seq;
for (int i = 1; i < len; ++i) table[i] = seq();
}
}
bool process_symbol(int j) {
seed_off = (block_skew * j + first_seed) % block_length;
auto clamp = [](int v) { return v < -127 ? -127 : v > 127 ? 127 : v; };
@ -887,15 +929,18 @@ private:
demod[i] *= nrz(seq());
}
// Save demod for post-decode corrected SNR
std::memcpy(&saved_demod[j * tone_count], demod, tone_count * sizeof(cmplx));
saved_seed_off[j] = seed_off;
// Notify constellation callback with fully-corrected demodulated symbols
if (constellation_callback) {
constellation_callback(demod, tone_count, mod_bits);
}
// SNR estimation and soft demapping
// SNR estimation from data tones only excluding seed/pilot tones
value sp = 0, np = 0;
for (int i = 0, l = k_; i < tone_count; ++i) {
cmplx hard(1, 0);
if (i % block_length != seed_off) {
int bits = mod_bits;
if (mod_bits == 3 && l % 32 == 30) bits = 2;
@ -903,12 +948,12 @@ private:
if (mod_bits == 10 && l % 128 == 120) bits = 8;
if (mod_bits == 12 && l % 128 == 120) bits = 8;
demap_hard(perm + l, demod[i], bits);
hard = map_bits(perm + l, bits);
cmplx hard = map_bits(perm + l, bits);
cmplx error = demod[i] - hard;
sp += norm(hard);
np += norm(error);
l += bits;
}
cmplx error = demod[i] - hard;
sp += norm(hard);
np += norm(error);
}
value precision = sp / np;
@ -969,10 +1014,10 @@ private:
return;
}
// calculate average SNR from data symbols
// Fallback: average per-symbol SNR
value total_snr = 0;
int snr_count = 0;
for (int i = 1; i < symbol_index_; ++i) { // skip symbol 0
for (int i = 1; i < symbol_index_; ++i) {
if (snr[i] > 0) {
total_snr += snr[i];
snr_count++;
@ -991,8 +1036,50 @@ private:
for (int i = 0; i < data_bytes; ++i)
data[i] ^= scrambler();
std::cerr << "Decoder: Frame decoded " << data_bytes << " bytes, SNR=" << last_avg_snr_ << " dB" << std::endl;
// BER: re-encode decoded message and compare against received hard decisions
int code_len = 1 << code_order;
for (int i = 0; i < data_bits + 32; ++i)
ber_mesg[i] = (mesg[i].v[best] < 0) ? -1 : 1;
ber_encoder(ber_code, ber_mesg, frozen_bits, code_order);
int bit_errors = 0;
for (int i = 0; i < code_len; ++i) {
if ((code[i] < 0) != (ber_code[i] < 0))
bit_errors++;
}
last_ber_ = value(bit_errors) / value(code_len);
if (ber_ema_ < 0)
ber_ema_ = last_ber_;
else
ber_ema_ = value(0.3) * last_ber_ + value(0.7) * ber_ema_;
// use known-correct codeword as referenc
build_fwd_perm(fwd_perm_table, code_order);
value corr_sp = 0, corr_np = 0;
int bk = 0;
for (int sj = 1; sj <= symbol_count; ++sj) {
int soff = saved_seed_off[sj];
for (int i = 0; i < tone_count; ++i) {
if (i % block_length != soff) {
int bits = mod_bits;
if (mod_bits == 3 && bk % 32 == 30) bits = 2;
if (mod_bits == 6 && bk % 64 == 60) bits = 4;
if (mod_bits == 10 && bk % 128 == 120) bits = 8;
if (mod_bits == 12 && bk % 128 == 120) bits = 8;
code_type ideal_bits[mod_max];
for (int b = 0; b < bits; ++b)
ideal_bits[b] = ber_code[fwd_perm_table[bk + b]];
cmplx ideal = map_bits(ideal_bits, bits);
cmplx error = saved_demod[sj * tone_count + i] - ideal;
corr_sp += norm(ideal);
corr_np += norm(error);
bk += bits;
}
}
}
if (corr_np > 0)
last_avg_snr_ = 10 * std::log10(corr_sp / corr_np);
std::cerr << "Decoder: Frame decoded " << data_bytes << " bytes, SNR=" << last_avg_snr_ << " dB, BER=" << (last_ber_ * 100) << "%" << std::endl;
callback(data, data_bytes);
}

View file

@ -150,6 +150,7 @@ struct TNCUIState {
std::atomic<int> rx_frame_count{0};
std::atomic<int> tx_frame_count{0};
std::atomic<int> rx_error_count{0};
std::atomic<float> last_rx_ber{-1.0f};
// Decode statistics
std::atomic<int> sync_count{0};
@ -237,6 +238,7 @@ struct TNCUIState {
bool is_tx;
int size;
float snr;
float ber; // pre-FEC BER as percentage, -1 if unavailable
std::chrono::steady_clock::time_point timestamp;
};
static constexpr int MAX_RECENT_PACKETS = 8;
@ -382,10 +384,10 @@ struct TNCUIState {
return result;
}
void add_packet(bool is_tx, int size, float snr = 0.0f) {
void add_packet(bool is_tx, int size, float snr = 0.0f, float ber = -1.0f) {
{
std::lock_guard<std::mutex> lock(packets_mutex);
recent_packets.push_back({is_tx, size, snr, std::chrono::steady_clock::now()});
recent_packets.push_back({is_tx, size, snr, ber, std::chrono::steady_clock::now()});
if (recent_packets.size() > MAX_RECENT_PACKETS) {
recent_packets.pop_front();
}
@ -1777,6 +1779,28 @@ private:
draw_snr_chart(20);
y += 2;
mvaddstr(y, c1, "BER");
{
float ber_pct = state_.last_rx_ber.load() * 100.0f;
if (ber_pct < 0) {
attron(A_DIM);
mvaddstr(y, c2, " ---");
attroff(A_DIM);
} else if (ber_pct < 1.0f) {
attron(COLOR_PAIR(1) | A_BOLD);
mvprintw(y, c2, "%5.2f%%", ber_pct);
attroff(COLOR_PAIR(1) | A_BOLD);
} else if (ber_pct < 5.0f) {
attron(COLOR_PAIR(3) | A_BOLD);
mvprintw(y, c2, "%5.2f%%", ber_pct);
attroff(COLOR_PAIR(3) | A_BOLD);
} else {
attron(COLOR_PAIR(2) | A_BOLD);
mvprintw(y, c2, "%5.2f%%", ber_pct);
attroff(COLOR_PAIR(2) | A_BOLD);
}
}
y++;
attron(A_DIM);
mvaddstr(y, c1, "CSMA");
@ -1949,6 +1973,21 @@ private:
printw(" %.0fdB", pkt.snr);
attroff(COLOR_PAIR(4) | A_BOLD);
}
// BER
if (!pkt.is_tx && pkt.ber >= 0) {
float ber_pct = pkt.ber;
if (ber_pct < 1.0f) {
attron(COLOR_PAIR(1));
} else if (ber_pct < 5.0f) {
attron(COLOR_PAIR(3));
} else {
attron(COLOR_PAIR(2));
}
printw(" %.1f%%", ber_pct);
attroff(COLOR_PAIR(1));
attroff(COLOR_PAIR(2));
attroff(COLOR_PAIR(3));
}
}
}