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
This commit is contained in:
bowei
2026-07-27 10:39:53 +08:00
parent 6dddab8923
commit ea4541ee90
23 changed files with 3171 additions and 12 deletions
+208
View File
@@ -0,0 +1,208 @@
function rep = audit_hdl_blocks(target, varargin)
%AUDIT_HDL_BLOCKS Policy audit of an RTL-bound Simulink subtree (Task12-1).
%
% Rule 1: everything under a subsystem that is going to be turned into RTL must
% be built from Simulink blocks that generate RTL directly -- no embedded
% MATLAB (m) code.
% Rule 2: an m-code block is only acceptable if a specific Xilinx IP is named
% as its replacement AND the swap is proven cycle-true by simulation. Such
% blocks must be listed in the exemptions file, with evidence.
%
% rep = AUDIT_HDL_BLOCKS('model/HdlSubsystem')
% rep = AUDIT_HDL_BLOCKS(..., 'Exemptions', 'Notes/hdl_mcode_exemptions.json')
% rep = AUDIT_HDL_BLOCKS(..., 'Out', 'hdl_audit.json')
%
% The report (struct + JSON) lists every offending block with the metrics
% needed to plan its migration: script size, persistent state, loops, ports,
% and a suggested native-block replacement.
%
% Verdict: PASS no m-code in the subtree
% EXEMPT all m-code blocks are listed as exempted (evidence is
% checked separately by check_exemptions.py)
% VIOLATION at least one undocumented m-code block
p = inputParser;
p.addParameter('Exemptions', '');
p.addParameter('Out', '');
p.parse(varargin{:});
exFile = p.Results.Exemptions;
outFile = p.Results.Out;
parts = strsplit(target, '/');
model = parts{1};
if ~bdIsLoaded(model)
load_system(model);
end
opts = {'LookUnderMasks', 'all', 'FollowLinks', 'on'};
blocks = find_system(target, opts{:}, 'Type', 'Block');
% ---- classification ------------------------------------------------------
% VIOLATION: behaviour written in m code (or opaque to HDL Coder)
% WARN : Stateflow control constructs -- allowed for control only, must be
% justified; they are not the "embedded m language" this rule targets
% but they still hide code from review.
VIOL_SF = {'MATLAB Function', 'Truth Table'};
WARN_SF = {'Chart', 'State Transition Table'};
VIOL_BT = {'MATLABSystem', 'S-Function', 'M-S-Function', 'Fcn', 'MATLABFcn'};
exempt = local_read_exemptions(exFile);
items = struct('block', {}, 'kind', {}, 'severity', {}, 'lines', {}, ...
'persistent_state', {}, 'loops', {}, 'n_in', {}, 'n_out', {}, ...
'suggestion', {}, 'exempt', {}, 'xilinx_ip', {});
% Stateflow blocks carry an internal ' SFunction ' child; report the owner, not
% its implementation detail.
sfOwners = {};
for k = 1:numel(blocks)
if strcmp(get_param(blocks{k}, 'BlockType'), 'SubSystem')
try
t = get_param(blocks{k}, 'SFBlockType');
if ~isempty(t) && ~strcmp(t, 'NONE'), sfOwners{end+1} = blocks{k}; end %#ok<AGROW>
catch
end
end
end
for k = 1:numel(blocks)
blk = blocks{k};
bt = get_param(blk, 'BlockType');
sfType = '';
if strcmp(bt, 'SubSystem')
try, sfType = get_param(blk, 'SFBlockType'); catch, sfType = ''; end
end
if isempty(sfType) && any(cellfun(@(o) startsWith(blk, [o '/']), sfOwners))
continue % inside a Stateflow block already accounted for
end
if any(strcmp(sfType, VIOL_SF))
kind = sfType; sev = 'VIOLATION';
elseif any(strcmp(sfType, WARN_SF))
kind = sfType; sev = 'WARN';
elseif any(strcmp(bt, VIOL_BT))
kind = bt; sev = 'VIOLATION';
else
continue
end
[nl, hasPersist, hasLoop, script] = local_script_metrics(blk, kind);
ph = get_param(blk, 'PortHandles');
isEx = any(strcmp(blk, {exempt.block}));
ip = '';
if isEx
ip = exempt(strcmp(blk, {exempt.block})).xilinx_ip;
sev = 'EXEMPT';
end
items(end+1) = struct( ...
'block', blk, 'kind', kind, 'severity', sev, 'lines', nl, ...
'persistent_state', hasPersist, 'loops', hasLoop, ...
'n_in', numel(ph.Inport), 'n_out', numel(ph.Outport), ...
'suggestion', local_suggest(script), 'exempt', isEx, 'xilinx_ip', ip); %#ok<AGROW>
end
nViol = sum(strcmp({items.severity}, 'VIOLATION'));
nWarn = sum(strcmp({items.severity}, 'WARN'));
nEx = sum(strcmp({items.severity}, 'EXEMPT'));
if nViol > 0
verdict = 'VIOLATION';
elseif nEx > 0
verdict = 'EXEMPT';
else
verdict = 'PASS';
end
rep = struct('target', target, 'model', model, ...
'generated_at', datestr(now, 'yyyy-mm-dd HH:MM:SS'), ...
'exemptions_file', exFile, 'n_blocks_scanned', numel(blocks), ...
'n_violation', nViol, 'n_warn', nWarn, 'n_exempt', nEx, ...
'verdict', verdict, 'items', {items});
% ---- report --------------------------------------------------------------
fprintf('\n=== HDL m-code audit: %s ===\n', target);
fprintf('scanned %d block(s)\n', numel(blocks));
for k = 1:numel(items)
it = items(k);
fprintf(' [%-9s] %-11s %s\n', it.severity, it.kind, it.block);
fprintf(' %d line(s), persistent=%d, loops=%d, %d in / %d out\n', ...
it.lines, it.persistent_state, it.loops, it.n_in, it.n_out);
if ~isempty(it.xilinx_ip)
fprintf(' exempt -> Xilinx IP: %s\n', it.xilinx_ip);
elseif ~isempty(it.suggestion)
fprintf(' suggest: %s\n', it.suggestion);
end
end
fprintf('VERDICT: %s (violation=%d warn=%d exempt=%d)\n\n', verdict, nViol, nWarn, nEx);
if ~isempty(outFile)
fid = fopen(outFile, 'w');
fwrite(fid, jsonencode(rep));
fclose(fid);
fprintf('report -> %s\n', outFile);
end
end
% -------------------------------------------------------------------------
function ex = local_read_exemptions(f)
ex = struct('block', {}, 'xilinx_ip', {});
if isempty(f) || ~isfile(f)
return
end
raw = jsondecode(fileread(f));
if isfield(raw, 'exemptions')
raw = raw.exemptions;
end
if isstruct(raw)
raw = num2cell(raw);
end
for i = 1:numel(raw)
e = raw{i};
ip = '';
if isfield(e, 'xilinx_ip'), ip = e.xilinx_ip; end
ex(end+1) = struct('block', e.block, 'xilinx_ip', ip); %#ok<AGROW>
end
end
function [nl, hasPersist, hasLoop, script] = local_script_metrics(blk, kind)
script = '';
nl = 0; hasPersist = false; hasLoop = false;
if ~strcmp(kind, 'MATLAB Function')
return
end
try
c = sfroot().find('-isa', 'Stateflow.EMChart', 'Path', blk);
if isempty(c), return, end
script = c(1).Script;
nl = numel(strsplit(script, newline));
hasPersist = ~isempty(regexp(script, '\<persistent\>', 'once'));
hasLoop = ~isempty(regexp(script, '^\s*(for|while)\>', 'once', 'lineanchors'));
catch
end
end
function s = local_suggest(script)
%LOCAL_SUGGEST keyword -> native Simulink block hints (see reference/block_substitutions.md)
s = '';
if isempty(script), return, end
hits = {};
map = { ...
'\<(sin|cos)\s*\(', 'Sine/Cosine: NCO HDL Optimized or Direct Lookup Table (n-D)'; ...
'\<atan2?\s*\(', 'atan/atan2: Complex to Magnitude-Angle HDL Optimized (CORDIC)'; ...
'\<(sqrt|abs)\s*\(', 'sqrt/abs: Sqrt HDL Optimized / Complex to Magnitude-Angle HDL Optimized'; ...
'\<(filter|conv)\s*\(', 'FIR: Discrete FIR Filter (HDL) -- keeps taps as a block parameter'; ...
'persistent\s+\w*(buf|mem|ram|dl)', 'sample memory: Simple Dual Port RAM / Delay / Tapped Delay'; ...
'\<(fft|ifft)\s*\(', 'FFT: FFT HDL Optimized'; ...
'switch\s+\w*(state|st)\>', 'control FSM: Stateflow Chart (control only, no datapath math)'; ...
'\<mod\s*\(|\+\s*1\s*;.*count', 'counter: HDL Counter / Counter Free-Running'};
for i = 1:size(map, 1)
if ~isempty(regexp(script, map{i,1}, 'once'))
hits{end+1} = map{i,2}; %#ok<AGROW>
end
end
if isempty(hits)
s = 'no keyword match -- rebuild from primitives (Delay/Product/Sum/Switch/Lookup Table)';
else
s = strjoin(hits, ' | ');
end
end
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Gate for rule 2 of Task12-1: an m-code block inside an RTL-bound subtree is
acceptable ONLY if a specific Xilinx IP is named as its replacement AND the swap
is proven cycle-true by simulation.
python check_exemptions.py <hdl_audit.json> [<exemptions.json>]
Exit code 0 = the subtree may go to makehdl / synthesis:
every m-code block is either gone, or exempted with (a) a named Xilinx IP and
(b) an evidence file whose first line is PASS (rtl-ip-replace cmp_result.txt).
Exit code 1 = blocked, with the reason printed per block.
Evidence paths are resolved relative to the repo root (the directory containing
Notes/), or absolutely.
"""
import json, os, sys
REQUIRED = ('block', 'reason', 'xilinx_ip', 'cycle_true_evidence', 'approved_by', 'date')
def repo_root(start):
d = os.path.abspath(start)
while True:
if os.path.isdir(os.path.join(d, 'Notes')) or os.path.isdir(os.path.join(d, '.git')):
return d
nd = os.path.dirname(d)
if nd == d:
return os.path.abspath(start)
d = nd
def evidence_ok(path, roots):
tried = []
for r in ([''] if os.path.isabs(path) else roots):
p = path if os.path.isabs(path) else os.path.join(r, path)
tried.append(p)
if os.path.isfile(p):
first = open(p, errors='replace').readline().strip()
if not first.upper().startswith('PASS'):
return False, f'evidence is not a PASS: "{first}" ({p})'
return True, first
return False, 'evidence file not found: ' + ' | '.join(tried)
def main():
argv = [a for a in sys.argv[1:] if not a.startswith('--root=')]
root_opt = [a.split('=', 1)[1] for a in sys.argv[1:] if a.startswith('--root=')]
if not argv:
sys.exit(__doc__)
audit = json.load(open(argv[0], encoding='utf-8-sig'))
exf = argv[1] if len(argv) > 1 else audit.get('exemptions_file') or ''
roots = [r for r in (root_opt + [repo_root(os.getcwd()),
repo_root(os.path.dirname(os.path.abspath(argv[0])) or '.'),
os.getcwd()]) if r]
items = audit.get('items') or []
if isinstance(items, dict):
items = [items]
mcode = [i for i in items if i['severity'] in ('VIOLATION', 'EXEMPT')]
warns = [i for i in items if i['severity'] == 'WARN']
ex = []
if exf and os.path.isfile(exf):
raw = json.load(open(exf, encoding='utf-8-sig'))
ex = raw.get('exemptions', raw if isinstance(raw, list) else [])
by_block = {e.get('block'): e for e in ex}
print(f"target : {audit.get('target')}")
print(f"root : {roots[0]}")
print(f"audit : {len(items)} flagged block(s) "
f"({len(mcode)} m-code, {len(warns)} stateflow-warn)")
print(f"exempt : {exf or '(none)'}\n")
problems = []
for it in mcode:
blk = it['block']
e = by_block.get(blk)
if not e:
problems.append(f"{blk}\n m-code with no exemption entry -- rebuild it from "
f"native Simulink blocks, or file an exemption (rule 2).")
print(f" [BLOCKED ] {blk}")
continue
missing = [k for k in REQUIRED if not str(e.get(k, '')).strip()]
if missing:
problems.append(f"{blk}\n exemption is incomplete: missing {', '.join(missing)}")
print(f" [BLOCKED ] {blk} (incomplete exemption)")
continue
ok, info = evidence_ok(e['cycle_true_evidence'], roots)
if not ok:
problems.append(f"{blk}\n {info}")
print(f" [BLOCKED ] {blk} ({info})")
continue
print(f" [EXEMPT ] {blk}\n IP: {e['xilinx_ip']}\n"
f" cycle-true: {info} [{e['cycle_true_evidence']}]")
for it in warns:
print(f" [WARN ] {it['block']} ({it['kind']}) -- control-only Stateflow is tolerated; "
f"no datapath arithmetic in it")
stale = [b for b in by_block if b not in {i['block'] for i in items}]
for b in stale:
print(f" [STALE ] exemption for a block that is no longer m-code (or gone): {b}")
if problems:
print(f'\nGATE: BLOCKED -- {len(problems)} problem(s)\n')
for p in problems:
print(' * ' + p)
print('\nRule (Task12-1): an RTL-bound subsystem and everything under it must be built '
'from Simulink blocks that generate RTL directly; embedded m code is allowed only '
'against a named Xilinx IP, proven cycle-true.')
sys.exit(1)
print('\nGATE: PASS -- no undocumented m code in the RTL-bound subtree')
if __name__ == '__main__':
main()
@@ -0,0 +1,64 @@
function ok = check_hdl_support(target, varargin)
%CHECK_HDL_SUPPORT HDL Coder compatibility check for an RTL-bound subsystem.
%
% ok = CHECK_HDL_SUPPORT('model/HdlSubsystem')
% ok = CHECK_HDL_SUPPORT(..., 'Out', 'hdl_support.txt')
%
% Wraps checkhdl: catches blocks that cannot become RTL at all -- the other
% half of Task12-1 rule 1 ("must be built from blocks that generate RTL
% directly"). The m-code policy audit is audit_hdl_blocks.m; run both.
p = inputParser;
p.addParameter('Out', '');
p.parse(varargin{:});
model = strtok(target, '/');
if ~bdIsLoaded(model)
load_system(model);
end
txt = '';
threw = false;
try
txt = evalc('checkhdl(target)');
catch e
threw = true;
txt = sprintf('checkhdl threw: %s\n%s', e.identifier, e.message);
end
% The summary line is localised ("0 个错误、2 个警告" / "0 errors, 2 warnings"),
% so count both spellings rather than grepping for the word "error".
nErr = local_count(txt, {'(\d+)\s*个错误', '(\d+)\s+errors?'});
nWarn = local_count(txt, {'(\d+)\s*个警告', '(\d+)\s+warnings?'});
ok = ~threw && nErr == 0;
rep = regexp(txt, '(?<=file:///)[^''"<>]*_report\.html', 'match', 'once');
fprintf('=== checkhdl %s : %s (%d error(s), %d warning(s)) ===\n', ...
target, string(ok), nErr, nWarn);
if ~isempty(rep)
fprintf('report: %s\n', strrep(rep, '/', filesep));
end
disp(txt);
if nWarn > 0
fprintf(['NOTE: warnings are not a pass/fail gate here, but read them -- they are usually\n' ...
' "this block will cost you area/latency in RTL" hints.\n']);
end
if ~isempty(p.Results.Out)
fid = fopen(p.Results.Out, 'w');
fwrite(fid, txt);
fclose(fid);
fprintf('checkhdl log -> %s\n', p.Results.Out);
end
end
function n = local_count(txt, pats)
n = 0;
for i = 1:numel(pats)
t = regexp(txt, pats{i}, 'tokens', 'once');
if ~isempty(t)
n = str2double(t{1});
return
end
end
end
@@ -0,0 +1,17 @@
{
"_README": "Rule 2 of Task12-1: m code inside an RTL-bound subtree is only acceptable against a NAMED Xilinx IP, proven cycle-true. Copy this file to Notes/<model>_mcode_exemptions.json and fill it in. Every field is mandatory; check_exemptions.py rejects the entry otherwise.",
"model": "dpd_ntn_05",
"hdl_target": "dpd_ntn_05/DpdAlign_HDL",
"exemptions": [
{
"block": "dpd_ntn_05/DpdAlign_HDL/RxAlign_HDL/RxAlignCore",
"reason": "为什么原生 Simulink 模块搭不出来(必须具体:算法结构/时序/资源,不接受“太麻烦”)",
"xilinx_ip": "cordic 6.0 (Arc_Tan) + dist_mem_gen sin/cos ROM + blk_mem_gen SDP",
"ip_scope": "IP 覆盖的范围;剩下仍由 m 代码实现的部分要写清楚,它们仍然是欠账",
"cycle_true_evidence": "vivado/rtl_ip/dpd_ntn_05/cmp/cmp_result.txt",
"evidence_kind": "rtl-ip-replace 双 DUT 逐拍对比(首行须为 PASS)",
"approved_by": "bowei",
"date": "2026-07-24"
}
]
}
@@ -0,0 +1,50 @@
function hits = hdl_block_search(varargin)
%HDL_BLOCK_SEARCH Find a native, RTL-generatable Simulink block (Task12-1 rule 1).
%
% hdl_block_search list every block in the HDL Coder library
% hdl_block_search('ram') blocks whose name matches a keyword
% hdl_block_search('sin','cos') several keywords (OR)
%
% Source of truth is the HDL Coder block library (hdlsllib) plus the HDL
% Optimized blocks of DSP HDL / Wireless HDL / Vision HDL: anything in there
% generates RTL directly, so it is a legal building block. Anything NOT in
% there needs checkhdl before you rely on it.
% Sub-libraries are listed individually: the top-level DSP HDL library holds
% links, which find_system will not walk into.
libs = {'hdlsllib', ... % HDL Coder baseline set
'dsphdlfiltering2', 'dsphdlmathfun2', ... % FIR/CIC/LMS/Biquad, CORDIC
'dsphdlxfrm2', 'dsphdlsigops2', 'dsphdlsrcs2', ...% FFT/IFFT, Farrow/NCO/resample
'whdllib', 'visionhdllib'};
hits = {};
for i = 1:numel(libs)
try
load_system(libs{i});
catch
continue
end
blks = find_system(libs{i}, 'SearchDepth', 5, 'Type', 'Block');
for k = 1:numel(blks)
nm = strrep(get_param(blks{k}, 'Name'), newline, ' ');
if isempty(varargin) || any(cellfun(@(p) ~isempty(regexpi(nm, p, 'once')), varargin))
hits{end+1, 1} = nm; %#ok<AGROW>
hits{end, 2} = blks{k}; %#ok<AGROW>
end
end
end
if isempty(hits)
fprintf(['no HDL library block matches %s\n' ...
'-> if you cannot build it from these blocks, rule 2 applies: name the Xilinx IP\n' ...
' that will implement it, prove the swap cycle-true, and file the exemption.\n'], ...
strjoin(varargin, ', '));
return
end
[~, ia] = unique(hits(:, 1));
hits = hits(sort(ia), :);
fprintf('%d HDL-generatable block(s):\n', size(hits, 1));
for k = 1:size(hits, 1)
fprintf(' %-42s %s\n', hits{k, 1}, hits{k, 2});
end
end