Verilog计数器与累加器实战:时序逻辑状态保持与仿真验证
2026/9/18 15:06:33 网站建设 项目流程

简介:数字系统设计课程的计数器与累加器实验,在这份PDF中有完整记录。文档面向数字系统设计学习者,涵盖实验目的、器材、内容、原理图、仿真波形、文本程序及结果分析。包体为单个PDF文件,约191KB,集中呈现实验五十进制计数器与实验六8位累加器的设计与实现过程。学习热度方面,已有174人浏览学习。报告详细展示了图形法与Verilog HDL两种设计方法:计数器要求加法计数、时钟上升沿触发、异步清零低电平有效、同步置数高电平有效;累加器则综合8位全加器与8位寄存器,在原理图下组合成add8.bdf电路。针对试验箱抖动问题给出添加去抖电路的处理,并在仿真时移除以便观察,体现了实际调试与验证的灵活思路。通过阅读可掌握数字系统设计从原理图/文本输入、功能仿真到硬件下载测试的完整流程,适合复习实验或准备课程设计参考。

1. 计数器与累加器实验,先盯住“状态保持”这个点

数字系统设计实验里,计数器和累加器经常出现在同一份报告里,原因是它们都建立在一根骨架上:寄存器保存当前结果,组合逻辑计算下一个结果,输出反馈回输入端。换句话说,这两个模块的输出不只由当前输入决定,还由上一拍的自己决定。很多同学仿真能过、下载到板上也能亮灯,但被问一句“计数器和加法器到底差在哪”就卡住,问题就出在没把“状态保持”当成主线。

这篇不按开发板例程的顺序讲,而是按一份实验报告需要回答的问题来排:先拆结构,给出可综合的 Verilog 代码和最小 testbench,再对照波形说明复位、使能、进位和溢出这些容易被追问的点。适合正在写数字系统设计实验报告,或者想把计数器、累加器的边界条件一次弄清楚的人。

2. 计数器和累加器的结构差异:一个加固定值,一个加外部值

2.1 寄存器做状态保持,组合逻辑做数据变换

数字系统里任何“能记住上一拍”的模块,核心都是一组寄存器。组合逻辑无法记忆,输出完全由当前输入决定;时序逻辑则在时钟沿把结果锁存进寄存器。计数器实验和累加器实验放在一起讲,是因为它们的电路骨架相同:一个寄存器保存当前状态,一组组合逻辑计算下一拍状态,再用一个选择器决定是写入新值还是保持旧值。

以加法器为例,它的输出永远是“两个输入相加的结果”,输入变了输出立刻变。计数器则不同:即使组合逻辑已经算出count + 1,是否把这个值写回寄存器,还取决于使能信号。使能为低时,选择器把寄存器当前值原封不动送回寄存器,输出就保持不变。这个“原封不动送回”的动作,就是“状态保持”。累加器也是同样的结构,只不过组合逻辑不再是“加 1”,而是“加外部输入 din”。

理解到这一层,实验报告的结构也可以顺出来:先画寄存器与组合逻辑的框图,再说明使能信号控制的是“写回路径”而不是时钟,最后才是看波形验证。很多报告把功耗性能写得很多,却没有一句话说清状态保持,答辩时反而容易被问倒。

2.2 模数计数器:谁决定回绕,谁决定保持

计数器在数字系统设计里至少要能说清两个行为:什么时候加 1,什么时候回绕,什么时候保持。“回绕”指的是计数到上限后回到起点,比如模 10 计数器从 9 回到 0;“保持”指的是使能无效时,输出维持当前值,不随时钟变化。

这里有一个常见的理解偏差:教材里写“4 位计数器最大计到 15”,很多人就把 15 当成固定上限。实际上 15 只是2^4 - 1的默认值,只要在计数到某个设定值 MAX 时提前归零,模数就可以自由设定。4 位寄存器配MAX = 9,就是模 10 计数器,从 0 到 9 循环;配MAX = 15,才回到“最大计到 15”的默认行为。所以实验报告里应当把 WIDTH 和 MAX 分开定义,把“模数”当作设计规格而不是寄存器的自然溢出。

计数器行为可以浓缩成下面的状态转移表,写报告和仿真验证时都可以直接对照:

当前 count使能 en复位 rst_n下一拍 count说明
0 ~ MAX-111count + 1正常递增
MAX110回绕到计数起点
任意01count使能无效,保持
任意任意00复位优先生效

这张表值得多看一眼的是第二行:回绕动作发生在“当前值等于 MAX”的时钟沿,而不是等于 MAX 之后再多计一拍。也就是说,比较器判断的是当前状态,回绕和递增在同一拍内决定,体现在波形上就是 count 从 MAX 直接跳到 0,中间不会出现 MAX+1 的值。

2.3 累加器:反馈回环和进位标志的处理

累加器把外部输入 din 和寄存器当前值相加,结果写回寄存器。如果把 din 固定成常数 1,累加器就退化成计数器。因此从结构上说,累加器是计数器的超集,计数器是累加器在“输入恒为 1”时的一个特例。实验报告里如果能写出这一句,说明不是简单抄代码,而是真的理解了反馈路径。

累加器比计数器多一个必须回答的问题:溢出怎么办。WIDTH 位加 WIDTH 位,结果可能需要 WIDTH+1 位。比如 8 位累加器里8'hFF + 8'h01 = 9'h100,低 8 位变成 0,第 9 位是进位。硬件上常见做法是保留低 WIDTH 位,同时把最高位提取成进位标志 carry。提取进位不能靠“结果变小”这种办法判断,最稳妥的是在做加法前把两个操作数各扩展一位:

wire [WIDTH:0] sum_ext = {1'b0, acc} + {1'b0, din}; assign carry = sum_ext[WIDTH];

{1'b0, acc}把 acc 扩成 WIDTH+1 位,加法结果自然保留进位。这种写法比“加完再比较大小”综合出来的电路更直接,也方便后面对进位打一拍寄存。

累加器还牵扯到控制信号的优先级。实验板上的累加器通常有清零和累加使能两个控制端,必须明确“同时有效时谁说了算”。常见做法是清零优先:只要 clr 为高,不管使能是否有效,下一拍寄存器都清成 0。这个优先级要写进报告,也要写进代码,否则仿真的结果会依赖编写顺序。

3. 用 Verilog 写可综合的计数器和累加器代码

3.1 参数化模数计数器:模块代码与参数说明

实验里我倾向先做一个参数化计数器,用两个参数把位宽和模数分开,这样模 10、模 60、模 100 都只需要改参数。实现上使用同步复位,使能无效时保持,计数到 MAX 时下一拍归零:

module counter #( parameter WIDTH = 8, parameter MAX = 9 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] count ); always @(posedge clk) begin if (!rst_n) begin count <= {WIDTH{1'b0}}; end else if (en) begin if (count == MAX) count <= {WIDTH{1'b0}}; else count <= count + 1'b1; end // en 为低时没有任何分支生效,count 保持原值 end endmodule

这段代码里值得注意的有三点。第一,rst_n写在always @(posedge clk)内,是同步复位,只在时钟上升沿生效,这一点在后面的实验板验证部分会展开对比。第二,count == MAX是回绕判断,MAX=9 时从 9 回到 0,实现模 10 计数。第三,en无效时没有 else 分支,寄存器自己保持,这里不会产生锁存器,因为 always 块由时钟驱动,寄存器天然具有保持能力。

参数的选择直接影响电路规模和计数范围,实验报告里可以把参数表列出来:

参数含义典型取值备注
WIDTH计数器寄存器位宽4、8、16最大可表示值为 2^WIDTH - 1
MAX计数上限9、59、99模数 = MAX + 1,MAX 不能超过 2^WIDTH - 1
en计数使能1 或 0高电平加 1,低电平保持
rst_n同步复位低电平有效优先级最高,任意时刻拉低下一拍归零

3.2 累加器:清零、使能与进位输出的实现

累加器的写法与计数器类似,差别只在数据通路:加法器的另一端从常数1'b1换成外部输入din,另外多出一个进位标志。下面的模块实现了清零优先、使能累加、进位输出三个功能:

module accumulator #( parameter WIDTH = 8 )( input wire clk, input wire rst_n, input wire clr, input wire ce, input wire [WIDTH-1:0] din, output reg [WIDTH-1:0] acc, output wire carry ); wire [WIDTH:0] sum_ext = {1'b0, acc} + {1'b0, din}; assign carry = sum_ext[WIDTH]; always @(posedge clk) begin if (!rst_n) acc <= {WIDTH{1'b0}}; else if (clr) acc <= {WIDTH{1'b0}}; else if (ce) acc <= sum_ext[WIDTH-1:0]; // ce 为低时保持累加值 end endmodule

进位信号由组合逻辑直接产生,sum_ext的结果在加法完成的瞬间就稳定,不需要等时钟。acc <= sum_ext[WIDTH-1:0]截取低 WIDTH 位存入寄存器,溢出的最高位由carry对外呈现。

控制信号的优先级值得单独说明:rst_n最高,clr次之,ce最低。也就是说,清零信号和使能同时为高时,寄存器被清零而不是累加。实验报告里建议画一张和 2.2 节类似的真值表,把这种优先级明确写出来。实际使用中,如果要把carry接到下一级电路作为“进位参与后续计算”,还需要把carry用寄存器打一拍,否则组合输出直接驱动其他时序模块容易出时序问题,这在第 5 章会继续讨论。

3.3 最小 testbench:把两个模块一起仿真起来

实验报告里至少要有一次能自证正确的仿真。我一般会为每个模块单独写一个 testbench,时钟用always生成,复位和使能用initial控制,再用$dumpvars把信号导成 VCD 文件供波形查看。下面以计数器为例:

module tb_counter; reg clk = 0; reg rst_n = 0; reg en = 0; wire [3:0] count; counter #( .WIDTH(4), .MAX(9) ) u_dut ( .clk (clk), .rst_n (rst_n), .en (en), .count (count) ); always #5 clk = ~clk; // 10 ns 时钟周期 initial begin $dumpfile("counter_tb.vcd"); $dumpvars(0, tb_counter); #23 rst_n = 1; // 释放复位 #37 en = 1; // 使能拉高,观察 0~9 循环 #140 en = 0; // 拉低使能,输出应保持 #30 en = 1; // 恢复计数 #60 rst_n = 0; // 拉低复位,验证同步复位 #25 rst_n = 1; // 再次释放复位 #100 $finish; end always @(posedge clk) begin #1; if (en && rst_n) $display("t=%0t count=%0d", $time, count); end endmodule

测试平台里always #5 clk = ~clk生成了周期 10 ns 的时钟;#1延时让$display打印非阻塞赋值更新后的新值,避免打印到旧值。$dumpvars(0, tb_counter)里的tb_counter是顶层模块名,不是文件名,这一项写错会导致 VCD 里找不到信号,仿真排查时经常遇到。

仿真打印输出可以直接用来做“波形不好量化”时的对照,比如确认 count 是否真的按 0 到 9 循环,是否在 en 拉低期间保持。波形文件则用于查看信号间的相对时序,二者互补。testbench 里所有#延时不参与综合,只服务于验证,实验报告里不需要把它们当作设计的一部分。

4. 实验流程:从一条仿真命令到报告里的数据表

4.1 用 Icarus Verilog 跑通仿真并打开波形

如果实验环境里没有商业仿真工具,用 Icarus Verilog 加 GTKWave 是最常见的开源方案。两个工具分别负责编译仿真和查看波形。命令一般是这样:

iverilog -g2012 -o counter_tb.vvp counter.v tb_counter.v vvp counter_tb.vvp gtkwave counter_tb.vcd

第一条命令把设计文件counter.v和测试平台tb_counter.v一起编译,-g2012支持更多 Verilog 语法,-o指定输出文件。第二条命令执行仿真,运行结束后会生成counter_tb.vcd,终端里能看到$display打印的计数序列。第三条命令打开波形查看器,在 GTKWave 左侧面板找到tb_counter层级,把clkrst_nencount拖入波形窗。

仿真过程打印的信息比波形更直接,适合快速确认行为。比如模 10 计数器应当输出 0 到 9 再回到 0,一旦打印序列里出现 10,说明回绕条件写错或者 MAX 参数没有生效。波形则用来观察信号之间的边沿关系,比如复位释放后第一个上升沿 count 是否从 0 开始,en 拉低后输出是否保持。

4.2 实验报告里值得记录的时序参数

实验报告不能只贴一张波形图,还应该给出可量化的验证数据。我建议按“验证项—输入条件—期望行为—观察点”的格式整理一张表,每个验证项对应一段波形,报告里放表加波形截图,说服力比大段文字更强。

模拟 10 计数器、8 位累加器时,可以填这样一张记录表:

验证项输入条件期望行为波形检查点
模数回绕en=1,rst_n=1,MAX=9count 在 0~9 间循环count 从 9 跳变到 0 的沿
使能保持en 拉低 3 个时钟周期count 数值不变en 拉低后 count 保持平直
同步复位rst_n 拉低 1 个时钟周期下一上升沿 count=0复位释放后的第一个边沿
累加进位din=8'h01,连续累加acc 每周期加 1,加到 8'hFF 后变为 0carry 拉高且 acc 归零的同一周期

除了行为验证,量化数据也要记录:时钟周期多少 ns,回绕一次用几拍,使能无效时保持了几拍,进位信号高电平持续多长时间。这些数据写进报告,老师的追问空间会小很多,因为每一行都有明确的观测来源。

4.3 波形与预期不一致时的排查顺序

仿真波形不对时,我通常按下面的顺序排查,而不是盯着一处信号反复看。首先确认 count 是否一直是 x,如果是,多半是复位没有释放或者时钟没有翻转,检查rst_n初始值和always #5 clk = ~clk是否生效。其次看 count 是不是“每个时钟都在跳”,如果是,说明使能信号没接对,或者代码里漏写了else if (en)的保持分支。然后看回绕点,count 从 MAX 跳回 0 比预期晚了一拍,通常是比较器写成了count == MAX + 1,或者把判断放在了非阻塞赋值之后。最后看进位信号,如果carry出现在极窄的毛刺上,说明观察的是组合输出,需要回到 3.2 节的办法给进位打一拍。

排查时要把“仿真模型的期望值”和“代码行为”分开想。仿真波形是代码行为的直接结果,它不对,反映的是代码和预期不一致,而代码又是设计意图的编码。顺着“设计意图→代码→波形”这条链逐层对照,大多数问题都能定位到具体一行。最容易浪费时间的是直接怀疑仿真工具,实际上工具出错概率远低于代码。

5. 时钟、复位与赋值的边界:让计数器和累加器在实验板上稳定工作

5.1 同步复位还是异步复位:实验板上的取舍

第 3 章的计数器代码用了同步复位,复位信号只参与always @(posedge clk)里的逻辑判断。与之对应的是异步复位,复位信号出现在敏感列表里:

// 同步复位:只在时钟上升沿判断 rst_n always @(posedge clk) begin if (!rst_n) count <= 0; else if (en) begin // 递增与回绕 end end // 异步复位:rst_n 变成低电平立即清零 always @(posedge clk or negedge rst_n) begin if (!rst_n) count <= 0; else if (en) begin // 递增与回绕 end end

两种风格都能综合,差异在行为边界上。异步复位在复位信号拉低的瞬间生效,不需要等时钟,缺点是复位释放时如果刚好靠近时钟上升沿,可能不满足恢复时间,导致寄存器进入亚稳态。同步复位没有这个问题,但要求复位信号必须持续足够长,确保被时钟沿采到。

实验板上的按键复位通常是异步信号,按键按下和释放都存在抖动。我的习惯是:仿真和教学代码用同步复位,便于控制;上板验证时如果按键直接接复位,则改用异步复位风格,同时把按键经过消抖电路再接进逻辑。实验报告里把这两种风格对比写一下,能体现出对时序边界的理解。

对比项同步复位异步复位
生效时刻时钟上升沿复位信号变化沿
复位宽度要求至少覆盖一个时钟沿立即生效,释放时需满足恢复时间
综合实现当作普通数据输入通常接入触发器的专用复位端
实验板注意点按键持续时长足够释放瞬间可能产生亚稳态

5.2 阻塞赋值与非阻塞赋值的典型症状

计数器实验里最常出现的仿真“怪毛病”,是把阻塞赋值写进了时序 always 块。比如:

always @(posedge clk) begin a = a + 1'b1; c = a; end

这段代码仿真的结果是c拿到a更新后的新值,看起来“逻辑对”。但综合工具处理时序逻辑时,ac都是时钟驱动的寄存器,硬件上两个寄存器在同一个时钟沿同时采样,c应该采到a更新前的旧值。前仿真里阻塞赋值的顺序执行掩盖了这一点,后仿真或门级仿真就会暴露差异。

正确的做法是时序 always 块里统一使用非阻塞赋值:

always @(posedge clk) begin a <= a + 1'b1; c <= a; end

两条赋值语句在同一个时钟沿并行发生,c寄存器的输入是“这一个沿到来之前 a 的值”,与硬件行为一致。实验报告里写代码时,规则可以简化成一条:驱动寄存器的 always 块内全部用非阻塞赋值,组合逻辑用连续赋值assign或阻塞赋值,不要混用。

5.3 用时钟使能替代“把计数器输出当时钟”

计数器做分频时,新手很容易写出门控时钟。比如拿 4 位计数器的最高位count[3]当作另一个模块的时钟:

// 不推荐:门控时钟会引入毛刺 wire clk_div = count[3]; always @(posedge clk_div) begin cnt_slow <= cnt_slow + 1'b1; end

问题在于count[3]是组合逻辑输出,在count变化过程中可能先跳变再稳定,形成毛刺。毛刺一旦进入时钟端口,下游寄存器就可能误触发。而且门控时钟在综合时会产生额外的时钟网络,布局布线难度变大,实验板上表现不稳定。

常见做法是保留一个主时钟,把“计数器到终点”当成使能信号,而不是新时钟:

// 推荐:用计数回绕脉冲做时钟使能 wire en_slow = (count == 9); always @(posedge clk) begin if (en_slow) cnt_slow <= cnt_slow + 1'b1; end

en_slow只作为数据路径上的使能,不驱动时钟端口。这样下游逻辑和计数器共享同一个时钟,时序关系简单,也不存在毛刺问题。

5.4 累加器位宽不足时的溢出处理

累加器实验里,din 的位宽和累加结果位宽不匹配是常见现象。8 位累加器连续累加 16 个数据,结果可能超过 255,这时候有几种处理策略。第一种是截断,直接丢掉溢出位,行为简单,但数据不再单调;第二种是保留进位标志,让外部逻辑决定是否继续处理;第三种是饱和累加,累加到最大值后不再增加,适合传感器计数这类应用。

饱和累加可以在组合逻辑里加判断:

wire [WIDTH-1:0] acc_next = sum_ext[WIDTH-1:0]; always @(posedge clk) begin if (!rst_n) acc <= {WIDTH{1'b0}}; else if (clr) acc <= {WIDTH{1'b0}}; else if (ce) begin if (carry) acc <= {WIDTH{1'b1}}; // 饱和到最大值 else acc <= acc_next; end end

如果还要保留“到底溢出过没有”这个信息,建议把进位打一拍输出:

reg carry_r; always @(posedge clk) carry_r <= sum_ext[WIDTH];

打拍后的carry_r是一个标准的寄存器输出,波形干净,也方便作为下一级电路的启动信号。这样做的基本思路是:组合逻辑负责算,寄存器负责对齐时序,不让组合输出直接跨级传递。

6. 把计数器和累加器连起来用:分频、累计与车位计数

6.1 计数器输出 tick 脉冲而不是分频时钟

实验报告最后一个加分项,是把计数器、累加器组合成一个小系统。最常用的组合是“计数器回绕脉冲作为累加器使能”。比如需要一个 1 Hz 的慢速节拍,而系统时钟是 50 MHz,就让计数器数到 49 999 999 时产生一个单周期脉冲:

wire tick = (count == 49_999_999);

这个tick直接作为累加器或下一级计数器的使能信号,而不是接进时钟端口。这样的设计在板级验证时稳定,逻辑层次也清楚。

6.2 用累加器做多周期累计

累加器的典型场景是“连续 N 个周期累加外部数据”。假设每个时钟周期来一个 8 位采样值,累加 16 个周期后把结果读出,清一次零,再继续下一轮。这时用累加器配合一个计数到 15 的计数器:

wire done = (cnt == 15);

done拉高时读走acc,同时拉clr清零。用 tick 和清零事件把两个模块串起来,比在状态机里写一堆计数器要直观,也更容易仿真。

6.3 用可逆计数器实现一个 32 车位停车场计数

把计数器的加计数扩展成“可加可减”,就是出入口方向相反的车位计数器。入口传感器脉冲让占用数加 1,出口传感器脉冲让占用数减 1,再加上空满保护:

module parking_counter #( parameter MAX = 32 )( input wire clk, input wire rst_n, input wire in_pulse, input wire out_pulse, output reg [5:0] occupied, output wire full ); assign full = (occupied == MAX); always @(posedge clk) begin if (!rst_n) occupied <= 0; else if (in_pulse && !full) occupied <= occupied + 1'b1; else if (out_pulse && (occupied != 0)) occupied <= occupied - 1'b1; end endmodule

occupied位宽取 6,刚好覆盖 0 到 32 的表示范围。full是组合输出,直接接 LED 显示“车位已满”。传感器脉冲进模块前先经过两级寄存器同步到本地时钟域,避免亚稳态,这一步在实验板上接真实传感器时不能省。可逆计数和累加器累加共用同一套“使能 + 保持 + 复位”的骨架,只是方向信号从外部输入变成了加减控制。

本文还有配套的精品资源,点击获取

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

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

立即咨询