Verilog有限状态机(4)

HDLBits链接


前言

今天我们继续学习状态机的题目,这次会涉及两个重要的知识点:

  1. 独热编码状态机的组合逻辑设计
  2. PS/2接口的数据包解析器

PS/2接口是一种经典的键盘鼠标接口,虽然现在已经逐渐被USB取代,但它的协议设计非常经典,很适合用来学习状态机。


题库

题目1:One-hot FSM - 独热编码状态机

1

基础知识:什么是独热编码?

独热编码(One-hot encoding) 是一种特殊的状态编码方式,它的特点是:

  • N个状态就需要N位寄存器
  • 任意时刻,只有一位是1(热),其他都是0(冷)
  • 比如有3个状态:S0=001, S1=010, S2=100

为什么叫”独热”? 想象一排灯泡,每次只有一个灯泡亮着,其他都是灭的,这就是”独热”。

独热编码的优点:

  • 状态判断超级简单:只要看对应的位是不是1就行
  • 组合逻辑通常更简单,速度更快
  • 没有多个位同时变化的情况,不容易产生毛刺

独热编码的缺点:

  • 占用更多的寄存器资源(状态越多越明显)

这就像给每个房间配一把专用钥匙,而不是用一串通用钥匙——开门更快,但钥匙更多。

题目理解

本题中,状态已经用独热编码的方式输入给我们了(state[9:0]),我们需要根据状态转移图推导出:

  • next_state[9:0]:下一状态的独热编码
  • out1out2:输出信号

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
module top_module(
input wire in,
input wire [9:0] state, // 独热编码的当前状态
output reg [9:0] next_state,// 独热编码的下一状态
output reg out1,
output reg out2
);

// 状态参数定义(对应独热编码的位索引)
parameter S0 = 4'd0;
parameter S1 = 4'd1;
parameter S2 = 4'd2;
parameter S3 = 4'd3;
parameter S4 = 4'd4;
parameter S5 = 4'd5;
parameter S6 = 4'd6;
parameter S7 = 4'd7;
parameter S8 = 4'd8;
parameter S9 = 4'd9;

// 下一状态逻辑:利用独热编码特性,单独推导每一位
assign next_state[S0] = ~in & (state[S0] | state[S1] | state[S2] | state[S3] |
state[S4] | state[S7] | state[S8] | state[S9]);
assign next_state[S1] = in & (state[S0] | state[S8] | state[S9]);
assign next_state[S2] = in & state[S1];
assign next_state[S3] = in & state[S2];
assign next_state[S4] = in & state[S3];
assign next_state[S5] = in & state[S4];
assign next_state[S6] = in & state[S5];
assign next_state[S7] = in & (state[S6] | state[S7]);
assign next_state[S8] = ~in & state[S5];
assign next_state[S9] = ~in & state[S6];

// 输出逻辑:同样很简单
assign out1 = (state[S8] | state[S9]);
assign out2 = (state[S7] | state[S9]);

endmodule

代码要点

为什么可以这样写? 因为独热编码中,每个状态位是独立的!我们可以单独推导每一位的逻辑表达式:

  • next_state[S0]:哪些状态在输入为0时会转到S0
  • next_state[S1]:哪些状态在输入为1时会转到S1
  • 以此类推…

这种写法看起来代码量不少,但逻辑非常清晰,综合器也能很好地优化。


题目2:PS/2 packet parser - PS/2数据包解析器(基础版)

2

基础知识:PS/2协议简介

PS/2是一种用于键盘和鼠标的接口协议。数据是以字节为单位传输的,每个字节通常包含:

  • 1个起始位(0)
  • 8个数据位
  • 1个停止位(1)

不过本题简化了协议,我们只需要检测数据包的开始。规则是:

  • in[3]为1时,表示新的数据包开始
  • 我们需要接收3个字节(BYTE_FIRST → BYTE_SECOND → BYTE_THIRD)
  • 不允许重叠检测:接收完3个字节后,要等下一个in[3]=1才能开始新的包

题目理解

这个状态机的工作流程如下:

  1. WAIT状态:等待in[3]=1(数据包开始)
  2. BYTE_FIRST状态:接收第1个字节
  3. BYTE_SECOND状态:接收第2个字节
  4. BYTE_THIRD状态:接收第3个字节,此时done=1
  5. 然后回到WAIT或BYTE_FIRST,取决于in[3]的值

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
module top_module(
input wire clk,
input wire [7:0] in, // PS/2输入数据
input wire reset, // 同步复位
output reg done // 完成信号:在第三个字节时为1
);

// 状态参数定义
parameter BYTE_FIRST = 2'd0;
parameter BYTE_SECOND = 2'd1;
parameter BYTE_THIRD = 2'd2;
parameter WAIT = 2'd3;

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

// 第一段:组合逻辑,状态转移
always @(*) begin
case (state)
BYTE_FIRST: begin
next_state = BYTE_SECOND; // 第1个字节后去第2个
end
BYTE_SECOND: begin
next_state = BYTE_THIRD; // 第2个字节后去第3个
end
BYTE_THIRD: begin
if (in[3] == 1'b1) begin
next_state = BYTE_FIRST; // 下一个包已经开始了
end else begin
next_state = WAIT; // 等待下一个包开始
end
end
WAIT: begin
if (in[3] == 1'b1) begin
next_state = BYTE_FIRST; // 检测到包开始
end else begin
next_state = WAIT; // 继续等待
end
end
default: begin
next_state = WAIT;
end
endcase
end

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

// 第三段:输出逻辑
assign done = (state == BYTE_THIRD); // 仅在第三个字节时done=1

endmodule

关键设计要点

注意状态机的复位状态! 复位时应该在WAIT状态,而不是BYTE_FIRST,因为我们要先等待数据包开始。


题目3:PS/2 packet parser and datapath - PS/2数据包解析器(数据通路版)

3

题目理解

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

  • done=1时,输出完整的24bit数据out_bytes[23:0]
  • 这24bit由3个字节组成:
    • 高8位:第1个接收到的字节
    • 中8位:第2个接收到的字节
    • 低8位:第3个接收到的字节

所以我们需要数据寄存器来暂存接收到的字节!

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
module top_module(
input wire clk,
input wire [7:0] in, // PS/2输入数据
input wire reset, // 同步复位
output reg [23:0] out_bytes, // 输出:24bit完整数据
output reg done // 完成信号
);

// 状态参数定义
parameter BYTE_FIRST = 2'd0;
parameter BYTE_SECOND = 2'd1;
parameter BYTE_THIRD = 2'd2;
parameter DONE = 2'd3;

reg [1:0] state;
reg [1:0] next_state;
reg [23:0] out_bytes_reg; // 数据暂存寄存器

// 第一段:组合逻辑,状态转移
always @(*) begin
case (state)
BYTE_FIRST: begin
if (in[3]) begin
next_state = BYTE_SECOND; // 检测到包开始,继续
end else begin
next_state = BYTE_FIRST; // 还在等待第1个字节
end
end
BYTE_SECOND: begin
next_state = BYTE_THIRD; // 去第3个字节
end
BYTE_THIRD: begin
next_state = DONE; // 去完成状态
end
DONE: begin
if (in[3]) begin
next_state = BYTE_SECOND; // 下一个包已经开始
end else begin
next_state = BYTE_FIRST; // 等待下一个包
end
end
default: begin
next_state = BYTE_FIRST;
end
endcase
end

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

// 第三段:数据通路 - 暂存接收到的字节
// 注意:这里用next_state判断,可以提前一个周期捕获数据
always @(posedge clk) begin
if (next_state == BYTE_SECOND) begin
out_bytes_reg[23:16] <= in; // 第1个字节 → 高8位
end else if (next_state == BYTE_THIRD) begin
out_bytes_reg[15:8] <= in; // 第2个字节 → 中8位
end else if (next_state == DONE) begin
out_bytes_reg[7:0] <= in; // 第3个字节 → 低8位
end else begin
out_bytes_reg <= 24'd0; // 其他情况清零(可选)
end
end

// 第四段:输出逻辑
assign done = (state == DONE);
assign out_bytes = out_bytes_reg;

endmodule

关键设计技巧:用next_state提前捕获数据

注意看数据通路那段代码,我们用的是next_state而不是state!为什么?

因为状态在时钟边沿更新

  • 如果用state判断:状态更新的那个周期,我们还在用旧状态判断,会错过数据
  • 如果用next_state判断:我们可以提前知道下一状态是什么,刚好在正确的周期捕获数据

这是一个非常实用的技巧!就像接棒赛跑,你要提前知道下一个接棒的人是谁,而不是等他已经站在那里了才准备。


入门者避坑指南

在做PS/2解析器这类题目时,初学者最容易犯以下错误:

错误1:用state判断数据捕获,错过一个周期

错误表现:

1
2
3
4
5
6
// 用state判断,会晚一个周期!
always @(posedge clk) begin
if (state == BYTE_SECOND) begin
out_bytes_reg[23:16] <= in;
end
end

错误原因:

  • 状态在时钟边沿更新
  • state变成BYTE_SECOND时,已经是下一个周期了
  • 数据应该在前一个周期就捕获

正确做法:

1
2
3
4
5
6
// 用next_state判断,刚好捕获
always @(posedge clk) begin
if (next_state == BYTE_SECOND) begin
out_bytes_reg[23:16] <= in;
end
end

错误2:状态转移顺序混乱

错误表现:

1
2
3
4
5
6
7
8
BYTE_THIRD: begin
next_state = DONE;
end
DONE: begin
if (in[3]) begin
next_state = BYTE_FIRST; // 错了!应该直接去BYTE_SECOND
end
end

错误原因:

  • 没有仔细看时序图
  • 当在DONE状态时,如果in[3]=1,表示新包的第1个字节已经在接收了!
  • 所以应该直接去BYTE_SECOND,跳过BYTE_FIRST

调试技巧:

  • 画波形图!把instatenext_state都画出来
  • 对照题目给的时序图,看看每个周期应该在什么状态

错误3:复位状态选错

错误表现:

1
2
3
4
5
6
7
always @(posedge clk) begin
if (reset) begin
state <= BYTE_FIRST; // 有些题目是对的,但不一定全对
end else begin
state <= next_state;
end
end

错误原因:

  • 不同的题目,复位状态可能不同
  • 要仔细看题目要求:复位后应该在哪个状态?

正确做法:

  • 看题目描述和时序图,确定复位状态
  • 题目2中复位应该在WAIT
  • 题目3中复位应该在BYTE_FIRST
  • 不要想当然!

错误4:数据位序搞反

错误表现:

1
2
3
4
5
6
7
8
// 位序搞反了
if (next_state == BYTE_SECOND) begin
out_bytes_reg[7:0] <= in; // 第1个字节存到低8位
end else if (next_state == BYTE_THIRD) begin
out_bytes_reg[15:8] <= in;
end else if (next_state == DONE) begin
out_bytes_reg[23:16] <= in; // 第3个字节存到高8位
end

错误原因:

  • 题目通常会明确说明字节顺序
  • 仔细看题目:”高8位、中8位、低8位分别从in[3]为1开始计起”

正确做法:

  • 用笔在纸上画一下:第1个字节 → 哪里?第2个?第3个?
  • 确认清楚再写代码

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

错误表现:

1
2
3
4
5
6
7
8
// 独热编码却用case(state)
always @(*) begin
case (state)
10'b0000000001: next_state[S0] = 1; // 太麻烦了!
10'b0000000010: next_state[S1] = 1;
// ... 要写10个case!
endcase
end

错误原因:

  • 独热编码不需要用case语句
  • 单独推导每一位的逻辑表达式更清晰

正确做法:

1
2
3
4
// 单独推导每一位
assign next_state[S0] = ~in & (...);
assign next_state[S1] = in & (...);
// ...


小结

今天我们学习了三个重要的状态机题目,主要收获有:

  1. 独热编码状态机:利用每个状态位独立的特性,可以单独推导每一位的逻辑表达式,代码清晰,速度快

  2. PS/2协议解析器:一个典型的协议解析状态机应用,从简单到复杂,逐步增加功能

  3. 数据通路设计:当需要暂存数据时,要设计数据寄存器,并且用next_state判断来提前捕获数据,这是一个非常实用的技巧

  4. 复位状态的选择:不同的题目复位状态可能不同,一定要仔细看题目要求

作为一名通信IC设计师,笔者想说:协议解析是通信芯片设计中最常见的任务之一,从UART、SPI、I2C到PCIe、以太网,几乎所有接口都需要状态机来解析协议。学好这类题目,对未来的工程设计非常有帮助!