3. 硬件描述语言基础(HDL):Verilog/VHDL核心语法回顾,组合逻辑与时序逻辑设计,仿真与测试平台编写
好,咱们正式开始进入硬件描述语言的世界。说实话,很多刚接触FPGA的同学,总觉得Verilog或者VHDL就是写代码,跟C语言差不多。嗯,这个想法其实挺危险的。我经常在项目里看到有人把HDL当软件写,结果综合出来的电路完全不是那么回事。
今天这一章,我带大家快速回顾一下HDL的核心语法,重点放在组合逻辑和时序逻辑的设计上。最后再聊聊仿真和测试平台怎么写。这些都是基本功,但也是决定你设计质量的关键。
3.1 Verilog与VHDL:选哪个?
我个人习惯用Verilog,因为它在数字电路设计领域更流行,语法也更简洁。但VHDL在军工、航天这些领域依然很强势。你想想看,如果你去欧洲做项目,大概率要用VHDL。
其实两者本质是一样的,都是描述硬件。我建议你至少精通一种,另一种能看懂就行。我自己就是Verilog为主,VHDL能读能改。
核心观点:HDL不是编程语言,是硬件描述语言。你写的每一行代码,最终都会变成实实在在的门电路和触发器。
3.2 Verilog核心语法速览
咱们快速过一下Verilog里最常用的东西。我不会讲太细,重点是你得知道这些语法对应什么硬件。
3.2.1 模块与端口
一个Verilog设计的基本单元就是模块。模块就像是一个黑盒子,有输入、输出,内部实现具体功能。
module counter (
input wire clk,
input wire rst_n,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else
count <= count + 1'b1;
end
endmodule
看到没?input和output定义了端口方向。wire和reg是数据类型。这里有个坑:reg并不一定代表寄存器,它只是表示在always块里被赋值。我在项目中遇到过有人以为reg就是触发器,结果组合逻辑也用reg,综合出来一堆没必要的锁存器。
3.2.2 组合逻辑:assign与always@(*)
组合逻辑就是输出只取决于当前输入,没有记忆功能。Verilog里两种写法:
- assign语句:用于连续赋值,适合简单的逻辑。
- always@(*)块:用于过程赋值,适合复杂的组合逻辑。
// 方式一:assign
assign sum = a + b;
assign carry = a & b;
// 方式二:always@(*)
always @(*) begin
if (sel)
out = a;
else
out = b;
end
我的小技巧:写组合逻辑时,always块里一定要用阻塞赋值=,别用非阻塞<=。我曾经因为这个问题,仿真怎么都对不上,查了半天才发现是赋值方式用错了。
3.2.3 时序逻辑:always@(posedge clk)
时序逻辑就不一样了,它依赖时钟边沿来更新状态。说白了,就是触发器在干活。
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里必须用非阻塞赋值<=。为什么?因为非阻塞赋值能保证在同一个时钟周期内,所有赋值同时生效。你想想看,如果多个触发器级联,用阻塞赋值就会产生竞争,仿真结果和实际电路完全不一样。
3.3 组合逻辑与时序逻辑的设计要点
很多新手分不清什么时候用组合逻辑,什么时候用时序逻辑。我给大家一个简单的判断标准:
| 逻辑类型 | 特点 | 典型应用 | 赋值方式 |
|---|---|---|---|
| 组合逻辑 | 无记忆,输出即时响应 | 加法器、多路选择器、译码器 | 阻塞赋值 = |
| 时序逻辑 | 有记忆,依赖时钟边沿 | 计数器、移位寄存器、状态机 | 非阻塞赋值 <= |
避坑指南:我曾经在一个项目里,把组合逻辑和时序逻辑混写在同一个always块里,结果综合出来的电路多了一堆没用的锁存器。后来我养成了一个习惯:组合逻辑和时序逻辑分开写,一个always块只做一件事。
3.4 仿真与测试平台编写
写完了设计,怎么知道它对不对?仿真!说白了,仿真就是给设计喂激励,看它输出是不是符合预期。
3.4.1 测试平台的基本结构
测试平台(testbench)本身也是一个模块,但它没有输入输出端口。它的任务就是实例化被测试的设计,然后产生激励。
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
wire [7:0] count;
// 实例化被测试模块
counter uut (
.clk (clk),
.rst_n(rst_n),
.count(count)
);
// 产生时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 周期10ns
end
// 产生复位和激励
initial begin
rst_n = 0;
#20;
rst_n = 1;
#200;
$finish;
end
// 监控输出
initial begin
$monitor("time=%0t, count=%d", $time, count);
end
endmodule
你看,测试平台里用initial块来产生激励,用forever来产生时钟。这些都是不可综合的,只用于仿真。
3.4.2 仿真中的常见陷阱
嗯,这里要注意几个点:
- 时间尺度:
`timescale一定要写,不然仿真时间单位默认是1ns,但精度可能不够。 - 初始化:仿真里
reg默认是X态,所以一定要给初始值或者复位。 - 仿真时间:别设得太短,也别太长。我一般先跑100个时钟周期看看波形,没问题再跑长一点。
我的经验:写测试平台时,我习惯把激励分成几个阶段:复位、配置、正常工作、异常情况。这样仿真结果一目了然,出了问题也容易定位。
3.5 小结
这一章咱们回顾了Verilog的核心语法,重点区分了组合逻辑和时序逻辑的设计方法,最后聊了仿真测试平台的写法。说实话,这些内容看起来简单,但真正用好需要大量实践。
我建议大家动手写几个小设计,比如一个4位加法器、一个8位计数器,再写个测试平台仿真一下。只有亲手写过,你才能真正理解HDL和软件语言的区别。
下一章咱们会深入讲状态机的设计,那可是FPGA设计的核心技能之一。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。