【HDLBits 实战解析】FSM 进阶:从游戏角色到通信协议的 Verilog 状态机建模
1. 有限状态机从游戏角色到通信协议的核心建模工具有限状态机FSM是数字电路设计中最强大的建模工具之一。它通过定义有限数量的状态和状态之间的转移条件能够清晰地描述复杂系统的行为逻辑。在Verilog中实现FSM时通常采用三段式编码风格这种结构不仅层次分明还能有效避免组合逻辑和时序逻辑的混合问题。我刚开始接触状态机设计时常常纠结于状态定义不清晰导致逻辑混乱。后来在HDLBits上通过Lemmings游戏角色和PS2通信协议这两个典型案例的反复练习才真正掌握了状态机的设计精髓。这两个场景看似差异很大实则完美展现了FSM在不同应用场景下的通用建模方法。2. Lemmings游戏角色的状态机建模实战2.1 基础行走行为建模Lemmings1展示了最简单的两状态模型向左走LEFT和向右走RIGHT。当碰到障碍物时切换方向。这个基础版本的状态转移逻辑非常直观parameter LEFT 0; parameter RIGHT 1; always (*) begin case(state) LEFT: next_state bump_left ? RIGHT : LEFT; RIGHT: next_state bump_right ? LEFT : RIGHT; endcase end实际调试时我发现如果同时触发bump_left和bump_right代码会优先执行先判断的条件。这在游戏中表现为角色碰到墙角时会先响应右侧碰撞这种细微差别需要通过仿真波形仔细观察。2.2 坠落行为的优先级处理Lemmings2引入了坠落状态AAAH这需要增加两个新状态LEFT_AAAH和RIGHT_AAAH。关键点在于处理状态转移的优先级always (*) begin case(state) LEFT: if(~ground) next_state LEFT_AAAH; // 坠落优先 else if(bump_left) next_state RIGHT; else next_state LEFT; // 其他状态处理... endcase end在FPGA开发板上实测时如果不把ground信号判断放在bump信号之前会导致角色在悬崖边碰撞时出现非预期行为。这个教训让我深刻理解了状态优先级的重要性。2.3 多行为交互的状态扩展Lemmings3加入了挖掘行为DIG状态数量扩展到8个。此时需要明确行为优先级坠落 挖掘 转向。代码中通过if-else的层级关系体现这一点always (*) begin case(state) LEFT: if(~ground) next_state LEFT_AAAH; else if(dig) next_state LEFT_DIG; else if(bump_left) next_state RIGHT; else next_state LEFT; // 其他状态处理... endcase end在资源受限的CPLD器件上实现时我最初使用二进制编码导致时序违例。后来改用独热码one-hot编码方式虽然多用了一些触发器但显著提高了最大时钟频率。2.4 状态持续时间的计数控制Lemmings4新增了飞溅SPLATTER状态需要计数器跟踪坠落时间。这里展示了如何在状态机中集成数据路径reg [7:0] fall_cnt; always (posedge clk) begin if(state LEFT_AAAH || state RIGHT_AAAH) fall_cnt fall_cnt 1; else fall_cnt 0; end always (*) begin case(state) LEFT_AAAH: if(ground) next_state (fall_cnt 20) ? SPLATTER : LEFT; // 其他状态处理... endcase end实际项目中计数器位宽需要根据最大坠落时间合理设置。我曾遇到过计数器溢出导致的bug后来增加了饱和处理逻辑才解决。3. 通信协议解析的状态机设计3.1 PS/2协议的三字节消息识别PS2题目要求识别三字节消息的起始边界。关键点在于第一个字节的bit[3]1这个特征parameter IDLE 0; parameter BYTE1 1; parameter BYTE2 2; parameter BYTE3 3; always (*) begin case(state) IDLE: next_state in[3] ? BYTE1 : IDLE; BYTE1: next_state BYTE2; BYTE2: next_state BYTE3; BYTE3: next_state in[3] ? BYTE1 : IDLE; endcase end在真实PS/2设备测试时我发现有些设备会在字节间插入额外时钟周期。为此增加了超时返回IDLE状态的保护机制提高了协议解析的健壮性。3.2 数据路径的集成设计PS2data题目需要在状态机基础上增加数据采集功能。这里展示了如何缓存接收到的三个字节reg [23:0] byte_rx; always (posedge clk) begin if(next_state BYTE1) byte_rx {byte_rx[15:0], in}; else if(next_state BYTE2) byte_rx {byte_rx[15:0], in}; else if(next_state BYTE3) byte_rx {byte_rx[15:0], in}; else if(reset) byte_rx 0; end调试时遇到过一个典型问题当连续接收多组数据时第二组数据的第一个字节会覆盖第一组数据的第三个字节。通过增加字节有效标志信号解决了这个数据对齐问题。3.3 串行通信协议的完整解析Serial题目要求处理包含起始位、数据位和停止位的串行协议。这个11状态的状态机需要精确控制每个比特的采样时机parameter IDLE 0; parameter START 1; parameter DATA_ONE 2; // ...其他数据位状态 parameter STOP 10; always (*) begin case(state) IDLE: next_state ~in ? START : IDLE; START: next_state DATA_ONE; DATA_ONE: next_state DATA_TWO; // ...其他状态转移 DATA_EIGHT: next_state in ? STOP : WAIT; WAIT: next_state in ? IDLE : WAIT; STOP: next_state in ? IDLE : START; endcase end在实际UART通信中我增加了波特率时钟生成模块确保在每个数据位的中间时刻采样。同时添加了奇偶校验位验证功能使协议解析更加完整可靠。4. 状态机设计的工程实践要点4.1 三段式编码的最佳实践经过多个项目验证我总结出三段式编码的几个关键点状态寄存器使用非阻塞赋值状态转移逻辑使用阻塞赋值输出逻辑尽量使用组合逻辑为每个状态添加默认输出值复位信号要覆盖所有寄存器// 第一段状态寄存器 always (posedge clk or posedge reset) if(reset) state IDLE; else state next_state; // 第二段状态转移 always (*) begin next_state state; // 默认保持当前状态 case(state) IDLE: if(cond) next_state NEXT; // 其他状态... endcase end // 第三段输出逻辑 assign output (state SOME_STATE);4.2 状态编码方式的选择根据设计需求选择合适的编码方式二进制编码最省资源但状态译码逻辑复杂独热码每个状态用一个触发器译码简单格雷码适合需要避免毛刺的场合在Lemmings4中我对比了不同编码方式二进制编码使用4个触发器独热码使用9个触发器格雷码使用4个触发器最终根据时序要求选择了独热码因为虽然多用了一些寄存器但将最大时钟频率从80MHz提升到了125MHz。4.3 状态机的验证方法有效的验证策略包括仿真时添加状态覆盖检查在硬件上使用LED指示关键状态添加调试接口输出当前状态值对边界条件进行充分测试我在项目中常使用SystemVerilog断言来检查状态机属性assert property ((posedge clk) (state LEFT_AAAH ground) | (state LEFT || state SPLATTER));这种验证方法在复杂状态机调试中特别有效可以快速定位违反设计假设的情况。