FPGA 不用浮点开平方:逐位恢复整数平方根、余数与完整握手验证

能量、距离平方或计数统计常会产生一个非负整数 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 源码、原始日志和逐周期输出。

相关推荐
Enclustra_China1 小时前
FPGA设计的隐藏难题:算力、功耗与散热如何平衡?
fpga开发·fpga·fpga解决方案
好焊孙辉1 小时前
从六代螺柱焊机到AI质检:鸿栢科技的螺柱焊国产化路径
人工智能·python·科技·fpga开发
unicrom_深圳市由你创科技3 小时前
高帧率机器视觉 FPGA 图像板卡及传输开发定制解决方案
fpga开发
幻智星科技20 小时前
纯FPGA网络通信实现微秒级确定性延迟
网络协议·fpga开发·硬件架构
泡泡糖的中文规格书21 小时前
HDLbits 1.5 学习笔记
fpga开发
幻智星科技1 天前
FPGA不同品牌开发平台约束文件迁移难题解析
fpga开发·架构·硬件架构
幻智星科技1 天前
Karatsuba与CSD优化FPGA大数乘法
c++·fpga开发·密码学
星夜离尘561 天前
FPGA 实战:W1C 中断状态寄存器,事件置位与软件清除撞在同一拍怎么办
fpga开发
像枫一样1 天前
STM32串口轮询收发实战教程
arm开发·c++·stm32·单片机·嵌入式硬件·fpga开发·51单片机