第四章:FPGA数字逻辑基础
各位同学,欢迎来到第四章。这一章,咱们要啃的是FPGA设计的“地基”——数字逻辑基础。说白了,就是Verilog/VHDL语法、组合逻辑、时序逻辑、状态机,还有同步异步设计。这些东西,你玩透了,后面写复杂系统才不慌。
我刚开始带项目那会儿,有个小伙子写了个状态机,仿真怎么跑都对,一上板子就乱跳。查了三天,最后发现是组合逻辑输出没寄存,毛刺直接送进了状态跳转逻辑。嗯,这种坑,咱们今天就得提前填上。
核心要点:本章所有内容,最终都指向一个目标——让你写的代码,在FPGA里跑得稳、跑得快、不莫名其妙。
4.1 Verilog/VHDL语法入门——别被语法吓住
很多新手一上来就背语法,其实没必要。你想想看,FPGA设计不是写软件,我们是在描述硬件。Verilog和VHDL,说白了就是硬件描述语言,不是编程语言。
我个人习惯用Verilog,因为它简洁。但VHDL在军工、航天领域用得更多,因为语法严谨。你选哪个?我建议:先精通一个,另一个能看懂就行。
这里我列几个最常用的语法结构,你记牢了,80%的代码都能写:
| 语法元素 | Verilog示例 | VHDL示例 | 说明 |
|---|---|---|---|
| 模块定义 | module counter(...) |
entity counter is |
每个设计的基本单元 |
| 输入输出 | input clk, output reg [7:0] q |
port (clk: in std_logic; q: out std_logic_vector) |
端口方向与类型 |
| always/process | always @(posedge clk) |
process(clk) |
时序逻辑的敏感列表 |
| 赋值 | assign a = b & c; |
a <= b and c; |
组合逻辑用阻塞赋值 |
| 非阻塞赋值 | q <= d; |
q <= d; |
时序逻辑用非阻塞赋值 |
我的小技巧:写Verilog时,always @(*) 用于组合逻辑,always @(posedge clk) 用于时序逻辑。千万别混用敏感列表,否则综合出来一堆奇怪的锁存器。我曾经见过一个项目,就因为少写了一个敏感信号,综合出了200多个latch,功耗直接翻倍。
4.2 组合逻辑与时序逻辑——两个世界的分界线
组合逻辑,输出只取决于当前输入。时序逻辑,输出还取决于过去的状态。这个区别,你写代码时必须刻在脑子里。
为什么?因为组合逻辑容易产生毛刺。你想想看,信号从A传到B,路径长度不一样,到达时间就有先后。这中间就会产生短暂的错误电平——毛刺。时序逻辑用时钟沿采样,天然免疫毛刺。
我给大家画个图,看看组合逻辑和时序逻辑在FPGA里是怎么工作的:
看明白了吗?组合逻辑就像一条直路,信号过去就过去了。时序逻辑像有个检查站,时钟沿来了才放行。所以,我建议你:所有跨时钟域的信号,必须经过时序逻辑同步。这是铁律。
避坑指南:我曾经接手过一个射频控制模块,原工程师把组合逻辑的输出直接连到了状态机的跳转条件上。结果FPGA在高温下频繁误触发,查了两个月。最后发现是组合逻辑的毛刺在作怪。从那以后,我所有组合逻辑输出后面,必加一级D触发器。
4.3 有限状态机(FSM)设计——系统的“大脑”
FSM是FPGA设计的核心技能。你写通信协议、控制逻辑、数据处理,都离不开它。FSM分两种:Moore型和Mealy型。Moore型的输出只取决于当前状态,Mealy型的输出还取决于输入。
我个人偏好Moore型,因为输出稳定,没有毛刺。但Mealy型更灵活,响应更快。怎么选?看你的应用场景。
这里我给出一个经典的三段式FSM写法,这是业界标准:
// 三段式FSM示例 - 一个简单的序列检测器
// 检测 "101" 序列
module seq_detector (
input wire clk,
input wire rst_n,
input wire din,
output reg dout
);
// 第一段:状态编码
localparam IDLE = 2'b00,
S1 = 2'b01,
S10 = 2'b10,
S101 = 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: next_state = (din) ? S1 : IDLE;
S1: next_state = (din) ? S1 : S10;
S10: next_state = (din) ? S101 : IDLE;
S101: next_state = (din) ? S1 : S10;
default: next_state = IDLE;
endcase
end
// 输出逻辑(时序逻辑,避免毛刺)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= 1'b0;
else if (state == S101)
dout <= 1'b1;
else
dout <= 1'b0;
end
endmodule
我的经验:三段式FSM的好处是,状态转移、次态逻辑、输出逻辑完全分离。调试时,你只需要看state寄存器的值,就知道系统走到哪一步了。我曾经用这个方法,半天就定位了一个复杂的握手协议bug。另外,状态编码尽量用独热码(one-hot),虽然多用了寄存器,但译码逻辑简单,跑得快。
4.4 同步与异步设计——时钟域的艺术
同步设计,所有寄存器用同一个时钟沿。异步设计,存在多个时钟域。在FPGA里,我们尽量用同步设计,因为时序分析简单,不容易出问题。
但现实是,你的系统里往往有多个时钟:ADC采样时钟、DAC转换时钟、处理器接口时钟、射频本振时钟...这些时钟之间怎么通信?这就是跨时钟域(CDC)问题。
我给大家总结几个CDC处理的黄金法则:
- 单比特信号:用两级同步器(两个D触发器串联)
- 多比特信号:用异步FIFO(双口RAM + 读写指针同步)
- 控制信号:用握手协议(req/ack)
- 绝对禁止:组合逻辑输出直接跨时钟域
这里我画一个两级同步器的结构图,这是最常用的CDC方法:
血的教训:我曾经设计一个射频收发系统,ADC时钟是122.88MHz,DAC时钟是245.76MHz。我偷懒,直接用组合逻辑把ADC数据送给了DAC。结果呢?误码率高达10^-3,完全不能用。后来老老实实加了异步FIFO,误码率直接降到10^-12以下。记住:跨时钟域,永远不要相信组合逻辑。
4.5 本章小结——你该带走什么
这一章内容不少,但核心就几点:
- 语法是工具,不是目的。别在语法上钻牛角尖,多想想你描述的是什么硬件。
- 组合逻辑和时序逻辑要分清楚。组合逻辑输出加寄存器,这是最便宜的保险。
- FSM用三段式。状态转移、次态逻辑、输出逻辑分开写,调试时你会感谢我的。
- 跨时钟域必须同步。单比特用两级同步器,多比特用异步FIFO,别偷懒。
嗯,这些内容,你写代码时每一条都用得上。下一章,我们会把这些基础应用到实际的射频接口设计中。但今天,先把这些基础打牢。
最后说一句:FPGA设计没有捷径。你踩过的每一个坑,都会变成你简历上的亮点。我当年调试那个异步FIFO,整整熬了三个通宵。但搞明白之后,后面所有跨时钟域问题,我都能半小时内搞定。所以,别怕犯错,怕的是错了不知道为什么。
公众号:蓝海资料掘金营,微信deep3321