免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Verilog计数器与累加器实战:时序逻辑状态保持与仿真验证

Verilog计数器与累加器实战:时序逻辑状态保持与仿真验证 简介数字系统设计课程的计数器与累加器实验在这份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中间不会出现 MAX1 的值。2.3 累加器反馈回环和进位标志的处理累加器把外部输入 din 和寄存器当前值相加结果写回寄存器。如果把 din 固定成常数 1累加器就退化成计数器。因此从结构上说累加器是计数器的超集计数器是累加器在“输入恒为 1”时的一个特例。实验报告里如果能写出这一句说明不是简单抄代码而是真的理解了反馈路径。累加器比计数器多一个必须回答的问题溢出怎么办。WIDTH 位加 WIDTH 位结果可能需要 WIDTH1 位。比如 8 位累加器里8hFF 8h01 9h100低 8 位变成 0第 9 位是进位。硬件上常见做法是保留低 WIDTH 位同时把最高位提取成进位标志 carry。提取进位不能靠“结果变小”这种办法判断最稳妥的是在做加法前把两个操作数各扩展一位wire [WIDTH:0] sum_ext {1b0, acc} {1b0, din}; assign carry sum_ext[WIDTH];{1b0, acc}把 acc 扩成 WIDTH1 位加法结果自然保留进位。这种写法比“加完再比较大小”综合出来的电路更直接也方便后面对进位打一拍寄存。累加器还牵扯到控制信号的优先级。实验板上的累加器通常有清零和累加使能两个控制端必须明确“同时有效时谁说了算”。常见做法是清零优先只要 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{1b0}}; end else if (en) begin if (count MAX) count {WIDTH{1b0}}; else count count 1b1; end // en 为低时没有任何分支生效count 保持原值 end endmodule这段代码里值得注意的有三点。第一rst_n写在always (posedge clk)内是同步复位只在时钟上升沿生效这一点在后面的实验板验证部分会展开对比。第二count MAX是回绕判断MAX9 时从 9 回到 0实现模 10 计数。第三en无效时没有 else 分支寄存器自己保持这里不会产生锁存器因为 always 块由时钟驱动寄存器天然具有保持能力。参数的选择直接影响电路规模和计数范围实验报告里可以把参数表列出来参数含义典型取值备注WIDTH计数器寄存器位宽4、8、16最大可表示值为 2^WIDTH - 1MAX计数上限9、59、99模数 MAX 1MAX 不能超过 2^WIDTH - 1en计数使能1 或 0高电平加 1低电平保持rst_n同步复位低电平有效优先级最高任意时刻拉低下一拍归零3.2 累加器清零、使能与进位输出的实现累加器的写法与计数器类似差别只在数据通路加法器的另一端从常数1b1换成外部输入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 {1b0, acc} {1b0, din}; assign carry sum_ext[WIDTH]; always (posedge clk) begin if (!rst_n) acc {WIDTH{1b0}}; else if (clr) acc {WIDTH{1b0}}; 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层级把clk、rst_n、en、count拖入波形窗。仿真过程打印的信息比波形更直接适合快速确认行为。比如模 10 计数器应当输出 0 到 9 再回到 0一旦打印序列里出现 10说明回绕条件写错或者 MAX 参数没有生效。波形则用来观察信号之间的边沿关系比如复位释放后第一个上升沿 count 是否从 0 开始en 拉低后输出是否保持。4.2 实验报告里值得记录的时序参数实验报告不能只贴一张波形图还应该给出可量化的验证数据。我建议按“验证项—输入条件—期望行为—观察点”的格式整理一张表每个验证项对应一段波形报告里放表加波形截图说服力比大段文字更强。模拟 10 计数器、8 位累加器时可以填这样一张记录表验证项输入条件期望行为波形检查点模数回绕en1rst_n1MAX9count 在 0~9 间循环count 从 9 跳变到 0 的沿使能保持en 拉低 3 个时钟周期count 数值不变en 拉低后 count 保持平直同步复位rst_n 拉低 1 个时钟周期下一上升沿 count0复位释放后的第一个边沿累加进位din8h01连续累加acc 每周期加 1加到 8hFF 后变为 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 1b1; c a; end这段代码仿真的结果是c拿到a更新后的新值看起来“逻辑对”。但综合工具处理时序逻辑时a和c都是时钟驱动的寄存器硬件上两个寄存器在同一个时钟沿同时采样c应该采到a更新前的旧值。前仿真里阻塞赋值的顺序执行掩盖了这一点后仿真或门级仿真就会暴露差异。正确的做法是时序 always 块里统一使用非阻塞赋值always (posedge clk) begin a a 1b1; 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 1b1; end问题在于count[3]是组合逻辑输出在count变化过程中可能先跳变再稳定形成毛刺。毛刺一旦进入时钟端口下游寄存器就可能误触发。而且门控时钟在综合时会产生额外的时钟网络布局布线难度变大实验板上表现不稳定。常见做法是保留一个主时钟把“计数器到终点”当成使能信号而不是新时钟// 推荐用计数回绕脉冲做时钟使能 wire en_slow (count 9); always (posedge clk) begin if (en_slow) cnt_slow cnt_slow 1b1; enden_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{1b0}}; else if (clr) acc {WIDTH{1b0}}; else if (ce) begin if (carry) acc {WIDTH{1b1}}; // 饱和到最大值 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 1b1; else if (out_pulse (occupied ! 0)) occupied occupied - 1b1; end endmoduleoccupied位宽取 6刚好覆盖 0 到 32 的表示范围。full是组合输出直接接 LED 显示“车位已满”。传感器脉冲进模块前先经过两级寄存器同步到本地时钟域避免亚稳态这一步在实验板上接真实传感器时不能省。可逆计数和累加器累加共用同一套“使能 保持 复位”的骨架只是方向信号从外部输入变成了加减控制。本文还有配套的精品资源点击获取
返回列表