09. 状态机与模块化设计
状态机适合描述“系统当前在哪一步、什么条件进入下一步、每一步输出什么”。写代码前先画状态图,能显著减少遗漏。
状态机的五个问题
跳转到“状态机的五个问题”- 有哪些状态?
- 复位进入哪个状态?
- 每个状态在什么条件下转移?
- 每个状态输出什么?
- 非法状态怎样恢复?
例如一个简单发送控制器:
IDLE --start--> SEND --last_byte--> DONE --ack--> IDLE ^ | | +---- reset -----+-------- reset ----+Moore 与 Mealy
跳转到“Moore 与 Mealy”- Moore:输出只由当前状态决定;输出通常更稳定,可能比输入响应晚一个状态/周期。
- Mealy:输出由当前状态和输入共同决定;响应更直接,但组合输入变化可能影响输出。
选择取决于接口时序需求,不是简单判断谁“更高级”。跨模块控制脉冲常优先考虑寄存输出,便于时序分析和消除组合毛刺。
三段式结构
跳转到“三段式结构”
第一段:状态寄存器
跳转到“第一段:状态寄存器”always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state;end第二段:下一状态组合逻辑
跳转到“第二段:下一状态组合逻辑”always @(*) begin next_state = state;
case (state) IDLE: begin if (start) next_state = SEND; end
SEND: begin if (last_byte) next_state = DONE; end
DONE: begin if (ack) next_state = IDLE; end
default: next_state = IDLE; endcaseend开头 next_state = state 表示默认留在当前状态,确保所有路径都有赋值。default 让非法编码回到安全状态。
第三段:输出逻辑
跳转到“第三段:输出逻辑”组合 Moore 输出:
always @(*) begin busy = 1'b0; done = 1'b0;
case (state) SEND: busy = 1'b1; DONE: done = 1'b1; default: begin busy = 1'b0; done = 1'b0; end endcaseend如果输出必须无毛刺或与总线严格对齐,可把第三段写成时序输出,但要重新核对输出延迟和复位行为。
完整模块
跳转到“完整模块”module send_controller ( input wire clk, input wire rst_n, input wire start, input wire last_byte, input wire ack, output reg busy, output reg done);
localparam IDLE = 2'b00; localparam SEND = 2'b01; localparam DONE = 2'b10;
reg [1:0] state; reg [1:0] next_state;
always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end
always @(*) begin next_state = state; case (state) IDLE: if (start) next_state = SEND; SEND: if (last_byte) next_state = DONE; DONE: if (ack) next_state = IDLE; default: next_state = IDLE; endcase end
always @(*) begin busy = 1'b0; done = 1'b0; case (state) SEND: busy = 1'b1; DONE: done = 1'b1; default: begin busy = 1'b0; done = 1'b0; end endcase end
endmodule状态编码
跳转到“状态编码”三种常见方式:
- 二进制编码:状态位少,如 8 个状态用 3 位;
- 独热码:每个状态占一位,译码简单,在 FPGA 中常有良好速度;
- 格雷码:相邻状态尽量只变一位,适合部分特定场景。
初学时先用清晰的 localparam。性能需要优化时,让综合器选择或根据报告调整,不要凭印象认定某种编码永远最好。
状态机常见错误
跳转到“状态机常见错误”- 下一状态没有默认值,推导锁存器;
case没有default,非法状态无法恢复;- 状态寄存器使用阻塞赋值;
- 输出和转移条件混在一大段代码中,维护困难;
- 输入是异步信号,未经同步就直接影响状态转移;
- 状态含义和转移条件没有画图或写清楚。
模块化设计
跳转到“模块化设计”
大型设计应自顶向下拆分:
top├── clock_reset├── input_filter├── controller├── datapath└── output_driver拆分原则:模块功能相对独立、内部连接紧密、模块间接口简单、时钟域明确。顶层主要做实例化和连接:
send_controller u_send_controller ( .clk (sys_clk), .rst_n (sys_rst_n), .start (start_pulse), .last_byte (last_byte), .ack (ack), .busy (busy), .done (done));每个子模块先独立仿真,再做顶层集成,定位问题会容易得多。
练习
跳转到“练习”设计一个交通灯控制器,包含 GREEN/YELLOW/RED 三个状态。先画状态图,再写三段式代码。每个状态用计数器停留指定周期,计数到终值后转移。testbench 应至少运行两个完整循环并检查状态顺序。