matlab/matlab-analyze-ams-waveform
Analyze AMS waveform data using Mixed-Signal Blockset utilities: phase noise measurement, clock jitter, anti-aliased resampling, timing measurements, lock time, INL/DNL, ADC/DAC calibration, HSpice import. Use when analyzing time-domain voltage from PLL/VCO/clock simulations, measuring phase noise from variable-step solver output, computing jitter, or resampling non-uniform data.
npx skills add https://github.com/matlab/matlab-agentic-toolkit --skill matlab-analyze-ams-waveform
Analyze waveform data using msblksutilities functions from the
Mixed-Signal Blockset. Covers timing, phase noise, jitter, lock time,
resampling, INL/DNL, ADC/DAC calibration, and HSpice data import.
snr, thd, sfdr, sinad) directlyWhen the user asks to measure phase noise from waveform data, **ask for frequency
offset points before proceeding**:
Default: [10e3, 100e3, 1e6, 10e6] Hz — press Enter to use these or specify
your own."
RBW = min(offsets) / 2 (e.g., 5 kHz for 10 kHz lowest offset). State:
"I'll use RBW = X Hz (lowest offset / 2). Let me know if you'd like a
different value."
This ensures the measurement matches the user's application without requiring
them to know the API signature upfront.
% From workspace variables
x = t; y = v;
% From .mat file
data = load('waveform.mat');
x = data.time; y = data.voltage;
% From .csv
data = readmatrix('waveform.csv');
x = data(:,1); y = data(:,2);
% From HSpice transient (.tr0)
tr0Reader('sim.tr0', 'output.mat');
data = load('output.mat');
% From HSpice AC (.ac0)
ac0Reader('sim.ac0', 'output.mat');
% From HSpice DC sweep (.sw0)
sw0Reader('sim.sw0', 'output.mat');
% From Simulink simulation output (timeseries in logsout)
sig = simOut.logsout.get('signalName').Values;
x = sig.Time(:); % column vector
y = squeeze(sig.Data(:)); % column vector — squeeze removes trailing dims
Always print a summary before analysis:
fprintf('=== Waveform Summary ===\n');
fprintf('Points : %d\n', numel(x));
fprintf('X range : [%.6g, %.6g]\n', min(x), max(x));
fprintf('Y range : [%.6g, %.6g]\n', min(y), max(y));
fprintf('Y mean : %.6g\n', mean(y));
fprintf('Y RMS : %.6g\n', rms(y));
if all(diff(x) > 0)
dx = diff(x);
if max(dx)/min(dx) < 1.01, uStr = 'yes'; else, uStr = 'no'; end
fprintf('X step : %.6g (uniform: %s)\n', median(dx), uStr);
if median(dx) > 0
fprintf('Sample rate: %.6g Hz\n', 1/median(dx));
end
end
Available MSB analyses for time-domain waveform:
--- Timing Measurements ---
[1] Rise time — timeDomainSignal2RiseTime
[2] Fall time — timeDomainSignal2FallTime
[3] Duty cycle — timeDomainSignal2DutyCycle
--- Clock / PLL Measurements ---
[4] Phase noise from frequency-domain data — phaseNoiseMeasure (default Type='Frequency')
[5] Phase noise from time-domain voltage — phaseNoiseMeasure (Type='Time')
[6] Period jitter & cycle-to-cycle jitter — clockJitterMeasure
[7] Phase noise to jitter conversion — phaseNoiseToJitter
[8] Lock time from control voltage — lockTimeMeasure
--- Resampling ---
[9] Anti-aliased resampling — lowpassResample
--- ADC/DAC Characterization ---
[10] INL / DNL measurement — inldnl
[11] ADC calibration — calibrateADC
[12] DAC calibration — calibrateDAC
--- Data Import ---
[13] HSpice transient (.tr0) — tr0Reader
[14] HSpice AC (.ac0) — ac0Reader
[15] HSpice DC sweep (.sw0) — sw0Reader
--- Frequency-Domain Utilities ---
[16] Interpolate/extrapolate to new grid — interpExtrap
[17] Laplace to biquad SOS — laplace2sos
% Rise time — 3rd arg is [low high] percent reference levels (required)
rt = timeDomainSignal2RiseTime(x, y, [10 90]);
fprintf('Rise time (10%%-90%%): mean = %.4g s (std = %.4g s, N=%d)\n', ...
mean(rt), std(rt), numel(rt));
% Fall time — same 3-arg signature
ft = timeDomainSignal2FallTime(x, y, [10 90]);
fprintf('Fall time (90%%-10%%): mean = %.4g s (std = %.4g s, N=%d)\n', ...
mean(ft), std(ft), numel(ft));
% Duty cycle — returns per-cycle values for multi-cycle waveforms
dc = timeDomainSignal2DutyCycle(x, y);
fprintf('Duty cycle: mean = %.4f%%, std = %.4f%%\n', mean(dc)*100, std(dc)*100);
Pre-check (mandatory): Verify simulation duration before measuring.
% Sim duration pre-check — STOP if insufficient
minDuration = 10 / min(FrOffset); % need >= 10 cycles of lowest offset
simDuration = x(end) - x(1);
if simDuration < minDuration
error('Simulation too short: %.4g s < %.4g s needed for %.0f Hz offset.\nIncrease sim stop time to >= %.4g s.', ...
simDuration, minDuration, min(FrOffset), minDuration);
end
% From time-domain voltage waveform (MSB variable-step simulation output)
% Type='Time' is REQUIRED — extracts phase via zero-crossings internally
Rbw = 1e3; % resolution bandwidth (Hz)
FrOffset = [10e3 100e3 1e6 10e6]; % offsets to measure
[PnAtOffsets, freqAxis, pnProfile] = phaseNoiseMeasure( ...
x(:), y(:), Rbw, FrOffset, 'on', 'PN Measurement', ...
-inf, ... % 7th arg: target PN level for plot overlay (-inf = no target line)
Type='Time');
% Note: To reduce ripple in pnProfile, use smaller RBW (increases freq resolution)
% or increase simulation duration. SpectralAverages is a PLL Testbench block
% parameter, NOT a phaseNoiseMeasure argument.
% From frequency-domain data (e.g., imported spectrum analyzer measurement)
% Default Type='Frequency': Xin=freq offset vector, Yin=power in dBc/Hz
[PnAtOffsets, freqAxis, pnProfile] = phaseNoiseMeasure( ...
freqOffsets, pnPower_dBcHz, Rbw, FrOffset, 'on', 'PN from Spectrum');
fprintf('Phase Noise Results:\n');
for k = 1:numel(FrOffset)
fprintf(' @ %.0f kHz : %.1f dBc/Hz\n', FrOffset(k)/1e3, PnAtOffsets(k));
end
% Save figure for Claude to read
figPath = fullfile(tempdir, 'phase_noise_plot.png');
saveas(gcf, figPath);
fprintf('Phase noise figure saved to: %s\n', figPath);
Mandatory follow-up: After any phase noise measurement (Section 2.2), ALWAYS
compute integrated RMS jitter using phaseNoiseToJitter. Report jitter in
picoseconds — this is the metric engineers compare against specs.
% Clock jitter from time-domain waveform
% Returns 2 outputs: [periodJitter, c2cJitter] (RMS values)
% threshold MUST cross the signal — use midpoint or known logic level
% Inputs must be column vectors
threshold = (max(y) + min(y)) / 2;
clockFreq = 1e9; % expected clock frequency (Hz)
[periodJitter, c2cJitter] = clockJitterMeasure(x(:), y(:), threshold, clockFreq);
fprintf('Period jitter (RMS): %.4f ps\n', periodJitter * 1e12);
fprintf('C2C jitter (RMS) : %.4f ps\n', c2cJitter * 1e12);
% Convert phase noise profile to jitter
% Exclude DC bin (freqAxis==0) — integration from 0 Hz returns Inf
validIdx = freqAxis > 0;
[jitterRad, jitterDeg, jitterSec] = phaseNoiseToJitter( ...
freqAxis(validIdx), pnProfile(validIdx), Frequency=carrierFreq);
fprintf('RMS jitter from PN : %.4f ps\n', jitterSec * 1e12);
% x = time, y = control voltage (loop filter output)
% lockTimeMeasure takes (voltage, time, tolerance) — note: voltage FIRST
% Both must be column vectors
x_col = x(:); y_col = y(:);
targetVoltage = y_col(end); % assume final value is lock voltage
errorTol = 0.01; % 1% tolerance
lockTime = lockTimeMeasure(y_col, x_col, errorTol);
fprintf('Lock time (%.0f%% tolerance): %.4g s\n', errorTol*100, lockTime);
Preferred method: If a PLL Testbench block is present, use its measured lock
time (get_param(tbBlk, 'UserData').lockTime) — frequency-error detection is
more accurate than voltage settling.
% Anti-aliased resampling to new sample time
Ts_new = 1e-9;
tq = (x(1) : Ts_new : x(end))';
cfg.OutputRiseFall = Ts_new;
cfg.NDelay = 1;
cfg.SampleMode = 'variable';
cfg.CausalMode = 'off';
y_resampled = lowpassResample(x, y, tq, cfg);
x_resampled = tq;
% ADC: uses transition-based fit (works on ANY input stimulus, not just ramps)
result = inldnl(Analog, Digital, Range, 'ADC', ...
'INLMethod', 'All', 'DNLMethod', 'All', ...
'OffsetErrorUnit', 'All', 'GainErrorUnit', 'All');
fprintf('Max |Endpoint INL|: %.4f LSB\n', max(abs(result.EndpointINL)));
fprintf('Max |Endpoint DNL|: %.4f LSB\n', max(abs(result.EndpointDNL)));
fprintf('Offset Error: %.4f LSB\n', result.OffsetErrorLSB);
fprintf('Gain Error: %.4f LSB\n', result.GainErrorLSB);
% DAC: uses center-based fit (FitMode='centers' is default for DAC)
result_dac = inldnl(Analog, Digital, Range, 'DAC', ...
'INLMethod', 'All', 'DNLMethod', 'All');
Critical: Do NOT use histogram-based DNL (code bin counts). That method
requires a specific input stimulus (ramp or sine with known PDF). The inldnl
function uses transition-based analysis that works on arbitrary inputs.
ADC vs DAC: ADC uses FitMode='transitions' (default); DAC uses
FitMode='centers'. Using the wrong fit mode gives incorrect results.
% Calibrate ADC: correct offset and gain errors
y_cal = calibrateADC(Digital, NBits, Polarity);
% Or infer errors from measured data:
y_cal = calibrateADC(Analog, Digital, Range, 'OffsetError', oe, 'GainError', ge);
% Calibrate DAC:
y_cal = calibrateDAC(Digital, NBits, Polarity);
% Or with reference/bias:
y_cal = calibrateDAC(Digital, Analog, Ref, Bias);
| Region | Slope | Physical Meaning |
|--------|-------|-----------------|
| Close-in (< loop BW) | -30 dB/dec | 1/f^3 -- flicker FM noise dominates |
| Mid-range | -20 dB/dec | 1/f^2 -- white FM / VCO thermal noise |
| Far-out (> loop BW) | -20 dB/dec then flat | VCO open-loop noise, then thermal floor |
A hump or peak (3-10 dB) indicates the PLL closed-loop bandwidth.
If >10 dB, the loop may be under-damped (low phase margin).
Far-out floor (beyond 1-10 MHz offset) is set by VCO thermal noise
and simulation numerical noise. Should match VCO open-loop spec.
Phase noise from a 1 GHz VCO (MSB sim, 100 us, zero-crossing):
@ 10 kHz : -51.8 dBc/Hz <- marginal (sim too short)
@ 100 kHz : -82.4 dBc/Hz <- in 1/f^2 region, reasonable
@ 1 MHz : -98.4 dBc/Hz <- approaching noise floor
@ 10 MHz : -127.6 dBc/Hz <- VCO open-loop thermal floor
Observations:
- -20 dB/dec slope from 10-80 kHz confirms white FM noise
- Hump at 100-300 kHz suggests loop BW artifact
- Floor at -128 dBc/Hz consistent with VCO open-loop spec
- 10 kHz result unreliable -- need >= 1 ms sim for clean data
figPath = fullfile(tempdir, 'analysis_plot.png');
saveas(gcf, figPath);
fprintf('Figure saved to: %s\n', figPath);
After MATLAB prints figPath, use the Read tool to open the PNG
and provide observations (slope, artifacts, noise floor per Phase 3).
Always generate an HTML report with:
<pre> blockfile:/// URLNaming: report_{datafile_stem}.html in the same directory as the data.
| Function | Purpose |
|----------|---------|
| phaseNoiseMeasure | Phase noise measurement |
| phaseNoiseToJitter | Phase noise to jitter |
| clockJitterMeasure | Period & cycle-to-cycle jitter |
| lockTimeMeasure | PLL lock time |
| timeDomainSignal2RiseTime | Rise time |
| timeDomainSignal2FallTime | Fall time |
| timeDomainSignal2DutyCycle | Duty cycle |
| inldnl | INL/DNL measurement |
| calibrateADC | ADC error calibration |
| calibrateDAC | DAC error calibration |
| lowpassResample | Anti-aliased resampling |
| interpExtrap | Multi-signal interpolation |
| laplace2sos | Laplace to biquad SOS |
| ac0Reader | Import HSpice AC data |
| tr0Reader | Import HSpice transient data |
| sw0Reader | Import HSpice DC sweep data |
Fs from data (1/median(diff(x))) rather than assumingType='Time'. The only valid types are 'Frequency' (default) and 'Time'lowpassResampleclockJitterMeasure needs the nominal clock frequency from design spec, not estimated from dataphaseNoiseMeasure(..., Type='Time') which extracts phase via zero-crossings internallyphaseNoiseMeasure reports center frequency as f_carrier/2 -- this is a display convention, carrier is still correct10/f_offset_min seconds. E.g., 10 kHz offset requires >= 1 ms sim timeSpectralAverages is a PLL Testbench block parameter (set via set_param), NOT a phaseNoiseMeasure argumentmax(dt)/min(dt) >> 1. This is expected — use Type='Time' for PN, or lowpassResample to create a uniform grid for FFTclockJitterMeasure returns NaN if threshold doesn't cross the signal. Use (max(y)+min(y))/2 or the known logic thresholdphaseNoiseToJitter returns Inf if freqAxis(1)==0. Always exclude the DC bin before callinginldnl(Analog, Digital, Range, Type) which uses transition detectionFitMode='transitions' (default). For DAC use FitMode='centers'. Using the wrong fit mode corrupts INL resultslockTimeMeasure, clockJitterMeasure, phaseNoiseMeasure Type='Time') expect column vectors. Use x(:) and y(:) to ensure correct shape. Row vectors produce silent wrong results or errors----
Copyright 2026 The MathWorks, Inc.
----
Take matlab/matlab-analyze-ams-waveform 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.