uhd/host/lib/rfnoc/ddc_block_control.cpp
Niels Garibaldi e466f427fd host: dsp: add wideband ddc and duc rfnoc blocks
Updated the host related files to support wideband DxC block
variants.
The singlesample and multisample variants both have the same API,
but internally they are using different register address spaces to
interact with the FPGA implementation.
The host implementation checks which implementation a RFNoC DxC
block is using by checking the block version number and will then
automatically choose the correct register address scheme.
This is all transparent to the user, the Block API stays the same.

Co-authored-by: Niels Garibaldi <niels.steffen.garibaldi@emerson.com>
Co-authored-by: André Zoch <andre.zoch@emerson.com>
Co-authored-by: Meghana Halladamath <meghana.halladamath@emerson.com>
Co-authored-by: Wade Fife <wade.fife@ettus.com>
Co-authored-by: Max Köhler <max.koehler@emerson.com>
2026-08-05 08:43:42 +02:00

795 lines
36 KiB
C++

//
// Copyright 2019 Ettus Research, a National Instruments Brand
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
#include <uhd/exception.hpp>
#include <uhd/rfnoc/ddc_block_control.hpp>
#include <uhd/rfnoc/defaults.hpp>
#include <uhd/rfnoc/multichan_register_iface.hpp>
#include <uhd/rfnoc/property.hpp>
#include <uhd/rfnoc/registry.hpp>
#include <uhd/types/ranges.hpp>
#include <uhd/utils/compat_check.hpp>
#include <uhd/utils/log.hpp>
#include <uhd/utils/math.hpp>
#include <uhdlib/usrp/cores/dsp_core_utils.hpp>
#include <uhdlib/utils/math.hpp>
#include <cmath>
#include <set>
#include <string>
namespace {
constexpr double DEFAULT_SCALING = 1.0;
constexpr int DEFAULT_DECIM = 1;
constexpr double DEFAULT_FREQ = 0.0;
const uhd::rfnoc::io_type_t DEFAULT_TYPE = uhd::rfnoc::IO_TYPE_SC16;
// Multisample DDC (V1) register address building blocks
constexpr uint32_t MS_DDC_BASE = 0x000;
constexpr uint32_t MS_PORT_BASE = 0x100;
constexpr uint32_t MS_PORT_AXI_RATE = MS_PORT_BASE + 0x000;
constexpr uint32_t MS_PORT_SR = MS_PORT_BASE + 0x100;
constexpr uint32_t MS_PORT_DDS = MS_PORT_BASE + 0x200;
constexpr uint32_t MS_PORT_ADDR_W = 11; // 2^11 = 2048 bytes between channels
// MS shared registers
constexpr uint32_t MS_NUM_HB_ADDR = 0x04;
constexpr uint32_t MS_CIC_MAX_DECIM_ADDR = 0x08;
constexpr uint32_t MS_SPC_ADDR = 0x0C;
// MS Sample rate conversion registers (per-channel)
constexpr uint32_t MS_DECIM_ADDR = 0x00;
constexpr uint32_t MS_SCALE_IQ_ADDR = 0x08;
// MS DDS registers (per-channel)
constexpr uint32_t MS_FREQ_ADDR = 0x00;
// MS AXI rate registers (per-channel)
constexpr uint32_t MS_TIME_INCR_ADDR = 0x08;
} // namespace
using namespace uhd::rfnoc;
const uint32_t ddc_block_control::RB_COMPAT_NUM = 0;
const ddc_block_control::reg_addrs_t ddc_block_control::REG_ADDRS_V0 = {
.major_compat = 0,
.minor_compat = 1,
.rb_num_hb = 8,
.rb_cic_max_decim = 16,
.rb_spc = std::nullopt,
.sr_decim_addr = 134 * 8,
.sr_freq_addr = 132 * 8,
.sr_scale_iq_addr = 133 * 8,
.sr_n_addr = 128 * 8,
.sr_m_addr = 129 * 8,
.sr_time_incr_addr = 137 * 8,
.sr_config_addr = 130 * 8,
.sr_mux_addr = 135 * 8,
.sr_coeffs_addr = 136 * 8,
.reg_chan_offset = 2048, // legacy SS register bank spacing
};
const ddc_block_control::reg_addrs_t ddc_block_control::REG_ADDRS_V1 = {
.major_compat = 1,
.minor_compat = 0,
.rb_num_hb = MS_DDC_BASE + MS_NUM_HB_ADDR,
.rb_cic_max_decim = MS_DDC_BASE + MS_CIC_MAX_DECIM_ADDR,
.rb_spc = MS_DDC_BASE + MS_SPC_ADDR,
.sr_decim_addr = MS_PORT_SR + MS_DECIM_ADDR,
.sr_freq_addr = MS_PORT_DDS + MS_FREQ_ADDR,
.sr_scale_iq_addr = MS_PORT_SR + MS_SCALE_IQ_ADDR,
.sr_n_addr = std::nullopt,
.sr_m_addr = std::nullopt,
.sr_time_incr_addr = MS_PORT_AXI_RATE + MS_TIME_INCR_ADDR,
.sr_config_addr = std::nullopt,
.sr_mux_addr = std::nullopt,
.sr_coeffs_addr = std::nullopt,
.reg_chan_offset = 1 << MS_PORT_ADDR_W,
};
class ddc_block_control_impl : public ddc_block_control
{
public:
RFNOC_BLOCK_CONSTRUCTOR(ddc_block_control)
, _fpga_compat(regs().peek32(RB_COMPAT_NUM)),
_reg_addrs(_fpga_compat.get_major() == REG_ADDRS_V1.major_compat ? REG_ADDRS_V1
: REG_ADDRS_V0),
_ddc_reg_iface(*this, 0, _reg_addrs.reg_chan_offset),
_num_halfbands(regs().peek32(_reg_addrs.rb_num_hb)),
_cic_max_decim(regs().peek32(_reg_addrs.rb_cic_max_decim)),
_spc(_reg_addrs.rb_spc ? regs().peek32(*_reg_addrs.rb_spc) : 1),
_residual_scaling(get_num_input_ports(), DEFAULT_SCALING)
{
if (_fpga_compat.get_major() > REG_ADDRS_V1.major_compat) {
UHD_LOG_THROW(uhd::runtime_error,
get_unique_id(),
": Unsupported DDC compat major version: "
+ std::to_string(_fpga_compat.get_major()));
}
UHD_ASSERT_THROW(get_num_input_ports() == get_num_output_ports());
UHD_ASSERT_THROW(_cic_max_decim > 0 && _cic_max_decim <= 0xFF);
uhd::assert_fpga_compat(_reg_addrs.major_compat,
_reg_addrs.minor_compat,
_fpga_compat.get(),
get_unique_id(),
get_unique_id(),
false /* Let it slide if minors mismatch */
);
RFNOC_LOG_DEBUG("Loading DDC with "
<< _num_halfbands
<< " halfbands and "
"max CIC decimation "
<< _cic_max_decim
<< ", map=" << (_reg_addrs.rb_spc ? "multisample" : "legacy")
<< ", SPC=" << _spc);
// This line is not strictly necessary, as ONE_TO_ONE is the default.
// We set it make it explicit how this block works.
set_mtu_forwarding_policy(forwarding_policy_t::ONE_TO_ONE);
// Load list of valid decimation values
std::set<size_t> decims{1}; // 1 is always a valid decimation
for (size_t hb = 0; hb <= _num_halfbands; hb++) {
for (size_t cic_decim = 1; cic_decim <= _cic_max_decim; cic_decim++) {
decims.insert((1 << hb) * cic_decim);
}
}
for (size_t decim : decims) {
_valid_decims.push_back(uhd::range_t(double(decim)));
}
// Initialize properties. It is very important to first reserve the
// space, because we use push_back() further down, and properties must
// not change their base address after registration and resolver
// creation.
_samp_rate_in.reserve(get_num_ports());
_samp_rate_out.reserve(get_num_ports());
_scaling_in.reserve(get_num_ports());
_scaling_out.reserve(get_num_ports());
_decim.reserve(get_num_ports());
_freq.reserve(get_num_ports());
_type_in.reserve(get_num_ports());
_type_out.reserve(get_num_ports());
for (size_t chan = 0; chan < get_num_ports(); chan++) {
_register_props(chan);
}
register_issue_stream_cmd();
register_issue_tune_request();
}
inline void _poke_decim_word(const uint32_t decim_word, const size_t chan)
{
_ddc_reg_iface.poke32(_reg_addrs.sr_decim_addr, decim_word, chan);
}
inline void _poke_rate_n_m(const uint32_t n, const uint32_t m, const size_t chan)
{
if (_reg_addrs.sr_n_addr && _reg_addrs.sr_m_addr) {
_ddc_reg_iface.poke32(*_reg_addrs.sr_n_addr, n, chan);
_ddc_reg_iface.poke32(*_reg_addrs.sr_m_addr, m, chan);
}
}
inline void _poke_time_incr(const uint32_t time_incr, const size_t chan)
{
_ddc_reg_iface.poke32(_reg_addrs.sr_time_incr_addr, time_incr, chan);
}
inline void _poke_scale_iq(const uint32_t actual_factor, const size_t chan)
{
_ddc_reg_iface.poke32(_reg_addrs.sr_scale_iq_addr, actual_factor, chan);
}
inline void _poke_freq(
const uint32_t freq_word, const size_t chan, const uhd::time_spec_t& cmd_time)
{
_ddc_reg_iface.poke32(_reg_addrs.sr_freq_addr, freq_word, chan, cmd_time);
}
double set_freq(const double freq,
const size_t chan,
const std::optional<uhd::time_spec_t> time) override
{
// Store the current command time so we can restore it later
auto prev_cmd_time = get_command_time(chan);
if (time) {
set_command_time(*time, chan);
}
// This will trigger property propagation:
set_property<double>("freq", freq, chan);
set_command_time(prev_cmd_time, chan);
return get_freq(chan);
}
double get_freq(const size_t chan) const override
{
return _freq.at(chan).get();
}
uhd::freq_range_t get_frequency_range(const size_t chan) const override
{
const double input_rate = get_input_rate(chan);
// TODO add steps
return uhd::freq_range_t(-input_rate / 2, input_rate / 2);
}
double get_input_rate(const size_t chan) const override
{
return _samp_rate_in.at(chan).is_valid() ? _samp_rate_in.at(chan).get() : 1.0;
}
void set_input_rate(const double rate, const size_t chan) override
{
set_property<double>("samp_rate", rate, {res_source_info::INPUT_EDGE, chan});
}
double get_output_rate(const size_t chan) const override
{
return _samp_rate_out.at(chan).is_valid() ? _samp_rate_out.at(chan).get() : 1.0;
}
uhd::meta_range_t get_output_rates(const size_t chan) const override
{
uhd::meta_range_t result;
if (!_samp_rate_in.at(chan).is_valid()) {
result.push_back(uhd::range_t(1.0));
return result;
}
const double input_rate = _samp_rate_in.at(chan).get();
// The decimations are stored in order (from smallest to biggest), so
// iterate in reverse order so we can add rates from smallest to biggest
for (auto it = _valid_decims.rbegin(); it != _valid_decims.rend(); ++it) {
result.push_back(uhd::range_t(input_rate / it->start()));
}
return result;
}
double set_output_rate(const double rate, const size_t chan) override
{
if (_samp_rate_in.at(chan).is_valid()) {
const int coerced_decim = coerce_decim(get_input_rate(chan) / rate);
set_property<int>("decim", coerced_decim, chan);
} else {
RFNOC_LOG_DEBUG("Property samp_rate@"
<< chan << " is not valid, attempting to set output rate "
<< (rate / 1e6) << " Msps via the edge property.");
set_property<double>("samp_rate", rate, {res_source_info::OUTPUT_EDGE, chan});
}
return _samp_rate_out.at(chan).get();
}
// Somewhat counter-intuitively, we post a stream command as a message to
// ourselves. That's because it's easier to re-use the message handler than
// it is to reuse the issue_stream_cmd() API call, because this API call
// will always be forwarded to the upstream block, whereas the message
// handler goes both ways.
// This way, calling issue_stream_cmd() is the same as posting a message to
// our output port.
void issue_stream_cmd(const uhd::stream_cmd_t& stream_cmd, const size_t port) override
{
RFNOC_LOG_TRACE("issue_stream_cmd(stream_mode=" << char(stream_cmd.stream_mode)
<< ", port=" << port);
res_source_info dst_edge{res_source_info::OUTPUT_EDGE, port};
auto new_action = stream_cmd_action_info::make(stream_cmd.stream_mode);
new_action->stream_cmd = stream_cmd;
issue_stream_cmd_action_handler(dst_edge, new_action);
}
private:
// Declared before the protected _ddc_reg_iface so C++ initializes these first,
// allowing _reg_addrs.reg_chan_offset to be used in _ddc_reg_iface's construction.
//! Block compat number
const uhd::compat_num32 _fpga_compat;
//! Selected register address map (V0 = legacy, V1 = multisample)
const reg_addrs_t _reg_addrs;
protected:
//! Block-specific register interface
multichan_register_iface _ddc_reg_iface;
private:
//! Shorthand for num ports, since num input ports always equals num output ports
inline size_t get_num_ports()
{
return get_num_input_ports();
}
/**************************************************************************
* Initialization
************************************************************************/
void _register_props(const size_t chan)
{
// Create actual properties and store them
_samp_rate_in.push_back(
property_t<double>(PROP_KEY_SAMP_RATE, {res_source_info::INPUT_EDGE, chan}));
_samp_rate_out.push_back(
property_t<double>(PROP_KEY_SAMP_RATE, {res_source_info::OUTPUT_EDGE, chan}));
_scaling_in.push_back(
property_t<double>(PROP_KEY_SCALING, {res_source_info::INPUT_EDGE, chan}));
_scaling_out.push_back(
property_t<double>(PROP_KEY_SCALING, {res_source_info::OUTPUT_EDGE, chan}));
_decim.push_back(property_t<int>(
PROP_KEY_DECIM, DEFAULT_DECIM, {res_source_info::USER, chan}));
_freq.push_back(property_t<double>(
PROP_KEY_FREQ, DEFAULT_FREQ, {res_source_info::USER, chan}));
_type_in.emplace_back(property_t<std::string>(
PROP_KEY_TYPE, IO_TYPE_SC16, {res_source_info::INPUT_EDGE, chan}));
_type_out.emplace_back(property_t<std::string>(
PROP_KEY_TYPE, IO_TYPE_SC16, {res_source_info::OUTPUT_EDGE, chan}));
UHD_ASSERT_THROW(_samp_rate_in.size() == chan + 1);
UHD_ASSERT_THROW(_samp_rate_out.size() == chan + 1);
UHD_ASSERT_THROW(_scaling_in.size() == chan + 1);
UHD_ASSERT_THROW(_scaling_out.size() == chan + 1);
UHD_ASSERT_THROW(_decim.size() == chan + 1);
UHD_ASSERT_THROW(_freq.size() == chan + 1);
UHD_ASSERT_THROW(_type_in.size() == chan + 1);
UHD_ASSERT_THROW(_type_out.size() == chan + 1);
// give us some shorthands for the rest of this function
property_t<double>* samp_rate_in = &_samp_rate_in.back();
property_t<double>* samp_rate_out = &_samp_rate_out.back();
property_t<double>* scaling_in = &_scaling_in.back();
property_t<double>* scaling_out = &_scaling_out.back();
property_t<int>* decim = &_decim.back();
property_t<double>* freq = &_freq.back();
property_t<std::string>* type_in = &_type_in.back();
property_t<std::string>* type_out = &_type_out.back();
// register them
register_property(samp_rate_in);
register_property(samp_rate_out);
register_property(scaling_in);
register_property(scaling_out);
register_property(decim);
register_property(freq);
register_property(type_in);
register_property(type_out);
/**********************************************************************
* Add resolvers
*********************************************************************/
// Resolver for _decim: this gets executed when the user directly
// modifies _decim. the desired behaviour is to coerce it first, then
// keep the input rate constant, and re-calculate the output rate.
add_property_resolver({decim},
{decim, samp_rate_out, samp_rate_in, scaling_in},
[this,
chan,
&decim = *decim,
&samp_rate_out = *samp_rate_out,
&samp_rate_in = *samp_rate_in,
&scaling_in = *scaling_in]() {
RFNOC_LOG_TRACE("Calling resolver for `decim'@" << chan);
decim = coerce_decim(double(decim.get()));
if (decim.is_dirty()) {
set_decim(decim.get(), chan);
}
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();
}
if (scaling_in.is_valid()) {
scaling_in.force_dirty();
}
});
// Resolver for _freq: this gets executed when the user directly
// modifies _freq.
add_property_resolver(
{freq}, {freq}, [this, chan, &samp_rate_in = *samp_rate_in, &freq = *freq]() {
RFNOC_LOG_TRACE("Calling resolver for `freq'@" << chan);
if (samp_rate_in.is_valid()) {
const double new_freq =
_set_freq(freq.get(), samp_rate_in.get(), chan);
// If the frequency we just set is sufficiently close to the old
// frequency, don't bother updating the property in software
if (!uhd::math::frequencies_are_equal(new_freq, freq.get())) {
freq = new_freq;
}
} else {
RFNOC_LOG_DEBUG("Not setting frequency until sampling rate is set.");
}
});
// Resolver for the input rate:
// If this is called, then most likely, the input sampling rate was set.
// In that case, we try and keep the output sampling rate as it was, and
// modify decim to match the input/output ratio. If we can't exactly hit
// the previous output rate, then we coerce the desired decim to a valid
// decim value, and update the output rate.
// Note: This means that if the user set decim explicitly, then this
// resolver can undo the user's intentions. However, it is the option
// that retains the consistency of the graph as much as possible, and
// allows the user to call set_output_rate() on this block before the
// graph was committed.
//
// The scaling is modified in the same resolver to avoid circular
// dependencies. Note that changing the decimation will change the
// scaling ratio (input to output scaling), so we need to write to the
// decimation register in this resolver as well as the decim resolver
// in order to make sure that _residual_scaling is correctly set.
// Otherwise, the decim resolver and this resolver would conflict each
// other when writing to scaling_out.
//
// This resolver may affect the frequency: If the input sampling rate
// was changed, then the phase accumulator increment needs to be
// recalculated in order to retain the current value of the frequency
// offset, which is given in Hz (not in radians).
add_property_resolver({samp_rate_in, scaling_in},
{decim, samp_rate_out, freq, scaling_out},
[this,
chan,
&decim = *decim,
&freq = *freq,
&samp_rate_out = *samp_rate_out,
&samp_rate_in = *samp_rate_in,
&scaling_in = *scaling_in,
&scaling_out = *scaling_out]() {
RFNOC_LOG_TRACE(
"Calling resolver for `samp_rate_in/scaling_in'@" << chan);
if (samp_rate_in.is_valid()) {
RFNOC_LOG_TRACE("New samp_rate_in is " << samp_rate_in.get());
if (samp_rate_out.is_valid()) {
decim = coerce_decim(samp_rate_in.get() / samp_rate_out.get());
set_decim(decim.get(), chan);
const double new_samp_rate_out = samp_rate_in.get() / decim.get();
// Only update the samp_rate_out if the new value is not the same
// frequency. However, we still want to call the operator= to make
// sure metadata gets handled
samp_rate_out = (uhd::math::frequencies_are_equal(
samp_rate_out, new_samp_rate_out))
? samp_rate_out.get()
: new_samp_rate_out;
RFNOC_LOG_TRACE("New samp_rate_out is " << samp_rate_out.get());
} else if (decim.is_valid()) {
samp_rate_out = samp_rate_in.get() / decim.get();
}
// If the input rate changes, we need to update the DDS, too,
// since it works on frequencies normalized by the input rate.
freq.force_dirty();
}
if (scaling_in.is_valid()) {
scaling_out = scaling_in.get() * _residual_scaling.at(chan);
}
});
// Resolver for the output rate: like the previous one, but flipped.
add_property_resolver({samp_rate_out, scaling_out},
{decim, samp_rate_in, scaling_out},
[this,
chan,
&decim = *decim,
&samp_rate_out = *samp_rate_out,
&samp_rate_in = *samp_rate_in,
&scaling_in = *scaling_in,
&scaling_out = *scaling_out]() {
RFNOC_LOG_TRACE(
"Calling resolver for `samp_rate_out/scaling_out'@" << chan);
if (samp_rate_out.is_valid()) {
if (samp_rate_in.is_valid()) {
decim = coerce_decim(samp_rate_in.get() / samp_rate_out.get());
set_decim(decim.get(), chan);
}
// If decim is dirty, it will trigger the decim resolver.
// However, the decim resolver will set the output rate based
// on the input rate, so we need to force the input rate first.
if (decim.is_dirty()) {
const double new_samp_rate_in = samp_rate_out.get() * decim.get();
// Only update the samp_rate_in if the new value is not the same
// frequency. However, we still want to call the operator= to make
// sure metadata gets handled
if (samp_rate_in.is_valid()) {
samp_rate_in = (uhd::math::frequencies_are_equal(
samp_rate_in, new_samp_rate_in))
? samp_rate_in.get()
: new_samp_rate_in;
} else {
samp_rate_in = new_samp_rate_in;
}
RFNOC_LOG_TRACE("New samp_rate_in is " << samp_rate_in.get());
}
}
if (scaling_in.is_valid()) {
scaling_out = scaling_in.get() * _residual_scaling.at(chan);
}
});
// Resolvers for type: These are constants
add_property_resolver(
{type_in}, {type_in}, [&type_in = *type_in]() { type_in.set(IO_TYPE_SC16); });
add_property_resolver({type_out}, {type_out}, [&type_out = *type_out]() {
type_out.set(IO_TYPE_SC16);
});
}
void register_issue_stream_cmd()
{
register_action_handler(ACTION_KEY_STREAM_CMD,
[this](const res_source_info& src, action_info::sptr action) {
stream_cmd_action_info::sptr stream_cmd_action =
std::dynamic_pointer_cast<stream_cmd_action_info>(action);
if (!stream_cmd_action) {
throw uhd::runtime_error(
"Received stream_cmd of invalid action type!");
}
issue_stream_cmd_action_handler(src, stream_cmd_action);
});
}
void issue_stream_cmd_action_handler(
const res_source_info& src, stream_cmd_action_info::sptr stream_cmd_action)
{
res_source_info dst_edge{res_source_info::invert_edge(src.type), src.instance};
const size_t chan = src.instance;
uhd::stream_cmd_t::stream_mode_t stream_mode =
stream_cmd_action->stream_cmd.stream_mode;
RFNOC_LOG_TRACE("Received stream command: " << char(stream_mode) << " to "
<< src.to_string()
<< ", id==" << stream_cmd_action->id);
auto new_action = stream_cmd_action_info::make(stream_mode);
new_action->stream_cmd = stream_cmd_action->stream_cmd;
if (stream_mode == uhd::stream_cmd_t::STREAM_MODE_NUM_SAMPS_AND_DONE
|| stream_mode == uhd::stream_cmd_t::STREAM_MODE_NUM_SAMPS_AND_MORE) {
if (src.type == res_source_info::OUTPUT_EDGE) {
new_action->stream_cmd.num_samps *= _decim.at(chan).get();
} else {
new_action->stream_cmd.num_samps /= _decim.at(chan).get();
}
RFNOC_LOG_TRACE("Forwarding num_samps stream command, new value is "
<< new_action->stream_cmd.num_samps);
}
post_action(dst_edge, new_action, action_mode_t::ASYNC);
}
void register_issue_tune_request()
{
register_action_handler(ACTION_KEY_TUNE_REQUEST,
[this](const res_source_info& src, action_info::sptr action) {
tune_request_action_info::sptr tune_request_action =
std::dynamic_pointer_cast<tune_request_action_info>(action);
if (!tune_request_action) {
throw uhd::runtime_error(
"Received tune_request of invalid action type!");
}
issue_tune_request_action_handler(src, tune_request_action);
});
}
void issue_tune_request_action_handler(
const res_source_info& src, tune_request_action_info::sptr tune_request_action)
{
RFNOC_LOG_TRACE("DDC issue_tune_request_action_handler");
res_source_info dst_edge{res_source_info::invert_edge(src.type), src.instance};
const size_t chan = src.instance;
uhd::tune_request_t tune_request = tune_request_action->tune_request;
RFNOC_LOG_TRACE("Received tune_request to "
<< src.to_string() << ", id==" << tune_request_action->id);
uhd::freq_range_t dsp_range = get_frequency_range(chan);
uhd::freq_range_t tune_range = tune_request_action->overall_freq_range;
tune_request_action->dsp_range = dsp_range;
if (tune_range.empty()) {
tune_range = dsp_range;
}
if (src.type == res_source_info::INPUT_EDGE) {
auto set_dsp_freq = [this, chan](
double freq) { set_freq(freq, chan, std::nullopt); };
double clipped_requested_freq = tune_range.clip(tune_request.target_freq);
tune_request_action->tune_result.target_dsp_freq = std::abs(
tune_request_action->tune_result.actual_rf_freq - clipped_requested_freq);
//------------------------------------------------------------------
//-- Set the DSP frequency depending upon the DSP frequency policy.
//------------------------------------------------------------------
double target_dsp_freq = 0.0;
switch (tune_request.dsp_freq_policy) {
case uhd::tune_request_t::POLICY_AUTO:
/* If we are using the AUTO tuning policy, then we prevent the
* CORDIC from spinning us outside of the range of the baseband
* filter, regardless of what the user requested. This could happen
* if the user requested a center frequency so far outside of the
* tunable range of the FE that the CORDIC would spin outside the
* filtered baseband. */
target_dsp_freq = tune_request_action->tune_result.target_dsp_freq
- tune_request_action->tune_result.actual_dsp_freq;
break;
case uhd::tune_request_t::POLICY_MANUAL:
/* If the user has specified a manual tune policy, we will allow
* tuning outside of the baseband filter, but will still clip the
* target DSP frequency to within the bounds of the CORDIC to
* prevent undefined behavior (likely an overflow). */
target_dsp_freq = dsp_range.clip(tune_request.dsp_freq);
break;
case uhd::tune_request_t::POLICY_NONE:
break; // does not set
}
RFNOC_LOG_TRACE(
str(boost::format("Target DSP Freq: %.6fMHz") % (target_dsp_freq / 1e6)));
//------------------------------------------------------------------
//-- Tune the DSP
//------------------------------------------------------------------
if (tune_request.dsp_freq_policy != uhd::tune_request_t::POLICY_NONE) {
set_dsp_freq(target_dsp_freq);
}
const double actual_dsp_freq = get_freq(chan);
RFNOC_LOG_TRACE(
str(boost::format("Actual DSP Freq: %.6fMHz") % (actual_dsp_freq / 1e6)));
tune_request_action->tune_result.actual_dsp_freq += actual_dsp_freq;
RFNOC_LOG_TRACE(
"Tune_result details DDC : target_rf_frq = "
<< tune_request_action->tune_result.target_rf_freq
<< " target dsp freq = "
<< tune_request_action->tune_result.target_dsp_freq << " clipped rf_freq "
<< tune_request_action->tune_result.clipped_rf_freq
<< " actual_rf_freq = " << tune_request_action->tune_result.actual_rf_freq
<< " actual dsp_freq= "
<< tune_request_action->tune_result.actual_dsp_freq);
auto new_action = tune_request_action_info::make(tune_request);
new_action->tune_request = tune_request_action->tune_request;
new_action->tune_result = tune_request_action->tune_result;
new_action->dsp_range = tune_request_action->dsp_range;
new_action->overall_freq_range = tune_request_action->overall_freq_range;
post_action(dst_edge, new_action, action_mode_t::ASYNC);
} else {
auto new_action = tune_request_action_info::make(tune_request);
new_action->tune_request = tune_request_action->tune_request;
new_action->tune_result = tune_request_action->tune_result;
new_action->dsp_range = tune_request_action->dsp_range;
new_action->overall_freq_range = tune_request_action->overall_freq_range;
post_action(dst_edge, new_action, action_mode_t::ASYNC);
}
}
/**************************************************************************
* FPGA communication (register IO)
*************************************************************************/
/*! Update the decimation value
*
* \param decim The new decimation value. It must be valid decimation value.
* \throws uhd::assertion_error if decim is not valid.
*/
void set_decim(int decim, const size_t chan)
{
RFNOC_LOG_TRACE("Set decim to " << decim);
// Step 1: Calculate number of halfbands
uint32_t hb_enable = 0;
uint32_t cic_decim = decim;
while ((cic_decim % 2 == 0) and hb_enable < _num_halfbands) {
hb_enable++;
cic_decim /= 2;
}
// Step 2: Make sure we can handle the rest with the CIC
UHD_ASSERT_THROW(hb_enable <= _num_halfbands);
UHD_ASSERT_THROW(cic_decim > 0 and cic_decim <= _cic_max_decim);
const uint32_t decim_word = (hb_enable << 8) | cic_decim;
_poke_decim_word(decim_word, chan);
// Rate change = M/N
_poke_rate_n_m(uint32_t(decim), 1, chan);
// Configure the time increment for each output word containing _spc samples.
_poke_time_incr(uint32_t(get_tick_rate() / get_output_rate(chan) * _spc), chan);
if (cic_decim > 1 and hb_enable == 0) {
RFNOC_LOG_WARNING(
"The requested decimation is odd; the user should expect passband "
"CIC rolloff.\n"
"Select an even decimation to ensure that a halfband filter is "
"enabled.\n"
"Decimations factorable by 4 will enable 2 halfbands, those "
"factorable by 8 will enable 3 halfbands.\n"
"decimation = dsp_rate/samp_rate -> "
<< decim);
}
// The multisample DDS normalizes its NCO internally; the legacy DDS
// requires the historical factor-of-two gain correction.
const double dds_gain = _reg_addrs.rb_spc ? 1.0 : 2.0;
// Calculate algorithmic gain of CIC for a given decimation.
// For Ettus CIC R=decim, M=1, N=4. Gain = (R * M) ^ N
// The Ettus CIC also tries its best to compensate for the gain by
// shifting the CIC output. This reduces the gain by a factor of
// 2**ceil(log2(cic_gain))
const double cic_gain = std::pow(double(cic_decim * 1), 4);
const double total_gain =
dds_gain * cic_gain / std::pow(2, uhd::math::ceil_log2(cic_gain));
update_scaling(total_gain, chan);
}
/*! \brief Update scaling based on the current gain.
*
* Calculates the closest fixpoint value that this block can correct for in
* hardware (fixpoint). The residual gain is written to _residual_scaling.
*/
void update_scaling(const double dsp_gain, const size_t chan)
{
constexpr double FIXPOINT_SCALING = 1 << 15;
const double compensation_factor = 1. / dsp_gain;
// Convert to fixpoint
const double target_factor = FIXPOINT_SCALING * compensation_factor;
const int32_t actual_factor = std::lround(target_factor);
// Write DDC with scaling correction for CIC and DDS that maximizes
// dynamic range
_poke_scale_iq(uint32_t(actual_factor), chan);
// Calculate the error introduced by using fixedpoint representation for
// the scaler, can be corrected in host later.
_residual_scaling[chan] = dsp_gain * double(actual_factor) / FIXPOINT_SCALING;
}
/*! Return the closest possible decimation value to the one requested
*/
int coerce_decim(const double requested_decim) const
{
UHD_ASSERT_THROW(requested_decim >= 0);
return static_cast<int>(_valid_decims.clip(requested_decim, true));
}
//! Set the DDS frequency shift the signal to \p requested_freq
double _set_freq(
const double requested_freq, const double dds_rate, const size_t chan)
{
double actual_freq;
int32_t freq_word;
std::tie(actual_freq, freq_word) =
get_freq_and_freq_word(requested_freq, dds_rate);
// Write freq_word directly (not scaled by SPC). The hardware
// dds_multisample uses freq_word as the per-sample phase
// increment and multiplies internally by SPC for the per-clock
// accumulator advance.
_poke_freq(uint32_t(freq_word), chan, get_command_time(chan));
return actual_freq;
}
/**************************************************************************
* Attributes
*************************************************************************/
//! Number of halfbands
const size_t _num_halfbands;
//! Max CIC decim
const size_t _cic_max_decim;
//! Number of samples per clock as reported by hardware
const uint32_t _spc;
//! List of valid decimation values
uhd::meta_range_t _valid_decims;
//! Cache the current residual scaling
std::vector<double> _residual_scaling;
//! Properties for type_in (one per port)
std::vector<property_t<std::string>> _type_in;
//! Properties for type_out (one per port)
std::vector<property_t<std::string>> _type_out;
//! Properties for samp_rate_in (one per port)
std::vector<property_t<double>> _samp_rate_in;
//! Properties for samp_rate_out (one per port)
std::vector<property_t<double>> _samp_rate_out;
//! Properties for scaling_in (one per port)
std::vector<property_t<double>> _scaling_in;
//! Properties for scaling_out (one per port)
std::vector<property_t<double>> _scaling_out;
//! Properties for decim (one per port)
std::vector<property_t<int>> _decim;
//! Properties for freq (one per port)
std::vector<property_t<double>> _freq;
};
UHD_RFNOC_BLOCK_REGISTER_DIRECT(
ddc_block_control, 0xDDC00000, "DDC", CLOCK_KEY_GRAPH, "bus_clk")