From 4926cf8df5340b88a85935aa1b2081cc91f67e50 Mon Sep 17 00:00:00 2001 From: Kyle Reed <3761006+kallanreed@users.noreply.github.com> Date: Sat, 9 Sep 2023 06:18:42 -0700 Subject: [PATCH] POCSAG2 Revised bit extractor (#1439) * Better bit extraction WIP * Parallel clock signal detection for bit extraction * Relax clock detection accuracy. * Reset RateInfo state. TODOs --- firmware/baseband/proc_pocsag2.cpp | 177 ++++++++++------------------- firmware/baseband/proc_pocsag2.hpp | 52 ++++----- 2 files changed, 77 insertions(+), 152 deletions(-) diff --git a/firmware/baseband/proc_pocsag2.cpp b/firmware/baseband/proc_pocsag2.cpp index 7f85eca7..e9e0b3bb 100644 --- a/firmware/baseband/proc_pocsag2.cpp +++ b/firmware/baseband/proc_pocsag2.cpp @@ -36,7 +36,7 @@ using namespace std; namespace { /* Count of bits that differ between the two values. */ -uint8_t differ_bit_count(uint32_t left, uint32_t right) { +uint8_t diff_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) { @@ -127,150 +127,89 @@ uint32_t BitQueue::data() const { 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_; + auto sample = audio.p[i]; + samples_until_next_ -= 1; - // 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(); + if (!current_rate_) { + // Feed the known rate queues for clock detection. + for (auto& rate : known_rates_) { + if (handle_sample(rate, sample) && + diff_bit_count(rate.bits.data(), clock_magic_number) <= 2) { + // Clock detected. + // NB: This block should only happen on the second sample of a pulse. + // samples_until_next_ to start sampling the *next* pulse. + current_rate_ = &rate; + samples_until_next_ = rate.sample_interval; + ready_to_send_ = false; + } + } } - // 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. + // Have a clock rate and it's time to process the next sample. + if (current_rate_ && samples_until_next_ <= 0) { + // TODO: It seems like it would be possible to combine this + // code with handle_sample. Nearly the same work. - if (current_rate_) - next_bit_center_ += current_rate_->bit_length; + // Only send on the second sample of a bit. + // Sampling twice helps mitigate noisy audio data. + if (ready_to_send_) { + auto value = (prev_sample_ + sample) / 2; + bits_.push(signbit(value)); // NB: negative == '1' + } + + ready_to_send_ = !ready_to_send_; + prev_sample_ = sample; + samples_until_next_ += current_rate_->sample_interval; } - - 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(); + // Sampling at 2x the baud rate to synchronize to bit transitions + // without needing to know exact transition boundaries. + for (auto& rate : known_rates_) + rate.sample_interval = sample_rate / (2.0 * rate.baud_rate); } void BitExtractor::reset() { current_rate_ = nullptr; - rate_misses_ = 0; + samples_until_next_ = 0.0; + prev_sample_ = 0.0; + ready_to_send_ = false; - sample_ = 0.0; - last_sample_ = 0.0; - next_bit_center_ = 0.0; - - sample_index_ = 0; - last_transition_index_ = 0; - bad_transitions_ = 0; + for (auto& rate : known_rates_) { + rate.samples_until_next = 0.0; + rate.last_sample = 0.0; + rate.bits.reset(); + } } 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_; +bool BitExtractor::handle_sample(RateInfo& rate, float sample) { + // TODO: Still getting some clock misses at the start of messages. + rate.samples_until_next -= 1; - // Length is too short, ignore this. - if (length <= min_valid_length_) return false; + // Not time to process a sample yet. + if (rate.samples_until_next > 0) + 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; + // Sample signs are the same, both samples are in the same bit pulse. + auto has_new_bit = signbit(sample) == signbit(rate.last_sample); + if (has_new_bit) + rate.bits.push(signbit(sample)); // NB: negative == '1' - // 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; + // How long until the next sample? + rate.samples_until_next += rate.sample_interval; + rate.last_sample = sample; - // 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; + return has_new_bit; } /* CodewordExtractor *************************************/ @@ -286,9 +225,9 @@ void CodewordExtractor::process_bits() { // Wait for the sync frame. if (!has_sync_) { - if (differ_bit_count(data_, sync_codeword) <= 2) + if (diff_bit_count(data_, sync_codeword) <= 2) handle_sync(/*inverted=*/false); - else if (differ_bit_count(data_, ~sync_codeword) <= 2) + else if (diff_bit_count(data_, ~sync_codeword) <= 2) handle_sync(/*inverted=*/true); continue; } diff --git a/firmware/baseband/proc_pocsag2.hpp b/firmware/baseband/proc_pocsag2.hpp index 97623ba0..247c7808 100644 --- a/firmware/baseband/proc_pocsag2.hpp +++ b/firmware/baseband/proc_pocsag2.hpp @@ -84,52 +84,38 @@ class BitExtractor { 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; + /* Clock signal detection magic number. */ + static constexpr uint32_t clock_magic_number = 0xAAAAAAAA; - /* Number of rate misses that would cause a rate update. */ - static constexpr uint8_t bad_transition_reset_threshold = 10; + struct RateInfo { + const int16_t baud_rate = 0; + float sample_interval = 0.0; - struct BaudInfo { - uint16_t baud_rate = 0; - float bit_length = 0.0; - float min_bit_length = 0.0; - float max_bit_length = 0.0; + float samples_until_next = 0.0; + float last_sample = 0.0; + BitQueue bits{}; }; - /* Handle a transition, returns true if "good". */ - bool handle_transition(); + /* Updates a rate info with the given sample. + * Returns true if the rate info has a new bit in its queue. */ + bool handle_sample(RateInfo& rate, float sample); - /* 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}}; + std::array known_rates_{ + RateInfo{512}, + RateInfo{1200}, + RateInfo{2400}}; BitQueue& bits_; uint32_t sample_rate_ = 0; - uint16_t min_valid_length_ = 0; - const BaudInfo* current_rate_ = nullptr; - uint8_t rate_misses_ = 0; + RateInfo* current_rate_ = nullptr; - 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; + float samples_until_next_ = 0.0; + float prev_sample_ = 0.0; + bool ready_to_send_ = false; }; /* Extracts codeword batches from the BitQueue. */