matlab/matlab-transmit-capture-usrp
> 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.
npx skills add https://github.com/matlab/matlab-agentic-toolkit --skill matlab-transmit-capture-usrp
Generate, transmit, and capture RF waveforms using NI USRP radios in MATLAB.
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:
Default to basebandTransceiver. It supports all workflows — transmit only, capture only, or both simultaneously. Use it unless you have a specific reason not to.
| Object | When to Use |
|--------|-------------|
| basebandTransceiver | Default choice. Transmit, capture, or both. Works for loopback, signal generation, spectrum monitoring, and full-duplex |
| basebandTransmitter | Only when you need a dedicated transmit-only object (e.g., a separate script controlling TX independently) |
| basebandReceiver | Only 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:
| | basebandTransceiver | basebandTransmitter / basebandReceiver |
|--|-----|------|
| Gain | TransmitRadioGain, CaptureRadioGain | RadioGain |
| Frequency | TransmitCenterFrequency, CaptureCenterFrequency | CenterFrequency |
| Antennas | TransmitAntennas, CaptureAntennas | Antennas |
The transceiver uses Transmit/Capture prefixes to distinguish directions. The standalone objects do not need prefixes since they only handle one direction.
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
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:
| Property | Guidance |
|----------|----------|
| SampleRate | Must match waveform bandwidth. Common values: 30.72 MHz (LTE/NR), 61.44 MHz, 122.88 MHz, 245.76 MHz |
| TransmitRadioGain | Start low (10–20 dB) to avoid clipping. Increase until signal strength is adequate |
| CaptureRadioGain | Start moderate (30–40 dB). Too high clips the ADC; too low buries the signal in noise |
| TransmitCenterFrequency / CaptureCenterFrequency | Must be within the radio's supported range (device-dependent, typically 1 MHz – 6/8 GHz) |
| DroppedSamplesAction | Set to "warning" during development to continue despite drops; use "error" in production |
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:
| Requirement | Details |
|-------------|---------|
| Data type | Complex double, single, or int16 (controlled by TransmitDataType) |
| Amplitude | Peak magnitude ≤ 1.0 for double/single (values > 1 clip at the DAC) |
| Dimensions | Column vector (single antenna) or N×M matrix (M = number of TX antennas) |
| Row count | Must be an even number of rows |
| Minimum length | Waveforms < 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
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:
Stop continuous transmission:
stopTransmission(bbtrx);
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:
| Output | Type | Description |
|--------|------|-------------|
| data | complex vector/matrix | IQ samples — rows = samples, columns = antennas. Data type matches CaptureDataType property. First samples may contain transients |
| timestamp | datetime | Timestamp created immediately before hardware capture request |
| droppedSamples | logical | true 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:
Use foreground capture (the default) when:
Choosing the right pattern:
| Scenario | Pattern |
|----------|---------|
| Short capture, need data now | [data,~,dropped] = capture(bbtrx, milliseconds(100)); |
| Long capture, data fits in RAM | capture(bbtrx, seconds(30), Background=true); then captureOutputs |
| Long capture, too large for RAM | capture(bbtrx, seconds(60), Background=true, SaveLocation="data.mat", UseRadioBuffer=false); |
| Need notification when done | Add 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:
Use UseRadioBuffer=false when:
Onboard buffer capacities (determines when direct-to-host is required):
| Device | Max Samples | Approx Duration at 30.72 MHz |
|--------|-------------|------------------------------|
| USRP E320 / N-series | 2^29 (~537M) | ~17.5 s |
| USRP X300 / X310 | 2^28 (~268M) | ~8.7 s |
| USRP X410 | 2^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);
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);
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:
| Device | TransmitAntennas | CaptureAntennas |
|--------|------------------|-----------------|
| 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.
| Symptom | Cause | Fix |
|---------|-------|-----|
| Dropped samples | Host/network cannot sustain throughput | Run radioSetupWizard to optimize host settings, or reduce sample rate / antenna count |
| Captured signal clipped | CaptureRadioGain too high | Reduce gain until peak magnitude < 0.9 |
| Captured signal in noise floor | CaptureRadioGain too low | Increase gain incrementally |
| TX signal distorted | Waveform amplitude > 1.0 | Normalize: waveform = waveform / max(abs(waveform)) |
| Multi-second delay on first call | FPGA application loading | Use Preload=true at construction |
| "Resource not available" error | Another object holds the radio | Clear existing objects: clear bbtrx |
| Capture length exceeds buffer | Onboard memory full | Use UseRadioBuffer=false for long captures |
| Background capture never finishes | Capture still running | Check isCapturing(), use stopCapture() if stuck |
| Function/Method | Purpose |
|-----------------|---------|
| basebandTransceiver | Create radio object (default — handles TX, RX, or both) |
| basebandTransmitter | Create TX-only radio object (standalone use only) |
| basebandReceiver | Create 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 |
| radioConfigurations | List/load saved radio configurations |
basebandTransceiver. Use it for transmit-only, capture-only, or both. Only use basebandTransmitter/basebandReceiver when a separate script needs independent single-direction control.transmit(..., "continuous") first, then capture. This ensures the signal is present when capture begins.stopTransmission when done to release radio resources.capture — it is logical (true = drops occurred). If true, run radioSetupWizard to optimize host settings or reduce sample rate.captureOutputs until isCapturing returns false or stopCapture has been called. Calling it while capture is still running will error.milliseconds(N) or seconds(N) over raw sample counts for readability and portability across sample rates.clear on radio objects when finished to release hardware for other applications.----
Copyright 2026 The MathWorks, Inc.
----
Take matlab/matlab-transmit-capture-usrp from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.