星夜离尘56头像
FPGA 一路数据送两路接收端:独立背压下如何保证每路只收到一次封面图

FPGA 一路数据送两路接收端:独立背压下如何保证每路只收到一次

同一份采样数据需要同时送到计算模块和记录模块。计算模块多数时候能接收,记录模块偶尔停顿。如果把输入 valid 直接连到两个输出,只把上游 ready 写成两个 ready 的与,先就绪的一路可能反复收到同一份数据。

问题不在载荷复制,而在于每个接收端的进度需要分别记住。本文用一个载荷寄存器和两个 pending 位,实现一路输入、两路独立 ready 的精确复制:每个输入在每个输出上恰好握手一次,两路可以在不同的时钟周期完成。

实际验证环境:Windows 上的 Vivado/XSim 2022.1,SW Build 3526262,2026-11-06。 4组载荷宽度配置共完成48,408个自检周期,4个指定RTL变异与2个非法参数均被捕获;独立测试台另完成316,125次断言。以下时序表是契约推导,不冒充波形截图。验证仅为RTL二态行为,没有综合、实现、时序闭合或板级证据。

1. 先看会重复数据的直连方案

下面这组关系很常见,也很危险:

m0_valid = s_valid
m1_valid = s_valid
s_ready  = m0_ready && m1_ready

假设输入持续给出 A,输出0一直 ready,输出1连续三拍不 ready。上游因为 s_ready=0 保持 A,但输出0每拍都看到自己的 valid 和 ready 同时为 1,于是把 A 接收了三次。

“上游没有握手”不代表“某个下游没有握手”。三个接口各自有自己的握手事件。ready/valid 的基本传输条件可参考 AMD PG256 的 AXI4-Stream Interface。将多个接口相连时,必须保证这些独立事件的数量关系正确。

也不要简单把输出0的 valid 与输出1的 ready 相与。那会让一个输出的 valid 依赖另一个接收端的即时 ready,并可能在已经有效但被背压时撤回 valid。这种接法很难维持清晰的端口约定。

2. 两个 pending 位就是两个独立收据

本模块保存一份 data_q,以及两位 pending:

  • pending[0]=1:输出0还没有收到当前项。
  • pending[1]=1:输出1还没有收到当前项。
  • 两位都为 0:没有未完成的项,可以接收下一份输入。

输出 valid 直接取对应 pending 位。某路完成握手后,只清除自己的位,另一位不受影响。

以 pending[1:0] 的顺序表示状态,下面是预期时序:

边沿前 pending载荷ready0ready1本拍输出握手本拍输入边沿后 pending/载荷
00无00无接受 A11 / A
11A10输出0收到 AB 被背压10 / A
10A10无B 被背压10 / A
10A11输出1收到 A接受 B11 / B
11B11两路各收到 B无00 / B

第三行输出0虽然仍然 ready,但自己的 pending 位已经清零,所以不再握手。末行后 data_q 仍可以保留 B,valid 为 0 时数据内容不代表新的传输。

3. 最后一份副本取走时,可以同拍装入下一项

如果输入 ready 只在 pending==0 时拉高,每一份数据完全送出后都要再等一拍才能装入下一份,双方持续就绪时也会出现空泡。

本实现先计算本拍输出握手之后还剩哪些副本:

remaining = pending & ~{m1_ready, m0_ready}
s_ready   = !rst && (remaining == 0)

pending 为 0 的分支无论 ready 如何都不会影响 remaining。因此,已经完成的分支后来拉低 ready,也不会阻止另外一份副本结束。

当 remaining 为 0,当前数据本拍结束,或者当前本来就是空闲,可以接受新输入。时序过程先清除旧副本的 pending,再由新输入握手把它设成 11。新输入的装载必须优先于旧项的清除。 两处非阻塞赋值写同一个寄存器时,代码中的后一次赋值定义了这个优先级。

这也解释了为什么载荷可以共用一个寄存器:只有所有旧副本都在本拍完成,才允许覆盖 data_q。被背压的接收端不会在未握手时看到载荷更换。

如果两个接收端持续 ready,填入第一项后,模块可以每拍完成一份旧项并装入一份新项。这里说的是协议吞吐能力,不是测得的时钟频率。输入 ready 有从两个下游 ready 经过组合逻辑回传的路径,接入长链路时仍需检查时序、组合环和必要的寄存切分。

4. 复位、慢分支与元数据要单独约定

本文采用单时钟、同步高有效复位。rst 为 1 时屏蔽三处握手;在上升沿清空 pending 与载荷寄存器。所有输入信号必须来自同一时钟域,异步输入需要另行处理。

复位会取消还没有送出的副本。例如输出0已经收到 A、输出1尚未收到时发生复位,系统不能撤回输出0的 A。因此“每个输入恰好复制两次”只对一个没有复位的运行区间成立。需要跨复位一致性时,应增加事务编号、重放或系统级恢复协议,不能靠两位 pending 自动补齐。

一个分支永久不 ready,最终就会阻止新输入。该模块只保存一项,并未为快分支提供无限队列。若需要让快分支继续运行,可以考虑给各分支增加 FIFO,并明确队列满后的整体策略。

DATA_W 允许 1~64。真正接 AXI4-Stream 时,帧结束、字节有效和用户标签也必须与数据一起复制。可以先把这些字段打包进载荷,再在各输出拆开;示例并没有单独提供 TLAST/TKEEP/TUSER 端口,不能直接当作完整 AXI4-Stream IP。

5. 完整 RTL

保存为 stream_fork2.sv。文章代码由同名文件直接嵌入,数据与控制的所有寄存状态都在下面。

本次原生结果绑定实际受测源码哈希;下面发布副本仅更新第2行过时注释,其他字节经独立核对一致。没有把新注释对应的文件哈希宣称为重新跑过XSim。

// One registered payload, two independently consumed copies.
// Behavioral example; synthesis and hardware validation not performed.
module stream_fork2 #(
    parameter integer DATA_W=16,
    localparam integer DW=(DATA_W<1 || DATA_W>64) ? 1 : DATA_W
) (
    input logic clk, rst,
    input logic s_valid,
    output logic s_ready,
    input logic [DW-1:0] s_data,
    output logic m0_valid,
    input logic m0_ready,
    output logic [DW-1:0] m0_data,
    output logic m1_valid,
    input logic m1_ready,
    output logic [DW-1:0] m1_data
);
    logic [DW-1:0] data_q;
    logic [1:0] pending, remaining;
    assign remaining=pending & ~{m1_ready,m0_ready};
    assign s_ready=!rst && (remaining == 2'b00);
    assign m0_valid=!rst && pending[0];
    assign m1_valid=!rst && pending[1];
    assign m0_data=data_q;
    assign m1_data=data_q;
    always_ff @(posedge clk) begin
        if(rst) begin
            pending <= 2'b00;
            data_q <= '0;
        end else begin
            pending <= remaining;
            // Replacement must take priority over clearing the old copies.
            if(s_valid && s_ready) begin
                data_q <= s_data;
                pending <= 2'b11;
            end
        end
    end
    // synthesis translate_off
    initial begin
        if(DATA_W<1 || DATA_W>64) $fatal(1,"PARAM_ERROR stream_fork2");
    end
    // synthesis translate_on
endmodule

6. 记分板按每个输出单独记账

只比较 m0_data==m1_data 不能发现重复传输:两路都反复输出 A,数据比较仍然成立。测试台需要按输入序列给两个输出分别计数。

完整自检使用一个期望数组和三个游标:

written:本轮复位以来接受的输入数量
read0:输出0已接受的数量
read1:输出1已接受的数量

当输出0握手,必须等于 expected[read0],随后只增加 read0;输出1类似。输入握手才增加 written。由于模块只容纳一项,每一路还应满足:

0 <= written - read0 <= 1
0 <= written - read1 <= 1

一个周期可以既消费旧项,又接受新项。测试台必须先根据边沿前的 written/read 状态检查和消费旧输出,再把新输入追加到期望数组。顺序写反会把刚输入的数据当作可以同拍穿透输出的数据,掩盖延迟错误。

测试包含输出0先收、输出1先收、某一路连续停顿、两路连续就绪、最后一份副本与新输入同拍、部分副本已送出后复位,以及固定种子的独立随机背压。64位配置包含全1、最高位、交替位和高低两半分别生成的随机载荷,避免只测到低32位。

Python 参考检查把“待接收端集合减去本拍就绪端集合”与两位布尔表达式对照,遍历4种 pending、4种 ready 和2种输入 valid,共32种状态/输入组合;4种参考模型错误都找到了反例。这并不是 RTL 仿真,也不是形式化工具证明。

原生回归分别修改了“任意一路收到就清空两位”“先完成的分支不清位”“任意一路ready就接新输入”“旧状态清除覆盖新项装载”,四种变异均在成功编译展开后被对应断言捕获。源代码无法编译、仿真超时或不相关的错误,没有充当这些测试有效的证据。

7. 完整自检测试台

保存为 tb_stream_fork2.sv。每次 step 在下降沿驱动输入,检查边沿前的控制与数据,再等待上升沿更新。CSV记录每个分支自己的握手,便于直接数出 A 在每一路究竟出现了几次。

`timescale 1ns/1ps
module tb_stream_fork2 #(parameter integer DATA_W=16);
    logic clk=0;
    always #5 clk=~clk;
    logic rst=1,s_valid=0,s_ready,m0_valid,m0_ready=0,m1_valid,m1_ready=0;
    logic [DATA_W-1:0] s_data='0,m0_data,m1_data;
    stream_fork2 #(.DATA_W(DATA_W)) dut(.*);
    logic [DATA_W-1:0] expected[0:20000];
    integer written=0, read0=0, read1=0, checks=0, accepted_total=0;
    integer trace_fd;
    bit last_accept=0, stall0=0, stall1=0;
    logic [DATA_W-1:0] held0,held1;
    integer unsigned rng=32'h71d602b3;
    function automatic integer unsigned next_random();
        rng=rng^(rng<<13); rng=rng^(rng>>17); rng=rng^(rng<<5); return rng;
    endfunction
    function automatic logic [DATA_W-1:0] next_payload();
        logic [63:0] word;
        word[31:0]=next_random(); word[63:32]=next_random();
        return DATA_W'(word);
    endfunction
    task automatic step(input bit r,v, input logic[DATA_W-1:0] d, input bit r0,r1);
        bit p0,p1, expected_ready;
        @(negedge clk); rst=r;s_valid=v;s_data=d;m0_ready=r0;m1_ready=r1; #1;
        p0=(read0<written); p1=(read1<written);
        expected_ready=!r && ((!p0 || r0) && (!p1 || r1));
        if(s_ready !== expected_ready || m0_valid !== (!r && p0) || m1_valid !== (!r && p1))
            $fatal(1,"ASSERT_FORK_CONTROL checks=%0d wr=%0d r0=%0d r1=%0d",checks,written,read0,read1);
        if(!r) begin
            if(p0 && m0_data !== expected[read0]) $fatal(1,"ASSERT_FORK_DATA0");
            if(p1 && m1_data !== expected[read1]) $fatal(1,"ASSERT_FORK_DATA1");
            if(stall0 && (!m0_valid || m0_data!==held0)) $fatal(1,"ASSERT_FORK_STABILITY0");
            if(stall1 && (!m1_valid || m1_data!==held1)) $fatal(1,"ASSERT_FORK_STABILITY1");
        end
        stall0=!r && p0 && !r0; stall1=!r && p1 && !r1;
        held0=m0_data; held1=m1_data;
        last_accept=!r && v && expected_ready;
        $fdisplay(trace_fd,"%0d,%0d,%0d,%0d,%0h,%0d,%0d,%0h,%0d,%0d,%0h,%0d,%0d,%0d",checks,r,v,s_ready,d,m0_valid,r0,m0_data,m1_valid,r1,m1_data,written,read0,read1);
        // Scoreboard consumes old outputs BEFORE enqueueing a new input.
        if(r) begin written=0;read0=0;read1=0;end
        else begin
            if(p0 && r0) read0=read0+1;
            if(p1 && r1) read1=read1+1;
            if(last_accept) begin
                if(written>20000) $fatal(1,"ASSERT_FORK_SCOREBOARD_SIZE");
                expected[written]=d;written=written+1; accepted_total=accepted_total+1;
            end
            if(written-read0>1 || written-read1>1 || read0>written || read1>written)
                $fatal(1,"ASSERT_FORK_CONSERVATION");
        end
        @(posedge clk); #1; checks=checks+1;
    endtask
    bit rv,r0,r1;
    logic [DATA_W-1:0] rd;
    initial begin
        trace_fd=$fopen("trace.csv","w");
        if(!trace_fd) $fatal(1,"TRACE_OPEN_ERROR");
        $fdisplay(trace_fd,"cycle,rst,s_valid,s_ready,s_data_hex,m0_valid,m0_ready,m0_data_hex,m1_valid,m1_ready,m1_data_hex,inputs_before,out0_before,out1_before");
        step(1,0,'0,0,0);
        // Output0 accepts early and must not see the same item again.
        step(0,1,DATA_W'(16'h1111),0,0);
        step(0,1,DATA_W'(16'h2222),1,0);
        repeat(8) step(0,1,DATA_W'(16'h2222),1,0);
        // Last consumer and next input accept on this same edge.
        step(0,1,DATA_W'(16'h2222),1,1);
        step(0,0,'0,1,1);
        step(0,0,'0,1,1);
        // Symmetric branch order.
        step(0,1,DATA_W'(16'h3333),0,0);
        repeat(8) step(0,0,'0,0,1);
        step(0,0,'0,1,1);
        // Exercise every payload bit, including the DATA_W=64 high half.
        step(0,1,{DATA_W{1'b1}},1,1);
        step(0,1,(DATA_W'(1) << (DATA_W-1)),1,1);
        step(0,1,DATA_W'(64'haaaaaaaaaaaaaaaa),1,1);
        step(0,1,DATA_W'(64'h5555555555555555),1,1);
        step(0,0,'0,1,1);
        // Back-to-back inputs when both outputs are ready.
        for(integer n=0;n<64;n=n+1) step(0,1,DATA_W'(n),1,1);
        step(0,0,'0,1,1);
        // Reset with only one copy outstanding starts a new epoch.
        step(0,1,DATA_W'(16'h4444),0,0);
        step(0,0,'0,1,0);
        step(1,0,'0,1,1);
        step(0,0,'0,1,1);
        // Deterministic randomized independent backpressure.
        rv=0;rd='0;last_accept=1;
        for(integer n=0;n<12000;n=n+1) begin
            if(!rv || last_accept) begin rv=((next_random()%5)!=0);rd=next_payload();end
            r0=((next_random()%4)!=0);r1=((next_random()%3)!=0);
            step(0,rv,rd,r0,r1);
        end
        if(rv && !last_accept) step(0,rv,rd,1,1);
        repeat(3) step(0,0,'0,1,1);
        if(read0!=written || read1!=written) $fatal(1,"ASSERT_FORK_DRAIN");
        $display("PASS stream_fork2 checks=%0d accepted_total=%0d epoch_inputs=%0d out0=%0d out1=%0d DATA_W=%0d",checks,accepted_total,written,read0,read1,DATA_W);
        $fclose(trace_fd);
        $finish;
    end
    initial begin #10000000;$fatal(1,"ASSERT_FORK_TIMEOUT");end
endmodule

8. 运行入口与证据边界

将两个文件放到同一目录,在配置了 Vivado 环境的命令行中执行:

xvlog --sv stream_fork2.sv tb_stream_fork2.sv
xelab tb_stream_fork2 --timescale 1ns/1ps --debug typical -s stream_fork2_sim
xsim stream_fork2_sim -runall

本次默认16位配置的原始结果为:

PASS stream_fork2 checks=12102 accepted_total=6408 epoch_inputs=6336 out0=6336 out1=6336 DATA_W=16

1、8、16、64位四组配置每组都运行12,102个自检周期,总计48,408周期。checks按每个测试台step计一次,不是全部条件断言的执行次数;accepted_total包含复位前的输入,epoch_inputs/out0/out1只统计最后一次复位后的完整运行区间,不能跨字段直接比较为“丢失”。每组均保存真实trace.csv。仿真快照的生成与运行方式见 AMD UG900 xelab,命令选项另见 UG900 xsim Executable Options。文档版本不能代替实际工具版本日志。

源码包统一的 run_rtl.py 默认只打印计划;加 --execute --engine xsim 后才执行。复制器部分的4组载荷宽度正例、4个RTL变异和2个非法参数测试,本次全部完成。它拒绝把进程超时、异常退出、矛盾PASS或未知断言判作有效负例。本次XSim在$fatal后仍可返回进程码0,所以必须同时核对诊断文本与终态,不能只看退出码。

独立测试台另测1、7、64位配置,分别完成105,099、105,171、105,855次断言,共316,125次;“任一分支接收就清两份”和“丢掉64位载荷高半部”两个行为变异均被捕获,DATA_W=65也被明确拒绝。独立测试有一次只取消输出0待收副本的复位事件,因此每组记录discarded0=1、discarded1=0,核算时按复位契约记账,不把取消副本误报为传输丢失。

实际CSV还由独立双队列审计器重放,检查每个分支次数、顺序、背压载荷、同拍替换及复位边界。两篇共10组正例的137,523行记录逐行通过;这属于原生输出后处理,不另算RTL仿真次数。

本次最初的原生运行因时间单位设置在展开阶段失败;修复运行器并显式指定--timescale 1ns/1ps后完成全部回归,RTL行为与测试激励保持不变。最终运行保留命令日志、实际DUT信号WDB与CSV,并完成独立原生复核。行为仿真之后仍需要结合目标器件进行综合、时序和实现检查;本文没有下板证据,也没有资源或频率指标。

这个模块最值得保留的思路是:复制一份载荷时,也复制一份“尚未完成”的状态。把每个接收端的收据独立保存,才能在不同背压下保持次数、顺序和载荷一致。

附录:完整批跑器

下面是本次实际使用的 run_rtl.py,只依赖Python标准库,要求Python 3.10或更新版本;本次运行使用Python 3.14.5。将它放到工作根目录,把本文两份SV文件放入 stream_fork2/ 子目录。运行下面命令复现本文所属的全部正向、变异和非法参数配置:

python run_rtl.py --execute --engine xsim --only fork

如果两个题目的SV目录都已放齐,去掉--only即可运行两题共30项。筛选运行的结果会明确标记full_suite=false,不应把单题回归写成两题全部完成。--engine iverilog仅是备选入口,本次结果来自XSim,不宣称Icarus交叉验证已运行。

各项独立目录保存真实命令、UTC起止时间、stdout/stderr、退出状态、编译源哈希与WDB;正向用例另保存完整trace.csv。异常退出、超时、没有真实DUT波形对象、指定变异被其他断言击中,都会影响验收。版本检查必须识别实际工具版本。

#!/usr/bin/env python3
"""Native simulation runner. Default is plan-only; use --execute deliberately.
No third-party Python dependencies. Run in a Vivado environment with xvlog,
xelab and xsim on PATH. Icarus is an optional second simulator, not XSim evidence.
Compilation failure NEVER counts as a killed mutant or invalid-parameter pass.
"""
from pathlib import Path
import argparse, hashlib, json, os, re, shutil, subprocess, sys, datetime

ROOT=Path(__file__).resolve().parent
TOKEN_PARAMS=[
    {'CAPACITY':1,'REFILL':1,'INITIAL_TOKENS':0,'DATA_W':1},
    {'CAPACITY':2,'REFILL':1,'INITIAL_TOKENS':2,'DATA_W':8},
    {'CAPACITY':7,'REFILL':3,'INITIAL_TOKENS':0,'DATA_W':16},
    {'CAPACITY':8,'REFILL':8,'INITIAL_TOKENS':8,'DATA_W':32},
    {'CAPACITY':17,'REFILL':5,'INITIAL_TOKENS':4,'DATA_W':64},
    {'CAPACITY':65535,'REFILL':65535,'INITIAL_TOKENS':0,'DATA_W':16},
]
MUTATIONS={
 'token_bucket':{
  'wrap_credit':('assign available=(raw_credit > (CW+1)\'(CAPACITY)) ? CW\'(CAPACITY) : raw_credit[CW-1:0];','assign available=raw_credit[CW-1:0];'),
  'old_credit':('(s_cost <= available)','(s_cost <= tokens)'),
  'spend_on_valid':('else if(m_valid && m_ready)','else if(m_valid)'),
  'allow_zero':("(s_cost != '0) && ",''),
  'spend_before_saturate':('tokens <= available-s_cost;',"tokens <= ((raw_credit-{1'b0,s_cost}) > (CW+1)'(CAPACITY)) ? CW'(CAPACITY) : CW'(raw_credit-{1'b0,s_cost});"),
  'valid_depends_ready':('assign m_valid=!rst && s_valid && can_send;','assign m_valid=!rst && s_valid && can_send && m_ready;'),
 },
 'stream_fork2':{
  'clear_both_on_any':('assign remaining=pending & ~{m1_ready,m0_ready};',"assign remaining=(|(pending & {m1_ready,m0_ready})) ? 2'b00 : pending;"),
  'repeat_early_receiver':('assign remaining=pending & ~{m1_ready,m0_ready};',"assign remaining=(|(pending & ~{m1_ready,m0_ready})) ? pending : 2'b00;"),
  'accept_when_one_ready':("assign s_ready=!rst && (remaining == 2'b00);","assign s_ready=!rst && ((pending == 2'b00) || (|(pending & {m1_ready,m0_ready})));"),
  'clear_overwrites_replace':("                pending <= 2'b11;\n            end","                pending <= 2'b11;\n            end\n            pending <= remaining;"),
 }
}

EXPECTED_MUTATION_DIAGNOSTICS={
 'wrap_credit':'ASSERT_TOKEN_STATE',
 'old_credit':'ASSERT_TOKEN_CONTROL',
 'spend_on_valid':'ASSERT_TOKEN_STATE',
 'allow_zero':'ASSERT_TOKEN_CONTROL',
 'spend_before_saturate':'ASSERT_TOKEN_STATE',
 'valid_depends_ready':'ASSERT_TOKEN_CONTROL',
 'clear_both_on_any':'ASSERT_FORK_CONTROL',
 'repeat_early_receiver':'ASSERT_FORK_CONTROL',
 'accept_when_one_ready':'ASSERT_FORK_CONTROL',
 'clear_overwrites_replace':'ASSERT_FORK_CONTROL',
}


def cases():
    result=[]
    for i,p in enumerate(TOKEN_PARAMS):
        result.append({'id':f'token_positive_{i}','module':'token_bucket','kind':'positive','parameters':p})
    for w in (1,8,16,64):
        result.append({'id':f'fork_positive_w{w}','module':'stream_fork2','kind':'positive','parameters':{'DATA_W':w}})
    for module,mutants in MUTATIONS.items():
        for mutation in mutants:
            result.append({'id':f'{module}_{mutation}','module':module,'kind':'mutation','mutation':mutation,'parameters':{}})
    for key,value in [('CAPACITY',0),('CAPACITY',65536),('REFILL',0),('REFILL',8),('INITIAL_TOKENS',-1),('INITIAL_TOKENS',8),('DATA_W',0),('DATA_W',65)]:
        result.append({'id':f'token_invalid_{key}_{value}','module':'token_bucket','kind':'invalid','parameters':{key:value}})
    for w in (0,65):
        result.append({'id':f'fork_invalid_w{w}','module':'stream_fork2','kind':'invalid','parameters':{'DATA_W':w}})
    return result


def tool_path(name):
    # Vivado Windows commonly exposes .bat wrappers.
    value=shutil.which(name) or shutil.which(name+'.bat')
    if not value:
        raise RuntimeError(f'{name} not found on PATH; native stage remains NOT RUN')
    return value


def run_command(command, cwd, label, timeout=120):
    started=datetime.datetime.now(datetime.timezone.utc).isoformat()
    timed_out=False
    try:
        result=subprocess.run(command,cwd=cwd,stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,timeout=timeout,shell=False)
        stdout=result.stdout; stderr=result.stderr
        code=result.returncode
    except subprocess.TimeoutExpired as error:
        timed_out=True
        stdout=error.stdout or b''; stderr=error.stderr or b''
        code=124
    (cwd/(label+'.stdout.log')).write_bytes(stdout)
    (cwd/(label+'.stderr.log')).write_bytes(stderr)
    text=stdout.decode('utf-8',errors='replace')+stderr.decode('utf-8',errors='replace')
    if timed_out:text+='\nRUNNER_TIMEOUT\n'
    (cwd/(label+'.log')).write_text(text,encoding='utf-8',newline='\n')
    record={'command':command,'cwd':str(cwd),'start_utc':started,
            'end_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(),
            'exit_code':code,'timed_out':timed_out,'log':str(cwd/(label+'.log')),
            'stdout':str(cwd/(label+'.stdout.log')),'stderr':str(cwd/(label+'.stderr.log'))}
    (cwd/(label+'.command.json')).write_text(json.dumps(record,indent=2)+'\n',encoding='utf-8',newline='\n')
    return record,text


def prepare(case, folder):
    module=case['module']
    source=(ROOT/module/(module+'.sv')).read_text(encoding='utf-8')
    if case['kind']=='mutation':
        old,new=MUTATIONS[module][case['mutation']]
        if source.count(old)!=1:raise RuntimeError(f"mutation anchor not unique: {case['id']}")
        source=source.replace(old,new)
    (folder/(module+'.sv')).write_text(source,encoding='utf-8',newline='\n')
    params=', '.join(f'.{key}({value})' for key,value in case['parameters'].items())
    parameter_clause=f'#({params})' if params else ''
    if case['kind']=='invalid':
        wrapper=f'''`timescale 1ns/1ps
module tb_case;
  {module} {parameter_clause} dut();
  initial begin #10; $fatal(1,"INVALID_PARAMETER_ACCEPTED"); end
endmodule
'''
        sources=[module+'.sv','tb_case.sv']
    else:
        tb='tb_'+module
        shutil.copy2(ROOT/module/(tb+'.sv'),folder/(tb+'.sv'))
        wrapper=f'module tb_case; {tb} {parameter_clause} test(); endmodule\n'
        sources=[module+'.sv',tb+'.sv','tb_case.sv']
    (folder/'tb_case.sv').write_text(wrapper,encoding='utf-8',newline='\n')
    hashes={name:hashlib.sha256((folder/name).read_bytes()).hexdigest() for name in sources}
    (folder/'compiled-source-hashes.json').write_text(json.dumps(hashes,indent=2)+'\n',encoding='utf-8',newline='\n')
    return sources


def execute(case, run_root, engine, executables):
    folder=run_root/case['id'];folder.mkdir()
    sources=prepare(case,folder)
    logs=[]
    if engine=='xsim':
        compile_commands=[('xvlog',[executables['xvlog'],'--sv',*sources]),
                          ('xelab',[executables['xelab'],'tb_case','--timescale','1ns/1ps','--debug','typical','-s','sim'])]
        scope='/tb_case/dut/*' if case['kind']=='invalid' else '/tb_case/test/dut/*'
        tcl=f'set objects [get_objects {scope}]\nif {{[llength $objects] == 0}} {{error "WAVE_SCOPE_EMPTY"}}\nputs "WAVE_OBJECTS=[llength $objects]"\nlog_wave $objects\nrun all\nquit\n'
        (folder/'run.tcl').write_text(tcl,encoding='utf-8',newline='\n')
        sim_command=[executables['xsim'],'sim','--tclbatch','run.tcl','--wdb','simulation.wdb']
    else:
        compile_commands=[('iverilog',[executables['iverilog'],'-g2012','-s','tb_case','-o','sim.vvp',*sources])]
        sim_command=[executables['vvp'],'sim.vvp']
    for label,command in compile_commands:
        info,text=run_command(command,folder,label)
        logs.append(info)
        if info['exit_code']!=0:
            return {**case,'result':'FAIL_COMPILE','commands':logs}
    info,text=run_command(sim_command,folder,'simulation')
    logs.append(info)
    # A diagnostic printed before a timeout/crash is not a completed test.
    process_ok=(not info.get('timed_out',False) and info['exit_code'] in (0,1)
                and 'RUNNER_TIMEOUT' not in text and 'TRACE_OPEN_ERROR' not in text)
    if engine=='xsim':
        # Tcl echoes source lines: inspect the emitted count, not an error string
        # that also appears in an unexecuted guard's echoed source.
        process_ok=process_ok and bool(re.search(r'^WAVE_OBJECTS=[1-9]\d*\s*$',text,re.M))
    if not process_ok:
        status='FAIL_EXECUTION'
    elif case['kind']=='positive':
        ok=(info['exit_code']==0 and f"PASS {case['module']} " in text
            and 'ASSERT_' not in text and 'PARAM_ERROR' not in text)
        status='PASS_NATIVE' if ok else 'FAIL_NATIVE'
    elif case['kind']=='mutation':
        expected=EXPECTED_MUTATION_DIAGNOSTICS[case['mutation']]
        # A different assertion is a test-design issue, not the expected kill.
        diagnostics=set(re.findall(r'ASSERT_[A-Z0-9_]+',text))
        ok=(diagnostics=={expected} and f"PASS {case['module']} " not in text
            and 'PARAM_ERROR' not in text)
        status='KILLED_NATIVE' if ok else 'SURVIVED_OR_INVALID_FAILURE'
    else:
        ok=(f"PARAM_ERROR {case['module']}" in text
            and 'INVALID_PARAMETER_ACCEPTED' not in text and 'PASS ' not in text
            and 'ASSERT_' not in text)
        status='REJECTED_NATIVE' if ok else 'FAIL_PARAMETER_CHECK'
    return {**case,'result':status,'commands':logs}


def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--engine',choices=['xsim','iverilog'],default='xsim')
    parser.add_argument('--execute',action='store_true')
    parser.add_argument('--only',default='',help='optional case-id substring')
    parser.add_argument('--output',type=Path,default=ROOT/'native_runs')
    args=parser.parse_args()
    selected=[c for c in cases() if args.only in c['id']]
    if not selected:raise SystemExit('No matching cases')
    if not args.execute:
        print(json.dumps({'status':'NOT RUN','engine':args.engine,'cases':selected},ensure_ascii=False,indent=2))
        return
    names=['xvlog','xelab','xsim'] if args.engine=='xsim' else ['iverilog','vvp']
    executables={name:tool_path(name) for name in names}
    stamp=datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%S.%fZ')
    run_root=args.output.resolve()/f'{args.engine}_{stamp}';run_root.mkdir(parents=True)
    versions=[]
    for name,exe in executables.items():
        option='-V' if name in ('iverilog','vvp') else '-version'
        record,version_text=run_command([exe,option],run_root,name+'_version',30)
        pattern=r'(?<!\d)(?:19|20)\d{2}\.\d+(?:\.\d+)?\b' if args.engine=='xsim' else r'(?i)\bversion\s+\d+'
        record['version_identified']=bool(record['exit_code']==0 and not record.get('timed_out',False) and re.search(pattern,version_text))
        versions.append(record)
    hashes={str(p.relative_to(ROOT)):hashlib.sha256(p.read_bytes()).hexdigest()
            for p in sorted(ROOT.rglob('*.sv')) if 'native_runs' not in p.parts}
    results=[execute(case,run_root,args.engine,executables) for case in selected]
    okay={'PASS_NATIVE','KILLED_NATIVE','REJECTED_NATIVE'}
    report={'engine':args.engine,'utc_start':stamp,'versions':versions,'source_sha256':hashes,
            'result':('FAIL_VERSION_EVIDENCE' if not all(v['version_identified'] for v in versions)
                      else 'PASS_SELECTED_NATIVE' if all(r['result'] in okay for r in results) else 'FAIL_NATIVE'),
            'full_suite':len(selected)==len(cases()),'cases':results,
            'limits':'RTL behavioral simulation only. No synthesis, implementation, board or timing evidence.'}
    report['utc_end']=datetime.datetime.now(datetime.timezone.utc).isoformat()
    report['runner_sha256']=hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
    (run_root/'results.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8',newline='\n')
    print(json.dumps(report,ensure_ascii=False,indent=2))
    if report['result']!='PASS_SELECTED_NATIVE':raise SystemExit(1)


if __name__=='__main__':main()

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/hxy_programmer/article/details/170230808

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--