Verilog有限状态机(2)

HDLBits链接


前言

在有限状态机的设计中,状态编码方式的选择是一个非常重要的话题。今天我们继续学习状态机的几道经典习题,重点掌握独热编码(One-hot encoding)的设计方法,以及同步复位与异步复位的区别。这些都是实际工程中经常遇到的问题。


题库

题目6:One-hot FSM 组合逻辑实现

1

基础知识:状态编码方式

在深入题目之前,我们先了解一下常见的三种状态编码方式,它们各有优缺点:

二进制编码(Binary Encoding)

  • 状态数为N时,使用log₂(N)位
  • 优点:使用最少的寄存器资源
  • 缺点:组合逻辑通常更复杂,多个位同时变化可能产生毛刺

格雷码(Gray Code)

  • 相邻状态之间只有1位发生变化
  • 优点:减少功耗,降低毛刺产生的概率
  • 缺点:仅适用于状态按顺序转移的场景

独热编码(One-hot Encoding)

  • 每个状态使用独立的一位,任意时刻只有一位为1
  • 优点:状态判断简单(只需检查一位),组合逻辑更简单,速度更快
  • 缺点:使用更多的寄存器资源(状态数N就需要N位)

这就像给每个人配一把专用钥匙(独热编码),还是用几把钥匙的组合(二进制编码)。前者开锁快但钥匙多,后者钥匙少但开锁需要尝试组合。

题目理解

本题要求使用独热编码的方式来实现状态转移逻辑。注意,这里的状态已经是独热编码的输入了,我们只需要推导组合逻辑方程即可。

Solution6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module top_module(
input wire in,
input wire [3:0] state,
output reg [3:0] next_state,
output reg out
);

// 状态参数定义(独热编码)
parameter A = 4'd0;
parameter B = 4'd1;
parameter C = 4'd2;
parameter D = 4'd3;

// 状态转移逻辑:使用独热编码特性,判断简单
always @(*) begin
// 默认保持原状态
next_state = 4'b0000;

// 下一状态A:当前在A且输入为0,或当前在C且输入为0
next_state[A] = (state[A] & ~in) | (state[C] & ~in);

// 下一状态B:当前在A且输入为1,或当前在D且输入为1,或当前在B且输入为1
next_state[B] = (state[A] & in) | (state[D] & in) | (state[B] & in);

// 下一状态C:当前在B且输入为0,或当前在D且输入为0
next_state[C] = (state[B] & ~in) | (state[D] & ~in);

// 下一状态D:当前在C且输入为1
next_state[D] = (state[C] & in);
end

// 输出逻辑:仅在状态D时输出1
assign out = state[D];

endmodule

本题要点

为什么独热编码可以这样写?

  • 因为独热编码中,每个状态位是独立的
  • 我们可以单独推导每一位的逻辑表达式
  • 这样写的好处是代码可读性好,综合器优化效果也不错

题目7:异步复位的完整状态机

题目与上题相同,但这次我们要实现一个完整的状态机,包括状态寄存器,并且使用异步复位,复位到状态A。

2

基础知识:异步复位 vs 同步复位

异步复位

  • 复位信号独立于时钟,只要复位信号有效,立即复位
  • 优点:响应快,不依赖时钟,有些FPGA的触发器有专用异步复位端
  • 缺点:复位信号的毛刺可能导致误复位,复位释放时需要考虑同步问题

同步复位

  • 复位信号只在时钟边沿有效时才起作用
  • 优点:可以过滤掉复位信号上的毛刺,整个系统更同步
  • 缺点:复位必须等待时钟边沿,可能需要更多逻辑资源

这就像电梯的紧急停止按钮:异步复位是按下立即停,同步复位是等到下一层才停。

Solution7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
module top_module(
input wire clk,
input wire in,
input wire areset, // 异步复位,高电平有效
output reg out
);

// 状态参数定义(二进制编码)
parameter A = 2'd0;
parameter B = 2'd1;
parameter C = 2'd2;
parameter D = 2'd3;

reg [1:0] state;
reg [1:0] next_state;

// 第一段:组合逻辑,状态转移
always @(*) begin
case (state)
A: begin
next_state = (in == 1'b0) ? A : B;
end
B: begin
next_state = (in == 1'b0) ? C : B;
end
C: begin
next_state = (in == 1'b0) ? A : D;
end
D: begin
next_state = (in == 1'b0) ? C : B;
end
default: begin
next_state = A;
end
endcase
end

// 第二段:时序逻辑,状态更新(异步复位)
always @(posedge clk or posedge areset) begin
if (areset) begin
state <= A; // 复位到状态A
end else begin
state <= next_state;
end
end

// 第三段:输出逻辑
assign out = (state == D);

endmodule

代码结构说明

这里我们使用了经典的三段式状态机写法:

  1. 组合逻辑:根据当前状态和输入计算下一状态
  2. 时序逻辑:在时钟边沿更新状态(含复位逻辑)
  3. 输出逻辑:根据当前状态产生输出

题目8:同步复位的完整状态机

题目同上题,但将复位改为同步复位

3

Solution8:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
module top_module(
input wire clk,
input wire in,
input wire reset, // 同步复位,高电平有效
output reg out
);

// 状态参数定义
parameter A = 2'd0;
parameter B = 2'd1;
parameter C = 2'd2;
parameter D = 2'd3;

reg [1:0] state;
reg [1:0] next_state;

// 第一段:组合逻辑,状态转移
always @(*) begin
case (state)
A: begin
next_state = (in == 1'b0) ? A : B;
end
B: begin
next_state = (in == 1'b0) ? C : B;
end
C: begin
next_state = (in == 1'b0) ? A : D;
end
D: begin
next_state = (in == 1'b0) ? C : B;
end
default: begin
next_state = A;
end
endcase
end

// 第二段:时序逻辑,状态更新(同步复位)
always @(posedge clk) begin
if (reset) begin
state <= A; // 同步复位到状态A
end else begin
state <= next_state;
end
end

// 第三段:输出逻辑
assign out = (state == D);

endmodule

同步复位与异步复位的代码区别

注意看always块的敏感信号列表:

  • 异步复位:always @(posedge clk or posedge areset)
  • 同步复位:always @(posedge clk)

这就是最关键的区别!同步复位只在时钟上升沿时才检查复位信号。


题目9:优先级编码器状态机

4

题目理解

这是一个典型的优先级编码器状态机设计问题。想象一个有多个用户请求使用资源的场景,不同用户有不同的优先级,高优先级用户可以抢占资源(或者在资源释放后优先获得)。

从题目可以看出,这是一个类似水位控制的状态机:

  • s[3]优先级最高,s[2]次之,s[1]最低
  • 状态A2、B1、B2、C1、C2、D1对应不同的水位
  • 输出fr3、fr2、fr1表示哪些资源可用
  • dfr表示方向(水位是上升还是下降)

Solution9:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
module top_module (
input wire clk,
input wire reset,
input wire [3:1] s, // 传感器输入
output reg fr3, // 水位3可用
output reg fr2, // 水位2可用
output reg fr1, // 水位1可用
output reg dfr // 方向标志
);

// 状态参数定义
parameter A2 = 3'd0;
parameter B1 = 3'd1;
parameter B2 = 3'd2;
parameter C1 = 3'd3;
parameter C2 = 3'd4;
parameter D1 = 3'd5;

reg [2:0] state;
reg [2:0] next_state;

// 第一段:组合逻辑,状态转移
always @(*) begin
case (state)
A2: begin
next_state = s[1] ? B1 : A2;
end
B1: begin
next_state = s[2] ? C1 : (s[1] ? B1 : A2);
end
B2: begin
next_state = s[2] ? C1 : (s[1] ? B2 : A2);
end
C1: begin
next_state = s[3] ? D1 : (s[2] ? C1 : B2);
end
C2: begin
next_state = s[3] ? D1 : (s[2] ? C2 : B2);
end
D1: begin
next_state = s[3] ? D1 : C2;
end
default: begin
next_state = A2;
end
endcase
end

// 第二段:时序逻辑,状态更新
always @(posedge clk) begin
if (reset) begin
state <= A2;
end else begin
state <= next_state;
end
end

// 第三段:输出逻辑
always @(*) begin
case (state)
A2: {fr3, fr2, fr1, dfr} = 4'b1111;
B1: {fr3, fr2, fr1, dfr} = 4'b0110;
B2: {fr3, fr2, fr1, dfr} = 4'b0111;
C1: {fr3, fr2, fr1, dfr} = 4'b0010;
C2: {fr3, fr2, fr1, dfr} = 4'b0011;
D1: {fr3, fr2, fr1, dfr} = 4'b0000;
default: {fr3, fr2, fr1, dfr} = 4'bxxxx;
endcase
end

endmodule

输出逻辑说明

使用位拼接操作{}可以让输出逻辑更简洁。这种写法在状态机输出较多时非常方便。


入门者避坑指南

在做这几道题的过程中,初学者经常会犯以下几个错误:

错误1:组合逻辑中使用阻塞赋值给reg

错误表现:

1
2
3
4
5
6
always @(*) begin
state = A; // 错误:直接给状态寄存器赋值
if (in) begin
state = B;
end
end

错误原因:

  • 组合逻辑中不应该直接修改状态寄存器
  • 状态更新应该在时序逻辑(always @(posedge clk))中完成
  • 应该先用next_state过渡

正确做法:

1
2
3
4
5
6
7
8
9
10
always @(*) begin
next_state = A; // 先给next_state赋值
if (in) begin
next_state = B;
end
end

always @(posedge clk) begin
state <= next_state; // 时序逻辑中更新状态
end

错误2:异步复位和同步复位混用敏感信号

错误表现:

1
2
3
4
5
6
7
8
// 同步复位却写成异步复位的敏感信号
always @(posedge clk or posedge reset) begin
if (reset) begin
state <= A;
end else begin
state <= next_state;
end
end

错误原因:

  • 同步复位不需要把reset放入敏感信号列表
  • 这样写会让复位变成异步的

正确做法:

1
2
3
4
5
6
7
8
// 同步复位的正确写法
always @(posedge clk) begin
if (reset) begin
state <= A;
end else begin
state <= next_state;
end
end

错误3:组合逻辑没有default分支

错误表现:

1
2
3
4
5
6
7
8
always @(*) begin
case (state)
A: next_state = in ? B : A;
B: next_state = in ? B : C;
C: next_state = in ? D : A;
// 缺少default分支!
endcase
end

错误原因:

  • 如果状态不是A、B、C,next_state会保持之前的值
  • 这会生成锁存器(latch),在时序电路中是不希望看到的

正确做法:

1
2
3
4
5
6
7
8
always @(*) begin
case (state)
A: next_state = in ? B : A;
B: next_state = in ? B : C;
C: next_state = in ? D : A;
default: next_state = A; // 加上default分支
endcase
end

或者更安全的做法,在case语句前先给next_state一个默认值:

1
2
3
4
5
6
7
8
9
always @(*) begin
next_state = A; // 先给默认值
case (state)
A: next_state = in ? B : A;
B: next_state = in ? B : C;
C: next_state = in ? D : A;
default: next_state = A;
endcase
end

错误4:参数未指定位宽

错误表现:

1
2
parameter A = 0;  // 没有指定位宽
parameter B = 1;

错误原因:

  • 未指定位宽的参数默认是32位
  • 在状态机中通常只需要几位就够了
  • 虽然综合器可能会优化,但指定位宽是更好的代码习惯

正确做法:

1
2
parameter A = 2'd0;  // 指定2位宽度,值为0
parameter B = 2'd1;

调试技巧

  1. 画状态转移图:在写代码前先画好状态转移图,标清楚每个状态的输入输出
  2. 分模块测试:先测试状态转移是否正确,再测试输出逻辑
  3. 使用波形图:通过仿真工具查看波形,特别是在状态切换的时刻
  4. 打印信息:在仿真时可以用$display打印状态变化,帮助调试

小结

今天我们学习了四道经典的状态机题目,重点掌握了:

  1. 三种状态编码方式:二进制编码、格雷码、独热编码,以及各自的优缺点和适用场景
  2. 异步复位与同步复位:它们的区别和代码写法
  3. 三段式状态机结构:组合逻辑、时序逻辑、输出逻辑分离,这是工程中推荐的写法
  4. 优先级编码器:一个实际的状态机应用案例

状态机是数字电路设计中非常重要的工具,多练习这些经典题目会让你收获很大!