4. Verilog/VHDL基础回顾:模块化设计思想、常用语法、仿真时间尺度与可综合代码
各位同学,咱们今天聊点实在的。做验证,说白了就是跟代码打交道。你连设计代码都看不懂,那验证也就无从谈起。这一章,我带大家快速过一遍Verilog和VHDL里那些最核心、最常用的东西。别怕,咱们不搞教科书式的罗列,我挑我这些年踩过的坑、用顺手的招儿,讲给你听。
4.1 模块化设计思想:别把鸡蛋放在一个篮子里
模块化,听起来高大上,其实道理很简单。就是把一个复杂的大功能,拆成几个独立的小功能块。每个块干好自己的事,块与块之间通过接口(端口)通信。
我刚开始做项目时,就吃过亏。一个顶层文件写了上千行,所有逻辑揉在一起。后来要改一个计数器,结果把状态机也带崩了。从那以后,我养成了一个习惯:一个模块只干一件事,而且要把这件事干好。
模块化设计的核心原则:
- 高内聚:模块内部的功能要紧密相关。
- 低耦合:模块之间的依赖要尽可能少。
- 接口清晰:端口定义要明确,信号名要见名知意。
举个例子,一个简单的UART接收模块,我会拆成这样:
// 顶层模块:只负责例化子模块和连线
module uart_rx_top (
input wire clk,
input wire rst_n,
input wire rx,
output wire [7:0] data_out,
output wire data_valid
);
wire baud_tick;
wire rx_sync;
// 同步模块:消除亚稳态
sync_edge u_sync (
.clk (clk),
.rst_n (rst_n),
.din (rx),
.dout (rx_sync)
);
// 波特率发生器:产生采样时钟
baud_gen u_baud (
.clk (clk),
.rst_n (rst_n),
.baud_tick (baud_tick)
);
// 接收核心:采样并解析数据
uart_rx_core u_core (
.clk (clk),
.rst_n (rst_n),
.baud_tick (baud_tick),
.rx (rx_sync),
.data_out (data_out),
.data_valid (data_valid)
);
endmodule
你看,顶层模块就像一张电路图,只负责把各个芯片(子模块)连起来。每个子模块的代码都很短,容易读,也容易测。这就是模块化的好处。
4.2 常用语法:always、assign、initial
这三个关键字,是Verilog的基石。我敢说,你写的代码里,90%都离不开它们。咱们一个一个看。
4.2.1 assign:组合逻辑的“连线”
assign 用来描述组合逻辑。说白了,就是一根线,输入变了,输出立刻跟着变。没有时钟,没有延迟(除了门延迟)。
// 一个简单的与门
assign and_out = a & b;
// 一个多路选择器
assign mux_out = sel ? data1 : data0;
注意:assign 的左边必须是 wire 类型。我见过不少新手把 reg 放在左边,编译直接报错。
4.2.2 always:时序逻辑的“心脏”
always 块是时序逻辑的核心。它描述的是“在某个事件触发时,做什么事”。最常用的触发事件是时钟上升沿和复位下降沿。
// 一个带异步复位的D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里有个关键点:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。这是硬性规定,也是新手最容易犯的错。我曾经在一个项目里,因为把时序逻辑里的 <= 写成了 =,导致仿真波形完全不对,排查了整整一天。从那以后,我写代码时都会默念一遍:时序用非阻塞,组合用阻塞。
避坑指南:
我曾经在同一个 always 块里混用了阻塞和非阻塞赋值,结果综合出来的电路和仿真结果完全不一样。记住:同一个 always 块里,要么全用阻塞,要么全用非阻塞,千万别混着来。
4.2.3 initial:仿真的“启动器”
initial 块只在仿真开始时执行一次。它主要用于测试平台(testbench)中,用来初始化信号、生成激励。
// 一个简单的时钟生成器
initial begin
clk = 0;
forever #5 clk = ~clk; // 每5个时间单位翻转一次
end
// 一个复位序列
initial begin
rst_n = 0;
#100;
rst_n = 1;
#200;
rst_n = 0;
#50;
rst_n = 1;
end
注意:initial 块是不可综合的。你只能在仿真代码里用它,设计代码里千万别写。
4.3 仿真时间尺度(`timescale)
`timescale 是Verilog里的一个编译器指令,用来定义仿真时间单位。它的格式是:
`timescale 时间单位 / 时间精度
比如:
`timescale 1ns / 1ps
意思是:仿真时间单位是1纳秒,时间精度是1皮秒。也就是说,你写 #10,就代表延迟10纳秒。而精度决定了仿真器能分辨的最小时间间隔。
我的习惯:
我个人习惯在测试平台文件的开头加上 `timescale 1ns / 1ps。这样既保证了时间单位够用,又保证了精度足够高。如果精度设得太粗(比如 1ns / 1ns),你写 #0.5 会被四舍五入成 #1,仿真结果可能就不对了。
另外,`timescale 的作用域是文件级的。如果一个工程里有多个文件,每个文件都定义了不同的 `timescale,那仿真器会以最后一个生效的为准。为了避免混乱,我建议:所有文件统一使用同一个 `timescale。
4.4 可综合与不可综合代码的区别
这个问题,我每次面试新人都会问。说白了,可综合的代码,是能变成实际门电路的;不可综合的代码,只能用于仿真。
为什么会有不可综合的代码?因为有些语法是专门为仿真设计的,比如文件读写、延时控制、初始块等。这些功能在真实芯片里根本不存在。
| 类别 | 可综合 | 不可综合 |
|---|---|---|
| 赋值方式 | assign、always @(posedge clk) | initial、force、release |
| 延时控制 | 无(或门延迟) | #10、#delay |
| 循环 | for(固定次数)、generate | while、forever |
| 系统函数 | 无 | $display、$monitor、$readmemh、$fopen |
| 数据类型 | wire、reg、integer | real、time、event |
你想想看,如果你在RTL代码里写了个 #10,综合器会怎么处理?它根本不知道“10纳秒”是什么东西。所以,设计代码里,不能出现任何延时控制。
一个简单的判断标准:
如果你写的代码,描述的是“在某个时钟沿做什么事”,那大概率是可综合的。如果你写的代码,描述的是“等一段时间再做什么事”,那大概率是不可综合的。
嗯,这里要注意,有些语法虽然可综合,但综合出来的电路可能不是你想要的。比如 for 循环,如果循环次数不是常数,综合器会展开成一个巨大的组合逻辑,面积和延迟都可能爆炸。所以,能用状态机解决的问题,就别用循环。
4.5 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图。它把模块化设计、常用语法、时间尺度和可综合性的关系串了起来。
好了,这一章的内容就这些。模块化设计是骨架,常用语法是血肉,时间尺度是时钟,可综合性是红线。把这四点吃透了,后面的验证环境搭建,你就能游刃有余。