mirror of
https://github.com/RFnexus/modem73.git
synced 2026-04-27 14:30:33 +00:00
BER calculation and remove seed tones from SNR
This commit is contained in:
parent
5c15f85e26
commit
30002af301
3 changed files with 166 additions and 33 deletions
15
kiss_tnc.cc
15
kiss_tnc.cc
|
|
@ -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
105
modem.hh
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
43
tnc_ui.hh
43
tnc_ui.hh
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue