3. 硬件描述语言基础:组合逻辑与时序逻辑、模块化设计、Testbench编写
各位同学好,我是你们的FPGA讲师。今天咱们聊聊硬件描述语言的基础。说白了,Verilog和VHDL就是用来描述数字电路的“语言”。你想想看,画原理图太慢,用代码描述电路才是现代工程师的标配。
我个人习惯用Verilog,因为它语法简洁,上手快。但VHDL在军工、航天领域用得更多,语法严谨。咱们课程以Verilog为主,但概念是通用的。
3.1 组合逻辑 vs 时序逻辑
这是数字电路最核心的两个概念。我刚开始学的时候也迷糊过,后来用一句话就记住了:组合逻辑看输入,时序逻辑看时钟。
3.1.1 组合逻辑
组合逻辑的输出只取决于当前输入。没有记忆功能,没有时钟参与。说白了,就是一堆与门、或门、非门搭起来的电路。
举个例子,一个简单的与门:
// Verilog 组合逻辑示例
assign y = a & b; // y 只由 a 和 b 决定
我在项目中遇到过一个问题:组合逻辑的路径太长,导致时序不满足。嗯,这里要注意,组合逻辑的级数不能太多,否则延迟会累积。
3.1.2 时序逻辑
时序逻辑就不一样了。它依赖时钟,有记忆功能。输出不仅和当前输入有关,还和之前的状态有关。
最简单的时序逻辑——D触发器:
// Verilog 时序逻辑示例
always @(posedge clk) begin
q <= d; // 每个时钟上升沿,q 变成 d 的值
end
你想想看,为什么回测系统需要时序逻辑?因为我们需要记住历史数据。比如K线数据、订单状态,这些都需要寄存器来存储。
- 组合逻辑:用 assign 或 always @(*) 描述
- 时序逻辑:用 always @(posedge clk) 描述
- 组合逻辑用阻塞赋值 =
- 时序逻辑用非阻塞赋值 <=
3.2 模块化设计
写FPGA代码和写软件一样,不能把所有东西都塞到一个文件里。模块化设计是基本功。我习惯把每个功能单元封装成一个模块,就像搭积木一样。
3.2.1 模块的基本结构
一个Verilog模块包含端口定义、内部信号、功能描述三部分。看个例子:
module counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
这个计数器模块,输入有时钟、复位、使能,输出是8位计数值。模块化之后,其他地方要用计数器,直接实例化就行。
3.2.2 模块实例化
在顶层模块里,我们把各个子模块连起来:
module top (
input wire clk,
input wire rst_n,
input wire start,
output wire [7:0] result
);
wire [7:0] cnt_value;
// 实例化计数器
counter u_counter (
.clk (clk),
.rst_n (rst_n),
.en (start),
.count (cnt_value)
);
// 其他逻辑...
assign result = cnt_value * 2;
endmodule
3.3 Testbench编写
写代码不仿真,等于白写。Testbench就是用来验证你的设计对不对的。说白了,就是给设计加激励,看输出是否符合预期。
3.3.1 基本Testbench结构
一个Testbench通常包含:时钟生成、复位生成、激励输入、结果检查。看个例子:
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
// 实例化被测模块
counter uut (
.clk (clk),
.rst_n (rst_n),
.en (en),
.count (count)
);
// 生成时钟
initial begin
clk = 0;
forever #5 clk = ~clk; // 10ns周期,100MHz
end
// 生成激励
initial begin
rst_n = 0;
en = 0;
#20;
rst_n = 1;
#10;
en = 1;
#100;
en = 0;
#50;
$finish;
end
// 监视输出
initial begin
$monitor("time=%0t, count=%d", $time, count);
end
endmodule
3.3.2 仿真技巧
我在项目中常用的几个技巧:
- 使用任务(task)封装常用操作:比如读写寄存器、发送数据包
- 自动检查结果:用 $error 或 assert 语句,不要人工看波形
- 随机测试:用 $random 生成随机激励,覆盖边界情况
3.4 知识体系总览
为了让大家更直观地理解本章的知识结构,我画了一张图:
这张图把本章的核心内容串起来了。组合逻辑和时序逻辑是基础,模块化设计是组织代码的方法,Testbench是验证手段。四者缺一不可。
3.5 小结
今天的内容就到这里。总结一下:
- 组合逻辑:输出只和当前输入有关,用 assign 或 always @(*)
- 时序逻辑:输出和时钟、历史状态有关,用 always @(posedge clk)
- 模块化设计:把功能拆成小模块,方便复用和维护
- Testbench:仿真验证是保证设计正确性的关键
这些基础概念,在后续的FPGA回测加速课程中会反复用到。尤其是时序逻辑,回测引擎里的状态机、流水线、FIFO都离不开它。大家先把基础打牢,后面才能跑得快。