From 9e6f588d306976088d0e34ea733d9600065b7330 Mon Sep 17 00:00:00 2001 From: Rob Riggs Date: Sun, 29 May 2022 17:09:09 -0500 Subject: [PATCH] Update to version 2.4.4. Clock recovery improvements. Update the sample index more frequently to reduce EVM. Re-wrote the symbol deviation, offset, EVM code. More code comments. --- TNC/ClockRecovery.h | 16 +-- TNC/FreqDevEstimator.h | 229 ++++++++++++++++++++------------------ TNC/KalmanFilter.h | 48 +++++++- TNC/KissHardware.cpp | 8 +- TNC/M17Demodulator.cpp | 199 +++++++++++++++++++++------------ TNC/M17Demodulator.h | 13 ++- TNC/M17FrameDecoder.h | 18 +-- TNC/StandardDeviation.hpp | 30 ++++- 8 files changed, 340 insertions(+), 221 deletions(-) diff --git a/TNC/ClockRecovery.h b/TNC/ClockRecovery.h index 96060c2..6a0b1ba 100644 --- a/TNC/ClockRecovery.h +++ b/TNC/ClockRecovery.h @@ -1,4 +1,4 @@ -// Copyright 2021 Mobilinkd LLC. +// Copyright 2021-2022 Mobilinkd LLC. #pragma once @@ -19,8 +19,6 @@ namespace mobilinkd { namespace m17 { template struct ClockRecovery { - static constexpr FloatType MAX_CLOCK_OFFSET = 0.0005; // 500ppm - KalmanFilter kf_; size_t count_ = 0; int8_t sample_index_ = 0; @@ -41,17 +39,10 @@ struct ClockRecovery ++count_; } - static FloatType clock_limiter(FloatType offset) - { - return std::min(MAX_CLOCK_OFFSET, std::max(-MAX_CLOCK_OFFSET, offset)); - } - bool update(uint8_t sw) { - INFO("CR Update %d", int(sw)); - if (count_ < 480) + if (count_ < 8) { - sample_index_ = sw; return false; } @@ -77,8 +68,7 @@ struct ClockRecovery */ bool update() { - INFO("CR Update"); - auto csw = std::fmod((sample_estimate_ + (1.0 + clock_estimate_) * count_), SamplesPerSymbol); + auto csw = std::fmod((sample_estimate_ + clock_estimate_ * count_), SamplesPerSymbol); if (csw < 0.) csw += SamplesPerSymbol; else if (csw >= SamplesPerSymbol) csw -= SamplesPerSymbol; diff --git a/TNC/FreqDevEstimator.h b/TNC/FreqDevEstimator.h index b7c9c70..96f5134 100644 --- a/TNC/FreqDevEstimator.h +++ b/TNC/FreqDevEstimator.h @@ -1,129 +1,142 @@ -// Copyright 2021 Rob Riggs +// Copyright 2021-2022 Rob Riggs // All rights reserved. #pragma once -#include "IirFilter.hpp" +#include "KalmanFilter.h" +#include "StandardDeviation.hpp" -#include -#include #include #include namespace mobilinkd { namespace m17 { - -/** - * Deviation and zero-offset estimator. - * - * Accepts samples which are periodically used to update estimates of the - * input signal deviation and zero offset. - * - * Samples must be provided at the ideal sample point (the point with the - * peak bit energy). - * - * Estimates are expected to be updated at each sync word. But they can - * be updated more frequently, such as during the preamble. - */ -template +template class FreqDevEstimator { - using sample_filter_t = tnc::IirFilter<3>; + static constexpr FloatType DEVIATION = 1.; - // IIR with Nyquist of 1/4. - static constexpr std::array dc_b = { 0.09763107, 0.19526215, 0.09763107 }; - static constexpr std::array dc_a = { 1. , -0.94280904, 0.33333333 }; - - static constexpr FloatType MAX_DC_ERROR = 0.2; - - FloatType min_est_ = 0.0; - FloatType max_est_ = 0.0; - FloatType min_cutoff_ = 0.0; - FloatType max_cutoff_ = 0.0; - FloatType min_var_ = 0.0; - FloatType max_var_ = 0.0; - size_t min_count_ = 1; - size_t max_count_ = 1; - FloatType deviation_ = 0.0; - FloatType offset_ = 0.0; - FloatType error_ = 0.0; - FloatType idev_ = 1.0; - sample_filter_t dc_filter_{dc_b, dc_a}; + SymbolKalmanFilter minFilter_; + SymbolKalmanFilter maxFilter_; + RunningStandardDeviation stddev_; + FloatType idev_ = 1.; + FloatType offset_ = 0.; + uint8_t count_ = 0; + FloatType min_ = 0.; + FloatType max_ = 0.; + uint8_t minCount_ = 0; + uint8_t maxCount_ = 0; + size_t updateCount_ = 0; + bool reset_ = true; public: - void reset() - { - min_est_ = 0.0; - max_est_ = 0.0; - min_var_ = 0.0; - max_var_ = 0.0; - min_count_ = 1; - max_count_ = 1; - min_cutoff_ = 0.0; - max_cutoff_ = 0.0; - } + void reset() + { + idev_ = 1.; + offset_ = 0.; + count_ = 0; + min_ = 0.; + max_ = 0.; + minCount_ = 0; + maxCount_ = 0; + updateCount_ = 0; + stddev_.reset(); + reset_ = true; + } - void sample(FloatType sample) - { - if (sample < 1.5 * min_est_) - { - min_count_ = 1; - min_est_ = sample; - min_var_ = 0.0; - min_cutoff_ = min_est_ * 0.666666; - } - else if (sample < min_cutoff_) - { - min_count_ += 1; - min_est_ += sample; - FloatType var = (min_est_ / min_count_) - sample; - min_var_ += var * var; - } - else if (sample > 1.5 * max_est_) - { - max_count_ = 1; - max_est_ = sample; - max_var_ = 0.0; - max_cutoff_ = max_est_ * 0.666666; - } - else if (sample > max_cutoff_) - { - max_count_ += 1; - max_est_ += sample; - FloatType var = (max_est_ / max_count_) - sample; - max_var_ += var * var; - } - } + /** + * This function takes the index samples from the correlator to build + * the outer symbol samples for the frequency offset (signal DC offset) + * and the deviation (signal magnitude). It expects bursts of 8 samples, + * one for each symbol in a sync word. + * + * @param sample + */ + void sample(FloatType sample) + { + count_ += 1; - /** - * Update the estimates for deviation, offset, and EVM (error). Note - * that the estimates for error are using a sloppy implementation for - * calculating variance to reduce the memory requirements. This is - * because this is designed for embedded use. - */ - void update() - { - if (max_count_ < 2 || min_count_ < 2) return; - FloatType max_ = max_est_ / max_count_; - FloatType min_ = min_est_ / min_count_; - deviation_ = (max_ - min_) / 6.0; - if (deviation_ > 0) idev_ = 1.0 / deviation_; - offset_ = dc_filter_(std::max(std::min(max_ + min_, deviation_ * MAX_DC_ERROR), deviation_ * -MAX_DC_ERROR)); - error_ = (std::sqrt(max_var_ / (max_count_ - 1)) + std::sqrt(min_var_ / (min_count_ - 1))) * 0.5 * idev_; - min_cutoff_ = offset_ - deviation_ * 2; - max_cutoff_ = offset_ + deviation_ * 2; - max_est_ = max_; - min_est_ = min_; - max_count_ = 1; - min_count_ = 1; - max_var_ = 0.0; - min_var_ = 0.0; - } + if (sample < 0) + { + minCount_ += 1; + min_ += sample; + } + else + { + maxCount_ += 1; + max_ += sample; + } - FloatType deviation() const { return deviation_; } - FloatType offset() const { return offset_; } - FloatType error() const { return error_; } - FloatType idev() const { return idev_; } + if (count_ == SYNC_WORD_LEN) + { + auto minAvg = min_ / minCount_; + auto maxAvg = max_ / maxCount_; + if (reset_) + { + minFilter_.reset(minAvg); + maxFilter_.reset(maxAvg); + idev_ = 6.0 / (maxAvg - minAvg); + offset_ = maxAvg + minAvg; + reset_ = false; + } + else + { + auto minFiltered = minFilter_.update(minAvg, count_ + updateCount_); + auto maxFiltered = maxFilter_.update(maxAvg, count_ + updateCount_); + idev_ = 6.0 / (maxFiltered[0] - minFiltered[0]); + offset_ = maxFiltered[0] + minFiltered[0]; + } + + count_ = 0; + updateCount_ = 0; + min_ = 0.; + max_ = 0.; + minCount_ = 0; + maxCount_ = 0; + } + } + + FloatType normalize(FloatType sample) const + { + return (sample - offset_) * idev_; + } + + FloatType evm() const { return stddev_.stdev(); } + + void update() const {} + + /** + * Capture EVM of a symbol. + * + * @param sample is a normalized sample captured at the best sample point. + */ + void update(FloatType sample) + { + if (sample > 2) + { + stddev_.capture(sample - 3); + } + else if (sample > 0) + { + stddev_.capture(sample - 1); + } + else if (sample > -2) + { + stddev_.capture(sample + 1); + } + else + { + stddev_.capture(sample + 3); + } + + updateCount_ += 1; + } + + FloatType idev() const { return idev_; } + FloatType offset() const { return offset_; } + FloatType deviation() const { return DEVIATION / idev_; } + FloatType error() const { return evm(); } }; + }} // mobilinkd::m17 diff --git a/TNC/KalmanFilter.h b/TNC/KalmanFilter.h index aed2bd5..d3c36a4 100644 --- a/TNC/KalmanFilter.h +++ b/TNC/KalmanFilter.h @@ -63,10 +63,10 @@ struct KalmanFilter auto K = P * blaze::trans(H) * (1.0 / S(0, 0)); // Normalize incoming index - if (z - x[0] < (SamplesPerSymbol / -2.0)) + if (z - x[0] < (SamplesPerSymbol / -2.0)) // wrapped forwards 9 -> 0 z += SamplesPerSymbol; - else if (z - x[0] > (SamplesPerSymbol / 2.0)) - z-= SamplesPerSymbol; + else if (z - x[0] > (SamplesPerSymbol / 2.0)) // wrapped 0 -> 9 + z -= SamplesPerSymbol; auto y = z - H * x; @@ -80,4 +80,46 @@ struct KalmanFilter } }; +template +struct SymbolKalmanFilter +{ + blaze::StaticVector x; + blaze::StaticMatrix P; + blaze::StaticMatrix F; + blaze::StaticMatrix H = {{1., 0.}}; + blaze::StaticMatrix R = {{0.5}}; + blaze::StaticMatrix Q = {{6.25e-4, 1.25e-3},{1.25e-3, 2.50e-3}}; + + SymbolKalmanFilter() + { + reset(0.); + } + + void reset(FloatType z) + { + x = {z, 0.}; + P = {{4., 0.}, {0., 0.00000025}}; + F = {{1., 1.}, {0., 1.}}; + } + + [[gnu::noinline]] + auto update(FloatType z, size_t dt) + { + F(0,1) = FloatType(dt); + + x = F * x; + P = F * P * blaze::trans(F) + Q; + auto S = H * P * blaze::trans(H) + R; + auto K = P * blaze::trans(H) * (1.0 / S(0, 0)); + + auto y = z - H * x; + + x += K * y; + + // Normalize the filtered sample point + P = P - K * H * P; + return x; + } +}; + }} // mobilinkd::m17 diff --git a/TNC/KissHardware.cpp b/TNC/KissHardware.cpp index ee8aa67..b98f3b1 100644 --- a/TNC/KissHardware.cpp +++ b/TNC/KissHardware.cpp @@ -1,4 +1,4 @@ -// Copyright 2018-2021 Rob Riggs +// Copyright 2018-2022 Rob Riggs // All rights reserved. #include "KissHardware.hpp" @@ -43,13 +43,13 @@ int powerOffViaUSB(void) namespace mobilinkd { namespace tnc { namespace kiss { #if defined(NUCLEOTNC) -const char FIRMWARE_VERSION[] = "2.4.3"; +const char FIRMWARE_VERSION[] = "2.4.4"; const char HARDWARE_VERSION[] = "Mobilinkd NucleoTNC"; #elif defined(STM32L433xx) -const char FIRMWARE_VERSION[] = "2.4.3"; +const char FIRMWARE_VERSION[] = "2.4.4"; const char HARDWARE_VERSION[] = "Mobilinkd TNC3 2.1.1"; #elif defined(STM32L4P5xx) -const char FIRMWARE_VERSION[] = "2.4.3"; +const char FIRMWARE_VERSION[] = "2.4.4"; const char HARDWARE_VERSION[] = "Mobilinkd TNC3+ Rev A"; #endif Hardware& settings() diff --git a/TNC/M17Demodulator.cpp b/TNC/M17Demodulator.cpp index 3be14a4..d09baa3 100644 --- a/TNC/M17Demodulator.cpp +++ b/TNC/M17Demodulator.cpp @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Rob Riggs +// Copyright 2020-2022 Rob Riggs // All rights reserved. #pragma GCC diagnostic push @@ -23,7 +23,7 @@ namespace mobilinkd { namespace tnc { //m17::Indicator dcd_indicator{GPIOA, GPIO_PIN_2}; //m17::Indicator str_indicator{GPIOA, GPIO_PIN_7}; -static float scale = 1.f / 2560.f; +static float scale = 1.f / 32768.f; static float dc_block(float x) { @@ -51,8 +51,8 @@ void M17Demodulator::start() demod_filter.init(m17::rrc_taps_f); passall(kiss::settings().options & KISS_OPTION_PASSALL); polarity = kiss::settings().rx_rev_polarity() ? -1 : 1; - scale = 1.f / 2560.f * polarity; - // audio::virtual_ground = (VREF + 1) / 2; + scale = 1.f / 32768.f * polarity; + audio::virtual_ground = (VREF + 1) / 2; hadc1.Init.OversamplingMode = ENABLE; if (HAL_ADC_Init(&hadc1) != HAL_OK) @@ -76,11 +76,26 @@ void M17Demodulator::start() dcd_off(); } +/** + * Update the deviation & offset from the stored correlator samples. This is + * called after a sync word has been detected. + * + * @pre @p sample_index is the best estimate of the sample point and has + * been set, either by the clock recovery system or from a sync word if + * the demodulator was in an unlocked state. + * + * @post @p dev has been updated with the latest sync word samples. And + * @p sync_sample_index is set to the current sync word tigger point. + * + * @param index is the current sync word trigger point. + */ void M17Demodulator::update_values(uint8_t index) { - correlator.apply([this,index](float t){dev.sample(t);}, index); + // For deviation and offset to be accurate, this must be the stable + // sample_index. The sync word trigger point is too noisy, resulting + // in inaccurate frequency offset and deviation estimates. + correlator.apply([this,index](float t){dev.sample(t);}, sample_index); dev.update(); - idev = dev.idev(); sync_sample_index = index; } @@ -89,20 +104,23 @@ void M17Demodulator::dcd_on() // Data carrier newly detected. INFO("dcd = %d", int(dcd.level() * 1000)); dcd_ = true; - sync_count = 0; - dev.reset(); - framer.reset(); - decoder.reset(); - missing_sync_count = 0; + if (demodState == DemodState::UNLOCKED) + { + sync_count = 0; + missing_sync_count = 0; + + dev.reset(); + framer.reset(); + decoder.reset(); + } } void M17Demodulator::dcd_off() { // Just lost data carrier. INFO("dcd = %d", int(dcd.level() * 1000)); - dcd_ = false; demodState = DemodState::UNLOCKED; - adc_timing_adjust = 0; + dcd_ = false; } void M17Demodulator::initialize(const q15_t* input) @@ -154,8 +172,8 @@ void M17Demodulator::do_unlocked() missing_sync_count = 0; need_clock_reset_ = true; dev.reset(); - update_values(sync_index); sample_index = sync_index; + update_values(sync_index); demodState = DemodState::LSF_SYNC; INFO("P sync %d", sync_index); } @@ -166,12 +184,12 @@ void M17Demodulator::do_unlocked() auto sync_updated = lsf_sync.updated(); if (sync_updated) { - sync_count = 0; + sync_count = MAX_SYNC_COUNT; missing_sync_count = 0; need_clock_reset_ = true; dev.reset(); - update_values(sync_index); sample_index = sync_index; + update_values(sync_index); demodState = DemodState::FRAME; if (sync_updated < 0) { @@ -188,12 +206,12 @@ void M17Demodulator::do_unlocked() sync_updated = packet_sync.updated(); if (sync_updated < 0) { - sync_count = 0; + sync_count = MAX_SYNC_COUNT; missing_sync_count = 0; need_clock_reset_ = true; dev.reset(); - update_values(sync_index); sample_index = sync_index; + update_values(sync_index); demodState = DemodState::FRAME; sync_word_type = M17FrameDecoder::SyncWordType::BERT; INFO("B sync %d", int(sync_index)); @@ -217,6 +235,7 @@ void M17Demodulator::do_lsf_sync() // INFO("PSync = %d", int(sync_triggered)); if (sync_triggered > 0.1) { + need_clock_update_ = true; sync_count += 1; return; } @@ -225,6 +244,7 @@ void M17Demodulator::do_lsf_sync() if (bert_triggered < 0) { missing_sync_count = 0; + sync_count = MAX_SYNC_COUNT; need_clock_update_ = true; update_values(sample_index); demodState = DemodState::FRAME; @@ -234,6 +254,7 @@ void M17Demodulator::do_lsf_sync() else if (std::abs(sync_triggered) > 0.1) { missing_sync_count = 0; + sync_count = MAX_SYNC_COUNT; need_clock_update_ = true; update_values(sample_index); INFO("LSync = %d", int(sync_triggered)); @@ -253,15 +274,17 @@ void M17Demodulator::do_lsf_sync() else if (++missing_sync_count > 192) { if (sync_count >= 10) { + // Long preamble. Update clock and continue waiting for LSF. missing_sync_count = 0; need_clock_update_ = true; INFO("long preamble"); } else { + // No sync word. Recycle. sync_count = 0; demodState = DemodState::UNLOCKED; + missing_sync_count = 0; dcd.unlock(); INFO("l unlock %d", int(missing_sync_count)); - missing_sync_count = 0; } } else @@ -283,14 +306,18 @@ void M17Demodulator::do_stream_sync() static bool eot_flag = false; sync_count += 1; - if (sync_count < MIN_SYNC_INDEX) { + if (sync_count < MIN_SYNC_COUNT) { return; } - if (eot_sync.triggered(correlator) > 0.1) { + if (eot_sync.triggered(correlator) > EOT_TRIGGER_LEVEL) { + // Note the EOT flag but continue trying to decode. This is needed + // to avoid false triggers. If it is a true EOT, the stream will + // end the next time we try to capture a sync word. sync_word_type = M17FrameDecoder::SyncWordType::STREAM; demodState = DemodState::FRAME; eot_flag = true; + missing_sync_count = 0; INFO("EOT"); return; } @@ -302,28 +329,31 @@ void M17Demodulator::do_stream_sync() missing_sync_count = 0; update_values(sync_index); sync_word_type = M17FrameDecoder::SyncWordType::STREAM; - demodState = DemodState::FRAME; + demodState = DemodState::SYNC_WAIT; INFO("s sync %d", int(sync_count)); eot_flag = false; } - else if (sync_count > MAX_SYNC_INDEX) + else if (sync_count > MAX_SYNC_COUNT) { - // update_values(sync_index); - if (ber >= 0 && ber < 80) + if (ber >= 0 && ber < STREAM_COST_LIMIT) { - // Sync word missed but we are still decoding a stream reasonably well. + // Sync word missed but we are still decoding a stream reasonably + // well. Don't increment the missing sync count, but it must not + // be 0 when a sync word is missed for clock recovery to work. + if (!missing_sync_count) missing_sync_count = 1; sync_word_type = M17FrameDecoder::SyncWordType::STREAM; demodState = DemodState::FRAME; - if (!missing_sync_count) missing_sync_count = 1; INFO("s bunsync"); } else if (eot_flag) { + // EOT flag set, missing sync, and very high BER. Stream has ended. demodState = DemodState::UNLOCKED; dcd.unlock(); INFO("s eot"); } else if (missing_sync_count < MAX_MISSING_SYNC) { + // Sync word missed, very high error rate. Still trying to decode. missing_sync_count += 1; sync_word_type = M17FrameDecoder::SyncWordType::STREAM; demodState = DemodState::FRAME; @@ -331,6 +361,7 @@ void M17Demodulator::do_stream_sync() } else { + // No EOT, but too many sync words missed. Recycle. demodState = DemodState::UNLOCKED; dcd.unlock(); INFO("s unlock"); @@ -347,7 +378,7 @@ void M17Demodulator::do_stream_sync() void M17Demodulator::do_packet_sync() { sync_count += 1; - if (sync_count < MIN_SYNC_INDEX) { + if (sync_count < MIN_SYNC_COUNT) { return; } @@ -359,27 +390,31 @@ void M17Demodulator::do_packet_sync() missing_sync_count = 0; update_values(sync_index); sync_word_type = M17FrameDecoder::SyncWordType::PACKET; - demodState = DemodState::FRAME; + demodState = DemodState::SYNC_WAIT; INFO("k sync"); - return; } - else if (sync_count > MAX_SYNC_INDEX) + else if (sync_count > MAX_SYNC_COUNT) { - missing_sync_count += 1; - if (missing_sync_count < MAX_MISSING_SYNC) - { - sync_word_type = M17FrameDecoder::SyncWordType::PACKET; - demodState = DemodState::FRAME; - INFO("k unsync"); - } - else if (ber >= 0 && ber < 80) + if (ber >= 0 && ber < PACKET_COST_LIMIT) { + // Sync word missed but we are still decoding reasonably well. + if (!missing_sync_count) missing_sync_count = 1; sync_word_type = M17FrameDecoder::SyncWordType::PACKET; demodState = DemodState::FRAME; INFO("k bunsync"); } + else if (missing_sync_count < MAX_MISSING_SYNC) + { + // Sync word missed, very high error rate. Still trying to decode. + // This may not be appropriate for packet mode. + missing_sync_count += 1; + sync_word_type = M17FrameDecoder::SyncWordType::PACKET; + demodState = DemodState::FRAME; + INFO("k unsync"); + } else { + // Too many sync words missed. Recycle. demodState = DemodState::UNLOCKED; dcd.unlock(); INFO("k unlock"); @@ -394,7 +429,7 @@ void M17Demodulator::do_packet_sync() void M17Demodulator::do_bert_sync() { sync_count += 1; - if (sync_count < MIN_SYNC_INDEX) { + if (sync_count < MIN_SYNC_COUNT) { return; } @@ -407,23 +442,25 @@ void M17Demodulator::do_bert_sync() update_values(sync_index); sync_word_type = M17FrameDecoder::SyncWordType::BERT; INFO("b sync"); - demodState = DemodState::FRAME; + demodState = DemodState::SYNC_WAIT; } - else if (sync_count > MAX_SYNC_INDEX) + else if (sync_count > MAX_SYNC_COUNT) { - missing_sync_count += 1; - if (missing_sync_count < MAX_MISSING_SYNC) - { - sync_word_type = M17FrameDecoder::SyncWordType::BERT; - demodState = DemodState::FRAME; - INFO("b unsync"); - } - else if (ber >= 0 && ber < 80) + if (ber >= 0 && ber < STREAM_COST_LIMIT) { + // Sync word missed but we are still decoding a stream reasonably well. + if (!missing_sync_count) missing_sync_count = 1; sync_word_type = M17FrameDecoder::SyncWordType::BERT; demodState = DemodState::FRAME; INFO("b bunsync"); } + else if (missing_sync_count < MAX_MISSING_SYNC) + { + missing_sync_count += 1; + sync_word_type = M17FrameDecoder::SyncWordType::BERT; + demodState = DemodState::FRAME; + INFO("b unsync"); + } else { demodState = DemodState::UNLOCKED; @@ -433,24 +470,53 @@ void M17Demodulator::do_bert_sync() } } +/** + * Wait for the sync_count to hit MAX_SYNC_COUNT. This is necessary for + * proper timing. Otherwise we can be off by 1 byte if the sync arrives + * a bit early. This may happen due to timing mismatch or noise causing + * the sync word correlator to trigger early. + */ +[[gnu::noinline]] +void M17Demodulator::do_sync_wait() +{ + if (sync_count < MAX_SYNC_COUNT) + { + sync_count += 1; + return; + } + + need_clock_update_ = true; + demodState = DemodState::FRAME; +} + [[gnu::noinline]] void M17Demodulator::do_frame(float filtered_sample, hdlc::IoFrame*& frame_result) { + // Only do this when there is no chance of skipping a sample. So do + // this update as far from the sample point as possible. It should + // only ever change by +/- 1. + if (abs(int(sample_index - correlator.index())) == (SAMPLES_PER_SYMBOL / 2)) + { + clock_recovery.update(); + sample_index = clock_recovery.sample_index(); + return; + } + if (correlator.index() != sample_index) return; - float sample = filtered_sample - dev.offset(); - sample *= idev; + float sample = dev.normalize(filtered_sample); + dev.update(sample); auto n = mobilinkd::llr(sample); int8_t* tmp; auto len = framer(n, &tmp); if (len != 0) { - need_clock_update_ = true; + sync_count = 0; std::copy(tmp, tmp + len, buffer.begin()); auto valid = decoder(sync_word_type, buffer, frame_result, ber); - INFO("demod: %d, dt: %7d, evma: %5d, dev: %5d, freq: %5d, dcd: %d, index: %d, %d ber: %d", + INFO("demod: %d, dt: %4dppm, evma: %5d‰, dev: %5d, freq: %5d, dcd: %d, index: %d, %d ber: %d", int(decoder.state()), int(clock_recovery.clock_estimate() * 1000000), int(dev.error() * 1000), int(dev.deviation() * 1000), int(dev.offset() * 1000), int(dcd.level() * 1000), @@ -474,8 +540,6 @@ void M17Demodulator::do_frame(float filtered_sample, hdlc::IoFrame*& frame_resul break; } - sync_count = 0; - switch (valid) { case M17FrameDecoder::DecodeResult::FAIL: @@ -486,10 +550,6 @@ void M17Demodulator::do_frame(float filtered_sample, hdlc::IoFrame*& frame_resul frame_result = nullptr; } break; - case M17FrameDecoder::DecodeResult::EOS: - // demodState = DemodState::LSF_SYNC; - INFO("EOS"); - break; case M17FrameDecoder::DecodeResult::OK: break; case M17FrameDecoder::DecodeResult::INCOMPLETE: @@ -549,25 +609,15 @@ hdlc::IoFrame* M17Demodulator::operator()(const q15_t* input) } else if (need_clock_update_) // must avoid update immediately after reset. { - if (!missing_sync_count) { - // Have a real sync word match. - clock_recovery.update(sync_sample_index); - } else if (decoder.state() != M17FrameDecoder::State::LSF) { - // Not waiting on clock recovery. - clock_recovery.update(); - } - sample_index = clock_recovery.sample_index(); + // We have a valid sync word. Update the filter. + clock_recovery.update(sync_sample_index); need_clock_update_ = false; } } + // Do this here, after the potential clock recovery reset above. clock_recovery(filtered_sample); - if (demodState != DemodState::UNLOCKED && correlator.index() == sample_index) - { - dev.sample(filtered_sample); - } - switch (demodState) { case DemodState::UNLOCKED: @@ -588,6 +638,9 @@ hdlc::IoFrame* M17Demodulator::operator()(const q15_t* input) case DemodState::BERT_SYNC: do_bert_sync(); break; + case DemodState::SYNC_WAIT: + do_sync_wait(); + break; case DemodState::FRAME: do_frame(filtered_sample,frame_result); break; diff --git a/TNC/M17Demodulator.h b/TNC/M17Demodulator.h index a158795..6c421c4 100644 --- a/TNC/M17Demodulator.h +++ b/TNC/M17Demodulator.h @@ -1,4 +1,4 @@ -// Copyright 2020-2021 Rob Riggs +// Copyright 2020-2022 Rob Riggs // All rights reserved. #pragma once @@ -44,14 +44,17 @@ struct M17Demodulator : IDemodulator static constexpr float sample_rate = SAMPLE_RATE; static constexpr float symbol_rate = SYMBOL_RATE; + static constexpr int STREAM_COST_LIMIT = 80; + static constexpr int PACKET_COST_LIMIT = 60; static constexpr uint8_t MAX_MISSING_SYNC = 10; - static constexpr uint8_t MIN_SYNC_INDEX = 79; - static constexpr uint8_t MAX_SYNC_INDEX = 88; + static constexpr uint8_t MIN_SYNC_COUNT = 78; + static constexpr uint8_t MAX_SYNC_COUNT = 87; + static constexpr float EOT_TRIGGER_LEVEL = 0.1; using audio_filter_t = FirFilter; using sync_word_t = m17::SyncWord; - enum class DemodState { UNLOCKED, LSF_SYNC, STREAM_SYNC, PACKET_SYNC, BERT_SYNC, FRAME }; + enum class DemodState { UNLOCKED, LSF_SYNC, STREAM_SYNC, PACKET_SYNC, BERT_SYNC, SYNC_WAIT, FRAME }; audio_filter_t demod_filter; std::array demod_buffer; @@ -73,7 +76,6 @@ struct M17Demodulator : IDemodulator DemodState demodState = DemodState::UNLOCKED; M17FrameDecoder::SyncWordType sync_word_type = M17FrameDecoder::SyncWordType::LSF; uint8_t sample_index = 0; - float idev; bool dcd_ = false; bool need_clock_reset_ = false; @@ -100,6 +102,7 @@ struct M17Demodulator : IDemodulator void do_packet_sync(); void do_stream_sync(); void do_bert_sync(); + void do_sync_wait(); void do_frame(float filtered_sample, hdlc::IoFrame*& frame_result); void stop() override diff --git a/TNC/M17FrameDecoder.h b/TNC/M17FrameDecoder.h index 913ff75..f85657e 100644 --- a/TNC/M17FrameDecoder.h +++ b/TNC/M17FrameDecoder.h @@ -161,7 +161,7 @@ struct M17FrameDecoder enum class State {LSF, STREAM, BASIC_PACKET, FULL_PACKET, BERT}; enum class SyncWordType { LSF, STREAM, PACKET, BERT }; - enum class DecodeResult { FAIL, OK, EOS, INCOMPLETE }; + enum class DecodeResult { FAIL, OK, INCOMPLETE }; State state_ = State::LSF; @@ -282,7 +282,6 @@ struct M17FrameDecoder { depuncture(buffer, tmp.lsf, P1); ber = viterbi_.decode(tmp.lsf, output.lsf); - ber = ber > 60 ? ber - 60 : 0; detail::to_bytes(output.lsf, current_lsf); crc_.reset(); for (auto c : current_lsf) crc_(c); @@ -442,15 +441,6 @@ struct M17FrameDecoder if (ber < 128) stream->push_back(255 - ber * 2); else stream->push_back(0); -#if 0 // Using EOT sync word now - if ((ber < 60) && (stream_segment[0] & 0x80)) - { - INFO("EOS"); - state_ = State::LSF; - result = DecodeResult::EOS; - } -#endif - // Bogus CRC bytes to be dropped. stream->push_back(0); stream->push_back(0); @@ -616,17 +606,17 @@ struct M17FrameDecoder * When in STREAM mode, the state machine can transition to either: * * - STREAM when a any stream frame is received. - * - LSF when the EOS indicator is set, or when a packet frame is received. + * - LSF when reset(). * * When in BASIC_PACKET mode, the state machine can transition to either: * * - BASIC_PACKET when any packet frame is received. - * - LSF when the EOS indicator is set, or when a stream frame is received. + * - LSF when a complete paket superframe is received. * * When in FULL_PACKET mode, the state machine can transition to either: * * - FULL_PACKET when any packet frame is received. - * - LSF when the EOS indicator is set, or when a stream frame is received. + * - LSF when a complete packet superframe is received. */ [[gnu::noinline]] DecodeResult operator()(SyncWordType frame_type, buffer_t& buffer, diff --git a/TNC/StandardDeviation.hpp b/TNC/StandardDeviation.hpp index e014476..cf7e814 100644 --- a/TNC/StandardDeviation.hpp +++ b/TNC/StandardDeviation.hpp @@ -1,4 +1,4 @@ -// Copyright 2020 Mobilinkd LLC +// Copyright 2020-2022 Mobilinkd LLC // All rights reserved. #pragma once @@ -56,4 +56,32 @@ struct StandardDeviation } }; +template +struct RunningStandardDeviation +{ + FloatType S{1.0}; + FloatType alpha{1.0 / N}; + + void reset() + { + S = 0.0; + } + + void capture(float sample) + { + S -= S * alpha; + S += (sample * sample) * alpha; + } + + FloatType variance() const + { + return S; + } + + FloatType stdev() const + { + return std::sqrt(variance()); + } +}; + } // mobilinkd