From 3b39cb90df3e22897091735dc7bc0c3675eef518 Mon Sep 17 00:00:00 2001
From: Aaron Rossetto <aaron.rossetto@ni.com>
Date: Tue, 21 Jul 2020 16:05:56 -0500
Subject: tests: Add unit test for siggen RFNoC block controller

---
 host/tests/rfnoc_block_tests/siggen_block_test.cpp | 348 +++++++++++++++++++++
 1 file changed, 348 insertions(+)
 create mode 100644 host/tests/rfnoc_block_tests/siggen_block_test.cpp

(limited to 'host/tests/rfnoc_block_tests')

diff --git a/host/tests/rfnoc_block_tests/siggen_block_test.cpp b/host/tests/rfnoc_block_tests/siggen_block_test.cpp
new file mode 100644
index 000000000..d495e7a19
--- /dev/null
+++ b/host/tests/rfnoc_block_tests/siggen_block_test.cpp
@@ -0,0 +1,348 @@
+//
+// Copyright 2019 Ettus Research, a National Instruments Brand
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+
+#define _USE_MATH_DEFINES
+
+#include "../rfnoc_graph_mock_nodes.hpp"
+#include <uhd/rfnoc/actions.hpp>
+#include <uhd/rfnoc/defaults.hpp>
+#include <uhd/rfnoc/mock_block.hpp>
+#include <uhd/rfnoc/multichan_register_iface.hpp>
+#include <uhd/rfnoc/register_iface_holder.hpp>
+#include <uhd/rfnoc/siggen_block_control.hpp>
+#include <uhdlib/rfnoc/graph.hpp>
+#include <uhdlib/rfnoc/node_accessor.hpp>
+#include <uhdlib/utils/narrow.hpp>
+#include <boost/test/unit_test.hpp>
+#include <cmath>
+#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;
+
+constexpr size_t NUM_PORTS   = 4;
+constexpr size_t DEFAULT_MTU = 8000;
+
+/*
+ * This class extends mock_reg_iface_t, handling three particular registers
+ */
+class siggen_mock_reg_iface_t : public mock_reg_iface_t
+{
+public:
+    siggen_mock_reg_iface_t(size_t num_ports)
+        : _num_ports(num_ports)
+        , enables(_num_ports, false)
+        , spps(_num_ports, 0)
+        , waveforms(_num_ports, siggen_waveform::CONSTANT)
+        , gains(_num_ports, 0)
+        , constants(_num_ports, 0)
+        , phase_increments(_num_ports, 0)
+        , phasors(_num_ports, 0)
+    {
+    }
+
+    virtual void _poke_cb(
+        uint32_t addr, uint32_t data, uhd::time_spec_t /*time*/, bool /*ack*/)
+    {
+        const size_t port = addr / siggen_block_control::REG_BLOCK_SIZE;
+        if (port >= _num_ports) {
+            throw uhd::assertion_error("Invalid port index");
+        }
+
+        const size_t offset = addr % siggen_block_control::REG_BLOCK_SIZE;
+        if (offset == siggen_block_control::REG_ENABLE_OFFSET) {
+            enables[port] = (data > 0);
+        } else if (offset == siggen_block_control::REG_SPP_OFFSET) {
+            spps[port] = data;
+        } else if (offset == siggen_block_control::REG_WAVEFORM_OFFSET) {
+            waveforms[port] = static_cast<siggen_waveform>(data);
+        } else if (offset == siggen_block_control::REG_GAIN_OFFSET) {
+            gains[port] = data;
+        } else if (offset == siggen_block_control::REG_CONSTANT_OFFSET) {
+            constants[port] = data;
+        } else if (offset == siggen_block_control::REG_PHASE_INC_OFFSET) {
+            phase_increments[port] = data;
+        } else if (offset == siggen_block_control::REG_CARTESIAN_OFFSET) {
+            phasors[port] = data;
+        } else {
+            throw uhd::assertion_error("Invalid write to out of bounds offset");
+        }
+    }
+
+    virtual void _peek_cb(uint32_t addr, uhd::time_spec_t /*time*/)
+    {
+        const size_t port = addr / siggen_block_control::REG_BLOCK_SIZE;
+        if (port >= _num_ports) {
+            throw uhd::assertion_error("Invalid port index");
+        }
+
+        const size_t offset = addr % siggen_block_control::REG_BLOCK_SIZE;
+        if (offset == siggen_block_control::REG_ENABLE_OFFSET) {
+            read_memory[addr] = enables[port] ? 1 : 0;
+        } else if (offset == siggen_block_control::REG_SPP_OFFSET) {
+            read_memory[addr] = spps[port];
+        } else if (offset == siggen_block_control::REG_WAVEFORM_OFFSET) {
+            read_memory[addr] = static_cast<uint32_t>(waveforms[port]);
+        } else if (offset == siggen_block_control::REG_GAIN_OFFSET) {
+            read_memory[addr] = gains[port];
+        } else if (offset == siggen_block_control::REG_CONSTANT_OFFSET) {
+            read_memory[addr] = constants[port];
+        } else if (offset == siggen_block_control::REG_PHASE_INC_OFFSET) {
+            read_memory[addr] = phase_increments[port];
+        } else if (offset == siggen_block_control::REG_CARTESIAN_OFFSET) {
+            read_memory[addr] = phasors[port];
+        } else {
+            throw uhd::assertion_error("Invalid read from out of bounds offset");
+        }
+    }
+
+    template <class T>
+    static const T clamp(const double v)
+    {
+        constexpr T min_t = std::numeric_limits<T>::min();
+        constexpr T max_t = std::numeric_limits<T>::max();
+        return (v < min_t) ? min_t : (v > max_t) ? max_t : T(v);
+    }
+
+    static uint32_t gain_to_register(double gain)
+    {
+        const int16_t gain_fp = clamp<int16_t>(gain * 32768.0);
+        return static_cast<uint32_t>(gain_fp) & 0xffff;
+    }
+
+    static uint32_t constant_to_register(std::complex<double> constant)
+    {
+        const int16_t constant_i_fp = clamp<int16_t>(constant.real() * 32768.0);
+        const int16_t constant_q_fp = clamp<int16_t>(constant.imag() * 32768.0);
+        return (uint32_t(constant_i_fp) << 16) | (uint32_t(constant_q_fp) & 0xffff);
+    }
+
+    static uint32_t phase_increment_to_register(double phase_inc)
+    {
+        const int16_t phase_inc_scaled_rads_fp =
+            clamp<int16_t>((phase_inc / M_PI) * 8192.0);
+        return static_cast<uint32_t>(phase_inc_scaled_rads_fp) & 0xffff;
+    }
+
+    static uint32_t phasor_to_register(std::complex<double> phasor)
+    {
+        phasor /= 1.164435344782938; /* CORDIC scale value--see siggen_block_control */
+        const int16_t phasor_i_fp = clamp<int16_t>(phasor.real() * 32767.0);
+        const int16_t phasor_q_fp = clamp<int16_t>(phasor.imag() * 32767.0);
+        return (uint32_t(phasor_i_fp) << 16) | (uint32_t(phasor_q_fp) & 0xffff);
+    }
+
+private:
+    const size_t _num_ports;
+
+public:
+    std::vector<bool> enables;
+    std::vector<uint32_t> spps;
+    std::vector<siggen_waveform> waveforms;
+    std::vector<uint32_t> gains;
+    std::vector<uint32_t> constants;
+    std::vector<uint32_t> phase_increments;
+    std::vector<uint32_t> phasors;
+};
+
+/*
+ * siggen_block_fixture is a class which is instantiated before each test
+ * case is run. It sets up the block container, mock register interface,
+ * and siggen_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.
+ */
+struct siggen_block_fixture
+{
+    siggen_block_fixture()
+        : reg_iface(std::make_shared<siggen_mock_reg_iface_t>(NUM_PORTS))
+        , block_container(get_mock_block(SIGGEN_BLOCK,
+              NUM_PORTS,
+              NUM_PORTS,
+              uhd::device_addr_t(),
+              DEFAULT_MTU,
+              ANY_DEVICE,
+              reg_iface))
+        , test_siggen(block_container.get_block<siggen_block_control>())
+    {
+        node_accessor.init_props(test_siggen.get());
+    }
+
+    std::shared_ptr<siggen_mock_reg_iface_t> reg_iface;
+    mock_block_container block_container;
+    std::shared_ptr<siggen_block_control> test_siggen;
+    node_accessor_t node_accessor{};
+};
+
+/*
+ * This test case ensures that the hardware is programmed correctly with
+ * defaults when the siggen block is constructed.
+ */
+BOOST_FIXTURE_TEST_CASE(siggen_test_construction, siggen_block_fixture)
+{
+    for(size_t port = 0; port < NUM_PORTS; port++) {
+        BOOST_CHECK_EQUAL(reg_iface->enables.at(port), 0);
+        BOOST_CHECK(reg_iface->waveforms.at(port) == siggen_waveform::CONSTANT);
+        BOOST_CHECK_EQUAL(reg_iface->gains.at(port), siggen_mock_reg_iface_t::gain_to_register(1.0));
+        BOOST_CHECK_EQUAL(reg_iface->constants.at(port), siggen_mock_reg_iface_t::constant_to_register({1.0, 1.0}));
+        BOOST_CHECK_EQUAL(reg_iface->phase_increments.at(port), siggen_mock_reg_iface_t::phase_increment_to_register(1.0));
+        BOOST_CHECK_EQUAL(reg_iface->phasors.at(port),
+            siggen_mock_reg_iface_t::phasor_to_register({0.0, 0.0}));
+        BOOST_CHECK_EQUAL(reg_iface->spps.at(port), DEFAULT_SPP);
+    }
+}
+
+/*
+ * This test case exercises the API and ensures that the registers are
+ * programmed appropriately.
+ */
+BOOST_FIXTURE_TEST_CASE(siggen_test_api, siggen_block_fixture)
+{
+    for(size_t port = 0; port < NUM_PORTS; port++) {
+        test_siggen->set_enable(true, port);
+        BOOST_CHECK_EQUAL(reg_iface->enables.at(port), 1);
+        BOOST_CHECK_EQUAL(test_siggen->get_enable(port), true);
+
+        // Set constant mode on the function generator, and then make sure
+        // that the gain register stays fixed at 1 and that changing the
+        // amplitude has no effect on the register or the attribute value
+        // (which should always return 1 in this mode).
+        const double amplitude = 0.25 + (port * 0.1);
+        test_siggen->set_waveform(siggen_waveform::CONSTANT, port);
+        BOOST_CHECK(reg_iface->waveforms.at(port) == siggen_waveform::CONSTANT);
+        BOOST_CHECK(test_siggen->get_waveform(port) == siggen_waveform::CONSTANT);
+        test_siggen->set_amplitude(amplitude, port);
+        BOOST_CHECK_EQUAL(
+            reg_iface->gains.at(port), siggen_mock_reg_iface_t::gain_to_register(1.0));
+        BOOST_CHECK_EQUAL(test_siggen->get_amplitude(port), 1.0);
+
+        // Set sine wave mode on the function generator, and then make sure
+        // that the gain register stays fixed at 1, but that the Cartesian
+        // register changes to reflect the desired sinusoidal amplitude.
+        test_siggen->set_waveform(siggen_waveform::SINE_WAVE, port);
+        BOOST_CHECK(reg_iface->waveforms.at(port) == siggen_waveform::SINE_WAVE);
+        test_siggen->set_amplitude(amplitude, port);
+        BOOST_CHECK_EQUAL(
+            reg_iface->gains.at(port), siggen_mock_reg_iface_t::gain_to_register(1.0));
+        BOOST_CHECK_EQUAL(reg_iface->phasors.at(port),
+            siggen_mock_reg_iface_t::phasor_to_register({amplitude, 0.0}));
+        BOOST_CHECK_EQUAL(test_siggen->get_amplitude(port), amplitude);
+
+        // Set noise mode on the function generator, and then make sure
+        // that the gain register changes according to the amplitude.
+        test_siggen->set_waveform(siggen_waveform::NOISE, port);
+        BOOST_CHECK(reg_iface->waveforms.at(port) == siggen_waveform::NOISE);
+        BOOST_CHECK(test_siggen->get_waveform(port) == siggen_waveform::NOISE);
+        test_siggen->set_amplitude(amplitude, port);
+        BOOST_CHECK_EQUAL(reg_iface->gains.at(port),
+            siggen_mock_reg_iface_t::gain_to_register(amplitude));
+        BOOST_CHECK_EQUAL(test_siggen->get_amplitude(port), amplitude);
+
+        const std::complex<double> constant{-0.5 - (port * 0.05), 0.5 + (port * 0.05)};
+        test_siggen->set_constant(constant, port);
+        BOOST_CHECK_EQUAL(reg_iface->constants.at(port), siggen_mock_reg_iface_t::constant_to_register(constant));
+        BOOST_CHECK_EQUAL(test_siggen->get_constant(port), constant);
+
+        const double phase_inc = (port * M_PI / 16.0);
+        test_siggen->set_sine_phase_increment(phase_inc, port);
+        BOOST_CHECK_EQUAL(reg_iface->phase_increments.at(port), siggen_mock_reg_iface_t::phase_increment_to_register(phase_inc));
+        BOOST_CHECK_EQUAL(test_siggen->get_sine_phase_increment(port), phase_inc);
+
+        const double freq      = 1000 + (100 * port);
+        const double samp_rate = 1e6;
+        test_siggen->set_sine_frequency(freq, samp_rate, port);
+        const double calculated_phase_inc = freq / samp_rate * 2.0 * M_PI;
+        BOOST_CHECK_EQUAL(reg_iface->phase_increments.at(port),
+            siggen_mock_reg_iface_t::phase_increment_to_register(calculated_phase_inc));
+        BOOST_CHECK_EQUAL(
+            test_siggen->get_sine_phase_increment(port), calculated_phase_inc);
+
+        size_t spp = 100 + (200 * port);
+        test_siggen->set_samples_per_packet(spp, port);
+        BOOST_CHECK_EQUAL(reg_iface->spps.at(port), spp);
+        BOOST_CHECK_EQUAL(test_siggen->get_samples_per_packet(port), spp);
+    }
+}
+
+/*
+ * This test case exercises the range checking performed on the siggen
+ * settings, ensuring that the appropriate exception is thrown when out of
+ * range.
+ */
+BOOST_FIXTURE_TEST_CASE(siggen_test_ranges, siggen_block_fixture)
+{
+    for(size_t port = 0; port < NUM_PORTS; port++) {
+        BOOST_CHECK_THROW(test_siggen->set_property<int>("waveform", 100, port), uhd::value_error);
+
+        const double bad_amplitude = 100 + (port * 100);
+        // I got a bad amplitude!
+        BOOST_CHECK_THROW(
+            test_siggen->set_amplitude(bad_amplitude, port), uhd::value_error);
+        BOOST_CHECK_THROW(
+            test_siggen->set_amplitude(-bad_amplitude, port), uhd::value_error);
+
+        const std::complex<double> bad_constant_i{-100.0 + (port * 100), 0.1};
+        BOOST_CHECK_THROW(test_siggen->set_constant(bad_constant_i, port), uhd::value_error);
+        BOOST_CHECK_THROW(test_siggen->set_constant(-bad_constant_i, port), uhd::value_error);
+
+        const std::complex<double> bad_constant_q{-0.1, 100.0 + (port * 100)};
+        BOOST_CHECK_THROW(test_siggen->set_constant(bad_constant_q, port), uhd::value_error);
+        BOOST_CHECK_THROW(test_siggen->set_constant(-bad_constant_q, port), uhd::value_error);
+
+        const double bad_phase_inc = 5 * M_PI;
+        BOOST_CHECK_THROW(
+            test_siggen->set_sine_phase_increment(bad_phase_inc, port), uhd::value_error);
+        BOOST_CHECK_THROW(test_siggen->set_sine_phase_increment(-bad_phase_inc, port),
+            uhd::value_error);
+
+        const double bad_samp_rate_zero = 0.0;
+        BOOST_CHECK_THROW(test_siggen->set_sine_frequency(10.0, bad_samp_rate_zero, port),
+            uhd::value_error);
+
+        const double bad_samp_rate = 30.0 + (port * 10);
+        const double bad_freq      = bad_samp_rate * 10.0;
+        BOOST_CHECK_THROW(test_siggen->set_sine_frequency(bad_freq, bad_samp_rate, port),
+            uhd::value_error);
+    }
+}
+
+/*
+ * This test case exercises the coercion of the SPP parameter to ensure that
+ * it does not surpass the MTU.
+ */
+BOOST_FIXTURE_TEST_CASE(siggen_test_spp_coercion, siggen_block_fixture)
+{
+    constexpr size_t mtu_in_samps = DEFAULT_MTU / 4 /* bytes/samp */;
+    size_t high_spp = mtu_in_samps + 10;
+    test_siggen->set_samples_per_packet(high_spp, 0);
+    BOOST_CHECK_EQUAL(test_siggen->get_samples_per_packet(0), mtu_in_samps);
+}
+
+/*
+ * This test case ensures that the siggen block controller can be added
+ * to a graph.
+ */
+BOOST_FIXTURE_TEST_CASE(siggen_test_graph, siggen_block_fixture)
+{
+    detail::graph_t graph{};
+    detail::graph_t::graph_edge_t edge_info{
+        0, 0, detail::graph_t::graph_edge_t::DYNAMIC, true};
+
+    mock_terminator_t mock_sink_term(NUM_PORTS, {}, "MOCK_SINK");
+
+    mock_sink_term.set_edge_property<std::string>(
+        "type", "sc16", {res_source_info::INPUT_EDGE, 0});
+
+    UHD_LOG_INFO("TEST", "Creating graph...");
+    for(size_t port = 0; port < NUM_PORTS; port++) {
+        graph.connect(test_siggen.get(), &mock_sink_term, {port, port, detail::graph_t::graph_edge_t::DYNAMIC, true});
+    }
+    UHD_LOG_INFO("TEST", "Committing graph...");
+    graph.commit();
+    UHD_LOG_INFO("TEST", "Commit complete.");
+}
-- 
cgit v1.2.3