2. 硬件描述语言基础:组合逻辑与时序逻辑、模块化设计、Testbench编写基础
好,咱们直接进入正题。硬件描述语言,说白了就是用来“画”电路的代码。Verilog 和 VHDL 是两大主流,我个人更习惯用 Verilog,因为它语法更灵活,写起来快。不过核心思想都一样——描述硬件,而不是写软件。
你想想看,写 C 语言是告诉 CPU 一步步做什么,但写 Verilog 是在描述一堆逻辑门和触发器怎么连。这个思维转变,是新手最容易卡住的地方。
2.1 组合逻辑 vs 时序逻辑
这是 FPGA 设计的根基。我见过不少新手把这两者混在一起,结果仿真对,上板就错。
2.1.1 组合逻辑
组合逻辑的输出只取决于当前输入。没有记忆,没有时钟。说白了就是一堆与门、或门、非门搭出来的东西。
举个例子,一个简单的加法器:
module adder (
input [3:0] a, b,
output [3:0] sum
);
assign sum = a + b;
endmodule
这里 assign 就是组合逻辑。a 和 b 一变,sum 立刻变。没有时钟,没有延迟(理想情况下)。
2.1.2 时序逻辑
时序逻辑就不一样了。它依赖时钟沿,有记忆功能。寄存器(Flip-Flop)是它的核心。
我在项目中遇到过一个问题:一个计数器,用组合逻辑写,结果仿真波形乱跳。后来改成时序逻辑,用 always @(posedge clk) 就稳了。
module counter (
input clk,
input rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else
count <= count + 1'b1;
end
endmodule
注意这里用了 <=(非阻塞赋值),而不是 =。这是时序逻辑的标配。为什么?因为非阻塞赋值能模拟寄存器在时钟沿同时更新的行为。你用 = 试试,仿真结果可能对,但综合出来的电路会多出很多奇怪的 latch。
<=,组合逻辑用 =,别混用。
2.2 模块化设计
FPGA 设计不是写一个巨大的模块。你想想看,一个复杂的交易系统,如果所有逻辑都塞在一个文件里,调试起来会疯掉。模块化设计就是把大系统拆成小模块,每个模块干一件事。
我个人习惯把模块分成三层:
- 顶层模块:负责连接各个子模块,不写具体逻辑。
- 功能模块:比如行情解析、订单生成、风险控制,每个模块独立。
- 基础模块:比如 FIFO、计数器、加法器,这些可以复用。
举个例子,一个简单的交易信号处理模块:
module signal_processor (
input clk,
input rst_n,
input [31:0] price_in,
output reg [31:0] signal_out
);
// 内部模块实例化
wire [31:0] filtered_price;
lowpass_filter u_filter (
.clk(clk),
.rst_n(rst_n),
.data_in(price_in),
.data_out(filtered_price)
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
signal_out <= 32'b0;
else if (filtered_price > 32'd1000)
signal_out <= 32'd1; // 买入信号
else
signal_out <= 32'd0;
end
endmodule
这里 lowpass_filter 是一个独立的模块。如果以后想换滤波器,直接换模块就行,不用动顶层逻辑。这就是模块化的好处。
wire 或 reg 定义清楚。我习惯在模块顶部用注释写明每个信号的作用,这样半年后自己回来看代码还能看懂。
2.3 Testbench 编写基础
写完了代码,怎么验证对不对?靠 Testbench。说白了,Testbench 就是给设计模块喂激励信号,然后看输出对不对。
我刚开始做 FPGA 时,觉得仿真浪费时间,直接上板调。结果烧坏了两块板子……从那以后,我再也不敢跳过仿真了。
一个简单的 Testbench 结构:
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
wire [3: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;
#100;
$display("Count = %d", count);
#200;
$finish;
end
// 监控输出
initial begin
$monitor("Time = %t, count = %d", $time, count);
end
endmodule
这里有几个要点:
`timescale定义时间单位和精度。我一般用 1ns/1ps。initial块用来生成激励。时钟用forever循环,复位用延迟。$monitor和$display是系统函数,用来打印信息。调试时很有用。
2.4 本章知识体系
为了让你更直观地理解,我画了一张图,展示组合逻辑、时序逻辑、模块化设计和 Testbench 之间的关系:
这张图把本章的核心内容串起来了。组合逻辑和时序逻辑是两大基石,模块化设计帮你组织代码,Testbench 则是验证的保障。四者缺一不可。
好了,这一章就到这里。记住:硬件描述语言不是写软件,是在画电路。多仿真、多上板,慢慢就有感觉了。