三个通道的一组目标码需要一起生效,但控制端每拍只能写一个通道。直接把写值交给后级,会在更新途中出现"通道0已经是新目标,通道1和2还是旧目标"的混合状态。
本文用两组数字寄存器解决这个局部问题:shadow暂存逐通道写入,active仅在提交边沿取得整组快照。重点是多通道一致性、同拍优先级和非法索引。这里没有PWM周期发生器、DAC限速算法、SPI或LDAC驱动,不声称真实DAC模拟输出同时更新。
本次环境与结果
2026-10-26在 DESKTOP-396TDVB 使用已安装Vivado/XSim 2022.1 ,SW Build 3526262 、IP Build 3524634 ,仿真分辨率 1 ps。目录、源码、日志均属于今天的新实验。综合、STA、实现、上板均未执行。
| 测试 | 实际范围 |
|---|---|
| 主TB | 6个配置,合计12172个观察周期 |
| 独立TB | 10个配置、2799187个观察周期,其中529536个完整状态/输入穷举案例及60000个随机周期 |
| 参数 | W=1/2/8/16/32,CHANNELS=1/3/4/5 |
| 故意错误对照 | 4/4先成功编译展开,再触发功能断言 |
正常逻辑没有因测试失败而修改。独立TB初版漏记首次复位前的一个时钟沿,复核时修成t=0驱动复位并记录t=5 ns首个上升沿;原记录保留,最终统计来自重跑后的runs_review_final。这是测试记录覆盖修正,不是RTL功能缺陷。
契约先于实现
W、CHANNELS均至少为1;索引宽度IW=max(1,$clog2(CHANNELS)),保持派生默认值。码是W位不透明位型,不解释正负或换算电压。通道i对应打包总线i\*W +: W,通道0位于最低位段。
| 接口 | 定义 |
|---|---|
| clk/rst | 单时钟上升沿,高有效同步复位,优先级最高 |
| wr_valid/wr_index/wr_code | 本拍写请求、通道号、目标码;没有ready,每拍采样 |
| commit | 本拍提交整组目标 |
| shadow_codes | 暂存值,提交后仍保留 |
| active_codes | 最近一次提交的整组快照 |
| wr_error | 本拍非法索引写为1,次拍按新输入重算 |
| commit_pulse | 本拍非复位commit的登记标志,即使码未变也为1 |
所有输入必须已经在clk时钟域内。复位清零两组码和两个标志;非复位时先应用合法写,再把更新后的shadow作为提交快照。
| 条件 | shadow | active | 标志 |
|---|---|---|---|
| 同步复位 | 清零 | 清零 | 均清零 |
| 合法写,无提交 | 改指定通道 | 保持 | error=0,pulse=0 |
| 无写,有提交 | 保持 | 复制shadow | error=0,pulse=1 |
| 合法写+提交 | 改指定通道 | 包含本拍新写的整组快照 | error=0,pulse=1 |
| 非法写+提交 | 保持 | 复制原shadow | error=1,pulse=1 |
| wr_valid=0,索引非法 | 忽略索引 | 仍由commit决定 | error=0 |
同拍新写包括在提交中,是本文明确选择的契约,不是所有器件天然具有的规则。commit连续高表示每拍提交,commit_pulse可连续高,不是上升沿检测器。多次写同一shadow通道覆盖前值,模块不是请求队列,也没有dirty或"整组已写齐"检查。
CHANNELS=1时仍有1位索引,编码1非法;CHANNELS=3的编码3非法;CHANNELS=5的编码5/6/7非法。必须范围判断,不能把取模或截断当成正确通道选择。
实测快照与非法索引
W=8、CHANNELS=4时,主TB在25、35、45、55 ns依次写通道0~3的01/02/03/04,active保持零;65 ns提交整组。75 ns同拍写通道0=FF并提交,active得到FF/02/03/04。
85 ns把shadow通道3写零而不提交,active通道3保持04;105 ns提交后才变零。图每列为上升沿后1 ns采样,码为十六进制。

第二幅图取W=16、CHANNELS=5。非法索引5/6/7的写被忽略,error为1;同拍commit仍登记。wr_valid=0时,即使索引非法也没有写错误。

这两幅图来自本次CSV数字采样,不是模拟DAC波形,也不能用于证明组合毛刺或板级时序。
完整RTL:dac_shadow_bank.sv
next_shadow用组合逻辑合成待写快照,同一时钟边沿供shadow和active使用。若只在时序块里写shadow<=new、active<=shadow,非阻塞赋值让active看到旧shadow,会漏掉同拍新写。共享next值将优先级明确写出来。
systemverilog
`timescale 1ns/1ps
module dac_shadow_bank #(
parameter integer W=8,
parameter integer CHANNELS=3,
parameter integer IW=(CHANNELS<=1)?1:$clog2(CHANNELS)
)(
input wire clk,rst,
input wire wr_valid,
input wire [IW-1:0] wr_index,
input wire [W-1:0] wr_code,
input wire commit,
output reg [CHANNELS*W-1:0] shadow_codes,active_codes,
output reg wr_error,commit_pulse
);
// Contract: W>=1, CHANNELS>=1; keep IW at its derived default.
wire legal_write = wr_valid && (wr_index < CHANNELS);
reg [CHANNELS*W-1:0] next_shadow;
always @* begin
next_shadow = shadow_codes;
if(legal_write) next_shadow[wr_index*W +: W] = wr_code;
end
always @(posedge clk) begin
if(rst) begin
shadow_codes <= 0; active_codes <= 0;
wr_error <= 0; commit_pulse <= 0;
end else begin
shadow_codes <= next_shadow;
if(commit) active_codes <= next_shadow;
wr_error <= wr_valid && !legal_write;
commit_pulse <= commit;
end
end
endmodule
完整主测试:tb_bank.sv
主TB用数组建模,逐拍比较两个bank与标志,覆盖全部可编码索引、码边界、连续提交、写入提交冲突和复位。每配置2000个确定种子的随机周期。小配置还遍历输入组合,全状态笛卡尔积由独立TB完成。主CSV记录每个显式测试step;启动初始化复位拍不计入其12172行,最终独立TB则从首个上升沿完整记录。
systemverilog
`timescale 1ns/1ps
module bank_case #(parameter W=8,CHANNELS=3)(output reg finished);
localparam IW=(CHANNELS<=1)?1:$clog2(CHANNELS);
reg clk=0;
always #5 if(!finished) clk=~clk;
reg rst=1,wr_valid=0,commit=0;
reg [IW-1:0] wr_index=0;
reg [W-1:0] wr_code=0;
wire [CHANNELS*W-1:0] shadow_codes,active_codes;
wire wr_error,commit_pulse;
dac_shadow_bank #(.W(W),.CHANNELS(CHANNELS)) dut(.*);
reg [W-1:0] shadow_ref[0:CHANNELS-1],active_ref[0:CHANNELS-1];
reg [CHANNELS*W-1:0] shadow_expected,active_expected;
integer fd,cycle=0,phase=0,writes=0,invalid_writes=0,commits=0,same=0,resets=0;
reg [31:0] rng;
function automatic [31:0] random_word;
begin rng=rng^(rng<<13); rng=rng^(rng>>17); rng=rng^(rng<<5); random_word=rng; end
endfunction
task automatic step(input bit reset_i,input bit write_i,input integer index_i,
input [W-1:0] code_i,input bit commit_i);
integer k;
begin
@(negedge clk);
rst=reset_i; wr_valid=write_i; wr_index=index_i; wr_code=code_i; commit=commit_i;
@(posedge clk);
if(rst) begin
for(k=0;k<CHANNELS;k=k+1) begin shadow_ref[k]=0; active_ref[k]=0; end
resets=resets+1;
end else begin
if(wr_valid) begin
if(wr_index<CHANNELS) begin shadow_ref[wr_index]=wr_code; writes=writes+1; end
else invalid_writes=invalid_writes+1;
end
if(commit) begin
for(k=0;k<CHANNELS;k=k+1) active_ref[k]=shadow_ref[k];
commits=commits+1;
if(wr_valid && wr_index<CHANNELS) same=same+1;
end
end
for(k=0;k<CHANNELS;k=k+1) begin
shadow_expected[k*W+:W]=shadow_ref[k]; active_expected[k*W+:W]=active_ref[k];
end
#1;
$fdisplay(fd,"%0d,%0d,%0d,%0d,%0d,%0d,%h,%0d,%h,%h,%0d,%0d",cycle,$time,phase,rst,wr_valid,wr_index,wr_code,commit,shadow_codes,active_codes,wr_error,commit_pulse);
if($isunknown({shadow_codes,active_codes,wr_error,commit_pulse}) || shadow_codes!==shadow_expected || active_codes!==active_expected || wr_error!==(!rst && wr_valid && wr_index>=CHANNELS) || commit_pulse!==(!rst && commit))
$fatal(1,"BANK_MAIN_ASSERT W=%0d C=%0d cycle=%0d shadow=%h/%h active=%h/%h error=%b pulse=%b",W,CHANNELS,cycle,shadow_codes,shadow_expected,active_codes,active_expected,wr_error,commit_pulse);
cycle=cycle+1;
end
endtask
integer i,j,k;
initial begin
finished=0; rng=32'h2610babe+W+CHANNELS;
fd=$fopen($sformatf("bank_main_w%0d_c%0d.csv",W,CHANNELS),"w");
if(!fd) $fatal(1,"IO bank CSV");
$fdisplay(fd,"cycle,time_ns,phase,rst,wr_valid,wr_index,wr_code_hex,commit,shadow_hex,active_hex,wr_error,commit_pulse");
step(1,1,0,'1,1);
for(i=0;i<CHANNELS;i=i+1) step(0,1,i,i+1,0);
step(0,0,0,0,1);
step(0,1,0,'1,1);
step(0,1,CHANNELS-1,0,0);
step(0,0,0,0,0);
step(0,0,0,0,1); step(0,0,0,0,1);
for(i=CHANNELS;i<(1<<IW);i=i+1) begin
step(0,1,i,'1,0); step(0,1,i,'1,1); step(0,0,i,'1,0);
end
phase=1;
if(W<=2) begin
for(i=0;i<(1<<IW);i=i+1)
for(j=0;j<(1<<W);j=j+1)
for(k=0;k<4;k=k+1) step(0,k[0],i,j,k[1]);
end
phase=2;
for(i=0;i<2000;i=i+1) begin
step((i%199)==0,random_word()%2,random_word(),random_word(),random_word()%2);
end
phase=3;
step(1,1,0,'1,1); step(0,0,0,0,1);
if(writes==0 || commits==0 || same==0 || resets<3 || ((CHANNELS!=(1<<IW)) && invalid_writes==0))
$fatal(1,"BANK_MAIN_ASSERT missing coverage");
$display("BANK_MAIN_CASE W=%0d C=%0d cycles=%0d writes=%0d invalid=%0d commits=%0d same=%0d resets=%0d",W,CHANNELS,cycle,writes,invalid_writes,commits,same,resets);
$fclose(fd); finished=1;
end
endmodule
module tb_bank;
wire [5:0] finished;
bank_case #(.W(1),.CHANNELS(1)) c0(finished[0]);
bank_case #(.W(2),.CHANNELS(3)) c1(finished[1]);
bank_case #(.W(8),.CHANNELS(4)) c2(finished[2]);
bank_case #(.W(16),.CHANNELS(5)) c3(finished[3]);
bank_case #(.W(32),.CHANNELS(3)) c4(finished[4]);
bank_case #(.W(16),.CHANNELS(1)) c5(finished[5]);
initial begin wait(&finished); $display("TEST_PASS BANK_MAIN cases=6"); $finish; end
initial begin #1000000; $fatal(1,"WATCHDOG bank"); end
endmodule
完整独立测试:tb_bank_review.sv
独立复核者在看实现前,用无符号整数数组另写参考。穷举预状态经合法写/提交构造,不用force或修改DUT内部;构造状态的每个周期也检查。
每组状态数S=2(W×CHANNELS),索引编码数I=2IW。输入组合包括rst、wr_valid、commit、每个索引和每种码,每个小配置测试S²×8×I×2^W个案例。
| W | CHANNELS | 穷举案例 |
|---|---|---|
| 1 | 1 | 128 |
| 1 | 3 | 4096 |
| 2 | 1 | 1024 |
| 2 | 3 | 524288 |
每配置另跑6000随机周期。W=8/16/32分别搭配CHANNELS=4/5测试定向和随机行为;这些大配置不叫全状态穷举。输入改变后、上升沿之前还检查寄存器未提前变化,验证同步语义。
systemverilog
`timescale 1ns/1ps
// Independent contract-derived review bench. Written before viewing DUT/main TB.
// Each channel is modeled as an unsigned mathematical integer in two arrays.
// Legal stimulus reconstructs every exhaustive pre-state; no force/backdoor use.
module bank_review_case #(
parameter integer W = 1,
parameter integer CHANNELS = 1,
parameter integer EXHAUSTIVE = 1
)(output reg done);
localparam integer IW = (CHANNELS <= 1) ? 1 : $clog2(CHANNELS);
localparam integer INDEX_COUNT = (1 << IW);
localparam [63:0] CODE_MASK = (64'h1 << W) - 1;
localparam integer PH_SETUP = 0;
localparam integer PH_DIRECTED = 1;
localparam integer PH_RANDOM = 2;
localparam integer PH_EXHAUST = 3;
localparam integer RANDOM_CYCLES = 6000;
reg clk = 0;
reg rst = 0;
reg wr_valid = 0;
reg [IW-1:0] wr_index = 0;
reg [W-1:0] wr_code = 0;
reg commit = 0;
wire [CHANNELS*W-1:0] shadow_codes;
wire [CHANNELS*W-1:0] active_codes;
wire wr_error, commit_pulse;
// IW is deliberately not overridden: exercise the DUT's default formula.
dac_shadow_bank #(.W(W), .CHANNELS(CHANNELS)) dut (
.clk(clk), .rst(rst), .wr_valid(wr_valid),
.wr_index(wr_index), .wr_code(wr_code), .commit(commit),
.shadow_codes(shadow_codes), .active_codes(active_codes),
.wr_error(wr_error), .commit_pulse(commit_pulse)
);
longint unsigned model_shadow [0:CHANNELS-1];
longint unsigned model_active [0:CHANNELS-1];
longint unsigned goal_shadow [0:CHANNELS-1];
longint unsigned goal_active [0:CHANNELS-1];
reg [CHANNELS*W-1:0] expected_shadow, expected_active;
reg expected_error, expected_pulse;
reg initialized;
integer csv;
reg [31:0] prng;
longint unsigned cycles, exhaustive_cases, random_cases;
longint unsigned legal_writes, invalid_writes, ignored_indices;
longint unsigned commits, write_commits, invalid_commits, no_commit_holds;
longint unsigned shadow_kept, repeated_commits, unchanged_commits;
longint unsigned resets, reset_conflicts, sync_prechecks, multi_commits;
longint unsigned index_seen [0:INDEX_COUNT-1];
longint unsigned reset_control_seen [0:3];
always begin
#5;
if (!done) clk = ~clk;
end
function automatic [31:0] random_word;
reg [31:0] x;
begin
x = prng;
x = x ^ (x << 13);
x = x ^ (x >> 17);
x = x ^ (x << 5);
prng = x;
random_word = x;
end
endfunction
task automatic pack_model;
integer ch;
begin
expected_shadow = 0;
expected_active = 0;
for (ch = 0; ch < CHANNELS; ch = ch + 1) begin
expected_shadow[ch*W +: W] = model_shadow[ch];
expected_active[ch*W +: W] = model_active[ch];
end
end
endtask
task automatic fail(input string reason);
begin
$fflush(csv);
$display("BANK_REVIEW_CONTEXT W=%0d CHANNELS=%0d cycle=%0d rst=%0b wr_valid=%0b index=%0d code=%0h commit=%0b shadow=%0h expected_shadow=%0h active=%0h expected_active=%0h error=%0b expected_error=%0b pulse=%0b expected_pulse=%0b",
W, CHANNELS, cycles, rst, wr_valid, wr_index, wr_code, commit,
shadow_codes, expected_shadow, active_codes, expected_active,
wr_error, expected_error, commit_pulse, expected_pulse);
$fatal(1, "BANK_REVIEW_ASSERT %s W=%0d CHANNELS=%0d cycle=%0d", reason, W, CHANNELS, cycles);
end
endtask
task automatic step(
input bit in_rst,
input bit in_write,
input integer in_index,
input longint unsigned in_code,
input bit in_commit,
input integer phase,
input longint unsigned state_tag
);
integer ch, changed_channels;
reg [CHANNELS*W-1:0] prior_shadow, prior_active;
reg prior_pulse;
begin
// Prime reset at t=0 so no rising edge precedes the first CSV row.
// Subsequent stimulus changes occur on falling edges.
if (cycles != 0) @(negedge clk);
rst = in_rst;
wr_valid = in_write;
wr_index = in_index;
wr_code = in_code;
commit = in_commit;
// Synchronous controls must not change any registered output here.
#1;
if (initialized) begin
sync_prechecks = sync_prechecks + 1;
if ((shadow_codes !== expected_shadow) ||
(active_codes !== expected_active) ||
(wr_error !== expected_error) ||
(commit_pulse !== expected_pulse))
fail("output changed before active clock edge");
end
prior_shadow = expected_shadow;
prior_active = expected_active;
prior_pulse = expected_pulse;
@(posedge clk);
cycles = cycles + 1;
index_seen[in_index] = index_seen[in_index] + 1;
if (phase == PH_EXHAUST) exhaustive_cases = exhaustive_cases + 1;
if (phase == PH_RANDOM) random_cases = random_cases + 1;
expected_error = 0;
expected_pulse = 0;
if (in_rst) begin
for (ch = 0; ch < CHANNELS; ch = ch + 1) begin
model_shadow[ch] = 0;
model_active[ch] = 0;
end
resets = resets + 1;
reset_control_seen[(in_write ? 2 : 0) + (in_commit ? 1 : 0)] =
reset_control_seen[(in_write ? 2 : 0) + (in_commit ? 1 : 0)] + 1;
if (in_write || in_commit) reset_conflicts = reset_conflicts + 1;
end else begin
// Integer-array specification: first write, then copy the bank.
if (in_write) begin
if (in_index < CHANNELS) begin
model_shadow[in_index] = in_code & CODE_MASK;
legal_writes = legal_writes + 1;
if (in_commit) write_commits = write_commits + 1;
end else begin
expected_error = 1;
invalid_writes = invalid_writes + 1;
if (in_commit) invalid_commits = invalid_commits + 1;
end
end else if (in_index >= CHANNELS) begin
ignored_indices = ignored_indices + 1;
end
if (in_commit) begin
expected_pulse = 1;
commits = commits + 1;
changed_channels = 0;
for (ch = 0; ch < CHANNELS; ch = ch + 1) begin
if (model_active[ch] != model_shadow[ch])
changed_channels = changed_channels + 1;
model_active[ch] = model_shadow[ch];
end
if (changed_channels >= 2) multi_commits = multi_commits + 1;
if (changed_channels == 0) unchanged_commits = unchanged_commits + 1;
if (prior_pulse) repeated_commits = repeated_commits + 1;
end else begin
no_commit_holds = no_commit_holds + 1;
end
if (in_commit && (!in_write || in_index >= CHANNELS))
shadow_kept = shadow_kept + 1;
end
pack_model();
#1;
// Write observed and expected values BEFORE asserting, retaining failures.
$fdisplay(csv, "%0d,%0t,%0d,%0d,%0b,%0b,%0d,%0h,%0b,%0h,%0h,%0h,%0h,%0b,%0b,%0b,%0b",
cycles, $time, phase, state_tag, rst, wr_valid, wr_index,
wr_code, commit, shadow_codes, expected_shadow, active_codes,
expected_active, wr_error, expected_error, commit_pulse, expected_pulse);
if (shadow_codes !== expected_shadow) fail("shadow integer-array mismatch");
if (active_codes !== expected_active) fail("active integer-array mismatch");
if (wr_error !== expected_error) fail("write error pulse mismatch");
if (commit_pulse !== expected_pulse) fail("commit pulse mismatch");
if (!in_rst && !in_commit && active_codes !== prior_active)
fail("active changed without commit");
if (!in_rst && (!in_write || in_index >= CHANNELS) && shadow_codes !== prior_shadow)
fail("shadow changed without a legal write");
initialized = 1;
end
endtask
// Restore a requested state only through the public synchronous interface.
// All shadow/active pairs are reachable: stage active, commit, then stage shadow.
task automatic restore_state(
input longint unsigned active_state,
input longint unsigned shadow_state,
input longint unsigned state_tag
);
integer ch;
bit active_differs;
begin
active_differs = 0;
for (ch = 0; ch < CHANNELS; ch = ch + 1) begin
goal_active[ch] = (active_state >> (ch*W)) & CODE_MASK;
goal_shadow[ch] = (shadow_state >> (ch*W)) & CODE_MASK;
if (model_active[ch] != goal_active[ch]) active_differs = 1;
end
if (active_differs) begin
for (ch = 0; ch < CHANNELS; ch = ch + 1)
if (model_shadow[ch] != goal_active[ch])
step(0, 1, ch, goal_active[ch], 0, PH_SETUP, state_tag);
step(0, 0, 0, 0, 1, PH_SETUP, state_tag);
end
for (ch = 0; ch < CHANNELS; ch = ch + 1)
if (model_shadow[ch] != goal_shadow[ch])
step(0, 1, ch, goal_shadow[ch], 0, PH_SETUP, state_tag);
for (ch = 0; ch < CHANNELS; ch = ch + 1)
if ((model_active[ch] != goal_active[ch]) ||
(model_shadow[ch] != goal_shadow[ch]))
fail("review bench failed to construct requested state");
end
endtask
task automatic run_directed;
integer ch, idx, ctrl, pattern;
longint unsigned value;
begin
// Distinct nonzero per-channel values also exercise packed lane ordering.
for (ch = 0; ch < CHANNELS; ch = ch + 1)
step(0, 1, ch, ((64'h9e3779b9 * (ch + 1)) & CODE_MASK), 0, PH_DIRECTED, 0);
step(0, 0, INDEX_COUNT-1, CODE_MASK, 1, PH_DIRECTED, 0);
step(0, 0, INDEX_COUNT-1, 0, 1, PH_DIRECTED, 0);
step(0, 0, INDEX_COUNT-1, CODE_MASK, 1, PH_DIRECTED, 0);
step(0, 0, INDEX_COUNT-1, 0, 0, PH_DIRECTED, 0);
for (idx = 0; idx < INDEX_COUNT; idx = idx + 1) begin
for (pattern = 0; pattern < 5; pattern = pattern + 1) begin
case (pattern)
0: value = 0;
1: value = CODE_MASK;
2: value = 64'h1 << (W-1);
3: value = 64'ha5a5a5a5 & CODE_MASK;
default: value = (idx + 1) & CODE_MASK;
endcase
step(0, 1, idx, value, 0, PH_DIRECTED, 0);
step(0, 0, idx, value ^ CODE_MASK, 0, PH_DIRECTED, 0);
step(0, 1, idx, value ^ CODE_MASK, 1, PH_DIRECTED, 0);
step(0, 0, idx, value, 1, PH_DIRECTED, 0);
end
end
// Every reset/write/commit control combination, every encoded index.
// Rebuild nonzero shadow and active first to make priority observable.
for (ctrl = 0; ctrl < 4; ctrl = ctrl + 1) begin
for (idx = 0; idx < INDEX_COUNT; idx = idx + 1) begin
for (ch = 0; ch < CHANNELS; ch = ch + 1)
step(0, 1, ch, CODE_MASK, 0, PH_DIRECTED, 0);
step(0, 0, 0, 0, 1, PH_DIRECTED, 0);
step(1, (ctrl & 2) != 0, idx, CODE_MASK, (ctrl & 1) != 0, PH_DIRECTED, 0);
end
end
end
endtask
task automatic run_exhaustive;
longint unsigned states, a, s, tag, expected_cases;
integer reset_bit, write_bit, commit_bit, idx, code;
begin
states = 64'h1 << (W*CHANNELS);
expected_cases = states * states * 8 * INDEX_COUNT * (1 << W);
for (a = 0; a < states; a = a + 1) begin
for (s = 0; s < states; s = s + 1) begin
tag = a * states + s;
for (reset_bit = 0; reset_bit < 2; reset_bit = reset_bit + 1)
for (write_bit = 0; write_bit < 2; write_bit = write_bit + 1)
for (commit_bit = 0; commit_bit < 2; commit_bit = commit_bit + 1)
for (idx = 0; idx < INDEX_COUNT; idx = idx + 1)
for (code = 0; code < (1 << W); code = code + 1) begin
restore_state(a, s, tag);
step(reset_bit != 0, write_bit != 0, idx, code,
commit_bit != 0, PH_EXHAUST, tag);
end
end
end
if (exhaustive_cases != expected_cases) fail("exhaustive Cartesian count mismatch");
$display("BANK_REVIEW_EXHAUST W=%0d CHANNELS=%0d states_per_bank=%0d state_pairs=%0d cases=%0d expected=%0d",
W, CHANNELS, states, states*states, exhaustive_cases, expected_cases);
end
endtask
initial begin : test_case
integer j, n, ri;
reg [31:0] rcontrol, rcode;
done = 0;
initialized = 0;
expected_error = 0;
expected_pulse = 0;
cycles = 0; exhaustive_cases = 0; random_cases = 0;
legal_writes = 0; invalid_writes = 0; ignored_indices = 0;
commits = 0; write_commits = 0; invalid_commits = 0; no_commit_holds = 0;
shadow_kept = 0; repeated_commits = 0; unchanged_commits = 0;
resets = 0; reset_conflicts = 0; sync_prechecks = 0; multi_commits = 0;
prng = 32'h6d2b79f5 ^ (W * 32'h10203) ^ (CHANNELS * 32'h40507);
for (j = 0; j < CHANNELS; j = j + 1) begin
model_shadow[j] = 0;
model_active[j] = 0;
end
for (j = 0; j < INDEX_COUNT; j = j + 1) index_seen[j] = 0;
for (j = 0; j < 4; j = j + 1) reset_control_seen[j] = 0;
pack_model();
csv = $fopen($sformatf("bank_review_w%0d_c%0d.csv", W, CHANNELS), "w");
if (csv == 0) $fatal(1, "BANK_REVIEW_ASSERT cannot open CSV W=%0d CHANNELS=%0d", W, CHANNELS);
$fdisplay(csv, "cycle,time_ps,phase,state_tag,rst,wr_valid,wr_index,wr_code,commit,shadow,expected_shadow,active,expected_active,wr_error,expected_error,commit_pulse,expected_pulse");
step(1, 1, INDEX_COUNT-1, CODE_MASK, 1, PH_DIRECTED, 0);
run_directed();
if (EXHAUSTIVE) run_exhaustive();
for (n = 0; n < RANDOM_CYCLES; n = n + 1) begin
rcontrol = random_word();
rcode = random_word();
ri = (rcontrol >> 16) % INDEX_COUNT;
step((rcontrol[7:2] == 0), rcontrol[0] || rcontrol[8], ri,
rcode & CODE_MASK, rcontrol[1], PH_RANDOM, 0);
end
step(0, 0, INDEX_COUNT-1, CODE_MASK, 0, PH_DIRECTED, 0);
if (random_cases != RANDOM_CYCLES) fail("random count mismatch");
if (legal_writes == 0 || write_commits == 0 || no_commit_holds == 0 ||
shadow_kept == 0 || repeated_commits == 0 || unchanged_commits == 0 ||
reset_conflicts == 0 || sync_prechecks != cycles-1)
fail("required basic coverage missing");
if (CHANNELS < INDEX_COUNT &&
(invalid_writes == 0 || ignored_indices == 0 || invalid_commits == 0))
fail("required invalid-index coverage missing");
if (CHANNELS > 1 && multi_commits == 0) fail("multiple-channel commit coverage missing");
for (j = 0; j < INDEX_COUNT; j = j + 1)
if (index_seen[j] == 0) fail("encoded index not exercised");
for (j = 0; j < 4; j = j + 1)
if (reset_control_seen[j] == 0) fail("reset control combination not exercised");
$display("BANK_REVIEW_CASE W=%0d CHANNELS=%0d cycles=%0d exhaustive=%0d random=%0d legal_write=%0d invalid_write=%0d ignored_index=%0d commits=%0d write_commit=%0d invalid_commit=%0d no_commit=%0d shadow_keep=%0d repeated_commit=%0d unchanged_commit=%0d reset=%0d reset_conflict=%0d sync_precheck=%0d multi_commit=%0d indices=%0d reset_controls=4",
W, CHANNELS, cycles, exhaustive_cases, random_cases, legal_writes,
invalid_writes, ignored_indices, commits, write_commits, invalid_commits,
no_commit_holds, shadow_kept, repeated_commits, unchanged_commits,
resets, reset_conflicts, sync_prechecks, multi_commits, INDEX_COUNT);
$fclose(csv);
$display("BANK_REVIEW_CASE_PASS W=%0d CHANNELS=%0d", W, CHANNELS);
done = 1;
end
endmodule
module tb_bank_review;
wire [9:0] done;
bank_review_case #(.W(1), .CHANNELS(1), .EXHAUSTIVE(1)) c0(done[0]);
bank_review_case #(.W(1), .CHANNELS(3), .EXHAUSTIVE(1)) c1(done[1]);
bank_review_case #(.W(2), .CHANNELS(1), .EXHAUSTIVE(1)) c2(done[2]);
bank_review_case #(.W(2), .CHANNELS(3), .EXHAUSTIVE(1)) c3(done[3]);
bank_review_case #(.W(8), .CHANNELS(4), .EXHAUSTIVE(0)) c4(done[4]);
bank_review_case #(.W(8), .CHANNELS(5), .EXHAUSTIVE(0)) c5(done[5]);
bank_review_case #(.W(16), .CHANNELS(4), .EXHAUSTIVE(0)) c6(done[6]);
bank_review_case #(.W(16), .CHANNELS(5), .EXHAUSTIVE(0)) c7(done[7]);
bank_review_case #(.W(32), .CHANNELS(4), .EXHAUSTIVE(0)) c8(done[8]);
bank_review_case #(.W(32), .CHANNELS(5), .EXHAUSTIVE(0)) c9(done[9]);
initial begin
wait (&done);
$display("TEST_PASS BANK_REVIEW configs=10 exhaustive_configs=4 large_configs=6 exhaustive_cases=529536 random_cases=60000");
$finish;
end
initial begin
#100000000;
$fatal(1, "BANK_REVIEW_ASSERT watchdog timeout done=%b", done);
end
endmodule
从正文直接复制运行
将上述三个文件按标题名称保存,在同一新目录打开PowerShell运行下面脚本。无需取得私人ZIP或安装Python包;只使用已有Vivado。每次采用新运行目录保留旧结果。
powershell
# 当前目录保存本文三个 SV 文件;每次使用新的运行子目录。
$vivadoBin = 'D:\tools\Xilinx\Vivado\2022.1\bin'
function Invoke-Lab([string]$name, [string]$tb, [string]$pass) {
if (Test-Path -LiteralPath $name) { throw "请换一个不存在的运行目录: $name" }
New-Item -ItemType Directory -Path $name | Out-Null
Copy-Item -LiteralPath 'dac_shadow_bank.sv', ($tb + '.sv') -Destination $name
Push-Location $name
try {
& "$vivadoBin\xvlog.bat" -sv 'dac_shadow_bank.sv' ($tb + '.sv') 2>&1 | Tee-Object xvlog.console.log
if ($LASTEXITCODE -ne 0) { throw 'xvlog failed' }
& "$vivadoBin\xelab.bat" $tb -s sim -debug typical 2>&1 | Tee-Object xelab.console.log
if ($LASTEXITCODE -ne 0) { throw 'xelab failed' }
@('open_vcd trace.vcd','log_vcd /','run 4000 ns','close_vcd','run all','quit') |
Set-Content -Encoding Ascii run.tcl
& "$vivadoBin\xsim.bat" sim -tclbatch run.tcl -wdb trace.wdb 2>&1 | Tee-Object xsim.console.log
if ($LASTEXITCODE -ne 0) { throw 'xsim process failed' }
$result = Get-Content -Raw xsim.console.log
if ($result -notmatch [regex]::Escape($pass) -or $result -match 'Fatal:|ERROR:|Error:') {
throw '缺少 PASS 或出现仿真错误,请保留日志'
}
} finally { Pop-Location }
}
Invoke-Lab 'bank_main_run' 'tb_bank' 'TEST_PASS BANK_MAIN'
Invoke-Lab 'bank_review_run' 'tb_bank_review' 'TEST_PASS BANK_REVIEW'
实际自动执行使用同样的xvlog -sv、xelab -s sim -debug typical和xsim -tclbatch run.tcl -wdb trace.wdb选项。VCD仅记录前4000 ns,CSV保留完整测试观察序列;WDB为工具保存文件,不据此声称全程信号都已记录。XSim可能在fatal后仍返回0,因此必须检查PASS与错误文本。
实际结果与故意反例
正常主测试、最终独立测试的三个工具阶段退出码均为0,并有以下逐配置计数和PASS。cycles包括预状态构造拍,不能与exhaustive相加当成独立覆盖案例。
text
BANK_MAIN_CASE W=8 C=4 cycles=2013 writes=1005 invalid=0 commits=989 same=490 resets=13
BANK_MAIN_CASE W=16 C=1 cycles=2013 writes=495 invalid=509 commits=1007 same=238 resets=13
BANK_MAIN_CASE W=32 C=3 cycles=2015 writes=743 invalid=258 commits=1032 same=372 resets=13
BANK_MAIN_CASE W=16 C=5 cycles=2023 writes=630 invalid=374 commits=1022 same=335 resets=13
BANK_MAIN_CASE W=1 C=1 cycles=2029 writes=536 invalid=516 commits=1008 same=262 resets=13
BANK_MAIN_CASE W=2 C=3 cycles=2079 writes=800 invalid=269 commits=1020 same=411 resets=13
TEST_PASS BANK_MAIN cases=6
text
BANK_REVIEW_CASE W=8 CHANNELS=4 cycles=6186 exhaustive=0 random=6000 legal_write=4597 invalid_write=0 ignored_index=0 commits=3051 write_commit=2279 invalid_commit=0 no_commit=3038 shadow_keep=772 repeated_commit=1540 unchanged_commit=456 reset=97 reset_conflict=80 sync_precheck=6185 multi_commit=910 indices=4 reset_controls=4
BANK_REVIEW_CASE W=16 CHANNELS=4 cycles=6186 exhaustive=0 random=6000 legal_write=4590 invalid_write=0 ignored_index=0 commits=3043 write_commit=2309 invalid_commit=0 no_commit=3035 shadow_keep=734 repeated_commit=1501 unchanged_commit=413 reset=108 reset_conflict=88 sync_precheck=6185 multi_commit=911 indices=4 reset_controls=4
BANK_REVIEW_CASE W=32 CHANNELS=4 cycles=6186 exhaustive=0 random=6000 legal_write=4479 invalid_write=0 ignored_index=0 commits=2982 write_commit=2197 invalid_commit=0 no_commit=3090 shadow_keep=785 repeated_commit=1442 unchanged_commit=461 reset=114 reset_conflict=98 sync_precheck=6185 multi_commit=894 indices=4 reset_controls=4
BANK_REVIEW_CASE W=1 CHANNELS=1 cycles=6347 exhaustive=128 random=6000 legal_write=2334 invalid_write=2230 ignored_index=785 commits=3071 write_commit=1114 invalid_commit=1128 no_commit=3117 shadow_keep=1957 repeated_commit=1481 unchanged_commit=2217 reset=159 reset_conflict=125 sync_precheck=6346 multi_commit=0 indices=2 reset_controls=4
BANK_REVIEW_CASE W=8 CHANNELS=5 cycles=6395 exhaustive=0 random=6000 legal_write=2993 invalid_write=1648 ignored_index=626 commits=3134 write_commit=1459 invalid_commit=849 no_commit=3131 shadow_keep=1675 repeated_commit=1546 unchanged_commit=1176 reset=130 reset_conflict=112 sync_precheck=6394 multi_commit=523 indices=8 reset_controls=4
BANK_REVIEW_CASE W=16 CHANNELS=5 cycles=6395 exhaustive=0 random=6000 legal_write=3018 invalid_write=1653 ignored_index=612 commits=3108 write_commit=1443 invalid_commit=837 no_commit=3162 shadow_keep=1665 repeated_commit=1546 unchanged_commit=1149 reset=125 reset_conflict=107 sync_precheck=6394 multi_commit=530 indices=8 reset_controls=4
BANK_REVIEW_CASE W=32 CHANNELS=5 cycles=6395 exhaustive=0 random=6000 legal_write=3025 invalid_write=1703 ignored_index=612 commits=3043 write_commit=1428 invalid_commit=824 no_commit=3233 shadow_keep=1615 repeated_commit=1484 unchanged_commit=1098 reset=119 reset_conflict=102 sync_precheck=6394 multi_commit=547 indices=8 reset_controls=4
BANK_REVIEW_CASE W=2 CHANNELS=1 cycles=8871 exhaustive=1024 random=6000 legal_write=3522 invalid_write=2403 ignored_index=875 commits=3835 write_commit=1165 invalid_commit=1177 no_commit=4422 shadow_keep=2670 repeated_commit=1498 unchanged_commit=1863 reset=614 reset_conflict=472 sync_precheck=8870 multi_commit=0 indices=2 reset_controls=4
BANK_REVIEW_CASE W=1 CHANNELS=3 cycles=22361 exhaustive=4096 random=6000 legal_write=13611 invalid_write=1407 ignored_index=622 commits=6704 write_commit=2043 invalid_commit=727 no_commit=13495 shadow_keep=4661 repeated_commit=1581 unchanged_commit=1922 reset=2162 reset_conflict=1636 sync_precheck=22360 multi_commit=2234 indices=4 reset_controls=4
BANK_REVIEW_CASE W=2 CHANNELS=3 cycles=2723865 exhaustive=524288 random=6000 legal_write=1908061 invalid_write=33852 ignored_index=33136 commits=521127 write_commit=50797 invalid_commit=16941 no_commit=1940467 shadow_keep=470330 repeated_commit=3348 unchanged_commit=3339 reset=262271 reset_conflict=196721 sync_precheck=2723864 multi_commit=442740 indices=4 reset_controls=4
TEST_PASS BANK_REVIEW configs=10 exhaustive_configs=4 large_configs=6 exhaustive_cases=529536 random_cases=60000
| 故意变体 | 注入错误 | 首条功能断言 |
|---|---|---|
| bank_old_snapshot | 同拍提交漏掉新写 | BANK_REVIEW_ASSERT active integer-array mismatch W=1 CHANNELS=1 cycle=9 |
| bank_write_leak | 普通写直接改变active | BANK_REVIEW_ASSERT active integer-array mismatch W=1 CHANNELS=1 cycle=2 |
| bank_invalid_alias | 非法索引取模写合法通道 | BANK_REVIEW_ASSERT shadow integer-array mismatch W=2 CHANNELS=1 cycle=27 |
| bank_clear_shadow | 提交后清空shadow | BANK_REVIEW_ASSERT shadow integer-array mismatch W=1 CHANNELS=1 cycle=3 |
这些是主动注入的故障,正常实现没有这些行为。每个变体保留源码、哈希、编译/展开成功记录及首条功能断言。手工复现同拍错误,可复制RTL,将 active_codes <= next_shadow; 改成 active_codes <= shadow_codes; 再跑独立TB;异码写与提交同拍的向量会捕获它。
第一次开发运行曾因VCD Tcl对象路径错误而未执行测试,日志保留;修正后两个故意占位核的功能失败用于测试先行,不计入上述4个变体。最终bank独立TB修正首拍记录后完整重跑,正常RTL未改。
从数字原子提交到系统集成
所有active通道在同一逻辑边沿更新,只保证本模块数字寄存器快照。如果串行发送器跨周期依次读取通道0、1、2,而中途发生下一次提交,仍可能拼出新旧混合的一组。发送器需要在一轮开始时另取快照,或由系统在一轮发送期间冻结提交;本模块没有添加这层事务机制。同一时钟边沿的其他时序逻辑读取active会得到边沿前旧值,需要约定下一拍读取新快照或采用握手。
这也不等于模拟DAC同时变化。实际器件还涉及串行装载、更新控制、时序和模拟建立过程。ADI AD5686R/AD5685R/AD5684R Rev. F第23页介绍输入寄存器与DAC寄存器双缓冲和LDAC控制,只是背景;今天的RTL没有实现该芯片的协议、管脚或功能全集,不宣称器件兼容。
影子暂存也不等于"所有通道已更新":只写通道0就提交,其余通道保留旧shadow值,这是契约允许的。若需要整组完整性、版本号、发送中快照排队或丢弃,需要另行定义系统策略。
参考访问日2026-10-26:ADI数据手册实际Rev. F,29页;工具依据 AMD UG900 2022.1。代码、TB、图为本次独立编写/生成,官方资料只作背景事实核查。没有综合、STA、实现、上板或模拟输出测试。
工程接入时先画出这条时间线:控制端写完一组并提交,到串行发送器读完所有通道,期间允许几次新的提交?
与实测源码对应的SHA256
| 文件 | SHA256 |
|---|---|
| src/dac_shadow_bank.sv | 398331502e366343605d72cd6a305bd12c76dda734845580412fea3b6478e1bb |
| src/tb_bank.sv | 1262bb0293af596a574495b85b8f754e0682626f8b3a0ab60201773cd0694f9c |
| review/tb_bank_review.sv | 71dd24cdfe8164c1e5df4ccfff524776472cc0eb58d01315f939b2894ecd8370 |