kopia lustrzana https://github.com/f4exb/sdrangel
				
				
				
			
		
			
				
	
	
		
			253 wiersze
		
	
	
		
			6.7 KiB
		
	
	
	
		
			C++
		
	
	
			
		
		
	
	
			253 wiersze
		
	
	
		
			6.7 KiB
		
	
	
	
		
			C++
		
	
	
// Copyright 2021 Mobilinkd LLC.
 | 
						|
 | 
						|
#pragma once
 | 
						|
 | 
						|
#include <vector>
 | 
						|
#include <cstddef>
 | 
						|
#include <cstdint>
 | 
						|
#include <numeric>
 | 
						|
#include <cassert>
 | 
						|
 | 
						|
#include "export.h"
 | 
						|
 | 
						|
namespace modemm17
 | 
						|
{
 | 
						|
 | 
						|
/**
 | 
						|
 * Calculate the phase estimates for each sample position.
 | 
						|
 *
 | 
						|
 * This performs a running calculation of the phase of each bit position.
 | 
						|
 * It is very noisy for individual samples, but quite accurate when
 | 
						|
 * averaged over an entire M17 frame.
 | 
						|
 *
 | 
						|
 * It is designed to be used to calculate the best bit position for each
 | 
						|
 * frame of data.  Samples are collected and averaged.  When update() is
 | 
						|
 * called, the best sample index and clock are estimated, and the counters
 | 
						|
 * reset for the next frame.
 | 
						|
 *
 | 
						|
 * It starts counting bit 0 as the first bit received after a reset.
 | 
						|
 *
 | 
						|
 * This is very efficient as it only uses addition and subtraction for
 | 
						|
 * each bit sample.  And uses one multiply and divide per update (per
 | 
						|
 * frame).
 | 
						|
 *
 | 
						|
 * This will permit a clock error of up to 500ppm.  This allows up to
 | 
						|
 * 250ppm error for both transmitter and receiver clocks.  This is
 | 
						|
 * less than one sample per frame when the sample rate is 48000 SPS.
 | 
						|
 *
 | 
						|
 * @inv current_index_ is in the interval [0, SAMPLES_PER_SYMBOL).
 | 
						|
 * @inv sample_index_ is in the interval [0, SAMPLES_PER_SYMBOL).
 | 
						|
 * @inv clock_ is in the interval [0.9995, 1.0005]
 | 
						|
 */
 | 
						|
class MODEMM17_API ClockRecovery
 | 
						|
{
 | 
						|
    std::vector<float> estimates_;
 | 
						|
    size_t sample_count_ = 0;
 | 
						|
    uint16_t frame_count_ = 0;
 | 
						|
    uint8_t sample_index_ = 0;
 | 
						|
    uint8_t prev_sample_index_ = 0;
 | 
						|
    uint8_t index_ = 0;
 | 
						|
    float offset_ = 0.0;
 | 
						|
    float clock_ = 1.0;
 | 
						|
    float prev_sample_ = 0.0;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Find the sample index.
 | 
						|
     *
 | 
						|
     * There are @p SAMPLES_PER_INDEX bins.  It is expected that half are
 | 
						|
     * positive values and half are negative.  The positive and negative
 | 
						|
     * bins will be grouped together such that there is a single transition
 | 
						|
     * from positive values to negative values.
 | 
						|
     *
 | 
						|
     * The best bit position is always the position with the positive value
 | 
						|
     * at that transition point.  It will be the bit index with the highest
 | 
						|
     * energy.
 | 
						|
     *
 | 
						|
     * @post sample_index_ contains the best sample point.
 | 
						|
     */
 | 
						|
    void update_sample_index_()
 | 
						|
    {
 | 
						|
        uint8_t index = 0;
 | 
						|
 | 
						|
        // Find falling edge.
 | 
						|
        bool is_positive = false;
 | 
						|
        for (size_t i = 0; i != samples_per_symbol_; ++i)
 | 
						|
        {
 | 
						|
            float phase = estimates_[i];
 | 
						|
 | 
						|
            if (!is_positive && phase > 0)
 | 
						|
            {
 | 
						|
                is_positive = true;
 | 
						|
            }
 | 
						|
            else if (is_positive && phase < 0)
 | 
						|
            {
 | 
						|
                index = i;
 | 
						|
                break;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        sample_index_ = index == 0 ? samples_per_symbol_ - 1 : index - 1;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Compute the drift in sample points from the last update.
 | 
						|
     *
 | 
						|
     * This should never be greater than one.
 | 
						|
     */
 | 
						|
    float calc_offset_()
 | 
						|
    {
 | 
						|
        int8_t offset = sample_index_ - prev_sample_index_;
 | 
						|
 | 
						|
        // When in spec, the clock should drift by less than 1 sample per frame.
 | 
						|
        if (offset >= max_offset_)
 | 
						|
        {
 | 
						|
            offset -= samples_per_symbol_;
 | 
						|
        }
 | 
						|
        else if (offset <= -max_offset_)
 | 
						|
        {
 | 
						|
            offset += samples_per_symbol_;
 | 
						|
        }
 | 
						|
 | 
						|
        return offset;
 | 
						|
    }
 | 
						|
 | 
						|
    void update_clock_()
 | 
						|
    {
 | 
						|
        // update_sample_index_() must be called first.
 | 
						|
 | 
						|
        if (frame_count_ == 0)
 | 
						|
        {
 | 
						|
            prev_sample_index_ = sample_index_;
 | 
						|
            offset_ = 0.0;
 | 
						|
            clock_ = 1.0;
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        offset_ += calc_offset_();
 | 
						|
        prev_sample_index_ = sample_index_;
 | 
						|
        clock_ = 1.0 + (offset_ / (frame_count_ * sample_count_));
 | 
						|
        clock_ = std::min(MAX_CLOCK, std::max(MIN_CLOCK, clock_));
 | 
						|
    }
 | 
						|
 | 
						|
public:
 | 
						|
    ClockRecovery(size_t sampleRate, size_t symbolRate) :
 | 
						|
        sampleRate_(sampleRate),
 | 
						|
        symbolRate_(symbolRate)
 | 
						|
    {
 | 
						|
        samples_per_symbol_ = sampleRate_ / symbolRate_;
 | 
						|
        max_offset_ = samples_per_symbol_ / 2;
 | 
						|
        dx_ = 1.0 / samples_per_symbol_;
 | 
						|
        estimates_.resize(samples_per_symbol_);
 | 
						|
        std::fill(estimates_.begin(), estimates_.end(), 0);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Update clock recovery with the given sample.  This will advance the
 | 
						|
     * current sample index by 1.
 | 
						|
     */
 | 
						|
    void operator()(float sample)
 | 
						|
    {
 | 
						|
        float dy = (sample - prev_sample_);
 | 
						|
 | 
						|
        if (sample + prev_sample_ < 0)
 | 
						|
        {
 | 
						|
            // Invert the phase estimate when sample midpoint is less than 0.
 | 
						|
            dy = -dy;
 | 
						|
        }
 | 
						|
 | 
						|
        prev_sample_ = sample;
 | 
						|
 | 
						|
        estimates_[index_] += dy;
 | 
						|
        index_ += 1;
 | 
						|
        if (index_ == samples_per_symbol_)
 | 
						|
        {
 | 
						|
            index_ = 0;
 | 
						|
        }
 | 
						|
        sample_count_ += 1;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Reset the state of the clock recovery system.  This should be called
 | 
						|
     * when a new transmission is detected.
 | 
						|
     */
 | 
						|
    void reset()
 | 
						|
    {
 | 
						|
        sample_count_ = 0;
 | 
						|
        frame_count_ = 0;
 | 
						|
        index_ = 0;
 | 
						|
        sample_index_ = 0;
 | 
						|
        std::fill(estimates_.begin(), estimates_.end(), 0);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Return the current sample index.  This will always be in the range of
 | 
						|
     * [0..SAMPLES_PER_SYMBOL).
 | 
						|
     */
 | 
						|
    uint8_t current_index() const
 | 
						|
    {
 | 
						|
        return index_;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Return the estimated sample clock increment based on the last update.
 | 
						|
     *
 | 
						|
     * The value is only valid after samples have been collected and update()
 | 
						|
     * has been called.
 | 
						|
     */
 | 
						|
    float clock_estimate() const
 | 
						|
    {
 | 
						|
        return clock_;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Return the estimated "best sample index" based on the last update.
 | 
						|
     *
 | 
						|
     * The value is only valid after samples have been collected and update()
 | 
						|
     * has been called.
 | 
						|
     */
 | 
						|
    uint8_t sample_index() const
 | 
						|
    {
 | 
						|
        return sample_index_;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Update the sample index and clock estimates, and reset the state for
 | 
						|
     * the next frame of data.
 | 
						|
     *
 | 
						|
     * @pre index_ = 0
 | 
						|
     * @pre sample_count_ > 0
 | 
						|
     *
 | 
						|
     * After this is called, sample_index() and clock_estimate() will have
 | 
						|
     * valid, updated results.
 | 
						|
     *
 | 
						|
     * The more samples between calls to update, the more accurate the
 | 
						|
     * estimates will be.
 | 
						|
     *
 | 
						|
     * @return true if the preconditions are met and the update has been
 | 
						|
     *  performed, otherwise false.
 | 
						|
     */
 | 
						|
    bool update()
 | 
						|
    {
 | 
						|
        if (!(sample_count_ != 0 && index_ == 0)) return false;
 | 
						|
 | 
						|
        update_sample_index_();
 | 
						|
        update_clock_();
 | 
						|
 | 
						|
        frame_count_ = std::min(0x1000, 1 + frame_count_);
 | 
						|
        sample_count_ = 0;
 | 
						|
        std::fill(estimates_.begin(), estimates_.end(), 0);
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
private:
 | 
						|
    size_t sampleRate_;
 | 
						|
    size_t symbolRate_;
 | 
						|
    size_t samples_per_symbol_;
 | 
						|
    int8_t max_offset_;
 | 
						|
    float dx_;
 | 
						|
 | 
						|
    static const float MAX_CLOCK;
 | 
						|
    static const float MIN_CLOCK;
 | 
						|
};
 | 
						|
 | 
						|
} // modemm17
 |