基于有限状态机的计数器

HDLBits链接


前言

欢迎来到HDLBits系列的第22篇!今天我们要学习的内容非常有意思——基于有限状态机(FSM)的计数器。这是数字电路设计中非常经典的组合,在实际工程中应用非常广泛。

为什么我们需要把FSM和计数器结合起来呢?想象一下,如果你要设计一个自动洗衣机的控制器:它需要先注水,然后洗涤,再漂洗,最后脱水。每个阶段都需要持续特定的时间,这时候就需要用计数器来计时,用FSM来管理不同的工作阶段。

在这一章里,我们会从简单的计数器开始,逐步过渡到复杂的FSM设计,最后把它们组合成一个完整的系统。让我们开始吧!


题库

Counter with period 1000

题目理解

首先我们来看一个基础的计数器题目:构造一个0-999的计数器,具有同步高电平复位功能。

这个题目看似简单,但其实包含了计数器设计的几个核心概念:

  1. 同步复位:复位信号只在时钟上升沿时生效,这是时序电路设计的最佳实践
  2. 周期计数:从0数到999,总共1000个状态,然后回到0重新开始
  3. 位宽计算:1000需要10位二进制数来表示(2^10=1024)

生活实例类比:这个计数器就像一个三位数的机械里程表,从000走到999后,会自动归零重新开始。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module top_module (
input clk, // 时钟信号
input reset, // 同步高电平复位信号
output [9:0] q // 10位计数器输出
);

// 时钟上升沿触发的时序逻辑
always @(posedge clk) begin
if (reset) begin
// 复位时计数器归零,注意指定位宽
q <= 10'd0;
end
else if (q == 10'd999) begin
// 计数到999时,下一个周期回到0
q <= 10'd0;
end
else begin
// 正常情况下计数器加1
q <= q + 10'd1;
end
end

endmodule

要点小结

  • 同步复位比异步复位更稳定,因为它避免了复位信号在时钟有效沿之外变化可能带来的亚稳态问题
  • 所有常量都指定位宽(如10'd0而不是0),这是良好的代码规范
  • 使用<=进行非阻塞赋值,这是时序逻辑的标准做法

4-bit shift register and down counter

题目理解

这道题稍微复杂一点:我们需要构造一个既可以作为4位移位寄存器,又可以作为倒数计数器的电路。

功能说明:

  • shift_ena为1时,数据从data输入,高位先进入移位寄存器
  • count_ena为1时,计数器从当前值开始逐时钟递减
  • 题目明确说明shift_enacount_ena不会同时使能,所以不需要考虑优先级

生活实例类比:这就像一个两用的数字保险箱:你可以先把4位密码一位一位输入(移位功能),然后从这个密码开始倒计时(计数功能)。

1

代码实现

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
module top_module (
input clk, // 时钟信号
input shift_ena, // 移位使能信号
input count_ena, // 计数使能信号
input data, // 串行输入数据
output [3:0] q // 4位输出
);

reg [3:0] q_temp; // 内部寄存器,存储当前值

// 时钟上升沿触发的时序逻辑
always @(posedge clk) begin
if (shift_ena) begin
// 移位模式:{原低3位, 新数据},实现高位先入
q_temp <= {q_temp[2:0], data};
end
else if (count_ena) begin
// 计数模式:递减计数
if (q_temp == 4'd0) begin
// 到0时下一个数是15(4位全1)
q_temp <= 4'd15;
end
else begin
// 正常递减
q_temp <= q_temp - 4'd1;
end
end
// 注意:如果两个使能都为0,保持当前值不变
end

// 输出连接到内部寄存器
assign q = q_temp;

endmodule

要点小结

  • 移位操作使用拼接运算符{}{q_temp[2:0], data}表示保留低3位,在最低位添加新数据
  • 题目要求高位先入,所以需要注意移位方向
  • 计数器到0后变成15而不是停止,这是一个容易理解错的地方

FSM: Sequence 1101 recognizer

题目理解

现在我们开始接触有限状态机(FSM)!这道题要求我们设计一个序列检测器,检测输入数据流中的1101序列。一旦检测到这个序列,输出start_shifting就一直拉高,直到同步复位信号为高。

什么是有限状态机? 简单来说,FSM就是一个有记忆的电路,它能记住当前处于什么状态,根据输入决定下一个状态,并产生相应的输出。

生活实例类比:这就像一个门禁密码锁,你需要按顺序输入正确的密码(1-1-0-1),门锁才会打开(输出拉高),直到你重置系统。

2

状态设计

让我们来设计这个FSM的状态:

  • IDLE:空闲状态,等待序列开始
  • S1:收到了1个’1’
  • S2:收到了2个连续的’1’(”11”)
  • S3:收到了”110”
  • OUT:收到了完整的”1101”序列,输出保持高电平

代码实现

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 clk, // 时钟信号
input reset, // 同步复位信号
input data, // 输入数据
output start_shifting // 检测到序列后的输出
);

// 状态定义,使用parameter提高可读性
parameter IDLE = 3'd0,
S1 = 3'd1,
S2 = 3'd2,
S3 = 3'd3,
OUT = 3'd4;

reg [2:0] current_state; // 当前状态寄存器
reg [2:0] next_state; // 下一个状态组合逻辑

// 第一段:组合逻辑,计算下一个状态
always @(*) begin
case (current_state)
IDLE: begin
// 空闲状态:收到1进入S1,否则保持
next_state = data ? S1 : IDLE;
end
S1: begin
// 已收到1个1:再收到1进入S2,否则回到IDLE
next_state = data ? S2 : IDLE;
end
S2: begin
// 已收到11:收到0进入S3,收到1保持在S2
next_state = data ? S2 : S3;
end
S3: begin
// 已收到110:收到1进入OUT(成功!),否则回到IDLE
next_state = data ? OUT : IDLE;
end
OUT: begin
// 成功状态:一直保持,直到复位
next_state = OUT;
end
default: begin
// 默认状态,防止综合器报警
next_state = IDLE;
end
endcase
end

// 第二段:时序逻辑,状态更新
always @(posedge clk) begin
if (reset) begin
// 复位时回到空闲状态
current_state <= IDLE;
end
else begin
// 正常情况下更新到下一个状态
current_state <= next_state;
end
end

// 第三段:输出逻辑,Moore型FSM(输出只取决于当前状态)
assign start_shifting = (current_state == OUT);

endmodule

要点小结

这是一个经典的三段式FSM结构:

  1. 组合逻辑:根据当前状态和输入计算下一个状态
  2. 时序逻辑:在时钟上升沿更新状态
  3. 输出逻辑:根据当前状态产生输出

这种结构清晰易读,是FSM设计的推荐方式。


FSM: Enable shift register

题目理解

这道题的FSM功能比较简单:当FSM被复位时,将shift_ena拉高4个周期,之后保持为0直到再次复位。

生活实例类比:这就像停车场的道闸,当你刷卡(复位)后,道闸会打开一段时间(4个时钟周期)让车辆通过,然后自动关闭。

3

代码实现

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
72
73
74
75
module top_module (
input clk, // 时钟信号
input reset, // 同步复位信号
output shift_ena // 移位使能输出
);

// 状态定义
parameter IDLE = 2'd0,
ENA = 2'd1,
STOP = 2'd2;

reg [1:0] current_state; // 当前状态
reg [1:0] next_state; // 下一个状态
reg [2:0] counter; // 计数器,记录已经拉高的周期数

// 第一段:组合逻辑,计算下一个状态
always @(*) begin
case (current_state)
IDLE: begin
// 复位后直接进入使能状态
next_state = ENA;
end
ENA: begin
// 计数到3(4个周期)时停止
next_state = (counter == 3'd3) ? STOP : ENA;
end
STOP: begin
// 停止状态一直保持
next_state = STOP;
end
default: begin
next_state = IDLE;
end
endcase
end

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

// 第三段:计数器逻辑
always @(posedge clk) begin
if (reset) begin
counter <= 3'd0;
end
else begin
case (next_state)
IDLE: begin
counter <= 3'd0;
end
ENA: begin
// 使能状态下计数器递增
counter <= counter + 3'd1;
end
STOP: begin
counter <= 3'd0;
end
default: begin
counter <= 3'd0;
end
endcase
end
end

// 输出逻辑:IDLE和ENA状态下输出高电平
// 注意:复位时(IDLE状态)输出也为高
assign shift_ena = (current_state == ENA) || (current_state == IDLE);

endmodule

要点小结

  • 复位期间(current_state == IDLE)输出也需要保持高电平,这是一个容易忽略的细节
  • 计数器从0数到3,正好是4个周期(0, 1, 2, 3)
  • 这是一个FSM与计数器结合的简单示例

FSM: The complete FSM

题目理解

现在我们来看一个更完整的FSM!这道题把前面的序列检测和移位使能结合起来了。让我们先看一下官方提供的状态转移图:

4

5

工作流程:

  1. 首先检测1101序列(状态S→S1→S11→S110)
  2. 检测到序列后,进入4个周期的移位使能状态(B0→B1→B2→B3)
  3. 然后进入计数状态(Count),直到done_counting信号有效
  4. 最后进入等待状态(Wait),直到ack信号有效后回到初始状态

代码实现

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
module top_module (
input clk, // 时钟信号
input reset, // 同步复位信号
input data, // 输入数据
output shift_ena, // 移位使能
output counting, // 正在计数标志
input done_counting, // 计数完成输入
output done, // 完成标志
input ack // 确认输入
);

// 状态定义:10个状态,需要4位
parameter S = 4'd0,
S1 = 4'd1,
S11 = 4'd2,
S110 = 4'd3,
B0 = 4'd4,
B1 = 4'd5,
B2 = 4'd6,
B3 = 4'd7,
Count = 4'd8,
Wait = 4'd9;

reg [3:0] current_state; // 当前状态
reg [3:0] next_state; // 下一个状态

// 第一段:组合逻辑,计算下一个状态
always @(*) begin
case (current_state)
S: begin
// 初始状态:等待序列开始
next_state = data ? S1 : S;
end
S1: begin
// 收到1个1
next_state = data ? S11 : S;
end
S11: begin
// 收到11
next_state = data ? S11 : S110;
end
S110: begin
// 收到110
next_state = data ? B0 : S;
end
B0: begin
// 移位使能第1个周期
next_state = B1;
end
B1: begin
// 移位使能第2个周期
next_state = B2;
end
B2: begin
// 移位使能第3个周期
next_state = B3;
end
B3: begin
// 移位使能第4个周期
next_state = Count;
end
Count: begin
// 计数状态:等待done_counting
next_state = done_counting ? Wait : Count;
end
Wait: begin
// 等待状态:等待ack确认
next_state = ack ? S : Wait;
end
default: begin
next_state = S;
end
endcase
end

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

// 第三段:输出逻辑
assign shift_ena = (current_state == B0) ||
(current_state == B1) ||
(current_state == B2) ||
(current_state == B3);
assign counting = (current_state == Count);
assign done = (current_state == Wait);

endmodule

要点小结

  • 这是一个标准的Moore型FSM,输出只取决于当前状态
  • 状态转移清晰,每个状态的职责明确
  • 输出逻辑简单直观,直接判断当前状态即可

The complete timer

题目理解

终于到了本章节的重头戏!这道题是前面几道题的综合应用,我们需要把序列检测、移位寄存器和计数器组合成一个完整的定时器。

工作原理:

  1. 检测1101序列
  2. 检测到后,读取接下来的4位数据作为延时值(delay)
  3. 然后开始计数,计数时长为(delay + 1) * 1000个周期
  4. 计数完成后,输出done信号,等待ack确认后回到初始状态

6

代码实现

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
module top_module (
input clk, // 时钟信号
input reset, // 同步复位信号
input data, // 输入数据
output [3:0] count, // 当前计数输出
output counting, // 正在计数标志
output done, // 完成标志
input ack // 确认输入
);

// 状态定义
parameter IDLE = 4'd0,
S1 = 4'd1,
S2 = 4'd2,
S3 = 4'd3,
C0 = 4'd4,
C1 = 4'd5,
C2 = 4'd6,
C3 = 4'd7,
Count_1000 = 4'd8,
Done = 4'd9;

reg [3:0] current_state; // 当前状态
reg [3:0] next_state; // 下一个状态
reg [15:0] num; // 主计数器,最大需要16*1000=16000
reg [3:0] delay; // 存储读取到的延时值
reg [3:0] already_count; // 已经完成的千周期数

// 组合逻辑:判断计数是否完成
wire count_state;
assign count_state = (num == (delay + 1'b1) * 16'd1000) ? 1'b1 : 1'b0;

// 组合逻辑:计算已经完成的千周期数
always @(*) begin
if (num <= 16'd1000) begin
already_count = 4'd0;
end
else if (num > 16'd1000 && num <= 16'd2000) begin
already_count = 4'd1;
end
else if (num > 16'd2000 && num <= 16'd3000) begin
already_count = 4'd2;
end
else if (num > 16'd3000 && num <= 16'd4000) begin
already_count = 4'd3;
end
else if (num > 16'd4000 && num <= 16'd5000) begin
already_count = 4'd4;
end
else if (num > 16'd5000 && num <= 16'd6000) begin
already_count = 4'd5;
end
else if (num > 16'd6000 && num <= 16'd7000) begin
already_count = 4'd6;
end
else if (num > 16'd7000 && num <= 16'd8000) begin
already_count = 4'd7;
end
else if (num > 16'd8000 && num <= 16'd9000) begin
already_count = 4'd8;
end
else if (num > 16'd9000 && num <= 16'd10000) begin
already_count = 4'd9;
end
else if (num > 16'd10000 && num <= 16'd11000) begin
already_count = 4'd10;
end
else if (num > 16'd11000 && num <= 16'd12000) begin
already_count = 4'd11;
end
else if (num > 16'd12000 && num <= 16'd13000) begin
already_count = 4'd12;
end
else if (num > 16'd13000 && num <= 16'd14000) begin
already_count = 4'd13;
end
else if (num > 16'd14000 && num <= 16'd15000) begin
already_count = 4'd14;
end
else begin
already_count = 4'd15;
end
end

// 时序逻辑:主计数器
always @(posedge clk) begin
if (reset) begin
num <= 16'd0;
end
else if (next_state == Done) begin
num <= 16'd0;
end
else if (next_state == Count_1000) begin
num <= num + 16'd1;
end
end

// 第一段:组合逻辑,计算下一个状态和输出
always @(*) begin
// 默认值,防止综合器报警
next_state = IDLE;
delay = delay; // 保持原值

case (current_state)
IDLE: begin
next_state = data ? S1 : IDLE;
end
S1: begin
next_state = data ? S2 : IDLE;
end
S2: begin
next_state = data ? S2 : S3;
end
S3: begin
next_state = data ? C0 : IDLE;
end
C0: begin
next_state = C1;
delay[3] = data; // 读取第1位(最高位)
end
C1: begin
next_state = C2;
delay[2] = data; // 读取第2位
end
C2: begin
next_state = C3;
delay[1] = data; // 读取第3位
end
C3: begin
next_state = Count_1000;
delay[0] = data; // 读取第4位(最低位)
end
Count_1000: begin
next_state = count_state ? Done : Count_1000;
end
Done: begin
next_state = ack ? IDLE : Done;
end
default: begin
next_state = IDLE;
end
endcase
end

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

// 第三段:输出逻辑
assign count = (current_state == Count_1000) ? (delay - already_count) : 4'd0;
assign counting = (current_state == Count_1000);
assign done = (current_state == Done);

endmodule

要点小结

  • 这是一个复杂的FSM与多个计数器结合的示例
  • 计数器位宽的设计很重要:最大计数值是16×1000=16000,需要16位(2^14=16384就够了,但用16位更安全)
  • 输出count显示剩余的千周期数,需要用delay - already_count计算
  • 在组合逻辑块中给delay赋值时要小心,这里使用了锁存器的方式,但在实际设计中应该避免在组合逻辑中对变量进行赋值(除非是临时变量)

FSM: One-hot logic equations

题目理解

最后这道题很特别:它不是让我们设计FSM,而是直接使用独热编码(one-hot)的状态来编写状态转移逻辑。

什么是独热编码? 独热编码就是每个状态用一个独立的位来表示,在任何时刻只有一个位是1(热的),其他都是0。例如,有4个状态时:

  • 状态0:4’b0001
  • 状态1:4’b0010
  • 状态2:4’b0100
  • 状态3:4’b1000

独热编码的优点是状态解码简单,缺点是需要更多的触发器。

7

代码实现

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 d, // 数据输入
input done_counting, // 计数完成输入
input ack, // 确认输入
input [9:0] state, // 10位独热编码状态输入
output B3_next, // B0状态的下一个状态
output S_next, // S状态的下一个状态
output S1_next, // S1状态的下一个状态
output Count_next, // Count状态的下一个状态
output Wait_next, // Wait状态的下一个状态
output done, // 完成输出
output counting, // 计数中输出
output shift_ena // 移位使能输出
);

// 参数定义,方便访问state的各个位
parameter S = 0,
S1 = 1,
S11 = 2,
S110 = 3,
B0 = 4,
B1 = 5,
B2 = 6,
B3 = 7,
Count = 8,
Wait = 9;

// 状态转移逻辑:根据当前状态和输入计算下一个状态
// B3的下一个状态:当当前状态是B2时,下一个是B3
assign B3_next = state[B2];

// S的下一个状态:多种情况会回到S
assign S_next = (~d & state[S]) | // 在S状态且d=0
(~d & state[S1]) | // 在S1状态且d=0
(~d & state[S110]) | // 在S110状态且d=0
(ack & state[Wait]); // 在Wait状态且ack=1

// S1的下一个状态:在S状态且d=1
assign S1_next = d & state[S];

// Count的下一个状态:在B3状态,或者在Count状态且未完成
assign Count_next = state[B3] | (~done_counting & state[Count]);

// Wait的下一个状态:在Count状态且完成,或者在Wait状态且未确认
assign Wait_next = (done_counting & state[Count]) | (~ack & state[Wait]);

// 输出逻辑
assign done = state[Wait];
assign counting = state[Count];
assign shift_ena = state[B0] | state[B1] | state[B2] | state[B3];

endmodule

要点小结

  • 独热编码的状态转移逻辑非常直观,直接用逻辑表达式描述即可
  • 不需要case语句,每个状态的转移独立描述
  • 输出逻辑特别简单:直接检查对应的状态位是否为1
  • 这种方式在某些FPGA架构上可以获得更好的性能

入门者避坑指南

在学习这一章的过程中,笔者发现初学者很容易犯一些共性的错误。下面我总结了最常见的5个问题,希望能帮助大家少走弯路。

坑点1:计数器位宽设计不当

错误表现

1
2
3
4
5
6
7
8
9
10
// 错误示例:位宽不够
reg [8:0] q; // 9位最大只能到511,不够999
always @(posedge clk) begin
if (reset)
q <= 0; // 没有指定位宽
else if (q == 999)
q <= 0;
else
q <= q + 1;
end

错误原因

  • 没有仔细计算需要的位宽:2^9=512 < 1000,至少需要10位(2^10=1024)
  • 常量没有指定位宽,可能导致综合时出现意外

正确做法

1
2
3
4
5
6
7
8
9
10
// 正确示例
reg [9:0] q; // 10位足够
always @(posedge clk) begin
if (reset)
q <= 10'd0; // 指定位宽
else if (q == 10'd999)
q <= 10'd0;
else
q <= q + 10'd1;
end

调试技巧

  • 计算位宽时留有余量,比如需要10位就用10位,不要刚好卡着边界
  • 在仿真时观察计数器的值,看是否达到预期的最大值

坑点2:移位方向搞反

错误表现
题目要求高位先入,但写成了低位先入:

1
2
3
4
5
// 错误示例:移位方向反了
always @(posedge clk) begin
if (shift_ena)
q_temp <= {data, q_temp[3:1]}; // 低位先入
end

错误原因

  • 没有理解”高位先入”的含义:新数据应该放在最低位,这样随着移位进行,先进入的数据会逐渐移到高位
  • 拼接运算符的顺序搞反了

正确做法

1
2
3
4
5
// 正确示例:高位先入
always @(posedge clk) begin
if (shift_ena)
q_temp <= {q_temp[2:0], data}; // 保留低3位,新数据放最低位
end

调试技巧

  • 画一个简单的示意图,标注每个时钟周期后寄存器的值
  • 先输入4位已知数据(比如1101),看输出是否符合预期

坑点3:FSM状态遗漏或转移错误

错误表现

1
2
3
4
5
6
7
8
// 错误示例:状态转移错误
always @(*) begin
case (current_state)
S11: next_state = data ? S11 : S110;
S110: next_state = data ? B0 : IDLE; // 正确
// 漏掉了B0、B1等状态的处理!
endcase
end

错误原因

  • 没有仔细对照状态转移图
  • case语句中漏掉了某些状态

正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 正确示例:完整的状态转移
always @(*) begin
case (current_state)
S: next_state = data ? S1 : S;
S1: next_state = data ? S11 : S;
S11: next_state = data ? S11 : S110;
S110: next_state = data ? B0 : S;
B0: next_state = B1;
B1: next_state = B2;
B2: next_state = B3;
B3: next_state = Count;
Count: next_state = done_counting ? Wait : Count;
Wait: next_state = ack ? S : Wait;
default: next_state = S; // 加上default
endcase
end

调试技巧

  • 先画出完整的状态转移图
  • 使用parameter定义所有状态,避免使用魔术数字
  • 一定要加上default分支,防止综合器报警和出现意外行为

坑点4:组合逻辑中使用阻塞赋值不当

错误表现

1
2
3
4
5
6
7
8
9
10
// 错误示例:在组合逻辑中对变量赋值产生锁存器
always @(*) begin
case (current_state)
C0: begin
next_state = C1;
delay[3] = data; // 在组合逻辑中对delay赋值
end
// ... 其他状态没有给delay赋值,会产生锁存器
endcase
end

错误原因

  • 在组合逻辑中对变量赋值,但不是所有分支都赋值,会产生锁存器
  • delay应该用寄存器存储,在时序逻辑中赋值

正确做法

1
2
3
4
5
6
7
8
9
// 正确示例:用时序逻辑存储delay
always @(posedge clk) begin
case (next_state)
C0: delay[3] <= data;
C1: delay[2] <= data;
C2: delay[1] <= data;
C3: delay[0] <= data;
endcase
end

调试技巧

  • 检查综合报告,看是否有意外的锁存器(latch)
  • 组合逻辑只用来计算下一个状态和输出,状态变量的更新放在时序逻辑中

坑点5:复位功能设计错误

错误表现

1
2
// 错误示例:复位期间输出不正确
assign shift_ena = (current_state == ENA); // 复位时(IDLE)输出为0

错误原因

  • 没有仔细阅读题目要求:题目明确说复位期间输出也应该为高
  • 输出逻辑只考虑了正常工作状态,没有考虑复位状态

正确做法

1
2
// 正确示例:复位期间也输出高电平
assign shift_ena = (current_state == ENA) || (current_state == IDLE);

调试技巧

  • 仔细阅读题目要求,特别注意复位时的行为
  • 仿真时专门测试复位场景,观察输出是否符合要求

结语

这一章的内容非常丰富,我们从简单的计数器开始,逐步学习了移位寄存器、序列检测FSM,最后把它们组合成一个完整的定时器系统。

学习这一章的意义在于

  • 掌握FSM的三段式设计方法
  • 学会将FSM与计数器结合使用
  • 理解复杂数字系统的设计思路

这些技能在实际工程中非常有用,比如通信协议控制器、外设接口、时序控制电路等,都离不开FSM和计数器的组合。

希望这篇文章对大家有帮助!如果代码有错误的地方,欢迎大家在评论区指正。