3. Verilog基础回顾:模块与端口、组合逻辑与时序逻辑、阻塞与非阻塞赋值、Testbench编写
好,咱们开始第三章。这一章我打算带大家快速过一遍Verilog的核心基础。你可能会想:「我都学过Verilog了,为什么还要听你讲?」
嗯,原因很简单。学校里教的Verilog和工业界用的Verilog,其实是两码事。我在刚入行那会儿,写出来的代码被老工程师批得一文不值——「你这代码综合出来全是latch!」「仿真能过,上板就挂!」
所以这一章,我重点讲那些真正影响你设计质量的关键点。不是语法大全,而是实战中必须搞明白的东西。
3.1 模块与端口:芯片设计的「黑盒子」
模块(module)是Verilog最基本的封装单元。说白了,它就是芯片设计里的「黑盒子」——你定义好输入输出,别人只管用,不用管里面怎么实现的。
核心要点:每个模块必须有明确的端口定义。端口类型就三种:input、output、inout。
我个人习惯这样写模块声明:
module counter (
input wire clk, // 时钟
input wire rst_n, // 异步复位,低有效
input wire en, // 使能信号
output reg [7:0] count // 8位计数器输出
);
// 内部逻辑
endmodule
注意几个细节:
- 端口方向一定要写清楚。我见过有人把input写成output,仿真能跑,综合报错,查半天才发现。
- wire和reg的区别要搞明白。简单记:端口默认是wire类型。如果你要在always块里赋值,就得声明成reg。
- inout端口要小心。双向端口在FPGA里常用,但ASIC设计里尽量少用。我曾经在一个项目里用了十几个inout,结果后端的哥们儿差点跟我翻脸——时序收敛太难了。
3.2 组合逻辑与时序逻辑:两种思维模式
这是Verilog设计里最核心的概念,没有之一。我把它叫做「两种思维模式」。
3.2.1 组合逻辑:输入一变,输出就变
组合逻辑没有记忆功能。输入变了,输出立刻跟着变(当然,实际有门延迟)。
写组合逻辑,我推荐两种方式:
- assign语句:适合简单的逻辑
- always @(*):适合复杂的逻辑
// 方式一:assign
assign sum = a + b;
assign full = (a > b) ? 1'b1 : 1'b0;
// 方式二:always @(*)
always @(*) begin
if (sel)
out = a;
else
out = b;
end
避坑指南:写组合逻辑的always块时,一定要把所有输入信号都列在敏感列表里。用always @(*)可以自动推导,省心。我曾经手写敏感列表漏了一个信号,仿真结果对,综合出来完全不对——这叫「仿真与综合不一致」,是新手最容易踩的坑。
3.2.2 时序逻辑:时钟边沿触发
时序逻辑有记忆功能。它只在时钟边沿(上升沿或下降沿)更新状态。
// 上升沿触发的D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里有个关键点:时序逻辑的敏感列表里,除了时钟,就是异步复位/置位信号。不要把其他信号放进去,否则综合出来的东西可能不是你想要的。
3.3 阻塞赋值与非阻塞赋值:一字之差,天壤之别
这个话题,我敢说每个Verilog工程师都在这上面吃过亏。我自己就曾经因为用错赋值方式,导致一个模块的仿真结果和实际行为完全相反,查了整整两天。
记住这个黄金法则:
组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。
为什么?我们来看一个例子:
// 错误示范:时序逻辑用了阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里c得到的是a的新值,即b的值
end
// 综合出来:a和c都是b的延迟一拍,但c比a早半个周期?不对,实际综合结果可能产生两个寄存器串联
// 正确做法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里c得到的是a的旧值,即上一拍的值
end
// 综合出来:两个寄存器串联,c比a晚一拍
你想想看,如果混用会怎样?仿真结果可能对,但综合出来的电路行为完全不一样。这就是所谓的「仿真与综合不一致」——芯片设计里最头疼的问题之一。
| 赋值方式 | 适用场景 | 执行顺序 | 综合结果 |
|---|---|---|---|
| = (阻塞赋值) | 组合逻辑 | 顺序执行 | 组合逻辑 |
| <= (非阻塞赋值) | 时序逻辑 | 并行执行 | 寄存器 |
3.4 Testbench编写:验证你的设计
写Testbench,说白了就是给你的设计造一个「测试环境」。我见过很多新手,设计写完了,Testbench随便写两下,仿真通过就以为万事大吉——结果上板就出问题。
一个好的Testbench应该包含:
- 时钟生成:最基本的激励
- 复位逻辑:上电复位、异步复位等
- 测试向量:覆盖正常情况、边界情况、异常情况
- 自检机制:自动比对结果,而不是人眼盯着波形看
`timescale 1ns/1ps
module tb_counter;
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
// 实例化待测模块
counter u_counter (
.clk (clk),
.rst_n (rst_n),
.en (en),
.count (count)
);
// 生成时钟:周期10ns
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 测试流程
initial begin
// 初始化
rst_n = 0;
en = 0;
#20;
rst_n = 1;
#10;
// 使能计数
en = 1;
#100;
// 关闭使能
en = 0;
#50;
// 检查结果
if (count == 8'd10)
$display("Test Passed!");
else
$display("Test Failed! count = %d", count);
#20;
$finish;
end
endmodule
我的经验:写Testbench时,多用$monitor和$display打印关键信息。别完全依赖波形查看器——有时候几百个信号,你根本看不过来。另外,建议用`ifdef做条件编译,方便切换不同的测试用例。
3.5 本章知识体系
下面这张图,是我自己总结的Verilog基础知识结构。你可以把它当作一个「思维导图」来用:
这张图把本章的核心知识点串起来了。你仔细看,四个分支其实是有内在联系的——模块是容器,组合逻辑和时序逻辑是两种基本电路类型,而Testbench是验证手段。阻塞赋值和非阻塞赋值的选择,直接决定了你写的是组合逻辑还是时序逻辑。
好了,这一章的内容就到这里。记住我反复强调的那句话:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值。这个习惯养成了,你以后写Verilog会少踩很多坑。
公众号:蓝海资料掘金营,微信deep3321