DDC Block Control Class
Overview
The DDC is a multi-channel digital downconverter with a built-in DDS frequency shifter. It is commonly placed directly after a radio block to select a channel from a wider-band input and reduce the sampling rate before the data is sent to another RFNoC block or to the host.
The block processes signed complex 16-bit samples (sc16). Each channel has an independent frequency shift and decimation setting. The number of channels and the maximum supported decimation 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 decimation using a CIC filter followed by a configurable cascade of half-band filters.
- IQ scaling to compensate for the gain of the decimation filters.
- Independent rate and frequency configuration for every channel.
Theory of Operation

The total decimation is the product of the CIC decimation and the half-band decimation stages. With NUM_HB half-band stages and a CIC limit of CIC_MAX_DECIM, the maximum supported decimation is \(\mathrm{CIC\_MAX\_DECIM} \times 2^{\mathrm{NUM\_HB}}\).
FPGA Compile-Time Configuration
The DDC FPGA block is configured through the following parameters:
NUM_PORTS: Number of independent DDC channels.
NUM_HB: Number of half-band filter stages in each channel.
CIC_MAX_DECIM: Maximum decimation 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 DDC 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.
decim: Integer decimation value.
The decimation value can be changed at runtime, but a change is applied only between bursts. Once a decimation value has been applied, it remains in effect for the entire burst.
The legacy single-sample DDC and the multisample (wideband) DDC 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 DDC and all subsequent samples until the next frequency change. If the DDC 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 DDC 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 DDC 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 RB_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) |
| |
| 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 void | set_input_rate (const double rate, const size_t chan)=0 |
| |
| virtual double | get_output_rate (const size_t chan) const =0 |
| |
| virtual uhd::meta_range_t | get_output_rates (const size_t chan) const =0 |
| |
| virtual double | set_output_rate (const double rate, const size_t chan)=0 |
| |
| virtual void | issue_stream_cmd (const uhd::stream_cmd_t &stream_cmd, const size_t port)=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...
|
| |