Verilog有限状态机(7)

HDLBits链接


前言

今天我们继续学习状态机的题目,这组题目类型比较丰富,包括:

  1. 带计数器的状态机:统计一段时间内输入为1的次数
  2. 状态转移逻辑推导:给状态转移表,写逻辑表达式
  3. 独热编码状态机:利用独热编码的特性简化逻辑

通过这组题目,你能更全面地掌握状态机设计的各种技巧!


题库

Q3a: FSM - 带计数检测的状态机

1
2

题目理解

这个题目有点意思,让我们仔细分析一下规则:

  1. 状态切换:当s为0时,保持在S0;当s为1时,进入S1状态
  2. 计数窗口:进入S1后,我们要检查连续3个周期内的w
  3. 判断条件:如果这3个周期中恰好有2个周期w=1,则z=1,否则z=0
  4. 不重叠检测:3个周期为一组,不重叠

这就像老师检查学生的作业:连续3次作业中,如果刚好2次得优,就给奖励!

Solution:

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
module top_module (
input wire clk,
input wire reset, // 同步复位
input wire s, // 启动信号
input wire w, // 数据输入
output reg z // 检测结果
);

// 状态参数定义
parameter S0 = 1'd0;
parameter S1 = 1'd1;

reg current_state;
reg next_state;
reg [1:0] counter; // 计数器:0-2,表示3个周期中的第几个
reg [1:0] num_one; // 统计3个周期中w=1的次数

// 第一段:组合逻辑,状态转移
always @(*) begin
case (current_state)
S0: next_state = s ? S1 : S0;
S1: next_state = S1; // 一旦进入S1就不回去了
default: next_state = S0;
endcase
end

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

// 第三段:计数器逻辑
always @(posedge clk) begin
if (reset) begin
counter <= 2'd0;
end else if (counter == 2'd2) begin
counter <= 2'd0; // 数到2后回到0,开始下一组
end else if (current_state == S1) begin
counter <= counter + 1'd1; // 在S1状态时计数
end
end

// 第四段:统计w=1的次数
always @(posedge clk) begin
if (reset) begin
num_one <= 1'd0;
end else begin
if (counter == 2'd0) begin
num_one <= w ? 1'd1 : 1'd0; // 新的一组开始
end else if (current_state == S1) begin
num_one <= w ? (num_one + 1'd1) : num_one; // 累加
end
end
end

// 输出逻辑:在counter回到0时,检查上一组的num_one是否为2
assign z = (current_state == S1) && (num_one == 2'd2) && (counter == 2'd0);

endmodule

设计要点说明

为什么counter==2’d0时检查?

  • 因为counter从0→1→2→0,刚好3个周期
  • 当counter回到0时,我们已经统计完了上一组的3个周期

注意num_one的清零时机

  • 在counter==2’d0时,num_one重新开始计数
  • 这样可以保证每组都是独立统计的

Q3b: FSM - 看图写状态机

3

题目理解

这是一个”看图说话”的题目,给了状态转移图,我们直接照着写就行。唯一需要注意的是:输出逻辑用的是next_state而不是state

Solution:

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
module top_module (
input wire clk,
input wire reset, // 同步复位
input wire x,
output reg z
);

// 状态参数定义
parameter S0 = 3'b000;
parameter S1 = 3'b001;
parameter S2 = 3'b010;
parameter S3 = 3'b011;
parameter S4 = 3'b100;

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

// 第一段:组合逻辑,状态转移
always @(*) begin
case (current_state)
S0: next_state = x ? S1 : S0;
S1: next_state = x ? S4 : S1;
S2: next_state = x ? S1 : S2;
S3: next_state = x ? S2 : S1;
S4: next_state = x ? S4 : S3;
default: next_state = S0;
endcase
end

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

// 第三段:输出逻辑(注意:用next_state!)
always @(posedge clk) begin
if (reset) begin
z <= 1'b0;
end else begin
case (next_state)
S3: z <= 1'b1;
S4: z <= 1'b1;
default:z <= 1'b0;
endcase
end
end

endmodule

重要心得

题目中特意提醒了:当输出逻辑用到next_state时,记得要把reset条件加上!

为什么?因为:

  • reset只影响current_state,不直接影响next_state
  • 如果不加reset条件,复位时输出可能不确定

Q3c: FSM logic - 状态转移表推导逻辑

4

题目理解

这道题给了状态转移表,让我们直接推导:

  • Y0:下一状态的第0位
  • z:输出

这种题目不需要写完整的状态机,只需要根据表格写逻辑表达式就行。

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module (
input wire clk,
input wire [2:0] y, // 当前状态
input wire x,
output reg Y0, // 下一状态的第0位
output reg z // 输出
);

// 根据状态转移表直接写逻辑表达式
assign Y0 = ((~y[2] & y[0]) | (y == 3'b100)) & ~x | (~y[2] & ~y[0]) & x;
assign z = (y == 3'b011) | (y == 3'b100);

endmodule

Q6b: FSM next-state logic - 独热编码前的逻辑推导

5
6

题目理解

同样是给状态转移表,推导下一状态的Y2位。我们只需要:

  1. 找出所有下一状态Y2=1的情况
  2. 把这些情况用逻辑表达式表示出来

Solution:

1
2
3
4
5
6
7
8
9
10
11
module top_module (
input wire [3:1] y, // 当前状态
input wire w,
output reg Y2 // 下一状态的Y2位
);

// 按照状态转移表,列出所有Y2=1的情况
assign Y2 = ((y == 3'b001) | (y == 3'b101)) & ~w |
((y == 3'b001) | (y == 3'b010) | (y == 3'b100) | (y == 3'b101)) & w;

endmodule

Q6c: FSM one-hot next-state logic - 独热编码逻辑

题目理解

这道题使用独热编码,需要输出Y2Y4

独热编码的好处就在这里体现出来了!因为每个状态对应独立的一位,我们不需要复杂的比较,直接看对应的位就行!

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input wire [6:1] y, // 独热编码的当前状态
input wire w,
output reg Y2,
output reg Y4
);

// 独热编码就是简单!直接看对应的位
assign Y2 = ~w & y[1];
assign Y4 = (w & y[2]) | (w & y[3]) | (w & y[5]) | (w & y[6]);

endmodule

对比一下

对比Q6b和Q6c:

  • Q6b(二进制编码):需要比较y等于多少
  • Q6c(独热编码):直接看y的某一位

这就是独热编码的优势!虽然用了更多寄存器,但组合逻辑更简单,速度更快。


Q6: FSM - 看图写状态机(另一版本)

7

题目理解

又是一道”看图说话”的题目,和Q3b类似,照着状态转移图写就行。

Solution:

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
module top_module (
input wire clk,
input wire reset, // 同步复位
input wire w,
output reg z
);

// 状态参数定义
parameter A = 3'd0;
parameter B = 3'd1;
parameter C = 3'd2;
parameter D = 3'd3;
parameter E = 3'd4;
parameter F = 3'd5;

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

// 第一段:组合逻辑,状态转移
always @(*) begin
case (current_state)
A: next_state = w ? A : B;
B: next_state = w ? D : C;
C: next_state = w ? D : E;
D: next_state = w ? A : F;
E: next_state = w ? D : E;
F: next_state = w ? D : C;
default:next_state = A;
endcase
end

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

// 第三段:输出逻辑
assign z = (current_state == E) | (current_state == F);

endmodule

入门者避坑指南

在做这组题目时,初学者最容易犯以下错误:

错误1:计数器和统计逻辑的时序不对

错误表现:

1
2
3
4
5
6
7
8
9
// 统计num_one时,没有考虑counter的时机
always @(posedge clk) begin
if (reset) begin
num_one <= 1'd0;
end else if (current_state == S1) begin
num_one <= w ? (num_one + 1'd1) : num_one;
end
// 忘记了counter==0时要清零!
end

错误原因:

  • 每组3个周期统计完后,num_one应该重新开始
  • 如果不清零,下一组的统计会累加上一组的值

正确做法:

1
2
3
4
5
6
7
8
9
10
11
always @(posedge clk) begin
if (reset) begin
num_one <= 1'd0;
end else begin
if (counter == 2'd0) begin
num_one <= w ? 1'd1 : 1'd0; // 新的一组开始
end else if (current_state == S1) begin
num_one <= w ? (num_one + 1'd1) : num_one;
end
end
end

错误2:输出逻辑漏掉counter判断

错误表现:

1
2
// 只要num_one==2就输出,不管是不是在正确的时机
assign z = (current_state == S1) && (num_one == 2'd2);

错误原因:

  • 应该在一组(3个周期)结束时才判断
  • 也就是counter回到0的时候

正确做法:

1
2
// 加上counter==2'd0的判断
assign z = (current_state == S1) && (num_one == 2'd2) && (counter == 2'd0);

错误3:用next_state做输出时忘记reset

错误表现:

1
2
3
4
5
6
7
8
// 输出逻辑用next_state,但没有reset条件
always @(posedge clk) begin
case (next_state)
S3: z <= 1'b1;
S4: z <= 1'b1;
default: z <= 1'b0;
endcase
end

错误原因:

  • reset只影响current_state
  • 如果不加reset条件,复位时输出可能不正确

正确做法:

1
2
3
4
5
6
7
8
9
10
11
always @(posedge clk) begin
if (reset) begin
z <= 1'b0; // 记得复位时清零
end else begin
case (next_state)
S3: z <= 1'b1;
S4: z <= 1'b1;
default: z <= 1'b0;
endcase
end
end

错误4:独热编码状态机用case语句

错误表现:

1
2
3
4
5
6
7
8
// 独热编码却用case(y)
always @(*) begin
case (y)
6'b000001: Y2 = 1;
6'b000010: Y2 = 0;
// ... 要写很多case!
endcase
end

错误原因:

  • 独热编码的优势就是不需要用case
  • 直接看对应的位就行

正确做法:

1
2
3
// 独热编码直接看位
assign Y2 = ~w & y[1];
assign Y4 = (w & y[2]) | (w & y[3]) | (w & y[5]) | (w & y[6]);

错误5:逻辑表达式中常数没有指定位宽

错误表现:

1
assign Y0 = ((~y[2] & y[0]) | (y == 100)) & ~x;  // 100是十进制!

错误原因:

  • 没有指定位宽和进制,容易出错
  • y是3位二进制,应该写成3'b100

正确做法:

1
assign Y0 = ((~y[2] & y[0]) | (y == 3'b100)) & ~x;


小结

今天我们学习了多种类型的状态机题目,主要收获有:

  1. 状态机+计数器:当需要统计一段时间内的数据时,可以用状态机控制计数器的启停和清零,用计数器记录周期数或特定事件的次数

  2. 输出逻辑的时机:要注意是用state还是next_state,如果用next_state,记得加上reset条件

  3. 状态转移表推导逻辑:给表格写表达式时,要仔细找出所有满足条件的情况,然后用逻辑或拼起来

  4. 独热编码的优势:独热编码虽然用了更多寄存器,但组合逻辑更简单,直接看对应的位就行,不需要复杂的比较

作为一名通信IC设计师,笔者想说:这组题目非常全面地涵盖了状态机设计的各种场景!从”看图说话”的基础题,到需要配合计数器的综合题,再到只写逻辑表达式的推导题。把这些题都吃透,你对状态机的理解就很扎实了!