Modulator — Control Systems/Waveform Functions
Control_Systems/Waveform_Functions/Modulator · 1 input / 1 output port(s) at insert · exports to Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text
Description#
The block's own DESCRIPTION_HTML, rendered verbatim — the same text the config dialog's info panel and the library navigator show. Fix a wrong sentence in the block's .cpp (R-D9), never here.
Modulator
Control Systems / Waveform Functions
Puts a message signal onto a carrier, one sample at a time. The carrier phase is θ[k] = 2πFc·k·Ts and f is the carrier function (cosine or sine); the method decides what is done with it:
- AM suppressed carrier – y = x·f(θ). The carrier disappears when the message does.
- AM transmitted carrier – y = (x − c)·f(θ), with c the Carrier Offset. Shifting the message so it never changes sign leaves a carrier present at all times, which is what an envelope detector needs.
- PM – y = f(θ + kp·x). The message moves the phase; the amplitude never changes.
These are the three methods of MATLAB's modulate that produce
one output sample per input sample. The rest are covered elsewhere or
cannot stream – see Notes.
Ports
- x – the message, scalar. In the two AM methods it is the amplitude the carrier is multiplied by, so its units are the output's; in PM it is multiplied by the phase deviation and so is a phase in radians before it reaches the carrier. Any value is accepted.
- y – the modulated carrier, scalar. In PM it never leaves [−1, +1]; in the AM methods its size follows the message's.
Parameters
- Modulation – which of the three relations above is computed.
- AM suppressed carrier – y = x·f(θ), the default.
This is
modulate's'am'and its'amdsb-sc': the two are one branch of the function and return identical values, so one setting covers both. - AM transmitted carrier –
y = (x − c)·f(θ),
'amdsb-tc'. Reads Carrier Offset. - PM – y = f(θ + kp·x),
'pm'. Reads Phase Deviation (rad).
- AM suppressed carrier – y = x·f(θ), the default.
This is
- Carrier Frequency (Hz) – Fc. Any real value; a negative one runs the phase backwards. A frequency at or beyond half the block's sampling rate aliases, as any sampled oscillation does – MATLAB's function refuses that case outright and this block does not, because the rate is the model's rather than an argument.
- Carrier Function – cos (MATLAB's, and the default) or sin, the same carrier a quarter cycle later. It is a setting rather than a fixed cosine so that a quadrature pair can be built from two of these blocks – see Notes.
- Carrier Offset – c, subtracted from the message before it multiplies the carrier. Read by AM transmitted carrier only, and ignored by the other two. MATLAB's function defaults it to the smallest sample of the whole message; a block that sees one sample at a time cannot know that number, so it is entered here.
- Phase Deviation (rad) – kp, radians of carrier phase per unit of message. Read by PM only, and ignored by the other two. MATLAB's function defaults it to π divided by the largest magnitude in the whole message – again a number a streaming block cannot have – so it is entered here.
- Sampling Time (s) – zero or less inherits the solver's rate; a positive value runs the block at that period.
Code export
All ten targets: Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog and PLC Structured Text.
Everything except the message is structural and is inlined at export time: the carrier step folds the frequency together with the block's own period, and the offset, the deviation and the choice of method and carrier function all become part of the emitted arithmetic rather than tunable parameters. The period is inside the carrier step, so a core exported at one rate does not carry the frequency to another – re-export after changing either.
The three HDL targets are simulation-only, and only because of
the trigonometric call: the phase, its wrap and the product all stay in the
Q16.16 datapath and just the carrier value converts at the port boundary and
evaluates in real. A synthesizable carrier is a phase accumulator
driving a lookup table, which is a different block.
Simulink bridge
No equivalent (Support::None). modulate is a
Signal Processing Toolbox function, and that toolbox ships no Simulink
library at all. The modulator blocks in the installed libraries belong to
Communications Toolbox and are different blocks – they carry symbol
mapping, pulse shaping and a complex baseband convention that this block does
not – so mapping onto one would misreport what crossed. The bridge reports
this block instead, and it carries no parity testbench; code export
verification covers it across all ten languages.
Notes
- Stateful (one phase register) and discrete by nature
(
setDiscreteOnlyBlock(true)): the carrier advances once per sample. - The first output sits at θ = 0, as
modulate's first sample sits at t = 0. The carrier step is added after the sample is produced, never before. - The carrier is accumulated, not multiplied out. θ is a running sum wrapped into 0, 2π) rather than 2πFct computed from a growing t. The two agree to 2.2×10−16 over 8 samples and 2.9×10−14 over 500, measured at Fc = 3.4 Hz and Ts = 0.01 s; the accumulator is what keeps the argument inside the range the HDL cores carry.
- QAM is two of these blocks.
modulate's'qam'is x·cos(θ) + x₂·sin(θ) over two messages: one modulator with Carrier Function cos, one with sin, and a Sum. That is why the carrier function is a setting. - FM is the Voltage Controlled Oscillator in this same family, which is
modulate(…, 'fm')exactly –vcois implemented by calling it. Reach for that block rather than looking for an FM setting here. - PWM is the PWM block under Discontinuities, which is that method with the duty arriving on a port and one output sample per input sample.
- Three methods are deliberately absent.
'amssb'needs the analytic signal of the whole message, which one sample cannot form;'ptm'and'ppm'emit Fs/Fc samples for every message sample, so they change the signal's rate where this block produces one output per input. - Scalar only: one message, one carrier phase. Wire one block per channel, so that two channels cannot silently share a phase.
- No state space. The block multiplies its input by a time-varying quantity (and in PM is not even linear in it), so it carries none and model reduction correctly declines to merge it.
Code facts#
| Fact | Value |
|---|---|
| registered type | Control_Systems/Waveform_Functions/Modulator |
| family | Control_Systems/Waveform_Functions |
| solver environment class | ICoreBlock_0_Control_Systems_1_Waveform_Functions_2_Modulator |
| source | [src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Waveform_Functions/Modulator/ICoreBlock_0_Control_Systems_1_Waveform_Functions_2_Modulator.cpp |
| header | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Waveform_Functions/Modulator/ICoreBlock_0_Control_Systems_1_Waveform_Functions_2_Modulator.h |
| default size on canvas | 124 × 76 px |
| ports at insert | 1 in, 1 out |
| code generators implemented | Python, MATLAB, Java, Rust, C, C++, VHDL, Verilog, SystemVerilog, PLC Structured Text |
Ports#
| # | Direction | Signal type | Description label |
|---|---|---|---|
| 1 | in | ICoreDouble | x |
| 2 | out | ICoreDouble | y |
Ports the constructor creates. A block whose port list changes with its configuration adds or removes ports at load time; the count above is the one a freshly inserted block has.
Configuration variables#
| Config variable | Default | Simulink parameter |
|---|---|---|
Modulation | AM suppressed carrier%~%AM transmitted carrier%~%PM~~AM s… | — |
Carrier Frequency (Hz) | 1 | — |
Carrier Function | cos%~%sin~~cos | — |
Carrier Offset | 0 | — |
Phase Deviation (rad) | 1 | — |
Every block also carries Sampling Time (s) from ICoreBlockSolverEnvironment: zero or less inherits the solver's rate, a positive value runs the block at that period.
Simulink bridge#
| support | Support::None |
| Simulink path | — |
| port-count rule | PortsParam::None |
SampleTime parameter | yes |
Caveat (shown to the user): no Simulink equivalent: modulate() is a Signal Processing Toolbox FUNCTION and that toolbox ships no Simulink library at all. The modulator blocks in the installed libraries belong to Communications Toolbox and are different blocks -- they carry symbol mapping, pulse shaping and a complex baseband convention this block does not -- so mapping onto one would misreport what crossed. Reported rather than dropped, and it carries no parity testbench
Catalog contract: src/ICoreBlocks/ICoreCoder/ICoreCommandSystem/SimulinkBridge/ICoreSimulinkBlockCatalog.h
Description vs code#
The lists agree. check_block_descriptions.py finds no disagreement between the description's Ports, Parameters, Code export and Simulink bridge lists and the code's.
The verdict above is
tools/docs/check_block_descriptions.py(P7.1), which compares LISTS. It cannot read a sentence: "stateless" on a block with a state, an initial-value semantic the recursion does not implement, a "not synthesizable" caveat the HDL banner contradicts. That is the agent audit (P7.3) on BLOCK_DESCRIPTION_AUDIT.md, and this tool's green is not a substitute for one.
File banner (developer view)#
The top comment of the block's .cpp — the maths, the realization and the export strategy, addressed to whoever changes it. It must not contradict the description above (P7.5).
Modulator -- one message sample onto one carrier sample, three methods, one phase register THE METHODS ARE READ OUT OF THE FUNCTION. MATLAB's modulate(x, Fc, Fs, method, opt) builds t = k/Fs from k = 0 and then, for the three methods a one-sample-in one-sample-out block can carry:
'am' / 'amdsb-sc' y = x .* cos(2*pi*Fc*t) 'amdsb-tc' y = (x - c) .* cos(2*pi*Fc*t) c the 5th argument 'pm' y = cos(2*pi*Fc*t + kp*x) kp the 5th argument
⚠ 'am' AND 'amdsb-sc' ARE ONE BRANCH OF THE FUNCTION, not two. Measured in R2026a at Fc = 3.4 Hz, Fs = 100 Hz over 8 samples: isequal(modulate(x,..,'am'), modulate(x,.., 'amdsb-sc')) is 1. So this block offers one setting for both and says so.
⚠ THE OTHER FIVE METHODS ARE NOT MISSING, THEY BELONG SOMEWHERE ELSE, and each is named in the description with its reason:
- 'fm' is the Voltage Controlled Oscillator standing beside this block in this same
family -- vco() hands its work to modulate(..., 'fm'), so building it here would be a second copy of a block this library already has;
- 'pwm' is the PWM block under Discontinuities, which is that method at a one-sample
rate with the duty on a port;
- 'amssb' needs the analytic signal of the WHOLE message, which a block that sees one
sample cannot form;
- 'ptm'/'ppm' emit Fs/Fc samples for every message sample -- they change the rate, and
a block here produces one output per input;
- 'qam' takes two messages onto a cosine and a sine. Both halves ARE this block, which
is why the carrier function is a setting: two of these and one Sum is a QAM modulator.
THE CARRIER IS A WRAPPED PHASE ACCUMULATOR rather than a multiplication of a growing time: theta starts at 0, the step 2*pi*Fc*Ts is added after every output, and the result is wrapped into [0, 2*pi). That keeps the argument inside the fixed-point range the three hardware description targets carry, and it costs 2.2e-16 over 8 samples and 2.9e-14 over 500 against the function's own cos(2*pi*Fc*t) -- both measured, not estimated.
⚠ THE STEP IS ADDED AFTER THE OUTPUT, because the function's first sample sits at t = 0. The other order lags the reference by one carrier step on every sample, which a constant message cannot reveal -- which is why the block is verified against a message that moves.
⚠ THE THREE HARDWARE DESCRIPTION TARGETS ARE SIMULATION-ONLY, and only because of the trigonometric call: the phase, the wrap and the product all stay in the Q16.16 datapath, and just the carrier value converts at the port boundary and evaluates in
real. The
Sample results#
The same rig also ran:
| Stimulus | What it is | Output range |
|---|---|---|
impulse | Impulse: one sample of 1 at k = 5, 0 elsewhere (Repeating Sequence Stair) | -1 … 0 |
ramp | Ramp: slope 1 from t = 0 | -5.5 … 5 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -0.8415 … 1 |
table | Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample | -3 … 2.427 |
Plotted: step — Step: 0 -> 1 at t = 1 s
Category dynamic · sample time 0.1 · 60 steps · commit 05a7af9545eeffe8cf9dc83de88a165a633fca2a · produced by docsSample --out <folder> --blocks Modulator Demodulator Hilbert_Transform --steps 60 · data docs/generated/samples/Control_Systems__Waveform_Functions__Modulator.json · the SVG is generated from those numbers by tools/docs/plot_svg.py, so it is a run and not a drawing (R-D10).