☰
Verilog期末题拆解:可综合原则、分频打拍与FPGA上板验证
2026/10/26 14:52:13 网站建设 项目流程

简介:面向高校数字电路与FPGA课程学习者,这是一份Verilog期末测试题及答案文档,适合用于考前自测、课堂复习与代码查漏补缺。内容按题型组织,系统设计部分覆盖二输入与门、4位全加器、带异步复位的D触发器、乘法函数、四位加法计数器、阶乘任务、偶分频电路及4微秒延时模块等八道典型题,并给出可对照的Verilog参考代码;后续还包含填空、英文缩写释义与程序设计等题型,涉及Quartus工程、Modelsim仿真、negedge、wire、LUT、CPLD/FPGA实现方式等知识点。压缩包共1个docx文件,约118KB,便于直接查阅和打印。已有3383人学习,可作为课程复习提纲和实验前的代码核对材料,帮助读者理解always块、复位与置数优先级、分频计数和延时移位等写法,并据此整理自己的答题思路。

1. 一份只有题和答案的 docx,为什么值得反复翻

很多人复习 Verilog 的姿势是翻教材目录,觉得自己全会,结果真坐到电脑前写一个带异步复位的计数器,always 块的敏感列表漏了 reset,波形跑出来复位压根没生效。这份 Verilog 期末测试题及答案,本质上是一套 8 道补全程序题加填空、缩写、简答、程序设计的小型题集,知识点从二输入与门一路排到偶分频器和延时打拍,旁边还挂着 FPGA 生态的常识问答。它适合两类人:期末要过关的学生,和刚转 FPGA、想用一套最小题集把语法和建模习惯过一遍的开发者。答案不是终点,把每道题跑通、波形对上,才算真的会。

2. 卷面结构拆解:五类题型对应的 Verilog 能力地图

这份 docx 不是按章节组织的教材,而是一张卷子:一、系统设计 8 道补全程序题;二、填空题 11 道;三、英文缩写 7 个;四、程序设计 2 道;五、填空题 18 道;六、简答题 4 道;后面还附了程序设计题的 5 份完整实现。题型看着杂,把答案拆开看,其实能拼出一条从门电路到分频器的完整上升路径。搞清这条路径,比死记答案有用得多,因为面试和实际项目里问的永远是「你为什么这么写」,不是「答案是什么」。

2.1 八道补全题构成的难度台阶

这 8 道题不是随机抽的,每一道对应一个建模手法,难度是递增的:

题号模块名核心考点最常见的错处
1my_and数据流建模 assign端口列表分隔符写错、以为输出必须声明 reg
2my_add位宽拼接与进位只写 sum 不拼 c_out,进位被截断
3my_D异步复位时序模板敏感列表漏 negedge reset、用阻塞赋值
4mulfunction 定义在 function 里写延时或时序控制
5my_counter复位/置数/计数优先级敏感列表混入 load,同步置数变成异步
6factorialtask 定义位宽溢出、把 task 当 function 用
7fp_even计数器加分频比较值算错、cnt 位宽不够
8my_delay移位寄存器打拍用 # 延时代替寄存器链

第 1、2 题属于组合逻辑,靠 assign 一条语句搞定;第 3、5、7、8 题是时序逻辑,全部落在 always 块加非阻塞赋值这套模板里;第 4、6 题考的是过程建模的两种封装形式。也就是说,把这 8 道题写熟,Verilog 的可综合子集基本就覆盖了,剩下的只是工程经验问题。

2.2 填空与缩写题:FPGA 生态里必须背下来的那一层

填空和缩写看着是纯背诵,实际上全是上机时会直接撞上的东西。把两批填空题的考点归拢一下,大概是这样几类:

类别需要记住的点
工程与文件工程文件后缀 .qpf,Verilog 源文件 .v,CPLD 早期工具与主流 IDE 的区别
关键字上升沿 posedge,下降沿 negedge,双向端口 inout
数据类型assign 左值必须是 wire,reg 默认 1 位宽,输出要存值必须显式声明 reg
位宽与拼接拼接运算符 {} 的用法,X 表示不定值,Z 表示高阻
器件常识FPGA 主流实现方式分 SRAM、反熔丝、FLASH 三类,CPLD 基于乘积项,FPGA 基本单元含 4 输入查找表
建模习惯时序逻辑优先用非阻塞赋值,case 语句一般综合成多路选择器

缩写那 7 个(EDA、SoPC、FPGA、RTL、HDL、CPLD、ASIC)建议连英文全称一起记。RTL 是 Register Transfer Level,寄存器传输级,这个词在后面看综合报告、看网表的时候天天出现,只记住中文翻译没什么用。

记忆方法上,我一般不建议按卷面顺序背,而是按「建工程 → 写代码 → 跑仿真 → 看综合」这条动线串起来记。比如 .qpf 和 .v 的关系,你只要亲手建过一次工程就不会忘;posedge 和 negedge,只要写过一次带复位的触发器就刻进去了。纯背下来的东西一周就忘,动过手的能留很久。

2.3 简答题里的 14 条可综合原则,是全卷最值钱的部分

卷子最后一道简答题列了 14 条建立可综合模型的原则,这部分比前面所有代码题都值钱,因为它决定了你写出来的东西能不能变成电路。挑几条最容易被忽略的展开说。

第一条「不使用 initial」。仿真里 initial 块用来给激励、给初值,但综合工具对它的处理是直接忽略,或者只在部分器件上支持上电初值。你要是靠 initial 给寄存器赋初值,仿真跑得通,上板之后寄存器是随机态,这就是典型的仿真与实物不一致。

第二条「不使用 #10 这类延时」。延时语句只描述仿真时间,不描述硬件。综合时被无条件丢弃,你以为自己写了 10ns 延迟,实际电路里什么都没有。要延迟就用时钟打拍,这也是第 8 题延时模块的用意。

第三条「不使用循环次数不确定的循环」。for 循环在综合时是被展开的,次数必须是个常量;forever 和 while 展不开,综合工具直接报错。想写状态机就一直循环可以用 always,别用 forever。

第七条「always 描述组合逻辑时,敏感信号列表要列全所有输入」。这条是老生常谈,但翻车率极高。漏掉一个输入信号,仿真时那个信号变化不会触发块执行,波形看起来「卡住」;综合工具则会据此推断出锁存器(latch),因为它在某些输入组合下需要保持原值。锁存器在 FPGA 里是能不用就不用的东西,时序分析麻烦,还容易引入毛刺。

第九条「时序逻辑尽量用非阻塞赋值」。这条要配合第十条一起看:一个变量不能在一个以上的 always 块里赋值。两个 always 块同时用阻塞赋值改同一个变量,仿真器按代码顺序执行,不同的仿真器、不同的优化等级可能给出不同结果,这种 bug 查起来非常难受。

第十四条「避免在 case 分支里用 x 或 z」。x 是不定态,z 是高阻,它们在仿真里有意义,在综合时会被当成 don't care 处理,综合出来的电路和你写的分支对不上。写 case 就老老实实写 0 和 1,再补一个 default 兜底。

这 14 条不用一次背完,建议写成一张便签贴在显示器边上,每写完一个模块对一遍,两三周就成肌肉记忆了。

3. 组合与时序基础题:与门、全加器、D 触发器逐题拆

卷子第一部分是补全程序,题干里留了编号的空,让你填代码。这一批题的题干本身有几处印刷错误,比如把 Verilog 拼成 Venilog、把 assign 拼成 assgin、把 input 拼成 imput、端口列表用点号分隔。真上机的时候照着抄一定编译不过,所以下面给的代码都是修正后的版本,顺便把每道题的判定逻辑讲清楚。

3.1 二输入与门与 4 位全加器:位宽拼接才是真考点

第 1 题最简单,但简单题里藏着两个新手常犯的错。修正后的写法:

module my_and( input a, input b, output c ); assign c = a & b; // 数据流建模,组合逻辑,无时钟 endmodule

逻辑说明:assign是连续赋值,右值任何一个操作数变化,左值立刻跟着变,等价于一根导线加一个与门。参数说明:a、b是 1 位输入,c是 1 位输出,因为用 assign 驱动,c默认就是 wire 类型,不需要也不允许再声明成 reg。

新手第一个错是把输出声明成output reg c,然后接 assign,编译直接报「不能对 reg 做连续赋值」。第二个错是端口列表写成my_and(a.b,c),点号是印刷错误,实际必须用逗号,端口之间用点号在例化时才出现(.a(a)这种命名连接)。

第 2 题的全加器,真正考的是位宽:

module my_add( input [3:0] a, input [3:0] b, input c_in, output [3:0] sum, output c_out ); // 拼接后左值 5 位,加法结果的进位不会被丢掉 assign {c_out, sum} = a + b + c_in; endmodule

逻辑说明:{c_out, sum}是把两个信号拼成一个 5 位向量,高位是进位,低 4 位是和。左边 5 位、右边三个操作数相加,Verilog 会按左值位宽对结果做截断或扩展,进位就保住了。参数说明:如果写成assign sum = a + b + c_in;,左值只有 4 位,第 5 位的进位直接被丢弃,c_out永远是 0,仿真一看就知道不对,但很多人第一版就是这么写的。

再补一句位宽规则:两个 4 位数相加,最大是 4'b1111 + 4'b1111 + 1 = 31,需要 5 位才能完整表示。凡是做加法、累加、计数器,位宽都要多留一位,这是血泪经验,溢出问题在波形上表现为「数到某个值突然回到 0」,特别容易误判成复位误触发。

3.2 异步复位 D 触发器:一套要背下来的模板

第 3 题要求带异步复位、低电平有效、时钟上升沿触发:

module my_D( input d, input clk, input reset, output reg q // 在 always 里赋值,必须是 reg ); // 时钟上升沿 或 复位下降沿 都会触发 always @(posedge clk or negedge reset) begin if (!reset) q <= 1'b0; // 异步复位优先,不等待时钟 else q <= d; // 正常采样 end endmodule

逻辑说明:敏感列表里同时写了posedge clk和negedge reset,意味着复位信号一拉低,块立刻执行,不等时钟沿,这就是「异步」。复位分支必须写在最前面,if-else 的优先级从上往下,写在后面就被时钟分支挡住了。参数说明:q要保存数值,必须声明成reg;<=是非阻塞赋值,时序逻辑的标准写法。

对照一下同步复位的版本,差别只在敏感列表:

// 同步复位:敏感列表只有时钟,复位要靠时钟沿才能生效 always @(posedge clk) begin if (!reset) q <= 1'b0; else q <= d; end

同步复位的好处是复位信号不占用触发器的异步复位端,时序分析简单,抗毛刺能力强;坏处是复位必须比时钟宽,时钟停了复位就不起作用。异步复位的好处是时钟没起来也能复位,坏处是复位释放的时刻如果不跟着时钟走,容易产生亚稳态。工程上的常见做法是异步复位、同步释放,用一个两级触发器把复位信号同步到时钟域,这一段在卷子里没考,但做项目绕不开。

3.3 计数器题:优先级和敏感列表的矛盾

第 5 题要求异步复位高有效、同步置数低有效、时钟上升沿触发,题干给的答案有一处硬伤:

// 题干版:敏感列表里混进了 negedge load always @(posedge clk or negedge load or posedge clr) begin if (clr) qout <= 4'b0; else if (load == 1'b0) qout <= data; else qout <= qout + 1'b1; end

问题在于题目明说「同步置数」,同步的意思是只在时钟沿采样,可敏感列表里写了negedge load,等于让 load 的下降沿也能直接触发块执行,这就变成异步了。更麻烦的是 load 是低电平有效,如果它一直保持低,块会反复进入置数分支,实际行为取决于仿真器的调度,波形很难解释。修正版应该只留时钟和复位:

module my_counter( input clk, input clr, // 异步复位,高有效 input load, // 同步置数,低有效 input [3:0] data, output reg [3:0] qout ); always @(posedge clk or posedge clr) begin if (clr) qout <= 4'b0000; // 优先级最高 else if (load == 1'b0) qout <= data; // 次高,只在时钟沿生效 else qout <= qout + 1'b1; // 4 位自然回绕,15 之后回 0 end endmodule

逻辑说明:clr高有效异步复位,所以敏感列表用posedge clr,分支判断用if (clr);load低有效但同步,所以不进敏感列表,判断写成load == 1'b0。参数说明:qout是 4 位,加到 15 再 +1 会自动回绕到 0,不需要额外写清零逻辑,这是二进制计数器的天然特性。

顺带说一个易错点:qout + 1'b1里如果写qout + 1,1默认是 32 位整数,综合工具会推断出 32 位加法器再截断,虽然功能对,但资源浪费,还可能带出位宽警告。养成写1'b1的习惯。

3.4 3-8 译码器与奇偶校验:case 和归约运算

程序设计题里的 3-8 译码器和奇偶校验生成电路,是卷子里少见的完整实现题:

module yimaqi3_8( input [2:0] a, output reg [7:0] out // case 在 always 里赋值,必须 reg ); always @(*) begin // 组合逻辑,用 * 自动列全敏感信号 case (a) 3'b000: out = 8'b0000_0001; 3'b001: out = 8'b0000_0010; 3'b010: out = 8'b0000_0100; 3'b011: out = 8'b0000_1000; 3'b100: out = 8'b0001_0000; 3'b101: out = 8'b0010_0000; 3'b110: out = 8'b0100_0000; 3'b111: out = 8'b1000_0000; default: out = 8'b0000_0000; // 兜底,防止综合出锁存器 endcase end endmodule

逻辑说明:case 语句在综合时一般被映射成多路选择器,8 个分支就是 8 选 1。参数说明:敏感列表写@(*)而不是@(a),前者由工具自动推导,后者靠人肉维护,加一个输入忘了改就会漏。另外 3 位输入理论上只有 8 种取值,default 看似多余,但它是防止综合出锁存器的保险,建议一律写上。

奇偶校验那道题一行就够:

module parity( output even, output odd, input [7:0] bus ); assign even = ^bus; // 归约异或:8 位里 1 的个数为偶数时结果为 1 assign odd = ~(^bus); // 取反即奇校验位 endmodule

逻辑说明:^bus是归约异或运算符,把向量的每一位依次异或起来,等价于逐位判断奇偶,综合出来是一棵异或树,延迟低。参数说明:归约运算只能用在向量上,^单目形式和双目异或a ^ b是两回事,别混。

4. function、task 与分频延时:过程建模的四道硬题

这一批题考的是 Verilog 里两个容易被当成「函数」用的东西:function 和 task。它们长得像软件里的函数,行为却受硬件建模约束,边界搞不清就会写出仿真通过、综合报错、或者干脆不可综合的代码。

4.1 function 与 task 的边界:能不能带时序

先看 function,题目要求定义一个 8 位输出的函数,把两个 4 位数相乘:

function [7:0] mul; // 返回值位宽 8 位,函数名即返回值 input [3:0] data1; input [3:0] data2; begin mul = data1 * data2; // 组合逻辑乘法 end endfunction

逻辑说明:function 内部是纯组合逻辑,不能有#、@、wait这类时序控制,不能有 output 或 inout 端口,至少要有 1 个输入,返回值通过函数名本身传出。参数说明:两个 4 位数相乘最大 15×15=225,8 位最大 255,刚好装得下,位宽再小就会溢出。function 可以直接在 assign 里调用,也可以写在 always 的组合逻辑里,是可综合的。

再看 task,题目要求算 4 位数的阶乘:

task factorial; input [3:0] a; output [31:0] c; reg [3:0] i; begin c = 1; // 结果从 1 开始连乘 for (i = a; i > 0; i = i - 1) c = i * c; end endtask

逻辑说明:task 可以有多个 input、output、inout,没有返回值,靠输出端口带结果;它允许内部有局部变量(这里的i),调用位置在 initial 或 always 块里。参数说明:原题答案里把c同时声明成 output 和 reg,重复了,实际写法二选一,声明成 output 就行,task 内部给 output 赋值会自动当寄存器用。

这里有个值得琢磨的细节:输入是 4 位,最大能到 15,但 13! 已经超过 32 位无符号数的上限,所以这个 task 实际上是算不准 13 以上的。真要工程化,要么把输入改成 3 位(最大 7,7!=5040 绰绰有余),要么把输出加宽到 64 位。题目没较真,你写的时候得知道这件事。

一句话总结边界:function 只做纯组合,能综合;task 灵活但一般只用在 testbench 里做激励封装,别拿它写综合代码。

4.2 偶分频电路:比较值和位宽是两个雷

第 7 题是偶分频,给定参数 N=6:

module fp_even( input clk_in, input rst, // 同步复位,低有效 output reg clk_out ); reg [7:0] cnt; parameter N = 6; always @(posedge clk_in) begin if (!rst) begin cnt <= 8'd0; // 复位时计数器清零 clk_out <= 1'b0; end else if (cnt == N/2 - 1) begin cnt <= 8'd0; // 计到一半翻转一次,重新开始 clk_out <= ~clk_out; end else begin cnt <= cnt + 1'b1; end end endmodule

逻辑说明:计数器从 0 计到 N/2-1,共 N/2 个时钟周期,到点翻转一次输出并清零,翻转两次就是一个完整的输出周期,所以输出周期是 N 个输入时钟周期,N=6 时是 6 分频。参数说明:cnt位宽 8 位对 N=6 是浪费,实际按$clog2(N)取就够;复位是同步的,所以敏感列表里只有posedge clk_in,复位判断写在块内第一句。

坑一:比较值写错。写成cnt == N-1再翻转,输出周期就变成 2N 个时钟,分频比翻倍;写成cnt == N/2则会多计一个周期,输出频率偏高但波形上看不出明显差异,只有数周期才发现。

坑二:位宽不够。N 取得大一点,比如要 500 分频,reg [7:0] cnt最大 255,N/2-1 = 249还勉强够;做到 512 分频时N/2-1 = 255,刚好卡在边界,再大一点就永远等不到比较值,计数器一路加到 255 回绕,输出纹丝不动。这种 bug 仿真跑再久也不报错,只是波形不对,排查起来很费劲。

另外 N 为奇数时,N/2会被取整,上半个周期和下半个周期不等长,占空比就不是 50%。所以这类电路叫偶分频,奇数分频要另写一套,通常是两个计数器分别对上升沿和下降沿计数再组合。

4.3 延时模块:用寄存器打拍,别用井号

第 8 题给定系统时钟 1MHz,要求把输入信号延时 4us 输出。1MHz 意味着一个时钟周期正好 1us,4us 就是 4 个时钟周期:

module my_delay( input clk, input inta, output reg out ); reg [2:0] ina_r; // 三级中间寄存器 always @(posedge clk) begin ina_r[0] <= inta; // 第 1 拍 ina_r[1] <= ina_r[0]; // 第 2 拍 ina_r[2] <= ina_r[1]; // 第 3 拍 out <= ina_r[2]; // 第 4 拍,输出相对 inta 延迟 4 个时钟 end endmodule

逻辑说明:每一级寄存器都在时钟上升沿把上一级的值搬到下一级,信号从inta走到out正好穿过 4 个触发器,延迟 4 个时钟周期,也就是 4us。参数说明:out声明成 reg 是因为它在 always 里赋值;如果改用assign out = ina_r[2],那out就只能是 wire,延迟变成 3 拍,因为 assign 不额外消耗一拍寄存器。

这是可综合的标准做法,叫延迟链或者打拍。为什么不能用#4?因为#描述的是仿真时间,综合工具直接忽略,你以为延时了,实际电路里一根线都没有。这是新手最容易翻车的地方之一:仿真波形完美,上板之后延迟为零,时序全乱。

在实际项目里,打拍的拍数经常是参数化的:

// 参数化延迟拍数,DELAY 为 1 时退化成单级寄存 module delay_n #(parameter DELAY = 4) ( input clk, input din, output reg dout ); reg [DELAY-1:0] pipe; always @(posedge clk) pipe <= {pipe[DELAY-2:0], din}; // 左移一位,新数据从低位进 always @(posedge clk) dout <= pipe[DELAY-1]; endmodule

这种写法在跨时钟域同步、按键消抖、信号边沿检测里到处都在用,记住模板比记住某一道题答案划算。

4.4 全加器的门级写法:原语实例化的参数顺序

卷子最后给了一份门级全加器实现,正好用来讲原语实例化的语法:

module fulladd( output sum, output cout, input a, input b, input cin ); wire s1, s2, c1; xor (s1, a, b); // s1 = a ^ b and (c1, a, b); // c1 = a & b xor (sum, s1, cin); // sum = a ^ b ^ cin and (s2, s1, cin); // s2 = (a ^ b) & cin or (cout, s2, c1); // cout = (a&b) | ((a^b)&cin) endmodule

逻辑说明:门级原语的实例化格式是「原语名 (输出, 输入1, 输入2, ...)」,输出永远排第一个,这和模块例化时的位置连接是一个道理。参数说明:这份答案里进位那行写成了xor (cout, s2, c1),按全加器的真值表,进位应该是s2 | c1,用异或会算错,a=1、b=1、cin=0 时正确进位是 1,异或得到 0。这是一处实打实的错误,抄答案的时候必须改过来。

门级建模在可综合性上没有毛病,但可读性差、改起来麻烦,工程上基本只在教学和极少数需要精确控制结构的场合用。日常写 RTL,assign {cout, sum} = a + b + cin;一行就够了,综合工具会自己选最优的加法器结构。

5. 上机验证里的常见问题排查:从编译报错到波形不对的 5 条记录

题目做过一遍不等于会了,真正的分水岭是你把代码丢进工具链,从编译、仿真一路走到综合,中间会遇到一堆卷子上不会写的问题。下面这几条是新人复现这套题时最常撞上的,按「现象 → 原因 → 解决」记。

5.1 编译与综合阶段的两类硬伤

第一条,端口列表报语法错误。

现象:编译直接停在模块声明那一行,提示意外的符号,或者端口数量对不上。

原因:题干里端口用点号分隔(my_and(a.b,c)),照着抄就错;另一种情况是复制答案时把逗号带成了中文全角逗号,肉眼看不出区别。

解决:端口之间一律英文逗号,模块声明的推荐写法是用 ANSI 风格,把方向和位宽直接写在端口名前面(input [3:0] a),比老式的「端口列表加方向声明」少一大截出错概率。抄任何代码之前先扫一遍标点。

第二条,对 wire 赋值的报错。

现象:cannot assign to wire或者reg is not a valid l-value。

原因:在 always 块里给一个声明成 wire 的输出赋值,或者反过来,用 assign 驱动一个声明成 reg 的信号。Verilog 里连续赋值(assign)只能驱动线网,过程赋值(always)只能驱动寄存器。

解决:记一句话——用 assign 就是 wire,用 always 就是 reg。模块内部要把信号接到 assign 上,就把 reg 改成 wire;要在 always 里改,就老老实实声明 reg 并去掉 assign。新手常犯的是「全声明成 reg」,然后在顶层用 assign 接出去,编译一报错就懵。

5.2 仿真与波形阶段的功能偏差

第三条,波形跑出来复位不生效。

现象:复位信号拉低,输出还是老样子,或者只在时钟沿才变化。

原因:想做异步复位,但敏感列表只写了posedge clk,没写negedge reset,复位被当成了同步信号处理。

解决:确认复位类型。异步复位必须把复位边沿写进敏感列表,且判断分支放在最前面。用第 3 题的模板对着改,别自己凭感觉补。

第四条,计数器数到一半就不动了。

现象:仿真跑到某个时刻,计数值卡在一个固定值反复回绕,输出信号停止翻转。

原因:分频器里的比较值超过了计数器的表示范围,cnt == N/2 - 1永远不成立;或者位宽少了导致高位被截断。

解决:先算清楚需要的最大计数值,用$clog2(N)声明位宽。仿真时把 N 改小(比如改成 6 或 10)能快速看出问题,跑到 50MHz 的真实时钟下一分钟都翻不了几次,波形上根本看不出对错。

5.3 上板之后的现象和一份最小验证环境

第五条,仿真对了,板子上 LED 常亮或者完全不亮。

现象:仿真波形一切正常,烧进板子之后没有任何反应。

原因:通常是约束文件没写或者写错,引脚没绑到实际的 LED 和按键上;也可能是时钟接到了普通 IO 而不是时钟专用引脚,导致全局时钟网络没建立,触发器的行为不可预期;还有可能是复位极性搞反,按下按键反而是把系统锁在复位态。

解决:先看综合和布局布线的报告,确认时钟有没有走全局网络、有没有时序违例;再对一遍约束文件里的引脚编号和板子原理图。最简单的一步是点一个 LED 常亮,确认工程链路是通的,再往下加功能。

仿真环境不需要多复杂,一个 testbench 就够:

`timescale 1ns / 1ps // 时间单位 1ns,精度 1ps module tb_my_delay; reg clk = 0; reg inta = 0; wire out; // 例化待测模块,命名连接比位置连接更不容易错 my_delay u_dut ( .clk (clk), .inta (inta), .out (out) ); // 时钟周期 20ns,对应 50MHz always #10 clk = ~clk; initial begin inta = 0; #55 inta = 1; // 制造一个上升沿,观察延迟拍数 #200 inta = 0; #100 $display("out = %b at %0t", out, $time); $finish; // 结束仿真 end endmodule

逻辑说明:timescale决定仿真时间单位,#10表示 10 个时间单位;always 块生成自由运行的时钟;initial 块给激励。$display在控制台打印,配合波形窗口一起看最快。参数说明:$finish一定要写,否则仿真不会自己停;待测模块的端口用命名连接(.clk(clk)),端口顺序改了也不用动 testbench。

6. 二次开发:把答案改造成能上板的验证工程

把 8 道题各自孤立地跑一遍,收获有限。我的做法是把它们串成一个闭环:分频器产生慢时钟,慢时钟驱动计数器,计数器输出接译码器点亮 LED。这样做一次,分频、计数、译码三条线全串起来了,比单独跑八遍印象深得多。

难点在于分频器要参数化。卷子里的fp_even固定 8 位计数器、N=6,想用 50MHz 分频到 1Hz,N 要写成 5000 万,8 位根本装不下。改造的关键是用$clog2自动算位宽:

module clk_div #(parameter N = 50000000) ( input clk_in, input rst, output reg clk_out ); // $clog2 返回容纳 N 所需的最小位宽,综合工具支持 reg [$clog2(N)-1:0] cnt; always @(posedge clk_in) begin if (!rst) begin cnt <= 0; clk_out <= 1'b0; end else if (cnt == N/2 - 1) begin cnt <= 0; clk_out <= ~clk_out; end else begin cnt <= cnt + 1'b1; end end endmodule

改完之后,仿真时把 N 传成 10,几个微秒就能看到一次翻转;上板时传 50000000,LED 就变成秒闪。同一份代码两个场景复用,这是参数化的价值。

验证流程我一般走三步:先在仿真里把 N 改小,数清楚输出翻转之间隔了几个输入时钟;再把计数器的输出接到译码器,逐一点亮 8 个状态,确认 case 分支没有漏;最后上板,用按键当复位,肉眼看 LED 节奏。三步里任何一步对不上,都回到公式重新算周期,不要凭感觉改代码。

改造时最容易忽略的是复位释放的时刻。分数频器用同步复位、计数器用异步复位,两个复位源混在一起,上电瞬间的状态可能不是你想要的。稳妥的做法是全局只用一个复位信号,在顶层做一次异步复位同步释放,再分发给各个子模块。

还有一个技巧:把这批题里的模块名、端口名统一改一遍,别留着my_add、my_counter这种教学命名,改成adder_pipe、up_counter这类能表达功能的名字。看似小事,等模块数涨到二十个,你会庆幸当初改了。

从那以后我每次拿到一份题集或者一段教学代码,都强制自己先改成参数化、补上 testbench、跑通仿真再上板,绝不让「答案看起来对」当成「我写对了」。这套笨办法救过我很多次,也省下了大量在板子上瞎猜的时间。希望帮到你。

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

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

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

立即咨询