From 8cf9d6135cb937f57400a44cc3a629bd193d8d69 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Fri, 22 Dec 2023 10:29:02 -0500 Subject: [PATCH] implement Reed-Solomon decoding; --- EDAC/RS/GaloisField.cs | 624 ++++++++++++++++++++++++++++++++ EDAC/RS/ReedSolomonAlgorithm.cs | 91 ++++- EDAC/RS/ReedSolomonDecoder.cs | 463 ++++++++++++++++++++++++ EDAC/RS/ReedSolomonEncoder.cs | 49 +-- 4 files changed, 1181 insertions(+), 46 deletions(-) create mode 100644 EDAC/RS/GaloisField.cs create mode 100644 EDAC/RS/ReedSolomonDecoder.cs diff --git a/EDAC/RS/GaloisField.cs b/EDAC/RS/GaloisField.cs new file mode 100644 index 0000000..4f4a255 --- /dev/null +++ b/EDAC/RS/GaloisField.cs @@ -0,0 +1,624 @@ +/** +* Digital Voice Modem - Fixed Network Equipment +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Fixed Network Equipment +* +*/ +// +// Based on code from the ErrorCorrection project. (https://github.com/antiduh/ErrorCorrection) +// Licensed under the BSD 2-clause License (https://opensource.org/license/bsd-2-clause/) +// +/* +* Copyright (C) 2016 by Kevin Thompson +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, this +* list of conditions and the following disclaimer. +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +using System; +using System.Text; + +namespace fnecore.EDAC.RS +{ + /// + /// Implements polynomial math for polynomials over a galois field with characteristic 2 (a binary + /// galois field), with restrictions on the practical size of the field. + /// + /// + /// This paper has a Reed Solomon-relevant intro to GFs: + /// http://downloads.bbc.co.uk/rd/pubs/whp/whp-pdf-files/WHP031.pdf + /// + /// A Galois field is an arithmetical field that is: + /// - Finite: Arithmetic operations are defined for only the elements defined to be in the field, + /// and there are a finite number of elements. + /// - Closed: Arithmetic operations on elements of the field return a value also in the field. + /// + /// A Galois field's size is a parameter of the field, but only certain sizes can exist. The + /// field must be a size = p^k where p is prime and k is a positive integer. p is also called the + /// characteristic of the field. + /// + /// A Galois field is composed of an additive group and a multiplicative group - what it means to + /// perform addition or multiplication in the field depends on the definitions of these groups for + /// the field in question - the choice of what meaning to use is up to the designer of the field. + /// However, both groups must meet the requirements for operations in a Galois field - they must be + /// finite and closed. + /// + /// One such possible definition for addition over GF(p) (where p is prime) could be `z = (x + y) mod p`. + /// Another might be `z = a XOR b` where XOR represents the bitwise exclusive-or operation. Both of these + /// definitions are sufficient for defining the meaning of addition in a Galois field. + /// + /// The same is true for defining multiplication - in GF(p), multiplication could be defined as + /// `z = (x * y ) mod p` - such a definition is finite and closed. + /// + /// A Galois field need not be defined simply over a prime number of elements - it is possible + /// to define a Galois field GF(p^n) (where p is prime and n is > 1), however, providing definitions for + /// addition and multiplication prove a bit tricky - particularly, multiplication is the trouble maker. + /// Our go-to definition for multiplication over such a field might be `z = (x * y) mod p^k`, however + /// such a definition is not sufficient - the ring of integers modulo p^k doesn't meet the definition of + /// a field. + /// + /// Instead, however, we can define the multiplicative group for our finite field in a different manner. + /// What if we treated the elements of GF(p^n) as polynomials over the field GF(p)? Each element in GF(p^n) + /// would be decomposed into a set of elements in GF(p), and those elements would be used as coefficients + /// for a polynomial over GF(p). + /// + /// Consider a galois field that uses characteristic '2', such as GF(2^4). Such a field has 16 elements, and + /// we could consider each element as a set of elements from GF(2) - we could consider them by their bits. + /// + /// For instance, the element '14' from GF(2^4) could be decomposed into binary '1110'. Now we could + /// represent the element '14' from GF(2^4) as the polynomial '1x^3 + 1x^2 + 1x^1 + 0x^0' from GF(2). + /// + /// Let's keep that in our pocket for now. + /// + /// In Galois Fields, non-zero elements of the field form a multiplicative group that is also a cyclic + /// group. For certain elements of the field, successive powers of these elements form a cyclic group + /// that contains the entire set of non-zero elements in the field. These elements are called primitive + /// elements, usually denoted as some value 'a'. Since successive powers of these elements forms + /// a complete cyclic group containing all non-zero elements of the field - all elements are + /// represented, and represented only once - we can represent the elements of the field indexed + /// by the power of some primitive element a, since we can guarantee that there is a one-to-one + /// mapping from some power a^i to some element in the field. The field can be represented in + /// an alternate representation { 0, a^0, a^1, ..., a^(n-2) }, which is particularly useful when + /// defining multiplication in some GF(p^n) as polynomials over GF(p). + /// + /// For example, consider GF(5) - this is a prime group, so we can choose to define arithmetic as + /// simply being addition modulo p and multiplication modulo p. + /// + /// The elements of GF(5) are {0, 1, 2, 3, 4}. 2 + 4 = 6 mod 5 = 1. 2 * 4 = 8 mod 5 = 3. + /// + /// Lets test to see if the element 2 in GF(5) is also a primitive element: + /// - 2^1 = 2 + /// - 2^2 = 4 + /// - 2^3 = 8 mod 5 = 3 + /// - 2^4 = 16 mod 5 = 1 + /// + /// Each element was generated, and generated only once, thus the element 2 in GF(5) is primitive in GF(5). + /// We can now write the elements of GF(5) indexed by a=2: + /// {0, a^0, a^1, a^2, a^3} = + /// {0 1 2, 4, 3} + /// + /// Consider the field GF(7) equipped with addition/multiplication modulo 7, and see if 2 is a primitive element: + /// - 2^0 = 1 + /// - 2^1 = 2 + /// - 2^2 = 4 + /// - 2^3 = 8 mod 7 = 1 xxx + /// + /// 2 in GF(7) forms a cyclic group {2, 4, 1}, but isn't a complete cyclic group, + /// and thus is not a primitive element in GF(7). + /// + /// -- + /// + /// Recall that we don't currently have a definition for how to perform arithmetic on elements + /// from some arbitrary field GF(p^k), since our go-to operator modulus doesn't fit the bill - + /// the ring of integers modulo p^k doesn't meet the definitions for a field. + /// + /// However, we *can* use modulus to define arthimetic on Galois Fields of the form GF(p). Then, we + /// can use polynomials over GF(p), which we are now equiped to compute, as the basis for defining + /// arithmetic over some arbitrary GF(p^k). + /// + /// A polynomial over some arbitrary GF(x) has polynomial coefficients that are elements of GF(x). + /// Consider the following equation of polynomials over the GF(5) that is using modulus to define + /// arithmetic: + /// ( 3x^2 + x + 2 ) + ( 3x^2 + x + 3) = ( 6x^2 + 2x + 5 ) = ( x^2 + 2x + 0 ) + /// + /// We can represent *elements* of GF(p^k) as *polynomials* over GF(p). + /// For instance, element '7' in GF(2^4) would be the following polynomial over GF(2): + /// 0x^3 + 1x^2 + 1x + 1. + /// + /// And element '10' would be the polynomial: + /// 1x^3 + 0x^2 + 1x + 0 + /// + /// We can then add those two polynomials together according to our definition of arithmetic in GF(2): + /// 1x^3 + 1x^2 + 2x + 1 which reduces modulo 2 to + /// 1x^3 + 1x^2 + 0x + 1 + /// + /// Which, when converted back to an element in GF(2^4) would be 13. + /// Thus, we've defined elemental addition in GF(2^4) to be polynomial addition in + /// the system of GF(2) equipped with elemental addition modulu 2. + /// + /// We can similarly use GF(2) with modulu to define multiplication in GF(2^4), however, we + /// run into one little snag - we end up with polynomials with higher-power terms than defined + /// in GF(2^4): + /// + /// Multiply 7 * 6 in GF(2^4): + /// (x^2 + x + 1) * (x^2 + x) = + /// (x^4 + x^3) + (x^3 + x^2) + (x^2 + x) = + /// x^4 + 2x^3 + 2x^2 + x + 0 = + /// x^4 + 0x^3 + 0x^2 + x + 0 = + /// x^4 + x + /// + /// We're left with this x^4 term that has no representation in GF(2^4). + /// + /// However, if we can find a irreducible polynomial in GF(p) that has degree k, then we can + /// perform polynomial division and use the remainder as the result, effectively, implmenting + /// polynomial modulus. The irreducible polynomial must have a non-zero term of degree k (it must be monic) + /// for it to have the reducing strength required to remove polynomial terms that are too high order. + /// + /// For GF(2^4), such a polynomial might be x^4 + x + 1 + /// + /// Thus, we could take our earlier result x^4 + x and reduce it x^4 + x + 1 via polynomial division: + /// 1 + /// __________ + /// x^4 + x + 1 | x^4 + x + 0 + /// - x^4 + x + 1 + /// 0 + 0 + 1 + /// + /// And thus, we're left with just 1. This was a polynomial in GF(2), which + /// we can convert back to an element in GF(2^4), which is simply element 1. + /// + /// Thus, for the field GF(2^4), with reducing polynomial x^4 + x + 1, + /// 7 * 6 = 1. + /// + /// We call this reducing polynomial the field generator polynomial p(x). + /// + /// -- + /// + /// The primitive elements of the field GF(p^k) also form roots of all generator polynomials. + /// For instance, the generator polynomial we chose above: + /// p(x) = x^4 + x + 1 --> + /// p(a) = a^4 + a + 1 = 0 ---> + /// a^4 = a + 1 + /// + /// We have defined addition and multiplication for non-prime field of the form GF(p^k) + /// by using polynomal representation in GF(p) equipped with modular arithmetic. + /// + /// We can use this fact to generate the field of GF(p^k) in terms of the elements { 0, a^0, .., a^(n-2) } + /// + /// Suppose we pick 2 as our primitive element for GF(2^4), and use reducing polynomial x^4 + x + 1, and thus + /// know that a^4 = a + 1 + /// + /// + /// a^0 = = 1 + /// a^1 = = 2 + /// a^2 = = 4 + /// a^3 = = 8 + /// a^4 = a + 1 = 3 + /// a^5 = a(a^4 + 1) = a(a+1) = a^2 + a = 6 + /// ... + /// + /// And thus, we can represent the entire field indexed by the power of our primitive element, and implement + /// multiplication and division using logarithms. + /// + /// -- + /// + /// This design uses the following optimizating restrictions: + /// - The field characteristic must be 2; that is the field must be of the form GF(2^m). + /// - The generator polynomial is specified in the form of a 32-bit 'int' variable, + /// limiting the length of the polynomial to 32 coefficients. This limit will never be + /// practically hit, due to the next restriction. + /// - Multiplication and division are implemented using a look-up table, for performance. + /// Large galois fields require n^2 memory to store the table. For GF(2^8), this is + /// 256 * 256 == 65536 entries, times 4 bytes per entry = 256 kiB. + /// GF(2^16) would require 16 GiB of memory. + /// + public sealed class GaloisField + { + // Note: Most examples in the comments for this class are made in GF(2^4) with p(x) = x^4 + x + 1 + // The field looks like: + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // { 0, 1, 2, 4, 8, 3, 6, 12, 11, 5, 10, 7, 14, 15 13, 9} + // + // Eg: + // 0 = field[0] = 0 + // a^0 = field[1] = 1; + // a^1 = field[2] = 2; + // a^2 = feild[3] = 4; + // a^3 = field[4] = 8; + // ... + // a^7 = field[8] = 11. + + /// + /// The elements of the field, ordered as generated by the field generator polynomial. + /// + public readonly int[] Field; + + /// + /// Stores the multiplicative inverses of the elements of the field, eg + /// the value of 1/a^9 is stored + /// + public readonly int[] Inverses; + + /// + /// Stores the values of the field indexed by their multiplicative logarithm. + /// + public readonly int[] Logarithms; + + /// + /// The primitive polynomial used to generate the elements of the field by taking successive powers of + /// the polynomial. + /// + private int fieldGenPoly; + + /// + /// Caches multiplication values for field elements. + /// + private int[,] multTable; + + /// + /// Stores the total number of elements in the field. As a consequence of the structure of + /// Galois Feilds, this value must be of the form p^k where p is a prime number and k is a + /// positive integer. + /// + private int size; + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// The total number of elements in the field. Must be a power of two. + /// A primitive element of the field, represented in polynomial form, + /// that is used to generate the elements of the field in an ordered manner. The bits of the value + /// indicate the coefficients of the polynomial, eg, 0b1011 indicate 1*x^3 + 0*x^2 + 1*x^1 + 1*x^0. + public GaloisField(int size, int fieldGenPoly) + { + this.size = size; + this.fieldGenPoly = fieldGenPoly; + + this.Field = new int[size]; + this.Logarithms = new int[this.Field.Length]; + this.Inverses = new int[this.Field.Length]; + + BuildField(); + BuildLogarithms(); + BuildMultTable(); + BuildInverses(); + } + + /// + /// Pretty-prints a polynomial with the given coefficients. + /// + /// + /// + public static string PolyPrint(int[] poly) + { + StringBuilder builder = new StringBuilder(poly.Length * 3); + for (int i = poly.Length - 1; i >= 0; i--) + { + if (i > 1) + builder.Append(poly[i]).Append("x^").Append(i).Append(" + "); + else if (i == 1) + builder.Append(poly[i]).Append("x").Append(" + "); + else + builder.Append(poly[i]); + } + + return builder.ToString(); + } + + /// + /// Performs division, as defined by by this Galois field implementation, on the + /// given operands. + /// + /// The value to act as the dividend. + /// The value to act as the divisor. + /// + public int Divide(int dividend, int divisor) + { + // Using the original computation is the same speed as the multiplication table. + // I don't know why. + return this.multTable[dividend, Inverses[divisor]]; + } + + /// + /// Performs the multiplication operation, as defined by this Galois field implemention, + /// on the given operands. + /// + /// The first value to multiply. + /// The second value to multiply. + /// + public int Multiply(int left, int right) + { + // Using the multiplication table is a lot faster than the original computation. + return this.multTable[left, right]; + } + + /// + /// Evaluates the polynomial for the given value of x + /// + /// The polynomial to evaluate + /// The value with which to evaluate the polynomail. + /// + public int PolyEval(int[] poly, int x) + { + int sum; + int xLog; + int coeffLog; + int power; + + // The constant term in the poly requires no multiplication. + sum = poly[0]; + + xLog = Logarithms[x]; + + for (int i = 1; i < poly.Length; i++) + { + // The polynomial at this spot has some coefficent, call it a^j. + // x itself has some value, call it a^k. + // x is raised to some power, which is 'i' in the loop. + // a^j * (a^k)^i + // == a^j * a^(k*i) + // == a^(j+k+i) + // Remember that exponents are added modulo 'size - 1', because the field elements + // are {0, a^0, a^1, ... } - we use size - 1 because we don't use 0. + + // If the coeff is 0, then this monomial contributes no value to the sum. + if (poly[i] == 0) + continue; + + coeffLog = Logarithms[poly[i]]; + + // Add the powers together, then lookup which a^L you ended up with. + power = (coeffLog + xLog * i) % (size - 1); + sum ^= Field[power + 1]; + } + + return sum; + } + + /// + /// Multiplies two polynomials of degrees X and Y to produce a single polynomial + /// of degree X + Y, where 'degree' means the exponent of the highest order monomial. + /// Eg, x^2 + x + 1 is of degree 2, and has 3 monomials. + /// + /// + /// + /// + public int[] PolyMult(int[] left, int[] right) + { + int[] result; + + // Lets say we have the following two polynomials: + // 3x^2 + 0x + 1 == int[]{1, 0, 3} + // 1x^2 + 1x + 0 == int[]{0, 1, 1} + // + // The naive result (ignoring galois fields) will be: + // (3x^4 + 3x^3 + 0) + (0x^3 + 0x^2 + 0) + (1x^2 + 1x + 0) == + // 3x^4 + (3 + 0)x^3 + (0+1)x^2 + 1x + 0 == + // 3x^4 + 3x^3 + 1x^2 + 1x + 0 == {3, 3, 1, 1, 0} + + // The result is of degree X + Y == 2 + 2 = 4. The number of monomials, and thus the number of + // coefficients is degree + 1. Thus, for two polynomials with degree X and Y, the resultant + // polynomial has degree X + Y but has X + Y - 1 coefficents. + // This establishes our basis for the degree of the resultant polynomial. + + // Now, in our galois fields, coefficient multiplication and addition are different. + // In our GF(2^x), addition is simple XOR and multiplication is best represented as being logarithm based. + // If + // a^9 = 10 and a^13 = 13 in GF(16) p(x) = x^4 + x + 1, + // Then + // 10 * 13 == + // a^9 * a^13 == + // a^((9+13) mod 15) == + // a^(22 mod 15) == a^7 == + // 11 + // + // Taking another example: + // 5x + 1 == {1, 5} + // 7x + 10 == {10, 7} + // + // (5x^1 + 2x^0)(7x^1 + 10x^0) == + // [(5*7)x^2 + (5*10)x^1 ] + [(2*7)x^1 + (2*10)x^0] + // + // --> Perform multiplications: + // 5*7 = a^8 * a^10 = a^18 = a^3 = 8 + // 5*10 = a^8 * a^9 = a^17 = a^2 = 4 + // 2*7 = a^1 * a^10 = a^11 = a^11 = 14 + // 2*10 = a^1 * a^9 = a^10 = a^10 = 7 + // [8x^2 + 4x^1 ] + [14x^1 + 7x^0] + // + // --> Combine like terms + // 8x^2 + (4+14)x^1 + 7x^0 + // + // --> Perform sums + // 4+14 = 4 XOR 14 = 10 + // 8x^2 + 10x^1 + 7x^0 + // + // Done. + + int coeff; + result = new int[left.Length + right.Length - 1]; + + for (int i = 0; i < left.Length; i++) + { + for (int j = 0; j < right.Length; j++) + { + coeff = InternalMult(left[i], right[j]); + result[i + j] = result[i + j] ^ coeff; + } + } + + return result; + } + + /// + /// + /// + private void BuildField() + { + int next; + int last; + + this.Field[0] = 0; + this.Field[1] = 1; + last = 1; + + for (int i = 2; i < this.Field.Length; i++) + { + next = last << 1; + if (next >= size) + next = next ^ fieldGenPoly; + + this.Field[i] = next; + last = next; + } + } + + /// + /// + /// + private void BuildInverses() + { + // Build the mulitiplicative inverses. + // if a^9 = 10, then what is 1/a^9 aka a^(-9) ? + this.Inverses[0] = 0; + for (int i = 1; i < this.Inverses.Length; i++) + this.Inverses[this.Field[i]] = InternalDivide(1, this.Field[i]); + } + + /// + /// + /// + private void BuildLogarithms() + { + // In GF(2^8) with p(x) = x^4 + x + 1, the field has elements 0, a^0, .., a^15: + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // { 0, 1, 2, 4, 8, 3, 6, 12, 11, 5, 10, 7, 14, 15 13, 9} + + // In the above, we have the elements of the field by their index. + // What about going from the element to its power of a, for multiplying? + // In above, we have element 15 at index 13, but 15 is a^12 - it's inverse is one higher + // than its power, because 0 is in there. + + // field[13] = 15; + // logarithms[15] = 13 - 1; + + // logarithms[ field[13] ] = 13 - 1; + // logarithms[ 15 ] = 13 - 1; + // logarithms[ 15 ] = 12; + + // This means that zero will be stored with a logarithm of -1. + // This is intentional, but we have to be careful to handle it specially when we actually + // do multiplication. + for (int i = 0; i < this.Field.Length; i++) + this.Logarithms[this.Field[i]] = i - 1; + } + + /// + /// + /// + private void BuildMultTable() + { + this.multTable = new int[this.size, this.size]; + for (int left = 0; left < size; left++) + for (int right = 0; right < size; right++) + this.multTable[left, right] = InternalMult(left, right); + } + + /// + /// + /// + /// + /// + /// + private int InternalDivide(int dividend, int divisor) + { + // Same general concept as Mult. Convert to logarithms and subtract. + if (dividend == 0) + return 0; + + dividend = Logarithms[dividend]; + divisor = Logarithms[divisor]; + + // Note the extra '... + size - 1' term. This is to push the subtraction above + // zero, so that the modulus operator will do the right thing. + // (10 - 11) % 5 == -1 wrong + // ((10 - 11) + 5) % 5 == 4 right + dividend = (dividend - divisor + (size - 1)) % (size - 1); + + return this.Field[dividend + 1]; + } + + /// + /// + /// + /// + /// + /// + private int InternalMult(int left, int right) + { + // Conceptual notes: + // If + // a^9 = 10 and a^13 = 13 in GF(16) p(x) = x^4 + x + 1, + // Then + // 10 * 13 == + // a^9 * a^13 == + // a^((9+13) mod 15) == + // a^(22 mod 15) == a^7 == + // 11 + // + // Implementation notes: + // Our logarithms table stores a^9 at logarithms[9] = 10; + // Our field table stores a^9 at field[10] (0 is the first element, a^0 is the second). + // a^i is stored at field[i+1]; + // + // Plan: + // Convert each field element to its logarithm: + // left = a^i --> i + // right = a^j --> j + // + // Sum the logarithms to perform the multiplication. + // Modulus the sum to convert from, eg, a^15 back to a^0 + // k = (i + j) mod (size-1). + // + // Convert k back to a^k. Remember that a^k is stored at field[k+1]; + // a^k = field[k+1]; + // + // Return a^k. + + // Handle the special case 0 + if (left == 0 || right == 0) + return 0; + + // Convert each to their logarithm; + left = Logarithms[left]; + right = Logarithms[right]; + + // Sum the logarithms, using left to store the result. + left = (left + right) % (size - 1); + + // Convert the logarithm back to the field value. + return this.Field[left + 1]; + } + } // public sealed class GaloisField +} // namespace fnecore.EDAC.RS diff --git a/EDAC/RS/ReedSolomonAlgorithm.cs b/EDAC/RS/ReedSolomonAlgorithm.cs index df3920e..2051940 100644 --- a/EDAC/RS/ReedSolomonAlgorithm.cs +++ b/EDAC/RS/ReedSolomonAlgorithm.cs @@ -30,7 +30,7 @@ namespace fnecore.EDAC.RS /// /// (63,47,17) /// - ReedSolomon_634717, + ReedSolomon_362017, /// /// (24,12,13) /// @@ -62,7 +62,7 @@ namespace fnecore.EDAC.RS byte[] output = new byte[message.Length]; Buffer.BlockCopy(message, 0, output, 0, message.Length); - if (eccType == ErrorCorrectionCodeType.ReedSolomon_634717) + if (eccType == ErrorCorrectionCodeType.ReedSolomon_362017) { reedSolomonEncoder.encode362017(ref output); return output; @@ -89,7 +89,92 @@ namespace fnecore.EDAC.RS /// Returns the repaired message, or null if it cannot be repaired. public static byte[] Decode(byte[] message, ErrorCorrectionCodeType eccType) { - throw new NotImplementedException(); + byte[] codeword = new byte[63]; + + if (eccType == ErrorCorrectionCodeType.ReedSolomon_362017) + { + uint offset = 0U; + for (uint i = 0U; i < 36U; i++, offset += 6) + codeword[27 + i] = bin2Hex(message, offset); + + ReedSolomonDecoder rs362017 = new ReedSolomonDecoder(64, 47, 16, 0x43); + byte[] codewordEC = rs362017.DecodeEx(codeword); + + byte[] messageOut = new byte[message.Length]; + offset = 0U; + for (uint i = 0U; i < 20U; i++, offset += 6) + hex2Bin(codewordEC[27 + i], ref messageOut, offset); + return messageOut; + } + else if (eccType == ErrorCorrectionCodeType.ReedSolomon_241213) + { + uint offset = 0U; + for (uint i = 0U; i < 24U; i++, offset += 6) + codeword[39 + i] = bin2Hex(message, offset); + + ReedSolomonDecoder rs241213 = new ReedSolomonDecoder(64, 51, 12, 0x43); + byte[] codewordEC = rs241213.DecodeEx(codeword); + + byte[] messageOut = new byte[message.Length]; + offset = 0U; + for (uint i = 0U; i < 12U; i++, offset += 6) + hex2Bin(codewordEC[39 + i], ref messageOut, offset); + return messageOut; + } + else if (eccType == ErrorCorrectionCodeType.ReedSolomon_24169) + { + uint offset = 0U; + for (uint i = 0U; i < 24U; i++, offset += 6) + codeword[39 + i] = bin2Hex(message, offset); + + ReedSolomonDecoder rs24169 = new ReedSolomonDecoder(64, 55, 8, 0x43); + byte[] codewordEC = rs24169.DecodeEx(codeword); + + byte[] messageOut = new byte[message.Length]; + offset = 0U; + for (uint i = 0U; i < 16U; i++, offset += 6) + hex2Bin(codewordEC[39 + i], ref messageOut, offset); + return messageOut; + } + else + throw new ArgumentException($"Invalid '{nameof(eccType)}' argument.", nameof(eccType)); + } + + /// + /// + /// + /// + /// + /// + internal static byte bin2Hex(byte[] input, uint offset) + { + byte output = 0x00; + + output |= (byte)(FneUtils.ReadBit(input, offset + 0U) ? 0x20U : 0x00U); + output |= (byte)(FneUtils.ReadBit(input, offset + 1U) ? 0x10U : 0x00U); + output |= (byte)(FneUtils.ReadBit(input, offset + 2U) ? 0x08U : 0x00U); + output |= (byte)(FneUtils.ReadBit(input, offset + 3U) ? 0x04U : 0x00U); + output |= (byte)(FneUtils.ReadBit(input, offset + 4U) ? 0x02U : 0x00U); + output |= (byte)(FneUtils.ReadBit(input, offset + 5U) ? 0x01U : 0x00U); + + return output; + } + + /// + /// + /// + /// + /// + /// + /// + internal static void hex2Bin(byte input, ref byte[] output, uint offset) + { + FneUtils.WriteBit(ref output, offset + 0U, (input & 0x20U) == 0x20U); + FneUtils.WriteBit(ref output, offset + 1U, (input & 0x10U) == 0x10U); + FneUtils.WriteBit(ref output, offset + 2U, (input & 0x08U) == 0x08U); + FneUtils.WriteBit(ref output, offset + 3U, (input & 0x04U) == 0x04U); + FneUtils.WriteBit(ref output, offset + 4U, (input & 0x02U) == 0x02U); + FneUtils.WriteBit(ref output, offset + 5U, (input & 0x01U) == 0x01U); } } // public static class ReedSolomonAlgorithm } // namespace fnecore.EDAC.RS diff --git a/EDAC/RS/ReedSolomonDecoder.cs b/EDAC/RS/ReedSolomonDecoder.cs new file mode 100644 index 0000000..9611ce9 --- /dev/null +++ b/EDAC/RS/ReedSolomonDecoder.cs @@ -0,0 +1,463 @@ +/** +* Digital Voice Modem - Fixed Network Equipment +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Fixed Network Equipment +* +*/ +// +// Based on code from the MMDVMHost project. (https://github.com/g4klx/MMDVMHost) +// Licensed under the GPLv2 License (https://opensource.org/licenses/GPL-2.0) +// +/* +* Copyright (C) 2016 by Jonathan Naylor G4KLX +* Copyright (C) 2017-2023 by Bryan Biedenkapp N2PLL +* +* 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 2 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, write to the Free Software +* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +*/ + +using System; +using System.Linq; + +namespace fnecore.EDAC.RS +{ + /// + /// Implements Reed-Solomon decoding, as the name implies. + /// + /// This does not do error correction, only detection. + internal sealed class ReedSolomonDecoder + { + private readonly int fieldSize; + private readonly int messageSymbols; + private readonly int paritySymbols; + + private readonly int fieldGenPoly; + + private readonly GaloisField gf; + + private readonly int[] syndroms; + + private readonly int[] lambda; + private readonly int[] corrPoly; + private readonly int[] lambdaStar; + + private readonly int[] lambdaPrime; + + private readonly int[] omega; + + private readonly int[] errorIndexes; + + private readonly int[] chienCache; + + /* + ** Properties + */ + + /// + /// The number of symbols that make up an entire encoded message. An encoded message is composed of the + /// original data symbols plus parity symbols. + /// + public int BlockSize { get; private set; } + + /// + /// The number of symbols per block that store original message symbols. + /// + public int MessageSize + { + get { return this.messageSymbols; } + } + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// The size of the Galois field to create. Must be a value that is + /// a power of two. The length of the output block is set to `fieldSize - 1`. + /// The number of original message symbols per block. + /// The number of parity symbols per block. + /// A value representing the field generator polynomial, + /// which must be order N for a field GF(2^N). + /// BlockSize is equal to `fieldSize - 1`. messageSymbols plus paritySymbols must equal BlockSize. + public ReedSolomonDecoder(int fieldSize, int messageSymbols, int paritySymbols, int fieldGenPoly) + { + if (fieldSize - 1 != messageSymbols + paritySymbols) + throw new ArgumentOutOfRangeException("Invalid reed-solomon block parameters were provided - the number of message symbols plus the number of parity symbols " + + "does not add up to the size of a block"); + + this.fieldSize = fieldSize; + this.messageSymbols = messageSymbols; + this.paritySymbols = paritySymbols; + this.BlockSize = fieldSize - 1; + + this.fieldGenPoly = fieldGenPoly; + + this.gf = new GaloisField(fieldSize, fieldGenPoly); + + // Syndrom calculation buffers + this.syndroms = new int[paritySymbols]; + + // Lamda calculation buffers + this.lambda = new int[paritySymbols - 1]; + this.corrPoly = new int[paritySymbols - 1]; + this.lambdaStar = new int[paritySymbols - 1]; + + // LambdaPrime calculation buffers + this.lambdaPrime = new int[paritySymbols - 2]; + + // Omega calculation buffers + this.omega = new int[paritySymbols - 2]; + + // Error position calculation + this.errorIndexes = new int[fieldSize - 1]; + + // Cache of the lookup used in the ChienSearch process. + this.chienCache = new int[fieldSize - 1]; + + for (int i = 0; i < this.chienCache.Length; i++) + this.chienCache[i] = gf.Inverses[gf.Field[i + 1]]; + } + + /// + /// Discovers and corrects any errors in the block provided. + /// + /// A block containing a reed-solomon encoded message. + public byte[] DecodeEx(byte[] message) + { + int[] received = message.Select(x => (int)x).ToArray(); + Decode(ref received); + + return received.Take(message.Length).Select(x => (byte)x).ToArray(); + } + + /// + /// Discovers and corrects any errors in the block provided. + /// + /// A block containing a reed-solomon encoded message. + public void Decode(ref int[] message) + { + if (message.Length != this.BlockSize) + throw new ArgumentException("The provided message's size was not the size of a block"); + + CalcSyndromPoly(message); + CalcLambda(); + CalcLambdaPrime(); + CalcOmega(); + + ChienSearch(); + + RepairErrors(ref message, errorIndexes, omega, lambdaPrime); + } + + /// + /// + /// + /// + /// + /// + /// + private void RepairErrors(ref int[] message, int[] errorIndexes, int[] omega, int[] lp) + { + int top, bottom, x, xInverse; + int messageLen = message.Length; + + for (int i = 0; i < messageLen; i++) + { + // If i = 2, then use a^2 in the evaluation. + // remember that field[i + 1] = a^i, since field[0] = 0 and field[1] = a^0. + + // i = 2 + // a^2 is the element. a^2 = 4 + // + // 13 = 4 * [6*(4^-1)^2 + 15] / 14 + + // 8 ^ 15 = 7 + // 6 * a^-2 = 8 + // 8 + 15 = 7 + + if (errorIndexes[i] == 0) + { + // This spot has an error. + // Equation: + // value = X_J * [ Omega(1/X_J) / L'(1/X_J) ]; + // Break it down to: + // top = eval(omega, 1/X_J); + // top = X_J * top; + // bottom = eval( lambaPrime, 1/X_J ); + // errorMagnitude = top / bottom; + // + // To repair the message, we add the error magnitude to the received value. + // message[i] = message[i] ^ errorMagnitude + + x = gf.Field[i + 1]; + + xInverse = gf.Inverses[x]; + + top = gf.PolyEval(omega, xInverse); + top = gf.Multiply(top, x); + bottom = gf.PolyEval(lp, xInverse); + + message[i] ^= gf.Divide(top, bottom); + } + } + } + + /// + /// + /// + private void CalcLambda() + { + // Explanation of terms: + // S = S(x) - syndrom polynomial + // C = C(x) - correction polynomial + // D = 'lamba'(x) - error locator estimate polynomial. + // D* = 'lambda-star'(x) - a new error locator estimate polynomial. + // S_x = the element at index 'x' in S, eg, if S = {5,6,7,8}, then S_0 = 5, S_1 = 6, etc. + // 2T = the number of error correction symbols, eg, numCheckBytes. + // T must be >= 1, so 2T is guarenteed to be at least 2. + // + // Start with + // K = 1; + // L = 0; + // C = 0x^n + ... + 0x^2 + x + 0 aka {0,1,0, ...}; + // D = 0x^n + ... + 0x^2 + 0x + 1 aka {1,0,0,...}; + // Both C and D are guarenteed to be at least 2 elements, which is why they can have + // hardcoded initial values. + + // Step 1: Calculate e. + // -------------- + // e = S_(K-1) + sum(from i=1 to L: D_i * S_(K-1-i) + // + // Example + // 0 1 2 0 1 2 3 + // K = 4, L = 2; D = {1, 11, 15}; S = {15, 3, 4, 12} + // + // e = S_3 + sum(from i = 1 to 2: D_i * S_(3- i) + // = 12 + D_1 * S_2 + D_2 * S_1 + // = 12 + 11 * 4 + 15 * 3 + // = 12 + 10 + 2 + // = 12 XOR 10 XOR 2 + // = 4 + + // Step 2: Update estimate if e != 0 + // + // If e != 0 { + // D*(x) = D(x) + e * C(X) -- Note that this just assigns D(x) to D*(x) if e is zero. + // If 2L < k { + // L = K - L + // C(x) = D(x) * e^(-1) -- Multiply D(x) times the multiplicative inverse of e. + // } + // } + + // Step 3: Advance C(x): + // C(x) = C(x) * x + // This just shifts the coeffs down; eg, x + 1 {1, 1, 0} turns into x^2 + x {0, 1, 1} + // + // D(x) = D*(x) (only if a new D*(x) was calulated) + // + // K = K + 1 + + // Step 4: Compute end conditions + // If K <= 2T goto 1 + // Else, D(x) is the error locator polynomial. + + int k, l, e; + int eInv; // temp to store calculation of 1 / e aka e^(-1) + + // --- Initial conditions ---- + // Need to clear lambda and corrPoly, but not lambdaStar. lambda and corrPoly + // are used and initialized iteratively in the algorithm, whereas lambdaStar isn't. + Array.Clear(corrPoly, 0, corrPoly.Length); + Array.Clear(lambda, 0, lambda.Length); + k = 1; + l = 0; + corrPoly[1] = 1; + lambda[0] = 1; + + while (k <= paritySymbols) + { + // --- Calculate e --- + e = syndroms[k - 1]; + + for (int i = 1; i <= l; i++) + e ^= gf.Multiply(lambda[i], syndroms[k - 1 - i]); + + // --- Update estimate if e != 0 --- + if (e != 0) + { + // D*(x) = D(x) + e * C(x); + for (int i = 0; i < lambdaStar.Length; i++) + lambdaStar[i] = lambda[i] ^ gf.Multiply(e, corrPoly[i]); + + if (2 * l < k) + { + // L = K - L; + l = k - l; + + // C(x) = D(x) * e^(-1); + eInv = gf.Inverses[e]; + for (int i = 0; i < corrPoly.Length; i++) + corrPoly[i] = gf.Multiply(lambda[i], eInv); + } + } + + // --- Advance C(x) --- + + // C(x) = C(x) * x + for (int i = corrPoly.Length - 1; i >= 1; i--) + corrPoly[i] = corrPoly[i - 1]; + corrPoly[0] = 0; + + if (e != 0) + { + // D(x) = D*(x); + Array.Copy(lambdaStar, lambda, lambda.Length); + } + + k += 1; + } + } + + /// + /// + /// + private void CalcLambdaPrime() + { + // Forney's says that we can just set even powers to 0 and then take the rest and + // divide it by x (shift it down one). + + // No need to clear this.lambdaPrime between calls; full assignment is done every call. + for (int i = 0; i < lambdaPrime.Length; i++) + if ((i & 0x1) == 0) + lambdaPrime[i] = lambda[i + 1]; + else + lambdaPrime[i] = 0; + } + + /// + /// + /// + private void CalcOmega() + { + // O(x) is shorthand for Omega(x). + // L(x) is shorthand for Lambda(x). + // + // O_i is the coefficient of the term in omega with degree i. Ditto for L_i. + // Eg, O(x) = 6x + 15; O_0 = 15, O_1 = 6 + // + // From the paper: + // O_0 = S_b + // ---> b in our implementation is 0. + // O_1 = S_{b+1} + S_b * L_1 + // + // O_{v-1} = S_{b+v-1} + S_{b+v-2} * L_1 + ... + S_b * L_{v-1}. + // O_i = S_{b+i} + S_{b+ i-1} * L_1 + ... + S_{b+0} * L_i + + // Lets say : + // L(x) = 14x^2 + 14x + 1 aka {1, 14, 14}. + // S(x) = 12x^3 + 4x^2 + 3x + 15 aka {15, 3, 4, 12} + // b = 0; + // v = 2 because the power of the highest monomial in L(x), 14x^2, is 2. + // + // O_0 = S_{b} = S_0 = 15 + // O_1 = S_{b+1} + S_b * L_1 = S_1 + S_0 * L_1 = 3 + 15 * 14 = 6. + // + // O(x) = 6x + 15. + + // Lets make up another example (these are completely made up so they may not work): + // L(x) = 10x^3 + 9x^2 + 8x + 7 aka { 7, 8, 9, 10} + // S(x) = 2^4 + 3x^3 + 4x^2 + 5x + 6 aka { 6, 5, 4, 3, 2} + // b = 0 (design parameter) + // v = 3 + + // O_i for i = 0 .. v - 1 = 2. Thus, O has form ax^2 + bx^1 + cx^0 + // Compute O_0, O_1, O_2 + + // O_0 = S_{b+0} + // = S_0 + // + // O_1 = S_{b+1} + S_{b+0} * L_1 + // = S_1 + S_0 * L_1 + // + // O_2 = S_{b+2} + S_{b+1} * L_1 + S_{b+0} * L_2 + // + S_2 + S_1 * L_1 + S_0 * L_2 + + // Don't need to zero this.omega first - it's assigned to before we use it. + + for (int i = 0; i < omega.Length; i++) + { + omega[i] = syndroms[i]; + for (int lIter = 1; lIter <= i; lIter++) + omega[i] ^= gf.Multiply(syndroms[i - lIter], lambda[lIter]); + } + } + + /// + /// + /// + private void ChienSearch() + { + // The cheap chien search evaluates the lamba polynomial for the multiplicate inverse + // each element in the field other than 0. + + // Don't need to zero this.errorIndexes first - it's not used before its assigned to. + + /* + * This loop uses an optimization where I precalculate one lookup. + * The code before the optimization was: + * for( int i = 0; i < errorIndexes.Length; i++ ) + * { + * errorIndexes[i] = gf.PolyEval( + * lambda, + * gf.Inverses[ gf.Field[ i + 1] ] + * ); + * } + * We precompute the lookup gf.Inverses[ gf.Field[ i + 1] ] when creating the decoder. + */ + + for (int i = 0; i < errorIndexes.Length; i++) + errorIndexes[i] = gf.PolyEval(lambda, chienCache[i]); + } + + /// + /// + /// + /// + private void CalcSyndromPoly(int[] message) + { + int syndrome, root; + + // Don't need to zero this.syndromes first - it's not used before its assigned to. + for (int synIndex = 0; synIndex < syndroms.Length; synIndex++) + { + // EG, if g(x) = (x+a^0)(x+a^1)(x+a^2)(x+a^3) + // = (x+1)(x+2)(x+4)(x+8), + // Then for the first syndrom S_0, we would provide root = a^0 = 1. + // S_1 --> root = a^1 = 2 etc. + + root = gf.Field[synIndex + 1]; + syndrome = 0; + + for (int i = message.Length - 1; i > 0; i--) + syndrome = gf.Multiply((syndrome ^ message[i]), root); + + syndroms[synIndex] = syndrome ^ message[0]; + } + } + } // internal sealed class ReedSolomonDecoder +} // namespace fnecore.EDAC.RS diff --git a/EDAC/RS/ReedSolomonEncoder.cs b/EDAC/RS/ReedSolomonEncoder.cs index f3c3feb..5222026 100644 --- a/EDAC/RS/ReedSolomonEncoder.cs +++ b/EDAC/RS/ReedSolomonEncoder.cs @@ -117,14 +117,14 @@ namespace fnecore.EDAC.RS uint offs = 0U; for (uint j = 0U; j < 12U; j++, offs += 6U) { - byte hexbit = bin2Hex(data, offs); + byte hexbit = ReedSolomonAlgorithm.bin2Hex(data, offs); codeword[i] ^= gf6Mult(hexbit, ENCODE_MATRIX[j][i]); } } uint offset = 0U; for (uint i = 0U; i < 24U; i++, offset += 6U) - hex2Bin(codeword[i], ref data, offset); + ReedSolomonAlgorithm.hex2Bin(codeword[i], ref data, offset); } /// @@ -145,14 +145,14 @@ namespace fnecore.EDAC.RS uint offs = 0U; for (uint j = 0U; j < 16U; j++, offs += 6U) { - byte hexbit = bin2Hex(data, offs); + byte hexbit = ReedSolomonAlgorithm.bin2Hex(data, offs); codeword[i] ^= gf6Mult(hexbit, ENCODE_MATRIX_24169[j][i]); } } uint offset = 0U; for (uint i = 0U; i < 24U; i++, offset += 6U) - hex2Bin(codeword[i], ref data, offset); + ReedSolomonAlgorithm.hex2Bin(codeword[i], ref data, offset); } /// @@ -173,51 +173,14 @@ namespace fnecore.EDAC.RS uint offs = 0U; for (uint j = 0U; j < 20U; j++, offs += 6U) { - byte hexbit = bin2Hex(data, offs); + byte hexbit = ReedSolomonAlgorithm.bin2Hex(data, offs); codeword[i] ^= gf6Mult(hexbit, ENCODE_MATRIX_362017[j][i]); } } uint offset = 0U; for (uint i = 0U; i < 36U; i++, offset += 6U) - hex2Bin(codeword[i], ref data, offset); - } - - /// - /// - /// - /// - /// - /// - private byte bin2Hex(byte[] input, uint offset) - { - byte output = 0x00; - - output |= (byte)(FneUtils.ReadBit(input, offset + 0U) ? 0x20U : 0x00U); - output |= (byte)(FneUtils.ReadBit(input, offset + 1U) ? 0x10U : 0x00U); - output |= (byte)(FneUtils.ReadBit(input, offset + 2U) ? 0x08U : 0x00U); - output |= (byte)(FneUtils.ReadBit(input, offset + 3U) ? 0x04U : 0x00U); - output |= (byte)(FneUtils.ReadBit(input, offset + 4U) ? 0x02U : 0x00U); - output |= (byte)(FneUtils.ReadBit(input, offset + 5U) ? 0x01U : 0x00U); - - return output; - } - - /// - /// - /// - /// - /// - /// - /// - private void hex2Bin(byte input, ref byte[] output, uint offset) - { - FneUtils.WriteBit(ref output, offset + 0U, (input & 0x20U) == 0x20U); - FneUtils.WriteBit(ref output, offset + 1U, (input & 0x10U) == 0x10U); - FneUtils.WriteBit(ref output, offset + 2U, (input & 0x08U) == 0x08U); - FneUtils.WriteBit(ref output, offset + 3U, (input & 0x04U) == 0x04U); - FneUtils.WriteBit(ref output, offset + 4U, (input & 0x02U) == 0x02U); - FneUtils.WriteBit(ref output, offset + 5U, (input & 0x01U) == 0x01U); + ReedSolomonAlgorithm.hex2Bin(codeword[i], ref data, offset); } ///