mirror of
https://github.com/saymrwulf/uhd.git
synced 2026-05-16 21:10:10 +00:00
Previously, the property propagation algorithm would first forward and resolve properties only along forward edges. Then, we would check that properties also align across back-edges. The assumption is that graphs are always structured in a way such that back-edges would align when the resolution is done. However, for the following graph, this would fail: Radio ---> Replay ^ | +---------+ The reason is that the radio block and the replay block both have an "atomic_item_size" property, which needs to be resolved both ways. If the default atomic_item_size is 4 for the radio, and 8 for the replay block, then the input atomic_item_size on the radio will never be aligned with the output atomic_item_size of the replay block, and there is no other mechanism to align those. The solution is to run the edge property propagation and resolution twice, first for the forward edges, then for the back-edges. For graphs that would previously work, this makes no difference: The additional step of propagation properties across the back-edges will not dirty any properties. However, for graphs like the one above, it will provide an additional resolution path for properties that are otherwise not connected.
483 lines
24 KiB
Text
483 lines
24 KiB
Text
/*! \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()) {
|
|
// FFT_SIZE_REG stores address of FFT size register
|
|
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.
|
|
- The manual step of checking the clean/dirty flag is used to avoid unnecessary
|
|
writes to hardware in the first approach. It is not required in the second
|
|
approach, because the write to hardware is coupled with the act of cleaning
|
|
the variable.
|
|
|
|
In this particular instance, the second approach is the more readable and is
|
|
recommended. However, not always does changing properties map to poking
|
|
registers (or other actions) in a clean, one-to-one manner, which is when the
|
|
first approach may be 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.
|
|
|
|
When declaring an edge as a back-edge, this signals to UHD that this edge should
|
|
not be used for viewing the graph as a directed, acyclic graph (DAG), which is
|
|
important during property propagation. In the graph above, UHD would sort blocks
|
|
topologically (Radio Block, DDC Block, Custom DSP Block) by ignoring back-edges.
|
|
Then, properties are forwarded in topological order first across forward edges,
|
|
then across back edges.
|
|
|
|
This can lead to resolution errors. 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.
|
|
This could either re-trigger a new cascade of property settings, which means
|
|
the algorithm can't converge, and UHD would detect that and throw an exception.
|
|
Or the radio block might not accept a sample rate update on its input port,
|
|
which would also trigger an exception.
|
|
|
|
It is highly recommended to not declare edges as back-edges unless
|
|
necessary. In the graph above, any of the three edges could have been declared
|
|
a back-edge to result in a topologically valid graph that can still resolve its
|
|
properties, but the one chosen is the most intuitive selection.
|
|
|
|
\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.
|
|
- `atomic_item_size`: Blocks (e.g. uhd::rfnoc::radio_control) might need a
|
|
non-dividable amount of data per clock cycle, e.g. the radio block needs
|
|
`sample_per_cycle` times the size of the type bytes of data in each cycle.
|
|
This property allows a block to specify this amount of data per cycle which
|
|
allows other blocks or the streamer objects to adapt the samples per packet
|
|
accordingly.
|
|
Note: Because all blocks in a graph must agree on one value for this property
|
|
the blocks have to calculate the `atomic_item_size` as the least common
|
|
multiple of their own `atomic_item_size` and the edge property to accommodate
|
|
for other blocks up- or downstream.
|
|
|
|
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 (`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 named `tick_rate`.
|
|
|
|
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 (`mtu`)
|
|
|
|
Because the maximum transmission unit (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:
|