1. 从实际问题到状态机建模
第一次看到HDLBits上这道蓄水池控制题目时,我盯着题目描述足足看了十分钟。三个水位传感器、四个注水控制信号,还有那个看似违反Moore机原则的补充注水条件——这分明就是个活生生的工业控制系统简化模型。很多初学者容易陷入直接写代码的误区,但真正关键的是前期的状态分析阶段。
让我们拆解这个系统的物理场景:想象一个圆柱形蓄水池,内壁从上到下依次安装着S3、S2、S1三个水位传感器。当水位上升淹没某个传感器时,对应的输入信号变为有效(假设为逻辑1);水位下降使传感器暴露时,信号恢复为0。两个注水口中,主注水口由fr3、fr2、fr1三个信号分级控制流量,而辅助注水口则由dfr信号单独控制。
这里最有趣的是dfr的控制逻辑:当水位从高处下降时需要开启辅助注水,而从低处上升时则关闭。这个"记忆"水位变化方向的需求,正是本题的设计难点。传统Moore机的输出仅取决于当前状态,但这里似乎需要记住状态转换的历史轨迹。
2. 状态定义的进化之路
最开始我尝试用四个基础状态来建模:
- BelowS1:水位在S1之下
- BetwS21:水位在S2和S1之间
- BetwS32:水位在S3和S2之间
- AboveS3:水位在S3之上
这种划分能处理主要注水逻辑,但对dfr信号束手无策。于是考虑在状态中嵌入历史信息,将每个中间状态拆分为两个子状态:
- BetwS21_u:上升到S2-S1区间
- BetwS21_d:下降到S2-S1区间
- BetwS32_u:上升到S3-S2区间
- BetwS32_d:下降到S3-S2区间
这种"状态细分"策略的精妙之处在于,它将时序信息编码进了状态本身。现在dfr的输出决策变得非常简单:
- 所有带"_u"后缀的状态(上升到达):dfr=0
- 所有带"_d"后缀的状态(下降到达):dfr=1
实测发现这种设计不仅满足Moore机的要求,状态转换逻辑也出奇地清晰。在Verilog实现时,我推荐使用独热码(one-hot)编码,虽然多用了几位寄存器,但大大简化了组合逻辑。
3. 状态转移的逻辑迷宫
定义好状态后,最烧脑的部分就是绘制完整的状态转移图。建议准备纸笔,从复位状态BelowS1开始,模拟所有可能的水位变化路径:
- 初始时{s3,s2,s1}=3'b000,全力注水(输出4'b1111)
- 水位上升到S1之上:
- 转移到BetwS21_u
- 输出变为4'b0110(注意dfr=0)
- 继续上升到S2之上:
- 转移到BetwS32_u
- 输出4'b0010
- 若此时水位下降:
- 先回到BetwS32_d(输出4'b0011,dfr=1)
- 继续下降则到BetwS21_d(输出4'b0111)
特别要注意边界情况:当水位在AboveS3时,只有s3变低才会转移到BetwS32_d;而在BelowS1时,只有s1变高才会转移到BetwS21_u。这些细节直接关系到实际系统的稳定性。
4. Verilog实现的艺术
采用经典的三段式状态机写法,代码结构会非常清晰:
module top_module ( input clk, input reset, input [3:1] s, output fr3, fr2, fr1, output dfr ); // 状态定义(独热码) localparam BelowS1 = 6'b000001, BetwS21_u = 6'b000010, BetwS21_d = 6'b000100, BetwS32_u = 6'b001000, BetwS32_d = 6'b010000, AboveS3 = 6'b100000; reg [5:0] state, next_state; // 状态转移逻辑 always @(*) begin case(state) BelowS1 : next_state = s[1] ? BetwS21_u : BelowS1; BetwS21_u: next_state = s[2] ? BetwS32_u : (s[1] ? BetwS21_u : BelowS1); BetwS21_d: next_state = s[2] ? BetwS32_u : (s[1] ? BetwS21_d : BelowS1); BetwS32_u: next_state = s[3] ? AboveS3 : (s[2] ? BetwS32_u : BetwS21_d); BetwS32_d: next_state = s[3] ? AboveS3 : (s[2] ? BetwS32_d : BetwS21_d); AboveS3 : next_state = s[3] ? AboveS3 : BetwS32_d; default : next_state = BelowS1; endcase end // 输出逻辑 always @(*) begin case(state) BelowS1 : {fr3,fr2,fr1,dfr} = 4'b1111; BetwS21_u: {fr3,fr2,fr1,dfr} = 4'b0110; BetwS21_d: {fr3,fr2,fr1,dfr} = 4'b0111; BetwS32_u: {fr3,fr2,fr1,dfr} = 4'b0010; BetwS32_d: {fr3,fr2,fr1,dfr} = 4'b0011; AboveS3 : {fr3,fr2,fr1,dfr} = 4'b0000; default : {fr3,fr2,fr1,dfr} = 4'b1111; endcase end // 时序部分 always @(posedge clk) begin if(reset) state <= BelowS1; else state <= next_state; end endmodule几个值得注意的实现细节:
- 使用独热码编码虽然浪费了些许寄存器资源,但大大简化了组合逻辑
- 输出逻辑采用组合电路实现,符合Moore机特性
- 每个状态转移条件都严格对应水位传感器的变化
- default分支虽然理论上不会执行,但良好的编码习惯不能少
5. 调试与验证技巧
在Modelsim里仿真这个设计时,我建议创建以下测试场景:
- 正常注水流程:从空池开始,水位逐步上升到满池
- 用水波动场景:上升到某个水位后反复升降
- 边界测试:水位恰好在传感器临界位置抖动
- 异常测试:突然复位时的状态恢复
特别要注意检查dfr信号的变化时机。有次我的设计在BetwS32_u到BetwS32_d转换时dfr没有立即置1,后来发现是状态编码重叠导致的优先级问题。这时候在仿真波形中标记出状态编码会非常有助于调试。
6. 状态机设计哲学
这道题教会我们一个重要的设计原则:当输出需要依赖状态转换历史时,可以考虑将历史信息编码到状态本身。这种方法虽然增加了状态数量,但保持了清晰的Moore机结构。相比之下,Mealy机方案虽然可能减少状态,但输出逻辑会变得复杂且容易产生毛刺。
在实际工程中,类似的设计模式随处可见。比如电梯控制系统需要区分是上行停靠还是下行停靠;交通灯系统需要记忆前一个相位等。掌握这种"状态细分"策略,面对复杂时序要求时就能游刃有余。
记得第一次实现这个设计时,我在状态转移条件里漏掉了s[1]的检查,导致仿真时出现状态机"卡死"。经过三个小时的波形分析才找到这个bug,这个教训让我养成了编写完备testbench的习惯。现在每次实现状态机,我都会先画出完整的状态转移图,标注所有可能的转换路径,这个前期工作虽然耗时,但能避免后期的很多调试痛苦。