USRP Spectrum Viewer#
Authors: Andrew Lynch andrew.lynch@emerson.com (National Instruments Corporation.), Johannes Lange johannes.lange@emerson.com (National Instruments Corporation.), Abhiram Anil abhiram.anil@emerson.com (National Instruments Corporation.)
Supported platforms: x86_64, aarch64
Language: C++
Last modified: September 9, 2026
Latest version: 1.0.0
Minimum Holoscan SDK version: 4.4.0
Tested Holoscan SDK versions: 4.4.0, 4.5.0, 4.6.0
Contribution metric: Level 3 - Developmental
Overview#
The USRP Spectrum Viewer application receives RF samples from a USRP radio, ingests CHDR packets through DAQIRI, converts them into GPU tensors, performs FFT-based spectral analysis, computes power in dB, and renders the resulting spectrum with Holoviz.
The processing pipeline is shown below. UsrpRxOp is a control-only operator. It commands the radio to start streaming but emits no tensors. The RF samples reach UhdChdrRxOp out-of-band over the NIC (via DPDK/DAQIRI), not through a Holoscan port, so there is no Holoscan data-flow edge between UsrpRxOp and UhdChdrRxOp.
flowchart LR
start([start_op]) -. triggers .-> USRP[UsrpRxOp - control only]
USRP -. commands radio .-> NIC[(USRP / NIC / DPDK)]
NIC == RF samples ==> CHDR[UhdChdrRxOp]
CHDR --> FFT[FFT]
FFT --> MAG[SpectrumMagnitudeOp]
FFT --> LOG[LogOp]
MAG --> VIZ[SpectrumVisualizerOp]
VIZ -->|outputs to receivers| HV[HolovizOp]
VIZ -->|output_specs to input_specs| HV
classDef green fill:#c7f9cc,stroke:#000,color:#000;
classDef white fill:#ffffff,stroke:#000,color:#000;
class start,USRP,CHDR,FFT,MAG,LOG,VIZ,HV green;
class NIC white;
USRP References#
The application uses the UHD API to configure and control the USRP, then receives the resulting CHDR/UDP stream through DAQIRI. Consult the following before adapting the radio and network settings:
- USRP Hardware Driver and UHD Manual
- UHD Configuration and Device Arguments
- USRP Hardware Documentation
Acronyms#
| Acronym | Meaning |
|---|---|
| CHDR | Condensed Hierarchical Datagram (USRP communication protocol) |
| DPDK | Data Plane Development Kit |
| FFT | Fast Fourier Transform |
| NIC | Network Interface Card |
| UHD | USRP Hardware Driver |
| USRP | Universal Software Radio Peripheral |
Requirements#
The following equipment is required to run the application:
- A USRP device capable of streaming CHDR/UDP data to the host NIC, e.g., a USRP X410 or X440
- An x86_64 PC with decent performance running Ubuntu Linux 24.04
- NVIDIA/Mellanox ConnectX-6 Dx or better
- NVIDIA RTX A4000 or better
- Alternatively, a NVIDIA DGX Spark can be used as well
- A QSFP+ cable capable of 100GbE transmission
[!IMPORTANT] The settings in
config.yamlmust be tailored to your system, including USRP device arguments, NIC PCIe address, host IP address, destination ports, and MAC addressing.
First-Time Machine Setup#
Before configuring or running this application, prepare the host:
-
High-performance networking: this application receives the USRP CHDR/UDP stream through DAQIRI and DPDK, placing the RF payload directly into GPU-accessible memory. Complete the High Performance Networking tutorial (hugepages, NIC/DPDK binding) first. This is required, not optional.
-
Socket buffer limits: increase the kernel socket buffers used by the UHD control path. If UHD warns about send/receive buffer sizes or you see dropped packets, run:
sudo sysctl -w net.core.rmem_max=2500000
sudo sysctl -w net.core.wmem_max=2500000
These apply to the current boot only; add them to /etc/sysctl.conf to persist.
- USRP FPGA image: load an FPGA image that supports your streaming rate with
uhd_image_loader. See the USRP X4xx manual for the loading procedure, and Limitations for the images used during testing.
Configuration#
Each major stage of the pipeline has its own configuration section in config.yaml.
The operator-specific options and their meaning are documented in each folder-level README:
USRP Receive Configuration#
The usrp_rx section configures the control-plane connection to the radio. When enabled is true, UsrpRxOp opens the device identified by args, configures the sample rate, RF center frequency, and gain for every selected channel, and starts one UHD receive streamer per channel. Each streamer sends its UDP/CHDR packets to the
matching dest_ports entry on dest_addr, channels and dest_ports must have the same number of entries. Their positions form the mapping between a radio channel and a DAQIRI receive queue; for example, channel 0 uses port 1234 and channel 1 uses port 1235 below.
The CH0/CH1 labels in the Holoviz legend, the peak readout, and the LogOp throughput lines
identify the DAQIRI receive queue, not the radio channel index. That is the only channel
identifier which is always defined, because usrp_rx is optional and the application can also
attach to an externally started streamer.
Which radio channel appears as CHn therefore follows from your configuration:
usrp_rx.channels[i] streams to usrp_rx.dest_ports[i], that port is matched by a
daqiri.cfg.interfaces[].rx.flows[] entry, and the flow's action.id selects the queue. With the
shipped configuration this chain is the identity mapping. If you stream a non-identity channel list
— for example channels: [1] or channels: [2, 0] — the traces are still correct, but CHn will
not match the physical radio channel number.
[!IMPORTANT]
enableddefaults tofalseso the application never tries to open a radio with the placeholder addresses shipped inconfig.yaml. Replace the placeholderargs,dest_addr, anddest_mac_addrvalues with your hardware's settings, then setenabled: trueto start in-app USRP control.Before starting the application, make the
spectrum_vizreference settings describe the RF stream configured inusrp_rx:usrp_rx: freq: 1000000000.0 # Hz: actual USRP center frequency rate: 500000000.0 # samples/s: FFT sample rate spectrum_viz: ref_center_hz: 1000000000.0 # must equal usrp_rx.freq ref_bandwidth_hz: 500000000.0 # must equal the FFT sample rateIf these values do not match the received RF stream, the spectrum still renders, but its frequency labels and peak-frequency readout are incorrect.
UHD Streaming Path#
The USRP uses separate network paths for control and high-rate CHDR data. The host runs UHD control through its management interface, while DAQIRI/DPDK receives the packet stream through the high-speed interface configured as sdr_data.
flowchart LR
subgraph USRP["USRP"]
direction TB
ETH0["eth0:<br/><device_addr>"]
SFP0["sfp0"]
ETH0 ~~~ SFP0
end
subgraph HOST["Host"]
direction TB
subgraph UHD["UHD Host"]
EN01["en01"]
end
subgraph REMOTE["Remote Destination"]
direction TB
ENP["enp1s0f1np1:<br/><dest_addr><br/><dest_mac_addr>"]
end
UHD ~~~ REMOTE
end
USRP ~~~ HOST
EN01 -->|"1/10 GbE management and UHD control"| ETH0
SFP0 -->|"100 GbE CHDR / UDP RF data (all channels)"| ENP
classDef white fill:#ffffff,stroke:#000,color:#000;
class EN01,ENP,ETH0,SFP0 white;
style USRP fill:#c7f9cc,stroke:#000,color:#000;
style HOST fill:#c7f9cc,stroke:#000,color:#000;
style UHD fill:#c7f9cc,stroke:#000,color:#000;
style REMOTE fill:#c7f9cc,stroke:#000,color:#000;
The addresses shown above illustrate the physical topology from the supplied setup diagram. Configure the matching USRP address and data destination in usrp_rx; replace the example addresses with those assigned to the USRP and host in your environment.
usrp_rx:
enabled: true
# UHD device address and optional device settings.
args: "addr=<device_addr>,master_clock_rate=500000000"
rate: 500000000.0
freq: 1000000000.0
gain: 0.0
channels: [0, 1]
# Destination for the USRP's CHDR/UDP streams.
dest_addr: <dest_addr>
dest_ports: [1234, 1235]
adapter: "sfp0"
dest_mac_addr: <dest_mac>
keep_hdr: true
spp: 1024
mtu: 8064
start_delay_seconds: 0.05
Shared Visualization State#
This application uses a shared SpectrumViewState object to communicate between:
- the Holoviz overlay callback, which owns the interactive center-frequency and bandwidth controls
SpectrumVisualizerOp, which remaps spectrum bins into screen-space coordinates and reports peak frequency / power information
Visualization High-Level Flow#
At a high level, the visualization subsystem has two separate paths that meet inside HolovizOp:
- the geometry path, where
SpectrumVisualizerOpconverts the dB spectrum into line-strip coordinates and draw specifications - the shared-state path, where the overlay callback and the visualizer exchange pan, zoom, peak-toggle, and peak-result values through
SpectrumViewState
flowchart LR
MAG["SpectrumMagnitudeOp"] -->|"Magnitude dB"| VIS["SpectrumVisualizerOp"]
STATE["SpectrumViewState"] -->|"reads: center_mhz,<br/>bandwidth_mhz, show_peak"| VIS
VIS -->|"writes:<br/>peak_freq_mhz, peak_db"| STATE
VIS -->|"Geometry tensors + LINE_STRIP InputSpecs"| HV
subgraph HE["Holoviz Ecosystem"]
HV["HolovizOp"] -->|"executes every<br/>render cycle"| OV["SpectrumOverlay<br/>(Holoviz layer_callback)"]
OV -->|"Final Frame<br/>(Geometry + Overlay)"| DW["Display Window"]
end
OV -->|"writes: center_mhz,<br/>bandwidth_mhz, show_peak"| STATE
STATE -->|"reads: peak_freq_mhz, peak_db"| OV
classDef white fill:#ffffff,stroke:#000,color:#000;
class MAG,VIS,STATE,HV,OV,DW white;
style HE fill:#c7f9cc,stroke:#000,color:#000;
Memory Layout#
The CHDR ingest path uses DAQIRI-managed memory regions. DAQIRI splits every received packet into three segments and places each segment according to the matching queue's memory_regions list. This avoids copying the RF sample payload through host memory before GPU processing.
For channel 1, the configured queue associates the packet segments with the following regions:
daqiri:
cfg:
memory_regions:
- name: "Headers_RX_CPU"
kind: "huge"
affinity: 0
access: [local]
num_bufs: 12500
buf_size: 42 # Ethernet + IPv4 + UDP headers
- name: "CH1_CHDR_Headers_RX_CPU"
kind: "huge"
affinity: 0
access: [local]
num_bufs: 12500
buf_size: 64 # CHDR header
- name: "CH1_Data_RX_GPU"
kind: "device"
affinity: 0
access: [local]
num_bufs: 12500
buf_size: 4096 # 1024 complex SC16 samples
interfaces:
- name: sdr_data
address: 0000:41:00.0
rx:
queues:
- name: "Channel 1 data"
id: 0
cpu_core: 10
batch_size: 2500
memory_regions:
- "Headers_RX_CPU" # segment 0
- "CH1_CHDR_Headers_RX_CPU" # segment 1
- "CH1_Data_RX_GPU" # segment 2
The second queue follows the same layout with CH2_CHDR_Headers_RX_CPU and CH2_Data_RX_GPU. Thus, each incoming packet is divided into:
- Ethernet, IP, and UDP headers in a CPU huge-page region.
- The CHDR header in a CPU huge-page region.
- The RF sample payload in a GPU device-memory region.
The batch_size must agree with uhd_chdr_rx batching. The current configuration uses 20 packets per output and 125 outputs per batch, so $20 \times 125 = 2500$ packets are collected per DAQIRI queue batch. UhdChdrRxOp gathers the segment-2 GPU pointers and converts the SC16 payload to complex float samples in a GPU tensor with shape:
[num_outputs_per_batch][num_packets_per_output * num_complex_samples_per_packet]
With the default settings, this is a 125 x 20480 complex-sample tensor per channel:
$20 \times 1024 = 20480$ samples per output and $125$ outputs per batch.
Output#
At runtime, Holoviz displays a live spectrum view with:
- one rendered line strip per configured RF channel,
- dB-scaled vertical axis labels,
- frequency labels along the horizontal axis,
- an ImGui control panel for center frequency and bandwidth, including a channel-color legend,
- an optional peak frequency / power readout.

[!NOTE] The screenshot above shows a single visible spectrum trace. With the 2-channel configuration, Holoviz renders one color-coded line strip per configured channel, and the control panel legend maps each color to its channel; the traces can overlap when both channels observe the same band.
Build & Run#
From the Holohub workspace root:
- Build the networking dev container:
./holohub build-container --docker-file pkg/holoscan-networking/Dockerfile --img holohub-networking:4.6.0 --dryrun --verbose
./holohub build-container --docker-file pkg/holoscan-networking/Dockerfile --img holohub-networking:4.6.0 --verbose
- Build the application:
./holohub build usrp_spectrum_viewer --docker-opts "-u root --privileged -v /mnt/huge:/mnt/huge" --dryrun --verbose
./holohub build usrp_spectrum_viewer --docker-opts "-u root --privileged -v /mnt/huge:/mnt/huge" --verbose
- Run the application:
./holohub run usrp_spectrum_viewer --docker-opts "-u root --privileged -v /mnt/huge:/mnt/huge --cpuset-cpus=0-11" --dryrun --verbose
./holohub run usrp_spectrum_viewer --docker-opts "-u root --privileged -v /mnt/huge:/mnt/huge --cpuset-cpus=0-11" --verbose
Troubleshooting#
Run on a local display, not a remote desktop#
Holoviz renders the live spectrum with Vulkan on the GPU over a remote desktop (VNC/RDP/X-forwarding), every rendered frame is copied off the GPU and streamed over the network, so the display becomes slow and choppy. For a smooth view, run the application on a monitor attached to the host. A remote session is fine for editing config and launching the app, but not for watching the live spectrum.
Known Issues#
- No runtime retuning of the USRP. The Holoviz
Center FreqandBandwidthcontrols are display-only (pan/zoom). They do not retune the radio once streaming has started. To change the actual RF tuning, editusrp_rx.freq/usrp_rx.rateinconfig.yamland restart the application. - Only 2 channels are tested. The color palette and legend support up to 4 channels, but the application has only been validated with 2. Running 4 channels also needs matching
config.yamlentries (extra DAQIRI queues,memory_regions, anddest_ports) and has not been verified.
Limitations#
- Hardware tested: USRP X410 and X440 with UHD 4.10 over a Mellanox ConnectX-6 Dx NIC. Other radios, NICs, or UHD versions may work but are unverified. The FPGA images used during testing were:
| USRP | FPGA image |
|---|---|
| X410 | CG_400 |
| X440 | CG_1600 |
-
Fixed bandwidth: the reference configuration assumes a 500 MHz sample rate (
spectrum_viz.ref_bandwidth_hz: 500000000). Using a different rate requires matching changes inusrp_rxandspectrum_viz. -
No FFT windowing: the FFT runs on the raw samples with no window function, so spectral leakage (side lobes) is present. Applying a window such as Hann or Blackman is not currently implemented.
Folder Summary#
usrp_rx/: Configures the USRP and starts or stops UHD streaming.uhd_chdr_rx/: Receives DAQIRI bursts and converts CHDR payloads into batched complex tensors.spectrum_magnitude/: Converts complex FFT output into averaged per-bin power in dB.spectrum_visualizer/: Converts dB power bins into Holoviz line geometry and reports peak values.spectrum_overlay/: Provides the Holoviz callback for ImGui controls, axis labels, and annotations.log/: Optional throughput and tensor inspection utilities.
Notes#
UsrpRxOpis a control operator. It starts the radio stream but does not emit tensors itself.UhdChdrRxOpis the packet-to-tensor bridge and is the main ingest point for RF sample data.SpectrumVisualizerOpemits both geometry tensors and matching Holoviz draw specifications.