Verilog实现4位串行加法器:从全加器到RCA的完整设计流程
2026/6/7 15:18:36 网站建设 项目流程

1. 从全加器到串行加法器:一个经典数字电路的Verilog实现之旅

在数字电路和FPGA设计领域,加法器是构成算术逻辑单元(ALU)乃至整个处理器最基础的模块之一。无论是做嵌入式开发、信号处理,还是单纯的数字逻辑学习,亲手实现一个加法器都是绕不开的“必修课”。上次我们聊了半加器,算是开了个头,今天咱们就深入一步,用Verilog HDL来实现一个完整的4位串行加法器。你别看它“只有”4位,从全加器的门级描述,到模块的级联封装,再到测试验证和综合实现,这一整套流程走下来,几乎涵盖了小型数字系统设计的全貌。我当年学FPGA的时候,就是从这个例子开始,才真正理解了数据流、时序和硬件描述语言之间的关系。这篇文章,我就把自己踩过的坑、总结的经验,以及那些教科书上不会写的细节,掰开揉碎了讲给你听。

2. 核心思路:为什么是串行进位加法器?

在动手写代码之前,我们得先想清楚要做什么,以及为什么这么做。加法器有很多种,比如超前进位加法器(Carry Look-ahead Adder, CLA)、选择进位加法器(Carry Select Adder)等,它们各有优劣,适用于不同的速度、面积和功耗场景。

我们这里实现的“串行加法器”,更准确的叫法是“行波进位加法器”(Ripple Carry Adder, RCA)。它的核心思想非常简单直接:将多个1位的全加器(Full Adder)像链条一样连接起来,低位全加器的进位输出(Carry Out)直接连接到高位全加器的进位输入(Carry In)。计算时,进位信号像水波一样从最低位向最高位依次传递、计算。

为什么先从它开始?

  1. 结构直观,易于理解:它的工作原理与我们在纸上竖式计算加法的过程完全一致,非常符合直觉。对于初学者而言,这是理解硬件加法最没有障碍的模型。
  2. 代码简洁,便于教学:用Verilog描述其结构非常清晰,是学习模块实例化(Module Instantiation)和线网(Wire)连接的绝佳范例。
  3. 是其他高级加法器的基础:CLA等快速加法器本质上是为了解决RCA进位延迟长的问题而做的优化。理解了RCA的瓶颈,才能更好地 appreciate 其他方案的巧妙之处。

当然,它的缺点也很明显:速度慢。因为高位必须等待低位的进位信号计算完成后才能开始计算,所以总延迟与加法器的位数成正比。对于一个n位的RCA,最坏情况下的延迟是n个全加器的延迟之和。在追求高速的场合(比如CPU的ALU),这通常是不可接受的。但在很多对速度要求不苛刻,或者资源受限的FPGA/CPLD设计中,RCA因其面积小、功耗低、设计简单的特点,依然有其用武之地。

我们的目标就是先把这个经典结构用Verilog实现出来,并完成仿真和综合,看看它在硬件上究竟是如何工作的。

3. 基石构建:1位全加器的门级描述

万丈高楼平地起,全加器就是我们的砖块。一个1位全加器有三个输入:加数a、加数b、来自低位的进位ci;有两个输出:本位和sum、向高位的进位co。

它的真值表大家应该都很熟悉了。用逻辑表达式表示就是:

  • Sum = a ⊕ b ⊕ ci (三者异或)
  • Carry_out = (a & b) | (a & ci) | (b & ci) (三者两两相与,再或)

在Verilog中,我们可以用多种方式描述它:行为级(用always块)、数据流级(用assign连续赋值语句)或结构级(用门级原语实例化)。这里采用最直观、也最能体现其电路结构的数据流级描述。

// 文件名:full_adder.v module full_adder ( input a, // 输入位 a input b, // 输入位 b input ci, // 进位输入 Carry-in output sum, // 和输出 Sum output co // 进位输出 Carry-out ); // 数据流建模:直接使用逻辑运算符 assign sum = a ^ b ^ ci; // 异或运算得到和 assign co = (a & b) | (a & ci) | (b & ci); // 根据逻辑表达式得到进位 endmodule

代码细节与思考:

  1. 模块命名:我习惯用下划线full_adder,清晰易懂。原文档用的fulladder也没问题,保持一致性即可。
  2. 端口声明:明确列出inputoutput。这里所有信号都是1位宽,所以没有写[width-1:0]的范围。
  3. assign语句:这是连续赋值语句。它意味着等式右边的任何变化都会立即反映到左边,描述的是组合逻辑电路中并行的、持续的连接关系。注意,这不是“执行顺序”,而是“连接关系”。
  4. 运算符^是按位异或,&是按位与,|是按位或。对于1位信号,按位运算与逻辑运算结果相同。

注意:这是最经典的门级实现。在实际的FPGA综合中,综合工具可能会根据目标器件(如Xilinx的LUT结构)对这个逻辑进行优化和映射,最终生成的电路可能不是精确的与门、或门、异或门,而是用查找表(LUT)实现同等功能。但这不影响我们理解其逻辑本质。

4. 级联与封装:构建4位行波进位加法器

有了全加器这个基本单元,我们就可以像搭积木一样构建多位加法器了。对于4位加法器,我们需要4个全加器实例,并将它们的进位链串联起来。

// 文件名:ripple_carry_adder_4bit.v module ripple_carry_adder_4bit ( input [3:0] a, // 4位输入 a input [3:0] b, // 4位输入 b input ci, // 最低位的进位输入 output [3:0] s, // 4位和输出 output co // 最高位的进位输出/溢出标志 ); // 声明内部连线,用于连接各级全加器之间的进位 wire [2:0] carry; // carry[0]连接FA0->FA1, carry[1]连接FA1->FA2, ... // 实例化第一个全加器(最低位 LSB) full_adder FA0 ( .a (a[0]), .b (b[0]), .ci (ci), // 使用模块的输入ci作为最低位进位 .sum (s[0]), .co (carry[0]) // 进位输出连接到内部线carry[0] ); // 实例化第二个全加器 full_adder FA1 ( .a (a[1]), .b (b[1]), .ci (carry[0]), // 进位来自前一级FA0 .sum (s[1]), .co (carry[1]) ); // 实例化第三个全加器 full_adder FA2 ( .a (a[2]), .b (b[2]), .ci (carry[1]), // 进位来自前一级FA1 .sum (s[2]), .co (carry[2]) ); // 实例化第四个全加器(最高位 MSB) full_adder FA3 ( .a (a[3]), .b (b[3]), .ci (carry[2]), // 进位来自前一级FA2 .sum (s[3]), .co (co) // 最高位的进位输出,直接作为模块的co输出 ); endmodule

关键解析与实操要点:

  1. 位宽定义input [3:0] a表示一个4位宽的向量,a[3]是最高有效位(MSB),a[0]是最低有效位(LSB)。这是Verilog表示总线(Bus)的标准方式。
  2. 内部连线(Wire)wire [2:0] carry;声明了一个3位宽的内部连线。为什么是3位?因为4位加法器有3个内部进位(从FA0到FA1,FA1到FA2,FA2到FA3)。最低位的进位输入ci和最高位的进位输出co已经是模块端口,不需要在内部连线中声明。
  3. 模块实例化:这是结构描述的核心。full_adder FA0 (...);表示创建一个名为FA0full_adder类型的实例。括号内使用.port_name (net_name)的语法进行端口映射,将当前模块的线网连接到子模块的端口上。这种按名称映射的方式非常清晰,推荐始终使用。
  4. 进位链的形成:仔细观察端口映射:FA0.co连到了carry[0],而FA1.ci连到了同一个carry[0]。这就构成了物理上的连接,进位信号从FA0流向FA1。依此类推,形成链式结构。
  5. 溢出标志:对于有符号数加法,最高位的进位输出co并不直接等于溢出(Overflow)。溢出ovf的判断逻辑是:ovf = carry[2] ^ co;(即次高位进位与最高位进位异或)。原文档中将co直接作为ovf输出,这实际上是将进位输出作为了一个标志,更准确的模块命名可能是add4bit_with_carry_out。在严谨的算术电路中,溢出标志需要单独计算。不过对于入门理解,我们可以暂时接受这种简化。

5. 验证与仿真:用测试平台验证逻辑功能

代码写完了,但它对吗?在硬件设计里,仿真(Simulation)是我们的“虚拟实验室”。我们需要编写一个测试平台(Testbench),给设计施加激励(输入),并观察其响应(输出)。

// 文件名:tb_ripple_carry_adder.v `timescale 1ns / 1ps // 时间单位/精度 module tb_ripple_carry_adder; // 声明与设计模块对应的信号 reg [3:0] a_tb, b_tb; reg ci_tb; wire [3:0] s_tb; wire co_tb; // 实例化待测试设计(Unit Under Test, UUT) ripple_carry_adder_4bit uut ( .a (a_tb), .b (b_tb), .ci (ci_tb), .s (s_tb), .co (co_tb) ); // 初始化过程块,产生测试激励 initial begin // 初始化所有输入 a_tb = 4'b0000; b_tb = 4'b0000; ci_tb = 1'b0; #100; // 等待100个时间单位(100ns),让电路稳定或进行初始复位 // 测试用例1:常规加法 12 + 10 + 1 = 23 (0b10111),进位为1 a_tb = 4'b1100; // 12 b_tb = 4'b1010; // 10 ci_tb = 1'b1; // +1 #100; // 等待100ns,观察输出 // 期望结果:s_tb = 4‘b0111 (7), co_tb = 1‘b1 (进位)。 因为12+10+1=23,二进制10111,低4位是0111,进位是1。 // 测试用例2:带进位的加法 10 + 3 + 1 = 14 (0b1110),进位为0 a_tb = 4'b1010; // 10 b_tb = 4'b0011; // 3 ci_tb = 1'b1; // +1 #100; // 期望:s_tb = 4‘b1110 (14), co_tb = 0 // 测试用例3:产生溢出的有符号数加法?(此处按无符号理解)11 + 9 = 20 (0b10100),进位为1 a_tb = 4'b1011; // 11 b_tb = 4'b1001; // 9 ci_tb = 1'b0; #200; // 等待更长时间 // 期望:s_tb = 4‘b0100 (4), co_tb = 1。 因为20的二进制是10100,低4位是0100,进位是1。 // 可以添加更多边界测试,如全加(1111+1111+1)等 a_tb = 4'b1111; b_tb = 4'b1111; ci_tb = 1'b1; #100; // 期望:s_tb = 4‘b1111 (15), co_tb = 1。 因为1111+1111+1 = 1_1111,低4位满,进位为1。 // 测试结束 #100; $finish; // 结束仿真 end // 可选:将信号变化记录到日志文件或波形窗口 initial begin $dumpfile("wave.vcd"); // 生成波形文件供GTKWave等工具查看 $dumpvars(0, tb_ripple_carry_adder); // 转储所有变量 end endmodule

仿真操作与结果解读:

在Modelsim、Vivado Simulator或Icarus Verilog等工具中运行这个测试平台,你会看到波形图。我们以原文档的测试用例为例进行分析:

  1. 0-100ns:初始化为0,输出s=0000,co=0
  2. 100-200ns:输入a=1100(12),b=1010(10),ci=1
    • 计算:12 + 10 + 1 = 23。二进制10111
    • 观察波形:输出s应该变为0111(即23的低4位7),co应该变为1(即第5位的进位)。注意:这里s=0111co=1,合起来是1_0111,正是23。验证正确。
  3. 200-300ns:输入a=1010(10),b=0011(3),ci=1
    • 计算:10 + 3 + 1 = 14。二进制1110
    • 期望s=1110,co=0。验证正确。
  4. 300-500ns:输入a=1011(11),b=1001(9),ci=0
    • 计算:11 + 9 + 0 = 20。二进制10100
    • 期望s=0100(20的低4位4),co=1。验证正确。

如果波形显示的结果与手工计算一致,那么恭喜你,你的4位加法器逻辑功能是正确的!

实操心得:仿真时一定要自己先手算预期结果,再与波形对比。不要只看波形“有变化”就觉得对了。针对进位和边界情况(如全1相加)的测试尤为重要。另外,在测试平台中适当添加$display语句打印关键时刻的输入输出值,能更直观地辅助调试。

6. 综合与实现:从代码到实际电路

仿真通过,只意味着逻辑正确。我们的代码最终是要在FPGA或ASIC上变成实实在在的电路的。这一步就需要综合(Synthesis)和实现(Implementation)工具(如Xilinx ISE/Vivado, Quartus等)来完成。

综合(Synthesis):工具将我们的Verilog行为描述“翻译”成目标工艺库(如FPGA的LUT、触发器、进位链等)的基本元件组成的网表(Netlist)。你可以查看综合后的“RTL Schematic”(RTL级原理图),它会展示出四个全加器级联的清晰结构,正如我们设计的那样。

关键指标查看:

  • 资源利用率(Utilization):会报告使用了多少个LUT、触发器(FF)。对于这个纯组合逻辑的4位RCA,主要消耗LUT资源。
  • 时序报告(Timing Report):这是分析性能的关键。工具会分析所有路径的延迟。
    • 关键路径(Critical Path):从输入到输出延迟最长的路径。对于RCA,关键路径通常就是进位信号从ci传播到co的路径。
    • 路径延迟(Path Delay):原文档提到综合后路径延迟为8.959ns。这个延迟是工具根据器件模型(如XC3S500E-5的“-5”代表速度等级)和逻辑复杂度估算出来的,还没有考虑布局布线带来的线延迟。

实现(Implementation)与布局布线(Place & Route):这一步将综合后的网表映射到FPGA芯片的具体物理资源上,并连接它们。完成后,你可以进行“布线后仿真”(Post-Route Simulation),这个仿真模型包含了真实的线延迟和器件延迟,结果更接近芯片实际行为。

  • 布线后延迟:原文档提到在XC3S500E-5器件上可以看到器件上的延迟。这个延迟(比如可能变成10ns以上)会比综合预估的延迟大,因为它包含了信号在FPGA内部走线的延迟。这对于评估设计能否在要求的时钟频率下稳定工作至关重要。

重要提示:原文档提供的工程文件链接可能已失效。我强烈建议你不要依赖任何外部链接的源码,而是根据本文的代码自己新建工程,从头到尾操作一遍。这个过程本身的学习价值远大于得到一个现成的工程文件。自己敲代码、建工程、配约束、看报告,遇到的问题才是你真正成长的阶梯。

7. 深度优化与扩展思考

一个基本的4位RCA做完了,但作为工程师,我们的思考不能止步于此。

7.1 如何扩展为任意位宽的加法器?

难道要写64个实例化语句来做64位加法吗?当然不用!Verilog支持生成语句(generate),可以优雅地实现参数化位宽。

module parameterized_ripple_adder #( parameter WIDTH = 8 // 默认8位,实例化时可修改 )( input [WIDTH-1:0] a, input [WIDTH-1:0] b, input ci, output [WIDTH-1:0] s, output co ); wire [WIDTH:0] carry; // 声明WIDTH+1位宽的进位链,carry[0]用作ci输入,carry[WIDTH]用作co输出 assign carry[0] = ci; genvar i; // 生成变量 generate for (i=0; i<WIDTH; i=i+1) begin : gen_adder full_adder FA_inst ( .a (a[i]), .b (b[i]), .ci (carry[i]), .sum (s[i]), .co (carry[i+1]) ); end endgenerate assign co = carry[WIDTH]; endmodule

这样,只需要修改WIDTH参数,就能轻松得到16位、32位甚至更宽的加法器,代码复用性极高。

7.2 行波进位加法器的性能瓶颈与优化方向

前面提到RCA的速度慢。我们来量化一下:假设一个全加器从输入到co输出的延迟为T_fa,那么一个N位RCA的最长延迟(即关键路径延迟)约为N * T_fa。当N很大时,这个延迟是不可接受的。

优化思路:

  1. 超前进位加法器(CLA):通过额外的逻辑并行计算出所有位的进位,将延迟复杂度从O(N)降低到O(log N)。但代价是增加了电路面积和复杂度。
  2. 进位选择加法器(CSLA):将加法器分成若干段,每段同时计算“进位为0”和“进位为1”两种结果,然后根据实际到来的进位选择正确的结果。这是一种用面积换速度的方法。
  3. 进位保留加法器(CSA)与Wallace树:常用于乘法器等需要多操作数加法的场景,通过减少进位传播的次数来提速。
  4. 使用FPGA专用进位链:现代FPGA(如Xilinx、Intel的器件)内部都有专用的、快速的垂直进位链(Carry Chain)。综合工具在映射RCA时,如果能正确推断并使用这些专用硬件资源,其实际速度会比用普通LUT搭建快很多。在代码中,确保进位逻辑被写成co = (a & b) | (a & ci) | (b & ci)这种形式,有助于工具进行进位链推断。

7.3 测试的完备性与自动化

我们上面的测试平台是手写固定激励。对于更复杂的设计,需要更系统的验证方法。

  • 随机测试:使用$random生成大量随机输入,并与参考模型(如直接用+运算符计算)的结果进行比较。
  • 断言(Assertion):在测试平台或设计代码中插入断言语句,自动检查某些条件是否永远成立。
  • 覆盖率分析:检查代码覆盖率(行覆盖、条件覆盖、状态机覆盖等),确保测试用例充分。

一个简单的随机测试片段:

integer i; initial begin for (i=0; i<1000; i=i+1) begin a_tb = $random; b_tb = $random; ci_tb = $random & 1'b1; // 随机0或1 #10; // 使用行为级模型作为参考 expected_sum = a_tb + b_tb + ci_tb; expected_co = (expected_sum > 4'b1111) ? 1'b1 : 1'b0; if ({co_tb, s_tb} !== expected_sum) begin $display("Error at time %t: a=%b, b=%b, ci=%b, got {co,s}=%b%b, expected=%b", $time, a_tb, b_tb, ci_tb, co_tb, s_tb, expected_sum); end end end

8. 常见问题与调试技巧实录

在实际操作中,你肯定会遇到各种问题。这里分享几个我踩过的坑和解决方法。

问题1:仿真结果全是‘X’(不定态)或‘Z’(高阻态)。

  • 可能原因1:测试平台中reg类型变量未初始化。虽然仿真器在initial块中会给它们赋值,但在赋值之前它们就是‘X’。确保在initial块开头给所有驱动输入的reg变量赋初值。
  • 可能原因2:设计中有组合逻辑环路(Combinational Loop)。检查你的assign语句或always @(*)块,确保没有信号不经过任何时序元件(如触发器)而直接依赖于自身的输出。在RCA中,只要连接正确,一般不会形成环路。
  • 可能原因3:模块实例化时端口连接错误,导致某些输入悬空(表现为‘Z’)。务必使用按名称映射(.port_name(net_name)),并仔细检查拼写。

问题2:综合后警告“Latch inferred”(推断出了锁存器)。

  • 原因:在描述组合逻辑的always块中(比如always @(*)),没有为所有可能的输入分支指定输出。综合工具为了保证功能,会生成一个锁存器来“记忆”之前的值,这通常不是我们想要的,会浪费资源并可能引起时序问题。
  • 解决:对于组合逻辑的always块,确保使用if-elsecase语句时,有elsedefault分支覆盖所有情况。在我们的全加器数据流描述(assign)中不存在这个问题。

问题3:时序不满足,建立/保持时间违例。

  • 原因:关键路径延迟太长,超过了时钟周期要求。对于大型RCA,这很常见。
  • 排查:查看综合或布局布线后的时序报告,找到关键路径。看看路径上的逻辑级数是否过多。
  • 解决:
    • 流水线(Pipeline):在进位链中间插入寄存器,将长的组合逻辑路径打断成多个时钟周期完成。这是最常用的提速方法,但会增加延迟(Latency)。
    • 改用更快的加法器结构:如CLA。
    • 优化综合约束:尝试不同的综合策略(如优化面积、优化速度)。
    • 检查是否使用了专用进位链:查看综合报告,确认工具是否将你的加法器映射到了FPGA的快速进位链上。

问题4:行为仿真正确,但下载到板子后结果不对。

  • 可能原因1:引脚约束(.xdc或.ucf文件)错误,输入输出信号没有分配到正确的物理引脚上。
  • 可能原因2:时钟约束不正确或未添加,导致时序混乱。
  • 可能原因3:板子上的按键抖动或开关接触不良,导致输入信号不稳定。可以在设计前端添加消抖模块。
  • 调试方法:使用嵌入式逻辑分析仪(如Xilinx的ILA,Intel的SignalTap)在真实硬件上抓取信号波形,与仿真波形对比。这是硬件调试的利器。

从门级的全加器开始,一步步构建出4位加法器,再通过仿真验证、综合实现,最后思考优化和扩展,这正是一个完整的数字设计流程的缩影。我个人的体会是,初学者最容易犯的错误是只关注代码本身,而忽略了仿真验证和时序分析。一定要养成“编写-仿真-综合-查看报告-再优化”的闭环习惯。这个串行加法器项目虽小,但它像一把钥匙,帮你打开了用硬件描述语言进行系统设计的大门。下次我们可以试试用行为级描述来实现同样的功能,或者挑战一下超前进位加法器,看看如何用更多的逻辑资源来换取速度的提升。

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

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

立即咨询