第二讲:状态转移图——从抽象到可视化的关键一步
状态机这东西,说白了就是“下一步该干啥”的逻辑。但光靠文字描述,你很难把逻辑理清楚。我刚开始做FPGA时,就吃过这个亏——直接写代码,结果状态跳来跳去,最后仿真一看,全乱套了。
后来我养成了一个习惯:先画图,再写代码。状态转移图,就是帮你把脑子里的逻辑“画”出来的工具。今天咱们就聊聊这个。
一、状态转移图的基本元素
一张完整的状态转移图,其实就三样东西:状态、转移条件、输出。咱们一个一个说。
1. 状态(State)
状态就是“当前在干什么”。比如一个简单的交通灯,红灯、黄灯、绿灯就是三个状态。在图中,我习惯用圆角矩形或圆圈来表示状态,里面写上状态名。
嗯,这里要注意:状态名最好用英文或拼音,别用中文。我在项目中见过有人用“红灯”作为状态名,结果综合工具报错,折腾了半天。
2. 转移条件(Transition Condition)
转移条件就是“什么时候从当前状态跳到下一个状态”。用带箭头的线连接两个状态,线上标注条件。比如“红灯→绿灯”的条件是“计时5秒结束”。
条件可以是组合逻辑,也可以是边沿触发。我个人习惯把条件写简洁,比如 cnt == 5 或者 start_i,别写太复杂的表达式,否则图会变得很难看。
3. 输出(Output)
输出就是“在这个状态下,对外发出什么信号”。输出可以写在状态框内部(Moore型),也可以写在转移线上(Mealy型)。我建议初学者先学Moore型,逻辑更清晰。
状态 = 当前在干啥
转移条件 = 啥时候换下一个
输出 = 干这个事时对外发啥信号
二、手绘状态转移图的方法
你可能会说:“现在都用工具画图了,谁还手绘?” 但我告诉你,手绘是快速理清思路的最好方式。我在项目初期,经常拿张A4纸,边想边画,画完再敲代码。
手绘的步骤很简单:
- 先列状态:把所有的状态写在一张纸上,比如 IDLE、READ、WRITE、DONE。
- 画状态框:每个状态画一个圆角矩形,均匀分布在纸上。
- 画转移箭头:从初始状态开始,按逻辑顺序画箭头。箭头指向下一个状态。
- 标条件:在箭头上写转移条件。如果条件不满足,就留在当前状态(画一个自环箭头)。
- 标输出:在状态框内或箭头上标注输出信号。
画图时先用铅笔,因为肯定要改。我画第一版时,状态数往往比实际多,画着画着就发现有些状态可以合并。这时候橡皮一擦,比在电脑上改快多了。
为什么会这样?因为人脑在处理图形信息时,比处理文字快得多。你画一遍图,相当于把逻辑在脑子里跑了一遍,很多隐藏的问题就暴露出来了。
三、从需求到状态图的转换
这是最核心的部分。我拿一个实际项目举例:一个简单的串口接收模块。
需求是这样的:
- 空闲时等待起始位(低电平)
- 检测到起始位后,开始接收8位数据
- 接收完8位后,等待停止位(高电平)
- 停止位正确后,输出接收到的数据
好,咱们一步步转成状态图。
第一步:提取状态
从需求里找“状态”。我一眼就看出来:IDLE(空闲)、START(起始位)、DATA(接收数据)、STOP(停止位)。一共4个状态。
第二步:确定转移条件
| 当前状态 | 转移条件 | 下一状态 |
|---|---|---|
| IDLE | rxd == 0(检测到起始位) | START |
| START | 位时钟计数到中间点 | DATA |
| DATA | 已接收8位 | STOP |
| STOP | rxd == 1(检测到停止位) | IDLE |
第三步:画状态转移图
下面是我用SVG画的一张图,展示了这个串口接收的状态机逻辑:
这张图里,每个状态框我都标了输出。比如在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
你看,代码结构和状态图是一一对应的。图上的每个状态、每个箭头,都能在代码里找到对应的行。这就是为什么我强调先画图再写代码——图对了,代码基本不会错。
状态转移图 = 状态 + 转移条件 + 输出
画图步骤 = 列状态 → 画框 → 画箭头 → 标条件 → 标输出
从需求到图 = 提取状态 → 确定转移 → 画图验证
嗯,这一讲的内容就到这儿。记住,状态转移图不是画给别人看的,是画给自己理清思路的。下次你拿到一个复杂需求,别急着敲代码,先拿张纸画一画。你会发现,很多问题在画图阶段就解决了。