Verilog HDL课后习题精讲:从仿真综合到参数化自检
发布时间:2026/9/20 3:43:54 锦皓数字建站

简介这份《数字系统设计与Verilog HDL课后习题》PDF面向电子信息、自动化及计算机相关专业的在校生以及准备转入FPGA与数字IC设计的自学者主要用于配合高校课程复习、作业核对与期末备考。内容按章节编排覆盖EDA技术特点、Top-down设计方法、ASIC与FPGA/CPLD实现方式、IP复用、综合与功能/时序仿真等理论题也包含8位加法器、模60 BCD计数器、4选1数据选择器、移位寄存器、串并转换、状态机交通灯、彩灯控制及UDP描述等Verilog编程练习并涉及Synplify、ModelSim、Quartus II等工具的使用思路较完整地串联起数字系统设计的基础概念与编码要点。资源包共1个PDF文件约126KB轻量便于打印、批注与分章练习目前已有1363人学习下载。读者可借助题目逐章检验概念掌握程度把阻塞与非阻塞赋值、任务与函数、状态机写法等易错点落到代码与仿真中形成从原理到工具流程的完整练习路径。1. 课后习题里的 Verilog HDL为什么仿真过了综合却报错很多人拿到《数字系统设计与 Verilog HDL》这套课后习题第一反应是照着课本把 module 敲进编辑器波形一出来就以为过关了。真正卡人的地方在后面同一段代码iverilog 跑得挺欢综合工具却冒出一堆 latch 推断、位宽截断、无法综合的警告。课后习题的价值从来不是答案本身而是它把数字系统设计里最基础的三件事——组合逻辑的完备描述、时序逻辑的边沿与复位、状态机的状态转移——压缩成几十行可以反复推敲的 Verilog HDL 代码。这些代码短到能背下来却又足够暴露你对赋值语义、位宽规则、综合边界的真实理解程度。这篇面向正在啃这套习题的人先把题型分类讲清楚再用仿真跑通最后落到参数化和排错给的都是能直接抄进自己工程的做法。2. 数字系统设计课后题的题型拆解Verilog HDL 的组合、时序与状态机写法课后习题看着五花八门归到 Verilog HDL 层面其实只有三类骨架纯组合逻辑、边沿触发的时序逻辑、带状态转移的有限状态机。把这三类写对八成的题目就稳了。判断题目属于哪一类有个简单办法问自己「输出是否只取决于当前输入」。只取决于当前输入就是组合逻辑用assign或always (*)输出跟历史有关、需要跨时钟保持就是时序逻辑输出依赖于一组有限个内部状态才上状态机。2.1 组合逻辑题assign 与 always (*) 的选择边界课本里最常见的组合逻辑题是多路选择器、译码器、优先编码器。这类题目用assign写数据流描述最直观综合结果也最好预测// 4 选 1 多路选择器纯组合逻辑数据流描述 module mux4to1 #(parameter W 8) ( input wire [W-1:0] d0, d1, d2, d3, input wire [1:0] sel, output wire [W-1:0] y ); assign y (sel 2b00) ? d0 : (sel 2b01) ? d1 : (sel 2b10) ? d2 : d3; endmoduleparameter W让位宽可配后面 8 位改 16 位不用动逻辑sel的 2 位刚好覆盖四种选择最后一级三元表达式的:后面直接给d3等价于补上了default。综合出来是一层 MUX 加一个小的选择译码延迟可估算。换成always (*)加case也能写同一道题但代价是必须处理「所有分支都赋值」这件事漏一个分支就会推断出锁存器。我一般这样分工题目本质是并行的数据选择就用assign题目带优先级语义比如优先编码器、中断仲裁就用always (*)里的if-else if链因为优先级在if-else里是天然的在assign里要嵌套三元表达式可读性差。2.2 时序逻辑题always (posedge clk) 的三个必设参数计数器、移位寄存器、分频器这类题模板几乎一致时钟、复位、使能三个信号缺一不可。// 带同步使能、异步复位的加法计数器 module counter #(parameter W 4) ( input wire clk, input wire rst_n, // 低有效异步复位 input wire en, output reg [W-1:0] cnt, output wire carry ); always (posedge clk or negedge rst_n) begin if (!rst_n) cnt {W{1b0}}; else if (en) cnt cnt 1b1; end assign carry (cnt {W{1b1}}); // 进位是组合输出不要放进时序块 endmodule三个参数值得逐条说。复位写进敏感列表or negedge rst_n表示异步复位上板时复位释放和时钟沿无关调试更省事如果题目明确要求同步复位就把rst_n从敏感列表去掉只在if (!rst_n)里判断代价是多占一点复位树的资源。复位值写{W{1b0}}而不是裸的0是为了让位宽显式对齐避免工具在不同位宽规则下给出意料之外的结果。en用else if挂上未使能时计数器保持这条隐含的「保持」路径正是时序逻辑和组合逻辑最大的区别——组合逻辑没有「保持」这个概念。carry用assign单独拉出来不让它进时序块。进位是当前计数值的纯函数打进寄存器会晚一个周期答非所问。2.3 状态机题三段式描述与状态编码序列检测是这类教材的常客比如检测输入流里的1101。三段式写法在这个场景里最好维护第一段管状态寄存第二段管次态组合逻辑第三段管输出。module seq_det_1101 ( input wire clk, rst_n, din, output reg dout ); localparam S0 3d0, S1 3d1, S2 3d2, S3 3d3, S4 3d4; reg [2:0] cur, nxt; always (posedge clk or negedge rst_n) // 第一段状态寄存器 if (!rst_n) cur S0; else cur nxt; always (*) begin // 第二段次态组合逻辑 nxt S0; // 先给默认值杜绝 latch case (cur) S0: nxt din ? S1 : S0; S1: nxt din ? S2 : S0; S2: nxt din ? S2 : S3; S3: nxt din ? S4 : S0; S4: nxt din ? S2 : S0; default: nxt S0; endcase end always (posedge clk or negedge rst_n) begin // 第三段输出寄存器 if (!rst_n) dout 1b0; else dout (nxt S4); end endmodulelocalparam定义状态编码比define安全作用域限于本模块第二段开头nxt S0;是防止锁存器的关键case里没覆盖到的分支都会落到这个默认值第三段用nxt而不是cur决定输出输出因此和状态同步跳变不会出现毛刺。状态数只有 5 个3 位编码够用default分支还能兜住上电时的非法状态。三类题型的对照关系可以整理成一张表做题前先定位题型再套模板题型特征典型题目描述方式高频错误输出只依赖当前输入译码器、MUX、比较器assign/always (*)case缺default推断 latch输出跨时钟保持计数器、移位寄存器、分频器always (posedge clk)用阻塞赋值、复位值位宽不明输出依赖有限状态序列检测、交通灯、电梯三段式状态机次态未赋默认值、输出用cur3. 用 iverilog 和 GTKWave 把一道 Verilog HDL 课后习题跑通写完代码只是第一步课后习题真正的训练量在「跑起来、看到波形、对上期望值」。iverilog 加 GTKWave 这套组合免费、启动快特别适合一道题一个目录反复折腾。3.1 最小可编译工程与命令行把设计文件和 testbench 放在同一目录三条命令走完编译、仿真、看波形# 编译设计文件在前testbench 在后一起交给 iverilog iverilog -g2012 -o sim.out mux4to1.v tb_mux4to1.v # 运行vvp 解释执行产生 vcd 波形文件 vvp sim.out # 打开波形观察各信号随时间的变化 gtkwave dump.vcd -g2012打开 IEEE 1800-2012 的一部分语法logic、$clog2、循环里声明变量这些写法才认不写这个开关很多教材上的新写法会直接报语法错误。-o sim.out指定输出文件名避免默认的a.out在多个题目之间互相覆盖。如果一个 testbench 文件里塞了多个模块做对比用-s tb_top显式指定顶层否则 iverilog 会自己挑一个挑错了波形是空的。3.2 testbench 里时钟、复位、激励的写法组合逻辑题的 testbench 相对简单把输入逐个试一遍就行timescale 1ns/1ps module tb_mux4to1; reg [7:0] d0, d1, d2, d3; reg [1:0] sel; wire [7:0] y; integer i; mux4to1 u_mux (.d0(d0), .d1(d1), .d2(d2), .d3(d3), .sel(sel), .y(y)); initial begin $dumpfile(dump.vcd); // 波形输出文件 $dumpvars(0, tb_mux4to1); // 0 表示 dump 全部层级 d0 8h11; d1 8h22; d2 8h33; d3 8h44; for (i 0; i 4; i i 1) begin sel i[1:0]; #10; // 等 10ns 让组合逻辑稳定 $display(sel%0d y%h, sel, y); end $finish; end endmoduletimescale 1ns/1ps决定了#10是 10 纳秒以及仿真精度到皮秒不写这行延时单位在不同工具里默认值不一样波形时间轴会对不上。$dumpvars(0, tb_mux4to1)的第一个参数 0 表示递归 dump 下面所有子模块的信号调时序题时不写这个只能看到顶层端口。sel i[1:0]把整数截成 2 位是 Verilog 里很常见的技巧省去手写四段赋值的麻烦。时序题的 testbench 要多一个时钟发生器// 周期 20ns 的时钟初始为 0每 10ns 翻转一次 initial clk 1b0; always #10 clk ~clk; initial begin rst_n 1b0; // 上电先拉低复位 #25 rst_n 1b1; // 25ns 后释放跨过时钟沿更真实 en 1b1; #200 $finish; end复位的释放时刻我一般故意不放在时钟沿上而是错开几个纳秒。这样跑出来的波形能看出复位释放和时钟沿的竞争关系比「刚好在沿上释放」更接近真实情况。3.3 仿真报错与波形异常的对照排查表跑不通的时候报错信息和波形症状基本能对应到固定几类问题报错 / 现象常见原因处理方式syntax error near ...用了-g2012才支持的语法加-g2012或改成 Verilog-2001 写法Unknown module type设计文件没加进编译命令编译时补上对应的.v文件波形全是x信号未初始化、复位没生效检查initial赋值与复位极性问题波形全是z输出被多个驱动源驱动检查是否重复例化或漏接端口波形停在 0 时刻缺$dumpvars或$finish没触发补$dumpvars(0, tb)与结束条件4. Verilog HDL 习题里最常翻车的参数位宽、赋值方式与综合结果代码能编译、波形有变化跟代码写对之间还隔着几道坎。这几道坎在课后习题里出现频率极高而且每一条都会在后面的综合阶段放大。4.1 位宽不匹配与隐式扩展Verilog 的表达式位宽由上下文决定赋值语句里左侧的位宽会反过来影响右侧的计算这一点极其反直觉reg [3:0] a, b; reg [3:0] sum4; reg [4:0] sum5; initial begin a 4d9; b 4d7; sum4 a b; // 右侧按 4 位计算9716 溢出成 0 sum5 a b; // 右侧扩展到 5 位计算得到 16 endsum4拿到的不是 16而是 0因为加法在 4 位宽度下做完才赋过去。习题里的加法器、计数器进位判断经常踩这个坑。稳妥做法是把中间结果和跨模块的比较都按可能的最大位宽声明或者显式用{1b0, a} {1b0, b}先把操作数扩宽。场景危险写法建议写法求和可能溢出sum a b;位宽未扩结果位宽取max(W)1计数到最大值判断cnt 4b1111cnt {W{1b1}}拼接后位数不对{a, b}未核对总宽注释里标出拼接后位宽有符号比较reg声明却按有符号比较显式用$signed()或改wire signed4.2 阻塞赋值与非阻塞赋值的边界一句话原则时序逻辑一律用组合逻辑一律用。违反这条原则最典型的症状是移位寄存器。// 错误阻塞赋值q1 先更新后面两级的输入已经是新值 always (posedge clk) begin q1 din; q2 q1; // 此时 q1 已经等于 din q3 q2; end // 正确非阻塞赋值三条语句在同一时钟沿统一取值 always (posedge clk) begin q1 din; q2 q1; q3 q2; end第一个版本编译通过、波形也不为全零但只移位了一级——q1、q2、q3在同一时刻都变成din。这种错误在单比特序列题里看不出来一旦做并行数据移位就会暴露。4.3 用 parameter 和 generate 把习题做成参数化模块课后习题的位宽往往是 4 位、8 位但真实工程里位宽是配置项。把generate用起来同一段代码可以覆盖多种位宽// 参数化行波进位加法器W 改大不用动逻辑 module ripple_adder #(parameter W 8) ( input wire [W-1:0] a, b, input wire cin, output wire [W-1:0] sum, output wire cout ); wire [W:0] c; assign c[0] cin; genvar i; generate for (i 0; i W; i i 1) begin : gen_adder assign sum[i] a[i] ^ b[i] ^ c[i]; assign c[i1] (a[i] b[i]) | (a[i] c[i]) | (b[i] c[i]); end endgenerate assign cout c[W]; endmodulegenvar只能出现在generate块里它是编译期变量不占硬件资源。begin : gen_adder的标签不能省否则综合出来的层次名是自动生成的波形里定位某一级进位时非常麻烦。c声明成[W:0]多出一位专门放最终进位避免和sum抢位宽。4.4 latch 推断组合块里的隐形陷阱综合日志里出现「inferred latch」警告几乎都出在组合 always 块。根因是某个输入组合下输出没有被赋值工具只能让它保持上一个值而「保持」在硬件上就是锁存器。// 有 latch 风险sel 等于 2b11 时 y 无赋值 always (*) begin case (sel) 2b00: y a; 2b01: y b; 2b10: y c; endcase end // 修复进 case 之前先给默认值一行解决 always (*) begin y 1b0; case (sel) 2b00: y a; 2b01: y b; 2b10: y c; endcase end默认值写在case之前比写default:分支更稳因为它对if-else链同样生效。修复之后综合日志干净时序分析也有了确定的组合路径延迟。综合日志关键词含义处理方向inferred latch组合块存在未赋值分支块首加默认赋值width mismatch两侧位宽不一致显式对齐或扩宽is not a constant把变量用在了需要常量的位置改localparam或genvarmultiple drivers同一信号被多处驱动合并驱动源或改用wire5. 把课后习题改造成自检 testbench断言、随机激励与批量回归肉眼对波形只适合第一道题做到第十道题就受不了了。自检 testbench 的核心思路是把期望值写进代码让 testbench 自己判断对错并累计错误数。用task把一组激励加校验打包每道题只需要重复调用integer errors 0; task check; input [1:0] s; input [7:0] exp; begin sel s; #10; if (y ! exp) begin $display(FAIL sel%0d exp%h got%h, s, exp, y); errors errors 1; end end endtask initial begin d0 8h11; d1 8h22; d2 8h33; d3 8h44; check(2b00, 8h11); check(2b01, 8h22); check(2b10, 8h33); check(2b11, 8h44); if (errors) $fatal(1, errors%0d, errors); else $display(PASS); $finish; end比较用!而不是!这样波形里出现x或z也能被判定为失败而不是悄悄放过。$fatal(1, ...)的第一个参数是退出码非零退出码会被 shell 捕获这是后面做批量回归的基础。errors声明成integer放在模块级task内部可以直接累加不用来回传参。有了自检批量回归就是一行 shell# 每个题目一个目录目录里放 design.v 和 tb.v for d in ex01 ex02 ex03 ex04; do iverilog -g2012 -o /tmp/sim.out $d/design.v $d/tb.v || { echo $d 编译失败; continue; } if vvp /tmp/sim.out | grep -q FAIL; then echo $d 未通过 else echo $d 通过 fi done|| { ...; continue; }处理编译失败避免一个题目挂掉整轮回归grep -q FAIL只看有没有失败行不关心输出多少。想把随机激励也加进来在initial块里用$random生成激励同时用同一份参考模型算出期望值做对比覆盖率比穷举法高得多。真要把这套东西挂到持续集成上靠的是$fatal的非零退出码和grep的返回状态两处都对上一条make test就能把整套课后习题变成可重复执行的验证用例。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。