Documentation Index
Fetch the complete documentation index at: https://mintlify.com/EttusResearch/uhd/llms.txt
Use this file to discover all available pages before exploring further.
A custom RFNoC block is two things: an FPGA module that implements the user logic and a C++ block controller that lives in UHD and lets host software configure it. Both sides connect to the framework through well-defined interfaces — the NoC Shell on the FPGA side and noc_block_base on the software side — so that the same APIs work regardless of what the block actually does.
Block Structure in the FPGA
Every NoC block in the FPGA is composed of two layers:
- User Logic — the custom RTL that performs the actual computation (FFT, FIR, custom DSP, …).
- NoC Shell — auto-generated Verilog glue code that connects the user logic to the RFNoC packet network. It handles CHDR packetization, flow control, control-plane routing, and clock/reset distribution.
┌─────────────────────────────────────────────────────┐
│ NoC Block │
│ ┌─────────────┐ ┌───────────────────────────┐ │
│ │ User Logic │◄─────►│ NoC Shell │ │
│ │ (your RTL) │ │ (CHDR, ctrl, flow ctrl) │ │
│ └─────────────┘ └───────────────────────────┘ │
└─────────────────────────────────────────────────────┘
The NoC Shell is generated by rfnoc_modtool and provides several interface options depending on how much abstraction you need.
Control Interfaces
Control Port (Simple)
AXIS-Ctrl (Low-level)
The recommended interface for most blocks. The NoC Shell parses incoming AXIS-Ctrl packets and exposes simple req_wr / req_rd strobes and a req_addr / req_data bus. Only read, write, and sleep operations are supported; the hardware handles ACKs automatically.// Example ctrlport slave signals (outputs from NoC Shell)
output ctrlport_clk,
output ctrlport_rst,
output m_ctrlport_req_wr,
output m_ctrlport_req_rd,
output [19:0] m_ctrlport_req_addr,
output [31:0] m_ctrlport_req_data,
input m_ctrlport_resp_ack,
input [31:0] m_ctrlport_resp_data
A 32-bit AXI4-Stream interface that exposes raw AXIS-Ctrl packets. Use this when you need block-read/write, poll, or user-defined opcodes, or when your block needs to initiate control transactions to other blocks.
Data Interfaces
The highest-level data interface. The NoC Shell strips CHDR headers and presents pure sample data on a standard AXI4-Stream bus. Sideband signals (ttimestamp, thas_time, teob, teov) carry packet metadata. Works well for blocks that process samples without needing header information.
Splits the stream into two buses: a payload bus carrying sample data and a context bus carrying CHDR header words (header, optional timestamp, optional metadata). Useful for blocks that need to inspect or modify timestamps and metadata.
Exposes the raw CHDR packet stream directly. Maximum control, but the block is responsible for all CHDR framing and parsing.
Clocks and Resets
Two clocks are always available to user logic:
rfnoc_chdr_clk — used by the CHDR data path
rfnoc_ctrl_clk — used by the control path
Additional user-defined clocks can be declared in the block YAML and connected at image-assembly time. Two synchronous resets (rfnoc_chdr_rst, rfnoc_ctrl_rst) are driven by the framework and must be used to reset user IP.
Register Interface: peek32 / poke32
Block configuration is done through a 32-bit memory-mapped register space exposed to software via the control-plane path. In the FPGA, user logic implements a ctrlport slave that decodes addresses:
// Verilog ctrlport slave example
localparam REG_GAIN_ADDR = 20'h00;
localparam REG_SHIFT_ADDR = 20'h04;
always @(posedge ctrlport_clk) begin
if (m_ctrlport_req_wr) begin
case (m_ctrlport_req_addr)
REG_GAIN_ADDR: gain <= m_ctrlport_req_data[15:0];
REG_SHIFT_ADDR: shift <= m_ctrlport_req_data[7:0];
endcase
end
m_ctrlport_resp_ack <= m_ctrlport_req_wr | m_ctrlport_req_rd;
m_ctrlport_resp_data <= (m_ctrlport_req_addr == REG_GAIN_ADDR) ? gain : 32'h0;
end
On the software side, the same registers are accessed through the register_iface:
// Inside your block controller:
regs().poke32(REG_GAIN_ADDR, static_cast<uint32_t>(gain_linear * 65535));
regs().poke32(REG_SHIFT_ADDR, static_cast<uint32_t>(shift_bits));
uint32_t readback = regs().peek32(REG_GAIN_ADDR);
Timed Commands
Control transactions can be associated with a hardware timestamp so that the FPGA executes the register write at a precise time:
uhd::time_spec_t cmd_time = mb->get_timekeeper(0)->get_time_now()
+ uhd::time_spec_t(0.01); // 10 ms from now
regs().poke32(REG_GAIN_ADDR, new_gain, cmd_time);
Custom Block Controller in C++
A block controller inherits from uhd::rfnoc::noc_block_base. The constructor is where you read initial register state, define properties, add resolvers, and register action handlers. The RFNOC_BLOCK_CONSTRUCTOR macro handles the required boilerplate.
class my_block_control_impl : public my_block_control
{
public:
RFNOC_BLOCK_CONSTRUCTOR(my_block_control)
{
// Read initial hardware state
uint32_t caps = regs().peek32(REG_CAPS);
_num_channels = (caps >> 8) & 0xFF;
// Register a user property
register_property(&_gain);
// Add a property resolver: triggered when gain changes
add_property_resolver(
{&_gain}, // input/trigger list
{&_gain}, // output list
[this]() {
// Coerce to valid range [0.0, 1.0]
double g = std::clamp(_gain.get(), 0.0, 1.0);
_gain = g;
if (_gain.is_dirty()) {
regs().poke32(REG_GAIN_ADDR,
static_cast<uint32_t>(g * 65535));
}
});
// Register an action handler for stream commands
register_action_handler(uhd::rfnoc::ACTION_KEY_STREAM_CMD,
[this](const uhd::rfnoc::res_source_info& src,
uhd::rfnoc::action_info::sptr action) {
auto cmd = std::dynamic_pointer_cast<
uhd::rfnoc::stream_cmd_action_info>(action);
if (cmd) {
issue_stream_cmd(cmd->stream_cmd);
}
});
}
// Public API
void set_gain(double gain)
{
set_property<double>("gain", gain, 0);
}
double get_gain() const
{
return _gain.get();
}
private:
void issue_stream_cmd(const uhd::stream_cmd_t& cmd)
{
// Write start/stop to hardware
const uint32_t start = (cmd.stream_mode !=
uhd::stream_cmd_t::STREAM_MODE_STOP_CONTINUOUS) ? 1 : 0;
regs().poke32(REG_CTRL, start);
}
static constexpr uint32_t REG_CAPS = 0x00;
static constexpr uint32_t REG_GAIN_ADDR = 0x04;
static constexpr uint32_t REG_CTRL = 0x08;
property_t<double> _gain{"gain", 1.0, {uhd::rfnoc::res_source_info::USER, 0}};
uint32_t _num_channels = 1;
};
Null Block Controller — Full Example
The following is the complete null source/sink block controller from the UHD source tree, showing all the major patterns:
class null_block_control_impl : public null_block_control
{
public:
RFNOC_BLOCK_CONSTRUCTOR(null_block_control)
{
uint32_t initial_state = regs().peek32(REG_CTRL_STATUS);
_nipc = (initial_state >> 24) & 0xFF;
_item_width = (initial_state >> 16) & 0xFF;
register_property(&_lpp);
add_property_resolver(
{&_lpp},
{&_lpp},
[this]() {
set_lines_per_packet(_lpp.get());
_lpp = get_lines_per_packet();
});
register_issue_stream_cmd();
}
void set_lines_per_packet(const uint32_t lpp)
{
regs().poke32(REG_SRC_LINES_PER_PKT, /* computed value */);
}
uint32_t get_lines_per_packet()
{
return regs().peek32(REG_SRC_LINES_PER_PKT) + 2;
}
private:
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 cmd =
std::dynamic_pointer_cast<stream_cmd_action_info>(action);
if (!cmd) {
throw uhd::runtime_error("Invalid stream_cmd action type!");
}
issue_stream_cmd(cmd->stream_cmd);
});
}
property_t<int> _lpp{"lpp", 100, {res_source_info::USER}};
uint32_t _nipc;
uint32_t _item_width;
};
YAML Block Definition File
Every RFNoC block has a YAML descriptor file (sometimes called the block definition or block_desc) that captures its interface. This file is used by the RFNoC Image Builder to instantiate the block in an FPGA image and by UHD to look up the correct controller class.
The following annotated example covers the most important fields:
schema: rfnoc_modtool_args # Identifies this as a modtool block descriptor
module_name: my_dsp_block # Verilog module name (rfnoc_block_my_dsp_block)
version: "1.0"
rfnoc_version: "1.0"
chdr_width: 64 # Must match the FPGA image's CHDR_W
noc_id: 0xABCD1234 # Unique 32-bit identifier for this block type
# Optional HDL generics exposed at image-assembly time
parameters:
NUM_CHANNELS: 2
clocks:
- rfnoc_chdr:
freq: 'range(100e6, 500e6)'
- rfnoc_ctrl:
freq: 'range(10e6, 100e6)'
- ce: # User/compute clock
freq: 'range(200e6, 400e6)'
control:
fpga_iface: ctrlport # Use simple Control Port interface
interface_direction: slave
fifo_depth: 32
clk_domain: rfnoc_ctrl
ctrlport:
byte_mode: False
timed: True # Block supports timed register writes
has_status: False
data:
fpga_iface: axis_pyld_ctxt # Payload + Context interface
clk_domain: ce
inputs:
in0:
context: True
item_width: 32
nipc: 1
format: sc16
payload_fifo_depth: 32
context_fifo_depth: 32
outputs:
out0:
context: True
item_width: 32
nipc: 1
format: sc16
payload_fifo_depth: 32
context_fifo_depth: 32
# Optional: add a hardware timestamp port
io_ports:
time:
type: timekeeper
drive: listener
The noc_id must be globally unique across all blocks that will coexist in a single FPGA image. By convention, use a value that reflects your organization or block function. The NoC ID is what UHD uses to match a discovered block to a block controller class.
Testing Custom Blocks
HDL Simulation
rfnoc_modtool generates a SystemVerilog testbench template at rfnoc/testbenches/rfnoc_block_<name>_tb.sv. The RFNoC simulation library provides:
rfnoc_block_ctrl_sim — a software bus-functional model (BFM) for the control plane.
rfnoc_chdr_bfm — a BFM for injecting and capturing CHDR packets on the data plane.
C++ Mock Blocks
For unit-testing block controller logic without hardware, UHD provides a mock register interface and a mock block environment. Tests can instantiate a block controller against a fake register file:
// Create a mock register interface backed by a std::map
auto mock_reg_iface = std::make_shared<mock_reg_iface_t>();
// Instantiate the block controller
auto block = std::make_shared<my_block_control_impl>(
make_block_args(noc_id, block_id, num_inputs, num_outputs),
mock_reg_iface
);
// Set a property and verify the resulting register write
block->set_property<double>("gain", 0.5, 0);
BOOST_CHECK_EQUAL(mock_reg_iface->read_memory[REG_GAIN_ADDR],
static_cast<uint32_t>(0.5 * 65535));
Development Checklist
Choose your interfaces
Decide on CHDR width, control interface (ctrlport vs. AXIS-Ctrl), and data interface (axis_data vs. axis_pyld_ctxt vs. axis_chdr). Fill in the YAML block descriptor.
Scaffold with ModTool
Run rfnoc_modtool new <block_name> to generate the NoC Shell, Verilog template, C++ controller template, testbench, and CMake build scripts.
Implement the HDL
Fill in the user logic in the generated Verilog template. Implement the ctrlport slave to handle register reads and writes.
Write HDL testbenches
Use the generated testbench template and the RFNoC BFMs to verify data-path and control-path behavior in simulation.
Implement the C++ controller
Flesh out the generated block controller: declare properties and resolvers, implement the public API, add action handlers.
Add to an FPGA image
Reference the block in an Image Builder YAML file and build the bitfile with rfnoc_image_builder.