3. Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、阻塞与非阻塞赋值、状态机设计
各位同学,欢迎来到第三章。说实话,很多讲Verilog的课一上来就讲语法,我总觉得少了点灵魂。咱们做低延迟交易的,代码写出来是要在FPGA上跑纳秒级竞争的,所以这一章我打算换个讲法——从实际设计的角度,把那些最基础、也最容易翻车的地方再过一遍。
你可能会问:“这些基础我早就会了,还用听吗?”嗯,我在面试新人时发现,很多人能把语法背得滚瓜烂熟,但一写代码就踩坑。尤其是阻塞赋值和非阻塞赋值的区别,我见过太多老手在这上面栽跟头。所以,咱们还是老老实实把地基打牢。
3.1 模块化设计——把大问题拆成小积木
模块化设计,说白了就是“分而治之”。一个复杂的交易引擎,你不可能在一个module里写完所有逻辑。我个人的习惯是:每个模块只干一件事,接口尽量简单,像搭积木一样拼起来。
举个例子,一个简单的UDP接收模块,我会拆成这样:
// 顶层模块
module udp_receiver_top (
input wire clk,
input wire rst_n,
input wire [7:0] rx_data,
input wire rx_valid,
output reg [63:0] payload_data,
output reg payload_valid
);
// 例化子模块
wire [31:0] header_info;
wire [63:0] data_out;
wire data_valid;
mac_decoder u_mac (
.clk(clk),
.rst_n(rst_n),
.rx_data(rx_data),
.rx_valid(rx_valid),
.header_info(header_info),
.data_out(data_out),
.data_valid(data_valid)
);
payload_extractor u_payload (
.clk(clk),
.rst_n(rst_n),
.header_info(header_info),
.data_in(data_out),
.data_valid(data_valid),
.payload_data(payload_data),
.payload_valid(payload_valid)
);
endmodule
3.2 组合逻辑与时序逻辑——两个世界
很多新手分不清什么时候用组合逻辑,什么时候用时序逻辑。其实很简单:
- 组合逻辑:输出只取决于当前输入,没有记忆功能。用
always @(*)或assign实现。 - 时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。用
always @(posedge clk)实现。
在低延迟交易中,我倾向于尽量多用组合逻辑——少一级寄存器就少一个时钟周期的延迟。但代价是组合逻辑容易产生毛刺,而且时序收敛更难。
// 组合逻辑示例:地址译码器
always @(*) begin
case (addr[3:2])
2'b00: sel = 4'b0001;
2'b01: sel = 4'b0010;
2'b10: sel = 4'b0100;
2'b11: sel = 4'b1000;
default: sel = 4'b0000;
endcase
end
// 时序逻辑示例:寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else if (en)
count <= count + 8'd1;
end
always @(*) 代替手动列敏感列表,省心多了。
3.3 阻塞与非阻塞赋值——最容易翻车的坑
这个知识点,我敢说90%的Verilog初学者都搞混过。咱们直接上对比:
| 特性 | 阻塞赋值 = |
非阻塞赋值 <= |
|---|---|---|
| 执行顺序 | 顺序执行,阻塞后续语句 | 并行执行,不阻塞后续语句 |
| 使用场景 | 组合逻辑(always @(*)) |
时序逻辑(always @(posedge clk)) |
| 仿真行为 | 立即更新 | 在时间步结束时更新 |
| 综合结果 | 组合逻辑或锁存器 | 触发器 |
记住一个黄金法则:写时序逻辑用非阻塞赋值,写组合逻辑用阻塞赋值。混用的话,仿真和综合结果可能不一致。
我曾经在一个项目中,同事在时序逻辑里用了阻塞赋值,结果仿真通过,但综合出来的电路多了一级不必要的延迟。在低延迟交易中,这一级延迟可能就是几百万的损失。
// 错误示范:时序逻辑中用阻塞赋值
always @(posedge clk) begin
a = b; // 阻塞赋值
c = a; // 这里a已经更新,c得到的是新值
end
// 综合后:a和c在同一时钟沿更新,但仿真行为可能让人误解
// 正确做法:时序逻辑用非阻塞赋值
always @(posedge clk) begin
a <= b; // 非阻塞赋值
c <= a; // 这里a还是旧值,c得到的是旧a
end
// 综合后:a和c都是寄存器,行为清晰
3.4 状态机设计——交易逻辑的核心
状态机是FPGA设计的灵魂。在低延迟交易中,我们常用的是三段式状态机:
- 第一段:时序逻辑,描述状态转移
- 第二段:组合逻辑,描述下一状态逻辑
- 第三段:时序逻辑或组合逻辑,描述输出
我个人强烈推荐三段式,因为它把状态转移和输出逻辑分开了,代码清晰,也容易做时序优化。
// 三段式状态机示例:简单的UDP包处理
localparam IDLE = 2'b00;
localparam HEADER = 2'b01;
localparam PAYLOAD = 2'b10;
localparam CHECKSUM = 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: begin
if (rx_valid)
next_state = HEADER;
end
HEADER: begin
if (header_done)
next_state = PAYLOAD;
end
PAYLOAD: begin
if (payload_done)
next_state = CHECKSUM;
end
CHECKSUM: begin
if (checksum_ok)
next_state = IDLE;
else
next_state = IDLE; // 错误处理
end
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_valid <= 1'b0;
error_flag <= 1'b0;
end else begin
case (state)
PAYLOAD: data_valid <= 1'b1;
CHECKSUM: begin
if (!checksum_ok)
error_flag <= 1'b1;
end
default: begin
data_valid <= 1'b0;
error_flag <= 1'b0;
end
endcase
end
end
嗯,状态机的设计还有一个要点:一定要有default分支。FPGA上电后状态是不确定的,如果没有default,状态机可能跑到非法状态里出不来。我见过一个案例,就是因为漏了default,导致系统偶尔死机,重启才能恢复。
好了,这一章的内容就到这里。基础的东西虽然简单,但越是基础越容易出错。下一章我们会进入更实战的内容——如何用Verilog实现一个低延迟的FIFO。到时候见。
公众号:蓝海资料掘金营,微信deep3321