mirror of
https://github.com/saymrwulf/uhd.git
synced 2026-05-14 20:58:09 +00:00
This fixes a bug where calling rfnoc_graph::disconnect() fails when the edge is not a forward edge, because the code would try and find an edge to remove that was a forward edge, even if source/destination ports would match. Error was discovered during use of X410_X4_400 image. The prevented the following use case: - Stream data from host to Replay Block - FDx Play and Record data via radio from/to replaz block - Stream recorded data from replay block to host The FDx use case requires one connection to be marked as back-edge (not is_forward_edge). The X410_X4_400 fpga does not include DDC/DUC block and the Radio and Replay blocks are directly connected to each other. To enable streaming data back to the host the required back-edge connecting needs to be disconnected. Commit also adds 2 additional rfnoc units tests modeling this use case w/o (X410_X4_400) and with (all 200MHz images) DDC/DUC blocks.
353 lines
13 KiB
C++
353 lines
13 KiB
C++
//
|
|
// Copyright 2019 Ettus Research, a National Instruments Brand
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
|
|
#include "rfnoc_graph_mock_nodes.hpp"
|
|
#include <uhd/rfnoc/node.hpp>
|
|
#include <uhd/utils/log.hpp>
|
|
#include <uhdlib/rfnoc/graph.hpp>
|
|
#include <uhdlib/rfnoc/node_accessor.hpp>
|
|
#include <uhdlib/rfnoc/prop_accessor.hpp>
|
|
#include <boost/test/unit_test.hpp>
|
|
#include <iostream>
|
|
|
|
using uhd::rfnoc::detail::graph_t;
|
|
using namespace uhd::rfnoc;
|
|
|
|
namespace uhd { namespace rfnoc { namespace detail {
|
|
|
|
/*! Helper class to access internals of detail::graph
|
|
*
|
|
* This is basically a cheat code to get around the 'private' part of graph_t.
|
|
*/
|
|
class graph_accessor_t
|
|
{
|
|
public:
|
|
using vertex_descriptor = graph_t::rfnoc_graph_t::vertex_descriptor;
|
|
|
|
graph_accessor_t(graph_t* graph_ptr) : _graph_ptr(graph_ptr)
|
|
{ /* nop */
|
|
}
|
|
|
|
graph_t::rfnoc_graph_t& get_graph()
|
|
{
|
|
return _graph_ptr->_graph;
|
|
}
|
|
|
|
template <typename VertexIterator>
|
|
graph_t::node_ref_t get_node_ref_from_iterator(VertexIterator it)
|
|
{
|
|
return boost::get(graph_t::vertex_property_t(), get_graph(), *it);
|
|
}
|
|
|
|
auto find_neighbour(vertex_descriptor origin, res_source_info port_info)
|
|
{
|
|
return _graph_ptr->_find_neighbour(origin, port_info);
|
|
}
|
|
|
|
auto find_dirty_nodes()
|
|
{
|
|
return _graph_ptr->_find_dirty_nodes();
|
|
}
|
|
|
|
auto get_topo_sorted_nodes()
|
|
{
|
|
return _graph_ptr->_vertices_to_nodes(_graph_ptr->_get_topo_sorted_nodes());
|
|
}
|
|
|
|
private:
|
|
graph_t* _graph_ptr;
|
|
};
|
|
|
|
}}}; // namespace uhd::rfnoc::detail
|
|
|
|
BOOST_AUTO_TEST_CASE(test_graph)
|
|
{
|
|
graph_t graph{};
|
|
uhd::rfnoc::detail::graph_accessor_t graph_accessor(&graph);
|
|
node_accessor_t node_accessor{};
|
|
|
|
auto& bgl_graph = graph_accessor.get_graph();
|
|
|
|
// Define some mock nodes:
|
|
// Source radio
|
|
mock_radio_node_t mock_rx_radio(0);
|
|
// Sink radio
|
|
mock_radio_node_t mock_tx_radio(1);
|
|
|
|
// These init calls would normally be done by the framework
|
|
node_accessor.init_props(&mock_rx_radio);
|
|
node_accessor.init_props(&mock_tx_radio);
|
|
|
|
// In this simple graph, all connections are identical from an edge info
|
|
// perspective, so we're lazy and share an edge_info object:
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info;
|
|
edge_info.src_port = 0;
|
|
edge_info.dst_port = 0;
|
|
edge_info.is_forward_edge = true;
|
|
edge_info.edge = uhd::rfnoc::detail::graph_t::graph_edge_t::DYNAMIC;
|
|
|
|
// Now create the graph:
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
|
|
|
|
// A whole bunch of low-level checks first:
|
|
BOOST_CHECK_EQUAL(boost::num_vertices(bgl_graph), 2);
|
|
auto vertex_iterators = boost::vertices(bgl_graph);
|
|
auto vertex_iterator = vertex_iterators.first;
|
|
auto rx_descriptor = *vertex_iterator;
|
|
graph_t::node_ref_t node_ref =
|
|
graph_accessor.get_node_ref_from_iterator(vertex_iterator++);
|
|
BOOST_CHECK_EQUAL(node_ref->get_unique_id(), mock_rx_radio.get_unique_id());
|
|
auto tx_descriptor = *vertex_iterator;
|
|
node_ref = graph_accessor.get_node_ref_from_iterator(vertex_iterator++);
|
|
BOOST_CHECK_EQUAL(node_ref->get_unique_id(), mock_tx_radio.get_unique_id());
|
|
BOOST_CHECK(vertex_iterator == vertex_iterators.second);
|
|
|
|
auto rx_neighbour_info =
|
|
graph_accessor.find_neighbour(rx_descriptor, {res_source_info::OUTPUT_EDGE, 0});
|
|
BOOST_REQUIRE(rx_neighbour_info.first);
|
|
BOOST_CHECK_EQUAL(
|
|
rx_neighbour_info.first->get_unique_id(), mock_tx_radio.get_unique_id());
|
|
BOOST_CHECK(
|
|
std::tie(rx_neighbour_info.second.src_port,
|
|
rx_neighbour_info.second.dst_port,
|
|
rx_neighbour_info.second.is_forward_edge)
|
|
== std::tie(edge_info.src_port, edge_info.dst_port, edge_info.is_forward_edge));
|
|
|
|
auto tx_neighbour_info =
|
|
graph_accessor.find_neighbour(tx_descriptor, {res_source_info::INPUT_EDGE, 0});
|
|
BOOST_REQUIRE(tx_neighbour_info.first);
|
|
BOOST_CHECK_EQUAL(
|
|
tx_neighbour_info.first->get_unique_id(), mock_rx_radio.get_unique_id());
|
|
BOOST_CHECK(
|
|
std::tie(tx_neighbour_info.second.src_port,
|
|
tx_neighbour_info.second.dst_port,
|
|
tx_neighbour_info.second.is_forward_edge)
|
|
== std::tie(edge_info.src_port, edge_info.dst_port, edge_info.is_forward_edge));
|
|
|
|
auto rx_upstream_neighbour_info =
|
|
graph_accessor.find_neighbour(rx_descriptor, {res_source_info::INPUT_EDGE, 0});
|
|
BOOST_CHECK(rx_upstream_neighbour_info.first == nullptr);
|
|
auto tx_downstream_neighbour_info =
|
|
graph_accessor.find_neighbour(tx_descriptor, {res_source_info::OUTPUT_EDGE, 0});
|
|
BOOST_CHECK(tx_downstream_neighbour_info.first == nullptr);
|
|
auto rx_wrongport_neighbour_info =
|
|
graph_accessor.find_neighbour(rx_descriptor, {res_source_info::OUTPUT_EDGE, 1});
|
|
BOOST_CHECK(rx_wrongport_neighbour_info.first == nullptr);
|
|
auto tx_wrongport_neighbour_info =
|
|
graph_accessor.find_neighbour(tx_descriptor, {res_source_info::INPUT_EDGE, 1});
|
|
BOOST_CHECK(tx_wrongport_neighbour_info.first == nullptr);
|
|
|
|
// Check there are no dirty nodes (init_props() will clean them all)
|
|
BOOST_CHECK_EQUAL(graph_accessor.find_dirty_nodes().empty(), true);
|
|
|
|
auto topo_sorted_nodes = graph_accessor.get_topo_sorted_nodes();
|
|
BOOST_CHECK_EQUAL(topo_sorted_nodes.size(), 2);
|
|
BOOST_CHECK_EQUAL(
|
|
topo_sorted_nodes.at(0)->get_unique_id(), mock_rx_radio.get_unique_id());
|
|
|
|
// Now initialize the graph (will force a call to resolve_all_properties())
|
|
graph.commit();
|
|
|
|
// This will be ignored
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
|
|
BOOST_CHECK_EQUAL(boost::num_vertices(bgl_graph), 2);
|
|
|
|
BOOST_REQUIRE_EQUAL(graph.enumerate_edges().size(), 1);
|
|
auto edge0_info = graph.enumerate_edges().at(0);
|
|
BOOST_CHECK_EQUAL(edge0_info.src_blockid, "MOCK_RADIO0");
|
|
BOOST_CHECK_EQUAL(edge0_info.src_port, 0);
|
|
BOOST_CHECK_EQUAL(edge0_info.dst_blockid, "MOCK_RADIO1");
|
|
BOOST_CHECK_EQUAL(edge0_info.dst_port, 0);
|
|
|
|
// Now attempt illegal connections (they must all fail)
|
|
edge_info.src_port = 1;
|
|
edge_info.dst_port = 0;
|
|
BOOST_REQUIRE_THROW(
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error);
|
|
edge_info.src_port = 0;
|
|
edge_info.dst_port = 1;
|
|
BOOST_REQUIRE_THROW(
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error);
|
|
edge_info.src_port = 0;
|
|
edge_info.dst_port = 0;
|
|
edge_info.is_forward_edge = false;
|
|
BOOST_REQUIRE_THROW(
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info), uhd::rfnoc_error);
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 1);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(test_graph_unresolvable)
|
|
{
|
|
graph_t graph{};
|
|
node_accessor_t node_accessor{};
|
|
|
|
// Define some mock nodes:
|
|
// Source radio
|
|
mock_radio_node_t mock_rx_radio(0);
|
|
// Sink radio
|
|
mock_radio_node_t mock_tx_radio(1);
|
|
|
|
// These init calls would normally be done by the framework
|
|
node_accessor.init_props(&mock_rx_radio);
|
|
node_accessor.init_props(&mock_tx_radio);
|
|
|
|
// In this simple graph, all connections are identical from an edge info
|
|
// perspective, so we're lazy and share an edge_info object:
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info(
|
|
0, 0, graph_t::graph_edge_t::DYNAMIC, true);
|
|
|
|
// Now create the graph and commit:
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
|
|
graph.commit();
|
|
|
|
// Now set a property that will cause the graph to fail to resolve:
|
|
BOOST_REQUIRE_THROW(mock_tx_radio.set_property<double>("master_clock_rate", 100e6, 0),
|
|
uhd::resolve_error);
|
|
|
|
// Now we add a back-edge
|
|
edge_info.src_port = 0;
|
|
edge_info.dst_port = 0;
|
|
edge_info.is_forward_edge = false;
|
|
graph.connect(&mock_tx_radio, &mock_rx_radio, edge_info);
|
|
UHD_LOG_INFO("TEST", "Testing back edge error path");
|
|
mock_tx_radio.disable_samp_out_resolver = true;
|
|
// The set_property would be valid if we hadn't futzed with the back-edge
|
|
BOOST_REQUIRE_THROW(mock_tx_radio.set_property<double>("master_clock_rate", 200e6, 0),
|
|
uhd::resolve_error);
|
|
UHD_LOG_INFO("TEST", "^^^ Expected ERROR here.");
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(test_graph_disconnect_reconnect)
|
|
{
|
|
graph_t graph{};
|
|
node_accessor_t node_accessor{};
|
|
|
|
// Define some mock nodes:
|
|
// Source radio
|
|
mock_radio_node_t mock_rx_radio(0);
|
|
// Sink radio
|
|
mock_radio_node_t mock_tx_radio(1);
|
|
|
|
// These init calls would normally be done by the framework
|
|
node_accessor.init_props(&mock_rx_radio);
|
|
node_accessor.init_props(&mock_tx_radio);
|
|
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info(
|
|
0, 0, graph_t::graph_edge_t::DYNAMIC, true);
|
|
|
|
// Now create the graph and commit:
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
|
|
graph.commit();
|
|
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 1);
|
|
|
|
// disconnect:
|
|
graph.disconnect(&mock_rx_radio, &mock_tx_radio, edge_info);
|
|
graph.release();
|
|
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 0);
|
|
|
|
// Reconnect:
|
|
graph.connect(&mock_rx_radio, &mock_tx_radio, edge_info);
|
|
graph.commit();
|
|
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 1);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(test_graph_backedge_w_ddc_disconnect_reconnect)
|
|
{
|
|
graph_t graph{};
|
|
node_accessor_t node_accessor{};
|
|
|
|
// Define some mock nodes:
|
|
// radio
|
|
mock_radio_node_t mock_radio(0);
|
|
// Replay (use mock_edge_node instead of replay)
|
|
mock_edge_node_t mock_replay_node(1, 1, "MOCK_EDGE_NODE<replay>");
|
|
// TX streamer
|
|
mock_streamer_t mock_rx_streamer(1);
|
|
// RX streamer
|
|
mock_streamer_t mock_tx_streamer(1);
|
|
// DUC
|
|
mock_ddc_node_t mock_ddc_node{};
|
|
// DDC
|
|
mock_ddc_node_t mock_duc_node{};
|
|
|
|
// These init calls would normally be done by the framework
|
|
node_accessor.init_props(&mock_radio);
|
|
node_accessor.init_props(&mock_replay_node);
|
|
node_accessor.init_props(&mock_rx_streamer);
|
|
node_accessor.init_props(&mock_tx_streamer);
|
|
node_accessor.init_props(&mock_ddc_node);
|
|
node_accessor.init_props(&mock_duc_node);
|
|
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info_ddc(
|
|
0, 0, graph_t::graph_edge_t::STATIC, true);
|
|
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info(
|
|
0, 0, graph_t::graph_edge_t::DYNAMIC, true);
|
|
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info_backedge(
|
|
0, 0, graph_t::graph_edge_t::DYNAMIC, false);
|
|
|
|
// Now create the graph (FDx Replay and Record):
|
|
// RX: radio (src) >> DDC >> Replay Block (sink)
|
|
graph.connect(&mock_radio, &mock_ddc_node, edge_info);
|
|
graph.connect(&mock_ddc_node, &mock_replay_node, edge_info);
|
|
// TX: Replay Block (src) >> DUC >> radio (sink)
|
|
graph.connect(&mock_replay_node, &mock_duc_node, edge_info);
|
|
graph.connect(&mock_duc_node, &mock_radio, edge_info_backedge);
|
|
graph.commit();
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 4);
|
|
|
|
// Disconnect Replay Block source (used for TX Radio)
|
|
// in preparation for connecting to RX streamer
|
|
graph.disconnect(&mock_replay_node, &mock_duc_node, edge_info);
|
|
graph.commit();
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 3);
|
|
}
|
|
|
|
BOOST_AUTO_TEST_CASE(test_graph_backedge_wo_ddc_disconnect_reconnect)
|
|
{
|
|
graph_t graph{};
|
|
node_accessor_t node_accessor{};
|
|
|
|
// Define some mock nodes:
|
|
// radio
|
|
mock_radio_node_t mock_radio(0);
|
|
// Replay (use mock_edge_node instead of replay)
|
|
mock_edge_node_t mock_replay_node(1, 1, "MOCK_EDGE_NODE<replay>");
|
|
// TX streamer
|
|
mock_streamer_t mock_rx_streamer(1);
|
|
// RX streamer
|
|
mock_streamer_t mock_tx_streamer(1);
|
|
|
|
// These init calls would normally be done by the framework
|
|
node_accessor.init_props(&mock_radio);
|
|
node_accessor.init_props(&mock_replay_node);
|
|
node_accessor.init_props(&mock_rx_streamer);
|
|
node_accessor.init_props(&mock_tx_streamer);
|
|
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info(
|
|
0, 0, graph_t::graph_edge_t::DYNAMIC, true);
|
|
|
|
uhd::rfnoc::detail::graph_t::graph_edge_t edge_info_backedge(
|
|
0, 0, graph_t::graph_edge_t::DYNAMIC, false);
|
|
|
|
// Now create the graph (FDx Replay and Record):
|
|
// RX: radio (src) sends data to Replay Block (sink)
|
|
graph.connect(&mock_radio, &mock_replay_node, edge_info);
|
|
// TX: Replay Block (src) sends data to radio (sink)
|
|
graph.connect(&mock_replay_node, &mock_radio, edge_info_backedge);
|
|
graph.commit();
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 2);
|
|
|
|
// Disconnect Replay Block source (used for TX Radio)
|
|
// in preparation for connecting to RX streamer
|
|
graph.disconnect(&mock_replay_node, &mock_radio, edge_info);
|
|
graph.commit();
|
|
BOOST_CHECK_EQUAL(graph.enumerate_edges().size(), 1);
|
|
}
|