4. Verilog基础回顾(下):always块、时序逻辑与组合逻辑、状态机基础
好,咱们接着聊。上一节我们把Verilog的门面功夫过了一遍,这一节要动真格的了——always块、时序逻辑、组合逻辑,还有状态机。这些是FPGA设计的骨架,也是你写套利信号检测逻辑时,天天要打交道的玩意儿。
我个人习惯把always块比作“FPGA的神经中枢”。你想想看,所有硬件行为,最终都要靠它来描述。但很多新手一上来就乱用,结果综合出来的电路跟自己想的完全两码事。嗯,咱们今天就把这事彻底捋清楚。
核心要点:always块不是软件里的“死循环”,它是硬件描述语言里的一种“敏感列表驱动”的并行执行体。每次敏感列表里的信号发生变化,它就被触发一次。
4.1 always块:组合逻辑 vs 时序逻辑
先看一个最简单的例子。组合逻辑的always块长这样:
// 组合逻辑:用always实现一个2选1多路器
module mux2to1(
input wire a, b, sel,
output reg y
);
always @(*) begin
if (sel)
y = b;
else
y = a;
end
endmodule
注意看,敏感列表写的是 @(*),意思是“所有输入信号变化都触发”。这里我用的是阻塞赋值 =。为什么?因为组合逻辑的输出只依赖当前输入,不需要记忆。
那时序逻辑呢?看这个:
// 时序逻辑:带同步复位的D触发器
module dff_sync(
input wire clk, rst_n, d,
output reg q
);
always @(posedge clk) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
这里敏感列表是 @(posedge clk),只在时钟上升沿触发。赋值用的是非阻塞赋值 <=。这是铁律——时序逻辑用非阻塞,组合逻辑用阻塞。我在项目中遇到过一位同事,把这两个搞反了,仿真怎么跑都对,上板子就乱跳。查了两天才发现是赋值方式的问题。
避坑指南:我曾经在一个高频交易信号处理模块里,因为组合逻辑里用了非阻塞赋值,导致一个简单的加法器变成了流水线寄存器。信号延迟多了一个时钟周期,整个套利策略的时序全乱了。记住:组合逻辑用 = ,时序逻辑用 <=,别问为什么,先养成习惯。
4.2 组合逻辑的两种写法
组合逻辑其实有两种常见写法。一种是 assign 连续赋值,另一种就是上面说的 always @(*)。我个人更倾向于用 always @(*) 写复杂逻辑,因为可读性更好,尤其是带 if-else 或 case 的时候。
举个例子,一个简单的译码器:
// 用always实现3-8译码器
module decoder3to8(
input wire [2:0] in,
output reg [7:0] out
);
always @(*) begin
case (in)
3'b000: out = 8'b0000_0001;
3'b001: out = 8'b0000_0010;
3'b010: out = 8'b0000_0100;
3'b011: out = 8'b0000_1000;
3'b100: out = 8'b0001_0000;
3'b101: out = 8'b0010_0000;
3'b110: out = 8'b0100_0000;
3'b111: out = 8'b1000_0000;
default: out = 8'b0000_0001;
endcase
end
endmodule
你看,这样写是不是很直观?每个输入组合对应一个输出。如果用 assign 写,你得写8条语句,累死。
小技巧:写组合逻辑时,一定要保证所有分支都被覆盖。if 要有 else,case 要有 default。否则综合工具会给你生成一个锁存器(latch),这玩意儿在FPGA里是定时炸弹。我见过有人因为漏了default,结果综合出来的电路多了一堆莫名其妙的latch,功耗和面积都炸了。
4.3 时序逻辑:寄存器与计数器
时序逻辑的核心是寄存器。说白了,就是带记忆的单元。每个时钟沿,它把输入“拍”到输出上。咱们做套利信号检测时,经常需要把信号延迟几个周期来做对齐,这时候寄存器就派上用场了。
看一个简单的移位寄存器:
// 3级移位寄存器,用于信号延迟对齐
module shift_reg3(
input wire clk,
input wire rst_n,
input wire din,
output reg [2:0] dout
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= 3'b0;
else begin
dout[0] <= din;
dout[1] <= dout[0];
dout[2] <= dout[1];
end
end
endmodule
这个模块可以把输入信号延迟3个时钟周期输出。在套利系统里,我们经常用这种结构来对齐不同路径上的数据,确保它们在同一个时钟周期被处理。
计数器也是时序逻辑的经典应用。比如我们做套利信号检测时,需要统计某个事件在固定时间窗口内发生的次数:
// 8位计数器,带使能
module counter8(
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
注意:时序逻辑的敏感列表里,一般只写时钟和异步复位。不要画蛇添足把其他信号加进去,否则综合工具会报错或者生成奇怪的东西。
4.4 状态机基础:三段式写法
状态机是FPGA设计的灵魂。套利信号检测里,从“空闲”到“检测”到“确认”再到“触发”,每一步都需要状态机来管理。我个人强烈推荐三段式状态机写法,结构清晰,不易出错。
三段式是哪三段?
- 第一段:时序逻辑,描述状态跳转(当前状态 → 次态)
- 第二段:组合逻辑,描述次态逻辑(根据输入和当前状态,计算下一个状态)
- 第三段:组合逻辑或时序逻辑,描述输出逻辑
看一个简单的例子:检测串行数据中是否出现“101”序列。
// 三段式状态机:检测"101"序列
module seq_detector(
input wire clk,
input wire rst_n,
input wire din,
output reg dout
);
// 状态编码
localparam IDLE = 2'b00;
localparam S1 = 2'b01;
localparam S10 = 2'b10;
localparam 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: begin
if (din == 1'b1)
next_state = S1;
else
next_state = IDLE;
end
S1: begin
if (din == 1'b0)
next_state = S10;
else
next_state = S1;
end
S10: begin
if (din == 1'b1)
next_state = S101;
else
next_state = IDLE;
end
S101: begin
if (din == 1'b1)
next_state = S1;
else
next_state = IDLE;
end
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑)
always @(*) begin
if (state == S101)
dout = 1'b1;
else
dout = 1'b0;
end
endmodule
你看,三段式把“状态跳转”、“次态计算”、“输出”分得清清楚楚。我在项目中遇到过有人把这三段混在一起写,结果状态机跑飞了,查bug查到怀疑人生。后来我强制团队都用三段式,问题少了一大半。
个人经验:状态编码尽量用独热码(one-hot)或者格雷码。独热码在FPGA里综合效率高,格雷码适合跨时钟域。别用二进制编码,除非你状态数很少。我曾经在一个128状态的状态机里用了二进制编码,结果组合逻辑路径延迟太大,时序收敛不了。换成独热码后,立马过了。
4.5 本章知识体系
下面这张图把本章的核心逻辑串起来了。你看一眼,心里就有谱了。
这张图把本章的三个核心模块串起来了。always块是根,往下分出组合逻辑和时序逻辑,状态机则是两者的综合应用。你写代码时,时刻想着这张图,就不容易跑偏。
最后说一句:Verilog不是写出来的,是“想”出来的。你写每一行代码前,脑子里要先有电路图。always块对应什么硬件?赋值语句对应什么连线?状态机对应什么状态寄存器?想清楚了再动键盘,事半功倍。
公众号:蓝海资料掘金营,微信deep3321