ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Verilog状态机设计实战:三段式写法与按键消抖案例解析

Verilog状态机设计实战:三段式写法与按键消抖案例解析 干了这么多年FPGA说句实话状态机这个名字听起来像教科书里才有的东西但它其实是每个写Verilog的人天天都在用的基本功。不管是写UART、SPI、I2C这类通信协议还是做按键消抖、DDR3读写控制、CRC计算翻来覆去干的事情就一件用状态机把一段有时序要求的过程“编排”出来。甚至可以说状态机的设计水平直接决定了你这段RTL代码是好维护还是留坑给后人。这篇文章我打算用实际工程的角度把Verilog状态机的设计思路、三段式写法、完整案例和调试经验串一遍。不管你是刚开始学Verilog的新手还是已经写了几年RTL、想系统整理一下状态机方法的工程师应该都能从里边找到点能直接用的东西。我尽量少讲虚的多讲怎么做、为什么这么做、坑在哪里。1. 状态机的本质数字电路里的“带记忆调度器”1.1 用生活里的现象理解状态机很多初学者一上来就是背定义有限状态机是指状态数量有限任意时刻系统处于其中一个状态且状态的迁移由输入和当前状态共同决定的时序逻辑模型。这定义没错但很难让人产生直觉。我更喜欢换一个说法状态机就是一个“带记忆的时钟节拍执行器”。每个时钟沿到来的时候它看一眼自己当前处在哪个“阶段”再结合外部输入决定下一步去哪个“阶段”同时给出当前阶段对应的输出。生活里最常见的例子就是自动售货机。你投一块钱它不会立刻让你选可乐你投到足够金额它进入“可选购”状态你选完商品、出货、找零它又回到“待投币”状态。整个流程是有先后顺序的系统必须记住“现在进行到哪一步”否则就会乱套。红绿灯也是同理绿-黄-红-绿循环每个状态停留固定时间再切换过去。数字电路里的状态机和这个逻辑一模一样只是“记住进行到哪一步”这件事是靠寄存器组来完成的切换节奏由时钟信号统一控制。所以概括起来就一句话用时序逻辑记录“我在第几步”用逻辑去算“下一步去哪”和“这一步输出什么”。理解了这一点再看状态机里那几个概念就非常顺了。状态寄存器保存当前状态次态逻辑根据当前状态和输入计算下一个状态输出逻辑根据当前状态Moore或加上输入Mealy给出输出信号。整个状态机在每一个时钟沿都完成一次“判断-迁移-输出”的循环周而复始直到复位信号把它拉回初始状态。1.2 什么情况下该用状态机做数字逻辑设计时只要遇到下面三类场景中的任何一类我就基本会考虑上状态机操作步骤有严格的先后顺序比如I2C读EEPROM必须先发送设备地址再等待ACK接着发寄存器地址最后才能读数据同一个流程中根据不同的输入走不同的分支比如轮询仲裁器根据各个请求信号决定把总线授权给谁某个环节需要一直等待一个条件满足才能继续比如UART接收器要等待起始位出现然后才开始采样数据位。经典的应用场景几乎覆盖了数字逻辑的方方面面。总线协议方面有I2C读写EEPROM、SPI主从通信、UART收发、HDLC帧处理外设控制方面有按键消抖、LCD/OLED驱动、电机步进控制数据处理方面有CRC计算、FIFO读写仲裁、滑动窗口滤波再往高性能方向走Cache的替换策略与写回流程、CPU流水线控制、FIR滤波器的系数加载背后全是状态机在调度。我说一个自己踩过的例子。前几年接手过一版DDR3读写控制器里面没有使用独立清晰的状态机而是用了一大堆使能信号和计数器互相配合。功能上能跑通但你想调整某一笔读操作的时序牵一发动全身改完这个信号又影响另一个模块调试成本非常高。后来我花了两天时间把它重构为标准的状态机结构每一个操作阶段对应一个状态改参数只影响特定状态的转移条件可维护性立刻就不一样了。这也就是为什么“用状态机收敛复杂度”这个提法在硬件设计里同样成立状态机是逻辑复杂度的收纳盒它强迫你按阶段而不是按信号去思考问题。1.3 Moore型还是Mealy型输出时机的取舍状态机的两个基本流派Moore型和Mealy型区别就在于输出和输入之间的关系。Moore型的输出只取决于当前状态和当前输入无关。也就是说不管输入怎么变只要状态不变输出就一直保持。这种特性带来一个很大的好处输出信号稳定不会因为输入上的毛刺而抖动时序上更容易收敛。代价是输出相对于输入的变化会晚一个周期因为状态切换需要时钟沿来触发。Mealy型的输出由当前状态和当前输入共同决定。输入变化的当下组合逻辑的输出立刻响应不需要等时钟沿所以响应速度更快。但代价也很明显如果输入信号本身有毛刺或者不稳定输出就会跟着毛刺时序分析也更难做。从工程选型的角度看凡是协议时序里对“输出必须严格对齐某个时钟沿”有要求的我都建议用Moore型省事又安全。比如I2C的SCL和SDA时序、UART的波特率定时输出需要确定性Moore型明显更合适。如果应用场景要求“输入条件满足的同一拍就要切换输出”比如某些高速数据通路里的旁路选择那Mealy型可能更合适但输出最好打一拍寄存器再送出去避免组合毛刺直接打到后级。对比项Moore型Mealy型输出依据仅当前状态当前状态 当前输入输出时序状态更替后稳定输入变化立即响应抗毛刺能力强弱输出可能带毛刺响应速度慢一拍快同一拍内响应典型场景协议时序、按键消抖高速数据选择、条件判断初学阶段不用太纠结选型先把Moore型三段式写熟练等到实际项目中遇到时序瓶颈再去尝试Mealy型那时候你会更容易理解两者的差异。2. 三段式写法工程中最用得住的FSM风格2.1 从一段式到三段式演化过程说白了就一个字“拆”很多Verilog教材喜欢按always块的数量把状态机分成一段式、二段式、三段式。我当年自学的时候也觉得这就是风格问题根本没有实际差别直到自己动手写了一个串口控制器才发现写法直接决定了这个模块好不好调、好不好改。一段式状态机就是所有逻辑塞进一个always块里状态跳转和输出全在同一个时序块中完成。代码最短看起来最“紧凑”但问题很大状态迁移和输出逻辑混在一起你要单独看某个输出是怎么来的必须把整个always块从头到尾读一遍而且因为输出在时序块里赋值很难单独对某个输出做特殊处理比如寄存器打拍、输出使能灵活性很差。这种写法只适合超级简单的例子练手可以上工程不建议。二段式状态机把状态迁移和次态计算分开。第一段是时序逻辑负责把next_state更新为current_state第二段是组合逻辑根据current_state和输入信号计算出下一个状态和全部输出。相比一段式结构已经清晰不少代码也更容易阅读。但二段式的输出往往是组合逻辑直接产生的会有毛刺风险而且输出逻辑和次态逻辑混在同一个组合块里稍微复杂一点就变得臃肿。三段式状态机在二段式的基础上再拆一步第一段时序逻辑负责状态寄存器更新第二段组合逻辑只负责次态计算第三段逻辑负责输出。第三段既可以是组合逻辑也可以改成时序逻辑把输出打一拍再做寄存器输出抗毛刺能力更强。这是我在工程中最常采用的写法也是推荐初学者直接学习的写法。这三种写法不是简单的“哪一种行哪一种不行”而是随着设计复杂度提升逻辑的“边界”越来越清晰。一段式把状态和输出混在一起二段式把状态和输出分开三段式把状态跳转、次态计算、输出三件事彻底隔离每一段只回答一个问题现在在哪、下一步去哪、这一步输出什么。设计者心里清楚阅读代码的人也能一目了然。2.2 三段式的标准模板逐段拆解三段式状态机的基本框架如下// 第一段状态寄存器时序逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state IDLE; else current_state next_state; end // 第二段次态计算组合逻辑 always (*) begin next_state current_state; // 默认保持避免锁存器 case (current_state) IDLE: begin if (start_req) next_state BUSY; end BUSY: begin if (done) next_state IDLE; end default: next_state IDLE; endcase end // 第三段输出逻辑这里使用时序输出 always (posedge clk or negedge rst_n) begin if (!rst_n) busy_flag 1b0; else if (current_state BUSY) busy_flag 1b1; else busy_flag 1b0; end这里有几个非常关键的点是我在实际开发中反复体会出来的。第一第二段的组合逻辑里next_state的默认值一定要在case之前赋值为current_state也就是所谓的“默认保持”。很多初学者不写这一句结果组合逻辑里有些状态分支没写到综合器推断出不想要的锁存器功能直接跑偏。记住组合逻辑里对未覆盖的条件必须给一个确定的默认赋值否则综合结果就不是你想要的东西。第二第二段用的是阻塞赋值第一段和第三段如果做时序输出用的是非阻塞赋值。这个规矩绝对不能乱。组合逻辑用阻塞赋值保证计算立即生效时序逻辑用非阻塞赋值保证状态统一在时钟沿更新。混用会带来仿真行为和综合结果不一致的问题排查起来极其痛苦。第三第三段输出逻辑我推荐直接写成时序逻辑也就是把输出信号也当作寄存器来打一拍。这样输出的毛刺会大大减少代价是输出信号相比组合逻辑输出晚一个时钟周期。如果这个输出要控制外部设备比如按键消抖后的脉冲信号晚一个周期根本无所谓如果输出要严格对齐某个协议时序那就需要重新评估。2.3 三种写法怎么选写法状态跳转次态计算输出逻辑优势劣势一段式时序混合混合代码少难读难改不推荐二段式时序组合组合混合结构较清晰输出易毛刺组合逻辑臃肿三段式时序组合组合或时序可读性最强输出可控代码量略大我的个人建议很直接工程代码一律用三段式。不是二段式或者一段式一定跑不起来而是当设计规模变大、需要多人协作维护时三段式的清晰边界能省下大量沟通成本。你拿到一段三段式代码不需要从头读到尾就能定位“状态转移在哪、输出在哪”这种心智负担的降低在长期维护中价值远大于多写几行代码的成本。3. 完整案例按键消抖状态机的代码实现3.1 为什么选按键消抖做案例按键消抖是FPGA入门里非常经典的状态机实例原因有三需求每个人都理解不需要复杂的协议背景状态数量少通常4个状态就能搞定但它涵盖了状态划分、计数判决、状态转移、输出控制这些状态机设计的核心要点。看完这个例子很多基础的FSM设计思路就可以直接迁移到别的模块上。先说说消抖到底在消什么。机械按键在按下和释放的瞬间金属触点之间会发生多次快速通断持续时间通常在5到10毫秒这个现象叫机械抖动。如果不对信号做处理单片机和FPGA会把一次按下误判成多次按下。传统的RC电路可以消抖但在FPGA里更方便可靠的做法是“逻辑消抖”对按键信号持续采样只有电平稳定超过一定时间比如10ms才认为是一次有效的按下或释放。3.2 状态划分与转移条件我设计的按键消抖状态机包含4个状态IDLE空闲状态等待按键按下。检测到key_in为低电平按下有效时进入按下稳定状态PRESS_CONFIRM按下稳定状态。在这个状态里持续计数如果key_in保持低电平超过设定的消抖时间说明按键确实被按下产生按键按下脉冲并进入释放等待状态如果中途又变回高电平说明是抖动返回IDLEWAIT_RELEASE释放等待状态等待按键释放。检测到key_in变高时进入释放确认状态RELEASE_CONFIRM释放确认状态。同样需要持续计数确认电平稳定为高后才认为完成一次完整按键操作产生释放脉冲并返回IDLE如果中途又变低说明还在抖动回到释放等待。整个设计里每个状态的职责非常清楚两个“等待进入”的状态负责检测沿变化两个“确认”的状态负责用时间过滤抖动。很多实际按键消抖例子只用两个状态加计数器也能实现但加上释放确认后输出的key_pressed和key_released脉冲都是经过消抖判定的信号质量明显更好对后级逻辑更友好。3.3 RTL代码与关键设计点下面是完整的RTL代码module key_debounce_fsm #( parameter CLK_FREQ 50_000_000, parameter DEBOUNCE_MS 10 ) ( input wire clk, input wire rst_n, input wire key_in, output reg key_pressed, output reg key_released ); // 消抖计数最大值 10ms * 50MHz 500_000 localparam integer CNT_MAX DEBOUNCE_MS * CLK_FREQ / 1000; // 状态编码 localparam S_IDLE 2d0; localparam S_PRESS_CONFIRM 2d1; localparam S_WAIT_RELEASE 2d2; localparam S_RELEASE_CONFIRM 2d3; reg [1:0] current_state; reg [1:0] next_state; reg [19:0] cnt; reg cnt_rst; // 第一段状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) current_state S_IDLE; else current_state next_state; end // 第二段次态计算 always (*) begin next_state current_state; // 默认保持 case (current_state) S_IDLE: begin if (key_in 1b0) next_state S_PRESS_CONFIRM; end S_PRESS_CONFIRM: begin if (cnt CNT_MAX) begin if (key_in 1b0) next_state S_WAIT_RELEASE; else next_state S_IDLE; end end S_WAIT_RELEASE: begin if (key_in 1b1) next_state S_RELEASE_CONFIRM; end S_RELEASE_CONFIRM: begin if (cnt CNT_MAX) begin if (key_in 1b1) next_state S_IDLE; else next_state S_WAIT_RELEASE; end end default: next_state S_IDLE; endcase end // 计数器控制与输出逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_pressed 1b0; key_released 1b0; end else begin cnt 20d0; key_pressed 1b0; key_released 1b0; case (current_state) S_PRESS_CONFIRM: begin if (cnt CNT_MAX) cnt cnt 1b1; else if (key_in 1b0) begin cnt 20d0; key_pressed 1b1; end end S_WAIT_RELEASE: begin if (key_in 1b1) cnt 20d0; end S_RELEASE_CONFIRM: begin if (cnt CNT_MAX) cnt cnt 1b1; else if (key_in 1b1) begin cnt 20d0; key_released 1b1; end end default: cnt 20d0; endcase end end // 显示当前状态仿真或调试用可以接ILA观察 wire [1:0] state_dbg current_state; endmodule这段代码有几个设计点需要特别说明。计数器的控制逻辑我选择放在第三段时序逻辑里和状态寄存器分开但又在同一个时钟节拍下工作。计数器只有在PRESS_CONFIRM和RELEASE_CONFIRM两个状态下才累加其他状态一律清零。这样的好处是计数器只在需要的时候工作功耗更低逻辑也更直观。key_pressed和key_released都是寄存器输出拉高的条件分别是“按下稳定确认”和“释放稳定确认”。进入确认状态并且计数满后输出一个宽度为一个时钟周期的脉冲。这个脉冲信号经过10ms的抖动过滤可以用作后级逻辑的触发信号比如状态机使能、数据锁存或者中断请求。代码里用localparam integer CNT_MAX DEBOUNCE_MS * CLK_FREQ / 1000;替代硬编码的常数这样当你的系统时钟从50MHz改成100MHz或者想调整消抖时间为20ms只需要修改顶层参数不用去代码里漫山遍野找魔法数字。这是我认为新手比较容易忽略但又很重要的工程习惯。3.4 状态编码选型决定面积和功耗的关键设计状态机的时候状态用几个寄存器和什么样的编码方式来表示不是个小事情。三种常见编码方式各有适用场合。二进制编码用最少的寄存器位数来表达全部状态。比如4个状态用2位寄存器状态转移需要比较多的组合逻辑去进行译码。优点是资源占用少缺点是状态切换时多个比特同时翻转如果恰好遇到组合逻辑路径较长时序压力会比较大而且状态跳变瞬间会有中间态出现虽然功能上没问题但低功耗设计里电流尖峰不好看。格雷码的特点是相邻两个状态之间只有一位变化比二进制编码更适合状态连续跳转的应用比如从0到1到2再到3这种顺序循环。由于每次只有一位翻转动态功耗更小时序也更干净。但它不是所有设计都能用只有状态跳转图比较“线”的时候才有效果。独热码是FPGA设计里非常常用的选择。每个状态对应一个寄存器位4个状态就要4位寄存器同一时刻只有一位为1。状态译码逻辑极其简单速度最快很适合状态数量不多一般不超过16个的控制状态机。代价是寄存器数量翻倍但对于寄存器资源丰富的现代FPGA来说这几乎不是问题。很多综合工具遇到写好的FSM会自动选择独热码综合就因为它在速度和面积之间取得了很好的平衡。我在这个按键消抖例子里用了两位二进制编码是因为状态数只有4个最简单的编码已经足够而且综合工具通常也会自动优化。如果你的状态机状态数在8到20之间我建议直接给状态用独热码编码写起来清晰综合出来的电路时序也更好。ASIC设计里则需要根据面积和功耗的要求再仔细权衡不能一概而论。4. 用testbench把状态机验透4.1 testbench的基本骨架RTL写完并不代表结束真正的重头戏是仿真验证。状态机的测试不外乎几个步骤生成时钟和复位施加激励输入监控输出是否符合预期。我把按键消抖模块的testbench框架写出来思路可以直接迁移到任何其他状态机的测试里。timescale 1ns / 1ps module tb_key_debounce_fsm; reg clk; reg rst_n; reg key_in; wire key_pressed; wire key_released; key_debounce_fsm #( .CLK_FREQ(50_000_000), .DEBOUNCE_MS(10) ) uut ( .clk(clk), .rst_n(rst_n), .key_in(key_in), .key_pressed(key_pressed), .key_released(key_released) ); // 50MHz时钟周期20ns initial clk 0; always #10 clk ~clk; // 按键输入初始化为空闲电平高电平 initial begin rst_n 1b0; key_in 1b1; #100; rst_n 1b1; #100; // 场景1正常的按下-释放过程 key_in 1b0; #600000; // 12ms超过消抖时间10ms key_in 1b1; #600000; // 场景2按下过程包含短抖动脉冲 key_in 1b0; #100000; // 2ms key_in 1b1; #50000; // 1ms抖动 key_in 1b0; #600000; // 12ms稳定按下 #200000; $finish; end endmodule这段testbench里有一个值得注意的细节我故意在场景2里加入了一个1ms的抖动脉冲然后再进入稳定按下。如果状态机设计正确这个1ms的抖动不会触发key_pressed必须等到后续12ms的稳定低电平才会输出按下脉冲。这种“针对边界情况的激励”是testbench里最有价值的部分它验证的正是状态机的核心逻辑——时间判决。4.2 状态机测试的场景覆盖清单写状态机testbench最忌只测一条“正常路径”。跑一遍正常流水线看着波形里状态变化一切正常就以为完事了结果一上板一按按钮就出问题这样的情况我见过太多次。一个负责任的状态机验证至少要覆盖以下几类场景正常完整流程从初始状态开始走完所有状态的迁移验证每个迁移条件满足后确实切到了目标状态每个状态的非法输入比如消抖确认状态中途输入变回高电平应该回到IDLE而不是保持在确认状态边界时序计数刚好在CNT_MAX附近时输入变化确保判决逻辑没有计数溢出或者提前输出复位行为任何一个时间点拉低复位状态机构清晰回到初始状态所有输出复位为默认值输出脉冲宽度确认key_pressed等输出信号确实只拉高一个时钟周期而不是持续多个周期。覆盖完这些场景之后再配合覆盖率工具的统计数据才能对状态机的验证质量有信心。如果没有覆盖率工具那就靠你自己“穷举”状态机所有状态和主要分支的转移路径逐一在testbench里写出来。4.3 仿真流程和波形检查要点关于“Modelsim如何仿真verilog文件”这类问题核心操作其实就几步在工程里添加RTL源文件、写testbench文件、编译、加载设计、运行仿真时间、打开波形窗口观察结果。Vivado里的流程也差不多新建工程后把源文件和仿真文件分别加入对应文件夹运行behavioral simulation就能看到波形。波形检查不要只看输出对不对更重要的是看状态寄存器的迁移是否符合预期。把current_state、next_state、key_pressed、key_released、cnt这些关键信号添加到波形窗口里观察每个时钟沿状态是否按照设计跳转。如果current_state在一个状态下停留了两个周期而不是预期的多半是次态逻辑里某个条件没写对如果计数器在不需要累加的状态里跳动就需要检查计数器的使能逻辑。我习惯在testbench里用$display打印关键状态变化信息比如状态跳转到了哪里、计数器达到最大值、输出脉冲产生等。这样即使不看波形也能从终端日志里快速定位是哪个环节出了问题。对于状态数较多的复杂状态机打印日志的效率比盯着波形高很多。5. 状态机设计避坑指南5.1 状态跑飞了怎么办状态机跑飞指的是系统进入了一个未定义的状态然后怎么都跳不回来。这个问题在所有状态机设计里都算是最棘手的故障之一因为它有时候只在特定条件下才会出现复现困难。跑飞的常见原因有三类。第一类是case语句没有default分支状态机遇到非法状态时没有明确的行为。第二类是复位不完整上电或者复位信号释放瞬间状态寄存器没有正确初始化到初始状态系统从任意状态开始运行。第三类是跨时钟域的输入信号没有同步信号上的亚稳态导致状态误判。对应的解决办法很明确。首先是代码层面每个case语句一定要带default分支并且default要做重置处理一般是跳回IDLE。其次是综合层面对于状态寄存器的非法状态做安全处理比如给状态信号加综合属性让工具自动把非法状态引导到安全状态Vivado里可以用(* synthesis_safe_implementation 1 *)来声明。最后是复位设计尽量使用异步复位、同步释放的复位逻辑并且复位信号必须保证足够宽让所有寄存器都完成初始化。我在实际项目中遇到过一次诡异的现象板子上电后状态机偶尔会卡死非要按一次全局复位才恢复。排查了很长时间最后发现是复位信号由外部按键产生低电平持续时间太短导致部分寄存器复位到位了另一部分没有。换成足够宽的复位脉冲再给状态机加上default回跳逻辑之后问题彻底消失。5.2 输出毛刺与意外锁存器组合逻辑输出的毛刺是状态机设计里很隐蔽的问题。当输入信号和状态切换同时发生时组合逻辑的输出可能会出现非常窄的脉冲这种脉冲后级寄存器不一定采到采到了就是错误数据。解决办法之一是把输出打一拍再做寄存器输出也就是三段式里推荐的那种写法。打拍之后输出延迟一个时钟周期但换来的是干净稳定的信号。另一个办法是仔细审查第二段组合逻辑尽量避免让多个输入信号同时参与状态判决时产生竞争。比如判决条件写成if (key_in flag)当key_in和flag变化有时间差时输出就可能产生毛刺。意外锁存器的问题则通常来自case语句分支不全或者if-else没有配套的else。比如组合逻辑里你只写了if (a) b 1b1;而没有else分支综合工具就会推断出一个锁存器来保持原来的值结果电路和你预期完全不一样。解决方法是所有组合逻辑里的条件分支必须完备要么写全else要么在case之前给信号赋默认值。这些习惯养成之后综合报告里的Warning会少很多。5.3 跨时钟域信号与复位设计如果状态机的输入信号来自其他时钟域比如一个异步按键信号、外部芯片的中断信号直接接到状态机的组合逻辑里是很危险的。亚稳态一旦被状态寄存器采到状态就会变成未知跑飞也就不奇怪了。正确处理方式是先用两级触发器同步把异步信号变成与本地时钟同步的信号再送入状态机逻辑。两级触发器同步是跨时钟域处理里最基础的手段对于单bit电平信号足够用了。如果是脉冲信号跨时钟域还需要根据具体场景考虑握手或者异步FIFO这里不展开。复位设计同样值得重视。状态机的复位信号必须保证在释放的时候不会出现亚稳态一般做法是异步复位、同步释放。也就是复位信号进入时钟域后先用两级触发器同步一下再作为复位使用这样复位的释放沿和时钟沿对齐避免复位释放瞬间出现的亚稳态。5.4 命名规范与协作习惯最后说一个不是很技术但很重要的点状态机的命名规范。我强烈建议在工程里统一状态变量的命名格式比如状态寄存器一律叫current_state和next_state状态定义用S_前缀开头且全大写参数声明用localparam而不是define。localparam的作用域只在模块内部不会跨文件污染其他模块而define是全局宏定义很容易产生重名冲突排查起来相当痛苦。一个模块内状态机数量多于1个时就用模块名或功能名作为前缀比如uart_rx_state、spi_fsm避免信号名重复。这些规范在个人项目里可能看不出太大差别但在多人协作的工程里能帮你省掉大量的无效沟通时间。接口的时序清晰、状态命名可读才是状态机设计里真正的“收敛复杂度”。按键消抖这个例子虽然小但它把状态机的核心知识点全部串了起来状态划分、次态计算、输出逻辑、计数器配合、仿真验证、边界问题。如果你能把这个例子的每一段代码、每一个状态迁移条件都吃透再遇到更复杂的I2C、SPI、UART控制器无非是状态数量变多一些、转移条件变复杂一些思路是完全相通的。我在实际设计中最大的体会是状态机能不能写稳往往不在于代码本身而在于你在编码之前有没有把“状态图”在脑子里画清楚把每个转移条件问明白动手写代码反而成了最机械的那一步。
返回列表