Transmit and Capture Waveforms with Wireless Testbench

SkillDev tools

Transmit and capture RF waveforms using Wireless Testbench with NI USRP radios (X410, X310, N310, N320, N321, N300, X300, E320). Use when generating test signals, transmitting over the air, capturing IQ data, performing loopback tests, configuring multi-antenna setups, or troubleshooting dropped samples and gain settings. Covers basebandTransceiver, basebandTransmitter, basebandReceiver, continuous and once transmit modes, foreground and background capture, and UseRadioBuffer options. Also use when the user mentions transmit waveform, capture signal, IQ data, loopback, RF gain, sample rate, or antenna configuration.

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Transmit and Capture Waveforms with Wireless Testbench skill

What this skill tells your AI

The instructions your AI receives, as published by matlab/matlab-agentic-toolkit in skills-catalog/wireless-communications/matlab-transmit-capture-usrp/SKILL.md and read by ahel’s review.

Generate, transmit, and capture RF waveforms using NI USRP radios in MATLAB.

When to Use

  • Transmitting a waveform (test tone, standard-compliant signal, custom IQ) over the air
  • Capturing IQ data from a frequency band
  • Loopback testing (transmit and capture on the same radio)
  • Multi-antenna transmit or capture
  • Continuous transmission for prolonged testing
  • Background capture for long-duration acquisition

When NOT to Use

  • Setting up or connecting a USRP radio for the first time — use the matlab-set-up-usrp-radio skill
  • FPGA targeting workflows — see Target NI USRP Radios
  • Intelligent capture with preamble/energy detection — see Spectrum Monitoring
  • Multi-device synchronization — see Radio Management

Prerequisites

A saved radio configuration must exist before using any transmit/capture object. List available configurations and let the user choose which one to use:

configs = radioConfigurations;
disp(configs)

If multiple configurations exist, ask the user which one they want to work with. If no configuration exists, guide the user to set one up first (see the matlab-set-up-usrp-radio skill).

Before generating code, confirm these parameters with the user if not already specified:

  • Center frequency — Accept any numeric value within the radio's supported range. Do not present a constrained list of bands; let the user type a frequency.
  • Sample rate — Ask what sample rate they need. Suggest common values (30.72 MHz, 61.44 MHz, 122.88 MHz, 245.76 MHz) but accept any valid rate.

Choosing the Right Object

Default to basebandTransceiver. It supports all workflows — transmit only, capture only, or both simultaneously. Use it unless you have a specific reason not to.

ObjectWhen to Use
basebandTransceiverDefault choice. Transmit, capture, or both. Works for loopback, signal generation, spectrum monitoring, and full-duplex
basebandTransmitterOnly when you need a dedicated transmit-only object (e.g., a separate script controlling TX independently)
basebandReceiverOnly when you need a dedicated receive-only object (e.g., a separate script controlling RX independently)

Why default to basebandTransceiver: A single radio configuration can only be used by one object at a time. Since basebandTransceiver handles transmit, capture, or both, it covers the vast majority of workflows without needing to switch objects. The standalone objects (basebandTransmitter, basebandReceiver) are useful when separate scripts or applications each need independent control of one direction.

Property naming differs between object types:

basebandTransceiverbasebandTransmitter / basebandReceiver
GainTransmitRadioGain, CaptureRadioGainRadioGain
FrequencyTransmitCenterFrequency, CaptureCenterFrequencyCenterFrequency
AntennasTransmitAntennas, CaptureAntennasAntennas

The transceiver uses Transmit/Capture prefixes to distinguish directions. The standalone objects do not need prefixes since they only handle one direction.

Workflow

Step 1: Create the Radio Object

radio = radioConfigurations("MyN310");
bbtrx = basebandTransceiver(radio);

Preload option: Pass Preload=true to load the FPGA application at construction time rather than on first use. This avoids a multi-second delay on the first transmit or capture call.

bbtrx = basebandTransceiver(radio, Preload=true);

Standalone objects (only when needed for independent single-direction control):

bbtx = basebandTransmitter(radio);  % TX only
bbrx = basebandReceiver(radio);     % RX only

Step 2: Configure Properties

Configure the object before transmitting or capturing. Setting properties after object creation is valid, but some changes cause a reload delay.

bbtrx.SampleRate = 30.72e6;
bbtrx.TransmitCenterFrequency = 3.5e9;
bbtrx.CaptureCenterFrequency = 3.5e9;
bbtrx.TransmitRadioGain = 20;
bbtrx.CaptureRadioGain = 40;

Only set the properties for the direction you need. For transmit-only workflows, skip the Capture* properties (and vice versa).

If using standalone objects (basebandTransmitter / basebandReceiver), properties are unprefixed:

bbtx.SampleRate = 30.72e6;
bbtx.CenterFrequency = 3.5e9;    % not TransmitCenterFrequency
bbtx.RadioGain = 20;              % not TransmitRadioGain

Property guidance:

PropertyGuidance
SampleRateMust match waveform bandwidth. Common values: 30.72 MHz (LTE/NR), 61.44 MHz, 122.88 MHz, 245.76 MHz
TransmitRadioGainStart low (10–20 dB) to avoid clipping. Increase until signal strength is adequate
CaptureRadioGainStart moderate (30–40 dB). Too high clips the ADC; too low buries the signal in noise
TransmitCenterFrequency / CaptureCenterFrequencyMust be within the radio's supported range (device-dependent, typically 1 MHz – 6/8 GHz)
DroppedSamplesActionSet to "warning" during development to continue despite drops; use "error" in production

Step 3: Prepare the Transmit Waveform

The waveform must be a complex column vector (single antenna) or complex matrix (multi-antenna, one column per antenna). Values must be normalized to the range [-1, 1].

Test tone:

numSamples = 30720;
t = (0:numSamples-1)' / bbtrx.SampleRate;
txWaveform = 0.8 * exp(1j*2*pi*1e6*t);

Random OFDM-like signal:

numSamples = 30720;
txWaveform = complex(randn(numSamples,1), randn(numSamples,1));
txWaveform = 0.7 * txWaveform / max(abs(txWaveform));

Load a pre-generated waveform from file:

waveStruct = load("myWaveform.mat");
txWaveform = waveStruct.waveform;
txWaveform = 0.8 * txWaveform / max(abs(txWaveform));

Use this pattern with waveforms generated by 5G Toolbox, LTE Toolbox, WLAN Toolbox, or any custom signal generation workflow.

Waveform requirements:

RequirementDetails
Data typeComplex double, single, or int16 (controlled by TransmitDataType)
AmplitudePeak magnitude ≤ 1.0 for double/single (values > 1 clip at the DAC)
DimensionsColumn vector (single antenna) or N×M matrix (M = number of TX antennas)
Row countMust be an even number of rows
Minimum lengthWaveforms < 513 samples reserve up to 1024 samples for underflow protection

Pass TransmitDataType to the constructor when using Preload=true:

The FPGA application is configured for a specific transmit data class at construction time. Without preload, the class is inferred from the waveform on the first transmit call. With preload, the transceiver has no waveform to infer from, so pass TransmitDataType explicitly — otherwise the FPGA reloads on the first transmit call and defeats the point of preloading.

bbtrx = basebandTransceiver(radio, Preload=true, TransmitDataType="double");
% ... configure frequency/gain/sample rate ...
transmit(bbtrx, txWaveform, "continuous");   % waveform class must match TransmitDataType

Step 4: Transmit

Once (single-shot) — transmit the waveform exactly once:

transmit(bbtrx, txWaveform, "once");

The radio transmits the waveform one time and then stops automatically. Use for pulsed or one-shot testing. Add a few redundant samples to the end of the waveform for reliability in this mode.

Continuous — transmit repeatedly until stopped:

transmit(bbtrx, txWaveform, "continuous");

The waveform loops continuously on the radio until stopTransmission is called. Use for:

  • Sustained interference or signal generation
  • Loopback testing where capture timing is decoupled from transmit
  • Over-the-air tests where a receiver needs a persistent signal

Stop continuous transmission:

stopTransmission(bbtrx);

Step 5: Capture

Foreground capture (blocks MATLAB until complete):

[data, timestamp, droppedSamples] = capture(bbtrx, milliseconds(10));

The length argument accepts a duration value (e.g., seconds(1), milliseconds(10)) or a sample count as a positive integer.

Output arguments:

OutputTypeDescription
datacomplex vector/matrixIQ samples — rows = samples, columns = antennas. Data type matches CaptureDataType property. First samples may contain transients
timestampdatetimeTimestamp created immediately before hardware capture request
droppedSampleslogicaltrue if samples were dropped (network/host issue), false if clean

CaptureDataType defaults to int16 — cast before FFT, filtering, or arithmetic:

The captured data is a complex vector/matrix whose class matches CaptureDataType. The default is "int16" (fixed-point) to minimize memory. Most MATLAB signal-processing functions — including fft, abs, filter, bandpass, pwelch, and element-wise math — do not accept complex int16 and will error at runtime. Handle this in one of two ways:

% Option A — set the property before capture (all downstream code sees double)
bbtrx.CaptureDataType = "double";
[data, ~, dropped] = capture(bbtrx, milliseconds(10));
X = fft(data);   % works

% Option B — cast after capture (keep the memory savings during transfer)
[data, ~, dropped] = capture(bbtrx, milliseconds(10));
data = double(data);
X = fft(data);   % works

Use Option A by default. Only prefer Option B when memory during capture is tight and you want the compact int16 payload until processing begins.

Background capture (non-blocking):

Use background capture when:

  • Capture duration is long (> a few seconds) and MATLAB must remain responsive
  • You want to do post-processing, display updates, or other work during acquisition
  • You are combining capture with continuous transmit and need to monitor both

Use foreground capture (the default) when:

  • Capture is short and you need the data immediately for the next step
  • The script is linear (no concurrent work needed)

Choosing the right pattern:

ScenarioPattern
Short capture, need data now[data,~,dropped] = capture(bbtrx, milliseconds(100));
Long capture, data fits in RAMcapture(bbtrx, seconds(30), Background=true); then captureOutputs
Long capture, too large for RAMcapture(bbtrx, seconds(60), Background=true, SaveLocation="data.mat", UseRadioBuffer=false);
Need notification when doneAdd CompletionFcn=@(data,ts,dropped) myCallback(data)

Background capture with polling:

capture(bbtrx, seconds(30), Background=true);

% Poll until complete
while isCapturing(bbtrx)
    pause(1);
end

% Retrieve results
[data, timestamp, droppedSamples] = captureOutputs(bbtrx);

Background capture with callback (no polling needed):

capture(bbtrx, seconds(30), Background=true, ...
    CompletionFcn=@(data, ts, dropped) handleCapture(data, dropped));

Long capture to file (background + SaveLocation + direct-to-host):

For captures that exceed onboard buffer or RAM, combine all three options:

capture(bbtrx, seconds(60), ...
    Background=true, ...
    SaveLocation="captured_data.mat", ...
    UseRadioBuffer=false);

while isCapturing(bbtrx)
    pause(5);
end
filePath = captureOutputs(bbtrx);
fprintf("Saved to: %s\n", filePath);

When SaveLocation is specified, captureOutputs returns the file path instead of loading data into the workspace.

Stop a background capture early:

stopCapture(bbtrx);
[data, timestamp, droppedSamples] = captureOutputs(bbtrx);

UseRadioBuffer — choosing between radio buffer and direct-to-host:

The UseRadioBuffer name-value argument controls where captured samples are stored during acquisition. The right choice depends on capture length, sample rate, and host network capability.

% Radio buffer (default) — data is stored in onboard radio memory, then
% transferred to the host after capture completes. Most reliable option.
[data, ~, dropped] = capture(bbtrx, milliseconds(100));

% Direct-to-host — data streams continuously from radio to host over the
% network during the capture. Required when capture exceeds onboard memory.
[data, ~, dropped] = capture(bbtrx, seconds(30), UseRadioBuffer=false);

Use UseRadioBuffer=true (default) when:

  • Capture length fits within the radio's onboard memory
  • You want the most reliable capture (no dependency on sustained host throughput)
  • Sample rate is high and you cannot tolerate any drops

Use UseRadioBuffer=false when:

  • Capture length exceeds the radio's onboard buffer capacity (see table below)
  • Using USRP X310 with TwinRX daughterboard capturing more than 2 antenna channels
  • You need arbitrarily long captures (minutes or hours of data)

Onboard buffer capacities (determines when direct-to-host is required):

DeviceMax SamplesApprox Duration at 30.72 MHz
USRP E320 / N-series2^29 (~537M)~17.5 s
USRP X300 / X3102^28 (~268M)~8.7 s
USRP X4102^30 (~1.07B)~34.8 s

Direct-to-host performance considerations:

Direct-to-host capture requires sustained network throughput for the entire capture duration. The maximum achievable sample rate depends on host and network configuration and varies between runs depending on system load. If drops occur, either reduce the sample rate, reduce the number of antennas, or run radioSetupWizard to optimize host network settings for your platform.

Evaluating host performance: To find the maximum sustainable direct-to-host rate, iterate captures at decreasing sample rates with DroppedSamplesAction="none" until one succeeds:

bbrx.DroppedSamplesAction = "none";
sampleRates = 245.76e6 : -10e6 : 10e6;
for idx = 1:numel(sampleRates)
    bbrx.SampleRate = sampleRates(idx);
    [~, ~, dropped] = capture(bbrx, 2*2^28, UseRadioBuffer=false);
    if ~dropped
        fprintf("Max sustained rate: %.1f MHz\n", sampleRates(idx)/1e6);
        break
    end
end

Stop a background capture early:

stopCapture(bbtrx);
[data, timestamp, droppedSamples] = captureOutputs(bbtrx);

Step 6: Loopback Test (Transmit + Capture on Same Radio)

A loopback test verifies the full TX/RX chain using basebandTransceiver. Set TransmitCenterFrequency and CaptureCenterFrequency to the same value so the signal couples internally.

Method A: Continuous transmit then capture

Start continuous transmission, allow a brief pause for the radio front-end to stabilize, then capture.

radio = radioConfigurations("MyN310");
bbtrx = basebandTransceiver(radio, Preload=true);
bbtrx.SampleRate = 61.44e6;
bbtrx.TransmitCenterFrequency = 2.4e9;
bbtrx.CaptureCenterFrequency = 2.4e9;
bbtrx.TransmitRadioGain = 10;
bbtrx.CaptureRadioGain = 30;

% Generate test tone
numSamples = 61440;
t = (0:numSamples-1)' / bbtrx.SampleRate;
txWaveform = 0.8 * exp(1j*2*pi*1e6*t);

% Transmit continuously, pause for stabilization, then capture
transmit(bbtrx, txWaveform, "continuous");
pause(1);
[rxData, ~, droppedSamples] = capture(bbtrx, milliseconds(10));
stopTransmission(bbtrx);

% Verify
if ~droppedSamples
    fprintf("Loopback OK: captured %d samples, no drops.\n", size(rxData,1));
else
    warning("Samples were dropped during loopback.");
end

% Visualize
sa = spectrumAnalyzer(SampleRate=bbtrx.SampleRate);
sa(rxData);

Step 7: Multi-Antenna Configuration

For MIMO or multi-channel operation, set antenna properties to arrays and provide a matrix waveform.

radio = radioConfigurations("MyX410");
bbtrx = basebandTransceiver(radio);
bbtrx.SampleRate = 61.44e6;

% Configure 2 TX and 2 RX antennas
bbtrx.TransmitAntennas = ["DB0:RF0:TX/RX0", "DB0:RF1:TX/RX0"];
bbtrx.CaptureAntennas = ["DB0:RF0:RX1", "DB0:RF1:RX1"];
bbtrx.TransmitCenterFrequency = [3.5e9, 3.5e9];
bbtrx.CaptureCenterFrequency = [3.5e9, 3.5e9];
bbtrx.TransmitRadioGain = [15, 15];
bbtrx.CaptureRadioGain = [35, 35];

% Waveform: N×2 matrix (one column per TX antenna)
numSamples = 61440;
t = (0:numSamples-1)' / bbtrx.SampleRate;
tx1 = 0.7 * exp(1j*2*pi*1e6*t);
tx2 = 0.7 * exp(1j*2*pi*2e6*t);
txWaveform = [tx1, tx2];

transmit(bbtrx, txWaveform, "continuous");
[rxData, ~, dropped] = capture(bbtrx, milliseconds(10));
stopTransmission(bbtrx);

% rxData is N×2: one column per RX antenna
fprintf("Captured %d samples on %d antennas.\n", size(rxData,1), size(rxData,2));

Antenna naming varies by device — TX and RX ports have different names:

DeviceTransmitAntennasCaptureAntennas
X410"DB0:RF0:TX/RX0", "DB0:RF1:TX/RX0", "DB1:RF0:TX/RX0", "DB1:RF1:TX/RX0""DB0:RF0:RX1", "DB0:RF1:RX1", "DB1:RF0:RX1", "DB1:RF1:RX1"
X310 (UBX)"RFA:TX/RX", "RFB:TX/RX""RFA:RX2", "RFB:RX2"
N310"RF0:TX/RX", "RF1:TX/RX", "RF2:TX/RX", "RF3:TX/RX""RF0:RX2", "RF1:RX2", "RF2:RX2", "RF3:RX2"
N320/N321"RF0:TX/RX", "RF1:TX/RX""RF0:RX2", "RF1:RX2"
E320"RFA:TX/RX""RFA:RX2", "RFB:RX2"

Assign an invalid value to see the full list of accepted antenna names for your device.

Troubleshooting

SymptomCauseFix
Dropped samplesHost/network cannot sustain throughputRun radioSetupWizard to optimize host settings, or reduce sample rate / antenna count
Captured signal clippedCaptureRadioGain too highReduce gain until peak magnitude < 0.9
Captured signal in noise floorCaptureRadioGain too lowIncrease gain incrementally
TX signal distortedWaveform amplitude > 1.0Normalize: waveform = waveform / max(abs(waveform))
Multi-second delay on first callFPGA application loadingUse Preload=true at construction
"Resource not available" errorAnother object holds the radioClear existing objects: clear bbtrx
Capture length exceeds bufferOnboard memory fullUse UseRadioBuffer=false for long captures
Background capture never finishesCapture still runningCheck isCapturing(), use stopCapture() if stuck

Key Functions Reference

Function/MethodPurpose
basebandTransceiverCreate radio object (default — handles TX, RX, or both)
basebandTransmitterCreate TX-only radio object (standalone use only)
basebandReceiverCreate RX-only radio object (standalone use only)
transmit(obj, waveform, "once")Single-shot transmit
transmit(obj, waveform, "continuous")Continuous transmit (loops until stopped)
stopTransmission(obj)Stop continuous transmission
capture(obj, length)Foreground capture (blocking)
capture(obj, length, Background=true)Background capture (non-blocking)
captureOutputs(obj)Retrieve background capture results
isCapturing(obj)Check if background capture is running
stopCapture(obj)Stop a background capture
radioConfigurationsList/load saved radio configurations

Conventions

  • Default to basebandTransceiver. Use it for transmit-only, capture-only, or both. Only use basebandTransmitter/basebandReceiver when a separate script needs independent single-direction control.
  • Normalize waveforms. Always scale to peak magnitude ≤ 0.8 to leave headroom and avoid DAC clipping.
  • Start with low gain. Begin TX gain at 10–20 dB and RX gain at 30–40 dB; adjust based on observed signal levels.
  • Continuous transmit before capture. For loopback, start transmit(..., "continuous") first, then capture. This ensures the signal is present when capture begins.
  • Always stop transmission. Call stopTransmission when done to release radio resources.
  • Check dropped samples. Always inspect the third output of capture — it is logical (true = drops occurred). If true, run radioSetupWizard to optimize host settings or reduce sample rate.
  • Wait before retrieving outputs. Never call captureOutputs until isCapturing returns false or stopCapture has been called. Calling it while capture is still running will error.
  • Use duration for capture length. Prefer milliseconds(N) or seconds(N) over raw sample counts for readability and portability across sample rates.
  • Clean up objects. Call clear on radio objects when finished to release hardware for other applications.

Copyright 2026 The MathWorks, Inc.


Signals

GitHub stars
1k
Forks
135
Last commit
Sep 2026
Advanced
Item type
skill
Key
matlab-transmit-capture-usrp
Source
github.com/matlab/matlab-agentic-toolkit