Demodulator — Control Systems/Waveform Functions
Control_Systems/Waveform_Functions/Demodulator · 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.
Demodulator
Control Systems / Waveform Functions
Recovers the message from an amplitude-modulated carrier by coherent detection: multiply by a local carrier of the same frequency, then low-pass what is left.
m[k] = g·y[k]·f(θ[k]) and x[k] = lowpass(m)[k] − d, with θ[k] = 2πFc·k·Ts, g the mixer gain, f the carrier function and d the Output Offset. The lowpass is a Butterworth of the configured order and cutoff.
The Detection setting picks g and f, and those two choices are the whole difference between an AM detector and either arm of a QAM one.
Ports
- y – the modulated signal, scalar. Nothing is assumed about its amplitude.
- x – the recovered message, scalar, in the message's own units.
Parameters
- Detection – which detector the block is.
- AM – g = 1 and a cosine carrier, the default. This is the
branch MATLAB's
demodtakes for'am','amdsb-sc','amdsb-tc'and'amssb'alike: all four share one mixer and one filter, and the only thing that distinguishes'amdsb-tc'is the offset below. - QAM in-phase – g = 2 and a cosine carrier, the first output of
demod(…, 'qam'). - QAM quadrature – g = 2 and a sine carrier, its second output. Wire one of each to recover both messages.
- AM – g = 1 and a cosine carrier, the default. This is the
branch MATLAB's
- Carrier Frequency (Hz) – Fc, which must be the frequency the signal was modulated at; detection is coherent, so a carrier even slightly off beats against the message rather than removing it. Its magnitude also supplies the default cutoff below.
- Lowpass Order – the Butterworth order, a whole number from 1 to 10. 5 is the default because it is the order MATLAB's function uses. Above 10 the block refuses rather than designing, since every export target unrolls the recursion into one straight-line block.
- Lowpass Cutoff (Hz) – where the filter turns over. Zero or
less means "use the carrier frequency", which is
demod's own choice (butter(5, Fc*2/Fs)is a cutoff at Fc). A cutoff is clamped inside (0, half the sampling rate), which is the only interval a digital filter can be designed in. - Output Offset – d, subtracted from the filtered result.
This is
demod's fifth argument for'amdsb-tc': the constant the modulator added so its carrier would never vanish. Leave it at zero for every other method. - 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.
The filter is designed once, here, and its coefficients are inlined. The order and the cutoff are configuration and the rate is the block's own, so no exported core computes a tangent, a pole or a bilinear map – each one receives the finished numerator and denominator as literals and runs the recursion. Everything is therefore structural, the period included: a core exported at one rate carries a filter designed for that rate, so re-export after changing the rate, the order, the cutoff or the frequency.
The three HDL targets are simulation-only and carry the block
in real arithmetic, quantizing only at the port boundary. A
Butterworth whose cutoff is well below the sampling rate has poles close to the
unit circle – at order 5 with a cutoff of 3.4 Hz at 100 Hz the
recursion's leading coefficient is −4.31 – and a Q16.16 datapath
subtracting large nearly-equal terms has nothing left of the signal. A
synthesizable version is a different filter structure, not a different
export.
Simulink bridge
No equivalent (Support::None). demod is a
Signal Processing Toolbox function, and that toolbox ships no Simulink
library at all. The demodulator blocks in the installed libraries belong to
Communications Toolbox and are different blocks, carrying symbol
decisions and a complex baseband convention 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 – the carrier phase and the filter's N delay elements
– and discrete by nature
(
setDiscreteOnlyBlock(true)). - The filter runs once, forwards. MATLAB's runs
filtfilt. That is a different estimator, not an approximation of the same one, and it is offered here as itself: a zero-phase filter runs forwards and then backwards over the whole record, which a block that sees one sample at a time cannot do at any price. The consequences are a phase lag wherefiltfilthas none, and a startup transient wherefiltfilthas none. Measured against R2026a on a 0.7 Hz message at Fc = 3.4 Hz and Fs = 100 Hz, order 5, cutoff at the carrier: the two differ by at most 0.248 where the message's rms is 0.462 – and that difference is the lag, not a distortion. Shifting this block's output back by the 15 samples of group delay the filter has at that message frequency leaves at most 0.043. It is not a startup effect either: the worst sample of the 500 is number 357. If the zero-phase result is what you need, filter offline; this block is the one that runs in a generated core. The tree's Signal Smoothing / Zero-Phase Filter block is the other answer. - The design agrees with
butter. Same prototype, same prewarped bilinear map as Polynomials / Butterworth Design, checked against R2026a:butter(5, 0.068)to 1.8×10−15 andbutter(3, 0.24)to 4.4×10−16. Reach for that block instead when the cutoff has to arrive on a port; this one designs from configuration only. - Detection is coherent and the phase is not recovered. The local carrier starts at θ = 0 and runs at exactly Fc; nothing here locks it to the incoming signal. A modulator and a demodulator started together in one model agree by construction, and a signal arriving with an unknown phase needs a carrier recovery loop this block does not contain.
- The output lags, and by how much is worth knowing before you wire it to anything. A causal lowpass delays what it passes; measured at order 5 with the cutoff at a 3.4 Hz carrier and a 100 Hz rate, a 0.7 Hz message comes out 15 samples late, and the delay grows both with the order and as the message approaches the cutoff. A lower order or a wider cutoff buys latency at the cost of leaving more of the carrier's second harmonic in the result.
- Scalar only: one signal, one carrier phase, one filter state. Wire one block per channel.
- No state space. The mixer multiplies the input by a time-varying quantity, so the block is not time-invariant and model reduction correctly declines to merge it – even though the filter behind the mixer is linear.
Code facts#
| Fact | Value |
|---|---|
| registered type | Control_Systems/Waveform_Functions/Demodulator |
| family | Control_Systems/Waveform_Functions |
| solver environment class | ICoreBlock_0_Control_Systems_1_Waveform_Functions_2_Demodulator |
| source | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Waveform_Functions/Demodulator/ICoreBlock_0_Control_Systems_1_Waveform_Functions_2_Demodulator.cpp |
| header | src/ICoreBlocks/ICoreBlockLibrary/Blocks/Control_Systems/Waveform_Functions/Demodulator/ICoreBlock_0_Control_Systems_1_Waveform_Functions_2_Demodulator.h |
| default size on canvas | 128 × 78 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 | y |
| 2 | out | ICoreDouble | x |
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 |
|---|---|---|
Detection | AM%~%QAM in-phase%~%QAM quadrature~~AM | — |
Carrier Frequency (Hz) | 1 | — |
Lowpass Order | 5 | — |
Lowpass Cutoff (Hz) | 0 | — |
Output Offset | 0 | — |
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: demod() is a Signal Processing Toolbox FUNCTION and that toolbox ships no Simulink library at all. The demodulator blocks in the installed libraries belong to Communications Toolbox and are different blocks -- they carry symbol decisions 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).
Demodulator -- coherent detection: mix down with a local carrier, then low-pass READ OUT OF THE FUNCTION. MATLAB's demod(y, Fc, Fs, method) does, for the methods that a one-sample-in one-sample-out block can carry:
'am' / 'amdsb-sc' / 'amdsb-tc' / 'amssb' x = filtfilt(butter(5, Fc*2/Fs), y .* cos(2*pi*Fc*t)) (minus an offset for 'amdsb-tc') 'qam' x = filtfilt(same filter, 2*y .* cos(2*pi*Fc*t)) x2 = filtfilt(same filter, 2*y .* sin(2*pi*Fc*t))
All four amplitude methods share ONE branch of the function -- the same mixer and the same filter -- so one setting covers them, and the 'amdsb-tc' offset is a configuration here. The two QAM outputs are the same arithmetic with a gain of 2 and a different carrier function, so each is a setting rather than a second output port: two of these blocks make a QAM detector, exactly as two Modulators make a QAM modulator.
⚠⚠ THE FILTER RUNS ONCE, FORWARDS, AND MATLAB'S RUNS filtfilt. THAT IS A DIFFERENT ESTIMATOR AND IT IS OFFERED HERE AS ITSELF. filtfilt filters forwards and then backwards, which cancels the phase and needs the WHOLE record; a block that sees one sample at a time cannot reach forwards in time at any price. So this block runs the same Butterworth once, causally, and every layer -- this banner, the description's summary and a Notes entry -- says which estimator it is. The gap is a measured number rather than a caveat, and it is a LAG rather than a distortion: on a 0.7 Hz message at Fc = 3.4 Hz, Fs = 100 Hz, order 5, R2026a's demod and this one-pass detector differ by at most 0.248 against a message whose rms is 0.462 -- and shifting the one-pass result back by 15 samples, the filter's group delay at that message frequency, leaves at most 0.043. The difference is NOT a startup transient: it is just as large at sample 357 as at sample 20.
⚠ THE DESIGN IS A CONFIGURATION CONSTANT, WHICH IS THE WHOLE REASON THIS BLOCK IS CHEAP. The order and the cutoff are configuration and the rate is the block's own, so the coefficients are computed once when the configuration is loaded and INLINED into all ten emitted bodies. No backend computes a tangent, a prototype or a bilinear map. That is what separates this block from Polynomials/Butterworth_Design, whose cutoff arrives on a PORT and which therefore has to carry the whole design into every language.
The design itself is that block's, so the two agree by construction rather than by coincidence: the analog prototype is built from REAL quadratic factors (each conjugate pole pair gives s^2 - 2cos(theta)s + 1, and an odd order adds the real pole at -1), and the prewarped bilinear map is expanded as sum_j d[j]T^j(z-1)^(N-j)*(z+1)^j with T = tan(pi*fc/fs). Checked against R2026a rather than assumed: butter(5, 0.068) to 1.8e-15
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) | -0.2263 … 0.05824 |
ramp | Ramp: slope 1 from t = 0 | -3.018 … 3.367 |
sine | Sine Wave: amplitude 1, 2 rad/s, no phase, no bias | -0.46 … 0.6063 |
table | Repeating Sequence Stair: [-2 -1 -0.5 0 0.5 1 2 3], one entry per sample | -1.013 … 1.009 |
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__Demodulator.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).