diff options
Diffstat (limited to 'host/docs')
-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 |
4 files changed, 466 insertions, 1 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: |