3. Moore状态机设计实战:以序列检测器为例
好,咱们今天来点真家伙。
前面讲了那么多理论,什么状态编码、状态转移图,说白了都是纸上谈兵。真正上手写代码的时候,你会发现——嗯,其实也没那么难。但坑也不少。
我个人习惯,学状态机最好的入门例子就是序列检测器。为什么?因为它逻辑清晰,状态不多,而且能完整展示Moore机的特点。今天我们就以检测“101”序列为例,手把手走一遍。
3.1 什么是序列检测器?
序列检测器,说白了就是在连续的比特流中,找出特定的子序列。比如我们要找“101”,那么输入“110101”时,检测到第3-5位是“101”,就输出一个高电平脉冲。
你想想看,这在实际项目里多常见?通信协议里的帧头检测、数据包同步,都离不开它。我在一个无线通信项目里就遇到过,当时要检测一个8位的同步头,用状态机做,稳得很。
3.2 Moore机 vs Mealy机:选哪个?
这里先插一句。同样是检测“101”,可以用Moore机,也可以用Mealy机。区别在哪?
- Moore机:输出只取决于当前状态。检测到序列时,进入一个专门的“检测成功”状态,然后输出1。
- Mealy机:输出取决于当前状态和输入。检测到序列的最后一位时,当场输出1,不需要额外状态。
我建议初学者先从Moore机入手。为什么?因为它的输出和输入是隔开的,不容易产生毛刺。我在项目中吃过Mealy机组合逻辑输出的亏,后来老老实实改成了Moore机。
核心要点:Moore机的输出比输入晚一个时钟周期,但更稳定。适合对时序要求严格的场景。
3.3 状态转移图:先画图再写代码
写状态机之前,我强烈建议你先画状态转移图。别急着敲代码,图画清楚了,代码就是翻译工作。
检测“101”的Moore机,需要几个状态?我们来推一下:
- S0:初始状态,什么都没检测到
- S1:检测到“1”
- S2:检测到“10”
- S3:检测到“101”(输出1)
注意,这里有个关键点:状态重叠。比如在S3(检测到101)之后,如果下一个输入是“1”,那其实已经构成了新的“10”序列的一部分,应该回到S1,而不是S0。我曾经在这里翻过车,以为检测完就回到起点,结果漏掉了重叠序列。
3.4 Verilog代码实现:三段式写法
我个人强烈推荐三段式状态机写法。为什么?因为可读性强,综合结果好,而且不容易出bug。我在公司里带新人,第一件事就是让他们用三段式。
三段式就是:
- 第一段:时序逻辑,描述状态转移(always @(posedge clk))
- 第二段:组合逻辑,描述下一状态逻辑(always @(*))
- 第三段:时序逻辑,描述输出(always @(posedge clk))
module moore_101_detector (
input wire clk,
input wire rst_n,
input wire data_in,
output reg detected
);
// 状态编码:用独热码,综合效率高
localparam S0 = 4'b0001;
localparam S1 = 4'b0010;
localparam S2 = 4'b0100;
localparam S3 = 4'b1000;
reg [3:0] current_state, next_state;
// 第一段:状态转移
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= S0;
else
current_state <= next_state;
end
// 第二段:下一状态逻辑
always @(*) begin
case (current_state)
S0: begin
if (data_in == 1'b1)
next_state = S1;
else
next_state = S0;
end
S1: begin
if (data_in == 1'b0)
next_state = S2;
else
next_state = S1; // 连续1,停留在S1
end
S2: begin
if (data_in == 1'b1)
next_state = S3;
else
next_state = S0; // 遇到0,回到起点
end
S3: begin
if (data_in == 1'b1)
next_state = S1; // 重叠检测:1011 -> 检测到新的1
else
next_state = S2; // 重叠检测:1010 -> 检测到10
end
default: next_state = S0;
endcase
end
// 第三段:输出
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
detected <= 1'b0;
else if (current_state == S3)
detected <= 1'b1;
else
detected <= 1'b0;
end
endmodule
小技巧:状态编码我习惯用独热码(one-hot),虽然寄存器用得多一点,但组合逻辑简单,时序容易收敛。如果状态少于8个,独热码是首选。
3.5 仿真验证:别偷懒
代码写完了,一定要仿真。我见过太多人写完代码直接上板,结果跑不起来,回头查半天发现是状态机逻辑错了。
写个简单的testbench,覆盖这些场景:
- 正常序列:输入101,看输出是否在第三个时钟沿拉高
- 重叠序列:输入1011,看是否检测到两次(101和1011中的101)
- 无效序列:输入100,看输出是否一直为0
- 复位测试:复位后状态是否回到S0
module tb_moore_101;
reg clk, rst_n, data_in;
wire detected;
moore_101_detector uut (
.clk(clk),
.rst_n(rst_n),
.data_in(data_in),
.detected(detected)
);
initial begin
clk = 0;
forever #5 clk = ~clk;
end
initial begin
rst_n = 0; data_in = 0;
#20 rst_n = 1;
// 测试序列:1 0 1 1 0 1 0
#10 data_in = 1; // S0 -> S1
#10 data_in = 0; // S1 -> S2
#10 data_in = 1; // S2 -> S3, detected=1
#10 data_in = 1; // S3 -> S1 (重叠)
#10 data_in = 0; // S1 -> S2
#10 data_in = 1; // S2 -> S3, detected=1
#10 data_in = 0; // S3 -> S2
#10 $finish;
end
endmodule
避坑指南:我曾经在仿真时发现输出脉冲只持续了一个时钟周期,但实际需要两个周期。后来发现是testbench里采样时机不对。记住:Moore机的输出比输入晚一拍,采样时要在时钟沿之后。
3.6 综合与调试:Lattice环境下的注意事项
如果你用的是Lattice的FPGA,比如ICE40或ECP5系列,有几点要注意:
- 状态编码:Lattice的合成工具对独热码支持很好,但如果你状态数超过16个,建议用二进制编码,否则寄存器太多。
- 复位方式:Lattice的器件通常支持异步复位,但要注意复位信号的毛刺。我建议在复位路径上加一个同步器。
- 调试手段:用Lattice的Reveal逻辑分析仪,把current_state信号拉出来看。我曾经靠这个抓到一个因为组合逻辑竞争导致的状态跳变错误。
| 调试场景 | 可能原因 | 解决方法 |
|---|---|---|
| 状态不跳变 | 复位未释放、时钟没来 | 检查rst_n和clk信号 |
| 输出脉冲宽度不对 | Moore机输出晚一拍 | 调整采样时机 |
| 检测到错误序列 | 状态转移图有遗漏 | 重新检查重叠情况 |
| 综合后时序不满足 | 状态编码太大 | 改用二进制编码 |
嗯,到这里,一个完整的Moore状态机序列检测器就搞定了。你想想看,从状态图到代码,再到仿真调试,每一步都有坑,但每一步也都有套路。多做几次,你就能找到感觉。
总结一句话:Moore状态机的核心是“状态决定输出”,画好状态转移图,代码就是水到渠成的事。
公众号:蓝海资料掘金营,微信deep3321