diff --git a/blocklib/digital/crc_append/.gitignore b/blocklib/digital/crc_append/.gitignore new file mode 100644 index 00000000..25d053a5 --- /dev/null +++ b/blocklib/digital/crc_append/.gitignore @@ -0,0 +1 @@ +meson.build \ No newline at end of file diff --git a/blocklib/digital/crc_append/crc_append.yml b/blocklib/digital/crc_append/crc_append.yml new file mode 100644 index 00000000..0f451367 --- /dev/null +++ b/blocklib/digital/crc_append/crc_append.yml @@ -0,0 +1,61 @@ +module: digital +block: crc_append +label: CRC Append +blocktype: block + +parameters: +- id: num_bits + label: CRC Size (bits) + dtype: size_t + grc: + default: 32 +- id: poly + label: CRC Polynomial + dtype: uint64_t + grc: + default: 0x4C11DB7 +- id: initial_value + label: Initial Register Value + dtype: uint64_t + grc: + default: 0xFFFFFFFF +- id: final_xor + label: Final XOR Value + dtype: uint64_t + grc: + default: 0xFFFFFFFF +- id: input_reflected + label: LSB-first Input + dtype: bool + grc: + default: True +- id: result_reflected + label: LSB-first Result + dtype: bool + grc: + default: True +- id: swap_endianness + label: LSB CRC in PDU + dtype: bool + grc: + default: False +- id: skip_header_bytes + label: Header Bytes to Skip + dtype: size_t + default: 0 + +ports: +- domain: message + id: in + direction: input + +- domain: message + id: out + direction: output + optional: true + +implementations: +- id: cpu +# - id: cuda + +file_format: 1 diff --git a/blocklib/digital/crc_append/crc_append_cpu.cc b/blocklib/digital/crc_append/crc_append_cpu.cc new file mode 100644 index 00000000..5e250482 --- /dev/null +++ b/blocklib/digital/crc_append/crc_append_cpu.cc @@ -0,0 +1,36 @@ +/* -*- c++ -*- */ +/* + * Copyright 2022 FIXME + * + * This file is part of GNU Radio + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "crc_append_cpu.h" +#include "crc_append_cpu_gen.h" + +namespace gr { +namespace digital { + +crc_append_cpu::crc_append_cpu(block_args args) + : INHERITED_CONSTRUCTORS, + d_num_bits(args.num_bits), + d_swap_endianness(args.swap_endianness), + d_crc(kernel::digital::crc(args.num_bits, + args.poly, + args.initial_value, + args.final_xor, + args.input_reflected, + args.result_reflected)), + d_header_bytes(args.skip_header_bytes) +{ + if (args.num_bits % 8 != 0) { + throw std::runtime_error("CRC number of bits must be divisible by 8"); + } +} + + +} // namespace digital +} // namespace gr \ No newline at end of file diff --git a/blocklib/digital/crc_append/crc_append_cpu.h b/blocklib/digital/crc_append/crc_append_cpu.h new file mode 100644 index 00000000..59a439aa --- /dev/null +++ b/blocklib/digital/crc_append/crc_append_cpu.h @@ -0,0 +1,70 @@ +/* -*- c++ -*- */ +/* + * Copyright 2022 FIXME + * + * This file is part of GNU Radio + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include + +#include +#include + +#include + +namespace gr { +namespace digital { + +class crc_append_cpu : public virtual crc_append +{ +public: + crc_append_cpu(block_args args); + +private: + unsigned d_num_bits; + bool d_swap_endianness; + kernel::digital::crc d_crc; + unsigned d_header_bytes; + + virtual void handle_msg_in(pmtf::pmt msg) override + { + auto meta = pmtf::get_as>( + pmtf::map(msg)["meta"]); + auto samples = pmtf::get_as>( + pmtf::map(msg)["data"]); + + const auto size = samples.size(); + if (size <= d_header_bytes) { + d_logger->warn("PDU too short; dropping"); + return; + } + + uint64_t crc = d_crc.compute(&samples[d_header_bytes], size - d_header_bytes); + + unsigned num_bytes = d_num_bits / 8; + if (d_swap_endianness) { + for (unsigned i = 0; i < num_bytes; ++i) { + samples.push_back(crc & 0xff); + crc >>= 8; + } + } + else { + for (unsigned i = 0; i < num_bytes; ++i) { + samples.push_back((crc >> (d_num_bits - 8 * (i + 1))) & 0xff); + } + } + + meta["packet_len"] = pmtf::get_as(meta["packet_len"]) + num_bytes; + auto pdu = pmtf::map({ { "data", samples }, { "meta", meta } }); + + this->get_message_port("out")->post(pdu); + } +}; + +} // namespace digital +} // namespace gr \ No newline at end of file diff --git a/blocklib/digital/meson.build b/blocklib/digital/meson.build index fa60cdb5..d9c354e4 100644 --- a/blocklib/digital/meson.build +++ b/blocklib/digital/meson.build @@ -1,19 +1,28 @@ +#autogenerated +# This file has been automatically generated and will be overwritten by meson build +# Remove the #autogenerated comment at the top if you wish for the build scripts to leave +# this file alone + subdir('include/gnuradio/digital') digital_sources = [] digital_cu_sources = [] +digital_pure_python_sources = [] digital_pybind_sources = [] digital_pybind_names = [] digital_deps = [] + + # Individual block subdirectories +subdir('crc_append') subdir('lib') -# if (get_option('enable_python')) -# subdir('python/digital') -# endif +if (get_option('enable_python')) + subdir('python/digital') +endif -# if (get_option('enable_testing')) -# subdir('test') -# endif \ No newline at end of file +if (get_option('enable_testing')) + subdir('test') +endif \ No newline at end of file diff --git a/blocklib/digital/test/meson.build b/blocklib/digital/test/meson.build index 7939bfe1..f8bc5527 100644 --- a/blocklib/digital/test/meson.build +++ b/blocklib/digital/test/meson.build @@ -3,7 +3,7 @@ ################################################### if get_option('enable_testing') - # test('qa_agc', find_program('qa_agc.py'), env: TEST_ENV) + # test('qa_crc', find_program('qa_crc.py'), env: TEST_ENV) # if (cuda_available and get_option('enable_cuda')) # test('qa_cufft', find_program('qa_cufft.py'), env: TEST_ENV) # endif diff --git a/blocklib/digital/test/qa_crc.py b/blocklib/digital/test/qa_crc.py new file mode 100755 index 00000000..7da72eac --- /dev/null +++ b/blocklib/digital/test/qa_crc.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# Copyright 2022 Daniel Estevez +# +# This file is part of GNU Radio +# +# SPDX-License-Identifier: GPL-3.0-or-later +# + +# FIXME: rework once PDU support is ready in PMTF + + +from gnuradio import gr, gr_unittest, blocks, digital +import pmtf + + +class qa_crc(gr_unittest.TestCase): + def setUp(self): + """Common part of all CRC tests + + Creates a flowgraph, a Message Debug block, and a PDU + containing the numbers 0x00 through 0x0F. + """ + self.tb = gr.top_block() + self.dbg = blocks.message_debug() + self.data = list(range(16)) + self.pdu = pmt.cons(pmt.PMT_NIL, + pmt.init_u8vector(len(self.data), self.data)) + + + def run_crc_append(self, crc_params, crc_result): + """Common part of CRC Append tests + + Creates a CRC Append block with the specified crc_params parameters, + connects it to the Message Debug block, sends a test PDU to the + CRC Append block, and checks that the output PDU matches the expected + crc_result. + """ + crc_append_block = digital.crc_append(*crc_params) + self.tb.msg_connect((crc_append_block, 'out'), (self.dbg, 'store')) + crc_append_block.to_basic_block()._post(pmt.intern('in'), self.pdu) + crc_append_block.to_basic_block()._post( + pmt.intern('system'), + pmt.cons(pmt.intern('done'), pmt.from_long(1))) + + self.tb.start() + self.tb.wait() + + self.assertEqual(self.dbg.num_messages(), 1) + out = pmt.u8vector_elements(pmt.cdr(self.dbg.get_message(0))) + self.assertEqual(out[:len(self.data)], self.data) + self.assertEqual(out[len(self.data):], crc_result) + + def common_test_crc_check(self, matching_crc, header_bytes=0): + """Common part of CRC Check tests + + Creates a CRC Append block and a CRC Check block using either the + same CRC or a different one depending on the whether matching_crc + is True or False. Connects CRC Append -> CRC Check -> Message Debug + and sends a PDU through. There are two message debugs to allow + checking whether the PDU ended up in the ok or fail port of the + CRC Check block. + """ + crc_append_block = digital.crc_append( + 16, 0x1021, 0x0, 0x0, False, False, False, header_bytes) + x = 0x0 if matching_crc else 0xFFFF + crc_check_block = digital.crc_check( + 16, 0x1021, x, x, False, False, False, True, header_bytes) + + self.dbg_fail = blocks.message_debug() + self.tb.msg_connect((crc_append_block, 'out'), (crc_check_block, 'in')) + self.tb.msg_connect((crc_check_block, 'ok'), (self.dbg, 'store')) + self.tb.msg_connect((crc_check_block, 'fail'), + (self.dbg_fail, 'store')) + + crc_append_block.to_basic_block()._post(pmt.intern('in'), self.pdu) + crc_append_block.to_basic_block()._post( + pmt.intern('system'), + pmt.cons(pmt.intern('done'), pmt.from_long(1))) + self.tb.start() + self.tb.wait() + + def test_crc_check(self): + """Test a successful CRC check + + Checks that the PDU ends in the ok port of CRC check + """ + self.common_test_crc_check(matching_crc=True) + self.assertEqual(self.dbg.num_messages(), 1) + out = pmt.u8vector_elements(pmt.cdr(self.dbg.get_message(0))) + self.assertEqual(out, self.data) + self.assertEqual(self.dbg_fail.num_messages(), 0) + + def test_crc_check_header_bytes(self): + """Test a successful CRC check (skipping some header bytes) + + Checks that the PDU ends in the ok port of CRC check + """ + self.common_test_crc_check(matching_crc=True, header_bytes=5) + self.assertEqual(self.dbg.num_messages(), 1) + out = pmt.u8vector_elements(pmt.cdr(self.dbg.get_message(0))) + self.assertEqual(out, self.data) + self.assertEqual(self.dbg_fail.num_messages(), 0) + + def test_crc_check_wrong_crc(self): + """Test a failed CRC check + + Checks that the PDU ends in the fail port of CRC check + """ + self.common_test_crc_check(matching_crc=False) + self.assertEqual(self.dbg.num_messages(), 0) + self.assertEqual(self.dbg_fail.num_messages(), 1) + out = pmt.u8vector_elements(pmt.cdr(self.dbg_fail.get_message(0))) + self.assertEqual(out, self.data) + + def test_crc_append_crc16_ccitt_zero(self): + """Test CRC-16-CCITT-Zero calculation""" + self.run_crc_append( + (16, 0x1021, 0x0, 0x0, + False, False, False), + [0x51, 0x3D]) + + def test_crc_append_crc16_ccitt_false(self): + """Test CRC-16-CCITT-False calculation""" + self.run_crc_append( + (16, 0x1021, 0xFFFF, 0x0, + False, False, False), + [0x3B, 0x37]) + + def test_crc_append_crc16_ccitt_x25(self): + """Test CRC-16-CCITT-X.25 calculation""" + self.run_crc_append( + (16, 0x1021, 0xFFFF, 0xFFFF, + True, True, False), + [0x13, 0xE9]) + + def test_crc_append_crc32(self): + """Test CRC-32 calculation""" + self.run_crc_append( + (32, 0x4C11DB7, 0xFFFFFFFF, 0xFFFFFFFF, + True, True, False), + [0xCE, 0xCE, 0xE2, 0x88]) + + def test_crc_append_crc32c(self): + """Test CRC-32C calculation""" + self.run_crc_append( + (32, 0x1EDC6F41, 0xFFFFFFFF, 0xFFFFFFFF, + True, True, False), + [0xD9, 0xC9, 0x08, 0xEB]) + + def test_crc_append_crc32c_endianness_swap(self): + """Test CRC-32C calculation with endianness swapped""" + self.run_crc_append( + (32, 0x1EDC6F41, 0xFFFFFFFF, 0xFFFFFFFF, + True, True, True), + [0xEB, 0x08, 0xC9, 0xD9]) + + def test_crc_append_crc32c_skip_header_bytes(self): + """Test CRC-32C calculation skipping some header bytes""" + skip_bytes = 3 + self.run_crc_append( + (32, 0x1EDC6F41, 0xFFFFFFFF, 0xFFFFFFFF, + True, True, False, skip_bytes), + [0xE8, 0x62, 0x60, 0x68]) + + +class qa_crc_class(gr_unittest.TestCase): + def test_crc_crc32c(self): + """Test CRC-32C calculation (using crc class directly)""" + c = digital.crc(32, 0x1EDC6F41, 0xFFFFFFFF, 0xFFFFFFFF, True, True) + out = c.compute(list(range(16))) + self.assertEqual(c.compute(list(range(16))), + 0xD9C908EB) + + +if __name__ == '__main__': + gr_unittest.run(qa_crc) + gr_unittest.run(qa_crc_class) diff --git a/kernel/include/gnuradio/kernel/digital/crc.h b/kernel/include/gnuradio/kernel/digital/crc.h new file mode 100644 index 00000000..b14a9c93 --- /dev/null +++ b/kernel/include/gnuradio/kernel/digital/crc.h @@ -0,0 +1,85 @@ +/* -*- c++ -*- */ +/* + * Copyright 2022 Daniel Estevez + * + * This file is part of GNU Radio + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include + +#include +#include +#include + +namespace gr { +namespace kernel { +namespace digital { + +/*! + * \brief Calculates a CRC + * \ingroup packet_operators_blk + * + * \details + * This class calculates a CRC with configurable parameters. + * A table-driven byte-by-byte approach is used in the CRC + * computation. + */ +class DIGITAL_API crc +{ +public: + /*! + * \brief Construct a CRC calculator instance. + * + * \param num_bits CRC size in bits + * \param poly CRC polynomial, in MSB-first notation + * \param initial_value Initial register value + * \param final_xor Final XOR value + * \param input_reflected true if the input is LSB-first, false if not + * \param result_reflected true if the output is LSB-first, false if not + */ + crc(unsigned num_bits, + uint64_t poly, + uint64_t initial_value, + uint64_t final_xor, + bool input_reflected, + bool result_reflected); + ~crc(); + + /*! + * \brief Computes a CRC + * + * \param data the input data for the CRC calculation + * \param len the length in bytes of the data + */ + uint64_t compute(const uint8_t* data, std::size_t len); + + /*! + * \brief Computes a CRC + * + * \param data the input data for the CRC calculation + */ + uint64_t compute(std::vector const& data) + { + return compute(data.data(), data.size()); + } + +private: + std::array d_table; + unsigned d_num_bits; + uint64_t d_mask; + uint64_t d_initial_value; + uint64_t d_final_xor; + bool d_input_reflected; + bool d_result_reflected; + + uint64_t reflect(uint64_t word) const; +}; + +} // namespace digital +} +} // namespace gr diff --git a/kernel/include/gnuradio/kernel/digital/meson.build b/kernel/include/gnuradio/kernel/digital/meson.build index add59f0c..03b04f29 100644 --- a/kernel/include/gnuradio/kernel/digital/meson.build +++ b/kernel/include/gnuradio/kernel/digital/meson.build @@ -1,6 +1,7 @@ headers = [ 'constellation.h', - 'metric_type.h' + 'metric_type.h', + 'crc.h' ] install_headers(headers, subdir : 'gnuradio/kernel/digital') \ No newline at end of file diff --git a/kernel/lib/digital/crc.cc b/kernel/lib/digital/crc.cc new file mode 100644 index 00000000..15f8f3b7 --- /dev/null +++ b/kernel/lib/digital/crc.cc @@ -0,0 +1,116 @@ +/* -*- c++ -*- */ +/* + * Copyright 2022 Daniel Estevez + * + * This file is part of GNU Radio + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + + +#include +#include + +namespace gr { +namespace kernel { +namespace digital { + +crc::crc(unsigned num_bits, + uint64_t poly, + uint64_t initial_value, + uint64_t final_xor, + bool input_reflected, + bool result_reflected) + : d_num_bits(num_bits), + d_mask(num_bits == 64 ? ~static_cast(0) + : (static_cast(1) << num_bits) - 1), + d_initial_value(initial_value & d_mask), + d_final_xor(final_xor & d_mask), + d_input_reflected(input_reflected), + d_result_reflected(result_reflected) +{ + if ((num_bits < 8) || (num_bits > 64)) { + throw std::runtime_error("CRC number of bits must be between 8 and 64"); + } + + d_table[0] = 0; + if (d_input_reflected) { + poly = reflect(poly); + uint64_t crc = 1; + int i = 128; + do { + if (crc & 1) { + crc = (crc >> 1) ^ poly; + } + else { + crc >>= 1; + } + for (int j = 0; j < 256; j += 2 * i) { + d_table[i + j] = (crc ^ d_table[j]) & d_mask; + } + i >>= 1; + } while (i > 0); + } + else { + const uint64_t msb = static_cast(1) << (num_bits - 1); + uint64_t crc = msb; + int i = 1; + do { + if (crc & msb) { + crc = (crc << 1) ^ poly; + } + else { + crc <<= 1; + } + for (int j = 0; j < i; ++j) { + d_table[i + j] = (crc ^ d_table[j]) & d_mask; + } + i <<= 1; + } while (i < 256); + } +} + +crc::~crc() {} + +uint64_t crc::compute(const uint8_t* data, std::size_t len) +{ + uint64_t rem = d_initial_value; + + if (d_input_reflected) { + for (std::size_t i = 0; i < len; ++i) { + uint8_t byte = data[i]; + uint8_t idx = (rem ^ byte) & 0xff; + rem = d_table[idx] ^ (rem >> 8); + } + } + else { + for (std::size_t i = 0; i < len; ++i) { + uint8_t byte = data[i]; + uint8_t idx = ((rem >> (d_num_bits - 8)) ^ byte) & 0xff; + rem = (d_table[idx] ^ (rem << 8)) & d_mask; + } + } + + if (d_input_reflected != d_result_reflected) { + rem = reflect(rem); + } + + rem = rem ^ d_final_xor; + return rem; +} + +uint64_t crc::reflect(uint64_t word) const +{ + uint64_t ret; + ret = word & 1; + for (unsigned i = 1; i < d_num_bits; ++i) { + word >>= 1; + ret = (ret << 1) | (word & 1); + } + return ret; +} + +} /* namespace digital */ +} // namespace kernel +} /* namespace gr */ diff --git a/kernel/lib/meson.build b/kernel/lib/meson.build index f07836e6..12aefd77 100644 --- a/kernel/lib/meson.build +++ b/kernel/lib/meson.build @@ -16,7 +16,8 @@ kernel_sources = [ 'math/fast_atan2f.cc', 'math/fxpt.cc', 'math/random.cc', - 'digital/constellation.cc' + 'digital/constellation.cc', + 'digital/crc.cc' ] compiler = meson.get_compiler('cpp')