2. FPGA基础与Verilog实战:组合逻辑与时序逻辑、有限状态机设计、AXI4-Stream接口实战
好,咱们直接进入正题。FPGA 开发,说白了就是两件事:逻辑设计和接口通信。逻辑设计是内功,接口通信是招式。这一章,我把这两块的核心都给你拆开揉碎。
我个人习惯,学 FPGA 先别急着上板子跑。先把组合逻辑、时序逻辑、状态机这三个基本功练扎实。然后再看 AXI4-Stream 这种总线,你会发现它其实就是个带握手的移位寄存器。
2.1 组合逻辑与时序逻辑:数字电路的两大基石
很多初学者分不清组合逻辑和时序逻辑的区别。我打个比方:组合逻辑就像一扇门,输入一变,输出立刻变;时序逻辑就像一个水桶,只有时钟沿来了,才舀一瓢水进去。
| 对比项 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 输出依赖 | 仅当前输入 | 当前输入 + 历史状态 |
| 存储能力 | 无 | 有(寄存器/锁存器) |
| 时钟驱动 | 不需要 | 必须有时钟沿 |
| 典型电路 | 与门、加法器、多路选择器 | 计数器、移位寄存器、状态机 |
| Verilog 描述 | assign 或 always @(*) |
always @(posedge clk) |
2.1.1 组合逻辑:别让它产生锁存器
写组合逻辑时,最容易犯的错误就是——不小心综合出锁存器(Latch)。我在项目中遇到过一位同事,写了一个 always @(*) 块,if 条件没写 else,结果综合出来一堆锁存器,时序怎么都跑不通。
always @(*) 块中,所有分支都必须赋值。要么写全 if-else,要么用 case 加 default。否则综合工具会认为你想保持上一个值,从而生成锁存器。
来看一个正确的组合逻辑例子——4 选 1 多路选择器:
module mux4to1 (
input [1:0] sel,
input [3:0] data_in,
output reg data_out
);
always @(*) begin
case (sel)
2'b00: data_out = data_in[0];
2'b01: data_out = data_in[1];
2'b10: data_out = data_in[2];
2'b11: data_out = data_in[3];
default: data_out = 1'b0; // 必须写
endcase
end
endmodule
2.1.2 时序逻辑:时钟沿才是老大
时序逻辑的核心就一句话:所有寄存器更新,都发生在时钟沿。你想想看,如果两个寄存器之间有一堆组合逻辑,那信号从第一个寄存器传到第二个寄存器,需要时间。这个时间不能超过一个时钟周期。
我曾经调试过一个 SPI 接口,数据老是错位。查了半天,发现是跨时钟域的信号没做同步处理。嗯,这里要注意:跨时钟域信号必须用两级寄存器同步,否则亚稳态会让你怀疑人生。
// 跨时钟域同步器(两级寄存器)
module sync_2ff (
input wire clk_dst,
input wire async_in,
output reg sync_out
);
reg sync_ff1;
always @(posedge clk_dst) begin
sync_ff1 <= async_in;
sync_out <= sync_ff1;
end
endmodule
2.2 有限状态机设计:控制逻辑的灵魂
有限状态机(FSM)是 FPGA 控制逻辑的核心。我个人的经验是:80% 的控制逻辑都可以用状态机来实现。状态机写得好,代码可读性高,调试也方便。
FSM 分两种:
- Moore 型:输出只取决于当前状态
- Mealy 型:输出取决于当前状态 + 输入
实际项目中,我更喜欢用 Moore 型。为什么?因为输出和时钟同步,不容易出毛刺。Mealy 型虽然更灵活,但组合逻辑输出容易产生毛刺,需要额外处理。
2.2.1 三段式状态机写法
我强烈推荐三段式状态机写法。三段式把状态跳转、状态寄存、输出逻辑分开,代码清晰,综合结果也好。
module fsm_example (
input wire clk,
input wire rst_n,
input wire start,
output reg done
);
// 状态编码
localparam IDLE = 2'b00;
localparam WORK = 2'b01;
localparam DONE = 2'b10;
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 = WORK;
WORK: 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
next_state = state 作为默认值,这样 case 里只需要写跳转条件,省去很多 else 分支。
2.3 AXI4-Stream 接口实战:FPGA 与 Zynq 的通信桥梁
AXI4-Stream 是 Xilinx 生态里最常用的高速流式接口。说白了,它就是一个带握手的移位寄存器。数据在时钟沿传输,通过 TVALID 和 TREADY 握手来控制节奏。
我在做图像加速项目时,整个数据通路全是 AXI4-Stream。从 DDR 读出来的像素数据,经过缩放、滤波、色彩转换,最后输出到显示,全部用 Stream 接口串联。调试起来非常方便,因为每个模块的接口都一样。
2.3.1 握手协议:VALID 和 READY
AXI4-Stream 的握手规则很简单:
- 发送方:当数据有效时,拉高
TVALID - 接收方:当准备好接收时,拉高
TREADY - 数据传输:在
TVALID && TREADY同时为高的时钟沿,数据被传输
你想想看,如果接收方还没准备好,发送方就得等。这就是流控。我曾经遇到过一个 bug,接收方 TREADY 一直为低,结果发送方数据堵在 FIFO 里,最后溢出。嗯,这里要注意:一定要确保握手信号正确,否则数据会丢。
// AXI4-Stream 发送端示例
module axis_sender (
input wire clk,
input wire rst_n,
input wire [7:0] data_in,
input wire data_valid,
output reg axis_tvalid,
output reg [7:0] axis_tdata,
input wire axis_tready
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
axis_tvalid <= 1'b0;
axis_tdata <= 8'd0;
end else if (data_valid && axis_tready) begin
axis_tvalid <= 1'b1;
axis_tdata <= data_in;
end else if (axis_tready) begin
axis_tvalid <= 1'b0;
end
end
endmodule
2.3.2 典型应用:数据流处理流水线
AXI4-Stream 最大的优势是模块化。每个处理模块都长一个样:输入 Stream,输出 Stream。你可以像搭积木一样把模块串起来。
下面这张图展示了典型的 Stream 处理流水线:
2.3.3 实战:用 AXI4-Stream 实现数据包检测
最后,咱们来个实战例子。假设你要检测数据流中的特定包头(比如 0xAA),检测到后拉高一个标志信号。用 AXI4-Stream 实现非常简单:
module packet_detector (
input wire clk,
input wire rst_n,
// Slave 接口(输入)
input wire [7:0] s_axis_tdata,
input wire s_axis_tvalid,
output reg s_axis_tready,
// Master 接口(输出)
output reg [7:0] m_axis_tdata,
output reg m_axis_tvalid,
input wire m_axis_tready,
// 检测结果
output reg packet_detected
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
s_axis_tready <= 1'b0;
m_axis_tvalid <= 1'b0;
m_axis_tdata <= 8'd0;
packet_detected <= 1'b0;
end else begin
// 默认准备好接收
s_axis_tready <= 1'b1;
// 当输入有效且输出准备好时,转发数据
if (s_axis_tvalid && m_axis_tready) begin
m_axis_tvalid <= 1'b1;
m_axis_tdata <= s_axis_tdata;
// 检测包头
if (s_axis_tdata == 8'hAA)
packet_detected <= 1'b1;
else
packet_detected <= 1'b0;
end else begin
m_axis_tvalid <= 1'b0;
end
end
end
endmodule