Optimized one-shot frequency synchronization

- Ramp-up length increased to 128 symbols (here is room for
  improvement!)
- Try to detect the frequency once during ramp-up. To do so, every
  second symbol is inverted (to remove the +/-1 symbol toggling) and the
  phase difference between neigboring resulting symbols is checked. When
  it is low enough for all symbols, the frequency is estimated and
  corrected. When frequency estimation was done, it is not retried for a
  number of incoming symbols in order to allow the timing estimator to
  converge again.
- This approach was verified in a simulated loopback test with frequency
  offset and AWGN.
This commit is contained in:
Thomas Kolb 2023-05-17 22:28:18 +02:00
parent b5bae84994
commit f5a367464f
9 changed files with 150 additions and 65 deletions

View file

@ -2,5 +2,5 @@
mkdir -p build mkdir -p build
cd build cd build
cmake .. cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=true ..
make make

View file

@ -29,7 +29,7 @@
#define RRC_DELAY 7 // delay in symbols #define RRC_DELAY 7 // delay in symbols
#define RRC_BETA 0.2f #define RRC_BETA 0.2f
#define TRANSMISSION_RAMP_UP_LEN 64 // symbols #define TRANSMISSION_RAMP_UP_LEN 128 // symbols
#define TRANSMISSION_RAMP_DOWN_LEN 32 // symbols #define TRANSMISSION_RAMP_DOWN_LEN 32 // symbols
/*** SDR CONFIG ***/ /*** SDR CONFIG ***/
@ -43,7 +43,7 @@
// actually transmitted or received signal frequencies, NOT the SDR center frequency. // actually transmitted or received signal frequencies, NOT the SDR center frequency.
#define SDR_TX_FREQ 434.100e6f #define SDR_TX_FREQ 434.100e6f
#define SDR_RX_FREQ 434.100e6f #define SDR_RX_FREQ 434.115e6f
// shift applied in the baseband, to get rid of SDR DC peak. If the value here // shift applied in the baseband, to get rid of SDR DC peak. If the value here
// is not 0, software mixing will be done on the received signal. // is not 0, software mixing will be done on the received signal.

View file

@ -5,6 +5,8 @@
#include "freq_est.h" #include "freq_est.h"
#include "utils.h"
#include "correlator.h" #include "correlator.h"
bool correlator_init(correlator_ctx_t *ctx, const float complex *search_seq, size_t nsymbols) bool correlator_init(correlator_ctx_t *ctx, const float complex *search_seq, size_t nsymbols)
@ -142,34 +144,8 @@ float correlator_get_mean_frequency_deviation(correlator_ctx_t *ctx, size_t L, f
size_t m0 = ctx->search_sequence_len - L; size_t m0 = ctx->search_sequence_len - L;
for(size_t m = m0; m < ctx->search_sequence_len; m++) { for(size_t m = m0; m < ctx->search_sequence_len; m++) {
size_t nm = (n + m) & ctx->buffer_size_mask; size_t nm = (n + m) & ctx->buffer_size_mask;
z[m - m0] = ctx->search_sequence[m] * ctx->input_history[nm]; z[m - m0] = conjf(ctx->search_sequence[m]) * ctx->input_history[nm];
} }
/* Louise & Regiannini frequency estimator */
/*
// Calculate averaged phase increments for <N> sub-sequences
size_t N = L/2;
float complex R_kappa[N];
for(size_t kappa = 0; kappa < N; kappa++) {
for(size_t k = 0; k < L - kappa; k++) {
R_kappa[kappa] += z[k + kappa] * conj(z[k]);
}
R_kappa[kappa] /= L - kappa;
}
// Calculate phase estimate (in radians/sample)
float complex sum_R_kappa = 0;
for(size_t kappa = 0; kappa < N; kappa++) {
sum_R_kappa += R_kappa[kappa];
}
float arg = cargf(sum_R_kappa);
return arg / (1 + N);
*/
return freq_est_data_free(z, L, phase_offset); return freq_est_data_free(z, L, phase_offset);
} }

View file

@ -1,9 +1,11 @@
#include <liquid/liquid.h> #include <liquid/liquid.h>
#include <math.h>
#include "freq_est.h" #include "freq_est.h"
#include "utils.h" #include "utils.h"
#if 0
// Fits a line y' = m*x' + t to the given points (x, y), where x is the // Fits a line y' = m*x' + t to the given points (x, y), where x is the
// zero-based index in the array. // zero-based index in the array.
static void linear_regression(size_t nx, const float *y, float *m, float *t) static void linear_regression(size_t nx, const float *y, float *m, float *t)
@ -27,7 +29,6 @@ static void linear_regression(size_t nx, const float *y, float *m, float *t)
*t = mean_y - (*m) * mean_x; *t = mean_y - (*m) * mean_x;
} }
#if 0
// Calculates an estimation of the true peak frequency in the signal by // Calculates an estimation of the true peak frequency in the signal by
// calculating the FFT und interpolating around the bin with the highest value. // calculating the FFT und interpolating around the bin with the highest value.
static float fft_true_peak(const float complex *recv, size_t n) static float fft_true_peak(const float complex *recv, size_t n)
@ -100,7 +101,7 @@ float freq_est_lr(const float complex *recv, size_t l0)
float arg = cargf(R_total); float arg = cargf(R_total);
// convert to frequency offset in radians per sample // convert to frequency offset in radians per sample
float freq_offset = arg / (N+1) / 2.0f; /* FIXME: not sure where the factor 2 comes from. */ float freq_offset = 2.0f * arg / (N+1); /* FIXME: check where the factor 2.0 comes from… */
return freq_offset; return freq_offset;
} }
@ -128,6 +129,42 @@ float freq_est_unknown_bpsk(const float complex *recv, size_t n, float *final_ph
} }
float freq_est_in_rampup(const float complex *recv, size_t n, float *final_phase)
{
float complex rotated[n];
float prev_phase = 0.0f;
// rotate every second symbol by 180°. As a plausibility check that we are
// in fact inside the ramp-up, we verify that there is no phase rotation by
// more than 90° between the symbols.
for(size_t i = 0; i < n; i++) {
if((i % 2) == 0) {
rotated[i] = recv[i];
} else {
rotated[i] = -recv[i];
}
float phase = cargf(rotated[i]);
if(i > 0) {
float phase_delta = phase - prev_phase;
if(fabsf(phase_delta) > 3.14159f/4.0f) {
// abort because we either have the wrong signal or too much noise.
if(final_phase) {
*final_phase = 0.0f;
}
return 0.0f;
}
}
prev_phase = phase;
}
// signal is clean (and all symbol information removed), so we run the estimator
return freq_est_data_free(rotated, n, final_phase);
}
float freq_est_known_symbols(const float complex *recv, const float complex *ref, size_t n, float *final_phase) float freq_est_known_symbols(const float complex *recv, const float complex *ref, size_t n, float *final_phase)
{ {
float complex symbols[n]; float complex symbols[n];
@ -140,6 +177,7 @@ float freq_est_known_symbols(const float complex *recv, const float complex *ref
return freq_est_data_free(symbols, n, final_phase); return freq_est_data_free(symbols, n, final_phase);
} }
float freq_est_data_free(const float complex *symbols, size_t n, float *final_phase) float freq_est_data_free(const float complex *symbols, size_t n, float *final_phase)
{ {
float phases[n]; float phases[n];
@ -151,10 +189,18 @@ float freq_est_data_free(const float complex *symbols, size_t n, float *final_ph
liquid_unwrap_phase(phases, n); liquid_unwrap_phase(phases, n);
float freq, phi0; float freq, phi0;
linear_regression(n, phases, &freq, &phi0); freq = freq_est_lr(symbols, n);
// calculate the mean of unwrapped symbol phases and from that the
// de-noised phase at the end of the sequence.
phi0 = 0.0f;
for(size_t i = 0; i < n; i++) {
phi0 += phases[i];
}
phi0 /= n;
if(final_phase) { if(final_phase) {
*final_phase = phi0 + n * freq; *final_phase = phi0 + (n+1)/2.0f * freq;
} }
return freq; return freq;

View file

@ -35,6 +35,23 @@ float freq_est_unknown_bpsk(const float complex *recv, size_t n, float *final_ph
*/ */
float freq_est_known_symbols(const float complex *recv, const float complex *ref, size_t n, float *final_phase); float freq_est_known_symbols(const float complex *recv, const float complex *ref, size_t n, float *final_phase);
/*!
* \brief Estimate the frequency during ramp-up.
*
* This is a special version for frequency estimation during signal ramp-up
* where alternating +1/-1 BPSK symbols are transmitted. Therefore, every
* second symbol is inverted (rotated by 180°). As a plausibility check, the
* phase between neighboring symbols is calculated after rotation and if it
* changes by more than 90°, frequency estimation is skipped.
*
* \param recv Pointer to the array of received symbols.
* \param n Number of symbols given.
* \param final_phase Pointer to a float where the phase correction value is
* stored. May be NULL if that value is not required.
* \returns The estimated frequency in radians per sample.
*/
float freq_est_in_rampup(const float complex *recv, size_t n, float *final_phase);
/*! /*!
* \brief Estimate the frequency from data-free symbols. * \brief Estimate the frequency from data-free symbols.
* *

View file

@ -15,28 +15,32 @@
#include "utils.h" #include "utils.h"
#define HEADER_SIZE_BYTES 4 #define HEADER_SIZE_BYTES 4
#define FREQ_EST_L 8 #define FREQ_EST_L 24
#define AGC_BW_ACQUISITION 5e-2f #define AGC_BW_ACQUISITION 5e-2f
#define AGC_BW_TRACKING 1e-4f #define AGC_BW_TRACKING 1e-4f
#define MAX_COARSE_FREQ_OFFSET 0.20f #define MAX_COARSE_FREQ_OFFSET 0.20f
#define SHOW_DEBUG_LOG 0 #define SHOW_DEBUG_LOG 1
#if SHOW_DEBUG_LOG #if SHOW_DEBUG_LOG
# define DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__) # define DEBUG_LOG(...) fprintf(stderr, "DBG: " __VA_ARGS__)
#else #else
# define DEBUG_LOG(...) {} # define DEBUG_LOG(...) {}
#endif #endif
static void update_nco_pll(nco_crcf nco, float phase_error) static void update_nco_pll(nco_crcf nco, float phase_error)
{ {
static const float pll_alpha = 0.20f; // phase adjustment factor static const float pll_alpha = 0.03f; // phase adjustment factor
static const float pll_beta = (pll_alpha * pll_alpha) / 2.0f; // frequency adjustment factor static const float pll_beta = (pll_alpha * pll_alpha) / 2.0f; // frequency adjustment factor
nco_crcf_adjust_phase(nco, pll_alpha * phase_error); nco_crcf_adjust_phase(nco, pll_alpha * phase_error);
nco_crcf_adjust_frequency(nco, pll_beta * phase_error); nco_crcf_adjust_frequency(nco, pll_beta * phase_error);
DEBUG_LOG("NCO adjusted: f=%.6f, φ=%.6f\n",
nco_crcf_get_frequency(nco),
nco_crcf_get_phase(nco));
} }
@ -45,6 +49,8 @@ static bool acquire_preamble(layer1_rx_t *rx, const float complex sample)
static float complex freq_est_history[FREQ_EST_L]; static float complex freq_est_history[FREQ_EST_L];
static unsigned int freq_est_history_write_idx = 0; static unsigned int freq_est_history_write_idx = 0;
static uint32_t freq_est_holdoff_samples = 0;
// preamble search // preamble search
float complex corr_out = correlator_step(&rx->preamble_correlator, sample); float complex corr_out = correlator_step(&rx->preamble_correlator, sample);
@ -59,10 +65,12 @@ static bool acquire_preamble(layer1_rx_t *rx, const float complex sample)
DEBUG_LOG("Preamble frequency deviation: %.6f rad/symbol\n", mean_frequency_error); DEBUG_LOG("Preamble frequency deviation: %.6f rad/symbol\n", mean_frequency_error);
// Set the fine carrier correction NCO to the values estimated from the preamble // Set the fine carrier correction NCO to the values estimated from the preamble
nco_crcf_set_frequency(rx->carrier_fine_nco, mean_frequency_error / RRC_SPS * 0.5f); nco_crcf_set_frequency(rx->carrier_fine_nco, mean_frequency_error);
nco_crcf_set_phase(rx->carrier_fine_nco, phase_offset); nco_crcf_set_phase(rx->carrier_fine_nco, phase_offset);
DEBUG_LOG("New estimated carrier frequency: %.6f + %.6f\n", nco_crcf_get_frequency(rx->carrier_coarse_nco), nco_crcf_get_frequency(rx->carrier_fine_nco)); float fcoarse = nco_crcf_get_frequency(rx->carrier_coarse_nco);
float ffine = nco_crcf_get_frequency(rx->carrier_fine_nco);
DEBUG_LOG("New estimated carrier frequency: %.6f + %.6f/4 = %.6f rad/sample\n", fcoarse, ffine, fcoarse+ffine/4);
//float complex input_history[preamble_get_symbol_count()]; //float complex input_history[preamble_get_symbol_count()];
//correlator_get_input_history(&rx->preamble_correlator, input_history); //correlator_get_input_history(&rx->preamble_correlator, input_history);
@ -79,19 +87,21 @@ static bool acquire_preamble(layer1_rx_t *rx, const float complex sample)
return true; // preamble found! return true; // preamble found!
} else { } else {
// preamble not found. // preamble not found.
//DEBUG_LOG("Preamble not found: %.3f < %.3f\n", cabsf(corr_out), 0.5f * preamble_get_symbol_count());
// Run the frequency estimator for every incoming sample overy the last // Run the frequency estimator for every incoming sample overy the last
// FREQ_EST_L samples. This is an implementation that works with unknown // FREQ_EST_L samples. This is an implementation that works with unknown
// BPSK symbols and therefore can be used during ramp-up and preamble. // BPSK symbols and therefore can be used during ramp-up and preamble.
if(freq_est_holdoff_samples == 0) { //freq_est_history_write_idx == FREQ_EST_L) {
float complex linearized_history[FREQ_EST_L]; float complex linearized_history[FREQ_EST_L];
for(size_t i = 0; i < FREQ_EST_L; i++) { for(size_t i = 0; i < FREQ_EST_L; i++) {
linearized_history[i] = freq_est_history[(i + freq_est_history_write_idx) % FREQ_EST_L]; linearized_history[i] = freq_est_history[(i + freq_est_history_write_idx) % FREQ_EST_L];
} }
float freq_est = freq_est_unknown_bpsk(linearized_history, FREQ_EST_L, NULL); float freq_est = freq_est_in_rampup(freq_est_history, FREQ_EST_L, NULL);
float freq_adjustment = freq_est / 32.0f; float freq_adjustment = freq_est / RRC_SPS;
nco_crcf_adjust_frequency(rx->carrier_coarse_nco, freq_adjustment); nco_crcf_adjust_frequency(rx->carrier_coarse_nco, freq_adjustment);
float actual_freq = nco_crcf_get_frequency(rx->carrier_coarse_nco); float actual_freq = nco_crcf_get_frequency(rx->carrier_coarse_nco);
@ -103,7 +113,17 @@ static bool acquire_preamble(layer1_rx_t *rx, const float complex sample)
nco_crcf_set_frequency(rx->carrier_coarse_nco, -MAX_COARSE_FREQ_OFFSET); nco_crcf_set_frequency(rx->carrier_coarse_nco, -MAX_COARSE_FREQ_OFFSET);
} }
DEBUG_LOG("Freq. est: %0.6f - Adj: %.6f - carrier frequency: %.6f\n", freq_est, freq_adjustment, nco_crcf_get_frequency(rx->carrier_coarse_nco)); //freq_est_history_write_idx = 0;
if(freq_est != 0.0f) {
DEBUG_LOG("Freq. est (x%d): %0.6f - Adj (x1): %.6f - carrier frequency (x1): %.6f\n",
RRC_SPS, freq_est, freq_adjustment, nco_crcf_get_frequency(rx->carrier_coarse_nco));
freq_est_holdoff_samples = preamble_get_symbol_count() * 2;
}
} else {
freq_est_holdoff_samples--;
}
assert(freq_est_history_write_idx < FREQ_EST_L); assert(freq_est_history_write_idx < FREQ_EST_L);
@ -123,11 +143,15 @@ result_t layer1_rx_process(layer1_rx_t *rx, const float complex *samples, size_t
float complex samples2dump[sample_count]; float complex samples2dump[sample_count];
size_t nsamples2dump = 0; size_t nsamples2dump = 0;
float complex samples2dump2[sample_count];
size_t nsamples2dump2 = 0;
// filter the input // filter the input
float complex filtered_samples[sample_count]; float complex filtered_samples[sample_count];
firfilt_crcf_execute_block(rx->channel_filter, (float complex *)samples, sample_count, filtered_samples); firfilt_crcf_execute_block(rx->channel_filter, (float complex *)samples, sample_count, filtered_samples);
fprintf(stderr, "\nagc: %f\n", agc_crcf_get_gain(rx->agc));
for(unsigned int i = 0; i < sample_count; i++) { for(unsigned int i = 0; i < sample_count; i++) {
rx_state_t last_state = rx->state; rx_state_t last_state = rx->state;
@ -149,6 +173,7 @@ result_t layer1_rx_process(layer1_rx_t *rx, const float complex *samples, size_t
symsync_crcf_execute(rx->symsync, &mixed_sample, 1, &symsync_out, &out_len); symsync_crcf_execute(rx->symsync, &mixed_sample, 1, &symsync_out, &out_len);
if(out_len != 0) { if(out_len != 0) {
switch(rx->state) { switch(rx->state) {
// Try to acquire packets by synchronizing the frequency // Try to acquire packets by synchronizing the frequency
// (symbol-independent search) and correlating the preamble. // (symbol-independent search) and correlating the preamble.
@ -175,13 +200,16 @@ result_t layer1_rx_process(layer1_rx_t *rx, const float complex *samples, size_t
break; break;
case RX_STATE_HEADER: case RX_STATE_HEADER:
nco_crcf_step(rx->carrier_fine_nco);
nco_crcf_mix_down(rx->carrier_fine_nco, symsync_out, &mixed_sample); nco_crcf_mix_down(rx->carrier_fine_nco, symsync_out, &mixed_sample);
if(symbol_counter < rx->hdr_len_symbols) { if(symbol_counter < rx->hdr_len_symbols) {
unsigned int sym_demod; unsigned int sym_demod;
modem_demodulate(rx->hdr_demod, mixed_sample, &sym_demod); modem_demodulate(rx->hdr_demod, mixed_sample, &sym_demod);
float phase_error = modem_get_demodulator_phase_error(rx->hdr_demod); float phase_error = modem_get_demodulator_phase_error(rx->hdr_demod);
DEBUG_LOG("Sym: %d; Phase error: %f\n", sym_demod, phase_error); //DEBUG_LOG("Sym: %d; Phase error: %f\n", sym_demod, phase_error);
update_nco_pll(rx->carrier_fine_nco, phase_error); update_nco_pll(rx->carrier_fine_nco, phase_error);
@ -230,13 +258,16 @@ result_t layer1_rx_process(layer1_rx_t *rx, const float complex *samples, size_t
break; break;
case RX_STATE_DATA: case RX_STATE_DATA:
nco_crcf_step(rx->carrier_fine_nco);
nco_crcf_mix_down(rx->carrier_fine_nco, symsync_out, &mixed_sample); nco_crcf_mix_down(rx->carrier_fine_nco, symsync_out, &mixed_sample);
samples2dump2[nsamples2dump2++] = mixed_sample;
if(symbol_counter < rx->payload_len_symbols) { if(symbol_counter < rx->payload_len_symbols) {
unsigned int sym_demod; unsigned int sym_demod;
modem_demodulate(rx->payload_demod, mixed_sample, &sym_demod); modem_demodulate(rx->payload_demod, mixed_sample, &sym_demod);
float phase_error = modem_get_demodulator_phase_error(rx->payload_demod); float phase_error = modem_get_demodulator_phase_error(rx->payload_demod);
DEBUG_LOG("Sym: %d; Phase error: %f\n", sym_demod, phase_error); //DEBUG_LOG("Sym: %d; Phase error: %f\n", sym_demod, phase_error);
update_nco_pll(rx->carrier_fine_nco, phase_error); update_nco_pll(rx->carrier_fine_nco, phase_error);
@ -290,6 +321,7 @@ result_t layer1_rx_process(layer1_rx_t *rx, const float complex *samples, size_t
assert(nsamples2dump <= (sizeof(samples2dump) / sizeof(samples2dump[0]))); assert(nsamples2dump <= (sizeof(samples2dump) / sizeof(samples2dump[0])));
dump_array_cf(samples2dump, nsamples2dump, 1.0f/(RRC_SPS * SYMBOL_RATE), "/tmp/rx_dbg.cpx64"); dump_array_cf(samples2dump, nsamples2dump, 1.0f/(RRC_SPS * SYMBOL_RATE), "/tmp/rx_dbg.cpx64");
dump_array_cf(samples2dump2, nsamples2dump2, 1.0f/(SYMBOL_RATE), "/tmp/rx_dbg2.cpx64");
return OK; return OK;
} }

View file

@ -218,6 +218,9 @@ int main(void)
unsigned rx_retries = 0; unsigned rx_retries = 0;
double old = get_hires_time();
size_t total_samples = 0;
while(m_running) { while(m_running) {
double now = get_hires_time(); double now = get_hires_time();
@ -290,6 +293,13 @@ int main(void)
} }
} }
double new = get_hires_time();
total_samples += n_rf_samples;
double rate = total_samples / (new - old);
fprintf(stderr, "\nEstimated rate: %.3f MS/s\n", rate / 1e6);
rx_retries = 0; rx_retries = 0;
fprintf(stderr, "r"); fprintf(stderr, "r");

View file

@ -14,7 +14,9 @@ typedef enum {
#ifdef DEBUG_LIQUID #ifdef DEBUG_LIQUID
#include <stdio.h> #include <stdio.h>
# define ERR_CHECK_LIQUID(call) \ #include <liquid/liquid.h>
#define ERR_CHECK_LIQUID(call) \
do { \ do { \
liquid_error_code lq_result; \ liquid_error_code lq_result; \
if((lq_result = (call)) != LIQUID_OK) { \ if((lq_result = (call)) != LIQUID_OK) { \

View file

@ -3,6 +3,7 @@
#include <string.h> #include <string.h>
#include <math.h> #include <math.h>
#include <ctype.h> #include <ctype.h>
#include <time.h>
#include <liquid/liquid.h> #include <liquid/liquid.h>
@ -107,11 +108,12 @@ int main(void)
// ** Initialize ** // ** Initialize **
srand(time(NULL));
RESULT_CHECK(layer1_tx_init(&tx)); RESULT_CHECK(layer1_tx_init(&tx));
RESULT_CHECK(layer1_rx_init(&rx, cb_rx)); RESULT_CHECK(layer1_rx_init(&rx, cb_rx));
// ** Generate packets ** // ** Generate packets **
RESULT_CHECK(layer1_tx_reset(&tx)); RESULT_CHECK(layer1_tx_reset(&tx));
@ -139,8 +141,8 @@ int main(void)
// ** Simulate channel effects ** // ** Simulate channel effects **
channel_cccf ch = channel_cccf_create(); channel_cccf ch = channel_cccf_create();
//channel_cccf_add_awgn(ch, -30, 15); channel_cccf_add_awgn(ch, -30, 10);
channel_cccf_add_carrier_offset(ch, 0.01f, 1.23f); channel_cccf_add_carrier_offset(ch, 0.05f, 1.23f);
float complex whole_burst_ch[burst_len]; float complex whole_burst_ch[burst_len];
channel_cccf_execute_block(ch, (float complex*)whole_burst, burst_len, whole_burst_ch); channel_cccf_execute_block(ch, (float complex*)whole_burst, burst_len, whole_burst_ch);