From 396f66a1f3d44206096c149484ee6f6d281245e0 Mon Sep 17 00:00:00 2001 From: Silvano Seva Date: Mon, 13 Dec 2021 21:33:46 +0100 Subject: [PATCH] Implementation of M17 Viterbi decoder and associated unit test --- meson.build | 23 ++- openrtx/include/protocols/M17/M17Viterbi.h | 219 +++++++++++++++++++++ tests/unit/M17_viterbi.cpp | 89 +++++++++ 3 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 openrtx/include/protocols/M17/M17Viterbi.h create mode 100644 tests/unit/M17_viterbi.cpp diff --git a/meson.build b/meson.build index 94e8a7d5..37b8bc06 100644 --- a/meson.build +++ b/meson.build @@ -599,17 +599,20 @@ endforeach ## ----------------------------------- Unit Tests ------------------------------ ## -unit_test_opts = {'c_args': linux_c_args, - 'cpp_args' : linux_cpp_args, +unit_test_opts = {'c_args' : linux_c_args, + 'cpp_args' : linux_cpp_args, 'include_directories': linux_inc, - 'dependencies': linux_dep, - 'link_args' : linux_l_args} + 'dependencies' : linux_dep, + 'link_args' : linux_l_args} unit_test_src = openrtx_src + minmea_src + linux_platform_src -#dummy_test = executable('dummy', 'tests/unit/dummy.c') -#test('Dummy Unit Test', dummy_test) - m17_golay_test = executable('m17_golay_test', - sources: unit_test_src + ['tests/unit/M17_golay.cpp'], - kwargs: unit_test_opts) -test('M17 Golay Unit Test', m17_golay_test) + sources : unit_test_src + ['tests/unit/M17_golay.cpp'], + kwargs : unit_test_opts) + +m17_viterbi_test = executable('m17_viterbi_test', + sources : unit_test_src + ['tests/unit/M17_viterbi.cpp'], + kwargs : unit_test_opts) + +test('M17 Golay Unit Test', m17_golay_test) +test('M17 Viterbi Unit Test', m17_viterbi_test) diff --git a/openrtx/include/protocols/M17/M17Viterbi.h b/openrtx/include/protocols/M17/M17Viterbi.h new file mode 100644 index 00000000..c151eb1f --- /dev/null +++ b/openrtx/include/protocols/M17/M17Viterbi.h @@ -0,0 +1,219 @@ +/*************************************************************************** + * Copyright (C) 2021 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN * + * Frederik Saraci IU2NRO * + * Silvano Seva IU2KWO * + * * + * Adapted from original code written by Phil Karn KA9Q * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, see * + ***************************************************************************/ + +#ifndef M17_VITERBI_H +#define M17_VITERBI_H + +#ifndef __cplusplus +#error This header is C++ only! +#endif + +#include +#include +#include +#include +#include "M17Utils.h" + +/** + * Hard decision Viterbi decoder tailored on M17 protocol specifications, + * that is for decoding of data encoded with a convolutional encoder with a + * coder rate R = 1/2, a constraint length K = 5 and polynomials G1 = 0x19 and + * G2 = 0x17. + */ + +class M17Viterbi +{ +public: + + /** + * Constructor. + */ + M17Viterbi() : prevMetrics(&prevMetricsData), currMetrics(&currMetricsData) + { } + + /** + * Destructor. + */ + ~M17Viterbi() { } + + /** + * Decode unpunctured convolutionally encoded data. + * + * @param in: input data. + * @param out: destination array where decoded data are written. + */ + template < size_t IN, size_t OUT > + void decode(const std::array< uint8_t, IN >& in, + std::array< uint8_t, OUT >& out) + { + static_assert(IN*4 < 244, "Input size exceeds max history"); + + currMetricsData.fill(0x00); + prevMetricsData.fill(0x00); + + size_t pos = 0; + for (size_t i = 0; i < IN*8; i += 2) + { + uint8_t s0 = getBit(in, i) ? 2 : 0; + uint8_t s1 = getBit(in, i + 1) ? 2 : 0; + + decodeBit(s0, s1, pos); + pos++; + } + + chainback(out, pos); + } + + /** + * Decode punctured convolutionally encoded data. + * + * @param in: input data. + * @param out: destination array where decoded data are written. + */ + template < size_t IN, size_t OUT, size_t P > + void decodePunctured(const std::array< uint8_t, IN >& in, + std::array< uint8_t, OUT >& out, + const std::array< uint8_t, P >& punctureMatrix) + { + static_assert(IN*4 < 244, "Input size exceeds max history"); + + currMetricsData.fill(0x00); + prevMetricsData.fill(0x00); + + size_t histPos = 0; + size_t punctIndex = 0; + size_t bitPos = 0; + + while(bitPos < IN*8) + { + uint8_t sym[2] = {1, 1}; + + for(uint8_t i = 0; i < 2; i++) + { + if(punctureMatrix[punctIndex++]) + { + sym[i] = getBit(in, bitPos) ? 2 : 0; + bitPos++; + } + + if(punctIndex >= P) punctIndex = 0; + } + + decodeBit(sym[0], sym[1], histPos); + histPos++; + } + + chainback(out, histPos); + } + +private: + + /** + * Decode one bit and update trellis. + * + * @param s0: cost of the first symbol. + * @param s1: cost of the second symbol. + * @param pos: bit position in history. + */ + void decodeBit(uint8_t s0, uint8_t s1, size_t pos) + { + static constexpr uint8_t COST_TABLE_0[] = {0, 0, 0, 0, 2, 2, 2, 2}; + static constexpr uint8_t COST_TABLE_1[] = {0, 2, 2, 0, 0, 2, 2, 0}; + + for(uint8_t i = 0; i < NumStates/2; i++) + { + uint16_t metric = std::abs(COST_TABLE_0[i] - s0) + + std::abs(COST_TABLE_1[i] - s1); + + + uint16_t m0 = (*prevMetrics)[i] + metric; + uint16_t m1 = (*prevMetrics)[i + NumStates/2] + (4 - metric); + + uint16_t m2 = (*prevMetrics)[i] + (4 - metric); + uint16_t m3 = (*prevMetrics)[i + NumStates/2] + metric; + + uint8_t i0 = 2 * i; + uint8_t i1 = i0 + 1; + + if(m0 >= m1) + { + history[pos].set(i0, true); + (*currMetrics)[i0] = m1; + } + else + { + history[pos].set(i0, false); + (*currMetrics)[i0] = m0; + } + + if(m2 >= m3) + { + history[pos].set(i1, true); + (*currMetrics)[i1] = m3; + } + else + { + history[pos].set(i1, false); + (*currMetrics)[i1] = m2; + } + } + + std::swap(currMetrics, prevMetrics); + } + + /** + * History chainback to obtain final byte array. + * + * @param out: destination byte array for decoded data. + * @param pos: starting position for the chainback. + */ + template < size_t OUT > + void chainback(std::array< uint8_t, OUT >& out, size_t pos) + { + uint8_t state = 0; + size_t bitPos = OUT*8; + + while(bitPos > 0) + { + bitPos--; + pos--; + bool bit = history[pos].test(state >> 4); + state >>= 1; + if(bit) state |= 0x80; + setBit(out, bitPos, bit); + } + } + + + static constexpr size_t K = 5; + static constexpr size_t NumStates = (1 << (K - 1)); + + std::array< uint16_t, NumStates > *prevMetrics; + std::array< uint16_t, NumStates > *currMetrics; + + std::array< uint16_t, NumStates > prevMetricsData; + std::array< uint16_t, NumStates > currMetricsData; + + std::array< std::bitset< NumStates >, 244 > history; +}; + +#endif /* M17_VITERBI_H */ diff --git a/tests/unit/M17_viterbi.cpp b/tests/unit/M17_viterbi.cpp new file mode 100644 index 00000000..1d5f62f3 --- /dev/null +++ b/tests/unit/M17_viterbi.cpp @@ -0,0 +1,89 @@ +/*************************************************************************** + * Copyright (C) 2021 by Federico Amedeo Izzo IU2NUO, * + * Niccolò Izzo IU2KIN * + * Frederik Saraci IU2NRO * + * Silvano Seva IU2KWO * + * * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, see * + ***************************************************************************/ + +#include +#include +#include +#include +#include "M17/M17ConvolutionalEncoder.h" +#include "M17/M17CodePuncturing.h" +#include "M17/M17Viterbi.h" +#include "M17/M17Utils.h" + +using namespace std; + +default_random_engine rng; + +/** + * Insert radom bit flips in input data. + */ +template < size_t N > +void generateErrors(array< uint8_t, N >& data) +{ + uniform_int_distribution< uint8_t > numErrs(0, 4); + uniform_int_distribution< uint8_t > errPos(0, N); + + for(uint8_t i = 0; i < numErrs(rng); i++) + { + uint8_t pos = errPos(rng); + bool bit = getBit(data, pos); + setBit(data, pos, !bit); + } +} + +int main() +{ + uniform_int_distribution< uint8_t > rndValue(0, 255); + + array< uint8_t, 18 > source; + + for(auto& byte : source) + { + byte = rndValue(rng); + } + + array encoded; + M17ConvolutionalEncoder encoder; + encoder.reset(); + encoder.encode(source.data(), encoded.data(), source.size()); + encoded[36] = encoder.flush(); + + array punctured; + puncture(encoded, punctured, Audio_puncture); + + generateErrors(punctured); + + array< uint8_t, 18 > result; + M17Viterbi decoder; + decoder.decodePunctured(punctured, result, Audio_puncture); + + for(size_t i = 0; i < result.size(); i++) + { + if(source[i] != result[i]) + { + printf("Error at pos %ld: got %02x, expected %02x\n", i, result[i], + source[i]); + return -1; + } + } + + return 0; +}