4、Verilog实现基础:always块与组合逻辑、时序逻辑的写法、三段式状态机模板
好,咱们今天聊点实在的。Verilog 里最核心的东西是什么?我个人觉得,就是 always 块。你想想看,整个硬件描述语言,说白了就是靠 always 来告诉综合工具:这里要生成一堆门,那里要生成一堆触发器。
很多新手一上来就被各种阻塞赋值、非阻塞赋值搞晕了。嗯,别急,咱们一步步拆开看。
4.1 always块:硬件描述的灵魂
always 块就像是一个永不停止的循环。只要敏感列表里的信号发生变化,它就执行一次。我刚开始学的时候,总把它当成软件里的 while(1),后来发现完全不是一回事——硬件里它是并行执行的,不是顺序执行的。
这里有个关键点:always 块的敏感列表决定了它是组合逻辑还是时序逻辑。
- 组合逻辑:敏感列表里只放电平信号,比如
always @(*)或always @(a or b)。 - 时序逻辑:敏感列表里放时钟边沿,比如
always @(posedge clk)或always @(negedge rst_n)。
重要提醒:千万别把时钟和电平混在一个敏感列表里,除非你真的知道自己在干什么。我曾经见过一个同事,把 always @(posedge clk or a) 写出来,结果综合出来的电路完全不是他想要的样子——那叫一个惨。
4.2 组合逻辑的写法
组合逻辑,说白了就是没有记忆功能的电路。输入一变,输出立马跟着变。在 Verilog 里,我们通常用两种方式写组合逻辑:
方式一:assign 语句
适合简单的赋值,比如:
assign y = a & b | c;
方式二:always @(*) 块
适合复杂的逻辑,比如多分支判断:
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = 0;
endcase
end
注意,组合逻辑里必须用阻塞赋值(=)。为什么?因为阻塞赋值是立即生效的,符合组合逻辑的“即时响应”特性。如果你在组合逻辑里用了非阻塞赋值(<=),仿真时可能会对,但综合出来的电路大概率会多出一些奇怪的锁存器。
我的小技巧:写组合逻辑时,我习惯在 always @(*) 块里把所有输出都赋一遍默认值,比如 y = 0;。这样可以避免综合出锁存器。你想想看,如果某个分支没覆盖到,综合工具就会认为“保持原值”,那就给你生成一个 latch——这往往不是我们想要的。
4.3 时序逻辑的写法
时序逻辑就带记忆功能了。它靠时钟边沿来触发,把输入的值“拍”到输出上。最经典的例子就是 D 触发器:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 0;
else
q <= d;
end
这里有几个要点:
- 必须用非阻塞赋值(
<=)。这是硬件描述语言的铁律——时序逻辑里用非阻塞赋值,可以避免仿真时的竞争冒险。 - 异步复位一般用低电平有效(
negedge rst_n),这是业界惯例。 - 敏感列表里,时钟和复位要分开写,不要写成
posedge clk or posedge rst,除非你用的是高电平复位。
我记得有一次做项目,一个同事在时序逻辑里用了阻塞赋值,仿真时一切正常,结果上板调试时发现数据总是慢一拍。查了两天才发现是赋值方式的问题——从那以后,我对这个规则就特别敏感。
4.4 三段式状态机模板
状态机是数字电路里的常客。我个人最推荐的是三段式状态机。为什么叫三段式?因为它把状态机的三个功能分到了三个 always 块里:
- 第一段:时序逻辑,描述状态寄存器的跳转。
- 第二段:组合逻辑,描述下一状态的逻辑。
- 第三段:组合逻辑或时序逻辑,描述输出。
下面是一个经典的三段式状态机模板:
// 状态定义
parameter IDLE = 2'b00;
parameter S1 = 2'b01;
parameter S2 = 2'b10;
parameter S3 = 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
case (state)
IDLE: begin
if (start)
next_state = S1;
else
next_state = IDLE;
end
S1: begin
if (done)
next_state = S2;
else
next_state = S1;
end
S2: begin
if (error)
next_state = IDLE;
else
next_state = S3;
end
S3: begin
next_state = IDLE;
end
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑)
always @(*) begin
// 默认输出
out1 = 0;
out2 = 0;
case (state)
S1: out1 = 1;
S2: out2 = 1;
S3: begin
out1 = 1;
out2 = 1;
end
endcase
end
避坑指南:我曾经在第三段输出逻辑里用了时序逻辑(always @(posedge clk)),结果输出总是晚一个时钟周期。后来才意识到,状态机的输出应该是组合逻辑,这样才能在当前状态立即生效。除非你特意需要打一拍,否则别在输出段加时钟。
4.5 知识体系总览
下面这张图是我自己画的,把本章的核心逻辑串起来了。你看一眼,应该就能明白 always 块、组合逻辑、时序逻辑、三段式状态机之间的关系。
从这张图可以看得很清楚:always 块是根,往下分出组合逻辑和时序逻辑两大分支。而三段式状态机,其实就是把这两种逻辑巧妙地组合在一起——第一段用时序逻辑存状态,第二段用组合逻辑算下一状态,第三段用组合逻辑出结果。
嗯,这套模板我用了快十年了,几乎所有的控制逻辑都能套用。你只要把状态定义和跳转条件换一换,就能适配各种场景。说白了,这就是硬件工程师的“万能钥匙”。
最后说一句:刚开始写三段式的时候,你可能会觉得代码有点啰嗦。但相信我,等你要调试一个复杂的状态机时,这种清晰的分离会让你少掉很多头发。我当年就是靠这个模板,三天调通了一个 16 状态的通信协议——要是用两段式,估计得一周。
好了,这一章的内容就到这儿。记住:always 块是硬件描述的灵魂,组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值,三段式状态机是工程实践中的最佳选择。把这些基础打牢,后面的路就好走了。