简介:这份文档面向数字逻辑与EDA设计课程的实验学习者,围绕LiberoSoc仿真验证与实验箱硬件测试两条主线,整理两份可直接参考的实验报告。内容从Verilog HDL描述基本门电路、组合与时序逻辑设计,到在LiberoSoc中新建工程、编写激励、运行仿真并分析波形,再延伸到74HC系列门电路、编码器、译码显示与计数器等芯片的接线、状态表记录和功能验证,同时包含组合及时序综合实验的设计要求、时序图与问题讨论栏。压缩包内为1个docx文件,约1.43MB,以可编辑实验报告模板形式呈现,附代码和截图,便于对照填写数据、检查输入输出逻辑和复盘实验过程。已有938人学习,适合广东工业大学等相关课程学生及刚接触Verilog与EDA工具的学习者用于实验预习、报告撰写和查漏补缺,尤其适合还原实验数据表与理解仿真结果对应关系。
1. 从实验箱拨线到 LiberoSoc 跑波形:这份资源能省掉多少返工
实验箱上最后一根跳线插进面包板孔,LED 亮没亮,直接决定这一组真值表写不写得下去——这是数字逻辑与 EDA 设计这门课最真实的体验。这份资源把同一批电路搬进了 LiberoSoc:六个 74HC 系列门电路、74HC148 优先编码器、74HC4511 七段译码器、74HC161 计数器,每个都给到 Verilog 模块、测试平台源码、仿真波形截图,另外还配一份实验箱实测报告,表 1 到表 9 的输入输出状态列得整整齐齐。它解决的是一个很具体的问题:报告要有数据,数据要能复现,而仿真波形和硬件实测的判读标准并不一致。适合两类人——正在交这份课设、需要把 Verilog 代码跑通再截图交差的人;以及想拿一套最小可跑的 LiberoSoc 工程熟悉「写模块 → 写激励 → 看波形 → 综合」这条链路的人。下面按源码结构一层层拆,代码能直接抄,坑也一并标出来。
2. 资源结构拆解:两份报告与一套 Verilog 源码怎么对应
先把包里东西清点一遍,不然很容易拿着一堆截图不知道对应哪个模块。整个资源是「双轨」结构:一条轨是实验箱实测,用 DIP 芯片插在实验箱上、拨开关、看 LED;另一条轨是 LiberoSoc 仿真,用 Verilog 描述同样的逻辑、写测试平台、看波形。两条轨覆盖的器件完全相同,只是验证手段不一样。这个设计对读代码很友好——你可以在波形上看到的每一组 0/1,都能在报告的数据表里找到对应的 LED 亮灭记录,反过来也一样。
2.1 文件构成与各自的读法
| 文件类别 | 具体内容 | 读它的目的 |
|---|---|---|
| 实验报告文档 | 两份报告,含实验目的、仪器、原理、数据表 1~9、思考题 | 对照数据表核对逻辑功能,抄表格格式 |
| Verilog 设计源码 | HC00、HC02、HC04、HC08、HC32、HC86 六个门模块 | 看连续赋值assign怎么描述门 |
| 测试平台源码 | 每个模块配一个testbench,用initial块产生激励 | 看激励序列怎么拼出真值表 |
| 波形截图 | 仿真波形窗口截图,背景设为白色便于打印 | 交报告用,也用来判断激励是否覆盖完整 |
| 综合结果截图 | 资源占用、综合报告 | 说明代码是否可综合、被综合成了什么 |
| 硬件实验数据 | 表 1~表 9 的输入输出状态、问题与讨论 | 与仿真波形交叉验证 |
表格里最容易被忽略的是「综合结果截图」这一行。很多人写完 Verilog 只跑仿真,看到波形对了就提交,结果综合报告里一堆 warning 压根没看。门级电路用assign写出来的东西综合器能直接映射成 LUT,这是它最简单的地方;但一旦引入initial块里的赋值语句或者锁存器,综合器就会开始报警告,甚至推不出你想要的电路。
2.2 硬件实验与仿真实验的对照关系
两份报告不是随便凑在一起的,器件是一一对应的。实验箱那一份做的是基本门电路、组合逻辑(74HC148 + 74HC4511)、时序逻辑(74HC161)、以及最后的综合实验;LiberoSoc 那一份把同样的四块内容用 Verilog 重写了一遍,第四个实验还用 SmartDesign 工具做了框图级的顶层连接。这种对应关系是这份资源最有价值的地方:你在仿真里发现某个输出反了,可以立刻回硬件报告里查是不是自己接线时把低有效当成高有效了。
常见的翻车点是判读标准的差异。硬件实验里写的是「LED 亮 = 高电平 = 逻辑 1」,这是正逻辑约定;但 74HC148 的输入输出全是低有效,按键按下是 0,编码输出也是反码。如果你在 Verilog 里按「按下 = 1」写,仿真波形和实验箱记录就会整体反相,数据表填进去对不上,而且很难一眼看出来是自己建模错了还是芯片手册看错了。
2.3 工具链与语言子集:为什么是 Verilog 加 LiberoSoc
代码用的是 Verilog-2001 的一个很小子集:assign连续赋值描述组合逻辑,initial块加#10延时产生激励,reg/wire区分激励信号和观测信号,模块端口用位置关联例化。没有always @(posedge clk)之外的时序建模技巧,也没有 SystemVerilog 的logic、interface这些东西。这个子集的好处是几乎任何仿真器都能跑,LiberoSoc 自带的仿真流程、外部常见的 ModelSim/QuestaSim 一系仿真器都能直接吃进去。
LiberoSoc 在这套流程里承担的角色是「项目壳」:新建工程、指定目标器件、把.v文件加进设计源、把 testbench 加进仿真源、跑综合、跑仿真、打开波形窗口。仿真本身由后端的 HDL 仿真器完成,LiberoSoc 负责组织工程和收敛结果。我一般会先把六个门模块和各自的 testbench 全部加进同一个工程,综合一遍看有没有语法错误,再逐个跑仿真,避免写完一个跑一个、反复切工程。综合报告里的 LUT 占用对门电路这种规模没什么参考价值,但养成看报告的习惯,到后面 74HC161 计数器一类有时序逻辑的模块就会救你——异步清零写成同步、使能信号漏了一个,都能在综合警告里提前暴露。
3. 基本门电路的 Verilog 写法与测试平台激励设计
六个门模块是整个资源里最基础的部分,也是最容易被快速略过的部分。但从「能跑通」到「能交报告」,差别全在测试平台的激励覆盖度上。原始代码里assign那一行没什么可挑的,问题基本都出在 testbench 的激励序列:位宽对不上、循环次数不够、没有结束仿真,导致波形只出现了一半的真值表组合。这一章把模块和激励分开看。
3.1 六个门模块:一行 assign 与位宽的选择
设计源码的写法高度一致,都是把 4 位或 6 位向量整体做按位运算,靠 Verilog 的位运算天然支持向量这一点来省事。
// 74HC00 与非门:四路并联描述,一次覆盖 4 个门 module HC00(a, b, y); input [4:1] a, b; // 用 [4:1] 而不是 [3:0],是为了和芯片 1~4 号门对应 output [4:1] y; assign y = ~(a & b); // 按位取反,四个门同时计算 endmodule // 74HC02 或非门 module HC02(a, b, y); input [4:1] a, b; output [4:1] y; assign y = ~(a | b); endmodule // 74HC04 非门:只有单输入,位宽用了 6 位对应六路反相器 module HC04(A, Y); input [6:1] A; output [6:1] Y; assign Y = ~A; endmodule // 74HC08 与门 module HC08(A, B, Y); input [4:1] A, B; output [4:1] Y; assign Y = A & B; endmodule // 74HC32 或门 module HC32(A, B, Y); input [4:1] A, B; output [4:1] Y; assign Y = A | B; endmodule // 74HC86 异或门 module HC86(A, B, Y); input [4:1] A, B; output [4:1] Y; assign Y = A ^ B; endmodule参数说明上,[4:1]这个位宽不是为了省事随手写的,它对应 74HC00 这类芯片内部封装了四个独立二输入门,编号 1 到 4。用[4:1]的好处是仿真波形里信号名会显示成a[4]、a[3]一直到a[1],和芯片引脚编号对得上,截图放进报告时不用额外解释。缺点是后续如果想用for循环遍历,索引得写for (i = 1; i <= 4; i = i + 1),习惯从 0 开始的人会写错边界。
assign是连续赋值,左侧必须是wire类型,这个规则在这里没踩坑,但到了 74HC161 那种需要保持状态的模块就必须换成reg加always块。我见过有人在计数器里也用assign q = q + 1;,仿真直接不动,因为连续赋值不能自我引用形成状态。门电路之所以能这么写,是因为它没有记忆,输出完全由当前输入决定。
3.2 测试平台:用移位拼出真值表
原始 testbench 的思路是先用b = 4'b0001起始,再通过三次左移把b走一遍 0001、0010、0100、1000,配合a先全 0 再全 1,凑出 8 组输入组合。这个思路本身没问题,但移位次数和位宽要严格对齐,而且没有$finish,仿真会一直挂在那里。
`timescale 1ns/1ns // 时间单位 1ns,精度 1ns,波形横轴按这个刻度显示 module testbench(); reg [4:1] a, b; // 激励信号必须是 reg,才能在 initial 里赋值 wire [4:1] y; // 观测信号是 wire,接模块输出 integer i, j; HC00 u1(a, b, y); // 位置关联例化,端口顺序必须和模块定义完全一致 initial begin // 双重循环遍历全部 16 种 a、b 组合,而不是靠手写移位 for (i = 0; i < 16; i = i + 1) begin for (j = 0; j < 16; j = j + 1) begin a = i[3:0]; b = j[3:0]; #10; // 每 10ns 换一组输入,留出稳定时间 $display("a=%b b=%b y=%b", a, b, y); // 打印到控制台,方便和波形对照 end end $finish; // 主动结束,避免仿真进程一直挂着 end endmodule逻辑说明:外层循环给a遍历 0 到 15,内层给b遍历 0 到 15,一共 256 组,任意二输入门电路的真值表都覆盖得完完整整。#10是必须的,如果不加延时,所有赋值都在同一个仿真时刻完成,波形上只会看到最后一组值,前面的全部被覆盖掉。$display把结果打到控制台,好处是可以直接把文本复制进报告的数据表,不必一格一格从波形上抄。
参数怎么改:#10改成#20只是让波形更宽松、更好截图,不影响逻辑;i[3:0]这种写法要求i声明成integer才能做位选,如果声明成reg [3:0] i就直接写a = i。原始代码里 74HC86 的 testbench 写成了74HC86 u1(a, b, y);,而模块名是HC86,例化名和模块名对不上,编译会直接报找不到模块。这个错误很典型——照着芯片型号写例化名,忘了模块定义里已经把74前缀去掉了。
3.3 波形怎么读,才能和表 1 到表 6 对得上
仿真跑完之后打开波形窗口,第一件事是把背景改成白色,波形线改成黑色,不然打印出来一片深色块,报告里根本看不清。LiberoSoc 的波形查看器里可以给a、b设成十六进制显示,y设成二进制,因为输出只有一位有效信息,二进制最直观。
判读的时候注意两点。第一,门电路的输出是组合逻辑,输入变化后输出立刻跟着变,波形上应该看到y和a、b在同一时刻跳变,如果中间隔了一个时钟周期,说明你不小心把它写成了时序逻辑。第二,正逻辑约定要和硬件报告统一:表 1 里写「LED 亮 = 逻辑 1」,那仿真波形里y=1就对应灯亮,y=0对应灯灭,填表时不要中途换标准,否则前后数据自相矛盾。
对于 74HC04 这种单输入门,真值表只有两行,用 256 组循环跑出来的波形会显得很冗长。我一般会单独给它写一个短激励,只跑6'b000001到6'b100000的移位序列,波形截图更紧凑,报告里也更好排版。这个取舍看你要交什么——如果只是验证功能,短激励够用;如果要求「提交完整仿真结果」,那就老老实实遍历。
4. 组合与时序模块建模:74HC148、74HC4511、74HC161 的实现细节
门电路只是热身,真正撑起这份资源的是后面三个功能模块。它们的共同点是都带控制引脚,而且控制引脚大多是低有效,这是 74 系列器件的传统。把这三个模块写对,综合实验里 148 接 4511 显示按键、161 做十进制计数,基本就是拼积木。
4.1 74HC148:低有效输入输出的优先编码器
8-3 优先编码器的行为用casez描述最贴切,因为输入是低有效,而且优先级从 I7 到 I0 递减。
module HC148(i, ei, a, gs, eo); input wire [7:0] i; // I0~I7,低电平有效,I7 优先级最高 input wire ei; // 使能输入,低有效 output reg [2:0] a; // A2~A0,编码输出也是低有效(反码) output reg gs; // 组信号,低有效表示至少有一个输入有效 output reg eo; // 使能输出,低有效表示当前无有效输入 always @(*) begin if (ei) begin // 未使能,所有输出置无效 a = 3'b111; gs = 1'b1; eo = 1'b1; end else if (i == 8'hFF) begin // 使能了但没有任何键按下 a = 3'b111; gs = 1'b1; eo = 1'b0; // EO 拉低,可以级联下一片 end else begin gs = 1'b0; eo = 1'b1; casez (i) // casez 把 ? 当作无关位,天然表达优先级 8'b0???????: a = 3'b111; // I7 有效,编码 7 的反码 000 8'b10??????: a = 3'b110; // I6 8'b110?????: a = 3'b101; // I5 8'b1110????: a = 3'b100; // I4 8'b11110???: a = 3'b011; // I3 8'b111110??: a = 3'b010; // I2 8'b1111110?: a = 3'b001; // I1 8'b11111110: a = 3'b000; // I0 default: a = 3'b111; endcase end end endmodule参数说明里最关键是「反码」两个字。按下 I7 得到的是000,按下 I0 得到的是111,这是芯片内部就做了取反,不是你写错了。gs和eo的区别也要分清楚:gs低表示「有输入」,eo低表示「没输入」,两者互斥,级联多片时把上一片的eo接到下一片的ei就能扩展成 16-4 编码器。
casez里?表示不关心,综合器会把它当作 don't care 处理,从而把优先级逻辑优化掉。用普通case写全 256 种组合也能实现,但代码会长十几倍,而且容易漏。这里有个隐式约定:casez从上往下匹配,第一条命中就跳出,所以 I7 的0???????必须写在最前面,顺序写反了优先级就跟着反。
4.2 74HC4511:LT、BI、LE 三个控制脚一个都不能省
译码器本身只是一个 BCD 到七段的查表,但 4511 有三个控制脚,LT(灯测试)、BI(消隐)、LE(锁存使能),漏掉任何一个,综合实验里数码管的行为就会和预期不符。
module HC4511(lt, bi, le, d, seg); input wire lt; // 灯测试,低有效:拉低时七段全亮,用于检查数码管好坏 input wire bi; // 消隐,低有效:拉低时七段全灭,优先级低于 LT input wire le; // 锁存使能,高电平时锁存当前 BCD 值 input wire [3:0] d; // D C B A,BCD 输入 output reg [6:0] seg; // a~g 段,共阴极数码管高电平点亮 reg [3:0] latch; always @(posedge le) latch <= d; // LE 上升沿把当前值锁进 latch always @(*) begin if (!lt) seg = 7'b1111111; // 全亮 else if (!bi) seg = 7'b0000000; // 全灭 else case (le ? latch : d) 4'd0: seg = 7'b1111110; // a b c d e f 亮,g 灭 4'd1: seg = 7'b0110000; 4'd2: seg = 7'b1101101; 4'd3: seg = 7'b1111001; 4'd4: seg = 7'b0110011; 4'd5: seg = 7'b1011011; 4'd6: seg = 7'b1011111; 4'd7: seg = 7'b1110000; 4'd8: seg = 7'b1111111; 4'd9: seg = 7'b1111011; default: seg = 7'b0000000; // 10~15 输入非法,消隐 endcase end endmodule逻辑说明:seg[6:0]的顺序约定成a在最高位还是最低位,直接决定字形码表长得对不对。我这里按{a,b,c,d,e,f,g}从高到低排列,写0的时候a到f亮、g灭,就是1111110。如果你在别处看到7'b0111111表示 0,那是把a放在最低位了,两套写法混用会导致报告里的表 8 全错位。
le ? latch : d这种写法是行为级的近似:真实芯片里 LE 为高时输出保持锁存值,LE 为低时直通。仿真阶段这样写够用,但如果要综合到 FPGA 上,always @(posedge le)配合三态或者组合选择会引入额外的触发器资源,而且要确认le在你的顶层里是真的当锁存信号用,还是直接接地当直通。实验箱上通常把 LE 接固定电平,综合实验里为了简单也经常直接拉低,那就没必要引入latch这个寄存器,去掉以后代码更干净。
4.3 74HC161:异步清零、同步置数和 TC 进位
计数器是这份资源里唯一真正带状态的模块,也是综合报告里第一个出现触发器的地方。
module HC161(mr_n, cp, cep, cet, pe_n, d, q, tc); input wire mr_n; // 异步清零,低有效,不依赖时钟沿 input wire cp; // 时钟,上升沿触发 input wire cep; // 计数使能(并行) input wire cet; // 计数使能(串行,同时控制 TC) input wire pe_n; // 同步置数,低有效,只在时钟沿生效 input wire [3:0] d; // 置数数据 D3~D0 output reg [3:0] q; // 计数值 Q3~Q0 output wire tc; // 进位输出,计到 1111 且 CET 有效时为高 assign tc = cet & (q == 4'hF); // 进位是组合输出,不额外占触发器 always @(posedge cp or negedge mr_n) begin if (!mr_n) q <= 4'b0000; // 异步清零,优先级最高 else if (!pe_n) q <= d; // 同步置数 else if (cep & cet) q <= q + 1'b1; // 两个使能都有效才计数 // 其余情况保持,不需要写 else,时序块天然保持 end endmodule敏感列表里negedge mr_n是异步清零的关键,少了它清零就变成同步的,波形上要等下一个时钟沿才归零,和芯片手册对不上。pe_n没有进敏感列表,因为它只在时钟沿生效,这是同步信号,写进去反而错。
tc用assign写成组合输出而不是寄存器,可以省一个触发器。tc = cet & (q == 4'hF)意味着计到 15 且 CET 为高时进位拉高,级联时把tc接到下一片的cet就能串成 8 位计数器。注意很多人在写十进制计数器时用清零法:检测q == 4'b1010然后把mr_n拉低。这样会有一个极短的 1010 状态出现在波形上,虽然异步清零会立刻把它清掉,但在实际电路上这个毛刺可能被下游电路捕获。稳妥的做法是用置数法:检测到 9 时把pe_n拉低、d接0000,下一个时钟沿直接置 0,中间不出现非法状态。
4.4 综合实验:148 接 4511 显示按键,161 做 1Hz 十进制计数
综合实验要求用 74HC148 接 8 个开关模拟按键 0 到 7,输出经 74HC4511 驱动共阴极数码管显示对应数字,没按键时不显示。用 Verilog 搭这个顶层,就是把前面两个模块例化到一起,再加一层反码转换。
module key_display(key, seg); input wire [7:0] key; // 8 个按键,低有效,key[7] 对应按键 7 output wire [6:0] seg; wire [2:0] code; // 148 输出的反码 wire gs, eo; wire [3:0] bcd; HC148 u_enc( .i (key), .ei (1'b0), // 常使能 .a (code), .gs (gs), .eo (eo) ); // 反码转原码:按键 7 时 code=000,需要变成 BCD 的 7 assign bcd = (gs) ? 4'hF : (4'h7 - {1'b0, code}); HC4511 u_dec( .lt (1'b1), // 不做灯测试 .bi (gs), // 没按键时消隐,正好用 gs 低有效这个特性 .le (1'b0), // 直通,不锁存 .d (bcd), .seg(seg) ); endmodule逻辑说明:4'h7 - {1'b0, code}是把 3 位反码还原成 0 到 7 的十进制值。按下按键 7,code = 000,7 - 0 = 7;按下按键 0,code = 111,7 - 7 = 0。这种写法比写一堆case简洁,但要注意位宽扩展——code是 3 位,减去的时候要补一位 0 凑成 4 位,否则结果位宽不够会截断。
bi直接接gs是个很巧的复用:gs在有按键时是 0,没按键时是 1,而bi是低有效消隐,正好反过来用——有按键时bi=0正常显示,没按键时bi=1不消隐。等一下,这里方向反了:bi低有效,bi=0是正常显示,bi=1才是消隐。所以应该接~gs。这种「正好」的错觉是综合实验里最容易翻车的地方,我第一次写的时候也顺手接了gs,仿真一跑发现没按键时数码管全亮显示 8,盯着看了半天才反应过来极性反了。
1Hz 计数的部分需要一个分频器,把板上的高频时钟降到 1Hz 再喂给 161。常见做法是写一个计数器数到时钟频率的一半翻转一次,得到 1Hz 方波,再把它接到cp。如果板上时钟是 50MHz,分频比就是 25000000,这个数要写成参数,不然换一块板子就得改代码。
5. 避坑与排查:仿真跑不通时先看这五处
这套代码规模不大,但踩的坑很集中。下面五条是我自己跑这套实验、以及帮别人看代码时反复遇到的,按「现象 → 原因 → 解决」记下来,出问题时按顺序对一遍基本能定位。
5.1 编译与例化:模块名对不上、端口顺序错位
现象:点综合直接报错,提示找不到某个模块,或者例化之后输出全是 X(不定态)。
原因:最常见的是模块名和例化名混用。原始代码里 74HC86 的 testbench 写的是74HC86 u1(a, b, y);,但模块定义是module HC86(...),Verilog 标识符不能以数字开头,74HC86这种名字编译期就是非法的。另一个高频原因是位置关联例化时端口顺序写反,比如HC00 u1(a, b, y)写成了HC00 u1(b, a, y),对于与非门这种对称运算看不出来,但换成 148 这种输入输出都是多位的模块,a和gs位置一换,波形立刻全是 X。
解决:统一用命名关联例化,.i(key), .a(code)这样写,顺序错了编译器也会报错。模块名统一去掉74前缀,因为 Verilog 标识符不能以数字开头,这是一条硬性语法规则,不是风格问题。
5.2 激励写死了,真值表只跑了一半
现象:波形窗口里只有三四组输入变化,表 1 到表 6 中有一半的行填不出来。
原因:原始 testbench 用手写移位的办法产生激励,b = 4'b0001之后只移了三次,b走完 0001、0010、0100、1000 就停了,a只有全 0 和全 1 两种取值,一共 8 组。对于二输入门,完整真值表是 4 行,8 组够用;但如果想验证四路门电路各自的独立性,或者想覆盖全部 16 种a、b组合,手写移位就不够了。更麻烦的是没有$finish,仿真跑完不结束,波形横轴一直往后延伸,截图时很难截到干净的一段。
解决:改成双重for循环遍历全部组合,末尾加$finish。如果必须用手写移位,至少确认移位次数等于位宽减一,并且每一次移位后都有#10延时。另外建议在initial块开头加一句$monitor或者$display,把输入输出打到控制台,这样即使波形截不出来,文本结果也能填表。
5.3 时序与综合:能仿真不等于能上板
现象:仿真波形完全正确,但综合报告里一堆 warning,或者下到 FPGA 上行为不对。
原因:行为级代码和可综合代码之间有差距。always @(*)里写if分支不写全,综合器会推断出锁存器(latch),这类 warning 在仿真里完全看不出来,因为仿真器按你写的行为执行,而综合器按硬件规则理解。另一个原因是异步清零的敏感列表漏了negedge mr_n,仿真时清零要靠时钟沿,波形上看不出错,但和真实芯片行为不一致。
解决:综合完必须打开警告列表看一遍,重点找「inferred latch」「combinational loop」这两类。组合逻辑的always @(*)块里,所有分支都要给输出赋值,case一定要写default——4511 里default: seg = 7'b0000000;这一行就是为此存在的。
5.4 极性搞反:低有效信号当成高有效用
现象:仿真里数码管该显示 3 的时候显示了 4,或者没按键时反而全亮。
原因:74 系列器件的输入输出大量使用低有效,148 的输入i、使能ei、输出a、gs、eo全是低有效;4511 的lt、bi也是低有效。写代码时如果按「按下 = 1」的直觉来,整体就反相了。综合实验里把gs直接接到bi那个例子,就是极性没想清楚的典型。
解决:每个模块定义时,在端口注释里写清「低有效」三个字,综合顶层连线时逐个核对。判读波形时也要统一约定:低有效信号建议在波形查看器里加一层反相显示,看起来更符合直觉。
5.5 大位宽位选和变量类型:integer忘了声明
现象:a = i[3:0];这行报语法错误,或者仿真时a一直是 0。
原因:i[3:0]要求i可以是integer或reg向量,但integer在 Verilog 里是 32 位有符号数,位选语法在部分工具里支持不好。另外把integer声明在模块外部、initial块里直接用,作用域也不对。
解决:循环变量统一声明成integer放在模块内部,位选取值改成a = i[3:0];之前先确认工具链支持;如果报错,就改成a = i % 16;或者直接声明reg [3:0] i然后写a = i。这类问题在 LiberoSoc 里报错信息比较隐晦,看不出来就换一个仿真器交叉验证一下。
6. 二次开发:把实验报告改造成可复用的验证工程
这套代码如果只用来交一次作业,价值有限。真正值得做的是把它改成一个可复用的验证模板:以后写任何门级或小规模时序电路,直接套测试平台,自动比对,自动出表。
第一步是把 testbench 从「产生激励」升级成「自检式」。下面这段是在 HC00 的测试平台上加自动比对,期望值用函数当场算出来,结果不一致就打错误信息。
`timescale 1ns/1ns module tb_hc00_selfcheck(); reg [4:1] a, b; wire [4:1] y; integer i, j, err; HC00 u1(a, b, y); // 参考模型:与非门期望值,与设计分开写,避免抄同一份逻辑 function [4:1] ref_nand(input [4:1] x, input [4:1] z); ref_nand = ~(x & z); endfunction initial begin err = 0; for (i = 0; i < 16; i = i + 1) begin for (j = 0; j < 16; j = j + 1) begin a = i[3:0]; b = j[3:0]; #10; if (y !== ref_nand(a, b)) begin err = err + 1; $display("[FAIL] a=%b b=%b y=%b expect=%b", a, b, y, ref_nand(a, b)); end end end $display("total errors = %0d", err); $finish; end endmodule这里用!==而不是!=,因为X和Z参与比较时,!=会返回不定态,!==才能把「输出是 X」这种错误也判成失败。参考模型用function单独写,不要在 testbench 里直接抄设计模块的表达式,否则两边错得一样,自检就成了摆设。
第二步是做参数化。门电路位宽、分频比、计数器模值,这三类数字最适合提成parameter。
| 参数 | 改它的效果 | 常见取值 |
|---|---|---|
WIDTH | 门模块的向量位宽,控制同时验证几路门 | 4(对应四路门)或 1(单路验证) |
DIV | 分频比,决定 1Hz 计数时喂给 161 的时钟频率 | 板上时钟频率的一半 |
MOD | 计数器模值,改 10 就是十进制,改 16 就是十六进制 | 10、12、16 |
TICK | 测试平台每步延时,影响波形疏密 | 10、20、100 |
改完之后,同一套 testbench 就能验证不同模值的计数器,做十进制、十二进制实验时不用重写。
第三步,也是我认为最值的一点:把仿真波形的判读标准固化成一张对照表,贴在工程目录里。硬件实验记录的是 LED 亮灭,仿真记录的是波形高低,两者映射关系一旦写死,以后换谁来做这套实验都不会出现数据对不上的情况。我现在的习惯是每个工程目录下放一个README,第一段写清楚「本工程所有低有效信号以_n结尾,波形判读时0表示有效」,后面才写怎么编译怎么跑。这个习惯是被一次级联实验坑出来的——两片 161 串起来做 100 进制,tc接cet,结果忘了tc是组合输出,级联后中间那一位的进位晚了一个时钟周期,查了两个小时才定位到敏感列表上。
从那以后,我每次新建一个带时序逻辑的 Verilog 工程,都强制先写 testbench 再写设计模块,跑完第一版波形再回头看综合警告,两步都过了才动手写报告里的数据表。这样做的好处是报告里的每一个 0 和 1 都能在波形上指出来源,被问到「这个数怎么来的」时不至于卡壳。这套资源的代码量不大,正好适合拿来做这个流程的第一次演练。希望帮到你。
本文还有配套的精品资源,点击获取