Skip to main content

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:
  1. User Logic — the custom RTL that performs the actual computation (FFT, FIR, custom DSP, …).
  2. 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

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

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.

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

1

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.
2

Scaffold with ModTool

Run rfnoc_modtool new <block_name> to generate the NoC Shell, Verilog template, C++ controller template, testbench, and CMake build scripts.
3

Implement the HDL

Fill in the user logic in the generated Verilog template. Implement the ctrlport slave to handle register reads and writes.
4

Write HDL testbenches

Use the generated testbench template and the RFNoC BFMs to verify data-path and control-path behavior in simulation.
5

Implement the C++ controller

Flesh out the generated block controller: declare properties and resolvers, implement the public API, add action handlers.
6

Add to an FPGA image

Reference the block in an Image Builder YAML file and build the bitfile with rfnoc_image_builder.

Build docs developers (and LLMs) love