Generated banner for Matlab Generate 5g Waveform
Generated logo for Matlab Generate 5g Waveform

Matlab Generate 5g Waveform

MathWorksOfficialCreated by MathWorks

  • Skill
  • Tool integration
  • Software development
  • BSD-2-Clause
  • No issues found

From the author

Generate 3GPP-compliant 5G NR downlink and uplink baseband waveforms. Use to create NR signals, test model (TM) waveforms, fixed reference channels (FRC), test and measurement (T&M) signals, or test vectors for conformance testing. Covers configuring data, control, and broadcast channels and signals: PDSCH, PUSCH, PDCCH, PUCCH, SRS, SSBurst, CSI-RS, DM-RS, PT-RS, CORESET, and BWP parameters including bandwidth, subcarrier spacing (SCS), modulation (QPSK, QAM), numerology, FR1, FR2, TDD, FDD, and multi-bandwidth-part setups. Use for signal generation, RF instrument playback, or IQ baseband synthesis. Requires 5G Toolbox.
— the description by MathWorks

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured approach to generating standard-compliant 5G New Radio (NR) baseband waveforms using the nrWaveformGenerator function. It covers the configuration of both downlink and uplink channels, including PDSCH, PUSCH, PDCCH, and various reference signals. The workflow emphasizes using simplified constructors for standard configurations while providing guidance for manual adjustments in complex scenarios like multi-bandwidth-part or multi-numerology layouts. It is designed for engineers performing conformance testing, RF instrument playback, or IQ baseband synthesis.

What it does

  • Generates 3GPP-compliant 5G NR downlink and uplink waveforms.
  • Configures data, control, and broadcast channels including PDSCH, PUSCH, PDCCH, and PUCCH.
  • Supports complex setups like multi-bandwidth-part and multi-numerology configurations.
  • Provides methods for creating standard test models (TM) and fixed reference channels (FRC).
  • Handles channel multiplexing, power scaling, and OFDM modulation automatically.
  • Enables visualization of resource grids for verification.

When to use it

  • When you need to generate 5G NR signals for test and measurement or simulation.
  • When creating test vectors for conformance testing.
  • When you need to configure specific bandwidth, subcarrier spacing, or modulation parameters.
  • When you need to generate standard-compliant test model (TM) or fixed reference channel (FRC) waveforms.

Advantages

  • Ensures 3GPP compliance through standardized configuration objects.
  • Simplifies complex waveform generation with automated constructor defaults.
  • Provides a consistent API for both simple and advanced multi-carrier layouts.
  • Reduces configuration errors by separating primitive signal processing from waveform construction.

Usage examples

“Generate a default 5G NR downlink waveform for a 20 MHz channel with 30 kHz subcarrier spacing.”
The agent creates a configuration object using nrDLCarrierConfig('FR1', 20, 30), generates the waveform with nrWaveformGenerator, and displays the resource grid.
“Create a 5G NR test model TM1.1 waveform.”
The agent uses the hNRReferenceWaveformGenerator helper to set up the TM1.1 configuration, generates the waveform, and provides the resource grid information.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/matlab/matlab-generate-5g-waveform/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip matlab-generate-5g-waveform.zip -d .agents/skills

Documentation by MathWorks

Generate 5G NR Waveforms

Generate standard-compliant 5G NR downlink and uplink waveforms for test and measurement, simulation, and verification using nrWaveformGenerator.

When to Use

  • Generate a 5G, NR, or New Radio waveform (DL: PDSCH, PDCCH, SSBurst, CSI-RS; UL: PUSCH, PUCCH, SRS)

  • Generate NR test model (TM) or fixed reference channel (FRC) waveforms

  • Configure bandwidth, SCS, modulation, power, or multi-bandwidth-part layouts

When Not to Use

  • Channel modeling or propagation — use nrCDLChannel, nrTDLChannel

  • Receiver processing or decoding — use nrPDSCHDecode, nrDLSCHDecoder

  • Link-level simulation end-to-end

API Choice

NeedAPI
Custom DL waveformnrDLCarrierConfig + nrWaveformGenerator
Custom UL waveformnrULCarrierConfig + nrWaveformGenerator
Standard test model (TM) or FRChNRReferenceWaveformGenerator

Do not use primitive-level functions (nrCarrierConfig + nrPDSCH + nrOFDMModulate) for waveform generation, and do not mix primitive config objects (nrPDSCHConfig, nrPUSCHConfig, nrCarrierConfig) with nrWaveformGenerator — use the nrWavegen* objects. nrWaveformGenerator handles channel multiplexing, power scaling, and OFDM modulation correctly. Full function catalog, property names, and the incompatible-object table: references/api-reference.md.

Core Workflow (custom DL/UL)

  1. Create the carrier config with the simplified constructor (R2026a+), which auto-sizes SCSCarriers, BandwidthParts, SSBurst/CORESET (DL), and a default PDSCH/PUSCH to the bandwidth:

cfg = nrDLCarrierConfig('FR1', 20, 30);  % DL: FR1, 20 MHz, 30 kHz SCS
cfg = nrULCarrierConfig('FR1', 20, 15);  % UL: FR1, 20 MHz, 15 kHz SCS

For DL, if the requested SCS differs from the default SSB numerology the constructor adds a dedicated SSB SCS carrier (e.g. nrDLCarrierConfig('FR1', 5, 30) adds a hidden 15 kHz carrier). Pre-R2026a, or to control the SCS-carrier count exactly, build manually with nrDLCarrierConfig (no args).

  1. Customize only what the task requires (see the rule below). Set non-defaults such as cfg.PDSCH{1}.Modulation = '64QAM';. Never overwrite a constructor-placed value with a different one (e.g. forcing NStartBWP = 0 when the constructor centered it) — that breaks the validated layout.

  2. Generate — validateConfig checks structural rules but does not catch channel conflicts (overlapping PDSCH/CSI-RS, CSI-RS/SSBurst); always run the generator, which returns the waveform and the resource-grid info:

[waveform, info] = nrWaveformGenerator(cfg);

What the simplified constructor builds is generation-ready as-is — an unedited nrDLCarrierConfig(fr, bw, scs) or nrULCarrierConfig(fr, bw, scs) generates for every FR1/FR2 bandwidth/SCS combination, so do not re-check what it set. This does not apply to the no-argument nrDLCarrierConfig / nrULCarrierConfig, whose defaults (15 kHz, Case A SSB, 52-RB PRBSet) must be resized by hand. One silent caveat: at 3 MHz / 15 kHz the SSB is wider than the 15-RB carrier, so the PBCH is punctured — this is expected; keep the SSB (details in Critical Rules). Check the Critical Rules for whatever you change or build by hand: a no-argument (manual) carrier, an edited NSizeGrid/BWP/CORESET/PRBSet, a BlockPattern override, an added SCS carrier or BWP (Point A centering), CSI-RS or PDCCH across BWPs, or 2 codewords (layers/DM-RS). Those edits cause the most generation failures.

Set Only What the Task Requires

Set every property the task requires — including parameters forced by a requested feature, even when the prompt never names them — and nothing else. For example, 2 codewords → NumLayers 5–8 → a DM-RS that supports more than 4 ports (see Critical Rules); a requested CSI-RS → CSIRS{1}.Enable = true (off by default). These are required, not over-config. SSBurst is already enabled by default, so a requested SS burst needs no Enable line.

Recipes and References

Open the reference that matches the task; each has copy-ready, verified code:

  • Common single-carrier waveforms (Basic DL with dual-grid plot, Basic UL, power spectrum): references/patterns.md

  • Narrow-band 11-RB carriers, manual build only (5 MHz/30 kHz, 10 MHz/60 kHz). The simplified constructor already handles these — skip this reference when you use it. Open it only for a no-argument build (pre-R2026a, or to avoid the auto-added SSB carrier), where the defaults fail and need NSizeGrid = 11, a disabled/rehomed SSB, a shrunk CORESET, AL1, and an explicit PRBSet: references/narrow-band-manual-config.md

  • Multi-BWP / multi-numerology layouts: references/multi-bwp-guidance.md

  • Test models (TM) and fixed reference channels (FRC) — use the example helper hNRReferenceWaveformGenerator(RC, BW, SCS) (e.g. 'DL-FRC-FR1-64QAM', 'NR-FR1-TM1.1'), then generateWaveform + displayResourceGrid. It is not on the path by default: first make it available with a portable setup (setupExample('5g/NRTestModelWaveformGenerationExample', ...) + copyfile + cd, or openExample) — never a hardcoded path, never a made-up namespaced name. Valid names, portable-setup block, and makeConfigWritable edits: references/test-models-and-frc.md

  • Full API catalog + output structure (fields of info, correct property names): references/api-reference.md

  • Online documentation links: references/documentation-links.md

Conventions

  • After generating a waveform, unless the user asks for something specific:

    1. Tell the user the waveform variable name and that it is in the workspace

    2. Plot the resource grid from info.ResourceGrids(k).ResourceGridBWP (BWP-sized, one per BWP); use ResourceGridInCarrier only for a full-carrier view. The SS burst is a separate grid — when SSBurst.Enable is true and the SS burst is asked for, also plot info.ResourceGridSSBurst.ResourceGrid (it is not part of ResourceGridBWP)

    3. Save the generation code as a .m script and open it in the MATLAB editor with edit('scriptName.m')

  • Start with the simplified constructor nrDLCarrierConfig('FR1', bw, scs) when possible

  • Correct property names: DMRSPower (not PowerDMRS), PTRSPower (not PowerPTRS), NSizeGrid (not NRB), ChannelBandwidth (not Bandwidth)

  • SCSCarriers, BandwidthParts, CORESET, PDSCH, PUSCH are cell arrays — use {}; SSBurst is a direct object — use . not {}

  • Use tiledlayout/nexttile for multi-panel figures; always label axes with units and include figure titles

  • Comment non-obvious, interdependent choices (why a given NSizeGrid, why SSBurst is disabled, why a specific AggregationLevel) so the code explains itself

Copyright 2026 The MathWorks, Inc.

Files

references/multi-bwp-guidance.md

Multi-BWP Waveform Generation

Generate waveforms with multiple bandwidth parts at different numerologies. This is the most complex configuration pattern because of interdependencies between SCS carriers, BWPs, and channel allocations.

Key Constraints

  1. Each BWP needs its own SCS carrier at the matching subcarrier spacing

  2. Each BWP must fit within its SCS carrier (see BWP containment rule)

  3. Channels in different BWPs must not overlap in time-frequency resources

  4. BWP IDs must be unique across all bandwidth parts

  5. CORESET is only needed in the BWP that carries PDCCH

Separating the two BWPs: FDD (preferred), TDD, Hybrid

Two BWPs on co-located carriers share the same physical spectrum, so their channels will conflict unless you separate them. There are three ways to do it. Prefer FDD — it is the simplest to reason about and the most realistic.

StrategyHow BWPs are separatedChannels can run…When to use
FDD (preferred)In frequency: each BWP occupies a different sub-band (offset NStartBWP, reduced NSizeBWP). Carriers stay where the constructor put them.in every slot — no time coordinationDefault. Simplest and most realistic.
TDDIn time: both BWPs stay full-band (overlap in frequency) and are scheduled in disjoint slots.only in disjoint slotsYou specifically need each BWP to span the full channel.
HybridIn both: BWPs partially overlap in frequency; the overlap is resolved by scheduling in disjoint slots.full-band only where they don't overlap in timeWider BWPs than FDD allows, without full TDD time-sharing.

Why FDD is simplest: the constructor already produced legal carrier NStartGrid/NSizeGrid values, so subdividing the band into two BWPs is a bounded adjustment within numbers that were already validated — you are not solving the Point-A centering constraint yourself. And because the sub-bands don't overlap, you never have to reason about slot durations across numerologies.

FDD sizing math. A BWP's physical position runs from NStartBWP to NStartBWP + NSizeBWP, measured in that carrier's RBs. A 15 kHz RB is half the width of a 30 kHz RB, so 108 15-kHz RBs cover the same bandwidth as 54 30-kHz RBs. To split a band at its midpoint: give the 15 kHz BWP the lower half and the 30 kHz BWP the upper half starting at the matching offset. Containment must still hold in each carrier's own SCS units: NStartGrid_carrier ≤ NStartBWP and NStartBWP + NSizeBWP ≤ NStartGrid_carrier + NSizeGrid.

Mind the 30 kHz carrier's NStartGrid. On R2026b the constructor sets the 30 kHz carrier to NStartGrid = 1, not 0. That offset centers the narrower 30 kHz carrier (106 RB = 38.16 MHz) on the same Point A as the wider 15 kHz carrier (216 RB = 38.88 MHz): the 0.72 MHz (2 RB) width difference is split as one RB of guard on each side. Because BWP offsets are measured in the carrier's own grid, the 30 kHz BWP must satisfy NStartBWP ≥ 1 and end at ≤ NStartGrid + NSizeGrid = 107. Read the carrier's real values rather than assuming a zero origin.

TDD/Hybrid time separation: reckon overlap in absolute time, not slot indices. A 30 kHz slot is half the duration of a 15 kHz slot, so SlotAllocation = 0:2 on a 15 kHz BWP (0–3 ms) and 6:9 on a 30 kHz BWP (3–5 ms) are disjoint even though the raw index ranges look like they overlap.

Choosing a build approach (by release)

Independently of the separation strategy, there are three ways to build a dual-numerology layout. Prefer the highest one your release supports — each one lower is more manual and more error-prone.

ApproachFloorWhy
SCS-list constructor nrDLCarrierConfig("FR1", bw, [15 30])R2026b+Builds both SCS carriers and both BWPs, sized to fill the channel. No manual sizing.
Seeded scalar constructor — scalar constructor seeded with the highest SCS, then add the lower-SCS carrier manuallyR2026aThe generator centers Point A on the highest-SCS carrier; letting the constructor own that carrier means you never fight the centering constraint. (R2026b+ has the SCS-list form above.)
Fully manual — default-construct, then assign two SCSCarriers + two BandwidthPartsR2024b+The only route before R2026a.

The centering constraint that dictates this ordering: nrWaveformGenerator positions Point A so the highest-SCS carrier is centered within the channel, then requires guardbands. Hand-sizing carriers that ignore this raises "ChannelBandwidth too small to fit all carriers." The constructor forms solve it for you; the seeded scalar form sidesteps it by letting the constructor place the highest-SCS (hardest-to-place) carrier.

Each build approach below leads with the FDD separation, then shows the TDD and Hybrid variants as short deltas.

Sizing and placing carriers by hand (seeded / manual builds)

When you add a lower-SCS carrier yourself, don't invent RB counts or offsets. Two rules make every value legal and reproducible:

1. Look up NSizeGrid per numerology from the bandwidth table. The maximum RB count for each SCS at a given channel bandwidth is tabulated — read it instead of guessing:

T = nrDLCarrierConfig.FR1BandwidthTable;   % FR2BandwidthTable for FR2
n15 = T{"15kHz","40MHz"};   % 216
n30 = T{"30kHz","40MHz"};   % 106

A 30 kHz carrier at 40 MHz is 106 RB, not 108 — it must have an even RB count and can't perfectly match the 15 kHz carrier's width. Reading the table makes that fall out automatically.

2. Center each lower-SCS carrier on the highest-SCS carrier's Point A. The generator fixes Point A on the highest-SCS carrier, so every other carrier's NStartGrid follows from it. This four-step rule gives the exact offsets the constructor would (and explains why the 30 kHz carrier lands at NStartGrid = 1 above):

  1. Point A → center, in the highest carrier's RBs: pointA = NSizeGrid_high/2 + NStartGrid_high.

  2. Ideal start of a lower carrier (ratio = SCS_high / SCS_low): NStartGrid_low = pointA*ratio − NSizeGrid_low/2.

  3. If that start is negative, the low carrier can't fit around the current Point A — shift the high carrier right by ceil(|NStartGrid_low|/ratio), recompute pointA, and redo step 2. (This is what nudges the 30 kHz carrier to NStartGrid = 1.)

  4. If the start is non-integer, the carrier would straddle a guard band — round it up (ceil) and drop one RB (NSizeGrid_low − 1).

Set each BWP's NStartBWP/NSizeBWP to match its carrier (or a sub-range of it, for an FDD split).


Preferred build (R2026b+): SCS-list constructor

Pass a vector of subcarrier spacings and the constructor builds a legal, channel-filling dual-numerology layout — both SCS carriers and both BWPs, each sized to the standard maximum for the channel. On R2026b, a 40 MHz FR1 channel yields:

C1: 15 kHz, NStartGrid=0, NSizeGrid=216   ([0.00, 38.88] MHz)
C2: 30 kHz, NStartGrid=1, NSizeGrid=106   ([0.36, 38.52] MHz, centered on Point A)
BWP1 / BWP2 initially fill their carriers (216 / 106 RB).
FDD (preferred): separate the BWPs in frequency

Shrink each BWP and offset the 30 kHz BWP so the two occupy adjacent sub-bands. The carriers stay exactly where the constructor placed them.

cfg = nrDLCarrierConfig("FR1", 40, [15 30]);   % two numerologies in one call

% Split the band at its midpoint (19.44 MHz). 108 15-kHz RBs == 54 30-kHz RBs.
% BWP1 (15 kHz): lower half. NStartBWP stays at its default 0, so only resize.
cfg.BandwidthParts{1}.NSizeBWP = 108;                                          % -> [0.00, 19.44] MHz
% BWP2 (30 kHz): upper half. Its carrier starts at NStartGrid=1, so offsets are
% in that grid: index 54 == 19.44 MHz; fill to the carrier end (grid 107).
cfg.BandwidthParts{2}.NStartBWP = 54;   cfg.BandwidthParts{2}.NSizeBWP = 53;   % -> [19.44, 38.52] MHz

% One PDSCH per BWP, each full-BWP. The sub-bands don't overlap in frequency,
% so no time coordination is needed — SlotAllocation stays at its default
% (every slot, 0:9), which needn't be restated.
cfg.PDSCH{2} = cfg.PDSCH{1};
cfg.PDSCH{2}.Label = 'PDSCH2';   % the copy keeps 'PDSCH1'; rename to tell them apart
cfg.PDSCH{2}.RNTI = 2;          % a PDSCH on another BWP models a second UE
cfg.PDSCH{1}.PRBSet = 0:cfg.BandwidthParts{1}.NSizeBWP-1;
cfg.PDSCH{2}.BandwidthPartID = 2;
cfg.PDSCH{2}.PRBSet = 0:cfg.BandwidthParts{2}.NSizeBWP-1;

% The constructor enables PDCCH on BWP1 by default; left as-is, its CORESET
% sits in BWP1's sub-band and PDSCH{1} rate-matches around it.

[waveform, info] = nrWaveformGenerator(cfg);
TDD variant: full-band BWPs, separate in time

Keep both BWPs at their full constructor size (they overlap in frequency) and schedule the two PDSCHs in disjoint slots instead.

cfg = nrDLCarrierConfig("FR1", 40, [15 30]);   % BWPs keep full size (216 / 106 RB)

% The default PDCCH repeats every slot (Period=1), so with full-band BWPs it
% would collide with BWP2's PDSCH. Confine it to BWP1's active window instead
% of disabling it.
cfg.PDCCH{1}.SlotAllocation = 0:2;
cfg.PDCCH{1}.Period = [];            % do not repeat into BWP2's slots

cfg.PDSCH{2} = cfg.PDSCH{1};
cfg.PDSCH{2}.Label = 'PDSCH2';
cfg.PDSCH{2}.RNTI = 2;
cfg.PDSCH{1}.PRBSet = 0:cfg.BandwidthParts{1}.NSizeBWP-1;
cfg.PDSCH{1}.SlotAllocation = 0:2;   % 15 kHz slots 0..2 = 0..3 ms
cfg.PDSCH{2}.BandwidthPartID = 2;
cfg.PDSCH{2}.PRBSet = 0:cfg.BandwidthParts{2}.NSizeBWP-1;
cfg.PDSCH{2}.SlotAllocation = 6:9;   % 30 kHz slots 6..9 = 3..5 ms

[waveform, info] = nrWaveformGenerator(cfg);
Hybrid variant: partial frequency overlap, resolved in time

Give each BWP more bandwidth than a clean FDD split allows; where they overlap in frequency, keep the PDSCHs in disjoint slots.

cfg = nrDLCarrierConfig("FR1", 40, [15 30]);
cfg.BandwidthParts{1}.NSizeBWP = 140;                                          % 15 kHz -> [0.00, 25.20] MHz (NStartBWP stays 0)
cfg.BandwidthParts{2}.NStartBWP = 40;   cfg.BandwidthParts{2}.NSizeBWP = 66;   % 30 kHz -> [14.40, 38.16] MHz
% Bands overlap over 14.40–25.20 MHz, so the two PDSCHs must not share slots.

% Keep the default PDCCH out of BWP2's slots (see the TDD note on Period).
cfg.PDCCH{1}.SlotAllocation = 0:2;
cfg.PDCCH{1}.Period = [];

cfg.PDSCH{2} = cfg.PDSCH{1};
cfg.PDSCH{2}.Label = 'PDSCH2';
cfg.PDSCH{2}.RNTI = 2;
cfg.PDSCH{1}.PRBSet = 0:cfg.BandwidthParts{1}.NSizeBWP-1;
cfg.PDSCH{1}.SlotAllocation = 0:2;
cfg.PDSCH{2}.BandwidthPartID = 2;
cfg.PDSCH{2}.PRBSet = 0:cfg.BandwidthParts{2}.NSizeBWP-1;
cfg.PDSCH{2}.SlotAllocation = 6:9;

[waveform, info] = nrWaveformGenerator(cfg);

Seeded scalar constructor (R2026a): highest SCS in the constructor

Below R2026b there is no SCS-list form, but you can still let the scalar constructor place the hardest carrier. Seed it with the highest SCS, then add the lower-SCS carrier and BWP manually around it. The example uses the preferred FDD separation; for TDD/Hybrid, size the BWPs full/partial as above and separate the PDSCHs in time instead.

cfg = nrDLCarrierConfig('FR1', 40, 30);   % highest SCS -> stays Point-A centered

% Size the 15 kHz carrier from the bandwidth table and place it with the
% centering rule (see "Sizing and placing carriers by hand").
n15   = nrDLCarrierConfig.FR1BandwidthTable{"15kHz","40MHz"};   % 216 RB
c30   = cfg.SCSCarriers{1};                                     % 30 kHz, NStartGrid=0, 106 RB
ratio = 30/15;

% Center 15 kHz on the 30 kHz carrier's Point A.
pointA   = c30.NSizeGrid/2 + c30.NStartGrid;
nStart15 = pointA*ratio - n15/2;                 % ideal start: -2 (does not fit)

% Step 3: negative start -> shift the 30 kHz carrier right, then recompute.
NStartGrid30 = c30.NStartGrid;
if nStart15 < 0
    NStartGrid30 = max(NStartGrid30, ceil(abs(nStart15)/ratio));   % -> 1
    pointA   = c30.NSizeGrid/2 + NStartGrid30;
    nStart15 = pointA*ratio - n15/2;                               % -> 0
end
% Step 4: non-integer start -> round up and drop an RB (not needed here).
nSize15 = n15;
if floor(nStart15) ~= nStart15
    nStart15 = ceil(nStart15);
    nSize15  = nSize15 - 1;
end
cfg.SCSCarriers{1}.NStartGrid = NStartGrid30;    % apply the shift

scs15 = nrSCSCarrierConfig;   % SubcarrierSpacing defaults to 15
scs15.NStartGrid = nStart15;
scs15.NSizeGrid  = nSize15;
cfg.SCSCarriers{end+1} = scs15;

% Matching 15 kHz BWP, sized to the lower half of the band (FDD).
bwp15 = nrWavegenBWPConfig;   % SubcarrierSpacing defaults to 15
bwp15.BandwidthPartID   = 2;
bwp15.NStartBWP = scs15.NStartGrid;
bwp15.NSizeBWP  = floor(scs15.NSizeGrid/2);
cfg.BandwidthParts{end+1} = bwp15;

% Shrink the 30 kHz BWP to the upper half (FDD).
c30 = cfg.SCSCarriers{1};   % re-read: NStartGrid now reflects the shift
cfg.BandwidthParts{1}.NStartBWP = c30.NStartGrid + floor(c30.NSizeGrid/2);
cfg.BandwidthParts{1}.NSizeBWP  = (c30.NStartGrid + c30.NSizeGrid) - cfg.BandwidthParts{1}.NStartBWP;

% PDSCH per BWP. FDD sub-bands don't overlap, so both run every slot — the
% default SlotAllocation (0:9) already does this and needn't be restated.
cfg.PDSCH{2} = cfg.PDSCH{1};
cfg.PDSCH{2}.Label = 'PDSCH2';
cfg.PDSCH{2}.RNTI = 2;
cfg.PDSCH{1}.PRBSet = 0:cfg.BandwidthParts{1}.NSizeBWP-1;
cfg.PDSCH{2}.BandwidthPartID = 2;
cfg.PDSCH{2}.PRBSet = 0:bwp15.NSizeBWP-1;
% PDCCH stays enabled on BWP1 (its default); PDSCH{1} rate-matches around it.

[waveform, info] = nrWaveformGenerator(cfg);

Fallback build (R2024b+): fully manual dual-carrier build

When calculating offsets, remember that NStartGrid for the 30 kHz carrier is in units of 30 kHz RBs. Two 15 kHz RBs occupy the same bandwidth as one 30 kHz RB.

SCS carriers can overlap in frequency (both NStartGrid = 0) or be separated with NStartGrid offsets. The example below uses an offset layout and sizes the BWPs so they occupy different frequency bands — i.e. the preferred FDD separation. For TDD/Hybrid, size the BWPs full/partial and separate the PDSCHs in time.

Example: 40 MHz, Two BWPs (15 kHz + 30 kHz), FDD
% Set only the properties this layout requires; FrequencyRange (FR1),
% NCellID (1), NumSubframes (10), NStartGrid/NStartBWP (0), and CORESET
% Duration (2) are left at their defaults rather than restated.
cfg = nrDLCarrierConfig;
cfg.ChannelBandwidth = 40;

% SCS Carrier 1: 15 kHz, lower half (SubcarrierSpacing defaults to 15,
% NStartGrid to 0 — neither is restated)
scsC1 = nrSCSCarrierConfig;
scsC1.NSizeGrid = 52;     % ~half of 40 MHz at 15 kHz (max 216)

% SCS Carrier 2: 30 kHz, upper half
scsC2 = nrSCSCarrierConfig;
scsC2.SubcarrierSpacing = 30;
scsC2.NStartGrid = 28;    % Offset so carriers occupy different frequency bands
scsC2.NSizeGrid = 38;

cfg.SCSCarriers = {scsC1, scsC2};

% BWP 1: uses 15 kHz SCS carrier (BandwidthPartID defaults to 1,
% SubcarrierSpacing to 15, NStartBWP to 0 — none is restated)
bwp1 = nrWavegenBWPConfig;
bwp1.NSizeBWP = 48;       % Fits within 52-RB carrier

% BWP 2: uses 30 kHz SCS carrier
bwp2 = nrWavegenBWPConfig;
bwp2.BandwidthPartID = 2;
bwp2.SubcarrierSpacing = 30;
bwp2.NStartBWP = 28;      % Matches carrier NStartGrid
bwp2.NSizeBWP = 34;       % Fits within 38-RB carrier

cfg.BandwidthParts = {bwp1, bwp2};

% CORESET on BWP 1 only — sized to fit (Duration left at its default of 2)
cfg.CORESET{1}.FrequencyResources = ones(1,4);  % 24 RBs
% PDCCH{1}.BandwidthPartID defaults to 1 (BWP 1) — no need to set it

% PDSCH 1: BWP 1, starts after CORESET symbols. The two BWPs occupy different
% frequency bands, so no time separation is needed — SlotAllocation stays at
% its every-slot default on both.
pdsch1 = nrWavegenPDSCHConfig;   % BandwidthPartID defaults to 1 (BWP 1)
pdsch1.SymbolAllocation = [2 12];  % After 2-symbol CORESET
pdsch1.PRBSet = 0:47;

% PDSCH 2: BWP 2, full slot
pdsch2 = nrWavegenPDSCHConfig;
pdsch2.BandwidthPartID = 2;
pdsch2.SymbolAllocation = [0 14];
pdsch2.PRBSet = 0:33;

cfg.PDSCH = {pdsch1, pdsch2};

% SSBurst on BWP 1 (15 kHz carrier has 52 RBs >= 20). Case A is the 15 kHz
% SSB pattern and is already the BlockPattern default — no need to set it.

[waveform, info] = nrWaveformGenerator(cfg);

Plotting Multiple BWP Resource Grids

figure;
tiledlayout(2, 1);

nexttile;
imagesc(abs(info.ResourceGrids(1).ResourceGridBWP(:,:,1)));
axis xy;
xlabel('OFDM Symbols');
ylabel('Subcarriers');
title('BWP 1 — 15 kHz SCS');
colorbar;

nexttile;
imagesc(abs(info.ResourceGrids(2).ResourceGridBWP(:,:,1)));
axis xy;
xlabel('OFDM Symbols');
ylabel('Subcarriers');
title('BWP 2 — 30 kHz SCS');
colorbar;

Avoiding Channel Conflicts

If nrWaveformGenerator reports conflicting channels (e.g., "PDCCH{1} and PDSCH{2} are in conflict"), separate them. Prefer frequency separation — it lets every channel run in every slot:

  • Frequency (preferred): Use PRBSet, or size BWPs into non-overlapping sub-bands (FDD), so channels never share physical spectrum.

  • Time: Use SlotAllocation to schedule channels in different slots (TDD).

  • Both (Hybrid): Where bands must partially overlap, resolve just the overlap in time.

  • BWP: Assign channels to different BWPs (which use different SCS carriers) — but see the caveat below.

Remember: channels in different BWPs at different numerologies can still conflict if their physical frequency ranges overlap. Two full-band PDSCHs on co-located carriers will conflict unless separated in time or frequency.

When separating in time, reckon in absolute time, not slot indices. A 30 kHz slot is half the duration of a 15 kHz slot, so SlotAllocation = 0:2 on a 15 kHz BWP (0–3 ms) and SlotAllocation = 6:9 on a 30 kHz BWP (3–5 ms) are disjoint, even though the raw index ranges look like they might not be.

Copyright 2026 The MathWorks, Inc.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
MathWorks
Publisher
MathWorks (Official)
Source
matlab/matlab-agentic-toolkit · skills-catalog/wireless-communications/matlab-generate-5g-waveform · commit 2a7f215
Licence
BSD 2-Clause License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE.md)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by MathWorks or MathWorks.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download matlab-generate-5g-waveform.zip