FPGA 多通道 DAC 目标码影子寄存器:成组提交、同拍写入与快照边界

三个通道的一组目标码需要一起生效,但控制端每拍只能写一个通道。直接把写值交给后级,会在更新途中出现"通道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
相关推荐
t_niu_niu4 小时前
基于多帧累加平均的图像传感器暗电流抑制方法与 FPGA 实现
fpga开发·ram·fpn·固定模式噪声·image sensor
幻智星科技4 小时前
FPGA纯硬件UDP桥接:微秒级响应新范式
c++·fpga开发·硬件架构
小蘇的FPGA4 小时前
基于FPGA实现PCIe H2C过程中TAG的一种管理方法
fpga开发
威嵌神州5 小时前
一台加固平板,能替掉几台现场设备?
嵌入式硬件·fpga开发·电脑·平板
幻智星科技17 小时前
树形架构破解FPGA扇出瓶颈
fpga开发·硬件架构
FPGA小迷弟21 小时前
FPGA开发Vivado Block Design/Vitis/SDK/xlinx/开发教程,保姆级别,一看就会!
网络协议·tcp/ip·fpga开发·verilog·fpga
FPGA小迷弟21 小时前
基于FPGA实现智能网卡的概念,什么是智能网卡
网络协议·tcp/ip·fpga开发·verilog·fpga
好焊孙辉1 天前
螺柱焊机哪家质量好
python·fpga开发
XMAIPC_Robot1 天前
基于 i.MX95 平台在新一代工业控制器中的应用 — 工业车载平板、半导体设备主控方案解析
arm开发·人工智能·机器学习·fpga开发·rk3588+fpga·i.mx95