多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Counter 1-12 Verilog实现:同步复位与回绕条件深度解析

Counter 1-12 Verilog实现:同步复位与回绕条件深度解析 最近在HDLbits的Counter部分刷题做到Exam/ece241 2014 q7a这道题时我愣了一下题目要求实现的计数器居然不是从0开始而是从1数到12再绕回1。这道题在题库里名叫“Counter 1-12”题源是2014年ECE241的考试题看起来只是一个模12计数器的小练习实际上把同步复位、使能控制、回绕条件这三个时序电路的基本功全部串了一遍。适合刚学完Verilog时序逻辑、想在HDLbits上找题巩固理解的同学也适合准备数字逻辑考试前拿来做限时训练的人。题目本身的代码量不大但坑并不少尤其对习惯写“复位到0、数到最大值回绕”这类常规计数器的人来说第一次做很容易出错。这篇博文就把这道题从读题、设计、写码到仿真排错的过程完整拆开讲清楚。1. 题目解读Counter 1-12到底在考什么1.1 从题面拆出三个关键行为先别急着打开编辑器写代码把题面安静读两遍。这道题的核心行为可以拆成三句话计数区间是1到12不是0到11也不是0到15。复位信号reset有效时计数器的当前值q回到1而不是回到0。使能信号en为高时每个时钟上升沿计数值加1en为低时计数器保持当前值。三句话看起来都很普通组合在一起就变味了。常规计数器通常是0到N循环复位到0使能控制是否递增这类写法很多教材里都有模板。而这道题把起始值、回绕值、复位置全部换成了“1和12”这对组合正好打在教学模板的盲区上。如果只是机械地背过“always (posedge clk) if (reset) q 0; else q q 1;”这种写法拿到这道题会立刻翻车因为复位后的第一个值必须是1并且计数到12的下一拍要回到1而不是变成13。另外还要注意q的位宽是4位但计数值最大只用到12。4位二进制可以表示0到15共16个值这道题只使用其中12个合法值剩下0、13、14、15属于合法状态之外的空闲状态。这也是题目里往往会带一个valid输出的原因它用来指示当前q是否处于1到12的有效区间相当于一个状态合法性指示信号。这一点在后面设计和调试时很有用。1.2 为什么复位值是1而不是0这道题最大的坑我在第一次写这道题的时候特别自信地把复位分支写成q 4d0仿真一看波形q确实从0开始一路加到14、15完全没有按题目要求跑。检查了半天才发现问题不在使能逻辑不在回绕逻辑就是复位值写错了。这个问题看着小实际上很能反映一个人有没有真正把“状态机的初始状态”这个概念想清楚。计数器本质上是一个状态机每个计数值就是一个状态。复位信号的作用是让状态机进入一个确定且正确的初始状态。对于这道题合法状态的集合是1到12所以复位后必须落在1上才能保证后续每个时钟沿都在合法状态集合内移动。如果复位成0那0本身不是合法状态计数器相当于从非法状态启动虽然跑到12之后也能绕回1但波形输出就和题目的要求不一致了这在自动评测时直接判错。从工程角度理解这件事也很自然。很多真实系统中的计数器并不从0开始计数。比如通信协议里的序列号、帧序号经常从1开始递增再比如有些定时器需要装入一个非零初值后开始倒数。所以HDLbits这道题并不是单纯刁难人它是在用一道小练习题提醒你复位的目标值由系统状态定义决定不是统一写成0就完事。1.3 valid输出到底在指示什么我在HDLbits上看到的题目版本里除了输出q之外还要求输出一个valid信号。这个信号的含义是当q处于1到12这个合法区间内时valid为1否则为0。写成Verilog就是一句话assign valid (q 4d1) (q 4d12);刚拿到这个信号时我有点疑惑因为我们的计数器正常工作的情况下q永远不会落到0、13、14、15这些非法值上那valid岂不是永远为1这个信号看起来完全没有意义。后来我想明白valid信号的价值恰恰体现在“异常”场景中。如果复位逻辑写错、初值不对或者计数器受到外部干扰进入非法状态valid会立刻变低这时候设计者就能快速发现问题。它相当于一个状态健康指示灯也是FSM设计中“合法状态判断”的一个简化版本。在实际的工程代码中这种valid信号非常常见。比如一个模块只在特定数值范围内输出有效数据就会用一个valid标志来告诉后级“当前数据可以用你拿去处理吧”。所以别因为这道题里valid恒为1就觉得它多余把它当作一道组合逻辑练习来理解就可以了。2. 实现思路想清楚状态转移再动手写代码2.1 三种常见写法对比实现这个计数器的方法不止一种我在做的时候把几种常见思路都过了一遍这里做个对比。写法核心思路优点缺点时序块内if-else直接在时钟沿触发的always里判断复位、使能、回绕代码直观适合小规模计数器业务逻辑和寄存器混在一起复杂时不易扩展组合逻辑next_q 时序寄存器用always (*)计算下一个状态再用时序块寄存状态转移清晰适合逻辑变复杂时维护多了一段组合逻辑代码初学者觉得绕case枚举全状态把所有状态用case列出状态转移完全可见16个状态写起来太啰嗦工程上不推荐第一种写法最简单适合这道题这种状态少、逻辑短的场景。代码量小一眼能看明白优先级关系综合出来的电路也不差。第二种写法更接近规范化的状态机写法把“下一个状态是什么”和“状态什么时候更新”分离开在后续做复杂控制逻辑时优势会越来越大。第三种写在8状态、16状态的状态机里可以考虑对计数器来说没必要。我最后提交的版本用的就是第一种因为这道题的重点是理解回绕和复位代码越直接越好。如果你是在做工程项目而不是刷题建议养成第二种写法的习惯后面接复杂逻辑时会舒服很多。2.2 优先级关系复位大于使能使能大于计数写这段代码时最需要想清楚的是三个控制信号的优先级reset、en、回绕判断。在同一个always (posedge clk)块里if-else if-else的顺序就是综合后的优先级顺序。always (posedge clk) begin if (reset) begin q 4d1; end else if (en) begin if (q 4d12) begin q 4d1; end else begin q q 1b1; end end end把reset放在最前面是必须的复位信号不优先其他逻辑全是空谈。en作为第二级保证只有使能有效时才发生状态转移。回绕判断放在en内部表示“如果当前已经数到12下一个值就回到1如果还没数到12正常加1”。这个顺序一旦写反比如把en放在最外层再把reset放内层仿真时很容易出现复位不生效或者使能无效的诡异问题。还有一点值得注意当en为0时这段代码没有任何分支会执行q会保持原来的值。这是时序逻辑寄存器的天然特性不是Bug。有的同学会担心“不写else是不是会产生锁存器”在组合逻辑中确实存在这种风险但在时序逻辑中寄存器的默认行为就是保持所以不需要额外写一个else q q。这一点初学者经常混淆我在这里顺便说明一下。2.3 非法状态到底要不要处理前面提到4位计数器一共有16个状态这道题只用1到12剩下的0、13、14、15都是非法状态。那么在写回绕条件时用q 4d12和q 4d12到底有什么区别表面上看正常工作时没有区别因为q根本不会等于13、14、15。但如果系统受到干扰或者复位初值不对计数器落在13怎么办用q 4d12的话13、14、15都不会触发回绕计数器会一路加到15然后进位溢出变成0再从0加到1绕一大圈才回到合法状态。而用q 4d1213、14、15会立即触发回绕到1恢复速度就快得多。所以我个人建议代码里写q 4d12而不是q 4d12。虽然这个区别在简单的仿真中完全看不出来但在真实硬件环境中一个能快速从异常状态恢复的电路比只能靠运气恢复的电路好太多。这也算是一个从“能跑”到“可靠”的小进步。如果还想更严谨可以在!valid时也做一次复位处理让任何非法状态下一拍直接回到1。但这样会把计数器变成“带自恢复机制的安全计数器”代码量会增加。刷题阶段我不建议过度设计理解了这个思路就够了。3. RTL代码与仿真从能跑到能用的完整过程3.1 可综合RTL代码逐行解读下面是我最终提交的完整代码在HDLbits上可以直接通过评测module top_module ( input clk, input reset, input en, output reg [3:0] q, output valid ); always (posedge clk) begin if (reset) begin q 4d1; end else if (en) begin if (q 4d12) begin q 4d1; end else begin q q 1b1; end end end assign valid (q 4d1) (q 4d12); endmodule逐行解释一下关键点output reg [3:0] qq必须在always块里赋值所以声明为reg类型。虽然名字叫reg但它综合出来是寄存器不用觉得奇怪。always (posedge clk)敏感列表只有clk没有reset这说明reset是同步复位只在时钟上升沿检查reset是否有效。这也是HDLbits里计数器题目的常见约定。复位分支q 4d1注意常量写法位宽4值1。如果只写1综合器也能推断正确但写成4d1更规范别人读代码时也不会误解。使能分支里的回绕判断采用q 4d12前面已解释过理由。非阻塞赋值时序逻辑必须使用非阻塞赋值避免出现综合后仿真不一致的问题。assign valid组合逻辑输出用连续赋值语句实现。3.2 testbench怎么写才不算糊弄写完RTL只完成了一半仿真验证同样重要。很多同学刷HDLbits直接点“Submit”不怎么自己写testbench我建议还是在本地把关键场景跑一遍。这样既能验证功能也能加深对时序的理解。一个覆盖常用场景的testbench如下module tb_top_module; reg clk; reg reset; reg en; wire [3:0] q; wire valid; top_module dut ( .clk(clk), .reset(reset), .en(en), .q(q), .valid(valid) ); initial clk 0; always #5 clk ~clk; initial begin // 复位状态 reset 1; en 0; #10; // 释放复位使能计数观察从1到12循环 reset 0; en 1; #240; // 使能拉低计数器应保持 en 0; #20; // 重新使能继续计两轮 en 1; #240; // 运行中再复位一次q应回到1 reset 1; #10; reset 0; #20; $finish; end endmodule时钟周期是10ns#240表示24个时钟周期。复位释放后q会从1开始依照1、2、3到12的顺序循环。#240足够观察两轮多。使能拉低20nsq会保持两个时钟周期不变再恢复使能时继续递增。最后在运行中拉高resetq会在下一个时钟沿回到1这一步专门验证同步复位的时序行为。如果你用的是Vivado或者ModelSim直接跑行为仿真观察波形即可。如果想在命令行快速看结果也可以在initial块里加$monitor打印关键信号但刷题场景下我更推荐直接看波形波形能直观反映每个时钟沿的变化不遗漏边界情况。3.3 仿真结果里盯着这五个点看跑完仿真后别急着关掉波形界面按这五个点逐项检查q的序列必须是1、2、3……12、1、2……不会出现0也不会出现13。valid在整个正常计数过程中应为1。如果你看到valid有低电平说明计数器进入了非法状态代码有问题。使能en拉低期间q必须保持不变不能继续递增。运行中reset拉高后q在下一个时钟上升沿变成1而不是立刻变化这是同步复位的标志。全波形中不能出现x态或者z态尤其上电初始阶段必须能看到复位后q稳定为1。前四点覆盖功能正确性第五点排查初始化问题。如果仿真开始后q一直显示为x基本就是testbench没有给复位信号或者复位只给了一个很短的脉冲被漏采了。因为复位是同步的必须保证复位信号在时钟上升沿到来时依然有效才能被正确采到。4. 常见问题与排错实战4.1 把容易踩的坑整理成速查表做题过程中我把自己和身边人常犯的错误整理成了一张表每次报错不知道哪里出问题时按表排查效率很高。错误现象可能原因排查方法修正办法q跑出13、14、15回绕条件写错或未判断检查q12分支是否存在使用q4d12作为回绕条件q从0开始计数复位值写成了0查看复位分支代码改成q 4d1en无效时q仍在变化en没有参与逻辑检查en条件位置在else if分支处理enreset无效或效果不对reset不是同步复位检查敏感列表和复位分支确保不使用always (posedge clk or posedge reset)valid始终为xvalid用wire类型但未赋值检查assign语句确认valid赋值存在仿真输出全是x没有初始化复位或testbench时序错误观察复位信号和时钟沿对齐情况拉长复位时间确保沿上采到功能仿真正确但上板异常异步逻辑、时钟域问题或缺少约束检查是否有跨时钟域信号刷题阶段一般不考虑工程上需要同步处理这张表不是我凭空想的每一行都是实际会遇到的典型问题。尤其是第一行“回绕条件写错”如果把条件写成q 4d15那q到12之后继续加到15才回绕完全偏离题目要求。4.2 同步复位和异步复位怎么区分很多初学者在写计数器时分不清同步复位和异步复位的代码区别这里多说一句。同步复位代码的always块敏感列表只写时钟always (posedge clk) begin if (reset) q 4d1; end异步复位代码的敏感列表同时写时钟和复位always (posedge clk or posedge reset) begin if (reset) q 4d1; else if (en) ... end区别在于同步复位只在时钟上升沿检查reset异步复位则是reset一变高就立刻复位不等待时钟。HDLbits的计数器题目基本上都是同步复位因为题目描述里的reset通常是作为一个普通输入信号不特别说明为异步。这道题我用的写法是同步复位符合题目约定。从工程角度看异步复位容易产生复位释放时刻和时钟沿对齐的问题处理不好会出亚稳态。所以现代设计中常见做法是“异步复位同步释放”这里不展开但至少你在做题时要能分清两种写法的差异不要在一道同步复位的题目里写出异步复位的代码。4.3 为什么q 1b1不会出现进位毛刺还有一个容易被问到的细节q是4位q 1b1在二进制中可能会出现从15进位到0的情况这在电路上会不会有问题正常工作时q永远到不了15因为到12的下一拍就回绕成1了。所以q 1b1这个加法器的输入最高只会到11输出最高到12不会产生从15溢出到0的跳变。即使外部干扰让q变成13、14、15因为我们用q 4d12判断也会立刻回绕到1不会让加法器计算出16然后溢出。如果你还是不放心可以用组合逻辑next_q单独计算把回绕逻辑看得更清楚reg [3:0] next_q; always (*) begin if (reset) next_q 4d1; else if (en) begin if (q 4d12) next_q 4d1; else next_q q 1b1; end else next_q q; end always (posedge clk) q next_q;这种写法的好处是“下一个状态”和“当前状态”完全分离debug时直接看next_q的值就能判断逻辑是否合理。对于这道题用第一种写法已经够用但从学习角度我建议你两种都写一遍体会一下组合逻辑和时序逻辑分离的感觉。5. 从这道题能带走什么计数器不只是数数5.1 计数器在工程里比想象中更常见很多同学刷计数器题时会觉得枯燥心想“我以后又不做计数器刷这些干嘛”。实际上计数器是现代数字系统里最基础、最常用的模块之一只是它以各种形态藏在代码深处。举个例子你要在FPGA里产生一个1kHz的方波系统时钟是50MHz那就需要一个模25000的计数器来分频。又比如你要实现一个UART的波特率发生器本质上也是按照计数值触发分频输出。再比如你要处理一个总线burst传输总共要传8个数据那就要有一个计数器在每个数据结束后递增数到8之后产生一个完成信号并清零。这类逻辑随处可见而且出现频率非常高。这道题的特殊之处在于它不是简单的0到N计数而是1到12的循环计数并且带有同步复位和使能控制。这三个要素组合起来实际上是很多协议控制逻辑的缩影使能决定什么时候动作复位决定什么时候回到初始状态回绕条件决定什么时候完成一个周期。掌握这个模式之后很多看起来复杂的状态机拆开看也就是“计数”加“判断边界”的组合。5.2 这类题目的常见变体和应对策略HDLbits上还有不少和Counter 1-12类似的题目比如从1数到60、带up/down方向控制、支持load装载初值等。这些题目万变不离其宗核心都是在回答三个问题初始值是多少由什么信号决定什么时候加1、什么时候减1由什么信号控制什么时候回绕、回绕到哪里由什么条件判断只要抓住这三个问题几乎所有的计数器题都能顺畅解出来。我自己的做题习惯是先在草稿纸上画出状态转移图哪怕只画一个简短的序列比如“1 - 2 - 3 - ... - 12 - 1”再在图上标出reset、en分别在什么位置起作用。画完之后写代码就变成看图填空出错概率会小很多。还有一个应对考试的小技巧写完代码先别submit自己在心里跑一遍边界情况。先看复位分支再看使能分支最后看回绕分支三步走完基本就能确认逻辑没有大问题。这种做法同样适用于面试手撕代码的场景花一分钟检查边界比提交后才发现错误要划算得多。5.3 说说我做这道题的真实感受最后分享一点个人体会。我在开始学Verilog的时候也有过“刷题就是背代码”的阶段看到类似的计数器题目直接套模板。直到做了这道Counter 1-12被复位值为1这个细节坑过一次才真正意识到读题比写码更重要。题目里的每个信号都不是摆设reset给的是什么值en控制什么行为输出定义是什么范围这些细节直接决定代码怎么写。从那以后我养成了一个习惯拿到任何时序题目先把输入输出和状态转移用自然语言写一遍再落代码。这样看起来多花了一两分钟实际上能省下后面一小时的调试时间。如果你在做这道题的时候也遇到了问题建议先回到题目描述重新读一遍把“1到12”“复位到1”“使能控制”这三个关键词标注出来然后对着自己的代码逐行检查。大多数错误都能在这个过程中自己暴露出来。等你能完全独立写出这20行代码再回头看看过往踩过的坑会发现它们都变成了最扎实的经验。
返回列表