Files
skills-win/simulink-rtl-redev/measure_k.m
T
bowei ea4541ee90 sync: SHANGHAI-U9 2026-07-27 三个 Simulink→RTL skill 转系统级 + 新增 simulink-rtl-redev
- simulink_rtl_dev / rtl-ip-replace: 从工程级 .claude/skills 迁到用户级并去工程耦合
  (模型名/Notes 文档依赖/工程实测基线移出; addpath 与脚本路径改用户级)
- rtl-ip-replace/scripts/gen_cmp_tb.py: Vivado 路径不再写死, 改读 VIVADO_PATH /
  XILINX_VIVADO (兼容 install root / bin / bin\vivado.bat 三种写法), 回落时提示
  去设 skills_env.md 的 VIVADO_PATH
- simulink-rtl-redev: 新增。原生模型改动后的仿真模型同步规范, 含两个实测过的工具
  check_sync.py (三条时序链新鲜度闸门) 与 measure_k.m (时域/频域/包络三估计器互证)
- sim-rtl-flow/SKILL.md: 加入 sync_sim:* 步骤, 引用上述三个 skill
2026-07-27 10:39:53 +08:00

200 lines
8.7 KiB
Matlab

function [K, R] = measure_k(ref, dut, varargin)
%MEASURE_K Measure the integer delay K that <dut> lags <ref>, by TWO INDEPENDENT
% methods that must agree (Task16-3 section 5: "延迟必须实测,不能按设计意图推算.
% 用独立微台、至少两种互不依赖的方法测出一致值").
%
% K = measure_k(ref, dut)
% [K, R] = measure_k(ref, dut, 'Name', value, ...)
%
% ref, dut : real or COMPLEX vectors, same sample rate. dut is the signal that
% comes out K samples later than ref.
%
% Options
% 'MaxLag' (default 4096) search window, in samples
% 'OnsetTol' (default 1e-3) onset threshold, RELATIVE to each signal's own
% RMS -- so the two signals may differ in gain
% 'FracTol' (default 0.05) how far the interpolated correlation peak may sit
% from an integer before K is declared non-integer
% 'Verbose' (default true)
%
% Returns
% K : the agreed integer delay, or NaN if the methods disagree
% R : struct with per-method results and the verdict --
% .k_corr argmax of the normalised cross-correlation
% .k_onset difference of first-significant-sample indices
% .k_frac parabolic-interpolated peak (fractional)
% .frac_resid |k_frac - round(k_frac)| -- large => not a pure integer shift
% .rho |correlation| at the peak, 0..1
% .agree k_corr == k_onset
% .pass agree && frac_resid <= FracTol
%
% WHY TWO METHODS: a correlation argmax alone will happily return a number for
% two unrelated signals. Onset detection is a structurally different estimator
% (amplitude threshold, not inner product), so agreement between them is real
% evidence. The fractional peak is the third check: it catches the case where
% the change did NOT introduce a clean integer pipeline delay (e.g. it altered a
% fractional interpolator), which an integer-only estimator would silently round.
%
% Base MATLAB only -- no Signal Processing Toolbox (no xcorr/finddelay).
p = inputParser;
p.addParameter('MaxLag', 4096, @(x) isnumeric(x) && isscalar(x) && x > 0);
p.addParameter('OnsetTol', 1e-3, @(x) isnumeric(x) && isscalar(x) && x > 0);
p.addParameter('FracTol', 0.05, @(x) isnumeric(x) && isscalar(x) && x > 0);
p.addParameter('Verbose', true, @(x) islogical(x) || isnumeric(x));
p.parse(varargin{:});
o = p.Results;
ref = ref(:); dut = dut(:);
n = min(numel(ref), numel(dut));
if n < 16
error('measure_k: need at least 16 samples, got %d', n);
end
ref = ref(1:n); dut = dut(1:n);
maxlag = min(o.MaxLag, n - 1);
% ---- method 1: normalised cross-correlation, argmax over 0..maxlag -----------
% FFT-based so it stays base MATLAB and is fast for long vectors.
nfft = 2^nextpow2(2*n);
Rxy = ifft(fft(dut, nfft) .* conj(fft(ref, nfft))); % dut delayed by k => peak at k
c = abs(Rxy(1:maxlag+1));
den = norm(ref) * norm(dut);
if den == 0
error('measure_k: one of the signals is all zeros');
end
c = c / den;
[rho, idx] = max(c);
k_corr = idx - 1;
% ---- method 3 (on the same curve): parabolic interpolation of the peak -------
% Done here because it needs c; it is reported separately and is NOT one of the
% "two independent methods" -- it only tests integer-ness.
if idx > 1 && idx < numel(c)
y0 = c(idx-1); y1 = c(idx); y2 = c(idx+1);
d = (y0 - y2) / (2*(y0 - 2*y1 + y2));
if ~isfinite(d), d = 0; end
k_frac = k_corr + d;
else
k_frac = k_corr;
end
frac_resid = abs(k_frac - round(k_frac));
% ---- method 2: cross-spectral phase slope (FREQUENCY domain, always available)
% A pure delay of K samples makes angle(Y.*conj(X)) a straight line of slope -K
% against normalised frequency. Fitting that slope uses no argmax at all, so it
% is a genuinely different estimator from method 1 -- and it is fractional, so it
% doubles as an integer-ness check that cannot be fooled by rounding.
k_phase = local_phase_slope(ref, dut, nfft);
% ---- method 3: onset of the first significant sample (only if one exists) ----
% Threshold is a fraction of each signal's own PEAK envelope, not its RMS: a
% noise/quantisation floor in the DUT during the reference's silent lead-in must
% not trigger it. Returns NaN for continuously excited signals -- that is not a
% failure, it just means this method does not apply.
i_ref = local_onset(ref, o.OnsetTol);
i_dut = local_onset(dut, o.OnsetTol);
if isnan(i_ref) || isnan(i_dut)
k_onset = NaN;
else
k_onset = i_dut - i_ref;
end
% ---- adjudication -----------------------------------------------------------
% Require methods 1 and 2 (always available, different domains) to agree, and
% method 3 to agree as well WHEN it applies.
agree_phase = isfinite(k_phase) && abs(k_phase - k_corr) <= o.FracTol;
agree_onset = isnan(k_onset) || (k_onset == k_corr); % NaN = not applicable
n_methods = 2 + double(~isnan(k_onset));
agree = agree_phase && agree_onset;
pass = agree && (frac_resid <= o.FracTol);
if pass, K = k_corr; else, K = NaN; end
R = struct('k_corr', k_corr, 'k_phase', k_phase, 'k_onset', k_onset, ...
'k_frac', k_frac, 'frac_resid', frac_resid, 'rho', rho, ...
'agree_phase', agree_phase, 'agree_onset', agree_onset, ...
'n_methods', n_methods, 'agree', agree, 'pass', pass, ...
'n', n, 'maxlag', maxlag);
if o.Verbose
fprintf('measure_k: n=%d maxlag=%d\n', n, maxlag);
fprintf(' method 1 xcorr argmax (time domain) K = %d |rho| = %.6f\n', k_corr, rho);
if isfinite(k_phase)
fprintf(' method 2 phase slope (freq domain) K = %.3f\n', k_phase);
else
fprintf(' method 2 phase slope (freq domain) K = n/a (not enough usable bins)\n');
end
if isnan(k_onset)
fprintf(' method 3 onset diff (amplitude) K = n/a (no clear onset -- continuous signal)\n');
else
fprintf(' method 3 onset diff (amplitude) K = %d\n', k_onset);
end
fprintf(' check interpolated peak %.3f (residual %.4f, tol %.4f)\n', ...
k_frac, frac_resid, o.FracTol);
if pass
fprintf(' VERDICT: PASS -- %d independent methods agree, K = %d\n', n_methods, K);
else
fprintf(' VERDICT: FAIL -- K NOT established\n');
if ~agree_phase
fprintf(' methods 1 and 2 disagree (%d vs %.3f). Different domains giving\n', ...
k_corr, k_phase);
fprintf(' different answers means these are not a pure delayed copy of each\n');
fprintf(' other -- check you compared the right taps.\n');
end
if ~agree_onset
fprintf(' method 3 disagrees (%d vs %d). A noise/quantisation floor in the\n', ...
k_corr, k_onset);
fprintf(' DUT during the reference lead-in can do this: raise OnsetTol.\n');
end
if frac_resid > o.FracTol
fprintf(' peak sits %.3f off an integer: the change did not introduce a\n', frac_resid);
fprintf(' clean integer pipeline delay (fractional/resampling path?).\n');
end
fprintf(' Do NOT fall back to the design-intent value (Task16-3 section 5).\n');
end
end
end
function kp = local_phase_slope(ref, dut, nfft)
%LOCAL_PHASE_SLOPE Delay from the slope of the cross-spectrum phase.
% Only bins where the reference has real energy are used, and the phase is
% unwrapped over those bins before a weighted least-squares line fit.
X = fft(ref, nfft); Y = fft(dut, nfft);
half = floor(nfft/2);
X = X(2:half); Y = Y(2:half); % drop DC and Nyquist
w = abs(X);
if isempty(w) || max(w) == 0, kp = NaN; return, end
keep = w > 0.10 * max(w); % usable bins only
if nnz(keep) < 16, kp = NaN; return, end
k = (1:numel(X)).';
ph = unwrap(angle(Y(keep) .* conj(X(keep))));
kk = k(keep);
ww = w(keep);
% weighted least squares: ph ~ a + b*(2*pi*kk/nfft), delay = -b
x = 2*pi*kk/nfft;
sw = sum(ww);
mx = sum(ww.*x)/sw; my = sum(ww.*ph)/sw;
b = sum(ww.*(x-mx).*(ph-my)) / sum(ww.*(x-mx).^2);
kp = -b;
end
function i0 = local_onset(x, tol)
%LOCAL_ONSET First index where the SMOOTHED envelope exceeds tol * its own PEAK.
% Threshold is relative to the peak, not the RMS, so a noise or quantisation
% floor well below the signal does not trigger it. Returns NaN when the signal
% is already at level from the first sample (continuously excited) -- that means
% "this method does not apply", not "failure".
a = abs(x);
w = 8; % short moving average
e = filter(ones(w,1)/w, 1, a);
pk = max(e);
if pk <= 0, i0 = NaN; return, end
thr = tol * pk;
i0 = find(e > thr, 1, 'first');
if isempty(i0), i0 = NaN; return, end
% already hot at the very start => no usable onset
if i0 <= w, i0 = NaN; end
end