From 49c6cbd7c0c8e2bd35daeaaf041f3517c4cb6cfa Mon Sep 17 00:00:00 2001 From: Silvano Seva Date: Fri, 11 Nov 2022 22:04:34 +0100 Subject: [PATCH] Added implementation of soft-decision Viterbi decoder for M17 protocol, updated version number in meson.build --- meson.build | 2 +- .../include/protocols/M17/M17FrameDecoder.hpp | 2 +- openrtx/include/protocols/M17/M17Viterbi.hpp | 226 +++++++++++++++++- tests/unit/M17_viterbi.cpp | 10 +- 4 files changed, 230 insertions(+), 10 deletions(-) diff --git a/meson.build b/meson.build index 6cd4cc13..492acfda 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ ## OpenRTX - Modular Open Source Radio Firmware ## project('OpenRTX', ['c', 'cpp'], - version : '0.3.3', + version : '0.3.5', default_options : ['warning_level=3', 'b_staticpic=false']) ## diff --git a/openrtx/include/protocols/M17/M17FrameDecoder.hpp b/openrtx/include/protocols/M17/M17FrameDecoder.hpp index d6cb841a..30a2cc2c 100644 --- a/openrtx/include/protocols/M17/M17FrameDecoder.hpp +++ b/openrtx/include/protocols/M17/M17FrameDecoder.hpp @@ -140,7 +140,7 @@ private: M17LinkSetupFrame lsf; ///< Latest LSF received. M17LinkSetupFrame lsfFromLich; ///< LSF assembled from LICH segments. M17StreamFrame streamFrame; ///< Latest stream dat frame received. - M17Viterbi viterbi; ///< Viterbi decoder. + M17HardViterbi viterbi; ///< Viterbi decoder. ///< Maximum allowed hamming distance when determining the frame type. static constexpr uint8_t MAX_SYNC_HAMM_DISTANCE = 4; diff --git a/openrtx/include/protocols/M17/M17Viterbi.hpp b/openrtx/include/protocols/M17/M17Viterbi.hpp index 08f95e40..1ab05925 100644 --- a/openrtx/include/protocols/M17/M17Viterbi.hpp +++ b/openrtx/include/protocols/M17/M17Viterbi.hpp @@ -3,6 +3,7 @@ * Niccolò Izzo IU2KIN * * Frederik Saraci IU2NRO * * Silvano Seva IU2KWO * + * Wojciech Kaczmarski SP5WWP * * * * Adapted from original code written by Phil Karn KA9Q * * * @@ -43,20 +44,20 @@ namespace M17 * G2 = 0x17. */ -class M17Viterbi +class M17HardViterbi { public: /** * Constructor. */ - M17Viterbi() : prevMetrics(&prevMetricsData), currMetrics(&currMetricsData) + M17HardViterbi() : prevMetrics(&prevMetricsData), currMetrics(&currMetricsData) { } /** * Destructor. */ - ~M17Viterbi() { } + ~M17HardViterbi() { } /** * Decode unpunctured convolutionally encoded data. @@ -237,6 +238,225 @@ private: std::array< std::bitset< NumStates >, 244 > history; }; +/** + * Soft 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 M17SoftViterbi +{ +public: + + /** + * Constructor. + */ + M17SoftViterbi() : prevMetrics(&prevMetricsData), currMetrics(&currMetricsData) + { } + + /** + * Destructor. + */ + ~M17SoftViterbi() { } + + /** + * Decode unpunctured convolutionally encoded data. + * + * @param in: input data. + * @param out: destination array where decoded data are written. + * @return number of bit errors corrected. + */ + template < size_t IN, size_t OUT > + uint32_t decode(const std::array< uint16_t, IN >& in, + std::array< uint8_t, OUT >& out) + { + static_assert(IN < 244*2, "Input size exceeds max history"); + + currMetricsData.fill(0); + prevMetricsData.fill(0); + + size_t pos = 0; + for (size_t i = 0; i < IN; i += 2) + { + uint16_t s0 = in[i]; + uint16_t s1 = in[i + 1]; + + decodeBit(s0, s1, pos); + pos++; + } + + return chainback(out, pos); + } + + /** + * Decode punctured convolutionally encoded data. + * + * @param in: input data. + * @param out: destination array where decoded data are written. + * @param punctureMatrix: puncturing matrix. + * @return number of bit errors corrected. + */ + template < size_t IN, size_t OUT, size_t P > + uint16_t decodePunctured(const std::array< uint16_t, IN >& in, + std::array< uint8_t, OUT >& out, + const std::array< uint8_t, P >& punctureMatrix) + { + static_assert(IN < 244*2, "Input size exceeds max history"); + + currMetricsData.fill(0); + prevMetricsData.fill(0); + + size_t histPos = 0; + size_t punctIndex = 0; + size_t bitPos = 0; + uint16_t punctBitCnt = 0; + + while(bitPos < IN) + { + uint16_t sym[2] = {0xFFFF, 0xFFFF}; + + for(uint8_t i = 0; i < 2; i++) + { + if(punctureMatrix[punctIndex++]) + { + sym[i] = in[bitPos]; + bitPos++; + } + else + { + sym[i] = 0x7FFF; //half range for punctured out bit + punctBitCnt++; + } + + if(punctIndex >= P) punctIndex = 0; + } + + decodeBit(sym[0], sym[1], histPos); + histPos++; + } + + return chainback(out, histPos) - punctBitCnt; + } + +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(uint16_t s0, uint16_t s1, size_t pos) + { + static constexpr uint16_t COST_TABLE_0[] = {0, 0, 0, 0, 0xFFFF, + 0xFFFF, 0xFFFF, 0xFFFF}; + static constexpr uint16_t COST_TABLE_1[] = {0, 0xFFFF, 0xFFFF, 0, + 0, 0xFFFF, 0xFFFF, 0}; + + for(uint8_t i = 0; i < NumStates/2; i++) + { + uint32_t metric = q_AbsDiff(COST_TABLE_0[i], s0) + + q_AbsDiff(COST_TABLE_1[i], s1); + + + uint32_t m0 = (*prevMetrics)[i] + metric; + uint32_t m1 = (*prevMetrics)[i + NumStates/2] + (0x1FFFE - metric); + + uint32_t m2 = (*prevMetrics)[i] + (0x1FFFE - metric); + uint32_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. + * @return minimum Viterbi cost at the end of the decode sequence. + */ + template < size_t OUT > + uint32_t 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); + } + + uint32_t cost = (*prevMetrics)[0]; + + for(size_t i = 0; i < NumStates; i++) + { + uint32_t m = (*prevMetrics)[i]; + if(m < cost) cost = m; + } + + return cost; + } + + /** + * Utility function to compute the absolute value of a difference between + * two fixed-point values. + * + * @param v1: first value + * @param v2: second value + * @return abs(v1-v2) + */ + inline uint16_t q_AbsDiff(const uint16_t v1, const uint16_t v2) + { + if(v2 > v1) return v2 - v1; + return v1 - v2; + } + + + static constexpr size_t K = 5; + static constexpr size_t NumStates = (1 << (K - 1)); + + std::array< uint32_t, NumStates > *prevMetrics; + std::array< uint32_t, NumStates > *currMetrics; + + std::array< uint32_t, NumStates > prevMetricsData; + std::array< uint32_t, NumStates > currMetricsData; + + std::array< std::bitset< NumStates >, 244 > history; +}; + } // namespace M17 #endif // M17_VITERBI_H diff --git a/tests/unit/M17_viterbi.cpp b/tests/unit/M17_viterbi.cpp index 9d090f40..f4a45eda 100644 --- a/tests/unit/M17_viterbi.cpp +++ b/tests/unit/M17_viterbi.cpp @@ -23,10 +23,10 @@ #include #include #include -#include "M17/M17ConvolutionalEncoder.h" -#include "M17/M17CodePuncturing.h" -#include "M17/M17Viterbi.h" -#include "M17/M17Utils.h" +#include "M17/M17ConvolutionalEncoder.hpp" +#include "M17/M17CodePuncturing.hpp" +#include "M17/M17Viterbi.hpp" +#include "M17/M17Utils.hpp" using namespace std; @@ -72,7 +72,7 @@ int main() generateErrors(punctured); array< uint8_t, 18 > result; - M17Viterbi decoder; + M17HardViterbi decoder; decoder.decodePunctured(punctured, result, DATA_PUNCTURE); for(size_t i = 0; i < result.size(); i++)