4. Mealy状态机设计实战:以序列检测器为例
好,咱们今天来点真格的。前面讲了那么多理论,说白了,状态机到底怎么用,还得上手练。我选了一个非常经典的例子——序列检测器。这东西在通信协议解析、数据帧头识别里太常见了。我当年做SDI视频解码的时候,就靠它来锁定同步头。
这次咱们要检测的序列是“101”。我会分别用Mealy和Moore两种方式实现,然后你就能直观地看到它们的区别。嗯,先上Mealy。
4.1 Mealy型序列检测器设计
Mealy状态机的核心特点,就是输出不仅取决于当前状态,还取决于当前输入。说白了,输出是“即时反应”。检测到目标序列的最后一个bit时,输出立刻拉高,不需要等下一个时钟周期。
4.1.1 状态转移图
我个人习惯,写代码前一定先画图。脑子里的想法再清晰,落到纸上才能发现漏洞。咱们要检测“101”,需要几个状态?
- S0:初始状态,啥也没检测到
- S1:检测到了第一个bit “1”
- S2:检测到了前两个bit “10”
注意,这里只需要3个状态。为什么?因为Mealy的输出是在转移路径上完成的。当我们在S2状态,输入为“1”时,不仅转移到下一个状态,同时输出“1”表示检测成功。
关键点:Mealy状态数通常比Moore少。这是它的优势,也是容易让人迷惑的地方。
咱们用SVG画一张状态转移图,你一看就明白。
看到没?从S2到S1的那条红色路径,标注的是“1/1”。意思是输入为1时,输出为1,同时状态回到S1。为什么回到S1?因为输入的“1”可以作为下一个序列的开始。这种重叠检测在实际中很常见。
4.1.2 Verilog代码实现
好,图有了,代码就顺理成章了。我习惯用三段式状态机,清晰、好维护、时序也干净。
// Mealy型 101序列检测器
module mealy_101_detector (
input clk,
input rst_n,
input din,
output reg dout
);
// 状态编码
localparam S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10;
reg [1:0] state, next_state;
// 第一段:时序逻辑,状态更新
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= S0;
else
state <= next_state;
end
// 第二段:组合逻辑,次态和输出
always @(*) begin
next_state = S0; // 默认值
dout = 1'b0; // 默认输出为0
case (state)
S0: begin
if (din == 1'b1) begin
next_state = S1;
end else begin
next_state = S0;
end
end
S1: begin
if (din == 1'b0) begin
next_state = S2;
end else begin
next_state = S1; // 连续1,停留在S1
end
end
S2: begin
if (din == 1'b1) begin
next_state = S1; // 检测成功,且重叠
dout = 1'b1; // Mealy输出:即时拉高
end else begin
next_state = S0;
end
end
default: begin
next_state = S0;
end
endcase
end
endmodule
小技巧:注意第二段是组合逻辑,我给next_state和dout都赋了默认值。这样做可以避免产生锁存器(latch)。我曾经见过一个同事,就是因为少写了默认赋值,综合出来一堆latch,时序怎么都修不过去。
4.2 Moore型序列检测器对比
咱们再来看看Moore型。Moore的输出只取决于当前状态,所以检测到“101”后,需要进入一个专门的“成功状态”,在下一个时钟周期输出才拉高。
4.2.1 状态转移图
Moore需要4个状态:
- S0:初始
- S1:检测到“1”
- S2:检测到“10”
- S3:检测到“101”(输出为1)
你对比一下,Mealy是3个状态,Moore是4个。这就是区别。
4.2.2 Verilog代码实现
// Moore型 101序列检测器
module moore_101_detector (
input clk,
input rst_n,
input din,
output reg dout
);
localparam S0 = 2'b00,
S1 = 2'b01,
S2 = 2'b10,
S3 = 2'b11;
reg [1:0] state, next_state;
// 第一段:状态更新
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= S0;
else
state <= next_state;
end
// 第二段:次态逻辑
always @(*) begin
next_state = S0;
case (state)
S0: next_state = (din) ? S1 : S0;
S1: next_state = (din) ? S1 : S2;
S2: next_state = (din) ? S3 : S0;
S3: next_state = (din) ? S1 : S0;
default: next_state = S0;
endcase
end
// 第三段:输出逻辑(Moore,只取决于当前状态)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= 1'b0;
else
dout <= (state == S3) ? 1'b1 : 1'b0;
end
endmodule
注意:Moore的输出在第三段,是时序逻辑。这意味着检测到“101”后,输出要等到下一个时钟周期才拉高。如果你需要“即时响应”,Mealy更合适。但Moore的输出更干净,没有毛刺风险。
4.3 Mealy vs Moore:实战对比
咱们用一张表来总结,这样你面试时也能说得清楚。
| 对比项 | Mealy | Moore |
|---|---|---|
| 状态数 | 较少(本例3个) | 较多(本例4个) |
| 输出时机 | 输入变化时立即输出 | 时钟沿到来后输出 |
| 输出延迟 | 一个组合逻辑延迟 | 一个时钟周期延迟 |
| 毛刺风险 | 较高(组合输出) | 较低(寄存器输出) |
| 适用场景 | 高速、需要快速响应 | 对时序要求严格、需要稳定输出 |
我个人在实际项目中,更倾向于用Moore。为什么?因为它的输出是寄存器输出,时序上更好把控。我曾经在一个高速SerDes项目中,用了Mealy状态机,结果输出毛刺导致后级逻辑误触发,排查了两天才找到原因。从那以后,除非对延迟有极致要求,否则我都用Moore。
当然,Mealy也有它的用武之地。比如在通信协议中,需要在一个时钟周期内完成“检测+响应”,Mealy就是唯一选择。你想想看,如果每个响应都要等一个时钟周期,那协议效率就大打折扣了。
核心结论:
- 追求低延迟、少状态 → 选Mealy
- 追求稳定输出、易调试 → 选Moore
- 没有绝对的好坏,只有合不合适的场景
好了,这一章的内容就到这儿。代码你可以直接拿去仿真,用Vivado或Lattice Diamond都行。试着改一改,比如检测“1101”序列,看看状态图怎么画,代码怎么写。动手练才是王道。