FPGA串行除法器设计:恢复余数法的Verilog/VHDL实现与场景选择
2026/6/7 17:57:09 网站建设 项目流程

1. 项目背景与需求分析

最近在搞一个信号处理的算法,里面有个环节需要做除法运算。一开始图省事,想着直接用Xilinx的除法器IP核,毕竟官方的东西,性能有保障,集成也方便。但等我仔细研究了IP核的文档和时序后,发现了一个关键问题:这个IP核是流水线(Pipeline)架构的。

流水线架构的优势在于吞吐量高。如果你有一大堆数据需要连续做除法,数据一个接一个地喂进去,结果也会一个接一个地流出来,平均每个时钟周期都能出一个结果,效率非常高。这就像一条工厂流水线,虽然单个产品从上线到下线需要时间,但整条线可以源源不断地生产。

但我的算法逻辑不是这样的。我的算法步骤是迭代的,需要先算出A/B的结果,然后用这个结果去参与下一次运算,才能开始计算下一个除法。也就是说,下一次除法的开始,严格依赖于上一次除法的完成。这就尴尬了。流水线IP核虽然吞吐高,但它的“首拍延迟”(Latency)是固定的,比如可能需要5个时钟周期才能输出第一个结果。在我的场景下,我没办法在第一个结果出来之前就塞进去第二个数据,因为第二个数据依赖于第一个结果。这样一来,我实际使用IP核的效率,就退化成了它的延迟时间。每个除法都要等满整个流水线深度,反而比一个简单的、每个时钟周期都能推进一步的串行除法器更慢。

所以,与其用一个“大炮打蚊子”,让高性能流水线IP核在我这里英雄无用武之地,还不如自己动手,设计一个更贴合实际需求的、非流水线的串行除法器。目标很明确:在输入使能后,开始计算,计算期间输出“忙”信号,计算完成时给出结果和“完成”信号,且整个计算过程是确定性的,便于我进行精确的时序控制。

考虑到开源共享和不同工程师的习惯,我决定用Verilog和VHDL两种硬件描述语言分别实现,核心算法保持一致。这样无论是用哪种语言的兄弟,都能直接参考或移植。

2. 除法器核心算法:恢复余数法

既然决定自己写,就得选个合适的算法。在FPGA里实现除法,常见的有几种思路:查找表(LUT)、基于乘法的迭代算法(如牛顿-拉夫森法)、以及直接基于数字逻辑的移位相减算法。对于一般的整数除法,尤其是位宽不是特别大(比如32位以内)的情况,恢复余数法(Restoring Division Algorithm)是一个在面积、速度和实现复杂度上取得很好平衡的选择。它逻辑清晰,非常适合用寄存器传输级(RTL)描述。

它的核心思想模拟了我们手算竖式的过程。假设我们要计算dividend / divisor = quotient ... remainder

2.1 算法步骤拆解

我们以一个简单的4位无符号数为例:被除数(dividend)0111(7),除数(divisor)0011(3)。期望得到商(quotient)0010(2),余数(remainder)0001(1)。

  1. 初始化:将余数寄存器R清零,将被除数D加载到一个临时寄存器中。商的每一位初始为0。设位宽为n
  2. 循环迭代(进行 n 次): a.左移:将{R, D}组成的整体向左移动一位。这样,余数R的最高位移出,被除数D的最高位进入R的最低位。同时,商寄存器Q也左移一位,为新的商位腾出位置。 b.试探性减法:用左移后的新余数R_new减去除数divisor,得到一个临时结果R_sub。 c.判断与恢复: - 如果R_sub为负数(即最高位为1,表示不够减),那么这一步的商位设为0。并且,我们需要“恢复”余数,即保持R_new不变,进入下一轮。 - 如果R_sub为正数或零(即最高位为0,表示够减),那么这一步的商位设为1。并且,用R_sub更新余数寄存器R
  3. 结束n次循环后,商寄存器Q中就是最终结果,余数寄存器R中就是最终余数。

2.2 手算推演过程

让我们结合上面的例子,一步步看:

步骤操作R (余数)D (被除数/移位中)Q (商)说明
初始000001110000初始化
第1步左移0000111_000_{R,D}左移,D最高位0进入R
减除数0000 - 0011 = 1101111_000_R_sub为负 (1101)
商置0,恢复余数000011100000商位=0,R保持左移后的0000
第2步左移0001110_000_{R,D}左移,D1进入R
减除数0001 - 0011 = 1110110_000_R_sub为负 (1110)
商置0,恢复余数000111000000商位=0
第3步左移0011100_000_{R,D}左移
减除数0011 - 0011 = 0000100_000_R_sub非负 (0000)
商置1,更新余数000010010001商位=1,R更新为0000
第4步左移0010001_001_{R,D}左移
减除数0010 - 0011 = 1111001_001_R_sub为负 (1111)
商置0,恢复余数001000100010商位=0
结束0010(余数2?)0010(商2)等等,余数不对!

发现问题了吗?最终余数寄存器R0010(2),但我们期望的余数是1。这是因为在算法结束时,余数寄存器里保存的是最后一次左移后、未经减法修正的值。真正的余数应该是我们最后一次“够减”操作后的那个R_sub值吗?不完全是。

关键理解:在恢复余数法中,循环结束后,余数寄存器R中保存的就是最终的余数。但是,这个余数是相对于原始被除数左移了n位后的“部分余数”。为了得到真正的余数,我们需要对它进行右移n位的修正吗?对于整数除法,我们通常定义被除数 = 商 * 除数 + 余数,且0 <= 余数 < 除数。在上面的手算中,我们实际上是把被除数当作了一个整数来处理。在硬件实现时,我们通常会将被除数扩展一倍位宽,与余数寄存器一起左移。这样,循环结束后,余数寄存器中的值就是正确的余数,无需额外移位修正。

让我们修正一下,将4位被除数放在一个8位寄存器的低4位,高4位初始为0(作为余数寄存器R)。

步骤操作R[7:4] (余数)D[3:0] (被除数)Q[3:0] (商)说明
初始000001110000被除数放在低4位
第1步左移 {R,D}0000111_000_整体左移,D最高位0进入R
减除数0000 - 0011 = 1101111_000_R_sub为负
商置0,恢复000011100000
第2步左移 {R,D}0001110_000_
减除数0001 - 0011 = 1110110_000_R_sub为负
商置0,恢复000111000000
第3步左移 {R,D}0011100_000_
减除数0011 - 0011 = 0000100_000_R_sub非负
商置1,更新000010010001
第4步左移 {R,D}0010001_001_
减除数0010 - 0011 = 1111001_001_R_sub为负
商置0,恢复001000100010
结束0001(真余数)0010(商)循环结束,R寄存器值为0001,正确!

看,这样就对了。循环结束后,商Q=0010(2),余数寄存器R=0001(1)。这就是恢复余数法在硬件中的标准实现方式:将被除数存储在低位,初始余数(0)存储在高位,组成一个双倍位宽的寄存器进行左移操作。

3. Verilog版本设计与实现详解

基于上述算法,我们来设计一个参数化的、易于使用的除法器模块。

3.1 模块接口定义

首先明确输入输出。我们需要:被除数、除数、启动信号。输出需要:商、余数、忙状态指示。

module restoring_divider #( parameter WIDTH = 32 // 数据位宽,可配置 )( input wire clk, input wire rst_n, // 低电平复位 input wire start, // 高电平启动信号,至少保持一个时钟周期 input wire [WIDTH-1:0] dividend, // 被除数 input wire [WIDTH-1:0] divisor, // 除数 output reg [WIDTH-1:0] quotient, // 商 output reg [WIDTH-1:0] remainder, // 余数 output reg busy, // 忙标志,计算期间为高 output reg done // 计算完成脉冲,高电平一个周期 );

这里我增加了done信号,作为一个单周期脉冲,用来指示计算完成时刻,比单纯观察busy下降沿更便于后续电路捕获。

3.2 内部状态机设计

这是一个典型的顺序逻辑,用状态机(FSM)来实现非常清晰。我们定义三个状态:

  • IDLE:空闲状态,等待start信号。
  • CALC:计算状态,进行移位、比较、恢复的循环。
  • DONE:完成状态,输出结果并产生done脉冲。
localparam [1:0] IDLE = 2'b00, CALC = 2'b01, DONE = 2'b10; reg [1:0] current_state, next_state; reg [WIDTH-1:0] calc_cnt; // 计算循环计数器 reg [WIDTH*2-1:0] rd_reg; // {余数, 被除数} 双倍位宽寄存器 reg [WIDTH-1:0] d_reg; // 除数寄存器 reg [WIDTH-1:0] q_reg; // 商寄存器

rd_reg是关键,它是一个2*WIDTH位的寄存器,高WIDTH位是余数R,低WIDTH位初始存放被除数D。在计算过程中,它作为一个整体左移。

3.3 核心计算逻辑

状态机的转移和计算逻辑都在一个always块中描述。这里贴出CALC状态的核心部分:

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin current_state <= IDLE; quotient <= 0; remainder <= 0; busy <= 0; done <= 0; calc_cnt <= 0; rd_reg <= 0; d_reg <= 0; q_reg <= 0; end else begin current_state <= next_state; done <= 0; // 默认done为0,只在特定时刻拉高 case (current_state) IDLE: begin if (start) begin busy <= 1; d_reg <= divisor; // 初始化双倍寄存器:高WIDTH位为0(余数),低WIDTH位为被除数 rd_reg <= {{WIDTH{1'b0}}, dividend}; q_reg <= 0; calc_cnt <= 0; next_state <= CALC; end else begin next_state <= IDLE; end end CALC: begin // 1. 左移 {R, D} 和 Q rd_reg <= rd_reg << 1; q_reg <= q_reg << 1; // 2. 试探性减法:用左移后的余数部分(rd_reg的高WIDTH位)减除数 // 注意:左移后,余数部分在 rd_reg[2*WIDTH-1:WIDTH],被除数部分在 rd_reg[WIDTH-1:0] // 我们用一个临时变量存储减法结果 if (rd_reg[2*WIDTH-1:WIDTH] >= d_reg) begin // 够减:商位置1,更新余数 q_reg[0] <= 1'b1; // 商的最低位设为1 // 更新双倍寄存器的高位(余数部分)为减法结果 rd_reg[2*WIDTH-1:WIDTH] <= rd_reg[2*WIDTH-1:WIDTH] - d_reg; end else begin // 不够减:商位置0,余数部分保持不变(即“恢复”) q_reg[0] <= 1'b0; // rd_reg[2*WIDTH-1:WIDTH] 保持不变 end // 3. 循环计数 calc_cnt <= calc_cnt + 1; if (calc_cnt == WIDTH - 1) begin // 已完成WIDTH次迭代 next_state <= DONE; end else begin next_state <= CALC; end end DONE: begin // 计算完成,输出结果 quotient <= q_reg; remainder <= rd_reg[2*WIDTH-1:WIDTH]; // 最终余数 busy <= 0; done <= 1; // 产生一个周期的高电平完成脉冲 next_state <= IDLE; end default: next_state <= IDLE; endcase end end

关键细节与避坑指南

  1. 比较与减法时机:在CALC状态,我们是在rd_reg左移之后,再用它的高位去和除数比较。这个顺序很重要。也可以先比较左移前的余数,但那样逻辑会稍复杂。上述写法是标准流程。
  2. 位宽与符号:这里实现的是无符号整数除法。如果需要有符号除法,需要先记录符号位,将被除数和除数都转换为其绝对值进行无符号除法,最后再根据符号位调整商和余数的符号。余数的符号通常与被除数相同。
  3. 除数为零处理:这是一个严重的现实问题。在实际项目中,必须加入除零保护。可以在IDLE状态检查divisor是否为0。如果为0,可以跳转到一个错误状态,输出特定的错误码(比如将商和余数设为全1,或拉高一个error信号),并结束计算。本例为了核心算法清晰,暂未添加,但你一定要在自己的代码里加上。
  4. done信号:我只在DONE状态维持一个时钟周期的高电平。这样下游电路可以用done的上升沿来锁存结果,非常方便。busy信号则在计算开始到结束期间一直为高。

3.4 综合与面积考虑

恢复余数法每个时钟周期完成一位商的计算,需要WIDTH个时钟周期完成整个除法。它的优势是面积小,因为核心运算只是一个WIDTH位的减法器/比较器和一个移位寄存器。对于需要低延迟或高吞吐的场景,这不是最优选择,但正如项目开头所说,对于我这种“结果依赖型”的串行算法,它恰恰避免了流水线IP核的首拍延迟惩罚,总耗时是确定的WIDTH+2个周期左右(加上状态切换),反而更可控。

4. VHDL版本设计与实现详解

VHDL版本的算法核心与Verilog完全一致,主要是语法和描述风格上的转换。为了便于对比和理解,我采用相似的结构化描述方式。

4.1 实体(Entity)声明

library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.NUMERIC_STD.ALL; -- 必须使用这个库进行算术运算 entity restoring_divider is Generic ( WIDTH : integer := 32 ); Port ( clk : in STD_LOGIC; rst_n : in STD_LOGIC; start : in STD_LOGIC; dividend : in STD_LOGIC_VECTOR (WIDTH-1 downto 0); divisor : in STD_LOGIC_VECTOR (WIDTH-1 downto 0); quotient : out STD_LOGIC_VECTOR (WIDTH-1 downto 0); remainder : out STD_LOGIC_VECTOR (WIDTH-1 downto 0); busy : out STD_LOGIC; done : out STD_LOGIC ); end restoring_divider;

注意,我使用了IEEE.NUMERIC_STD库。这是VHDL中进行符号/无符号算术运算的标准且推荐的方式,避免使用非标准的std_logic_arith等库。

4.2 架构体(Architecture)与信号声明

architecture Behavioral of restoring_divider is type state_type is (S_IDLE, S_CALC, S_DONE); signal current_state, next_state : state_type := S_IDLE; signal calc_cnt : integer range 0 to WIDTH-1 := 0; signal rd_reg : unsigned(2*WIDTH-1 downto 0) := (others => '0'); -- {余数, 被除数} signal d_reg : unsigned(WIDTH-1 downto 0) := (others => '0'); signal q_reg : unsigned(WIDTH-1 downto 0) := (others => '0'); -- 为了方便,定义余数部分和被除数部分的别名 alias remainder_part : unsigned(WIDTH-1 downto 0) is rd_reg(2*WIDTH-1 downto WIDTH); alias dividend_part : unsigned(WIDTH-1 downto 0) is rd_reg(WIDTH-1 downto 0); begin

这里我使用了unsigned类型来表示无符号数,并使用alias(别名)来让代码中对rd_reg高低位的操作更清晰。

4.3 状态机与核心逻辑进程

VHDL通常使用进程(Process)来描述时序逻辑。以下是核心部分:

state_proc: process(clk, rst_n) begin if rst_n = '0' then current_state <= S_IDLE; quotient <= (others => '0'); remainder <= (others => '0'); busy <= '0'; done <= '0'; calc_cnt <= 0; rd_reg <= (others => '0'); d_reg <= (others => '0'); q_reg <= (others => '0'); elsif rising_edge(clk) then current_state <= next_state; done <= '0'; -- 默认done为低 case current_state is when S_IDLE => if start = '1' then busy <= '1'; d_reg <= unsigned(divisor); -- 初始化:高WIDTH位为0,低WIDTH位为被除数 remainder_part <= (others => '0'); dividend_part <= unsigned(dividend); q_reg <= (others => '0'); calc_cnt <= 0; next_state <= S_CALC; else next_state <= S_IDLE; end if; when S_CALC => -- 1. 左移 {R, D} 和 Q rd_reg <= rd_reg sll 1; -- sll 是逻辑左移 q_reg <= q_reg sll 1; -- 2. 试探性减法与商位确定 if remainder_part >= d_reg then -- 够减 q_reg(0) <= '1'; remainder_part <= remainder_part - d_reg; else -- 不够减,商位为0,余数部分已通过左移更新,无需额外操作 q_reg(0) <= '0'; -- remainder_part 保持为左移后的值 end if; -- 3. 循环计数与状态转移 if calc_cnt = WIDTH-1 then next_state <= S_DONE; else calc_cnt <= calc_cnt + 1; next_state <= S_CALC; end if; when S_DONE => quotient <= std_logic_vector(q_reg); remainder <= std_logic_vector(remainder_part); busy <= '0'; done <= '1'; next_state <= S_IDLE; when others => next_state <= S_IDLE; end case; end if; end process state_proc;

VHDL实现注意事项

  1. 类型转换:输入端口是STD_LOGIC_VECTOR,内部运算使用unsigned。需要使用unsigned()进行转换,输出时再用std_logic_vector()转回来。这确保了类型安全。
  2. 移位操作符sll(Shift Left Logical) 是逻辑左移,与Verilog的<<等价。注意sll的操作数需要是unsignedsigned类型。
  3. 比较运算remainder_part >= d_reg这种比较直接可行,因为两者都是unsigned类型。
  4. 初始化:VHDL中可以对信号声明时初始化(如:= (others => '0')),但要注意这通常只在仿真中有效,综合器的支持情况取决于目标和工具。可靠的复位逻辑仍然至关重要。

5. 仿真测试与验证

设计完成不代表工作结束,充分的仿真测试是保证设计正确的关键。我会搭建一个简单的测试平台(Testbench),用随机数和边界值来测试这个除法器。

5.1 Verilog Testbench 示例

`timescale 1ns / 1ps module tb_divider(); parameter WIDTH = 8; // 用8位测试,仿真快 reg clk, rst_n, start; reg [WIDTH-1:0] dividend, divisor; wire [WIDTH-1:0] quotient, remainder; wire busy, done; restoring_divider #(.WIDTH(WIDTH)) uut ( .clk(clk), .rst_n(rst_n), .start(start), .dividend(dividend), .divisor(divisor), .quotient(quotient), .remainder(remainder), .busy(busy), .done(done) ); // 生成时钟 always #5 clk = ~clk; // 100MHz时钟 initial begin // 初始化 clk = 0; rst_n = 0; start = 0; dividend = 0; divisor = 0; #20 rst_n = 1; // 释放复位 // 测试用例1:正常除法 150 / 13 #10; dividend = 150; divisor = 13; start = 1; #10 start = 0; // 启动一个周期 wait(done == 1); // 等待计算完成 #10; $display("Test 1: %d / %d = %d ... %d", dividend, divisor, quotient, remainder); if (quotient != 11 || remainder != 7) $error("Test 1 Failed!"); // 测试用例2:除数为1 #20; dividend = 200; divisor = 1; start = 1; #10 start = 0; wait(done == 1); #10; $display("Test 2: %d / %d = %d ... %d", dividend, divisor, quotient, remainder); if (quotient != 200 || remainder != 0) $error("Test 2 Failed!"); // 测试用例3:被除数小于除数 #20; dividend = 50; divisor = 100; start = 1; #10 start = 0; wait(done == 1); #10; $display("Test 3: %d / %d = %d ... %d", dividend, divisor, quotient, remainder); if (quotient != 0 || remainder != 50) $error("Test 3 Failed!"); // 测试用例4:随机测试 #20; for (int i=0; i<10; i=i+1) begin dividend = $urandom % (2**WIDTH); divisor = $urandom % (2**WIDTH); // 避免除数为0 while (divisor == 0) divisor = $urandom % (2**WIDTH); start = 1; #10 start = 0; wait(done == 1); #10; if (quotient * divisor + remainder !== dividend) begin $error("Random Test %0d Failed: %d / %d, Got Q=%d R=%d", i, dividend, divisor, quotient, remainder); end else begin $display("Random Test %0d Passed: %d / %d = %d ... %d", i, dividend, divisor, quotient, remainder); end #20; end $display("All tests completed."); $finish; end endmodule

5.2 仿真结果分析

在Modelsim或Vivado Simulator中运行上述测试平台,你应该能看到所有测试用例通过。重点关注以下几点波形:

  1. start拉高后,busy信号是否立即变高。
  2. busy为高期间,内部计数器calc_cnt是否从0递增到WIDTH-1
  3. 计算完成后,busy是否变低,同时done信号是否产生一个单周期脉冲。
  4. done脉冲对应的时钟沿,输出quotientremainder是否与软件计算的结果一致。
  5. 特别观察除数为0的测试(需要在设计中加入保护逻辑后再测试),确保电路不会进入不可控状态或产生错误结果。

5.3 关键测试场景

  • 边界值测试:被除数为0,除数为1,被除数等于除数,被除数远大于除数,除数等于2^WIDTH-1(最大值)。
  • 随机性测试:用大量随机数进行测试,验证被除数 = 商 * 除数 + 余数这一基本等式始终成立,且0 <= 余数 < 除数
  • 背靠背(Back-to-Back)操作测试:在第一个done脉冲后立即拉高start,开始下一次计算,检查状态机是否能正确复位并重新开始,中间没有遗漏状态。这对于我的迭代算法场景尤为重要。
  • 复位测试:在计算过程中(busy=1时)突然拉低复位信号rst_n,观察所有寄存器是否被清零,状态机是否回到IDLE,输出是否变为0。

6. 实际应用中的优化与扩展

基本的恢复余数除法器已经可用,但在实际项目中,我们还可以根据需求进行优化和扩展。

6.1 性能优化:提前终止

对于许多实际数据,除数可能很大,导致高位商都是0。我们可以加入“提前终止”逻辑。在CALC状态,每次左移后,检查当前余数部分(remainder_part)是否已经小于除数。如果是,并且剩余的移位次数(WIDTH - calc_cnt - 1)还很多,那么后续的商位肯定都是0。我们可以直接将剩余位数的0移入商寄存器,并跳转到完成状态。这可以显著减少计算时钟周期,但会增加比较逻辑的复杂度。

6.2 处理有符号数

如前所述,处理有符号数需要额外的步骤:

  1. 输入时,记录被除数和除数的符号位。
  2. 将被除数和除数都转换为其绝对值(如果是负数,取补码)。
  3. 调用无符号除法器进行计算。
  4. 根据规则确定结果的符号:
    • 商的符号:被除数符号 XOR 除数符号。
    • 余数的符号:通常与被除数符号相同(遵循被除数 = 商 * 除数 + 余数的等式)。
  5. 如果商或余数为负,将其转换回补码形式输出。

6.3 与系统集成:握手协议

目前的模块使用简单的start脉冲和done脉冲。在更复杂的系统中,可能需要使用标准的握手协议,如Valid/Ready协议。

  • in_valid:输入数据有效。
  • in_ready:模块准备好接收新数据。
  • out_valid:输出数据有效。
  • out_ready:下游模块准备好接收数据。

加入握手协议后,模块的接口会更通用,更容易集成到数据流中。这需要修改状态机,在IDLE状态等待in_valid && in_ready,在DONE状态等待out_valid && out_ready后再离开。

6.4 资源与时序评估

在Xilinx 7系列FPGA上综合一个32位的恢复余数除法器,主要消耗的资源是:

  • 触发器(FF):用于状态机、计数器、rd_regq_regd_reg等,大约需要2*32 + 32 + 32 + 2 = 130个左右。
  • 查找表(LUT):主要用于WIDTH位的比较器/减法器、状态机译码、计数器等。一个32位比较器会消耗一定数量的LUT。
  • 关键路径:通常是从rd_reg的高位经过比较器,再到多路选择器更新rd_regq_reg的路径。这个路径的延迟决定了电路能运行的最高时钟频率。

你可以使用综合工具(如Vivado)的综合报告来查看具体的资源占用和时序分析。对于速度要求不高的控制类应用,这个设计通常可以跑到很高的频率(>100MHz)。如果时序紧张,可以考虑在比较器路径上插入流水线寄存器,但这会额外增加一个时钟周期的延迟。

7. 总结与选择建议

自己动手实现这个除法器后,再回头看最初的问题,思路就非常清晰了。选择IP核还是自己写,不是一个单纯的技术高低问题,而是一个“合适”的问题。

  • 选择流水线除法器IP核的场景

    • 数据吞吐量是首要指标:你有大量独立的数据需要连续进行除法运算。
    • 系统时钟频率很高,足以掩盖流水线延迟带来的影响。
    • 项目时间紧迫,且IP核的性能和面积满足要求。
    • 你需要支持有符号数、多种舍入模式等复杂功能,自己实现成本高。
  • 选择自研串行除法器(如本文方案)的场景

    • 算法是串行依赖的:下一次计算必须用到上一次的结果。这是最核心的驱动力。
    • 对面积非常敏感:需要极致的面积优化,串行除法器面积通常远小于并行或流水线除法器。
    • 时钟频率要求不高,但需要确定性的、固定的计算延迟。
    • 作为学习或教学目的,理解除法器底层原理。

最后,分享一个我在调试时踩过的坑:仿真时一定要用随机数进行大量测试,尤其是边界情况。我最初只测试了几个简单用例就以为没问题了,结果集成到大的算法中时,偶尔会出现商差1的情况。后来发现是在某次“够减”判断的边界条件下,由于比较器的逻辑描述有一点瑕疵(当时用了组合逻辑判断,产生了细微的时序问题),导致该上商1的时候上了0。花了好长时间才定位到。所以,对于这种涉及算术运算的模块,一个健壮的测试平台价值连城。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询