From d1fdf590eea45294aa46b5d86b31ae3639474ec7 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Wed, 14 Jan 2026 14:14:07 -0500 Subject: [PATCH] add DMR EMB and QR 16,7,6 tests; correct issue where QR 16,7,6 decode() was correcting and returning the wrong number of bits; correct issue where DMR EMB would not actually use the corrected QR 16,7,6 codeword; --- src/common/dmr/data/EMB.cpp | 5 +- src/common/edac/QR1676.cpp | 2 +- tests/dmr/EMB_Tests.cpp | 185 ++++++++++++++++++++++++++++++++++++ tests/edac/QR1676_Tests.cpp | 157 ++++++++++++++++++++++++++++++ 4 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 tests/dmr/EMB_Tests.cpp create mode 100644 tests/edac/QR1676_Tests.cpp diff --git a/src/common/dmr/data/EMB.cpp b/src/common/dmr/data/EMB.cpp index 46625a26..115566a2 100644 --- a/src/common/dmr/data/EMB.cpp +++ b/src/common/dmr/data/EMB.cpp @@ -47,8 +47,9 @@ void EMB::decode(const uint8_t* data) DMREMB[1U] = (data[18U] << 4) & 0xF0U; DMREMB[1U] |= (data[19U] >> 4) & 0x0FU; - // decode QR (16,7,6) FEC - edac::QR1676::decode(DMREMB); + // decode QR (16,7,6) FEC and get corrected data + uint8_t corrected = edac::QR1676::decode(DMREMB); + DMREMB[0U] = (corrected << 1) & 0xFEU; m_colorCode = (DMREMB[0U] >> 4) & 0x0FU; m_PI = (DMREMB[0U] & 0x08U) == 0x08U; diff --git a/src/common/edac/QR1676.cpp b/src/common/edac/QR1676.cpp index f2a39dc7..ab67ccc7 100644 --- a/src/common/edac/QR1676.cpp +++ b/src/common/edac/QR1676.cpp @@ -76,7 +76,7 @@ uint8_t QR1676::decode(const uint8_t* data) code ^= error_pattern; - return code >> 7; + return (code >> 8) & 0x7FU; } /* Encode QR (16,7,6) FEC. */ diff --git a/tests/dmr/EMB_Tests.cpp b/tests/dmr/EMB_Tests.cpp new file mode 100644 index 00000000..1713b916 --- /dev/null +++ b/tests/dmr/EMB_Tests.cpp @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ + +#include "common/Log.h" +#include "common/Utils.h" + +#include +#include + +#include "common/dmr/data/EMB.h" + +using namespace dmr::data; + +TEST_CASE("EMB encodes and decodes without errors", "[dmr][emb]") { + EMB emb; + emb.setColorCode(7); + emb.setPI(true); + emb.setLCSS(2); + + uint8_t data[24U]; + ::memset(data, 0x00U, sizeof(data)); + + emb.encode(data); + + EMB decoded; + decoded.decode(data); + + REQUIRE(decoded.getColorCode() == 7); + REQUIRE(decoded.getPI() == true); + REQUIRE(decoded.getLCSS() == 2); +} + +TEST_CASE("EMB corrects single-bit errors in embedded signaling", "[dmr][emb]") { + EMB emb; + emb.setColorCode(5); + emb.setPI(false); + emb.setLCSS(1); + + uint8_t data[24U]; + ::memset(data, 0x00U, sizeof(data)); + + emb.encode(data); + + // Save the encoded data + uint8_t original[24U]; + ::memcpy(original, data, sizeof(data)); + + // EMB data is stored in nibbles at positions 13, 14, 18, 19 + // This gives us 16 bits total to test + const uint32_t embPositions[] = {13, 14, 18, 19}; + + // Test single-bit errors in each nibble + for (auto pos : embPositions) { + for (uint32_t bit = 0; bit < 8; bit++) { + ::memcpy(data, original, sizeof(data)); + + // Introduce single-bit error + data[pos] ^= (1U << bit); + + EMB decoded; + decoded.decode(data); + + // QR(16,7,6) should correct single-bit errors + REQUIRE(decoded.getColorCode() == 5); + REQUIRE(decoded.getPI() == false); + REQUIRE(decoded.getLCSS() == 1); + } + } +} + +TEST_CASE("EMB corrects two-bit errors in embedded signaling", "[dmr][emb]") { + EMB emb; + emb.setColorCode(12); + emb.setPI(true); + emb.setLCSS(3); + + uint8_t data[24U]; + ::memset(data, 0x00U, sizeof(data)); + + emb.encode(data); + + // Save the encoded data + uint8_t original[24U]; + ::memcpy(original, data, sizeof(data)); + + // Test two-bit errors in different positions + const struct { + uint32_t pos1, bit1, pos2, bit2; + } errorPairs[] = { + {13, 0, 13, 7}, // Same byte + {13, 4, 14, 3}, // Adjacent bytes + {13, 5, 18, 2}, // Distant bytes + {14, 1, 19, 6}, // Different nibble pairs + {18, 0, 19, 7} // Same nibble pair + }; + + for (auto& pair : errorPairs) { + ::memcpy(data, original, sizeof(data)); + + // Introduce two-bit errors + data[pair.pos1] ^= (1U << pair.bit1); + data[pair.pos2] ^= (1U << pair.bit2); + + EMB decoded; + decoded.decode(data); + + // QR(16,7,6) should correct two-bit errors + REQUIRE(decoded.getColorCode() == 12); + REQUIRE(decoded.getPI() == true); + REQUIRE(decoded.getLCSS() == 3); + } +} + +TEST_CASE("EMB tests all color code values", "[dmr][emb]") { + // Color code is 4 bits (0-15) + for (uint32_t cc = 0; cc < 16; cc++) { + EMB emb; + emb.setColorCode(cc); + emb.setPI(cc & 1); // Alternate PI + emb.setLCSS(cc & 3); // Cycle through LCSS values + + uint8_t data[24U]; + ::memset(data, 0x00U, sizeof(data)); + + emb.encode(data); + + EMB decoded; + decoded.decode(data); + + REQUIRE(decoded.getColorCode() == cc); + REQUIRE(decoded.getPI() == (bool)(cc & 1)); + REQUIRE(decoded.getLCSS() == (cc & 3)); + } +} + +TEST_CASE("EMB verifies error correction restores correct values after corruption", "[dmr][emb]") { + // This test specifically verifies that the bug fix works: + // Before the fix, decode() would return the corrected value but EMB + // would read from the uncorrected buffer, causing wrong results. + + EMB emb; + emb.setColorCode(9); + emb.setPI(false); + emb.setLCSS(2); + + uint8_t data[24U]; + ::memset(data, 0xAAU, sizeof(data)); // Non-zero background + + emb.encode(data); + + // Corrupt the EMB data with a single-bit error in position 13, bit 4 + // This should be correctable by QR(16,7,6) + data[13] ^= 0x10U; + + EMB decoded; + decoded.decode(data); + + // Verify the corrected values are read (not the corrupted buffer) + REQUIRE(decoded.getColorCode() == 9); + REQUIRE(decoded.getPI() == false); + REQUIRE(decoded.getLCSS() == 2); + + // Now encode again and verify we get the same result as original + uint8_t reencoded[24U]; + ::memset(reencoded, 0xAAU, sizeof(reencoded)); // Same background as original + decoded.encode(reencoded); + + // The EMB portions should match the original uncorrupted encoding + uint8_t original[24U]; + ::memset(original, 0xAAU, sizeof(original)); + emb.encode(original); + + // EMB data is in nibbles, so we need to mask and compare + REQUIRE((reencoded[13] & 0x0F) == (original[13] & 0x0F)); + REQUIRE((reencoded[14] & 0xF0) == (original[14] & 0xF0)); + REQUIRE((reencoded[18] & 0x0F) == (original[18] & 0x0F)); + REQUIRE((reencoded[19] & 0xF0) == (original[19] & 0xF0)); +} diff --git a/tests/edac/QR1676_Tests.cpp b/tests/edac/QR1676_Tests.cpp new file mode 100644 index 00000000..4c7d53a2 --- /dev/null +++ b/tests/edac/QR1676_Tests.cpp @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Digital Voice Modem - Test Suite + * GPLv2 Open Source. Use is subject to license terms. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright (C) 2026 Bryan Biedenkapp, N2PLL + * + */ + +#include "common/Log.h" +#include "common/Utils.h" + +#include +#include + +#include "common/edac/QR1676.h" + +using namespace edac; + +TEST_CASE("QR1676 preserves all-zero data", "[edac][qr1676]") { + uint8_t data[2U]; + ::memset(data, 0x00U, sizeof(data)); + + QR1676::encode(data); + uint8_t errors = QR1676::decode(data); + + REQUIRE(errors == 0x00U); + REQUIRE(data[0U] == 0x00U); + REQUIRE(data[1U] == 0x00U); +} + +TEST_CASE("QR1676 preserves all-ones data", "[edac][qr1676]") { + // QR(16,7,6): 7 data bits + 9 parity bits = 16 bits + // Data is stored in upper 7 bits of first byte, shifted left by 1 + uint8_t data[2U]; + data[0U] = 0xFEU; // 0b11111110 - all 7 data bits set, LSB is parity start + data[1U] = 0x00U; + + QR1676::encode(data); + Utils::dump(2U, "QR1676::encode() all ones", data, 2U); + + uint8_t errors = QR1676::decode(data); + + REQUIRE(errors == 0x7FU); // 7 data bits all set +} + +TEST_CASE("QR1676 encodes and decodes specific patterns", "[edac][qr1676]") { + const uint8_t testValues[] = {0x00U, 0x2AU, 0x54U, 0x0FU, 0x70U, 0x33U, 0x66U, 0x5AU, 0x4BU}; + + for (auto value : testValues) { + uint8_t data[2U]; + ::memset(data, 0x00U, sizeof(data)); + + // Store 7-bit value in upper bits, shifted left by 1 + data[0U] = (value & 0x7FU) << 1; + + QR1676::encode(data); + uint8_t decoded = QR1676::decode(data); + + REQUIRE(decoded == (value & 0x7FU)); + } +} + +TEST_CASE("QR1676 encodes all 128 possible 7-bit values", "[edac][qr1676]") { + for (uint32_t value = 0U; value < 128U; value++) { + uint8_t data[2U]; + ::memset(data, 0x00U, sizeof(data)); + + data[0U] = (value & 0x7FU) << 1; + + QR1676::encode(data); + uint8_t decoded = QR1676::decode(data); + + REQUIRE(decoded == value); + } +} + +TEST_CASE("QR1676 corrects single-bit errors", "[edac][qr1676]") { + uint8_t original = 0x5AU; // Test pattern + + uint8_t data[2U]; + ::memset(data, 0x00U, sizeof(data)); + data[0U] = (original & 0x7FU) << 1; + + QR1676::encode(data); + + // Save encoded data + uint8_t encoded[2U]; + ::memcpy(encoded, data, 2U); + + // Test single-bit errors in all 16 bit positions + for (uint32_t bit = 0U; bit < 16U; bit++) { + ::memcpy(data, encoded, 2U); + + // Introduce single-bit error + uint32_t bytePos = bit / 8; + uint32_t bitPos = bit % 8; + data[bytePos] ^= (1U << bitPos); + + uint8_t decoded = QR1676::decode(data); + + // QR(16,7,6) should correct all single-bit errors + REQUIRE(decoded == (original & 0x7FU)); + } +} + +TEST_CASE("QR1676 corrects two-bit errors", "[edac][qr1676]") { + uint8_t original = 0x3CU; // Test pattern + + uint8_t data[2U]; + ::memset(data, 0x00U, sizeof(data)); + data[0U] = (original & 0x7FU) << 1; + + QR1676::encode(data); + + // Save encoded data + uint8_t encoded[2U]; + ::memcpy(encoded, data, 2U); + + // Test two-bit error patterns + const uint32_t errorPairs[][2] = { + {0, 7}, {1, 8}, {2, 11}, {4, 13}, {6, 15} + }; + + for (auto& pair : errorPairs) { + ::memcpy(data, encoded, 2U); + + // Introduce two-bit errors + for (auto bitPos : pair) { + uint32_t bytePos = bitPos / 8; + uint32_t bit = bitPos % 8; + data[bytePos] ^= (1U << bit); + } + + uint8_t decoded = QR1676::decode(data); + + // QR(16,7,6) should correct two-bit errors + REQUIRE(decoded == (original & 0x7FU)); + } +} + +TEST_CASE("QR1676 handles random 7-bit patterns", "[edac][qr1676]") { + // Test with various pseudo-random patterns + for (uint32_t test = 0; test < 10; test++) { + uint8_t value = (uint8_t)((test * 37 + 53) % 128); + + uint8_t data[2U]; + ::memset(data, 0x00U, sizeof(data)); + data[0U] = (value & 0x7FU) << 1; + + QR1676::encode(data); + uint8_t decoded = QR1676::decode(data); + + REQUIRE(decoded == value); + } +}