Verilog有限状态机(5)

HDLBits链接


前言

今天我们来学习一组非常经典的串行接收器状态机题目。串行通信是数字电路中最常见的通信方式之一,从UART、SPI到USB、PCIe,本质上都是串行传输数据。

这组题目会从一个简单的串行接收器开始,逐步增加数据输出、奇偶校验等功能。通过这组题目,你能学会一个非常重要的设计技巧:用计数器代替大量状态


题库

题目1:串行接收器 - 基础版

1

题目理解

我们需要设计一个简单的串行接收器,接收格式如下:

  • 1个起始位:0
  • 8个数据位:从低位到高位依次接收
  • 1个停止位:1

状态机工作流程:

  1. IDLE状态:空闲,in=1(线路空闲时为高电平),检测到in=0表示起始位到来
  2. START状态:接收到起始位
  3. ONE到EIGHT状态:依次接收8个数据位
  4. STOP状态:接收到停止位,此时done=1
  5. 如果停止位不正确,进入WAIT状态等待

Solution1(直接用状态表示每个数据位):

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

// 状态参数定义:用了12个状态!
parameter [3:0] START = 4'd0;
parameter [3:0] ONE = 4'd1;
parameter [3:0] TWO = 4'd2;
parameter [3:0] THREE = 4'd3;
parameter [3:0] FOUR = 4'd4;
parameter [3:0] FIVE = 4'd5;
parameter [3:0] SIX = 4'd6;
parameter [3:0] SEVEN = 4'd7;
parameter [3:0] EIGHT = 4'd8;
parameter [3:0] STOP = 4'd9;
parameter [3:0] IDLE = 4'd10;
parameter [3:0] WAIT = 4'd11;

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

// 第一段:组合逻辑,状态转移
always @(*) begin
case (state)
START: begin
next_state = ONE; // 起始位后去第1位
end
ONE: begin
next_state = TWO;
end
TWO: begin
next_state = THREE;
end
THREE: begin
next_state = FOUR;
end
FOUR: begin
next_state = FIVE;
end
FIVE: begin
next_state = SIX;
end
SIX: begin
next_state = SEVEN;
end
SEVEN: begin
next_state = EIGHT;
end
EIGHT: begin
if (in) begin
next_state = STOP; // 正确的停止位
end else begin
next_state = WAIT; // 错误的停止位,等待
end
end
STOP: begin
if (in) begin
next_state = IDLE; // 线路回到空闲
end else begin
next_state = START; // 新的起始位来了
end
end
WAIT: begin
if (in) begin
next_state = IDLE; // 等到线路回到空闲
end else begin
next_state = WAIT;
end
end
IDLE: begin
if (~in) begin
next_state = START; // 检测到起始位(0)
end else begin
next_state = IDLE;
end
end
default: begin
next_state = IDLE;
end
endcase
end

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

// 第三段:输出逻辑
assign done = (state == STOP);

endmodule

思考:这种写法的问题

虽然上面的代码是正确的,但有一个明显的问题:

  • 接收8位数据就用了8个状态(ONE到EIGHT)
  • 如果要接收64位数据呢?难道要定义64个状态?
  • 这显然不现实!

在下一题中,我们会学习更好的方法:用计数器代替状态


题目2:串行接收器 + 数据输出

2

题目理解

这道题在上一题的基础上增加了数据输出功能:

  • done=1时,out_byte[7:0]输出接收到的8位数据
  • 我们需要用移位寄存器来暂存数据

而且,这里我们用更聪明的方法:用一个DATA状态代替原来的8个状态,配合计数器!

Solution2(推荐方法:状态+计数器):

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
module top_module(
input wire clk,
input wire in,
input wire reset, // 同步复位
output reg [7:0] out_byte, // 输出:接收到的8位数据
output reg done
);

// 状态参数定义:只需要5个状态!
parameter IDLE = 3'd0;
parameter START = 3'd1;
parameter DATA = 3'd2; // 用这一个状态代替原来的8个!
parameter STOP = 3'd3;
parameter WAIT = 3'd4;

reg [2:0] current_state;
reg [2:0] next_state;
reg [3:0] counter; // 计数器:记录收到了几位数据
reg [7:0] par_in; // 移位寄存器:暂存数据

// 第一段:组合逻辑,状态转移
always @(*) begin
case (current_state)
IDLE: begin
if (~in) begin
next_state = START; // 检测到起始位
end else begin
next_state = IDLE;
end
end
START: begin
next_state = DATA; // 去数据接收状态
end
DATA: begin
if (counter == 4'd8) begin
next_state = in ? STOP : WAIT; // 8位收完,看停止位
end else begin
next_state = DATA; // 继续接收数据
end
end
STOP: begin
next_state = in ? IDLE : START; // 看是否有新的起始位
end
WAIT: begin
next_state = in ? IDLE : WAIT; // 等待线路回到空闲
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
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end else begin
case (next_state) // 注意:用next_state!
IDLE: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end
START: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end
DATA: begin
done <= 1'd0;
out_byte <= 8'd0;
par_in[counter] <= in; // 用counter做索引!
counter <= counter + 1'd1;
end
STOP: begin
done <= 1'd1;
out_byte <= par_in; // 输出完整数据
counter <= 4'd0;
end
WAIT: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end
endcase
end
end

endmodule

关键设计技巧:状态+计数器

看!我们用一个DATA状态配合counter计数器,就代替了原来的8个状态!这种方法的优势是:

  • 状态数大大减少
  • 要接收更多位?只需要增加计数器位宽就行
  • 代码更简洁,可扩展性更好

这就像你去超市买东西:

  • 方法1:为每一件商品定义一个动作(拿第1件、拿第2件…)
  • 方法2:用一个”购物中”状态,用计数器数拿了几件

显然方法2更好!


题目3:串行接收器 + 奇偶校验

现在题目又升级了!我们需要增加奇偶校验功能。题目中已经给出了一个parity模块,我们直接调用即可。

题目理解

相比上一题,现在的数据包格式是:

  • 1个起始位
  • 8个数据位
  • 1个奇偶校验位(新增!)
  • 1个停止位

只有当奇偶校验正确时,done才会置1,数据才输出。

Solution3:

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
module top_module(
input wire clk,
input wire in,
input wire reset, // 同步复位
output reg [7:0] out_byte, // 输出:接收到的8位数据
output reg done
);

// 状态参数定义
parameter IDLE = 3'd0;
parameter START = 3'd1;
parameter DATA = 3'd2; // 现在要接收9位:8数据+1校验
parameter STOP = 3'd3;
parameter WAIT = 3'd4;

reg [2:0] current_state;
reg [2:0] next_state;
reg [3:0] counter; // 计数器:0-8(共9位)
reg [8:0] data_in; // 暂存:8数据+1校验
reg odd_temp; // 奇偶校验结果
wire is_done; // 给奇偶校验模块的信号

// 第一段:组合逻辑,状态转移
always @(*) begin
case (current_state)
IDLE: begin
if (~in) begin
next_state = START;
end else begin
next_state = IDLE;
end
end
START: begin
next_state = DATA;
end
DATA: begin
if (counter == 4'd9) begin
next_state = in ? STOP : WAIT; // 9位收完(8+1)
end else begin
next_state = DATA;
end
end
STOP: begin
next_state = in ? IDLE : START;
end
WAIT: begin
next_state = in ? IDLE : WAIT;
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
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end else begin
case (next_state)
IDLE: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end
START: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end
DATA: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= counter + 1'd1;
data_in[counter] <= in;
end
STOP: begin
// 只有奇偶校验正确时,done才置1
done <= odd_temp ? 1'd1 : 1'd0;
out_byte <= odd_temp ? data_in[7:0] : 8'd0;
counter <= 4'd0;
end
WAIT: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0;
end
endcase
end
end

// 调用题目提供的奇偶校验模块
assign is_done = (next_state == START); // 指示新包开始
parity u0 (
.clk(clk),
.reset(is_done),
.in(in),
.odd(odd_temp)
);

endmodule

要点说明

  1. 计数器范围变了:现在要接收9位(8数据+1校验),所以计数到9
  2. data_in位宽增加:从8位变成9位,多存一个校验位
  3. 调用奇偶校验模块:题目已经提供了,我们直接实例化就行
  4. done信号有条件:只有odd_temp为1时,done才置1

入门者避坑指南

在做串行接收器这类题目时,初学者最容易犯以下错误:

错误1:用太多状态,不会用计数器

错误表现:

1
2
// 接收8位数据就定义8个状态
parameter ONE = 0, TWO = 1, THREE = 2, ...;

错误原因:

  • 没有想到”状态+计数器”的组合
  • 这种方法在接收位数少时可以,但位数多了就不现实

正确做法:

1
2
parameter DATA = 0;  // 只用一个DATA状态
reg [3:0] counter; // 配合计数器

生活实例类比:

  • 错误做法:每走一步定义一个状态(第1步、第2步、第3步…)
  • 正确做法:用一个”走路中”状态,用计数器数走了几步

错误2:数据位序搞反

错误表现:

1
2
// 假设第1次收到的是bit0,却存到最高位
par_in[7 - counter] <= in; // 如果题目要求LSB优先,这就错了

错误原因:

  • 没有仔细看题目说明
  • 题目通常会明确:先收到的是最低位(LSB)还是最高位(MSB)

正确做法:

  • 仔细读题目!题目说”从低位到高位依次接收”,就按顺序存
  • 不确定时,可以看题目的波形图示例

错误3:用state判断而不是next_state

错误表现:

1
2
3
4
5
6
7
// 用state判断,数据捕获会晚一个周期
always @(posedge clk) begin
if (state == DATA) begin
par_in[counter] <= in;
counter <= counter + 1'd1;
end
end

错误原因:

  • 状态在时钟边沿更新
  • state变成DATA时,已经是第2个周期了
  • 第1个数据会错过!

正确做法:

1
2
3
4
5
6
7
// 用next_state判断,提前一个周期
always @(posedge clk) begin
if (next_state == DATA) begin
par_in[counter] <= in;
counter <= counter + 1'd1;
end
end

错误4:计数器没有正确清零

错误表现:

1
2
3
4
5
// 只有在IDLE状态才清零计数器
if (next_state == IDLE) begin
counter <= 4'd0;
end
// 但在START状态没有清零,第2个包会接着第1个包计数!

错误原因:

  • 每个新包都应该从零开始计数
  • 计数器清零的条件不够全面

正确做法:

1
2
3
4
5
6
// 在START状态也要清零计数器
START: begin
done <= 1'd0;
out_byte <= 8'd0;
counter <= 4'd0; // 记得清零!
end

错误5:组合逻辑中给reg赋值时没有default

错误表现:

1
2
3
4
5
6
always @(*) begin
case (state)
IDLE: next_state = START;
// 有些分支没有给next_state赋值!
endcase
end

错误原因:

  • 会生成锁存器(latch)
  • 在组合逻辑中,所有被赋值的变量在所有分支都必须有值

正确做法:

  • 要么在case前给一个默认值:next_state = IDLE;
  • 要么每个case分支都赋值,加上default分支

小结

今天我们学习了三组串行接收器的题目,最核心的收获是:

  1. 状态+计数器的设计方法:这是一个非常强大的技巧!用一个状态配合计数器,可以代替大量的独立状态,让代码更简洁、可扩展性更好

  2. 用next_state判断数据捕获:在时序逻辑中,用next_state而不是state来判断,可以提前一个周期捕获数据,避免错过第一个数据

  3. 移位寄存器的使用:在接收串行数据时,通常需要一个寄存器来暂存接收到的数据位

  4. 模块实例化:当题目提供现成的模块时,要学会正确实例化它

作为一名通信IC设计师,笔者想说:串行接收器是通信电路中最基础的模块之一。从简单的UART到复杂的以太网,都离不开接收状态机。学会”状态+计数器”的方法,能让你在未来的设计中事半功倍!