跳转到内容
新建笔记

09. 状态机与模块化设计

状态机适合描述“系统当前在哪一步、什么条件进入下一步、每一步输出什么”。写代码前先画状态图,能显著减少遗漏。

  1. 有哪些状态?
  2. 复位进入哪个状态?
  3. 每个状态在什么条件下转移?
  4. 每个状态输出什么?
  5. 非法状态怎样恢复?

例如一个简单发送控制器:

IDLE --start--> SEND --last_byte--> DONE --ack--> IDLE
^ | |
+---- reset -----+-------- reset ----+
  • 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;
endcase
end

开头 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
endcase
end

如果输出必须无毛刺或与总线严格对齐,可把第三段写成时序输出,但要重新核对输出延迟和复位行为。

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。性能需要优化时,让综合器选择或根据报告调整,不要凭印象认定某种编码永远最好。

  1. 下一状态没有默认值,推导锁存器;
  2. case 没有 default,非法状态无法恢复;
  3. 状态寄存器使用阻塞赋值;
  4. 输出和转移条件混在一大段代码中,维护困难;
  5. 输入是异步信号,未经同步就直接影响状态转移;
  6. 状态含义和转移条件没有画图或写清楚。

模块层次与验证流程

大型设计应自顶向下拆分:

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 应至少运行两个完整循环并检查状态顺序。

上一篇:函数、任务与仿真 · 返回目录 · 下一篇:综合项目与规范