第三章 硬件描述语言基础:Verilog/VHDL快速入门、组合逻辑与时序逻辑、状态机设计、同步设计原则
各位同学,欢迎来到第三章。这一章是FPGA设计的“内功心法”。
说实话,很多新手一上来就追着算法跑,结果综合出来一堆莫名其妙的毛刺。我见过太多这样的案例了。硬件描述语言(HDL)不是软件,你写的每一行代码,最终都会变成实实在在的电路。这个观念,必须从一开始就刻在脑子里。
3.1 Verilog与VHDL:选哪个?
这是个老生常谈的问题。我个人习惯用Verilog,因为它语法简洁,写起来快。尤其是在高频交易场景下,代码量巨大,Verilog的紧凑性优势很明显。
但VHDL也有它的铁粉。它的强类型系统在大型项目中能帮你提前发现很多类型不匹配的错误。我早期在军工项目里用过VHDL,那会儿觉得它啰嗦,现在回头看,严谨有严谨的好处。
不过,对于咱们这个课程,我建议你主攻Verilog。为什么?因为目前主流的FPGA工具链、IP核、以及开源社区的资源,Verilog占了绝对统治地位。你想想看,如果拿到一个别人的高速SerDes接口代码,十有八九是Verilog写的。
核心观点: 语言只是工具。真正值钱的是你的硬件思维。别纠结于语法细节,先把电路结构想清楚。
3.2 组合逻辑与时序逻辑:硬件的“阴阳”
这是FPGA设计里最基础、也最重要的概念。说白了,就是两种电路。
3.2.1 组合逻辑
组合逻辑的输出只取决于当前的输入。没有记忆功能。比如一个简单的与门、或门、加法器。
在Verilog里,用 assign 或者 always @(*) 来描述。注意,always @(*) 里的敏感列表是“*”,代表所有输入信号。我曾经见过有人手写敏感列表,漏了一个信号,结果仿真和综合结果不一致,查了两天。嗯,从那以后,我写组合逻辑只用 always @(*) 或者 assign。
// 组合逻辑示例:一个简单的加法器
module adder (
input [7:0] a,
input [7:0] b,
output [8:0] sum
);
assign sum = a + b;
endmodule
3.2.2 时序逻辑
时序逻辑的输出不仅取决于当前输入,还取决于电路之前的状态。它有记忆功能,核心元件就是触发器(Flip-Flop)。
在Verilog里,用 always @(posedge clk) 来描述。这里的 posedge clk 就是时钟上升沿。所有时序逻辑都应该在时钟沿的驱动下工作。
避坑指南: 千万不要在多个 always 块里对同一个变量赋值!这是新手最容易犯的错误。综合工具会报错,或者生成你意想不到的电路。我曾经在一个项目中,因为这个问题导致综合出来的电路多了一堆Latch,功耗和面积都炸了。
// 时序逻辑示例:一个带使能的8位寄存器
module reg_8bit (
input clk,
input rst_n,
input en,
input [7:0] d,
output reg [7:0] q
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 8'b0;
else if (en)
q <= d;
end
endmodule
3.3 状态机设计:控制逻辑的灵魂
在高频交易系统中,状态机无处不在。比如,一个交易指令的解析、一个数据包的接收、一个算法的流水线控制,背后都是状态机在调度。
我个人最喜欢用三段式状态机。为什么?因为它把状态跳转、状态输出、次态逻辑分得清清楚楚,代码可读性强,也容易维护。
3.3.1 三段式状态机模板
// 三段式状态机示例
module fsm_example (
input clk,
input rst_n,
input start,
output reg done
);
// 状态编码
localparam IDLE = 2'b00,
S1 = 2'b01,
S2 = 2'b10,
DONE = 2'b11;
reg [1:0] state, next_state;
// 第一段:时序逻辑,状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:组合逻辑,次态判断
always @(*) begin
next_state = state;
case (state)
IDLE: if (start) next_state = S1;
S1: next_state = S2;
S2: next_state = DONE;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:时序逻辑,输出
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
done <= 1'b0;
else if (state == DONE)
done <= 1'b1;
else
done <= 1'b0;
end
endmodule
个人经验: 状态机的状态编码,我建议用独热码(One-Hot)或者格雷码(Gray Code)。独热码译码快,适合速度要求高的场景;格雷码相邻状态只变化一位,适合跨时钟域。别用二进制编码,除非你的状态数很少。
3.4 同步设计原则:稳定压倒一切
这是FPGA设计的金科玉律。说白了,就是所有时序逻辑都在同一个时钟域下工作,避免异步设计带来的亚稳态问题。
你想想看,在高频交易里,一个亚稳态可能导致数据错误,甚至交易指令执行错误。那损失可不是闹着玩的。
3.4.1 核心原则
- 单时钟域设计: 尽量使用同一个时钟。如果必须跨时钟域,一定要用同步器(两级触发器打拍)。
- 避免组合逻辑反馈: 组合逻辑的输出不要直接反馈到自己的输入,这会形成Latch或者振荡器。
- 复位信号同步: 异步复位信号必须同步化后再使用。我习惯用异步复位、同步释放的方式。
3.4.2 跨时钟域同步器
// 两级触发器同步器
module sync_2ff (
input clk_dst,
input rst_n,
input async_in,
output sync_out
);
reg sync_reg1, sync_reg2;
always @(posedge clk_dst or negedge rst_n) begin
if (!rst_n) begin
sync_reg1 <= 1'b0;
sync_reg2 <= 1'b0;
end else begin
sync_reg1 <= async_in;
sync_reg2 <= sync_reg1;
end
end
assign sync_out = sync_reg2;
endmodule
警告: 两级触发器同步器只能用于单比特信号的同步。如果是多比特总线,必须使用异步FIFO或者握手协议。我曾经见过有人用两级触发器同步一个8位总线,结果数据错位,查了整整一周。
3.5 本章知识体系图
下面这张图,是我自己总结的。它把本章的核心知识点串在了一起。你把它打印出来,贴在工位上,写代码的时候看一眼,能少走很多弯路。
3.6 写在最后
这一章的内容,是FPGA设计的基石。你可能会觉得组合逻辑、时序逻辑这些概念很简单,但真正用好它们,需要大量的实践。
我记得自己刚入行时,写了一个简单的计数器,结果综合出来一堆Latch。当时师傅看了一眼代码,只说了一句:“你忘了给所有分支赋值。” 从那以后,我写组合逻辑时,一定会检查case语句是否覆盖了所有分支,if-else是否成对出现。
嗯,这些坑,你们现在知道了,就能少走很多弯路。
一句话总结: 写HDL代码,心里要时刻想着电路。你写的不是代码,是门、是触发器、是连线。
公众号:蓝海资料掘金营,微信deep3321