diff options
| -rw-r--r-- | host/tests/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | host/tests/fir_filter_block_test.cpp | 241 | 
2 files changed, 247 insertions, 0 deletions
diff --git a/host/tests/CMakeLists.txt b/host/tests/CMakeLists.txt index 33bcdb734..f9f94e023 100644 --- a/host/tests/CMakeLists.txt +++ b/host/tests/CMakeLists.txt @@ -252,6 +252,12 @@ UHD_ADD_NONAPI_TEST(  )  UHD_ADD_NONAPI_TEST( +    TARGET fir_filter_block_test.cpp +    EXTRA_SOURCES +    ${CMAKE_SOURCE_DIR}/lib/rfnoc/graph.cpp +) + +UHD_ADD_NONAPI_TEST(      TARGET "transport_test.cpp"      EXTRA_SOURCES      ${CMAKE_SOURCE_DIR}/lib/transport/inline_io_service.cpp diff --git a/host/tests/fir_filter_block_test.cpp b/host/tests/fir_filter_block_test.cpp new file mode 100644 index 000000000..3bfce4b2d --- /dev/null +++ b/host/tests/fir_filter_block_test.cpp @@ -0,0 +1,241 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include "rfnoc_graph_mock_nodes.hpp" +#include <uhd/rfnoc/actions.hpp> +#include <uhd/rfnoc/defaults.hpp> +#include <uhd/rfnoc/fir_filter_block_control.hpp> +#include <uhd/rfnoc/mock_block.hpp> +#include <uhdlib/rfnoc/graph.hpp> +#include <uhdlib/rfnoc/node_accessor.hpp> +#include <uhdlib/utils/narrow.hpp> +#include <boost/test/unit_test.hpp> +#include <iostream> + +using namespace uhd::rfnoc; + +// Redeclare this here, since it's only defined outside of UHD_API +noc_block_base::make_args_t::~make_args_t() = default; + +/* + * This class extends mock_reg_iface_t by adding poke and peek hooks that + * monitor writes and reads to the registers implemented within the FIR + * filter RFNoC block hardware and emulating the expected behavior of the + * hardware when those registers are read and written. For instance, writes + * to the coefficient registers store the coefficients in a vector and + * track the position of the last write to the REG_FIR_LOAD_COEFF_LAST_ADDR + * register to allow the unit test to gauge the proper operation of the FIR + * filter block controller. + */ +class fir_filter_mock_reg_iface_t : public mock_reg_iface_t +{ +public: +    fir_filter_mock_reg_iface_t(size_t max_num_coeffs) : _max_num_coeffs(max_num_coeffs) {} + +    virtual void _poke_cb( +        uint32_t addr, uint32_t data, uhd::time_spec_t /*time*/, bool /*ack*/) +    { +        if (addr == fir_filter_block_control::REG_FIR_MAX_NUM_COEFFS_ADDR) { +            throw uhd::assertion_error("Invalid write to read-only register"); +        } else if (addr == fir_filter_block_control::REG_FIR_LOAD_COEFF_ADDR) { +            coeffs.push_back(uhd::narrow_cast<int16_t>(data)); +        } else if (addr == fir_filter_block_control::REG_FIR_LOAD_COEFF_LAST_ADDR) { +            last_coeff_write_pos = coeffs.size(); +            coeffs.push_back(uhd::narrow_cast<int16_t>(data)); +        } else { +            throw uhd::assertion_error("Invalid write to out of bounds address"); +        } +    } + +    virtual void _peek_cb(uint32_t addr, uhd::time_spec_t /*time*/) +    { +        if (addr == fir_filter_block_control::REG_FIR_MAX_NUM_COEFFS_ADDR) { +            read_memory[addr] = uhd::narrow_cast<int32_t>(_max_num_coeffs); +        } else { +            throw uhd::assertion_error("Invalid read from out of bounds address"); +        } +    } + +    void reset() +    { +        last_coeff_write_pos = 0; +        coeffs.clear(); +    } + +    size_t last_coeff_write_pos = 0; +    std::vector<int16_t> coeffs{}; + +private: +    const size_t _max_num_coeffs; +}; + + +/* fir_filter_block_fixture is a class which is instantiated before each test + * case is run. It sets up the block container, mock register interface, + * and fir_filter_block_control object, all of which are accessible to the test + * case. The instance of the object is destroyed at the end of each test + * case. + */ +constexpr size_t MAX_NUM_COEFFS = 3000; +constexpr size_t DEFAULT_MTU = 8000; + +struct fir_filter_block_fixture +{ +    fir_filter_block_fixture() +        : reg_iface(std::make_shared<fir_filter_mock_reg_iface_t>(MAX_NUM_COEFFS)) +        , block_container(get_mock_block(FIR_FILTER_BLOCK, +              1, +              2, +              uhd::device_addr_t(), +              DEFAULT_MTU, +              ANY_DEVICE, +              reg_iface)) +        , test_fir_filter(block_container.get_block<fir_filter_block_control>()) +    { +        node_accessor.init_props(test_fir_filter.get()); +    } + +    std::shared_ptr<fir_filter_mock_reg_iface_t> reg_iface; +    mock_block_container block_container; +    std::shared_ptr<fir_filter_block_control> test_fir_filter; +    node_accessor_t node_accessor{}; +}; + +/* + * This test case ensures that the hardware is programmed correctly with + * defaults when the fir_filter block is constructed. + */ +BOOST_FIXTURE_TEST_CASE(fir_filter_test_construction, fir_filter_block_fixture) +{ +    // Check that the number of coefficients is expected +    BOOST_CHECK_EQUAL(reg_iface->coeffs.size(), MAX_NUM_COEFFS); +    // Check that the first coefficient is the only non-zero value +    // (impulse response) +    BOOST_CHECK_NE(reg_iface->coeffs.at(0), 0); +    for (size_t i = 1; i < reg_iface->coeffs.size(); i++) { +        BOOST_CHECK_EQUAL(reg_iface->coeffs.at(i), 0); +    } +    // Check that the LOAD_COEFF_LAST register was written at the right +    // time (i.e. with the last value) +    BOOST_CHECK_EQUAL(reg_iface->last_coeff_write_pos, MAX_NUM_COEFFS - 1); +} + +/* + * This test case exercises the get_max_num_coefficients() API. + */ +BOOST_FIXTURE_TEST_CASE(fir_filter_test_max_num_coeffs, fir_filter_block_fixture) +{ +    BOOST_CHECK_EQUAL(test_fir_filter->get_max_num_coefficients(), MAX_NUM_COEFFS); +} + +/* + * This test case exercises the set_coefficients() API and get_coefficients() + * APIs and ensures that the hardware registers are programmed appropriately + * when new coefficients are specified. + */ +BOOST_FIXTURE_TEST_CASE(fir_filter_test_set_get_coefficients, fir_filter_block_fixture) +{ +    // Reset state of mock FIR filter register interface +    reg_iface->reset(); + +    // First test: 10 coefficients +    std::vector<int16_t> coeffs1{1, 2, 3, 4, 5, -1, -2, -3, -4, -5}; +    test_fir_filter->set_coefficients(coeffs1); + +    // Check that all coefficients were written +    BOOST_CHECK_EQUAL(reg_iface->coeffs.size(), MAX_NUM_COEFFS); + +    // Check correctness of coefficients +    for (size_t i = 0; i < coeffs1.size(); i++) { +        BOOST_CHECK_EQUAL(reg_iface->coeffs.at(i), coeffs1.at(i)); +    } +    for (size_t i = coeffs1.size(); i < MAX_NUM_COEFFS; i++) { +        BOOST_CHECK_EQUAL(reg_iface->coeffs.at(i), 0); +    } +    // Check that the LOAD_COEFF_LAST register was written at the right +    // time (i.e. with the last value) +    BOOST_CHECK_EQUAL(reg_iface->last_coeff_write_pos, MAX_NUM_COEFFS - 1); + +    // Verify that get_coefficients() returns what we expect. Note that +    // get_coefficients() returns the padded set of coefficients. +    std::vector<int16_t> received_coeffs = test_fir_filter->get_coefficients(); + +    BOOST_CHECK_EQUAL(received_coeffs.size(), MAX_NUM_COEFFS); + +    // Check correctness of returned coefficients +    for (size_t i = 0; i < coeffs1.size(); i++) { +        BOOST_CHECK_EQUAL(received_coeffs.at(i), coeffs1.at(i)); +    } +    for (size_t i = coeffs1.size(); i < MAX_NUM_COEFFS; i++) { +        BOOST_CHECK_EQUAL(received_coeffs.at(i), 0); +    } + +    reg_iface->reset(); + +    // Now update the coefficients with a smaller set, and ensure that +    // the hardware gets the correct coefficients +    std::vector<int16_t> coeffs2{1, 3, 5, 7}; +    test_fir_filter->set_coefficients(coeffs2); + +    // Check that all coefficients were written +    BOOST_CHECK_EQUAL(reg_iface->coeffs.size(), MAX_NUM_COEFFS); + +    // Check correctness of coefficients +    for (size_t i = 0; i < coeffs2.size(); i++) { +        BOOST_CHECK_EQUAL(reg_iface->coeffs.at(i), coeffs2.at(i)); +    } +    for (size_t i = coeffs2.size(); i < MAX_NUM_COEFFS; i++) { +        BOOST_CHECK_EQUAL(reg_iface->coeffs.at(i), 0); +    } +    // Check that the LOAD_COEFF_LAST register was written at the right +    // time (i.e. with the last value) +    BOOST_CHECK_EQUAL(reg_iface->last_coeff_write_pos, MAX_NUM_COEFFS - 1); +} + +/* + * This test case exercises the coefficient length checking of + * set_coefficients(). + */ +BOOST_FIXTURE_TEST_CASE(fir_filter_test_length_error, fir_filter_block_fixture) +{ +    size_t num_coeffs = test_fir_filter->get_max_num_coefficients(); +    std::vector<int16_t> coeffs(num_coeffs * 2); +    BOOST_CHECK_THROW(test_fir_filter->set_coefficients(coeffs), uhd::value_error); +} + +/* + * This test case ensures that the FIR filter block can be added to + * an RFNoC graph. + */ +BOOST_FIXTURE_TEST_CASE(fir_filter_test_graph, fir_filter_block_fixture) +{ +    detail::graph_t graph{}; +    detail::graph_t::graph_edge_t edge_port_info; +    edge_port_info.src_port                    = 0; +    edge_port_info.dst_port                    = 0; +    edge_port_info.property_propagation_active = true; +    edge_port_info.edge                        = detail::graph_t::graph_edge_t::DYNAMIC; + +    mock_radio_node_t mock_radio_block{0}; +    mock_ddc_node_t mock_ddc_block{}; +    mock_terminator_t mock_sink_term(1, {}, "MOCK_SINK"); + +    UHD_LOG_INFO("TEST", "Priming mock block properties"); +    node_accessor.init_props(&mock_radio_block); +    node_accessor.init_props(&mock_ddc_block); +    mock_sink_term.set_edge_property<std::string>( +        "type", "sc16", {res_source_info::INPUT_EDGE, 0}); +    mock_sink_term.set_edge_property<std::string>( +        "type", "sc16", {res_source_info::INPUT_EDGE, 1}); + +    UHD_LOG_INFO("TEST", "Creating graph..."); +    graph.connect(&mock_radio_block, &mock_ddc_block, edge_port_info); +    graph.connect(&mock_ddc_block, test_fir_filter.get(), edge_port_info); +    graph.connect(test_fir_filter.get(), &mock_sink_term, edge_port_info); +    UHD_LOG_INFO("TEST", "Committing graph..."); +    graph.commit(); +    UHD_LOG_INFO("TEST", "Commit complete."); +}  | 
