diff --git a/firmware/application/apps/pocsag_app.cpp b/firmware/application/apps/pocsag_app.cpp index 248b73ee..6fcd21ec 100644 --- a/firmware/application/apps/pocsag_app.cpp +++ b/firmware/application/apps/pocsag_app.cpp @@ -105,6 +105,7 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav) &field_volume, &image_status, &text_packet_count, + &widget_baud, &widget_bits, &widget_frames, &button_ignore_last, @@ -274,11 +275,27 @@ void POCSAGAppView::on_packet(const POCSAGPacketMessage* message) { } void POCSAGAppView::on_stats(const POCSAGStatsMessage* stats) { + widget_baud.set_rate(stats->baud_rate); widget_bits.set_bits(stats->current_bits); widget_frames.set_frames(stats->current_frames); widget_frames.set_sync(stats->has_sync); } +void BaudIndicator::paint(Painter& painter) { + auto p = screen_pos(); + char top = '-'; + char bot = '-'; + + if (rate_ > 0) { + auto r = rate_ / 100; + top = (r / 10) + '0'; + bot = (r % 10) + '0'; + } + + painter.draw_char(p, Styles::white_small, top); + painter.draw_char({p.x(), p.y() + 8}, Styles::white_small, bot); +} + void BitsIndicator::paint(Painter&) { auto p = screen_pos(); for (size_t i = 0; i < sizeof(bits_) * 8; ++i) { @@ -295,7 +312,7 @@ void FrameIndicator::paint(Painter& painter) { painter.draw_rectangle({p, {2, height}}, has_sync_ ? Color::green() : Color::grey()); for (size_t i = 0; i < height; ++i) { - auto p2 = p + Point{2, 16 - (int)i}; + auto p2 = p + Point{2, 15 - (int)i}; painter.draw_hline(p2, 2, i < frame_count_ ? Color::white() : Color::black()); } } diff --git a/firmware/application/apps/pocsag_app.hpp b/firmware/application/apps/pocsag_app.hpp index d4f32a4f..69c47911 100644 --- a/firmware/application/apps/pocsag_app.hpp +++ b/firmware/application/apps/pocsag_app.hpp @@ -52,6 +52,24 @@ class POCSAGLogger { namespace ui { +class BaudIndicator : public Widget { + public: + BaudIndicator(Point position) + : Widget{{position, {5, height}}} {} + + void paint(Painter& painter) override; + void set_rate(uint16_t rate) { + if (rate != rate_) { + rate_ = rate; + set_dirty(); + } + } + + private: + static constexpr uint8_t height = 16; + uint16_t rate_ = 0; +}; + class BitsIndicator : public Widget { public: BitsIndicator(Point position) @@ -247,10 +265,13 @@ class POCSAGAppView : public View { "0"}; BitsIndicator widget_bits{ - {9 * 7 + 6, 1 * 16 + 2}}; + {8 * 8 + 1, 1 * 16 + 2}}; FrameIndicator widget_frames{ - {9 * 8, 1 * 16 + 2}}; + {8 * 8 + 4, 1 * 16 + 2}}; + + BaudIndicator widget_baud{ + {8 * 9 + 1, 1 * 16 + 2}}; Button button_ignore_last{ {10 * 8, 1 * 16, 12 * 8, 20}, diff --git a/firmware/application/apps/ui_settings.hpp b/firmware/application/apps/ui_settings.hpp index cad6f4dd..6168e159 100644 --- a/firmware/application/apps/ui_settings.hpp +++ b/firmware/application/apps/ui_settings.hpp @@ -370,7 +370,7 @@ class SetConverterSettingsView : public View { Button button_return{ {16 * 8, 16 * 16, 12 * 8, 32}, - "return", + "Return", }; }; diff --git a/firmware/baseband/dsp_squelch.cpp b/firmware/baseband/dsp_squelch.cpp index cb87189c..77874fa0 100644 --- a/firmware/baseband/dsp_squelch.cpp +++ b/firmware/baseband/dsp_squelch.cpp @@ -29,20 +29,19 @@ bool FMSquelch::execute(const buffer_f32_t& audio) { return true; } - // TODO: No hard-coded array size. - std::array squelch_energy_buffer; - const buffer_f32_t squelch_energy{ - squelch_energy_buffer.data(), - squelch_energy_buffer.size()}; + // TODO: alloca temp buffer, assert audio.count + std::array squelch_energy_buffer; + const buffer_f32_t squelch_energy{squelch_energy_buffer.data(), audio.count}; non_audio_hpf.execute(audio, squelch_energy); // "Non-audio" implies "noise" here. Find the loudest noise sample. float non_audio_max_squared = 0; - for (const auto sample : squelch_energy_buffer) { - const float sample_squared = sample * sample; - if (sample_squared > non_audio_max_squared) { + for (size_t i = 0; i < squelch_energy.count; ++i) { + auto sample = squelch_energy.p[i]; + float sample_squared = sample * sample; + + if (sample_squared > non_audio_max_squared) non_audio_max_squared = sample_squared; - } } // Is the noise less than the threshold? diff --git a/firmware/baseband/dsp_squelch.hpp b/firmware/baseband/dsp_squelch.hpp index 37a046a3..dbbca076 100644 --- a/firmware/baseband/dsp_squelch.hpp +++ b/firmware/baseband/dsp_squelch.hpp @@ -39,7 +39,6 @@ class FMSquelch { bool enabled() const; private: - static constexpr size_t N = 32; float threshold_squared{0.0f}; IIRBiquadFilter non_audio_hpf{non_audio_hpf_config}; diff --git a/firmware/baseband/proc_pocsag.cpp b/firmware/baseband/proc_pocsag.cpp index 5a4ec3ac..0101b748 100644 --- a/firmware/baseband/proc_pocsag.cpp +++ b/firmware/baseband/proc_pocsag.cpp @@ -144,7 +144,7 @@ void POCSAGProcessor::configure() { } void POCSAGProcessor::send_stats() const { - POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync); + POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync, getRate()); shared_memory.application_queue.push(message); } @@ -522,7 +522,7 @@ int POCSAGProcessor::getNoOfBits() { // ==================================================================== // // ==================================================================== -uint32_t POCSAGProcessor::getRate() { +uint32_t POCSAGProcessor::getRate() const { return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024; } diff --git a/firmware/baseband/proc_pocsag.hpp b/firmware/baseband/proc_pocsag.hpp index fd81adc1..66e5c47d 100644 --- a/firmware/baseband/proc_pocsag.hpp +++ b/firmware/baseband/proc_pocsag.hpp @@ -187,7 +187,7 @@ class POCSAGProcessor : public BasebandProcessor { short getBit(); int getNoOfBits(); - uint32_t getRate(); + uint32_t getRate() const; uint32_t m_averageSymbolLen_1024{0}; uint32_t m_lastStableSymbolLen_1024{0}; diff --git a/firmware/baseband/proc_pocsag2.cpp b/firmware/baseband/proc_pocsag2.cpp index a2f5a078..7f85eca7 100644 --- a/firmware/baseband/proc_pocsag2.cpp +++ b/firmware/baseband/proc_pocsag2.cpp @@ -3,7 +3,7 @@ * Copyright (C) 2012-2014 Elias Oenal (multimon-ng@eliasoenal.com) * Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc. * Copyright (C) 2016 Furrtek - * Copyright (C) 2016 Kyle Reed + * Copyright (C) 2023 Kyle Reed * * This file is part of PortaPack. * @@ -34,6 +34,323 @@ using namespace std; +namespace { +/* Count of bits that differ between the two values. */ +uint8_t differ_bit_count(uint32_t left, uint32_t right) { + uint32_t diff = left ^ right; + uint8_t count = 0; + for (size_t i = 0; i < sizeof(diff) * 8; ++i) { + if (((diff >> i) & 0x1) == 1) + ++count; + } + + return count; +} +} // namespace + +/* AudioNormalizer ***************************************/ + +void AudioNormalizer::execute_in_place(const buffer_f32_t& audio) { + // Decay min/max every second (@24kHz). + if (counter_ >= 24'000) { + // 90% decay factor seems to work well. + // This keeps large transients from wrecking the filter. + max_ *= 0.9f; + min_ *= 0.9f; + counter_ = 0; + calculate_thresholds(); + } + + counter_ += audio.count; + + for (size_t i = 0; i < audio.count; ++i) { + auto& val = audio.p[i]; + + if (val > max_) { + max_ = val; + calculate_thresholds(); + } + if (val < min_) { + min_ = val; + calculate_thresholds(); + } + + if (val >= t_hi_) + val = 1.0f; + else if (val <= t_lo_) + val = -1.0f; + else + val = 0.0; + } +} + +void AudioNormalizer::calculate_thresholds() { + auto center = (max_ + min_) / 2.0f; + auto range = (max_ - min_) / 2.0f; + + // 10% off center force either +/-1.0f. + // Higher == larger dead zone. + // Lower == more false positives. + auto threshold = range * 0.1; + t_hi_ = center + threshold; + t_lo_ = center - threshold; +} + +/* BitQueue **********************************************/ + +void BitQueue::push(bool bit) { + data_ = (data_ << 1) | (bit ? 1 : 0); + if (count_ < max_size_) ++count_; +} + +bool BitQueue::pop() { + if (count_ == 0) return false; + + --count_; + return (data_ & (1 << count_)) != 0; +} + +void BitQueue::reset() { + data_ = 0; + count_ = 0; +} + +uint8_t BitQueue::size() const { + return count_; +} + +uint32_t BitQueue::data() const { + return data_; +} + +/* BitExtractor ******************************************/ + +void BitExtractor::extract_bits(const buffer_f32_t& audio) { + // Assumes input has been normalized +/- 1.0f. + + for (size_t i = 0; i < audio.count; ++i) { + sample_ = audio.p[i]; + ++sample_index_; + + // There's a transition when both sides of the XOR are the + // same which will result in a the overall value being 0. + bool is_transition = ((last_sample_ < 0) ^ (sample_ >= 0)) == 0; + if (is_transition) { + if (handle_transition()) + bad_transitions_ = 0; + else + ++bad_transitions_; + + // Too many bad transitions? Reset. + if (bad_transitions_ > bad_transition_reset_threshold) + reset(); + } + + // Time to push the next bit? + if (sample_index_ >= next_bit_center_) { + // Use the two most recent samples for the bit value. + auto val = (sample_ + last_sample_) / 2.0; + bits_.push(val < 0); // NB: '1' is negative. + + if (current_rate_) + next_bit_center_ += current_rate_->bit_length; + } + + last_sample_ = sample_; + } +} + +void BitExtractor::configure(uint32_t sample_rate) { + sample_rate_ = sample_rate; + min_valid_length_ = UINT16_MAX; + + // Build the baud rate info table based on the sample rate. + for (auto& info : known_rates_) { + info.bit_length = sample_rate / info.baud_rate; + + // Allow for 20% deviation. + info.min_bit_length = 0.80 * info.bit_length; + info.max_bit_length = 1.20 * info.bit_length; + + if (info.min_bit_length < min_valid_length_) + min_valid_length_ = info.min_bit_length; + } + + reset(); +} + +void BitExtractor::reset() { + current_rate_ = nullptr; + rate_misses_ = 0; + + sample_ = 0.0; + last_sample_ = 0.0; + next_bit_center_ = 0.0; + + sample_index_ = 0; + last_transition_index_ = 0; + bad_transitions_ = 0; +} + +uint16_t BitExtractor::baud_rate() const { + return current_rate_ ? current_rate_->baud_rate : 0; +} + +bool BitExtractor::handle_transition() { + auto length = sample_index_ - last_transition_index_; + last_transition_index_ = sample_index_; + + // Length is too short, ignore this. + if (length <= min_valid_length_) return false; + + // TODO: should the following be "bad" or "rate misses"? + // Is length a multiple of the current rate's bit length? + uint16_t bit_count = 0; + if (!count_bits(length, bit_count)) return false; + + // Does the bit length correspond to a known rate? + auto bit_length = length / static_cast(bit_count); + auto rate = get_baud_info(bit_length); + if (!rate) return false; + + // Set current rate if it hasn't been set yet. + if (!current_rate_) + current_rate_ = rate; + + // Maybe current rate isn't the best rate? + auto rate_miss = rate != current_rate_; + if (rate_miss) { + ++rate_misses_; + + // Lots of rate misses, try another rate. + if (rate_misses_ > rate_miss_reset_threshold) { + current_rate_ = rate; + rate_misses_ = 0; + } + } else { + // Transition is aligned with the current rate, predict next bit. + auto half_bit = current_rate_->bit_length / 2.0; + next_bit_center_ = sample_index_ + half_bit; + } + + return true; +} + +bool BitExtractor::count_bits(uint32_t length, uint16_t& bit_count) { + bit_count = 0; + + // No rate yet, assume one valid bit. Downstream will deal with it. + if (!current_rate_) { + bit_count = 1; + return true; + } + + // How many bits span the specified length? + float exact_bits = length / current_rate_->bit_length; + + // < 1 bit, current rate is probably too low. + if (exact_bits < 0.80) return false; + + // Round to the nearest # of bits and determine how + // well the current rate fits the data. + float round_bits = std::round(exact_bits); + float error = std::abs(exact_bits - round_bits) / exact_bits; + + // Good transition are w/in 15% of current rate estimate. + bit_count = round_bits; + return error < 0.15; +} + +const BitExtractor::BaudInfo* BitExtractor::get_baud_info(float bit_length) const { + // NB: This assumes known_rates_ are ordered slowest first. + for (const auto& info : known_rates_) { + if (bit_length >= info.min_bit_length && + bit_length <= info.max_bit_length) { + return &info; + } + } + + return nullptr; +} + +/* CodewordExtractor *************************************/ + +void CodewordExtractor::process_bits() { + // Process all of the bits in the bits queue. + while (bits_.size() > 0) { + take_one_bit(); + + // Wait until data_ is full. + if (bit_count_ < data_bit_count) + continue; + + // Wait for the sync frame. + if (!has_sync_) { + if (differ_bit_count(data_, sync_codeword) <= 2) + handle_sync(/*inverted=*/false); + else if (differ_bit_count(data_, ~sync_codeword) <= 2) + handle_sync(/*inverted=*/true); + continue; + } + + save_current_codeword(); + + if (word_count_ == pocsag::batch_size) + handle_batch_complete(); + } +} + +void CodewordExtractor::flush() { + // Don't bother flushing if there's no pending data. + if (word_count_ == 0) return; + + pad_idle(); + handle_batch_complete(); +} + +void CodewordExtractor::reset() { + clear_data_bits(); + has_sync_ = false; + inverted_ = false; + word_count_ = 0; +} + +void CodewordExtractor::clear_data_bits() { + data_ = 0; + bit_count_ = 0; +} + +void CodewordExtractor::take_one_bit() { + data_ = (data_ << 1) | bits_.pop(); + if (bit_count_ < data_bit_count) + ++bit_count_; +} + +void CodewordExtractor::handle_sync(bool inverted) { + clear_data_bits(); + has_sync_ = true; + inverted_ = inverted; + word_count_ = 0; +} + +void CodewordExtractor::save_current_codeword() { + batch_[word_count_++] = inverted_ ? ~data_ : data_; + clear_data_bits(); +} + +void CodewordExtractor::handle_batch_complete() { + on_batch_(*this); + has_sync_ = false; + word_count_ = 0; +} + +void CodewordExtractor::pad_idle() { + while (word_count_ < pocsag::batch_size) + batch_[word_count_++] = idle_codeword; +} + +/* POCSAGProcessor ***************************************/ + void POCSAGProcessor::execute(const buffer_c8_t& buffer) { if (!configured) return; @@ -52,12 +369,12 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) { bool has_audio = squelch.execute(audio); squelch_history = (squelch_history << 1) | (has_audio ? 1 : 0); - // Has there been any signal? + // Has there been any signal recently? if (squelch_history == 0) { - // No signal for a while, flush and reset. - if (!has_been_reset) { - OnDataFrame(m_numCode, getRate()); - resetVals(); + // No recent signal, flush and prepare for next message. + if (word_extractor.current() > 0) { + flush(); + reset(); send_stats(); } @@ -69,18 +386,20 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) { return; } - // Filter out high-frequency noise. TODO: compensate gain? + // Filter out high-frequency noise then normalize. lpf.execute_in_place(audio); normalizer.execute_in_place(audio); audio_output.write(audio); - processDemodulatedSamples(audio.p, 16); - extractFrames(); + // Decode the messages from the audio. + bit_extractor.extract_bits(audio); + word_extractor.process_bits(); + // Update the status. samples_processed += buffer.count; if (samples_processed >= stat_update_threshold) { send_stats(); - samples_processed = 0; + samples_processed -= stat_update_threshold; } } @@ -104,8 +423,8 @@ void POCSAGProcessor::on_message(const Message* const message) { void POCSAGProcessor::configure() { constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; - const size_t channel_filter_output_fs = decim_1_output_fs / 2; - const size_t demod_input_fs = channel_filter_output_fs; + constexpr size_t channel_filter_output_fs = decim_1_output_fs / 2; + constexpr size_t demod_input_fs = channel_filter_output_fs; decim_0.configure(taps_11k0_decim_0.taps); decim_1.configure(taps_11k0_decim_1.taps); @@ -115,384 +434,41 @@ void POCSAGProcessor::configure() { // Don't process the audio stream. audio_output.configure(false); - // Set up the frame extraction, limits of baud. - setFrameExtractParams(demod_input_fs, 4000, 300, 32); + bit_extractor.configure(demod_input_fs); // Set ready to process data. configured = true; } -void POCSAGProcessor::send_stats() const { - POCSAGStatsMessage message(m_fifo.codeword, m_numCode, m_gotSync); - shared_memory.application_queue.push(message); +void POCSAGProcessor::flush() { + word_extractor.flush(); } -int POCSAGProcessor::OnDataWord(uint32_t word, int pos) { - packet.set(pos, word); - return 0; -} - -int POCSAGProcessor::OnDataFrame(int len, int baud) { - if (len > 0) { - packet.set_bitrate(baud); - packet.set_flag(pocsag::PacketFlag::NORMAL); - packet.set_timestamp(Timestamp::now()); - const POCSAGPacketMessage message(packet); - shared_memory.application_queue.push(message); - } - return 0; -} - -#define BAUD_STABLE (104) -#define MAX_CONSEC_SAME (32) -#define MAX_WITHOUT_SINGLE (64) -#define MAX_BAD_TRANS (10) - -#define M_SYNC (0x7cd215d8) -#define M_NOTSYNC (0x832dea27) - -#define M_IDLE (0x7a89c197) - -inline int bitsDiff(unsigned long left, unsigned long right) { - unsigned long xord = left ^ right; - int count = 0; - for (int i = 0; i < 32; i++) { - if ((xord & 0x01) != 0) ++count; - xord = xord >> 1; - } - return (count); -} - -void POCSAGProcessor::initFrameExtraction() { - m_averageSymbolLen_1024 = m_maxSymSamples_1024; - m_lastStableSymbolLen_1024 = m_minSymSamples_1024; - - m_badTransitions = 0; - m_bitsStart = 0; - m_bitsEnd = 0; - m_inverted = false; - - resetVals(); -} - -void POCSAGProcessor::resetVals() { - if (has_been_reset) return; - - // Reset the parameters - m_goodTransitions = 0; - m_badTransitions = 0; - m_averageSymbolLen_1024 = m_maxSymSamples_1024; - m_shortestGoodTrans_1024 = m_maxSymSamples_1024; - m_valMid = 0; - - // And reset the counts - m_lastTransPos_1024 = 0; - m_lastBitPos_1024 = 0; - m_lastSample = 0; - m_sampleNo = 0; - m_nextBitPos_1024 = m_maxSymSamples_1024; - m_nextBitPosInt = (long)m_nextBitPos_1024; - - // Extraction - m_fifo.numBits = 0; - m_fifo.codeword = 0; - m_gotSync = false; - m_numCode = 0; - - has_been_reset = true; +void POCSAGProcessor::reset() { + bits.reset(); + bit_extractor.reset(); + word_extractor.reset(); samples_processed = 0; } -void POCSAGProcessor::setFrameExtractParams(long a_samplesPerSec, long a_maxBaud, long a_minBaud, long maxRunOfSameValue) { - m_samplesPerSec = a_samplesPerSec; - m_minSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_maxBaud); - m_maxSymSamples_1024 = (uint32_t)(1024.0f * (float)a_samplesPerSec / (float)a_minBaud); - m_maxRunOfSameValue = maxRunOfSameValue; - - m_shortestGoodTrans_1024 = m_maxSymSamples_1024; - m_averageSymbolLen_1024 = m_maxSymSamples_1024; - m_lastStableSymbolLen_1024 = m_minSymSamples_1024; - - m_nextBitPos_1024 = m_averageSymbolLen_1024 / 2; - m_nextBitPosInt = m_nextBitPos_1024 >> 10; - - initFrameExtraction(); +void POCSAGProcessor::send_stats() const { + POCSAGStatsMessage message( + word_extractor.current(), word_extractor.count(), + word_extractor.has_sync(), bit_extractor.baud_rate()); + shared_memory.application_queue.push(message); } -int POCSAGProcessor::processDemodulatedSamples(float* sampleBuff, int noOfSamples) { - bool transition = false; - uint32_t samplePos_1024 = 0; - uint32_t len_1024 = 0; +void POCSAGProcessor::send_packet() { + packet.set_flag(pocsag::PacketFlag::NORMAL); + packet.set_timestamp(Timestamp::now()); + packet.set_bitrate(bit_extractor.baud_rate()); + packet.set(word_extractor.batch()); - has_been_reset = false; - - // Loop through the block of data - // ------------------------------ - for (int pos = 0; pos < noOfSamples; ++pos) { - m_sample = sampleBuff[pos]; - m_valMid += (m_sample - m_valMid) / 1024.0f; - - ++m_sampleNo; - - // Detect Transition - // ----------------- - transition = !((m_lastSample < m_valMid) ^ (m_sample >= m_valMid)); // use XOR for speed - - // If this is a transition - // ----------------------- - if (transition) { - // Calculate samples since last trans - // ---------------------------------- - int32_t fractional_1024 = (int32_t)(((m_sample - m_valMid) * 1024) / (m_sample - m_lastSample)); - if (fractional_1024 < 0) { - fractional_1024 = -fractional_1024; - } - - samplePos_1024 = (m_sampleNo << 10) - fractional_1024; - len_1024 = samplePos_1024 - m_lastTransPos_1024; - m_lastTransPos_1024 = samplePos_1024; - - // If symbol is large enough to be valid - // ------------------------------------- - if (len_1024 > m_minSymSamples_1024) { - // Check for shortest good transition - // ---------------------------------- - if ((len_1024 < m_shortestGoodTrans_1024) && - (m_goodTransitions < BAUD_STABLE)) // detect change of symbol size - { - int32_t fractionOfShortest_1024 = (len_1024 << 10) / m_shortestGoodTrans_1024; - - // If currently at half the baud rate - // ---------------------------------- - if ((fractionOfShortest_1024 > 410) && (fractionOfShortest_1024 < 614)) // 0.4 and 0.6 - { - m_averageSymbolLen_1024 /= 2; - m_shortestGoodTrans_1024 = len_1024; - } - // If currently at the wrong baud rate - // ----------------------------------- - else if (fractionOfShortest_1024 < 768) // 0.75 - { - m_averageSymbolLen_1024 = len_1024; - m_shortestGoodTrans_1024 = len_1024; - m_goodTransitions = 0; - m_lastSingleBitPos_1024 = samplePos_1024 - len_1024; - } - } - - // Calc the number of bits since events - // ------------------------------------ - int32_t halfSymbol_1024 = m_averageSymbolLen_1024 / 2; - int bitsSinceLastTrans = max((uint32_t)1, (len_1024 + halfSymbol_1024) / m_averageSymbolLen_1024); - int bitsSinceLastSingle = (((m_sampleNo << 10) - m_lastSingleBitPos_1024) + halfSymbol_1024) / m_averageSymbolLen_1024; - - // Check for single bit - // -------------------- - if (bitsSinceLastTrans == 1) { - m_lastSingleBitPos_1024 = samplePos_1024; - } - - // If too long since last transition - // --------------------------------- - if (bitsSinceLastTrans > MAX_CONSEC_SAME) { - resetVals(); - } - // If too long sice last single bit - // -------------------------------- - else if (bitsSinceLastSingle > MAX_WITHOUT_SINGLE) { - resetVals(); - } else { - // If this is a good transition - // ---------------------------- - int32_t offsetFromExtectedTransition_1024 = len_1024 - (bitsSinceLastTrans * m_averageSymbolLen_1024); - if (offsetFromExtectedTransition_1024 < 0) { - offsetFromExtectedTransition_1024 = -offsetFromExtectedTransition_1024; - } - if (offsetFromExtectedTransition_1024 < ((int32_t)m_averageSymbolLen_1024 / 4)) // Has to be within 1/4 of symbol to be good - { - ++m_goodTransitions; - uint32_t bitsCount = min((uint32_t)BAUD_STABLE, m_goodTransitions); - - uint32_t propFromPrevious = m_averageSymbolLen_1024 * bitsCount; - uint32_t propFromCurrent = (len_1024 / bitsSinceLastTrans); - m_averageSymbolLen_1024 = (propFromPrevious + propFromCurrent) / (bitsCount + 1); - m_badTransitions = 0; - // if ( len < m_shortestGoodTrans ){m_shortestGoodTrans = len;} - // Store the old symbol size - if (m_goodTransitions >= BAUD_STABLE) { - m_lastStableSymbolLen_1024 = m_averageSymbolLen_1024; - } - } - } - - // Set the point of the last bit if not yet stable - // ----------------------------------------------- - if ((m_goodTransitions < BAUD_STABLE) || (m_badTransitions > 0)) { - m_lastBitPos_1024 = samplePos_1024 - (m_averageSymbolLen_1024 / 2); - } - - // Calculate the exact positiom of the next bit - // -------------------------------------------- - int32_t thisPlusHalfsymbol_1024 = samplePos_1024 + (m_averageSymbolLen_1024 / 2); - int32_t lastPlusSymbol = m_lastBitPos_1024 + m_averageSymbolLen_1024; - m_nextBitPos_1024 = lastPlusSymbol + ((thisPlusHalfsymbol_1024 - lastPlusSymbol) / 16); - - // Check for bad pos error - // ----------------------- - if (m_nextBitPos_1024 < samplePos_1024) m_nextBitPos_1024 += m_averageSymbolLen_1024; - - // Calculate integer sample after next bit - // --------------------------------------- - m_nextBitPosInt = (m_nextBitPos_1024 >> 10) + 1; - - } // symbol is large enough to be valid - else { - // Bad transition, so reset the counts - // ----------------------------------- - ++m_badTransitions; - if (m_badTransitions > MAX_BAD_TRANS) { - resetVals(); - } - } - } // end of if transition - - // Reached the point of the next bit - // --------------------------------- - if (m_sampleNo >= m_nextBitPosInt) { - // Everything is good so extract a bit - // ----------------------------------- - if (m_goodTransitions > 20) { - // Store value at the center of bit - // -------------------------------- - storeBit(); - } - // Check for long 1 or zero - // ------------------------ - uint32_t bitsSinceLastTrans = ((m_sampleNo << 10) - m_lastTransPos_1024) / m_averageSymbolLen_1024; - if (bitsSinceLastTrans > m_maxRunOfSameValue) { - resetVals(); - } - - // Store the point of the last bit - // ------------------------------- - m_lastBitPos_1024 = m_nextBitPos_1024; - - // Calculate the exact point of the next bit - // ----------------------------------------- - m_nextBitPos_1024 += m_averageSymbolLen_1024; - - // Look for the bit after the next bit pos - // --------------------------------------- - m_nextBitPosInt = (m_nextBitPos_1024 >> 10) + 1; - - } // Reached the point of the next bit - - m_lastSample = m_sample; - - } // Loop through the block of data - - return getNoOfBits(); + POCSAGPacketMessage message(packet); + shared_memory.application_queue.push(message); } -void POCSAGProcessor::storeBit() { - if (++m_bitsStart >= BIT_BUF_SIZE) { - m_bitsStart = 0; - } - - // Calculate the bit value - float sample = (m_sample + m_lastSample) / 2; - // int32_t sample_1024 = m_sample_1024; - bool bit = sample > m_valMid; - - // If buffer not full - if (m_bitsStart != m_bitsEnd) { - // Decide on output val - if (bit) { - m_bits[m_bitsStart] = 0; - } else { - m_bits[m_bitsStart] = 1; - } - } - // Throw away bits if the buffer is full - else { - if (--m_bitsStart <= -1) { - m_bitsStart = BIT_BUF_SIZE - 1; - } - } -} - -int POCSAGProcessor::extractFrames() { - int msgCnt = 0; - // While there is unread data in the bits buffer - //---------------------------------------------- - while (getNoOfBits() > 0) { - m_fifo.codeword = (m_fifo.codeword << 1) + getBit(); - m_fifo.numBits++; - - // If number of bits in fifo equals 32 - //------------------------------------ - if (m_fifo.numBits >= 32) { - // Not got sync - // ------------ - if (!m_gotSync) { - if (bitsDiff(m_fifo.codeword, M_SYNC) <= 2) { - m_inverted = false; - m_gotSync = true; - m_numCode = -1; - m_fifo.numBits = 0; - } else if (bitsDiff(m_fifo.codeword, M_NOTSYNC) <= 2) { - m_inverted = true; - m_gotSync = true; - m_numCode = -1; - m_fifo.numBits = 0; - } else { - // Cause it to load one more bit - m_fifo.numBits = 31; - } - } // Not got sync - else { - // Increment the word count - // ------------------------ - ++m_numCode; // It got set to -1 when a sync was found, now count the 16 words - uint32_t val = m_inverted ? ~m_fifo.codeword : m_fifo.codeword; - OnDataWord(val, m_numCode); - - // If at the end of a 16 word block - // -------------------------------- - if (m_numCode >= 15) { - msgCnt += OnDataFrame(m_numCode + 1, getRate()); - m_gotSync = false; - m_numCode = -1; - } - m_fifo.numBits = 0; - } - } // If number of bits in fifo equals 32 - } // While there is unread data in the bits buffer - return msgCnt; -} // extractFrames - -short POCSAGProcessor::getBit() { - if (m_bitsEnd != m_bitsStart) { - if (++m_bitsEnd >= BIT_BUF_SIZE) { - m_bitsEnd = 0; - } - return m_bits[m_bitsEnd]; - } else { - return -1; - } -} - -int POCSAGProcessor::getNoOfBits() { - int bits = m_bitsEnd - m_bitsStart; - if (bits < 0) { - bits += BIT_BUF_SIZE; - } - return bits; -} - -uint32_t POCSAGProcessor::getRate() { - return ((m_samplesPerSec << 10) + 512) / m_lastStableSymbolLen_1024; -} +/* main **************************************************/ int main() { EventDispatcher event_dispatcher{std::make_unique()}; diff --git a/firmware/baseband/proc_pocsag2.hpp b/firmware/baseband/proc_pocsag2.hpp index 10b71c28..97623ba0 100644 --- a/firmware/baseband/proc_pocsag2.hpp +++ b/firmware/baseband/proc_pocsag2.hpp @@ -26,6 +26,8 @@ #ifndef __PROC_POCSAG2_H__ #define __PROC_POCSAG2_H__ +/* https://www.aaroncake.net/schoolpage/pocsag.htm */ + #include "audio_output.hpp" #include "baseband_processor.hpp" #include "baseband_thread.hpp" @@ -38,57 +40,17 @@ #include "portapack_shared_memory.hpp" #include "rssi_thread.hpp" +#include #include +#include -/* Takes audio stream and automatically normalizes it to +/-1.0f */ +/* Normalizes audio stream to +/-1.0f */ class AudioNormalizer { public: - void execute_in_place(const buffer_f32_t& audio) { - // Decay min/max every second (@24kHz). - if (counter_ >= 24'000) { - // 90% decay factor seems to work well. - // This keeps large transients from wrecking the filter. - max_ *= 0.9f; - min_ *= 0.9f; - counter_ = 0; - calculate_thresholds(); - } - - counter_ += audio.count; - - for (size_t i = 0; i < audio.count; ++i) { - auto& val = audio.p[i]; - - if (val > max_) { - max_ = val; - calculate_thresholds(); - } - if (val < min_) { - min_ = val; - calculate_thresholds(); - } - - if (val >= t_hi_) - val = 1.0f; - else if (val <= t_lo_) - val = -1.0f; - else - val = 0.0; - } - } + void execute_in_place(const buffer_f32_t& audio); private: - void calculate_thresholds() { - auto center = (max_ + min_) / 2.0f; - auto range = (max_ - min_) / 2.0f; - - // 10% off center force either +/-1.0f. - // Higher == larger dead zone. - // Lower == more false positives. - auto threshold = range * 0.1; - t_hi_ = center + threshold; - t_lo_ = center - threshold; - } + void calculate_thresholds(); uint32_t counter_ = 0; float min_ = 99.0f; @@ -97,24 +59,158 @@ class AudioNormalizer { float t_lo_ = 1.0; }; -// How to detect clock signal across baud rates? -// Maybe have a bit extraction state machine that reset -// then watches for the clocks, but there are multiple -// clock and the last one is the right one. -// So keep updating clock until a sync? +/* FIFO wrapper over a uint32_t's bits. */ +class BitQueue { + public: + void push(bool bit); + bool pop(); + void reset(); + uint8_t size() const; + uint32_t data() const; -class BitExtractor {}; + private: + uint32_t data_ = 0; + uint8_t count_ = 0; -class WordExtractor {}; + static constexpr uint8_t max_size_ = sizeof(data_) * 8; +}; +/* Extracts bits and bitrate from audio stream. */ +class BitExtractor { + public: + BitExtractor(BitQueue& bits) + : bits_{bits} {} + + void extract_bits(const buffer_f32_t& audio); + void configure(uint32_t sample_rate); + void reset(); + + uint16_t baud_rate() const; + + private: + /* Number of rate misses that would cause a rate update. */ + static constexpr uint8_t rate_miss_reset_threshold = 5; + + /* Number of rate misses that would cause a rate update. */ + static constexpr uint8_t bad_transition_reset_threshold = 10; + + struct BaudInfo { + uint16_t baud_rate = 0; + float bit_length = 0.0; + float min_bit_length = 0.0; + float max_bit_length = 0.0; + }; + + /* Handle a transition, returns true if "good". */ + bool handle_transition(); + + /* Count the number of bits the length represents. + * Returns true if valid given the current baud rate. */ + bool count_bits(uint32_t length, uint16_t& bit_count); + + /* Gets the best baud info associated with the specified bit length. */ + const BaudInfo* get_baud_info(float bit_length) const; + + std::array known_rates_{ + BaudInfo{512}, + BaudInfo{1200}, + BaudInfo{2400}}; + + BitQueue& bits_; + + uint32_t sample_rate_ = 0; + uint16_t min_valid_length_ = 0; + const BaudInfo* current_rate_ = nullptr; + uint8_t rate_misses_ = 0; + + float sample_ = 0.0; + float last_sample_ = 0.0; + float next_bit_center_ = 0.0; + + uint32_t sample_index_ = 0; + uint32_t last_transition_index_ = 0; + uint32_t bad_transitions_ = 0; +}; + +/* Extracts codeword batches from the BitQueue. */ +class CodewordExtractor { + public: + using batch_t = pocsag::batch_t; + using batch_handler_t = std::function; + + CodewordExtractor(BitQueue& bits, batch_handler_t on_batch) + : bits_{bits}, on_batch_{on_batch} {} + + /* Process the BitQueue to extract codeword batches. */ + void process_bits(); + + /* Pad then send any pending frames. */ + void flush(); + + /* Completely reset to prepare for a new message. */ + void reset(); + + /* Gets the underlying batch array. */ + const batch_t& batch() const { return batch_; } + + /* Gets in-progress codeword. */ + uint32_t current() const { return data_; } + + /* Gets the count of completed codewords. */ + uint8_t count() const { return word_count_; } + + /* Returns true if the batch has as sync frame. */ + bool has_sync() const { return has_sync_; } + + private: + /* Sync frame codeword. */ + static constexpr uint32_t sync_codeword = 0x7cd215d8; + + /* Idle codeword used to pad a 16 codeword "batch". */ + static constexpr uint32_t idle_codeword = 0x7a89c197; + + /* Number of bits in 'data_' member. */ + static constexpr uint8_t data_bit_count = sizeof(uint32_t) * 8; + + /* Clears data_ and bit_count_ to prepare for next codeword. */ + void clear_data_bits(); + + /* Pop a bit off the queue and add it to data_. */ + void take_one_bit(); + + /* Handles receiving the sync frame codeword, start of batch. */ + void handle_sync(bool inverted); + + /* Saves the current codeword in data_ to the batch. */ + void save_current_codeword(); + + /* Sends the batch to the handler, resets for next batch. */ + void handle_batch_complete(); + + /* Fill the rest of the batch with 'idle' codewords. */ + void pad_idle(); + + BitQueue& bits_; + batch_handler_t on_batch_{}; + + /* When true, sync frame has been received. */ + bool has_sync_ = false; + + /* When true, bit vales are flipped in the codewords. */ + bool inverted_ = false; + + uint32_t data_ = 0; + uint8_t bit_count_ = 0; + uint8_t word_count_ = 0; + batch_t batch_{}; +}; + +/* Processes POCSAG signal into codeword batches. */ class POCSAGProcessor : public BasebandProcessor { public: void execute(const buffer_c8_t& buffer) override; void on_message(const Message* const message) override; - int OnDataFrame(int len, int baud); - int OnDataWord(uint32_t word, int pos); - private: static constexpr size_t baseband_fs = 3072000; static constexpr uint8_t stat_update_interval = 10; @@ -122,106 +218,63 @@ class POCSAGProcessor : public BasebandProcessor { baseband_fs / stat_update_interval; void configure(); + void flush(); + void reset(); void send_stats() const; + void send_packet(); - // Set once app is ready to receive messages. + /* Set once app is ready to receive messages. */ bool configured = false; - // Buffer for decimated IQ data. - std::array dst{}; + /* Buffer for decimated IQ data. */ + std::array dst{}; const buffer_c16_t dst_buffer{dst.data(), dst.size()}; - // Buffer for demodulated audio. - std::array audio{}; + /* Buffer for demodulated audio. */ + std::array audio{}; const buffer_f32_t audio_buffer{audio.data(), audio.size()}; - // Decimate to 48kHz. + /* Decimate to 48kHz. */ dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; - // Filter to 24kHz and demodulate. + /* Filter to 24kHz and demodulate. */ dsp::decimate::FIRAndDecimateComplex channel_filter{}; dsp::demodulate::FM demod{}; - // LPF to reduce noise. - // scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) - IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f}, - {1.00000000f, -1.34896775f, 0.51398189f}}}; - - // Squelch to ignore noise. + /* Squelch to ignore noise. */ FMSquelch squelch{}; uint64_t squelch_history = 0; - // Attempts to de-noise signal and normalize to +/- 1.0f. + /* LPF to reduce noise. POCSAG supports 2400 baud, but that falls + * nicely into the transition band of this 1800Hz filter. + * scipy.signal.butter(2, 1800, "lowpass", fs=24000, analog=False) */ + IIRBiquadFilter lpf{{{0.04125354f, 0.082507070f, 0.04125354f}, + {1.00000000f, -1.34896775f, 0.51398189f}}}; + + /* Attempts to de-noise and normalize signal. */ AudioNormalizer normalizer{}; - // Handles writing audio stream to hardware. + /* Handles writing audio stream to hardware. */ AudioOutput audio_output{}; - // Holds the data sent to the app. + /* Holds the data sent to the app. */ pocsag::POCSAGPacket packet{}; - bool has_been_reset = true; + /* Used to keep track of how many samples were processed + * between status update messages. */ uint32_t samples_processed = 0; - //-------------------------------------------------- + BitQueue bits{}; - // ---------------------------------------- - // Frame extractraction methods and members - // ---------------------------------------- - void initFrameExtraction(); - struct FIFOStruct { - unsigned long codeword; - int numBits; - }; + /* Processes audio into bits. */ + BitExtractor bit_extractor{bits}; - void resetVals(); - void setFrameExtractParams(long a_samplesPerSec, long a_maxBaud = 8000, long a_minBaud = 200, long maxRunOfSameValue = 32); - - int processDemodulatedSamples(float* sampleBuff, int noOfSamples); - int extractFrames(); - - void storeBit(); - short getBit(); - - int getNoOfBits(); - uint32_t getRate(); - - uint32_t m_averageSymbolLen_1024{0}; - uint32_t m_lastStableSymbolLen_1024{0}; - - uint32_t m_samplesPerSec{0}; - uint32_t m_goodTransitions{0}; - uint32_t m_badTransitions{0}; - - uint32_t m_sampleNo{0}; - float m_sample{0}; - float m_valMid{0.0f}; - float m_lastSample{0.0f}; - - uint32_t m_lastTransPos_1024{0}; - uint32_t m_lastSingleBitPos_1024{0}; - - uint32_t m_nextBitPosInt{0}; // Integer rounded up version to save on ops - uint32_t m_nextBitPos_1024{0}; - uint32_t m_lastBitPos_1024{0}; - - uint32_t m_shortestGoodTrans_1024{0}; - uint32_t m_minSymSamples_1024{0}; - uint32_t m_maxSymSamples_1024{0}; - uint32_t m_maxRunOfSameValue{0}; - - static constexpr long BIT_BUF_SIZE = 64; - std::bitset<64> m_bits{0}; - long m_bitsStart{0}; - long m_bitsEnd{0}; - - FIFOStruct m_fifo{0, 0}; - bool m_gotSync{false}; - int m_numCode{0}; - bool m_inverted{false}; - - //-------------------------------------------------- + /* Processes bits into codewords. */ + CodewordExtractor word_extractor{ + bits, [this](CodewordExtractor&) { + send_packet(); + }}; /* NB: Threads should be the last members in the class definition. */ BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive}; diff --git a/firmware/common/message.hpp b/firmware/common/message.hpp index 04680d39..dac16176 100644 --- a/firmware/common/message.hpp +++ b/firmware/common/message.hpp @@ -346,16 +346,19 @@ class POCSAGStatsMessage : public Message { constexpr POCSAGStatsMessage( uint32_t current_bits, uint8_t current_frames, - bool has_sync) + bool has_sync, + uint16_t baud_rate) : Message{ID::POCSAGStats}, current_bits{current_bits}, current_frames{current_frames}, - has_sync{has_sync} { + has_sync{has_sync}, + baud_rate{baud_rate} { } uint32_t current_bits = 0; uint8_t current_frames = 0; bool has_sync = false; + uint16_t baud_rate = 0; }; class ACARSPacketMessage : public Message { diff --git a/firmware/common/pocsag.cpp b/firmware/common/pocsag.cpp index 415537d0..4e2d860f 100644 --- a/firmware/common/pocsag.cpp +++ b/firmware/common/pocsag.cpp @@ -412,7 +412,7 @@ bool pocsag_decode_batch(const POCSAGPacket& batch, POCSAGState& state) { state.ascii_idx -= 7; char ascii_char = (state.ascii_data >> state.ascii_idx) & 0x7F; - // Bottom's up (reverse the bits). + // Reverse the bits. (TODO: __RBIT?) ascii_char = (ascii_char & 0xF0) >> 4 | (ascii_char & 0x0F) << 4; // 01234567 -> 45670123 ascii_char = (ascii_char & 0xCC) >> 2 | (ascii_char & 0x33) << 2; // 45670123 -> 67452301 ascii_char = (ascii_char & 0xAA) >> 2 | (ascii_char & 0x55); // 67452301 -> 76543210 diff --git a/firmware/common/pocsag_packet.hpp b/firmware/common/pocsag_packet.hpp index c9835bc1..230a3b4e 100644 --- a/firmware/common/pocsag_packet.hpp +++ b/firmware/common/pocsag_packet.hpp @@ -45,6 +45,10 @@ enum PacketFlag : uint32_t { TOO_LONG }; +/* Number of codewords in a batch. */ +constexpr uint8_t batch_size = 16; +using batch_t = std::array; + class POCSAGPacket { public: void set_timestamp(const Timestamp& value) { @@ -55,16 +59,20 @@ class POCSAGPacket { return timestamp_; } - void set(const size_t index, const uint32_t data) { - if (index < 16) + void set(size_t index, uint32_t data) { + if (index < batch_size) codewords[index] = data; } - uint32_t operator[](const size_t index) const { - return (index < 16) ? codewords[index] : 0; + void set(const batch_t& batch) { + codewords = batch; } - void set_bitrate(const uint16_t bitrate) { + uint32_t operator[](size_t index) const { + return (index < batch_size) ? codewords[index] : 0; + } + + void set_bitrate(uint16_t bitrate) { bitrate_ = bitrate; } @@ -72,7 +80,7 @@ class POCSAGPacket { return bitrate_; } - void set_flag(const PacketFlag flag) { + void set_flag(PacketFlag flag) { flag_ = flag; } @@ -89,7 +97,7 @@ class POCSAGPacket { private: uint16_t bitrate_{0}; PacketFlag flag_{NORMAL}; - std::array codewords{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + batch_t codewords{}; Timestamp timestamp_{}; }; diff --git a/firmware/test/baseband/dsp_fft_test.cpp b/firmware/test/baseband/dsp_fft_test.cpp index c4dc597a..d0e173b7 100644 --- a/firmware/test/baseband/dsp_fft_test.cpp +++ b/firmware/test/baseband/dsp_fft_test.cpp @@ -50,8 +50,46 @@ TEST_CASE("ifft successfully calculates dc on zero frequency") { for (uint32_t i = 0; i < fft_width; i++) CHECK(v[i].imag() == 0); - free(v); - free(tmp); + delete[] v; + delete[] tmp; +} + +TEST_CASE("ifft successfully calculates sine of quarter the sample rate") { + uint32_t fft_width = 8; + complex16_t* v = new complex16_t[fft_width]; + complex16_t* tmp = new complex16_t[fft_width]; + + v[0] = {0, 0}; + v[1] = {0, 0}; + v[2] = {1024, 0}; // sample rate /4 bin + v[3] = {0, 0}; + v[4] = {0, 0}; + v[5] = {0, 0}; + v[6] = {0, 0}; + v[7] = {0, 0}; + + ifft(v, fft_width, tmp); + + CHECK(v[0].real() == 1024); + CHECK(v[1].real() == 0); + CHECK(v[2].real() == -1024); + CHECK(v[3].real() == 0); + CHECK(v[4].real() == 1024); + CHECK(v[5].real() == 0); + CHECK(v[6].real() == -1024); + CHECK(v[7].real() == 0); + + CHECK(v[0].imag() == 0); + CHECK(v[1].imag() == 1024); + CHECK(v[2].imag() == 0); + CHECK(v[3].imag() == -1024); + CHECK(v[4].imag() == 0); + CHECK(v[5].imag() == 1024); + CHECK(v[6].imag() == 0); + CHECK(v[7].imag() == -1024); + + delete[] v; + delete[] tmp; } TEST_CASE("ifft successfully calculates pure sine of half the sample rate") { @@ -82,6 +120,6 @@ TEST_CASE("ifft successfully calculates pure sine of half the sample rate") { for (uint32_t i = 0; i < fft_width; i++) CHECK(v[i].imag() == 0); - free(v); - free(tmp); + delete[] v; + delete[] tmp; }