diff options
author | Martin Braun <martin.braun@ettus.com> | 2021-11-29 14:50:18 +0100 |
---|---|---|
committer | Aaron Rossetto <aaron.rossetto@ni.com> | 2021-12-03 11:34:35 -0800 |
commit | b49e4f1eb62081a035c8d2e1d7b50e65e027ad72 (patch) | |
tree | 7be7376fcde20adc9e8a16111b5aabba50b53cf8 | |
parent | f249b73036ce55b3beecad94cd0f362b6a2b8b5d (diff) | |
download | uhd-b49e4f1eb62081a035c8d2e1d7b50e65e027ad72.tar.gz uhd-b49e4f1eb62081a035c8d2e1d7b50e65e027ad72.tar.bz2 uhd-b49e4f1eb62081a035c8d2e1d7b50e65e027ad72.zip |
docs: Improve documentation for properties and -propagation
-rw-r--r-- | host/docs/coding.dox | 4 | ||||
-rw-r--r-- | host/docs/group_defs.dox | 2 | ||||
-rw-r--r-- | host/docs/properties.dox | 453 | ||||
-rw-r--r-- | host/docs/rfnoc_docs.dox | 8 | ||||
-rw-r--r-- | host/include/uhd/rfnoc/noc_block_base.hpp | 3 | ||||
-rw-r--r-- | host/include/uhd/rfnoc/node.hpp | 8 | ||||
-rw-r--r-- | host/include/uhd/rfnoc/replay_block_control.hpp | 19 | ||||
-rw-r--r-- | host/include/uhd/rfnoc_graph.hpp | 3 |
8 files changed, 498 insertions, 2 deletions
diff --git a/host/docs/coding.dox b/host/docs/coding.dox index aab495c25..e5016ab04 100644 --- a/host/docs/coding.dox +++ b/host/docs/coding.dox @@ -10,6 +10,10 @@ The Multi-USRP class provides a high-level interface to a single USRP device with one or more channels, or multiple USRP devices in a homogeneous setup. See the documentation for uhd::usrp::multi_usrp. +\subsection coding_api_hilevel_rfnoc High-Level: The RFNoC API + +\li \subpage page_properties + \subsection coding_api_hilevelclock High-Level: The Multi-USRP-Clock The Multi-USRP-Clock class provides a high-level interface to a single clock diff --git a/host/docs/group_defs.dox b/host/docs/group_defs.dox index 312def6d9..769e6ec71 100644 --- a/host/docs/group_defs.dox +++ b/host/docs/group_defs.dox @@ -1,5 +1,5 @@ /*! - * \defgroup rfnoc RFNoC + * \defgroup rfnoc_docs RFNoC * \brief Additional documentation regarding RFNoC * * @{ diff --git a/host/docs/properties.dox b/host/docs/properties.dox new file mode 100644 index 000000000..8de4a50ab --- /dev/null +++ b/host/docs/properties.dox @@ -0,0 +1,453 @@ +/*! \page page_properties RFNoC Block Properties + +\tableofcontents + +\section props_intro Introduction + +One of the mechanisms that RFNoC blocks can use to control their configuration +are *properties*. There are two types of properties within a block: User +properties, and edge properties. + +To illustrate these types of properties, we start with an example: Take DDC +block (uhd::rfnoc::ddc_block_control), which can take in a signal of a certain +sampling rate, shift it in frequency, and resample to a lower sampling rate. + +First, we take a look at the *user properties*. User properties are properties +which are attributes of a block, they simply define certain characteristics of +the block. Here, we focus on two user properties: The frequency and the decimation. +The former is a floating point value which stores, in Hz, the amount by which +the incoming signal is shifted in frequency. The decimation is the integer +value by which the incoming sampling rate is divided. Put differently, the user +properties control the behaviour of the DDC block. + +On top of that, the DDC block also has an edge property for the sampling rate +on both the outgoing edge and the incoming edge. This property describes +something about the data on this graph edge. + +``` + ┌──────────────┐ + │ DDC Block │ + samp_rate │ │ samp_rate +───────────> - freq ├─────────> + │ - decim │ + │ │ + └──────────────┘ +``` + +The key thing, which separates properties from other attributes associated with +the block, is that we can describe relationships between the properties. In this +example, the following relationships exist: + +- The `samp_rate` property on the outgoing edge equals the `samp_rate` property + on the incoming edge divided by the `decim` value. +- The normalized frequency (required to program the digital hardware) is + calculated by dividing the `freq` user property by the `samp_rate` edge + property on the incoming edge. + +User properties also act as an API for RFNoC blocks. It is possible to access +user properties using the uhd::rfnoc::node_t::get_property() and +uhd::rfnoc::node_t::set_property() and uhd::rfnoc::node_t::set_properties() APIs. +A list of user properties can be read by calling uhd::rfnoc::node_t::get_property_ids(). + +Often, block controllers will provide explicit C++ APIs on top of the user +properties. For example, the DDC block control has an API call +uhd::rfnoc::ddc_block_control::set_freq(), which provides additional features on top of the +user property (e.g., it can also set a command time). Direct access of user +properties is thus often not necessary, and can be considered a lower-level API +than directly calling block APIs. + +\section props_propprop Property Propagation + +By itself, these relationships between properties are already useful to describe +something about the block, but the real power comes when connecting blocks. This +allows blocks to communicate their settings, even without knowing which block +they're connected to. Consider the following example, using a radio block +(uhd::rfnoc::radio_control), the DDC block, and a hypothetical Modem Block, which +expects incoming samples at a fixed data rate of 20 Msps. Assume we are running +this flow graph in a USRP X310, with a master clock rate of 200 Msps: + +``` + ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ + │ Radio Block │ │ DDC Block │ │ Modem Block │ + │ │samp_rate │ │ samp_rate│ │ + │ ├──────────> - freq ├──────────> │ + │ │ │ - decim │ │ │ + │ │ │ │ │ │ + └─────────────┘ └──────────────┘ └──────────────┘ +``` + +Upon initialization, the radio block will configure its hardware clocks, and +set a sampling rate of 200 Msps on its outgoing edge property (`samp_rate`). +This property is also the incoming edge property of the DDC block, which means +it is now initialized its sampling rate to 200 Msps. Setting the `freq` property +will now work accurately, because it can calculate normalized frequencies. + +The Modem Block will set its incoming edge property `samp_rate` to 20 Msps, which +is also the outgoing edge property of the DDC block. The DDC block must now find +a way to reconcile these edge properties, which it can achieve by setting the +`decim` user property to 20. Now, the DDC has automatically set a decimation +without anyone calling into its rate-changing APIs, and without the Modem Block +or the Radio Block knowing anything about how the DDC block works. + +Similarly, if the Modem Block would have requested a sampling rate of 30.72 Msps, +the DDC would not have been able to resolve the incoming and outgoing sampling +rates with an integer decimation factor. It would have requested the radio to +produce an integer multiple of 30.72 Msps, which the radio can't provide. Using +properties, UHD would be able to throw an exception, telling the user that the +requested combination is not achievable. + +\section props_multichan Properties on multi-channel blocks and resource types + +The DDC block is configurable to have a variable number of channels, and the +same is true for the properties. To accommodate for this, both edge and user +properties have an index value. A multi-channel DDC block is more accurately +depicted as such with regard to its properties: + +``` + ┌──────────────┐ + │ DDC Block │ +samp_rate[i] │ │ samp_rate[i] +──────────── > - freq[i] ├──────────────> + │ - decim[i] │ + │ │ + └──────────────┘ +``` + +\b Note: The index of edge properties is tightly coupled to the actual edge +being used (i.e., `samp_rate` on input edge 0 also has a property index of 0). +However, user properties do not enforce this relationship. In the DDC block, +it would be possible for the user property `decim[0]` to control channel 1 (which +it doesn't). This is useful for properties that don't map directly to channels +(e.g., a simpler version of the DDC block might only have a single `decim` user +property for all channels). + +In summary, to accurately identify a property, not only its name is required, but +also its type (input edge, user property, or output edge) and its index. For the +sake of convenience, the type and index are sometimes combined into a +uhd::rfnoc::res_source_info object. + +\section props_define Defining Properties + +To define properties in an RFNoC block, the block controller needs to declare +class attributes of type uhd::rfnoc::property_t. This is a template type which can +take in different types of C++ types (e.g., the `decim` user property is an +integer type, and the `freq` user property is of type `double`). Upon creation +of the class attribute, properties are configured with their property name (e.g. +`freq`, `decim`), optionally a default value, their type (user or edge property), +and their index. The uhd::rfnoc::ddc_block_control defines its frequency property +attribute variable as such: + +~~~{.cpp} +class ddc_block_control_impl : public ddc_block_control +{ + // ...all the other declarations ... +private: + // Declare the property_t attributes for the frequency + std::vector<property_t<double>> _freq; +}; +~~~ + +Next, the attribute needs to be initialized, like any other class attribute. +If we were to hard-code a 2-channel DDC with a default frequency shift of 0 Hz, +this would be a valid initialization: +~~~{.cpp} +// ... other initializations ... +_freq.reserve(2); +_freq.push_back(property_t<double>("freq", 0.0, {res_source_info::USER, 0})); +_freq.push_back(property_t<double>("freq", 0.0, {res_source_info::USER, 1})); +~~~ + +\b Note: It is important to choose a container for properties which does not +modify their memory locations! If a `std::vector<>` is chosen as a container, it +is required to `reserve()` memory before filling the container. + +Up until now, the property is just a class attribute. For UHD to make it +available for property propagation, it needs to be registered by calling +uhd::rfnoc::node_t::register_property(): + +~~~{.cpp} +// ... this is still happening within the context of the block controller: +register_property(&_freq[0]); +register_property(&_freq[1]); +~~~ + +The final step of creating properties is to define the relationship between +them, which is covered in the following section. + +\section props_resolvers Property Resolvers and Property Resolution + +A property resolver is a function object that is associated with an input list +of properties and an output list of properties. The function object will be +executed when any property on the input list changes. It may modify any property +in the output list. For more details, see uhd::rfnoc::node_t::add_property_resolver(). + +As an example, here is a shortened version of one of the resolvers in +uhd::rfnoc::ddc_block_control: + +~~~{.cpp} +add_property_resolver( + {&decim}, // Input list: We trigger this when the user changes `decim` + {&decim, &samp_rate_out, &samp_rate_in}, // Output list: These will be changed + // The resolver function is passed in as a lambda: + [this, chan, ...]() { // The capture list was cropped for better readability + RFNOC_LOG_TRACE("Calling resolver for `decim'@" << chan); + decim = coerce_decim(double(decim.get())); // Only some decimations are valid + if (decim.is_dirty()) { // If nothing changes, don't poke any registers + set_decim(decim.get(), chan); // This will control hardware registers + } + if (samp_rate_in.is_valid()) { + samp_rate_out = samp_rate_in.get() / decim.get(); + } else if (samp_rate_out.is_valid()) { + samp_rate_in = samp_rate_out.get() * decim.get(); + } + }); +~~~ + +A few noteworthy comments: +- Even though this function object is called when modifying `decim`, it is still + possible to coerce `decim` to a different, valid value. +- Properties may be in an invalid state (e.g., if they were never initialized) + which needs to be checked before accessing such properties. See also + uhd::rfnoc::property_base_t::is_valid(). +- Properties can be modified by assigning values to them which match their + template type. +- We can tell if a property was modified by checking its clean/dirty state + (see also uhd::rfnoc::property_base_t::is_dirty()). +- When the resolver function completes, we have ensured that `samp_rate_in`, + `samp_rate_out`, and `decim` are in a consistent state. + + +\subsection props_resolvers_cldrty Clean/Dirty Attributes of Properties + +To keep track of which properties have been modified, every property has a "dirty" +flag that is set when a property's value is changed. This dirty flagged is also +used to determine which property resolver functions need to be called. A property +resolver may modify (and thus dirty) other properties, but resolvers may never +set properties to conflicting values. For example, the DDC block controller has +a different resolver that is called when its input sampling rate is modified, +but it uses the same equations to relate decimation, input rate, and output rate +so the resulting values are consistent. + +Imagine a block that has both an `fft_size` and `bin_width` user property. They +are related by `bin_width = sampling_rate_in / fft_size`, where `sampling_rate_in` +is an input edge property. Adding two separate resolvers like this is valid: + +~~~{.cpp} +add_property_resolver( + {&fft_size}, + {&bin_width}, + [this, &fft_size, &bin_width, &samp_rate_in]() { + // This will trigger the other resolver + bin_width = samp_rate_in.get() / fft_size.get(); + }); +add_property_resolver( + {&bin_width}, + {&fft_size}, + [this, &fft_size, &bin_width, &samp_rate_in]() { + // This will trigger the other resolver + fft_size = samp_rate_in.get() / bin_width.get(); + }); +~~~ + +Both resolvers will modify the input for the other resolver. However, because +they use the same equations, this does not incur a circular resolution. + +\b Note: When using floating-point type properties, it is possible to incur +a circular resolution when floating point rounding errors occur, so they need +to be accounted for. See, for example, the implementation of +uhd::rfnoc::ddc_block_control. + +When all resolvers with dirty inputs have been run, and no conflicts have +occurred, properties are flagged clean. When this happens, a clean callback is +executed which can trigger further actions (it may not, however, modify +properties. See uhd::rfnoc::node_t::register_property()). + +As an example, assume a block configures an FFT size. It first checks the FFT +size is as power of two, then writes the log2 of the FFT size to a hardware +register. + +The following two implementations have the same effect. First, we use the resolver +to write to the hardware: +~~~{.cpp} +register_property(&fft_size); +add_property_resolver( + {&fft_size}, + {&fft_size}, + [this, &fft_size]() { + fft_size = coerce_to_power_of_2(fft_size.get()); + if (fft_size.is_dirty()) { + this->regs().poke32(FFT_SIZE_REG, log2(fft_size.get())); + } + }); +~~~ + +Alternatively, we poke the register as part of cleaning the property: +~~~{.cpp} +register_property( + &fft_size, + [this, &fft_size](){ this->regs().poke32(FFT_SIZE_REG, log2(FFT_SIZE.get()); } +); +add_property_resolver( + {&fft_size}, + {&fft_size}, + [this, &fft_size]() { + fft_size = coerce_to_power_of_2(fft_size.get()); + }); +~~~ + +The differences are subtle: +- The second approach separates the hardware interaction from the + coercion/resolution logic. Here, the resolver is *only* responsible for + maintaining the property in a valid state. +- In the first approach, the register is poked as soon as the new value is + known. In the second approach, the poke happens only after the resolution is + complete. + +In this particular instance, the second approach is the more readable and is +recommended. However, not always do properties map to poking registers (or other +actions) in a clean, one-to-one manner, which is when the first approach is +better suited. + + +\section props_graph_resolution Graph Property Resolution + +In an RFNoC graph, after calling `uhd::rfnoc::rfnoc_graph::commit()`, edge +properties are used to resolve properties of a whole graph. When any property is +changed, the corresponding block's properties are resolved as explained before. +However, by modifying the edge properties of a block, other blocks' properties +may be dirtied as well. In this case, the resolver algorithm will keep resolving +blocks until either all properties are clean, or a conflict is detected. + +\subsection props_graph_resolution_back_edges Back edges + +The graph object uses a topological sort to identify the order in which blocks +are resolved. However, in RFNoC, it is OK to have loops, or *back edges*. + +Consider the following graph: + +``` + + ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ + │ │ │ │ │ │ + │ Radio Block ├──────────> DDC Block ├──────────> Custom DSP │ + │ │ │ │ │ │ + └─────^───────┘ └──────────────┘ └──────┬───────┘ + │ │ + │ back edge │ + └───────────────────────────────────────────────────┘ +``` + +In this application, we receive a signal, use the DDC block to correct a +frequency offset digitally, and then apply some custom DSP before transmitting +the signal again, using the same radio block. In order to create a valid graph +that UHD can handle, one of the edges needs to be declared a back edge. + +Edge properties are not propagated across back edges, however, they must still +match up after property resolution is done. For example, if the DDC were to be +configured to decimate the sampling rate, and the custom DSP block would not +correct for that, the output edge property `samp_rate` of the custom DSP block +would be mismatched compared to the input edge property `samp_rate` of the radio. + + +\section props_unknown Handling unknown properties + +Since every block can define its own edge- and user properties, it is likely that +a block may not have defined an edge property that an up- or downstream block +has. + +Consider the example of the previous section. The "Custom DSP" block may not +have defined edge properties for the sampling rate. The Radio and DDC blocks +however, do have a `samp_rate` edge property defined. + +The way blocks handle such properties is by setting a *forwarding policy* (see +uhd::rfnoc::node_t::set_prop_forwarding_policy() and +uhd::rfnoc::node_t::set_prop_forwarding_map()). + +There are several forwarding policies (see uhd::rfnoc::node_t::forwarding_policy_t). +The most common forwarding policy is uhd::rfnoc::node_t::forwarding_policy_t::ONE_TO_ONE. +Here, properties that are applied to an input edge are forwarded to the +corresponding output edge. For more special use cases, +uhd::rfnoc::node_t::forwarding_policy_t::USE_MAP can be used to define +forwarding rules for non-static cases (e.g., see uhd::rfnoc::switchboard_block_control). + +\section props_common_props Common Properties + +There are some properties that are commonly used, and blocks should use these +property names if appropriate: + +Edge Properties: +- `type`: It is recommended to always use this property on an edge. It should + describe the data type on this edge so the graph can see if types match. The + type descriptions are the same as with CPU and OTW types (e.g., sc16, fc32, + sc8, u8, ...). +- `samp_rate`: Whenever a "sampling rate" is applicable, edges should set this + property. DDC, DUC, and Radio blocks all use this for configuring rates and + verifying sample rates match. +- `scaling`: This property describes how a block has distorted a signal with + respect to amplitude. A scaling value of 0.99 means that a signal that used + to be fullscale (max. amplitude of 1.0) will only have a maximum amplitude of + 0.99. For example, the DDC and DUC blocks use this property to signal how + much they have affected the amplitude. The TX and RX streamer objects can then + apply the inverse of this scaling factor to correct for the scaling before + returning the signal to the user. + +User properties: +- `spp`: Blocks that produce data in chunks of samples can use this to describe + their "sample per packet" values. Example: uhd::rfnoc::radio_control +- `interp`, `decim`: Interpolation and decimation factors when doing rational + resampling. Examples: uhd::rfnoc::ddc_block_control, uhd::rfnoc::duc_block_control + +See also the following sections for properties that receive special treatment by +the framework. + +\section props_special_props Special Properties + +There is a small number of edge properties that treated differently by the RFNoC +framework. + +\section props_special_props_tickrate Tick Rate + +The "tick rate" is the rate that is used for converting floating point timestamps +into integer ticks. The tick rate has the additional constraint that within a +graph, the tick rate must be the same for all blocks. Therefore, the tick rate +is defined by the framework for all blocks that derive from uhd::rfnoc::noc_block_base, +and block authors cannot register properties with that name. + +The tick rate property is propagated to all edges to enforce all blocks having +the same tick rate. Some blocks (like the radio blocks) set the tick rate via +their direct access to the hardware, but most blocks use the property propagation +mechanism to distribute the tick rate. + +To read back the tick rate, use uhd::rfnoc::noc_block_base::get_tick_rate(). +Within a block implementation, the uhd::rfnoc::noc_block_base::set_tick_rate() +API call can be used to update the tick rate (most blocks do not have to do +this). + +\section props_special_props_mtu MTU + +Because the MTU is part of the RFNoC framework (it is constrained by the buffer +size chosen between blocks, and is thus read back from a register in the FPGA), +its property is also created by the RFNoC framework for all blocks, and block +authors cannot use the name `mtu` for their properties. + +However, MTUs can differ within a graph, and blocks might have additional +constraints on MTU. For this reason, accessing the MTU properties directly is +not possible from within a block controller, but rfnoc::noc_block_base provides +several APIs to interact with MTUs: + +- noc_block_base::get_mtu(): Reading back the MTU determined by property + propagation on a particular edge. +- noc_block_base::set_mtu(): Reduce the MTU on a particular edge. *Increasing* + the MTU is not possible. +- noc_block_base::set_mtu_forwarding_policy(): Set the MTU forwarding policy. +- noc_block_base::get_mtu_prop_ref(): Request access to a MTU property reference, + which can be used to trigger property propagation off of an MTU change. + +Many blocks (e.g., uhd::rfnoc::ddc_block_control, uhd::rfnoc::duc_block_control) +do not resize blocks as they pass through them. That means the input MTU of +these blocks equals the output MTU. Thus, they set the MTU forwarding policy to +uhd::rfnoc::node_t::forwarding_policy_t::ONE_TO_ONE. + + +*/ +// vim:ft=doxygen: diff --git a/host/docs/rfnoc_docs.dox b/host/docs/rfnoc_docs.dox new file mode 100644 index 000000000..0f209bb0c --- /dev/null +++ b/host/docs/rfnoc_docs.dox @@ -0,0 +1,8 @@ +/*! \page page_rfnoc_docs RFNoC Block Properties + +\ingroup rfnoc_docs + +\li \ref page_properties + +*/ +// vim:ft=doxygen: diff --git a/host/include/uhd/rfnoc/noc_block_base.hpp b/host/include/uhd/rfnoc/noc_block_base.hpp index 6b1ac5bcd..21135227b 100644 --- a/host/include/uhd/rfnoc/noc_block_base.hpp +++ b/host/include/uhd/rfnoc/noc_block_base.hpp @@ -272,6 +272,9 @@ protected: * This can be used to make the MTU an input to a property resolver. For * example, blocks that have an spp property, such as the radio, can now * trigger a property resolver based on the MTU. + * + * The reference is guaranteed to remain valid for the lifetime of this + * block. */ property_base_t* get_mtu_prop_ref(const res_source_info& edge); diff --git a/host/include/uhd/rfnoc/node.hpp b/host/include/uhd/rfnoc/node.hpp index 0a49d1609..5478eed07 100644 --- a/host/include/uhd/rfnoc/node.hpp +++ b/host/include/uhd/rfnoc/node.hpp @@ -229,6 +229,8 @@ protected: * either be reset after the constructor is finished, or the next time * properties are resolved. * + * For more information, see \ref props_define. + * * \param prop A reference to the property * \param clean_callback A callback that gets executed whenever this property * is dirty and gets marked clean @@ -265,6 +267,8 @@ protected: * (which is considered advanced usage), then the block needs to serialize * access to this function itself. * + * For more information, see \ref props_resolvers. + * * \param inputs The properties that will cause this resolver to run * \param outputs The properties that this resolver will write to * \param resolver_fn The resolver function @@ -290,6 +294,8 @@ protected: * Typically, this function should only ever be called from within the * constructor. * + * See also \ref props_unknown. + * * \param policy The policy that is applied (see also forwarding_policy_t). * \param prop_id The property ID that this forwarding policy is applied to. * If \p prop_id is not given, it will apply to all properties, @@ -315,6 +321,8 @@ protected: * propagation time: * - Any value in the destination vector represents a non-existent port * + * See also \ref props_unknown. + * * \param map The map describing how properties should be propagated */ void set_prop_forwarding_map(const forwarding_map_t& map); diff --git a/host/include/uhd/rfnoc/replay_block_control.hpp b/host/include/uhd/rfnoc/replay_block_control.hpp index c7624d044..1afd8b4de 100644 --- a/host/include/uhd/rfnoc/replay_block_control.hpp +++ b/host/include/uhd/rfnoc/replay_block_control.hpp @@ -40,6 +40,25 @@ namespace uhd { namespace rfnoc { * achieve the best throughput to the downstream block without exceeding the supported * packet size. The maximum packet size can be explicitly set in terms of bytes or * number of items to allow users to tune the performance to suit their application. + * + * \section rfnoc_block_replay_properties Block Properties + * + * User Properties: + * + * | Key | Description + * |---------------:|--------------------------------------------------------- + * | record_offset | The base address for recordings (data will be written here). Will be set by record(). + * | record_size | Size of the record buffer. Will be set by record(). + * | play_offset | Base address for playback (streaming will start from here). Set by play() or config_play(). + * | play_size | Size of the playback buffer. Set by play() or config_play(). + * | packet_size | Size of outgoing packets (in bytes). Gets set by set_max_items_per_packet(). + * + * Edge properties: + * + * | Key | Description + * |------:|------------------------------------------------------------------- + * | type | Data type. On the input, it set by the upstream block (see get_record_type()). For output, it should be provided by set_play_type(). + * */ class UHD_API replay_block_control : public noc_block_base { diff --git a/host/include/uhd/rfnoc_graph.hpp b/host/include/uhd/rfnoc_graph.hpp index b25d7c207..dc35892b1 100644 --- a/host/include/uhd/rfnoc_graph.hpp +++ b/host/include/uhd/rfnoc_graph.hpp @@ -179,7 +179,8 @@ public: * \param src_port The port of the source block to connect. * \param dst_blk The block ID of the destination block to connect to. * \param dst_port The port of the destination block to connect to. - * \param skip_property_propagation Skip property propagation for this edge + * \param skip_property_propagation Skip property propagation for this edge. + * See also \ref props_graph_resolution_back_edges. * * \throws uhd::routing_error if the source or destination ports are * statically connected to a *different* block |