能量、距离平方或计数统计常会产生一个非负整数 N。如果后级只需要整数尺度,所求通常不是浮点结果,而是 q=floor(sqrt(N)),再用 r=N-q*q 保留没有被平方覆盖的部分。例如 N=32 时 q=5、r=7;N=255 时 q=15、r=30。结果向下取整,不能和采用其他舍入模式的 IP 输出直接混为一谈。
本实验从高位到低位,每拍处理两位输入,只使用移位、比较和减法。接口带请求/结果握手,明确停顿和复位的行为。2026-10-23 在 DESKTOP-396TDVB 的 XSim 2022.1 上执行原始和独立自检;本次没有综合、STA、器件资源或板级结论。
从 N=32 的四次迭代看算法
8 位输入 00100000 按最高位起分为 00 | 10 | 00 | 00。从 q=0、r=0 开始,每轮取两位 a,计算 u=4r+a、t=4q+1。若 u>=t,就让新根 q'=2q+1、新余数 r'=u-t;否则 q'=2q、r'=u。
| 次数 | 新两位 a | 已处理前缀 | u | t | 新 q | 新 r | 本次 VCD 更新时刻 ns |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 1 | 0 | 0 | 45 |
| 2 | 2 | 2 | 2 | 1 | 1 | 1 | 55 |
| 3 | 0 | 8 | 4 | 5 | 2 | 4 | 65 |
| 4 | 0 | 32 | 16 | 9 | 5 | 7 | 75 |
表中的 q/r/时刻取自本次 runs_corrected_original/sqrt_original/trace.vcd,整理到 sqrt32_prefix.csv;u/t 是依据这些实际前态计算的中间量。这个例子既展示算法,也固定了后面所说的周期定义。
为什么试减值是 4q+1?相邻两个候选根的平方差为 (2q+1)^2-(2q)^2=4q+1。若已处理前缀满足 Pprefix=q²+r 且 0<=r<2q+1,带入下一对位后:
text
u = 4r + a, t = 4q + 1, b = (u >= t)
q' = 2q + b
r' = u - b*t
4*Pprefix + a = q'^2 + r'
当 b=0 时,r'=u<t=2q'+1;当 b=1 时,r'>=0,而旧的整数余数满足 r<=2q、a<=3,所以 u<=8q+3,进而 r'<=4q+2<4q+3=2q'+1。不变量从全零初态持续成立,最后得到 q²<=N<(q+1)²。这里是算法推导;本次 RTL 对全部小位宽输入和选定大位宽输入做了仿真,不能把有限测试说成任意参数的形式化证明。
AMD 的 AM004 Square Root 给出 DSP58 相关平方根实现背景。本文没有实例化 DSP58 或厂商 CORDIC IP,而是自定义逐位移位减法结构;文档里的架构和性能不能直接搬来当本实现的实测结论。
接口契约:先说清楚哪一拍算完成
| 信号 / 参数 | 约定 |
|---|---|
| N_WIDTH | 正整数,至少为 1;输入无符号 |
| clk / rst | 上升沿工作;rst 高有效同步复位,优先于其他操作 |
| s_valid / s_ready / s_data | 边沿前 valid&&ready 时接受一个 N;阻塞时源必须保持 valid 和数据 |
| m_valid / m_ready / m_root / m_rem | 边沿前 valid&&ready 时结果被消费;阻塞时 valid、root、rem 稳定 |
| 根宽度 | R=ceil(N_WIDTH/2),即 (N_WIDTH+1)/2 |
| 余数宽度 | N_WIDTH+1;输出 r=N-q² |
| 内部补位 | P=2R;奇数输入宽度只在最高端补 0,不左移输入值 |
接收边沿称为 T0;T1...TR 各执行一次迭代;TR 边沿之后 m_valid 和结果有效,最早在 TR+1 被后级消费。完成计算的 TR 边沿不能再接收请求,因为边沿前 busy 仍为 1。空闲且旧结果将被消费时,可以在同一边沿接收下一请求。连续就绪情况下,相邻接收边沿最短相距 R+1 个时钟周期;这不是每拍一个输入的流水线。
输出被阻塞时,不接受新请求。未阻塞的旧结果被消费并接受新 N 后,输出数据寄存器仍可能保持旧数值,直到新结果产生;只有 m_valid=1 的周期才有有效结果。内部锁存已接受的 N,busy 期间无效输入总线变化不应影响计算。
同步复位边沿丢弃正在计算或尚未消费的结果,并清除输出有效标志及数据;复位不能撤销已被下游消费的结果。s_ready 组合地受 rst 抑制,但其他状态只在上升沿清除。测试将丢弃请求计入 discarded,不能把复位造成的结果缺失当成正常完成。N=0、1 没有特殊快通道,也执行 R 次;最小 N_WIDTH=1 时 R=1、P=2,仍能合法取两位。
本次真实握手时序

图 1 来自本次 VCD 的 W=8 实例。35 ns 接受 N=32,45/55/65/75 ns 完成四轮,75 ns 边沿后输出 q=5、r=7。后级暂不 ready,结果保持;源把下一请求 N=255 保持住。125 ns 边沿同时消费 32 的结果并接受 255;165 ns 边沿后得到 q=15、r=30,175 ns 消费。
原始 CSV 在上升沿非阻塞赋值生效前记录接口,所以第一次记录到 m_valid=1 是 85 ns,而 VCD 显示它在 75 ns 边沿后拉高。不要把这两种观察位置混用而多算一拍。独立 TB 则同时检查边沿前接口和边沿后 1 ns 的状态。
完整 RTL
保存为 src/isqrt_iter.sv。内部比较/减法量用 P+2 位,根先零扩展再左移,避免窄根在构造 4q+1 时丢掉高位。计数器最少保留 1 位,解决 R=1 时 $clog2(1)=0 的边界。
systemverilog
`timescale 1ns/1ps
module isqrt_iter #(parameter integer N_WIDTH=8) (
input wire clk, rst,
input wire s_valid, output wire s_ready,
input wire [N_WIDTH-1:0] s_data,
output reg m_valid, input wire m_ready,
output reg [(N_WIDTH+1)/2-1:0] m_root,
output reg [N_WIDTH:0] m_rem
);
localparam integer R=(N_WIDTH+1)/2, P=2*R;
localparam integer CW=(R<2)?1:$clog2(R);
reg busy;
reg [P-1:0] shifted;
reg [R-1:0] root_work;
reg [P+1:0] rem_work;
reg [CW-1:0] count;
wire [P+1:0] trial=(rem_work << 2) | {{P{1'b0}},shifted[P-1 -: 2]};
wire [P+1:0] divisor_trial=({{(P+2-R){1'b0}},root_work} << 2) | 1'b1;
wire take=(trial >= divisor_trial);
wire [P+1:0] rem_next=take ? trial-divisor_trial : trial;
wire [R-1:0] root_next=(root_work << 1) | take;
assign s_ready=!rst && !busy && (!m_valid || m_ready);
always @(posedge clk) begin
if(rst) begin
busy<=0;shifted<=0;root_work<=0;rem_work<=0;count<=0;
m_valid<=0;m_root<=0;m_rem<=0;
end else begin
if(m_valid && m_ready) m_valid<=0;
if(s_valid && s_ready) begin
shifted<={{(P-N_WIDTH){1'b0}},s_data};
root_work<=0;rem_work<=0;count<=0;busy<=1;
end else if(busy) begin
shifted<=shifted << 2;root_work<=root_next;rem_work<=rem_next;
if(count==R-1) begin
busy<=0;m_valid<=1;m_root<=root_next;m_rem<=rem_next;
end else count<=count+1'b1;
end
end
end
endmodule
代码块与本次实际编译文件逐字一致。它的接口要求 N_WIDTH>=1;N_WIDTH=0 不在定义域内。本文 TB 中的 64 位乘法参考模型覆盖所实例化的最大 32 位输入,不能不经检查就扩展到任意大位宽。
完整原始 testbench
保存为 src/tb_isqrt_iter.sv。参考根通过从高位到低位尝试候选根的平方获得,与 DUT 的恢复余数递推不同。先将 q、r 和输入提升到 longint unsigned,再验证 q*q+r=N 与 r<2*q+1,防止参考乘法自己先溢出。没有调用浮点 $sqrt。
测试实例为 1、3、8、9、16、32 位:前四者逐个输入穷举;后两者覆盖 0、1、最大值、选定平方数前后邻域和确定种子的伪随机输入。随机下游 ready、valid 空隙、停顿源稳定性、结果保持、同拍消费/接受以及每个迭代年龄上的复位都进入断言。每个实例结束后停掉自己的时钟,避免其他实例继续运行时向已关闭 CSV 写入。
systemverilog
`timescale 1ns/1ps
module sqrt_case #(parameter integer W=8)(output reg finished);
localparam integer R=(W+1)/2;
reg clk=0,rst=1,s_valid=0,m_ready=0;
reg [W-1:0] s_data=0;
wire s_ready,m_valid;
wire [R-1:0] m_root;
wire [W:0] m_rem;
isqrt_iter #(.N_WIDTH(W)) dut(.*);
always #5 if(!finished) clk=~clk;
longint unsigned pending_n,expected_q,actual_q,actual_r,max_n,trial_q,value;
reg pending_request=0,blocked=0,source_blocked=0;
reg [R+W:0] saved_result;
reg [W-1:0] saved_source;
integer age=0,cycles=0,accepted=0,delivered=0,discarded=0,stalls=0,duplex=0,fd,k,i,j;
reg [31:0] seed=32'h23841bad;
function automatic [31:0] rng;
begin seed=seed^(seed<<13);seed=seed^(seed>>17);seed=seed^(seed<<5);rng=seed;end
endfunction
function automatic longint unsigned root_ref(input longint unsigned n);
longint unsigned q,candidate;
begin
q=0;
for(integer b=R-1;b>=0;b=b-1) begin
candidate=q|(64'b1<<b);
if(candidate*candidate<=n) q=candidate;
end
root_ref=q;
end
endfunction
initial begin
finished=0;fd=$fopen($sformatf("sqrt_%0d.csv",W),"w");
$fdisplay(fd,"time_ns,width,rst,s_valid,s_ready,s_data,m_valid,m_ready,m_root,m_rem,accepted,delivered,discarded");
end
always @(posedge clk) begin
cycles=cycles+1;
if(rst) begin
if(pending_request) discarded=discarded+1;
pending_request=0;blocked=0;source_blocked=0;age=0;
end else begin
if(blocked && (!m_valid || {m_root,m_rem}!==saved_result))
$fatal(1,"SQRT_ASSERT W=%0d stalled result changed",W);
if(source_blocked && (!s_valid || s_data!==saved_source))
$fatal(1,"STIMULUS_ASSERT W=%0d source changed under stall",W);
if(m_valid) begin
if(!pending_request || age!=R) $fatal(1,"SQRT_ASSERT W=%0d response age=%0d",W,age);
actual_q=m_root;actual_r=m_rem;expected_q=root_ref(pending_n);
if(actual_q!=expected_q || actual_q*actual_q+actual_r!=pending_n || actual_r>=2*actual_q+1)
$fatal(1,"SQRT_ASSERT W=%0d N=%0d q=%0d r=%0d expected_q=%0d",W,pending_n,actual_q,actual_r,expected_q);
if(m_ready) begin pending_request=0;delivered=delivered+1;end
else stalls=stalls+1;
end
if(s_valid && s_ready) begin
if(pending_request) $fatal(1,"SQRT_ASSERT W=%0d overaccepted",W);
pending_request=1;pending_n=s_data;age=0;accepted=accepted+1;
if(m_valid && m_ready) duplex=duplex+1;
end else if(pending_request && !m_valid) age=age+1;
blocked=m_valid && !m_ready;saved_result={m_root,m_rem};
source_blocked=s_valid && !s_ready;saved_source=s_data;
end
$fdisplay(fd,"%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d,%0d",$time,W,rst,s_valid,s_ready,s_data,m_valid,m_ready,m_root,m_rem,accepted,delivered,discarded);
#1;
if(rst) begin
if(m_valid!==0 || m_root!==0 || m_rem!==0 || s_ready!==0) $fatal(1,"SQRT_ASSERT reset W=%0d",W);
end else if(m_valid !== (pending_request && age==R))
$fatal(1,"SQRT_ASSERT latency W=%0d age=%0d valid=%b",W,age,m_valid);
if(accepted!=delivered+discarded+pending_request) $fatal(1,"SQRT_ASSERT conservation W=%0d",W);
end
task automatic tick(input bit v,input [W-1:0] n,input bit ready,input bit reset);
begin
@(negedge clk);s_valid=v;s_data=n;m_ready=ready;rst=reset;
@(posedge clk);#2;
end
endtask
task automatic request(input [W-1:0] n);
reg take;
begin
take=0;
while(!take) begin
@(negedge clk);rst=0;s_valid=1;s_data=n;m_ready=(rng()%4)!=0;
@(posedge clk);take=s_ready;#2;
end
end
endtask
task automatic drain;
begin
while(pending_request) tick(0,0,1,0);
tick(0,0,1,0);
end
endtask
initial begin
max_n=(64'b1<<W)-1;
tick(0,0,0,1);tick(0,0,1,0);
request(32);repeat(R) tick(0,rng(),0,0);
repeat(4) tick(1,max_n,0,0); // pending next request holds legally
tick(1,max_n,1,0);drain();
// Exhaust all small widths, including the minimum and odd widths.
if(W<=9) for(i=0;i<(1<<W);i=i+1) begin
if(i%5==0) begin drain();tick(0,rng(),1,0);end
request(i);
end
else begin
request(0);request(1);request(max_n);
for(i=0;i<64;i=i+1) begin
trial_q=(i==63)?((64'b1<<R)-1):(64'b1<<(i%R));
value=trial_q*trial_q;
if(value>0) request(value-64'd1);
request(value);
if(value<max_n) request(value+64'd1);
end
for(i=0;i<256;i=i+1) request(rng() & max_n);
end
drain();
// Reset after acceptance, at every intermediate age, and on a blocked result.
for(k=0;k<=R;k=k+1) begin
request(max_n);
repeat(k) tick(0,rng(),0,0);
tick(0,0,0,1);tick(0,0,1,0);
repeat(R+2) tick(0,0,1,0);
request(k);drain();
end
if(pending_request || accepted!=delivered+discarded || discarded!=R+1 || stalls<4 || duplex<1)
$fatal(1,"SQRT_ASSERT final coverage W=%0d",W);
$display("CASE_PASS SQRT W=%0d accepted=%0d delivered=%0d discarded=%0d stalls=%0d duplex=%0d cycles=%0d",W,accepted,delivered,discarded,stalls,duplex,cycles);
$fclose(fd);finished=1;
end
endmodule
module tb_isqrt_iter;
wire [5:0] finished;
sqrt_case #(1) c1(finished[0]);sqrt_case #(3) c3(finished[1]);
sqrt_case #(8) c8(finished[2]);sqrt_case #(9) c9(finished[3]);
sqrt_case #(16) c16(finished[4]);sqrt_case #(32) c32(finished[5]);
initial begin wait(&finished);$display("TEST_PASS ISQRT original widths=1,3,8,9,16,32");$finish;end
initial begin #1000000;$fatal(1,"WATCHDOG ISQRT");end
endmodule
复现命令和原始证据
以下 PowerShell 命令假定两个 .sv 文件放在当前目录的 src/ 中。工作目录必须是新目录;本文实际使用的 Vivado 路径如下,其他电脑只需改 $vivadoBin。无需 Python 即可执行 RTL 自检。
powershell
$vivadoBin = 'D:\tools\Xilinx\Vivado\2022.1\bin'
New-Item -ItemType Directory -Path 'manual_isqrt_iter' -ErrorAction Stop
Set-Location 'manual_isqrt_iter'
& "$vivadoBin\xvlog.bat" --sv '../src/isqrt_iter.sv' '../src/tb_isqrt_iter.sv'
if ($LASTEXITCODE -ne 0) { throw 'xvlog failed' }
& "$vivadoBin\xelab.bat" tb_isqrt_iter -s sim -debug typical
if ($LASTEXITCODE -ne 0) { throw 'xelab failed' }
@'
open_vcd trace.vcd
log_vcd /tb_isqrt_iter/*
run 2000 ns
close_vcd
run all
quit
'@ | Set-Content -Encoding ascii 'simulate.tcl'
& "$vivadoBin\xsim.bat" sim -tclbatch simulate.tcl
if ($LASTEXITCODE -ne 0) { throw 'xsim process failed' }
$simText = Get-Content -LiteralPath 'xsim.log' -Raw
if ($simText -notmatch 'TEST_PASS' -or $simText -match 'Fatal:|ERROR:') { throw 'RTL self-check failed' }
Set-Location '..'
VCD 只记录前 2000 ns,然后 run all 完成剩余自检;CSV 覆盖整个测试。sim.wdb 是 XSim 实际生成的原生波形数据库,不能仅凭它存在推断所有信号、全时段都被记录。零退出码必须同时结合 PASS、断言和计数核验,不能代替功能检查。
完整交付包还包含自动化复现入口,使用已经安装的 Python 标准库,不安装软件。下列输出目录不能已存在,以免覆盖本次原始证据:
powershell
python run_tests.py --suite original --out rerun_original
python run_tests.py --suite negative --out rerun_negative
这两条命令分别运行两题的原始测试和 8 个故意错误变体。每个 case 独立保存源代码副本、命令、时间、退出码、原始日志、源码哈希。负例使用包内 review/tb_secded_review.sv(顶层 tb_secded_review)和 review/tb_isqrt_review.sv(顶层 tb_isqrt_review);只有编译、展开成功且出现指定功能断言才算"被捕获",编译错误或 watchdog 不算。独立正常测试可用 python run_review.py --out rerun_review 运行;该复现入口也已实际执行通过,证据在 runs_review_reproduction/。
本次工具为 XSim 2022.1,SW Build 3526262、IP Build 3524634。最终正常运行 runs_corrected_original/sqrt_original/ 的 xvlog、xelab、xsim 退出码均为 0,日志含 TEST_PASS ISQRT original widths=1,3,8,9,16,32,无 Fatal/ERROR。每个参数的 PASS 行和 CSV 数量一致:
| N_WIDTH | 接受请求 | 消费结果 | 复位丢弃 | 输出停顿边沿 | 同拍消费/接受 | 观察周期 |
|---|---|---|---|---|---|---|
| 1 | 8 | 6 | 2 | 5 | 2 | 43 |
| 3 | 16 | 13 | 3 | 4 | 7 | 85 |
| 8 | 268 | 263 | 5 | 61 | 205 | 1603 |
| 9 | 526 | 520 | 6 | 122 | 410 | 3644 |
| 16 | 471 | 462 | 9 | 145 | 451 | 4481 |
| 32 | 487 | 470 | 17 | 158 | 451 | 8682 |
总计 1776=1734+42。每个实例最终没有在途请求,复位丢弃数为 R+1,覆盖接收后尚未迭代、各中间年龄和结果待消费的状态。周期数是本次 TB 的观察次数,不是某颗 FPGA 的吞吐实测。
这里保留了一次真实测试缺陷:首轮主 TB 用无符号 value 与有符号 j=-1 混算构造邻域,在本次 XSim 中未得到预期的 value-1,大位宽"平方数前一位"定向用例被过滤。独立 reviewer 从实际 CSV 发现覆盖缺口。最终 TB 改成明确的 value-64'd1、value、value+64'd1 三个分支,重跑后重新检查这些点实际被消费。旧结果保留在 runs_original/sqrt_original/,不能作为最终邻域覆盖证据。这是主 TB 的 stimulus 覆盖缺陷;RTL 未改,独立 TB 原有邻域构造正确,也不是人为负例。

图 2 从实际消费的 W=9 CSV 选出全部 512 个输入,根为阶梯状,余数在下一个完全平方数处归零。这是实测结果图,横轴是输入整数,不是时间。图中最大 N=511 得到 q=22、r=27;根的平方与余数恢复原输入。
独立复核:另外一种参考算法和更大奇数宽度
独立 reviewer 先按 CONTRACT.md 写 review/tb_isqrt_review.sv,之后才检查 RTL;编写并执行独立 TB 时没有读取原始 TB,后续文章终审才读取主 TB 并发现前述覆盖缺口。其参考根通过 64 位整数二分搜索得到;协议模型独立跟踪 active、年龄和结果是否被消费,不读取 DUT 内部计算寄存器。
| N_WIDTH | 观察周期 | 接受 | 消费 | 复位丢弃 | 已确认不同输入数 |
|---|---|---|---|---|---|
| 1 | 654 | 262 | 260 | 2 | 2 |
| 3 | 711 | 197 | 194 | 3 | 8 |
| 8 | 2646 | 382 | 377 | 5 | 256 |
| 9 | 5115 | 623 | 617 | 6 | 512 |
| 16 | 2990 | 261 | 252 | 9 | 245 |
| 31 | 5395 | 268 | 251 | 17 | 241 |
| 32 | 5459 | 271 | 254 | 17 | 244 |
独立日志在 review_runs_isqrt/,三阶段退出码均为 0,最终出现 TEST_PASS ISQRT_REVIEW。22970 个观察周期中,2264=2205+59;所有小宽度输入都实际被消费并校验,大宽度是定向/随机抽样,不是穷举。31 位实例单独覆盖更大奇数位宽的补零行为;32 位覆盖最大 N=4294967295,对应 q=65535、r=131070。
独立测试每个宽度还执行 600 周期合法随机流,源在阻塞时保持,输入空隙和输出停顿会交错。完整报告在 review/ISQRT_REVIEW.md。两套 TB 的结果 CSV 又经 analyze_evidence.py 使用 Python 任意精度整数与 math.isqrt 重放核验,输出 independent_csv_checks.json;它处理真实仿真结果,没有用软件模型代替任何 RTL 执行。
四个故意错误,分别暴露什么
| 故意变体 | 独立 TB 首个功能失败 |
|---|---|
将 u>=t 写成 u>t |
W=1,完全平方边界 q=0/r=1,预期 q=1/r=0 |
将试减值 4q+1 写成 2q+1 |
W=3,q=3/r=0,预期 q=2/r=3 |
| 不管 ready,每拍清除 m_valid | W=1,结果阻塞时 m_valid 消失 |
| 奇数宽补位时左移 N | W=1,q=1/r=1,预期 q=1/r=0 |
这四个变体均通过 xvlog 和 xelab,再由 ISQRT_REVIEW_ASSERT 触发功能 Fatal;没有把语法错误、超时或非法 stimulus 当作成功捕获。故障日志和变体源码在 runs_negative/sqrt_*/。原始 RTL 没有这些人为错误,两位 reviewer 在本次契约和测试范围内没有发现原始功能缺陷。
完整测试既检查最终答案,也检查结果出现时刻和请求守恒。单纯对几组数打印 $display,或只检查 q*q<=N,会漏掉错误余数、过小根、提前 valid、被停顿吞掉的结果以及复位后的幽灵响应。
边界与后续工程工作
本模块一次只处理一个请求,计算中不接收下一请求;结果背压会继续阻止接收。复位取消的是本地未消费工作,系统若需要保留任务,应在更高层重发。输入事件和握手信号必须已属于这个时钟域;跨域处理不在本实验内。
算法支持最小和奇数输入宽度;本次实际仿真参数并不包括所有正整数宽度。也没有测综合后的最大频率、资源、功耗或真实硬件时序。只有完成目标器件上的综合、约束与实现分析后,才能给出这些结论。
工具命令参考 UG900 2022.1。官方资料解释工具与算法背景,本次通过依据是随包保存的 2026-10-23 源码、原始日志和逐周期输出。