From acea17221ee65fd29e70719747fa1136900635d5 Mon Sep 17 00:00:00 2001 From: Karlis Goba Date: Wed, 8 Dec 2021 23:33:52 +0200 Subject: [PATCH] Further steps towards FT4 decoding --- .clang-format | 1 + decode_ft8.c | 5 +- ft8/constants.h | 24 +++--- ft8/decode.c | 217 +++++++++++++++++++++++++++++++----------------- ft8/decode.h | 29 +++---- 5 files changed, 175 insertions(+), 101 deletions(-) diff --git a/.clang-format b/.clang-format index 3b11ab3..0e24b80 100644 --- a/.clang-format +++ b/.clang-format @@ -13,6 +13,7 @@ AllowShortCaseLabelsOnASingleLine: false AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AllowShortFunctionsOnASingleLine: false +AlignTrailingComments: true BreakConstructorInitializers: BeforeColon ConstructorInitializerAllOnOneLineOrOnePerLine: true ConstructorInitializerIndentWidth: 0 diff --git a/decode_ft8.c b/decode_ft8.c index 7e0dda4..3d8a313 100644 --- a/decode_ft8.c +++ b/decode_ft8.c @@ -169,7 +169,7 @@ int main(int argc, char** argv) // Compute DSP parameters that depend on the sample rate const int num_bins = (int)(sample_rate / (2 * kFSK_dev)); // number bins of FSK tone width that the spectrum can be divided into - const int block_size = (int)(sample_rate / kFSK_dev); // samples corresponding to one FSK symbol + const int block_size = (int)(sample_rate / kFSK_dev); // samples corresponding to one FSK symbol const int subblock_size = block_size / kTime_osr; const int nfft = block_size * kFreq_osr; const int num_blocks = (num_samples - nfft + subblock_size) / block_size; @@ -183,7 +183,8 @@ int main(int argc, char** argv) .num_bins = num_bins, .time_osr = kTime_osr, .freq_osr = kFreq_osr, - .mag = mag_power + .mag = mag_power, + .block_stride = (kTime_osr * kFreq_osr * num_bins) }; extract_power(signal, &power, block_size); diff --git a/ft8/constants.h b/ft8/constants.h index d595cc7..938b914 100644 --- a/ft8/constants.h +++ b/ft8/constants.h @@ -4,20 +4,24 @@ #include // Define FT8 symbol counts -#define FT8_ND (58) ///< Data symbols -#define FT8_NS (21) ///< Sync symbols (3 @ Costas 7x7) -#define FT8_NN (79) ///< Total channel symbols (FT8_NS + FT8_ND) +#define FT8_ND (58) ///< Data symbols +#define FT8_NN (79) ///< Total channel symbols (FT8_NS + FT8_ND) +#define FT8_LENGTH_SYNC (7) ///< Length of each sync group +#define FT8_NUM_SYNC (3) ///< Number of sync groups +#define FT8_SYNC_OFFSET (36) ///< Offset between sync groups // Define FT4 symbol counts -#define FT4_ND (87) ///< Data symbols -#define FT4_NS (16) ///< Sync symbols (3 @ Costas 7x7) -#define FT4_NR (2) ///< Ramp symbols (beginning + end) -#define FT4_NN (105) ///< Total channel symbols (FT4_NS + FT4_ND + FT4_NR) +#define FT4_ND (87) ///< Data symbols +#define FT4_NR (2) ///< Ramp symbols (beginning + end) +#define FT4_NN (105) ///< Total channel symbols (FT4_NS + FT4_ND + FT4_NR) +#define FT4_LENGTH_SYNC (4) ///< Length of each sync group +#define FT4_NUM_SYNC (4) ///< Number of sync groups +#define FT4_SYNC_OFFSET (33) ///< Offset between sync groups // Define LDPC parameters -#define FT8_LDPC_N (174) ///< Number of bits in the encoded message (payload with LDPC checksum bits) -#define FT8_LDPC_K (91) ///< Number of payload bits (including CRC) -#define FT8_LDPC_M (83) ///< Number of LDPC checksum bits (FT8_LDPC_N - FT8_LDPC_K) +#define FT8_LDPC_N (174) ///< Number of bits in the encoded message (payload with LDPC checksum bits) +#define FT8_LDPC_K (91) ///< Number of payload bits (including CRC) +#define FT8_LDPC_M (83) ///< Number of LDPC checksum bits (FT8_LDPC_N - FT8_LDPC_K) #define FT8_LDPC_N_BYTES ((FT8_LDPC_N + 7) / 8) ///< Number of whole bytes needed to store 174 bits (full message) #define FT8_LDPC_K_BYTES ((FT8_LDPC_K + 7) / 8) ///< Number of whole bytes needed to store 91 bits (payload + CRC only) diff --git a/ft8/decode.c b/ft8/decode.c index 2ac600f..065f264 100644 --- a/ft8/decode.c +++ b/ft8/decode.c @@ -28,107 +28,175 @@ static void heapify_up(candidate_t heap[], int heap_size); static void decode_symbol(const uint8_t* power, int bit_idx, float* log174); static void decode_multi_symbols(const uint8_t* power, int num_bins, int n_syms, int bit_idx, float* log174); -static int get_index(const waterfall_t* power, int block, int time_sub, int freq_sub, int bin) +static int get_index(const waterfall_t* power, const candidate_t* candidate) { - return ((((block * power->time_osr) + time_sub) * power->freq_osr + freq_sub) * power->num_bins) + bin; + int offset = candidate->time_offset; + offset = (offset * power->time_osr) + candidate->time_sub; + offset = (offset * power->freq_osr) + candidate->freq_sub; + offset = (offset * power->num_bins) + candidate->freq_offset; + return offset; +} + +static int ft8_sync_score(const waterfall_t* power, const candidate_t* candidate) +{ + int score = 0; + int num_average = 0; + + // Get the pointer to symbol 0 of the candidate + const uint8_t* mag_cand = power->mag + get_index(power, candidate); + + // Compute average score over sync symbols (m+k = 0-7, 36-43, 72-79) + for (int m = 0; m < FT8_NUM_SYNC; ++m) + { + for (int k = 0; k < FT8_LENGTH_SYNC; ++k) + { + int block = (FT8_SYNC_OFFSET * m) + k; // relative to the message + int block_abs = candidate->time_offset + block; // relative to the captured signal + // Check for time boundaries + if (block_abs < 0) + continue; + if (block_abs >= power->num_blocks) + break; + + // Get the pointer to symbol 'block' of the candidate + const uint8_t* p8 = mag_cand + (block * power->block_stride); + + // Weighted difference between the expected and all other symbols + // Does not work as well as the alternative score below + // score += 8 * p8[kFT8_Costas_pattern[k]] - + // p8[0] - p8[1] - p8[2] - p8[3] - + // p8[4] - p8[5] - p8[6] - p8[7]; + // ++num_average; + + // Check only the neighbors of the expected symbol frequency- and time-wise + int sm = kFT8_Costas_pattern[k]; // Index of the expected bin + if (sm > 0) + { + // look at one frequency bin lower + score += p8[sm] - p8[sm - 1]; + ++num_average; + } + if (sm < 7) + { + // look at one frequency bin higher + score += p8[sm] - p8[sm + 1]; + ++num_average; + } + if ((k > 0) && (block_abs > 0)) + { + // look one symbol back in time + score += p8[sm] - p8[sm - power->block_stride]; + ++num_average; + } + if (((k + 1) < FT8_LENGTH_SYNC) && ((block_abs + 1) < power->num_blocks)) + { + // look one symbol forward in time + score += p8[sm] - p8[sm + power->block_stride]; + ++num_average; + } + } + } + + if (num_average > 0) + score /= num_average; + + return score; +} + +static int ft4_sync_score(const waterfall_t* power, const candidate_t* candidate) +{ + int score = 0; + int num_average = 0; + + // Get the pointer to symbol 0 of the candidate + const uint8_t* mag_cand = power->mag + get_index(power, candidate); + + // Compute average score over sync symbols (block = 1-4, 34-37, 67-70, 100-103) + for (int m = 0; m < FT4_NUM_SYNC; ++m) + { + for (int k = 0; k < FT4_LENGTH_SYNC; ++k) + { + int block = 1 + (FT4_SYNC_OFFSET * m) + k; + int block_abs = candidate->time_offset + block; + // Check for time boundaries + if (block_abs < 0) + continue; + if (block_abs >= power->num_blocks) + break; + + // Get the pointer to symbol 'block' of the candidate + const uint8_t* p4 = mag_cand + (block * power->block_stride); + + // Check only the neighbors of the expected symbol frequency- and time-wise + int sm = kFT4_Costas_pattern[m][k]; // Index of the expected bin + if (sm > 0) + { + // look at one frequency bin lower + score += p4[sm] - p4[sm - 1]; + ++num_average; + } + if (sm < 3) + { + // look at one frequency bin higher + score += p4[sm] - p4[sm + 1]; + ++num_average; + } + if ((k > 0) && (block_abs > 0)) + { + // look one symbol back in time + score += p4[sm] - p4[sm - power->block_stride]; + ++num_average; + } + if (((k + 1) < FT4_LENGTH_SYNC) && ((block_abs + 1) < power->num_blocks)) + { + // look one symbol forward in time + score += p4[sm] - p4[sm + power->block_stride]; + ++num_average; + } + } + } + + if (num_average > 0) + score /= num_average; + + return score; } int ft8_find_sync(const waterfall_t* power, int num_candidates, candidate_t heap[], int min_score) { int heap_size = 0; - int sym_stride = power->time_osr * power->freq_osr * power->num_bins; + candidate_t candidate; // Here we allow time offsets that exceed signal boundaries, as long as we still have all data bits. // I.e. we can afford to skip the first 7 or the last 7 Costas symbols, as long as we track how many // sync symbols we included in the score, so the score is averaged. - for (int time_sub = 0; time_sub < power->time_osr; ++time_sub) + for (candidate.time_sub = 0; candidate.time_sub < power->time_osr; ++candidate.time_sub) { - for (int freq_sub = 0; freq_sub < power->freq_osr; ++freq_sub) + for (candidate.freq_sub = 0; candidate.freq_sub < power->freq_osr; ++candidate.freq_sub) { - for (int time_offset = -12; time_offset < 24; ++time_offset) + for (candidate.time_offset = -12; candidate.time_offset < 24; ++candidate.time_offset) { - for (int freq_offset = 0; freq_offset + 7 < power->num_bins; ++freq_offset) + for (candidate.freq_offset = 0; (candidate.freq_offset + 7) < power->num_bins; ++candidate.freq_offset) { - int score = 0; - int num_average = 0; + candidate.score = ft8_sync_score(power, &candidate); - // Compute average score over sync symbols (m+k = 0-7, 36-43, 72-79) - for (int m = 0; m <= 72; m += 36) - { - for (int k = 0; k < 7; ++k) - { - int block = time_offset + m + k; - // Check for time boundaries - if (block < 0) - continue; - if (block >= power->num_blocks) - break; - - int offset = get_index(power, block, time_sub, freq_sub, freq_offset); - const uint8_t* p8 = power->mag + offset; - - // Weighted difference between the expected and all other symbols - // Does not work as well as the alternative score below - // score += 8 * p8[kFT8_Costas_pattern[k]] - - // p8[0] - p8[1] - p8[2] - p8[3] - - // p8[4] - p8[5] - p8[6] - p8[7]; - // ++num_average; - - // Check only the neighbors of the expected symbol frequency- and time-wise - int sm = kFT8_Costas_pattern[k]; // Index of the expected bin - if (sm > 0) - { - // look at one frequency bin lower - score += p8[sm] - p8[sm - 1]; - ++num_average; - } - if (sm < 7) - { - // look at one frequency bin higher - score += p8[sm] - p8[sm + 1]; - ++num_average; - } - if ((k > 0) && (block > 0)) - { - // look one symbol back in time - score += p8[sm] - p8[sm - sym_stride]; - ++num_average; - } - if ((k < 6) && ((block + 1) < power->num_blocks)) - { - // look one symbol forward in time - score += p8[sm] - p8[sm + sym_stride]; - ++num_average; - } - } - } - - if (num_average > 0) - score /= num_average; - - if (score < min_score) + if (candidate.score < min_score) continue; // If the heap is full AND the current candidate is better than // the worst in the heap, we remove the worst and make space - if (heap_size == num_candidates && score > heap[0].score) + if (heap_size == num_candidates && candidate.score > heap[0].score) { heap[0] = heap[heap_size - 1]; --heap_size; - heapify_down(heap, heap_size); } // If there's free space in the heap, we add the current candidate if (heap_size < num_candidates) { - heap[heap_size].score = score; - heap[heap_size].time_offset = time_offset; - heap[heap_size].freq_offset = freq_offset; - heap[heap_size].time_sub = time_sub; - heap[heap_size].freq_sub = freq_sub; + heap[heap_size] = candidate; ++heap_size; - heapify_up(heap, heap_size); } } @@ -152,8 +220,7 @@ int ft8_find_sync(const waterfall_t* power, int num_candidates, candidate_t heap void ft8_extract_likelihood(const waterfall_t* power, const candidate_t* cand, float* log174) { - int sym_stride = power->time_osr * power->freq_osr * power->num_bins; - int offset = get_index(power, cand->time_offset, cand->time_sub, cand->freq_sub, cand->freq_offset); + const uint8_t* mag_cand = power->mag + get_index(power, cand); // Go over FSK tones and skip Costas sync symbols const int n_syms = 1; @@ -175,7 +242,7 @@ void ft8_extract_likelihood(const waterfall_t* power, const candidate_t* cand, f else { // Pointer to 8 bins of the current symbol - const uint8_t* ps = power->mag + offset + (sym_idx * sym_stride); + const uint8_t* ps = mag_cand + (sym_idx * power->block_stride); decode_symbol(ps, bit_idx, log174); } diff --git a/ft8/decode.h b/ft8/decode.h index 7d960eb..f18a24d 100644 --- a/ft8/decode.h +++ b/ft8/decode.h @@ -12,39 +12,40 @@ /// Values freq_osr > 1 mean the tone spacing is further subdivided by FFT analysis. typedef struct { - int num_blocks; ///< number of total blocks (symbols) in terms of 160 ms time periods - int num_bins; ///< number of FFT bins in terms of 6.25 Hz - int time_osr; ///< number of time subdivisions - int freq_osr; ///< number of frequency subdivisions - uint8_t* mag; ///< FFT magnitudes stored as uint8_t[blocks][time_osr][freq_osr][num_bins] + int num_blocks; ///< number of total blocks (symbols) in terms of 160 ms time periods + int num_bins; ///< number of FFT bins in terms of 6.25 Hz + int time_osr; ///< number of time subdivisions + int freq_osr; ///< number of frequency subdivisions + uint8_t* mag; ///< FFT magnitudes stored as uint8_t[blocks][time_osr][freq_osr][num_bins] + int block_stride; ///< Helper value = time_osr * freq_osr * num_bins } waterfall_t; /// Output structure of ft8_find_sync() and input structure of ft8_decode(). /// Holds the position of potential start of a message in time and frequency. typedef struct { - int16_t score; ///< Candidate score (non-negative number; higher score means higher likelihood) + int16_t score; ///< Candidate score (non-negative number; higher score means higher likelihood) int16_t time_offset; ///< Index of the time block int16_t freq_offset; ///< Index of the frequency bin - uint8_t time_sub; ///< Index of the time subdivision used - uint8_t freq_sub; ///< Index of the frequency subdivision used + uint8_t time_sub; ///< Index of the time subdivision used + uint8_t freq_sub; ///< Index of the frequency subdivision used } candidate_t; /// Structure that holds the decoded message typedef struct { // TODO: check again that this size is enough - char text[25]; // plain text - uint16_t hash; // hash value to be used in hash table and quick checking for duplicates + char text[25]; ///< Plain text + uint16_t hash; ///< Hash value to be used in hash table and quick checking for duplicates } message_t; /// Structure that contains the status of various steps during decoding of a message typedef struct { - int ldpc_errors; - uint16_t crc_extracted; - uint16_t crc_calculated; - int unpack_status; + int ldpc_errors; ///< Number of LDPC errors during decoding + uint16_t crc_extracted; ///< CRC value recovered from the message + uint16_t crc_calculated; ///< CRC value calculated over the payload + int unpack_status; ///< Return value of the unpack routine } decode_status_t; /// Localize top N candidates in frequency and time according to their sync strength (looking at Costas symbols)