ARTICLE DETAIL

资讯详情

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

FPGA状态机实战:三段式写法与设计技巧

FPGA状态机实战:三段式写法与设计技巧 前面五篇咱们把Verilog的基本语法、组合逻辑、时序逻辑都过了一遍寄存器、计数器这些基础元件也算熟悉了。这次写的位置比较关键——状态机。说句实在话在FPGA项目里待久了你会发现真正让一个逻辑设计“活”起来的往往是状态机。无论是通信协议解析、图像处理的行场同步控制还是ADC采样时序、PWM波形生成核心骨架基本都是状态机。很多从0开始学FPGA的朋友看到“状态机”三个字就觉得高深其实它没那么玄乎。你回想一下C语言里的switch-case回想一下你每天按部就班的起床流程——闹钟响、洗漱、出门、到公司这就是状态机。在FPGA里状态机就是用一个寄存器记住“现在在干什么”然后根据输入和当前状态决定“下一步干什么”仅此而已。这篇用最通俗的方式把逻辑设计和状态机这块掰开揉碎讲清楚附上完整可跑的代码和调试经验希望能帮你跨过这道坎。1. 为什么状态机是FPGA逻辑设计的核心1.1 状态机的本质让逻辑有“记忆”学习FPGA到一定程度你会发现一个绕不开的问题组合逻辑的输出只取决于当前输入它没有“记忆”。比如两个数相加的加法器输入是3和5输出就是8输入变成1和2输出就变3。它不会记得上一次加了多少、上上一次是多少。可实际项目里绝大多数功能都需要逻辑“记住”自己做到哪一步了串口协议要记住当前在接收起始位还是数据位SPI主机要记住当前发到第几个bit电机控制要记住当前是加速还是匀速还是减速。状态机做的事情就是给逻辑加上“记忆”。它由一个状态寄存器通常是一个always块驱动的reg变量和一组状态转移条件组成。每一拍时钟沿到来时状态寄存器根据当前状态和输入信号更新为下一个状态。就和打游戏存档一样控制器每时每刻都知道“现在在哪个关卡”再根据你按了什么键决定“下一关去哪个地图”。在FPGA工程里状态机用得好不好直接决定一个模块的代码是否清晰、是否容易调试。我见过很多新手把通信协议硬写成一堆if-else嵌套逻辑一复杂就彻底绕晕老一辈工程师则喜欢把所有时序控制塞进一个大计数器里靠计数值分段判断。这两种方式不是不行但代码可读性和可维护性都不如状态机。状态机把“阶段之间如何转换”显式地列出来出了问题看图说话比在几百行if-else里猜流程要舒服太多。1.2 组合逻辑、时序逻辑和状态机的关系想彻底搞懂状态机得先把前面几篇的基础串起来。组合逻辑就是纯门电路运算assign语句实现的那种不涉及时钟、不涉及存储时序逻辑则是寄存器在时钟边沿采样always块里posedge clk那种。状态机本质上是“组合逻辑 时序逻辑”的配合时序逻辑负责记住当前状态组合逻辑负责计算下一个状态和输出。你可以把状态机拆成三个部分来看状态寄存器存放当前状态这是时序逻辑次态逻辑根据当前状态和输入算出下一个状态这是组合逻辑输出逻辑根据当前状态以及输入产生输出信号可以是组合输出也可以是寄存器输出。理解了这三块以后后面讲三段式状态机时你会发现自己其实是在把这三块分别用单独的always块写出来结构会非常清晰。我看到很多初学者纠结“状态机到底是组合逻辑还是时序逻辑”答案是两者结合。状态寄存器那一部分是时序逻辑而状态的跳转判断和输出的生成可以用组合逻辑处理也可以打一拍变成时序逻辑输出。理解了这个层次你就不会把always块里应该写posedge还是写星号搞混了。2. Moore机、Mealy机和状态编码2.1 Moore与Mealy先看清两种模型的差异经典的数字电路教材会把状态机分成Moore型和Mealy型两类。Moore型状态机的输出只取决于当前状态跟输入没有直接关系Mealy型状态机的输出取决于当前状态和输入信号的组合。用白话讲Moore机的输出“晚半拍知道输入”Mealy机的输出“一看输入马上变”。怎么理解这个差异举交通灯的例子Moore机就是红灯亮30秒、绿灯亮30秒输出只跟“现在处于哪个灯的状态”有关即使来了一辆车按喇叭也不会立刻变灯Mealy机就像一个带紧急按钮的信号灯按下按钮输入变化输出立刻从红灯变成绿灯。FPGA里两边都用得很多——通信协议解析时收到某个特定数据要立即产生一个握手信号这种适合Mealy型状态指示类、控制类逻辑比如LED闪灯、模块使能用Moore型更安全稳定。实际工程中怎么取舍关键看两点第一输出是否需要即时响应输入变化第二你能不能容忍组合逻辑毛刺。Mealy机因为输出直接由输入组合逻辑决定输入有毛刺时输出也可能有毛刺所以对时序要求高的场合往往要慎用。Moore机的输出只跟随状态变化状态是寄存器信号输出自然干净稳定代价则是响应输入会慢一个周期。对于刚学状态机的朋友我建议先以Moore机为主等熟练了再尝试Mealy机。2.2 状态编码怎么选二进制、格雷码还是独热码状态编码这个问题很多教程一句话带过实际上工程里特别值得讲。常见的有三种二进制编码比如两个状态就编成2‘b00、2’b01、2‘b10、格雷码相邻状态之间只有1bit变化、独热码每个状态一个bit为1其余全0几个状态就用几个bit。三种编码各有适用场景。二进制编码最省寄存器资源写起来直观适合状态不多、逻辑不复杂的模块。格雷码的优势在相邻状态转换时只有一位翻转能显著降低状态跳转瞬间的毛刺概率对于需要连续步进的状态机比如电机步进控制、计数器分频很友好。独热码虽然费寄存器状态数多时消耗很大但它让每个状态只有一个bit翻转而且状态判断只需要看一个bit逻辑简单、速度快在资源充裕的FPGA时序优化场景下很常见。以Xilinx和Intel家的综合工具来看很多工程师习惯用独热码因为FPGA里寄存器资源相对丰富查表资源反而紧独热码刚好用寄存器换查找表。Vivado默认的FSM编码方式其实就是One-Hot。我个人的习惯是状态少于5个用二进制多于5个优先考虑独热码需要降低相邻跳变毛刺的场合用格雷码。另外要注意不同编码下状态机的初始状态和default状态都要覆盖到位否则综合时工具可能给你警告甚至综合出锁存器。2.3 关于状态定义的一些经验定义状态时推荐用localparam或者define来给状态命名而不要直接使用裸的数值。我在代码里写状态机时一定会把所有状态名集中写在模块最前面比如localparam S_IDLE 3d0; localparam S_START 3d1; localparam S_DATA 3d2; localparam S_STOP 3d3;这么写的好处有三个第一代码可读性高别人看代码时看到的是S_DATA而不是一个不知道含义的2b10第二后续要增加状态或者调整编码只需要改定义处状态跳转逻辑不用动第三仿真调试时把状态信号添加到波形窗口可以直接按名字查看当前状态效率高很多。在定义状态的时候还要留一个心眼根据状态数量把寄存器的位宽留足。比如定义了4个状态用3bit的寄存器是合理的如果你只用了2bit综合时工具可能做优化也可以工作但一旦代码里某个分支条件漏了多余的编码空间就成了未定义状态状态机就可能跑飞。这也是为什么我习惯把状态编码都顺位排满并且在default分支里做保护的原因。3. 三段式状态机工程中最常用的写法3.1 为什么三段式比一段式、两段式更好用网上讲状态机的文章一定会提到一段式、二段式、三段式。所谓“几段式”指的是用几个always块来写状态机。一段式就是把状态寄存、次态判断、输出逻辑全写在一个always块里二段式通常把状态寄存和次态逻辑放一个块输出单独一个块三段式则是把状态寄存、次态逻辑、输出逻辑分成三个always块分别处理。我强烈推荐新手直接学三段式原因很简单——三段式把“状态跳转”和“输出产生”彻底分开逻辑层次清楚后期维护最容易。一段式代码短但有隐患输出信号经常被描述成寄存器输出容易产生比预期晚一拍的时序而且综合出来的电路不够清晰代码一多还容易出错。二段式输出如果写组合逻辑毛刺风险又上来了。三段式每个always块各司其职第一个always块用时序逻辑处理状态寄存器的更新第二个always块用组合逻辑算次态第三个always块处理输出可以选组合输出也可以选寄存器输出。这么一分哪部分有问题一眼就能找到。用调试工具看波形时状态寄存器一条线、输入信号一条线、输出信号一条线逻辑流程明明白白。3.2 第一段状态寄存器的时序跳转这是状态机最简单的一段代码量很少。它的作用就是在时钟上升沿把次态赋给现态同时处理复位。如果没有使能信号复位后所有状态归零。always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else state next_state; end这段代码没什么花头但要提醒几个细节复位建议用异步复位、同步释放的方式这里用negedge rst_n是常见的异步复位风格能保证系统上电时有一个确定的初始状态。另外next_state信号名要固定别换来换去一会儿叫next_state一会儿叫state_next后期代码审查时容易看晕。如果你希望状态机在某些情况下保持不跳转比如收到暂停使能信号时停在当前状态不要在第一个块里做条件判断而是在第二个块的次态逻辑里处理让next_state等于state本身即可。这样第一个块永远保持“无条件寄存次态”的简洁形态代码层次更清晰。3.3 第二段次态组合逻辑第二段是整个状态机里最有“算法含量”的部分。它根据当前状态state和输入信号决定next_state是什么。由于这里用组合逻辑描述所以always块的触发列表要包含所有用到的信号最常见的是always (*)让工具自动推导敏感列表这样不容易漏信号。以UART接收为例空闲时如果检测到起始位rx拉低就进入接收数据状态接收数据状态下按bit计数依次采样8个数据位数据位收满后进入停止位状态停止位结束回到空闲。这段逻辑写出来就是一组case语句加若干条件判断always (*) begin case (state) S_IDLE: begin if (rx 1b0) next_state S_START; else next_state S_IDLE; end S_START: begin if (cnt_clk DIV_CNT) next_state S_DATA; else next_state S_START; end // ... 其余状态类似 default: next_state S_IDLE; endcase end写第二段时有三个容易踩的坑。第一要加default分支否则case没覆盖全时综合工具可能给你生成一个不太合理的保持电路第二组合逻辑里不能产生锁存器也就是说所有可能路径下next_state都必须被赋值所以最好在case开头先给next_state赋一个默认值或者确保每个分支都有完整的赋值第三不要把变量写进赋值号的左边却在别的分支没赋值这种“记忆”在组合逻辑里是不允许的会产生latch。经验做法是在case语句之前加一句next_state state;作为兜底这样即便哪个分支漏写了也不会产生锁存器。3.4 第三段输出逻辑第三段的写法有讲究。关键在于输出可以有两种形式纯净的组合逻辑输出或者寄存器输出。组合逻辑输出的写法always (*) begin case (state) S_READ: data_valid 1b1; default: data_valid 1b0; endcase end寄存器输出的写法则是在组合输出后面再打一拍或者直接在时序逻辑块里根据状态寄存器的当前值给输出赋值always (posedge clk or negedge rst_n) begin if (!rst_n) data_valid 1b0; else if (state S_READ) data_valid 1b1; else data_valid 1b0; end两者怎么取舍如果输出信号要驱动外部芯片的片选、复位等对毛刺敏感的信号寄存器输出更安全它让输出经过一级寄存器消除了组合逻辑上的毛刺代价是输出比状态变化晚一拍。如果输出只是内部逻辑使用而且这个输出本来就要跟输入信号组合出某个动作那组合输出更直接因为它响应快。实际项目里我经常混合用控制类信号尽量打拍寄存数据类信号保持组合输出以减少延迟。还有一点三段式的第三段不是必须单独用一个always块——如果输出信号较多各自按需寄存或组合灵活安排即可只要别把次态计算和输出寄存混在一起就行。3.5 完整代码示例UART接收状态机来一个能直接跑的完整示例把三段式串起来。这里实现一个简化的UART接收模块波特率分频通过CLK_DIV参数配置8个数据位无校验1个停止位module uart_rx_fsm #( parameter CLK_DIV 868 // 50MHz时钟波特率57600 )( input wire clk, input wire rst_n, input wire rx, output reg [7:0] rx_data, output reg rx_done ); localparam S_IDLE 3d0; localparam S_START 3d1; localparam S_DATA 3d2; localparam S_STOP 3d3; reg [2:0] state; reg [2:0] next_state; reg [9:0] clk_cnt; reg [2:0] bit_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else state next_state; end always (*) begin next_state state; case (state) S_IDLE: begin if (rx 1b0) next_state S_START; end S_START: begin if (clk_cnt CLK_DIV / 2) next_state S_DATA; end S_DATA: begin if (bit_cnt 3d7 clk_cnt CLK_DIV) next_state S_STOP; end S_STOP: begin if (clk_cnt CLK_DIV) next_state S_IDLE; end default: next_state S_IDLE; endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) begin clk_cnt 10d0; bit_cnt 3d0; end else if (state S_IDLE) begin clk_cnt 10d0; bit_cnt 3d0; end else begin if (clk_cnt CLK_DIV) clk_cnt 10d0; else clk_cnt clk_cnt 1b1; if (state S_DATA clk_cnt CLK_DIV) begin if (bit_cnt 3d7) bit_cnt 3d0; else bit_cnt bit_cnt 1b1; end end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_data 8d0; rx_done 1b0; end else if (state S_DATA clk_cnt CLK_DIV) begin rx_data[bit_cnt] rx; rx_done 1b0; end else if (state S_STOP clk_cnt CLK_DIV) begin rx_done 1b1; end else begin rx_done 1b0; end end endmodule这个例子里状态跳转、次态计算、输出寄存是靠前两个always块完成的第三个always块同时处理了数据采样和输出标志。你可以把它作为模板然后在此基础上改造加奇偶校验、加多字节FIFO缓存、加错误检测这些都是在状态机框架里加状态和判断而已。4. 从需求到状态机的完整设计流程4.1 第一步画状态转移图决定开始写状态机之前先画图。在很多初学者眼里状态转移图是课程作业里才需要的东西实际项目里直接写代码。以我带过的项目经验来看恰恰相反——越复杂的逻辑越要先画图。画状态转移图的过程就是把需求里的“看似复杂”压缩成“状态和转移条件”的过程。画图时注意三点第一用圆圈表示状态圆圈里写状态名最好标上状态编码第二箭头表示状态跳转箭头上写清楚跳转条件条件要写成布尔表达式比如rx 1b0、cnt 5d15第三把复位状态标出来一般从S_IDLE开始。碰到自己画不明白的地方先停下来跟需求方确认千万别边写代码边猜。一个实用小技巧状态转移图不需要画得太精致自己看得懂就行。画图的目标是逼自己把逻辑理清楚。等画完图你再写代码时基本就是把图翻译成Verilog速度能快很多而且逻辑漏洞在画图阶段就暴露了不用等仿真报错。4.2 第二步整理状态转移表画完图之后第二步把图翻译成状态转移表。表格的列可以分成当前状态、输入条件、次态、输出动作。用笔头整理一遍的好处是进一步确认逻辑没有遗漏而且表格可以直接当做代码注释写在模块头上方便以后维护。举个例子前面那个UART接收状态机状态表可以这样列当前状态输入/计数条件次态输出动作S_IDLErx 1b0S_START无S_IDLErx 1b1S_IDLE无S_STARTclk_cnt CLK_DIV/2S_DATA无S_DATAbit_cnt 7 clk_cnt CLK_DIVS_STOP采样数据S_STOPclk_cnt CLK_DIVS_IDLE拉高rx_done这张表的价值在于写代码前先确认每一行都无歧义。很多时候你跟别人对需求拿图比拿代码沟通效率高得多。另外这张表也是后面仿真用例的依据每个分支都是潜在的一个测试用例漏了哪一行就可能导致仿真覆盖不全。4.3 第三步写代码、仿真、上板验证状态转移表完成之后写代码基本上就是机械翻译。按三段式的框架把状态定义、状态寄存器、次态逻辑、输出逻辑填进去就行。这时候最需要花时间的是仿真验证。针对状态机模块仿真验证要有几个固定的观察点第一复位后的初始状态是否符合预期第二正常流程是否完全走通比如UART接收完整的一帧数据第三边界条件是否覆盖比如接收数据最后一位、计数器到最大值时的跳转第四异常条件是否会导致跑飞比如没有default分支时输入了未定义的状态值。写testbench时别只给一个从头到尾都正常的激励。针对状态机特别要测试输入信号不满足条件时能否保持在原状态、复位信号在任意时刻拉低能否可靠复位、连续触发时会不会漏状态。这些边界场景才是真正检验状态机健壮性的地方。上板验证阶段最常见的调试手段是逻辑分析仪和板载LED。把状态寄存器的低几位引到LED上观察LED的闪烁顺序可以直观判断状态机的流转是否符合预期。如果用不了逻辑分析仪也可以把状态值通过串口或SPI回传上位机只是这种折腾度大一点。在正式调试前仿真一定要做充分上板后状态机类问题往往比较难抓因为时序关系在真实环境里更复杂。4.4 以FMC通信为例的状态机拆解很多朋友在做STM32H743与FPGA通过FMC接口通信的项目这个场景很适合用来看看状态机怎么落地。FMC是一种并行总线接口主机侧有地址线、数据线、读写控制线、片选信号时序上分为写周期和读周期。FPGA侧要做的就是根据控制信号生成一套状态机来响应对应的读写操作。简化一下FPGA侧的状态可以分成S_IDLE等待片选和地址稳定S_WRITE根据写使能采样数据总线写入寄存器S_READ根据读使能输出数据到总线并维持总线一段时间S_DONE结束当前操作释放总线。每个状态跳转的边界条件就是片选信号、写使能、读使能和自己的等待计数器。用状态机拆解并行通信协议的好处非常明显协议时序上每个阶段都是显式的状态回波波形一对照就知道卡在哪一步而不需要去推算计数器某个区间在干什么。在这个项目里时序收敛要求也比较高输入数据线往往是异步进来的状态机在这些关键接口上还要额外考虑跨时钟域处理这部分后面单独展开写。5. 状态机的坑与排查记录5.1 状态机跑飞最经典的问题做FPGA最让人头疼的故障之一就是状态机“跑飞”——本来应该按顺序跳转的状态突然跳到一个没有定义的地方整个逻辑就乱套了。这种情况最常见的原因就是case没有覆盖所有可能的状态编码。举个例子你定义了4个状态用2bit编码刚好够用但某次代码改动后状态定义变成了5个却没有修改寄存器位宽或者状态定义里留了几个空洞没用一旦外部干扰或者复位不彻底把状态寄存器打到了空洞值状态机就再也回不到正常流程了。解决办法有三层第一状态寄存器位宽给足宁可多留1bit第二第二段的case语句里一定写defaultdefault里最重要的一句就是next_state S_IDLE;这样就算状态跑偏下一拍也能拉回来第三在仿真验证阶段加一个监控逻辑如果看到非预期的状态值立即报错强迫自己把状态机可能走到非法状态的路径堵上。另外第三段输出逻辑的case也建议加default否则非法状态下输出不确定后续模块可能连锁出错。5.2 组合逻辑毛刺和亚稳态前面提过Mealy机输出可能带毛刺实际上即便Moore机如果状态机的输入信号是异步的比如外部按键信号或者跨时钟域过来的信号状态机的次态判断就可能采到亚稳态。亚稳态意味着寄存器采样时输入信号正好在跳变窗口内寄存器的输出不确定而且这个不确定可能会传递到后续逻辑。处理异步输入最基础也最实用的办法就是把输入信号打两拍用同步器消除亚稳态。比如外部按键的异步信号先过两级寄存器reg key_r0, key_r1; always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_r0 1b1; key_r1 1b1; end else begin key_r0 key; key_r1 key_r0; end end同步之后再用key_r1作为状态机的判断输入。这个做法几乎适用于所有异步输入的场合代价只是两个周期延迟。实际项目中如果状态机输入来自外部引脚别嫌麻烦先同步再使用这是最稳妥的保险。5.3 多bit状态跨时钟域问题状态机本身工作在一个时钟域内部不会遇到跨时钟域问题。但是当状态机的状态值要被另一个时钟域的模块读取时麻烦就来了——如果状态编码是二进制比如从2‘b01跳到2’b10两个bit同时变化另一个时钟域采样时可能采到2‘b00或者2’b11这种中间错误值。解决办法通常有两种。一种是把多bit状态信号也打成两级同步但这样可能采到中间值状态会短暂紊乱另一种更常用——使用格雷码编码相邻状态只有一位变化同步后即便在某一个时钟沿采样采到的也是相邻状态之一不会出现完全错误的状态值。如果你要和另一个时钟域传递“状态机正在干什么”这种信息也要考虑是否引入异步FIFO或握手协议而不是直接拉一根多bit信号过去。我在做PCIe、DDR这类高速接口时状态机跨时钟域的场景特别多这种地方光靠状态编码解决不了所有问题更多时候是把状态机精简成“开始/忙/结束”这种单bit握手信号跨时钟域传递然后由对端重新生成自己的状态机。新手阶段只要记住“多bit信号不要直接跨时钟域”就算避开了最容易踩的坑。5.4 时序违例怎么查状态机代码写完后在Vivado或Quartus里跑综合实现有时候会报时序违例比如setup time violation。出现这种问题第一反应不是去调状态机结构而是先看关键路径在哪。在Vivado里打开Implementation后的时序报告找到违例路径它会告诉你路径是从哪个寄存器出发经过多少级组合逻辑到达哪个寄存器。如果关键路径正好落在状态机的次态组合逻辑上说明次态逻辑太长了也就是从一个状态的判断条件到下一个状态寄存器更新之间组合逻辑串联太深。应对策略有几种简化次态逻辑的条件把复杂的多条件判断拆成中间信号给输入信号打拍减少组合逻辑树的深度调整状态编码方式独热码能让状态判断逻辑更浅。加流水线也是一种方式但状态机本身的循环结构不能简单插pipe所以用得更多还是优化组合逻辑深度。5.5 常见笔试/面试题里状态机的套路顺便整理几个FPGA面试笔试里经常出现的状态机题对刚入门的人也挺有帮助。第一个经典题是“检测序列1011画出状态转移图并写三段式Verilog”。这种题考的是画状态转移图和写代码基本功注意序列可以重叠检测这一点细节就是区分是否能重叠。第二个经典题是“用状态机实现一个数字频率计的控制逻辑”。这个题考的是闸门控制状态什么时候开始计数什么时候停止计数什么时候锁存结果。状态机的状态划分是否清晰直接反映出你有没有实际做过类似控制逻辑。第三个经典题是“状态机跑飞怎么处理”。这个问题检验的是工程意识回答default分支兜底、状态寄存器位宽冗余、仿真监控非法状态基本就过关了。如果有人能提到“用看门狗思想让状态机超时自动复位”那基本就是有实际经验背书的回答。6. 给初学者的练习路线6.1 练习1按键消抖状态机如果你刚开始碰状态机第一个练习可以非常小按键消抖。按键按下和释放时机械抖动会产生一连串高低电平变化如果用组合逻辑直接判断会误触很多次。用状态机做消抖的基本思路是S_IDLE检测到按键拉低进入S_PRESS_WAIT在S_PRESS_WAIT里持续计时20ms如果20ms内按键一直保持低电平说明不是抖动确认按下进入S_PRESS_CONFIRM之后等待释放S_REL_WAIT里也要持续20ms高电平才回到空闲。这个练习麻雀虽小五脏俱全有状态定义、有异步输入同步、有计数器配合状态判断、有输出标志。做完这个基本状态机的套路就熟了。我建议一定要动手写testbench模拟按键抖动波形看看自己的状态机能不能正确滤除掉抖动。6.2 练习2数字频率计的闸门控制第二个练习可以做一个数字频率计的闸门控制部分。数字频率计的原理是测出单位时间内的脉冲个数而这“单位时间内”的门控信号正好适合状态机实现。状态划分大致是S_IDLE等待启动信号S_COUNT在固定的1秒内让计数模块对被测信号计数S_LATCH锁存计数值S_DONE产生完成信号并回到空闲。这个练习比按键消抖复杂的地方在于里面要控制和配合多个模块启动、计数、锁存、复位状态之间还有严格的时序关系。你会在写这个状态机时真正体会到“状态机是骨架其他模块是肌肉”这句话。6.3 练习3把一个简单协议吃透第三个练习建议选一个具体的通信协议来实现比如SPI主机或者UART发送。别找太复杂的比如PCIe、MIPI这种新手阶段碰不动Biss-C这类工业协议也先放一放但思路是一样的。从SPI主机开始就很合适状态划分为S_IDLE、S_SETUP、S_SAMPLE、S_HOLD、S_DONE每个bit的周期用计数器控制循环处理8个bit。做完SPI主机你会发现状态机设计和协议时序分析已经自然成为一体了。以后再拿到任何一个带时序图的芯片手册第一反应都是画出状态转移图而不是一上来就堆代码。到这一步状态机这个坎就算彻底跨过去了后面学卡尔曼滤波的FPGA实现、图像处理的行场同步控制、LVDS、HDMI这些复杂接口都会顺手很多。我自己带新手时一直强调一个观念状态机本身不难难的是在设计前把需求状态化。多画图、多写状态转移表、多仿真边界条件这套方法论比背多少代码都管用。状态机写多了之后你会慢慢形成一种直觉——看到一段时序图脑子里会自动冒出几个状态圈和跳转箭头。有了这种直觉FPGA开发才算真正入门了。
返回列表