1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
|
/*! \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.
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.
In that case, UHD will throw an exception when trying to resolve properties,
even if it is declared a back-edge.
Note that property propagation along edges is required for certain operations.
Therefore, 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. If any *two*
edges were declared back-edges, at least one of the DDC or Custom DSP blocks
might cease to function, e.g., because it doesn't have access to the tick rate
property.
\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:
|