DUC Block Control Class
Overview
The DUC is a multi-channel digital upconverter with a built-in DDS frequency shifter. It is commonly placed directly before a radio block to increase the sampling rate and shift a signal to the desired transmit frequency.
The block processes signed complex 16-bit samples (sc16). Each channel has an independent frequency shift and interpolation setting. The number of channels and the maximum supported interpolation are configured in the FPGA at synthesis time. The block controller reads these maximum capabilities, stored in capability registers, during initialization.
Features
- Per-channel frequency shifting with optional timed commands.
- Integer interpolation using a cascade of half-band filters followed by a CIC interpolator.
- IQ scaling to compensate for the gain of the interpolation filters.
- Independent rate and frequency configuration for every channel.
Theory of Operation

The total interpolation is the product of the CIC interpolation and the half-band interpolation stages. With NUM_HB half-band stages and a CIC limit of CIC_MAX_INTERP, the maximum supported interpolation is \(\mathrm{CIC\_MAX\_INTERP} \times 2^{\mathrm{NUM\_HB}}\).
FPGA Compile-Time Configuration
The DUC FPGA block is configured through the following parameters:
NUM_PORTS: Number of independent DUC channels.
NUM_HB: Number of half-band filter stages in each channel.
CIC_MAX_INTERP: Maximum interpolation through CIC filter stage in each channel.
NIPC: Number of samples processed per clock cycle. NIPC == 1 selects the legacy single-sample implementation. Values greater than one select the multisample implementation, which processes samples in parallel for wideband images.
The in-tree DUC YAML descriptor derives NIPC from the configured RF bandwidth. Each parallel processing chain is sized for approximately 200 MHz of RF bandwidth, with the resulting value rounded up to a power of two. Customized FPGA images may choose a different value based on their clock rates and resource budget.
Runtime Configuration
This block exposes two user-configurable configuration parameters per channel:
freq: Frequency shift in Hz. The set_freq() convenience method should be preferred over setting the property directly because it also handles a command time.
interp: Integer interpolation value.
The interpolation value can be changed at runtime, but a change is applied only between bursts. Once an interpolation value has been applied, it remains in effect for the entire burst.
The legacy single-sample DUC and the multisample (wideband) DUC react differently to timed and untimed frequency-shift commands:
- Legacy single-sample implementation:
- Untimed commands: Configure the frequency shift for the next IQ sample processed by the DUC and all subsequent samples until the next frequency change. If the DUC has not processed data since the command was issued, a subsequent untimed command overwrites the earlier one.
- Timed commands: Follow the standard timed-command mechanism. The command is held until the IQ sample corresponding to its timestamp is processed, and the new frequency shift applies to that sample and all subsequent samples until the next frequency change.
- Multisample (wideband) implementation: Timed and untimed commands are stored in the same 32-entry FPGA command queue. Commands are applied only when the DUC is actively processing data, and only the command at the front of the queue can be applied.
- Untimed commands: An untimed command waits for all commands already ahead of it in the queue, including timed commands. It is then applied on the next data transfer. Consequently, successive untimed commands are not collapsed or skipped; each remains active for at least one data transfer containing
NIPC IQ samples.
- Timed commands: A timed command waits in the queue until it reaches the front and its requested timestamp has been reached. It is applied starting with the data transfer containing the IQ sample corresponding to that timestamp and remains active until the next frequency change.
In the multisample implementation, timed frequency shifts have word-level rather than sample-level granularity. One word is one data transfer containing NIPC samples (see NIPC). Therefore, if the requested timestamp falls within a word, the new frequency shift will be applied to samples earlier in that same word. For example, if the timestamp corresponds to the third sample in a word of eight samples, the new frequency shift will be applied to all eight samples in that word, including the first two samples preceding the requested timestamp, and to all subsequent samples until the next frequency change. The exact sample-level transition is not guaranteed.
Register Maps and Compatibility
The DUC has two register-map generations:
- Compatibility major 0 is the legacy single-sample register map, used by implementations for bandwidths up to 200 MHz.
- Compatibility major 1 is the multisample register map, used by the wideband implementation.
The block controller reads the FPGA compatibility number from REG_COMPAT_NUM and selects the corresponding register map. An unsupported major version is rejected. REG_ADDRS_V0 and REG_ADDRS_V1 contain the version-specific addresses used by the controller.
|
| virtual double | set_freq (const double freq, const size_t chan, const std::optional< uhd::time_spec_t > time={})=0 |
| |
| double | set_freq (const double freq, const size_t chan, const uhd::time_spec_t time) |
| |
| virtual double | set_freq (const double freq, const size_t chan, const boost::optional< uhd::time_spec_t > time) |
| |
| virtual double | get_freq (const size_t chan) const =0 |
| |
| virtual uhd::freq_range_t | get_frequency_range (const size_t chan) const =0 |
| |
| virtual double | get_input_rate (const size_t chan) const =0 |
| |
| virtual double | get_output_rate (const size_t chan) const =0 |
| |
| virtual void | set_output_rate (const double rate, const size_t chan)=0 |
| |
| virtual uhd::meta_range_t | get_input_rates (const size_t chan) const =0 |
| |
| virtual double | set_input_rate (const double rate, const size_t chan)=0 |
| |
| | ~noc_block_base () override |
| |
| std::string | get_unique_id () const override |
| | Unique ID for an RFNoC block is its block ID. More...
|
| |
| size_t | get_num_input_ports () const override |
| |
| size_t | get_num_output_ports () const override |
| |
| noc_id_t | get_noc_id () const |
| |
| const block_id_t & | get_block_id () const |
| |
| double | get_tick_rate () const |
| |
| size_t | get_mtu (const res_source_info &edge) |
| |
| size_t | get_chdr_hdr_len (const bool account_for_ts=true) const |
| |
| size_t | get_max_payload_size (const res_source_info &edge, const bool account_for_ts=true) |
| |
| uhd::device_addr_t | get_block_args () const |
| |
| uhd::property_tree::sptr & | get_tree () const |
| | Return a reference to this block's subtree. More...
|
| |
| uhd::property_tree::sptr & | get_tree () |
| | Return a reference to this block's subtree (non-const version) More...
|
| |
| std::shared_ptr< mb_controller > | get_mb_controller () |
| |
| | node_t () |
| |
| virtual | ~node_t () |
| |
| std::vector< std::string > | get_property_ids () const |
| |
| template<typename prop_data_t > |
| void | set_property (const std::string &id, const prop_data_t &val, const size_t instance=0) |
| |
| void | set_properties (const uhd::device_addr_t &props, const size_t instance=0) |
| |
| template<typename prop_data_t > |
| const prop_data_t & | get_property (const std::string &id, const size_t instance=0) |
| |
| virtual void | set_command_time (uhd::time_spec_t time, const size_t instance) |
| |
| virtual uhd::time_spec_t | get_command_time (const size_t instance) const |
| |
| virtual void | clear_command_time (const size_t instance) |
| |
| | register_iface_holder (register_iface::sptr reg) |
| |
| virtual | ~register_iface_holder ()=default |
| |
| register_iface & | regs () |
| |
|
| using | sptr = std::shared_ptr< noc_block_base > |
| |
| using | make_args_ptr = std::unique_ptr< make_args_int_t, make_args_deleter > |
| | Opaque pointer to the constructor arguments with custom deleter. More...
|
| |
| enum class | action_mode_t { SYNC
, ASYNC
} |
| | Action execution modes. More...
|
| |
| enum class | forwarding_policy_t {
ONE_TO_ONE
, ONE_TO_FAN
, ONE_TO_ALL_IN
, ONE_TO_ALL_OUT
,
ONE_TO_ALL
, DROP
, USE_MAP
} |
| | Types of property/action forwarding for those not defined by the block itself. More...
|
| |
| using | resolver_fn_t = std::function< void(void)> |
| |
| using | resolve_callback_t = std::function< void(void)> |
| |
| using | graph_mutex_callback_t = std::function< std::recursive_mutex &(void)> |
| |
| using | action_handler_t = std::function< void(const res_source_info &, action_info::sptr)> |
| |
| using | post_action_handler_t = std::function< void(const res_source_info &, action_info::sptr, action_mode_t)> |
| |
| using | forwarding_map_t = std::unordered_map< res_source_info, std::vector< res_source_info > > |
| |
| using | prop_ptrs_t = std::vector< property_base_t * > |
| |
| | noc_block_base (make_args_ptr make_args) |
| |
| void | set_num_input_ports (const size_t num_ports) |
| |
| void | set_num_output_ports (const size_t num_ports) |
| |
| void | set_tick_rate (const double tick_rate) |
| |
| void | set_mtu_forwarding_policy (const forwarding_policy_t policy) |
| |
| void | set_mtu (const res_source_info &edge, const size_t new_mtu) |
| |
| property_base_t * | get_mtu_prop_ref (const res_source_info &edge) |
| |
| virtual void | deinit () |
| |
| void | register_property (property_base_t *prop, resolve_callback_t &&clean_callback=nullptr) |
| |
| void | add_property_resolver (prop_ptrs_t &&inputs, prop_ptrs_t &&outputs, resolver_fn_t &&resolver_fn) |
| |
| void | set_prop_forwarding_policy (forwarding_policy_t policy, const std::string &prop_id="") |
| |
| void | set_prop_forwarding_map (const forwarding_map_t &map) |
| |
| template<typename prop_data_t > |
| void | set_property (const std::string &id, const prop_data_t &val, const res_source_info &src_info) |
| |
| template<typename prop_data_t > |
| const prop_data_t & | get_property (const std::string &id, const res_source_info &src_info) |
| |
| void | register_action_handler (const std::string &id, action_handler_t &&handler) |
| |
| void | set_action_forwarding_policy (forwarding_policy_t policy, const std::string &action_key="") |
| |
| void | set_action_forwarding_map (const forwarding_map_t &map) |
| |
| void | post_action (const res_source_info &edge_info, action_info::sptr action, action_mode_t mode=action_mode_t::SYNC) |
| |
| virtual bool | check_topology (const std::vector< size_t > &connected_inputs, const std::vector< size_t > &connected_outputs) |
| |
| void | update_reg_iface (register_iface::sptr new_iface=nullptr) |
| |
| static dirtifier_t | ALWAYS_DIRTY |
| | A dirtifyer object, useful for properties that always need updating. More...
|
| |