一条数据通路平时流量不大,偶尔会连续送来一批请求。如果下游只能长期承担固定工作量,又允许短时间突发,单纯“每隔 N 拍放行一次”就不够灵活。令牌桶把空闲时间积累成有限信用:有信用就能发,没信用就对上游背压。
本文实现一个单时钟、可变成本的 ready/valid 限速器。每项请求携带 cost,一次 tick 补充 REFILL 个信用,信用上限为 CAPACITY。重点是同拍补充与消费、算术扩位、非法成本和背压期间的稳定性。
实际验证环境:Windows 上的 Vivado/XSim 2022.1,SW Build 3526262,2026-11-06。 本文的 6 组正向配置共完成 89,115 个自检周期,6 个指定 RTL 变异和 8 个非法参数均被捕获;另有独立测试台完成 175,621 次断言。验证限于同一时钟域的 RTL 二态行为,没有进行综合、布局布线、时序闭合或上板测试。下文将契约推导表与真实 DUT 记录分开标注。
1. 先给“成本”确定单位
成本可以是一个请求占用的工作量、待处理字节数,也可以统一为 1。模块只接收一个无符号整数,不会解析数据内容或替调用方计算包长。
本文约定:
CAPACITY为 1~65535,REFILL为 1~CAPACITY。INITIAL_TOKENS为 0~CAPACITY,表示复位后是否允许立即突发,默认 0。- 合法成本为 1~CAPACITY。0 成本被拒绝,避免流量绕过预算;超过容量的项永远不可能被服务。
DATA_W为 1~64,成本位宽CW自动由容量计算。tick是与clk同域的补充使能,不是另一个时钟。连续为 1 就是每拍补充。rst同步高有效,在上升沿把信用恢复为INITIAL_TOKENS;但rst=1时会立即组合屏蔽输入、输出握手。使用前至少经过一次有效复位沿,所有输入来自同一时钟域并满足建立、保持要求。
例如信用单位为字节,REFILL=3,补充脉冲平均每 10 个时钟出现一次,时钟频率为 f_clk,长期补充预算为 3*f_clk/10 字节每秒。这个算式没有包含真实链路开销,也没有证明电路能达到某个时钟频率。
RFC 2697 第3节给出了有上限的令牌计数及按包大小扣减的例子。该文档实际定义的是双桶三色标记器;本文是单桶、离散 tick、通过背压整形的教学模块,不宣称实现 RFC 2697 的完整行为。
2. 用一条递推式固定同拍顺序
设时钟边沿前的信用为 T,本拍是否补充为 tick,请求成本为 c。先计算:
raw = T + (tick ? REFILL : 0)
available = min(CAPACITY, raw)
fire = m_valid && m_ready
T_next = available - (fire ? c : 0)
这份契约明确采用“先补充并饱和,再消费”。已经溢出桶的信用不能因为本拍恰好消费而重新找回来。
以 CAPACITY=7、REFILL=3 为例,下表每行都表示一个即将发生的上升沿:
| T | tick | valid | cost | ready | available | 本拍消费 | T_next |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 1 | 2 | 1 | 0 | 0 | 0 |
| 0 | 1 | 1 | 2 | 1 | 3 | 2 | 1 |
| 1 | 1 | 1 | 4 | 0 | 4 | 0 | 4 |
| 4 | 0 | 1 | 4 | 0 | 4 | 0 | 4 |
| 4 | 1 | 1 | 4 | 1 | 7 | 4 | 3 |
| 3 | 1 | 0 | 1 | 1 | 6 | 0 | 6 |
| 6 | 1 | 1 | 1 | 1 | 7 | 1 | 6 |
最后一行最容易暴露顺序错误。正确结果是 min(7,6+3)-1=6;若写成 min(7,6+3-1),结果会是 7。这两个实现对应不同的边界规则,测试必须先选定其中一种,而不是看到结果不同才调整期望。
每拍溢出的信用为 lost=max(0,raw-CAPACITY)。在一个没有复位的区间中,应满足:
末态信用 = 初态信用 + 补充总量 - 饱和丢弃总量 - 已消费成本总量
因此,整个区间实际消费不会超过初始信用与区间补充量之和。这里约束的是成本总量,不是请求数量;成本不同,同样的令牌预算能服务的请求数量也不同。
真实 DUT 记录:背压期间不扣费,解除背压后再消费
下图来自本次 XSim 原生仿真的 token_positive_2/trace.csv,参数为 CAPACITY=7、REFILL=3、INITIAL_TOKENS=0、DATA_W=16。使用连续 cycle 2~13 的全部12行,只绘制实际 dut_* 观测和施加的输入,没有把 exp_* 参考值当作实测波形。

cycle 2 的信用从3补到6,cycle 3补充后饱和到7;cycle 6~12中,载荷 0x55aa 和valid保持,ready为0,信用保持7。cycle 13下游ready拉高,先把补充后的信用限制为7,再扣除成本1,实际边沿后信用为6。dut_tokens_before 采于边沿前,dut_tokens_after 采于上升沿后1 ns的测试台检查点;这个1 ns不是被测模块的物理延迟。
3. 为什么加法必须比信用寄存器宽一位
容量为 7 时,信用需要 3 位,但 7+3=10 需要 4 位。如果先在 3 位里相加,再与容量比较,结果已经回绕成 2,后面的饱和判断无法恢复原值。
代码把信用与补充量都扩到 CW+1 位后相加。由于 T<=CAPACITY 且 REFILL<=CAPACITY,宽一位足以表示本例的最大加数和。只有比较并截取合法结果之后,才写回 CW 位寄存器。
非法参数的仿真检查放在 initial 中,用 PARAM_ERROR 明确终止。CW 和数据端口宽度对非法值先采用安全的占位宽度,目的是让测试尽可能走到明确诊断,而不是先因零宽度或异常范围无法展开。占位宽度不表示非法参数受支持;综合前仍必须由工程配置检查参数范围。
4. 一次握手只扣一次信用
这里没有数据缓存,载荷和成本组合直通。上游必须遵守 ready/valid 约定:一旦提出有效请求,如果 s_ready=0,就保持 valid、data、cost,直到握手或复位。
本模块的控制关系为:
can_send = 成本合法 && 成本不大于本拍 available
m_valid = !rst && s_valid && can_send
s_ready = !rst && can_send && m_ready
m_valid 不等待 m_ready。只有两者同为 1,才真正扣减信用。输入与输出握手严格同时发生,因此不会出现已经接受输入、却没有地方保存载荷的问题。基本握手定义可参考 AMD PG256 的 AXI4-Stream Interface。本文使用相同的握手思想,但不是完整 AXI4-Stream IP。
当 m_valid=1、m_ready=0 时,信用不会被消费。上一拍可用信用会写回寄存器,下一拍只可能保持或增加,不能因补充脉冲消失而不足。因此,只要上游保持请求,已经拉高的 m_valid 就保持有效,数据与成本也保持不变。复位是这条持续性约定的明确例外。
5. 非法成本不能悄悄免费通行
当 s_valid=1 且成本为 0 或超过容量时,cost_error=1,同时关闭输入 ready 和输出 valid。信用计数仍可补充。
这是一种阻塞式错误接口:它既不会自动丢弃输入,也不会把它当成零成本请求接受。如果系统允许不可信数据到达这里,需要在上游增加显式的错误处理或丢弃状态机。不要一边要求上游无条件保持 valid,一边又期望非法请求自动消失。
另一个限制是队头阻塞。如果当前项需要 7 个信用,后面的一项只需 1 个信用,本模块不能绕过前者。需要重排序、优先级或分流时,应在更高层设计调度器。
6. 完整 RTL
保存为 token_bucket.sv。下面代码由同名源文件直接嵌入,避免文章与交付源码两处手工维护。
本次原生结果绑定实际受测源码哈希;下面发布副本仅更新第2行过时注释,其他字节经独立核对一致。没有把新注释对应的文件哈希宣称为重新跑过XSim。
// Single-clock ready/valid limiter. Refill is saturated BEFORE spending.
// Behavioral example; synthesis and hardware validation not performed.
module token_bucket #(
parameter integer CAPACITY=7,
parameter integer REFILL=3,
parameter integer INITIAL_TOKENS=0,
parameter integer DATA_W=16,
localparam integer CW=(CAPACITY<1 || CAPACITY>65535) ? 1 : $clog2(CAPACITY+1),
localparam integer DW=(DATA_W<1 || DATA_W>64) ? 1 : DATA_W
) (
input logic clk, rst, tick,
input logic s_valid,
output logic s_ready,
input logic [DW-1:0] s_data,
input logic [CW-1:0] s_cost,
output logic m_valid,
input logic m_ready,
output logic [DW-1:0] m_data,
output logic [CW-1:0] m_cost,
output logic cost_error,
output logic [CW-1:0] tokens
);
// The wider sum represents up to 2*CAPACITY without wraparound.
logic [CW:0] raw_credit;
logic [CW-1:0] available;
logic legal_cost, can_send;
assign raw_credit={1'b0,tokens} + (tick ? (CW+1)'(REFILL) : (CW+1)'(0));
assign available=(raw_credit > (CW+1)'(CAPACITY)) ? CW'(CAPACITY) : raw_credit[CW-1:0];
assign legal_cost=(s_cost != '0) && ({1'b0,s_cost} <= (CW+1)'(CAPACITY));
assign can_send=legal_cost && (s_cost <= available);
assign s_ready=!rst && can_send && m_ready;
assign m_valid=!rst && s_valid && can_send;
assign m_data=s_data;
assign m_cost=s_cost;
assign cost_error=!rst && s_valid && !legal_cost;
always_ff @(posedge clk) begin
if(rst) tokens <= CW'(INITIAL_TOKENS);
else if(m_valid && m_ready) tokens <= available-s_cost;
else tokens <= available;
end
// synthesis translate_off
initial begin
if(CAPACITY<1 || CAPACITY>65535 || REFILL<1 || REFILL>CAPACITY ||
INITIAL_TOKENS<0 || INITIAL_TOKENS>CAPACITY || DATA_W<1 || DATA_W>64)
$fatal(1,"PARAM_ERROR token_bucket");
end
// synthesis translate_on
endmodule
7. 自检为什么既看数值,也看协议
完整测试台在下一节,检查包括:
- 复位初值、容量为 1、非二次幂容量、最大合法容量以及 1/8/16/32/64 位载荷。
- 补充与消费同拍、桶满后继续补充、从空桶使用本拍补充、成本恰好等于容量。
- 下游连续不就绪时不扣信用,输出 valid、data、cost 稳定。
- 零成本和可表示的超容量成本错误注入。
- 复位取消被背压的项,随后从新的信用初值开始。
- 小容量配置下,重建每一种信用状态,遍历 tick、valid、ready 和全部可表示成本。
- 使用固定种子的随机流量,未接受的上游项保持不变;结束时恢复补充和 ready,检查能排空。
测试台同时累计“补充、饱和丢弃、实际消费”,核对信用守恒。输出 CSV 的 exp_* 与 dut_* 分别保存预期关系和实际观测,包含边沿前信用/控制与边沿后信用,可用于定位第一处错误,而不只是阅读最后一行 PASS。
Python 参考检查另用“令牌列表增加、裁剪、删除”的模型对照整数递推关系,已经检查 310,624 个状态/输入组合,并发现 5 类参考模型错误的反例。这只是参考关系检查,不编译、不模拟本篇 RTL。
本次原生批跑执行了 6 个 RTL 变异:去掉饱和而直接截断回绕、用旧信用判断、valid 即扣款、允许零成本、扣减后再饱和、valid 依赖 ready。六个变异均先成功编译展开,再由事先指定的断言判失败。编译错误、进程超时或不相关断言没有计作“成功杀死”。本次 XSim 在 $fatal 后仍可能返回进程码0,因此运行器同时检查指定诊断、进程完成状态和相反的PASS标记,不能只按退出码判断。
8. 完整自检测试台
保存为 tb_token_bucket.sv。仿真激励在下降沿驱动,在下一上升沿之后检查寄存器结果;这是避免测试台与被测时序逻辑在同一调度区域争用的安排,不是物理建立保持验证。
`timescale 1ns/1ps
module tb_token_bucket #(
parameter integer CAPACITY=7, REFILL=3, INITIAL_TOKENS=0, DATA_W=16,
localparam integer CW=(CAPACITY<1 || CAPACITY>65535) ? 1 : $clog2(CAPACITY+1)
);
logic clk=0;
always #5 clk=~clk;
logic rst=1, tick=0, s_valid=0, s_ready, m_valid, m_ready=0, cost_error;
logic [DATA_W-1:0] s_data='0, m_data;
logic [CW-1:0] s_cost='0, m_cost, tokens;
token_bucket #(.CAPACITY(CAPACITY), .REFILL(REFILL),
.INITIAL_TOKENS(INITIAL_TOKENS), .DATA_W(DATA_W)) dut(.*);
integer model=INITIAL_TOKENS, checks=0, transfers=0;
integer trace_fd;
integer total_add=0, total_spend=0, total_lost=0, epoch_initial=INITIAL_TOKENS;
bit was_stalled=0;
logic [DATA_W-1:0] stalled_data;
logic [CW-1:0] stalled_cost;
bit last_accept=0;
integer unsigned rng=32'h537a9c01;
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, t, v, input integer c,
input bit ready, input logic [DATA_W-1:0] d);
integer available, raw, expected_next;
bit legal, ev, er, ee;
logic [CW-1:0] dut_tokens_before,dut_cost_before;
logic [DATA_W-1:0] dut_data_before;
logic dut_valid_before,dut_ready_before,dut_error_before;
@(negedge clk);
rst=r; tick=t; s_valid=v; s_cost=CW'(c); m_ready=ready; s_data=d;
#1;
// Snapshot the DUT before the clock edge, separate from expectations.
dut_tokens_before=tokens; dut_cost_before=m_cost; dut_data_before=m_data;
dut_valid_before=m_valid; dut_ready_before=s_ready; dut_error_before=cost_error;
raw=model+(t ? REFILL : 0);
available=(raw>CAPACITY) ? CAPACITY : raw;
legal=(c>=1 && c<=CAPACITY);
ev=!r && v && legal && (c<=available);
er=!r && legal && (c<=available) && ready;
ee=!r && v && !legal;
if (m_valid !== ev || s_ready !== er || cost_error !== ee)
$fatal(1,"ASSERT_TOKEN_CONTROL checks=%0d model=%0d cost=%0d",checks,model,c);
if (m_data !== d || m_cost !== CW'(c)) $fatal(1,"ASSERT_TOKEN_PAYLOAD");
if (was_stalled && !r && (!m_valid || m_data!==stalled_data || m_cost!==stalled_cost))
$fatal(1,"ASSERT_TOKEN_STABILITY");
was_stalled=ev && !ready;
stalled_data=d; stalled_cost=CW'(c);
last_accept=v && er;
if (last_accept != (ev && ready)) $fatal(1,"ASSERT_TOKEN_HANDSHAKE");
if (r) begin
expected_next=INITIAL_TOKENS;
total_add=0; total_spend=0; total_lost=0; epoch_initial=INITIAL_TOKENS;
end else begin
expected_next=available-(last_accept ? c : 0);
total_add=total_add+(t ? REFILL : 0);
total_lost=total_lost+((raw>CAPACITY) ? raw-CAPACITY : 0);
total_spend=total_spend+(last_accept ? c : 0);
end
@(posedge clk); #1;
// Record both actual ports/state and independent model values.
$fdisplay(trace_fd,"%0d,%0d,%0d,%0d,%0d,%0d,%0h,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0h,%0d,%0d,%0d,%0d,%0d",
checks,r,t,v,c,ready,d,model,dut_tokens_before,available,
ev,dut_valid_before,er,dut_ready_before,ee,dut_error_before,
dut_data_before,dut_cost_before,last_accept,(dut_valid_before && ready),expected_next,tokens);
model=expected_next;
if (tokens !== CW'(model)) $fatal(1,"ASSERT_TOKEN_STATE checks=%0d got=%0d expected=%0d",checks,tokens,model);
if (model<0 || model>CAPACITY || model!=epoch_initial+total_add-total_lost-total_spend)
$fatal(1,"ASSERT_TOKEN_CONSERVATION");
if (last_accept) transfers=transfers+1;
checks=checks+1;
endtask
task automatic reset_case();
step(1,0,0,1,0,'0);
endtask
task automatic fill();
for(integer i=0;i<(CAPACITY+REFILL-1)/REFILL+2;i=i+1)
step(0,1,0,1,0,'0);
endtask
bit rv, rt, rr;
integer rc;
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,tick,s_valid,s_cost,m_ready,s_data_hex,exp_tokens_before,dut_tokens_before,exp_available,exp_m_valid,dut_m_valid,exp_s_ready,dut_s_ready,exp_cost_error,dut_cost_error,dut_m_data_hex,dut_m_cost,exp_fire,dut_fire,exp_tokens_after,dut_tokens_after");
reset_case();
fill();
// Backpressure cannot spend tokens or change payload.
repeat(7) step(0,1,1,1,0,DATA_W'(16'h55aa));
step(0,1,1,1,1,DATA_W'(16'h55aa));
// Exercise all payload bits, including DATA_W=64 high half.
reset_case(); fill();
step(0,1,1,1,1,{DATA_W{1'b1}});
step(0,1,1,1,1,(DATA_W'(1) << (DATA_W-1)));
step(0,1,1,1,1,DATA_W'(64'haaaaaaaaaaaaaaaa));
step(0,1,1,1,1,DATA_W'(64'h5555555555555555));
// Capacity-sized request, then same-edge refill used from empty.
reset_case(); fill();
step(0,0,1,CAPACITY,1,DATA_W'(1));
step(0,1,1,REFILL,1,DATA_W'(2));
// Error injection. Zero and representable over-capacity costs block.
reset_case(); fill();
step(0,1,1,0,1,DATA_W'(3));
if (CAPACITY < ((1<<CW)-1)) step(0,0,1,CAPACITY+1,1,DATA_W'(4));
// Reset cancels an otherwise valid stalled item.
reset_case(); fill();
step(0,0,1,1,0,DATA_W'(5));
reset_case();
// Exhaustive one-step state/input combinations for small capacity.
if(CAPACITY<=17) begin
for(integer state=0;state<=CAPACITY;state=state+1)
for(integer t=0;t<2;t=t+1)
for(integer v=0;v<2;v=v+1)
for(integer r=0;r<2;r=r+1)
for(integer c=0;c<(1<<CW);c=c+1) begin
reset_case(); fill();
if(state<CAPACITY) step(0,0,1,CAPACITY-state,1,DATA_W'(6));
step(0,t!=0,v!=0,c,r!=0,DATA_W'(state*31+c));
end
end
reset_case(); rv=0; rc=1; rd='0; last_accept=1;
// Legal producer: hold an unaccepted request unchanged.
for(integer n=0;n<6000;n=n+1) begin
if(!rv || last_accept) begin
rv=((next_random()%4)!=0);
rc=1+(next_random()%CAPACITY); rd=next_payload();
end
rt=((next_random()%3)!=0); rr=((next_random()%4)!=0);
step(0,rt,rv,rc,rr,rd);
end
// Drain any held input with a bounded completion check.
for(integer n=0;n<CAPACITY+3 && rv;n=n+1) begin
step(0,1,rv,rc,1,rd); if(last_accept) rv=0;
end
if(rv) $fatal(1,"ASSERT_TOKEN_DRAIN");
$display("PASS token_bucket checks=%0d transfers=%0d CAPACITY=%0d REFILL=%0d INITIAL=%0d DATA_W=%0d",checks,transfers,CAPACITY,REFILL,INITIAL_TOKENS,DATA_W);
$fclose(trace_fd);
$finish;
end
initial begin #100000000; $fatal(1,"ASSERT_TOKEN_TIMEOUT"); end
endmodule
9. 从空目录复现默认配置
把以上两个文件放在同一目录,进入已配置好的 Vivado 命令行环境,运行:
xvlog --sv token_bucket.sv tb_token_bucket.sv
xelab tb_token_bucket --timescale 1ns/1ps --debug typical -s token_bucket_sim
xsim token_bucket_sim -runall
本次完整批跑的默认配置原始输出为:
PASS token_bucket checks=10081 transfers=2985 CAPACITY=7 REFILL=3 INITIAL=0 DATA_W=16
其中 checks 是自检周期数,每次测试台 step 增加一次,不是该周期内全部条件断言的总执行次数。transfers 是这些测试场景中累计接受的请求数。该配置同时生成实际 trace.csv。 xelab 创建仿真快照、xsim 执行快照的流程可查 AMD UG900。文档版本与将来实际测试版本需分开记录。
源码包的 run_rtl.py 不依赖第三方 Python 库。默认只显示计划;只有加 --execute --engine xsim 才运行。令牌桶部分的6组正例、6个变异和8个非法参数用例本次全部完成,每例独立目录保留日志。只运行上面的默认配置,不等于复现全部矩阵。
| CAPACITY | REFILL | 初始信用 | DATA_W | 实际自检周期 | 实际接受请求 |
|---|---|---|---|---|---|
| 1 | 1 | 0 | 1 | 6215 | 2908 |
| 2 | 1 | 2 | 8 | 6684 | 2428 |
| 7 | 3 | 0 | 16 | 10081 | 2985 |
| 8 | 8 | 8 | 32 | 12824 | 4292 |
| 17 | 5 | 4 | 64 | 47272 | 6727 |
| 65535 | 65535 | 0 | 16 | 6039 | 3192 |
独立测试台另测了 (C,R,I,W)=(1,1,0,1)、(65535,32768,32767,64)、(11,4,3,64),共175,621次断言,并实际捕获“valid依赖ready”“消费后再饱和”两个行为变异和REFILL=0参数负例。独立CSV审计按实际输入与DUT观测重算信用,不以exp_*列作答案;两篇共10个正例的137,523行实际记录全部通过逐行审计。CSV后处理不是额外一次RTL仿真,不叠加到原生断言数。
本次原始运行曾因核心与测试台的时间单位设置差异在展开阶段失败;仅修复运行器的时间单位、版本识别和证据采集后,完整回归通过。没有为使测试通过而更改RTL行为或测试激励。最终运行使用显式--timescale 1ns/1ps,并保存真实DUT信号的WDB和CSV。行为仿真通过不能推断实际Fmax、资源数量或板级吞吐率,也不构成形式化证明。
接入自己的链路前,先回答三个具体问题:一个信用代表什么,复位后允许多少突发,非法成本由谁处理。把这三点和同拍顺序写清楚,令牌桶才有确定的系统含义。
附录:完整批跑器
下面是本次实际使用的 run_rtl.py,只依赖Python标准库,要求Python 3.10或更新版本;本次运行使用Python 3.14.5。将它放到工作根目录,把本文两份SV文件放入 token_bucket/ 子目录。运行下面命令复现本文所属的全部正向、变异和非法参数配置:
python run_rtl.py --execute --engine xsim --only token
如果两个题目的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()
217



被折叠的 条评论
为什么被折叠?



