第二讲:状态转移图——从抽象到可视化的关键一步

状态机这东西,说白了就是“下一步该干啥”的逻辑。但光靠文字描述,你很难把逻辑理清楚。我刚开始做FPGA时,就吃过这个亏——直接写代码,结果状态跳来跳去,最后仿真一看,全乱套了。

后来我养成了一个习惯:先画图,再写代码。状态转移图,就是帮你把脑子里的逻辑“画”出来的工具。今天咱们就聊聊这个。

一、状态转移图的基本元素

一张完整的状态转移图,其实就三样东西:状态、转移条件、输出。咱们一个一个说。

1. 状态(State)

状态就是“当前在干什么”。比如一个简单的交通灯,红灯、黄灯、绿灯就是三个状态。在图中,我习惯用圆角矩形或圆圈来表示状态,里面写上状态名。

嗯,这里要注意:状态名最好用英文或拼音,别用中文。我在项目中见过有人用“红灯”作为状态名,结果综合工具报错,折腾了半天。

2. 转移条件(Transition Condition)

转移条件就是“什么时候从当前状态跳到下一个状态”。用带箭头的线连接两个状态,线上标注条件。比如“红灯→绿灯”的条件是“计时5秒结束”。

条件可以是组合逻辑,也可以是边沿触发。我个人习惯把条件写简洁,比如 cnt == 5 或者 start_i,别写太复杂的表达式,否则图会变得很难看。

3. 输出(Output)

输出就是“在这个状态下,对外发出什么信号”。输出可以写在状态框内部(Moore型),也可以写在转移线上(Mealy型)。我建议初学者先学Moore型,逻辑更清晰。

核心记忆点:
状态 = 当前在干啥
转移条件 = 啥时候换下一个
输出 = 干这个事时对外发啥信号

二、手绘状态转移图的方法

你可能会说:“现在都用工具画图了,谁还手绘?” 但我告诉你,手绘是快速理清思路的最好方式。我在项目初期,经常拿张A4纸,边想边画,画完再敲代码。

手绘的步骤很简单:

  1. 先列状态:把所有的状态写在一张纸上,比如 IDLE、READ、WRITE、DONE。
  2. 画状态框:每个状态画一个圆角矩形,均匀分布在纸上。
  3. 画转移箭头:从初始状态开始,按逻辑顺序画箭头。箭头指向下一个状态。
  4. 标条件:在箭头上写转移条件。如果条件不满足,就留在当前状态(画一个自环箭头)。
  5. 标输出:在状态框内或箭头上标注输出信号。
我的小技巧:
画图时先用铅笔,因为肯定要改。我画第一版时,状态数往往比实际多,画着画着就发现有些状态可以合并。这时候橡皮一擦,比在电脑上改快多了。

为什么会这样?因为人脑在处理图形信息时,比处理文字快得多。你画一遍图,相当于把逻辑在脑子里跑了一遍,很多隐藏的问题就暴露出来了。

三、从需求到状态图的转换

这是最核心的部分。我拿一个实际项目举例:一个简单的串口接收模块。

需求是这样的:

  • 空闲时等待起始位(低电平)
  • 检测到起始位后,开始接收8位数据
  • 接收完8位后,等待停止位(高电平)
  • 停止位正确后,输出接收到的数据

好,咱们一步步转成状态图。

第一步:提取状态

从需求里找“状态”。我一眼就看出来:IDLE(空闲)、START(起始位)、DATA(接收数据)、STOP(停止位)。一共4个状态。

第二步:确定转移条件

当前状态 转移条件 下一状态
IDLE rxd == 0(检测到起始位) START
START 位时钟计数到中间点 DATA
DATA 已接收8位 STOP
STOP rxd == 1(检测到停止位) IDLE

第三步:画状态转移图

下面是我用SVG画的一张图,展示了这个串口接收的状态机逻辑:

IDLE START DATA STOP rxd==0 位时钟到中点 已收8位 rxd==1 rxd==1 输出: data_valid=0 输出: shift_en=1 输出: data_valid=1

这张图里,每个状态框我都标了输出。比如在DATA状态,输出 shift_en=1,表示正在移位接收数据。在STOP状态,输出 data_valid=1,表示数据已经准备好了。

我曾经踩过的坑:
画图时漏掉了自环条件。比如IDLE状态,如果rxd一直为1,应该留在IDLE。如果不画这个自环箭头,别人看你的图会以为IDLE会无条件跳走。所以,每个状态都要考虑“条件不满足时怎么办”。

四、从状态图到代码的映射

图画好了,写代码就是水到渠成的事。我一般用三段式状态机写法:

// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    state <= IDLE;
  else
    state <= next_state;
end

// 第二段:次态逻辑
always @(*) begin
  case (state)
    IDLE:   next_state = (rxd == 0) ? START : IDLE;
    START:  next_state = (bit_mid)  ? DATA  : START;
    DATA:   next_state = (bit_cnt == 8) ? STOP : DATA;
    STOP:   next_state = (rxd == 1) ? IDLE  : STOP;
    default: next_state = IDLE;
  endcase
end

// 第三段:输出逻辑
always @(*) begin
  case (state)
    IDLE:   {data_valid, shift_en} = 2'b00;
    START:  {data_valid, shift_en} = 2'b00;
    DATA:   {data_valid, shift_en} = 2'b01;
    STOP:   {data_valid, shift_en} = 2'b10;
    default: {data_valid, shift_en} = 2'b00;
  endcase
end

你看,代码结构和状态图是一一对应的。图上的每个状态、每个箭头,都能在代码里找到对应的行。这就是为什么我强调先画图再写代码——图对了,代码基本不会错。

总结一下:
状态转移图 = 状态 + 转移条件 + 输出
画图步骤 = 列状态 → 画框 → 画箭头 → 标条件 → 标输出
从需求到图 = 提取状态 → 确定转移 → 画图验证

嗯,这一讲的内容就到这儿。记住,状态转移图不是画给别人看的,是画给自己理清思路的。下次你拿到一个复杂需求,别急着敲代码,先拿张纸画一画。你会发现,很多问题在画图阶段就解决了。

可可精品资料库