拓冰建站拓冰建站
首页 / 资讯中心 / 正文

状态机设计入门:FPGA自动售货机完整课设实战

简介一份来自东南大学信息学院短学期数字系统设计课程的项目资源围绕自动售货机场景演示如何用FPGA完成硬件逻辑设计与实现。资源共160个文件压缩包仅1.46MB核心为10个VHDL源文件.vhd并配套Xilinx约束文件.xdc、Vivado工程与检查点.xpr/.dcp以及Tcl、Bat等脚本和各类报告日志便于复现综合、实现与仿真流程。目前已有532人学习适合数字逻辑课程设计、FPGA入门或短学期项目参考。通过分析源码可掌握自动售货机中投币识别、商品选择、找零控制等状态机设计方法理解FPGA开发从RTL编码、时序约束到上板调试的完整链路同时还能借鉴工程目录组织与仿真验证思路。 每年春季学期东南大学信息学院的数字系统设计课设都会布置一批经典题目自动售货机就是其中最常被选中的一道。很多同学乍一看觉得这题太简单——不就是收钱、出货、找零吗可真到写状态机、调时序、上板实测的时候才发现坑远比想象的多。这篇文章把我自己完整做一遍这个项目的全过程梳理出来包括方案选型、模块拆分、核心代码思路、仿真测试和上板踩坑记录给后面选这道题的同学一份能直接抄作业的参考。不管你是第一次接触FPGA开发还是已经写了一阵子Verilog想看看别人怎么组织状态机这篇都应该能帮到你。1. 项目整体设计与思路拆解1.1 这个课设到底在考什么先说结论自动售货机这道题本质上考的就是两件事——状态机的设计能力和时序逻辑的工程化思维。很多人以为难点在“判定投币金额够不够”或者“怎么找零”这些用组合逻辑堆也能堆出来但一旦涉及多个输入事件投币、选商品、确认、取消按顺序发生组合逻辑就会变得一团乱麻这时候状态机的价值就体现出来了。课设要求通常长这样售货机支持三种商品价格分别为2元、3元、4元只接受5角和1元两种硬币用户先选择商品再投币金额达到商品价格后可以出货超出部分需要找零如果用户取消购买已投入的钱要能退回。部分题目还会加一个“找零功能需要支持最大找零面额优先”的约束实际就是在考贪心算法的硬件实现。这类题目的隐藏考点还有一个边界情况处理。比如用户投了2.5元买2元的商品需要找5角用户连续投币超过10元怎么办投币过程中按下取消键已投的币怎么统计退回。这些都要在状态图里画清楚否则代码写一半就会发现逻辑根本兜不住。1.2 为什么第一版就该用状态机我见过不少同学刚开始想避开状态机直接用若干个计数器加标志位的“野路子”来实现。短期看确实代码量少但调试起来非常痛苦——你很难说清楚系统当前到底处在“等待选商品”还是“正在投币”的阶段一旦加需求比如加一个取消功能就得大改逻辑。状态机的优势在于把整个系统的行为变成一个显式的“状态迁移图”。每个时刻系统处于且仅处于一个状态输入事件触发状态跳转同时在跳转过程中产生输出。用课程里的话说就是Moore型状态机的输出只取决于当前状态Mealy型状态机的输出还取决于输入。对于售货机这种场景推荐用Mealy型或者“Moore为主、输出端口做组合判断”的混合写法因为有些输出比如找零金额、出货信号需要在收到输入的瞬间就产生纯Moore会慢一个周期。我这次用的是三段式状态机写法也就是状态跳转时序逻辑、次态判断组合逻辑、输出逻辑三段分离。这种方式比一段式、两段式代码量大一些但是每一段职责单一仿真和上板排错都要轻松得多尤其是输出逻辑独立出来后想加一个“出货LED闪烁”这种操作完全不影响状态跳转。1.3 需求拆解与功能边界动手写代码之前我花了半天时间把需求拆成了几个功能点每个功能点对应到后续的模块或状态这步非常关键。拆解结果如下商品选择三个按键对应三种商品选完锁定再按其他键无效直到本次交易结束或取消。投币处理两个按键模拟5角和1元投币每按一次金额累加数码管实时显示当前金额。出货判定当前金额大于等于所选商品价格时允许出货出完货后计算找零进入找零状态。找零实现只找5角硬币用找零指示灯闪烁次数来表示找零个数或者驱动一个找零电机具体看板子资源。取消功能在未出货前按下取消键当前金额清零不产生出货信号。金额上限约定单次交易金额上限为9.9元超过后拒绝继续投币防止计数器溢出。这个边界定义很重要因为一旦出了问题老师检查时一定会问“如果用户投到100元怎么办”。你提前定义好上限并实现了对应的拒绝逻辑这在答辩时是个明显的加分项。后面写状态机的时候每一个状态都对应这一小节里的一个功能点逻辑会非常清晰。2. 系统架构与各模块功能划分2.1 整体数据流是怎么走的我在设计架构时把整个系统分成了五个模块按键消抖模块、分频模块、投币与金额累加模块、状态机控制模块、数码管显示模块。分频模块是因为开发板上的系统时钟通常是50MHz或100MHz而人按键的频率和数码管扫描频率都在几十到几百赫兹直接拿系统时钟驱动必然出问题所以需要分频产生合适的时钟使能信号。数据流大概是这样的按键信号经过消抖后变成干净的脉冲送到投币与金额累加模块累加结果输出到状态机控制模块状态机根据当前状态和输入事件选商品、投币、确认出货、取消决定下一步去向同时输出“出货”“找零个数”“清零”等控制信号数码管显示模块负责把当前金额和找零信息实时刷到数码管上。分频模块则给所有需要慢速时钟的模块提供统一的使能时钟。这里有一个我自己踩过的坑一开始我把分频后的慢时钟直接接到了所有模块的时钟端口上结果板子跑起来时序乱得一塌糊涂后来才意识到FPGA设计的标准做法是全局时钟管脚进来的时钟只经过全局时钟网络BUFG之后接触发器时钟端口需要慢速功能应该用“时钟使能”而不是再分频。改成分频模块输出一个高电平持续一个时钟周期的使能信号后所有模块都继续用系统时钟做敏感列表问题迎刃而解。2.2 各模块的接口设计和技术要点模块之间怎么通信我在各模块接口上加了明确的输入输出定义消抖模块输入原始按键信号输出一个周期的脉冲。核心是“连续采样N次为同一电平才认为有效”我用的方案是20ms左右的延时消抖具体实现是靠计数器对系统时钟计数采到低电平后开始计数计数满后确认按键按下松开时同理。分频模块输入系统时钟输出一个使能脉冲每1ms或10ms产生一个高电平周期其他模块把这个使能当作“节拍”。投币累加模块输入两个消抖后的脉冲5角、1元内部用BCD码寄存器存金额输出当前金额到显示模块和状态机模块。状态机控制模块输入当前金额、所选商品价格、确认键、取消键输出出货信号、找零信号、当前状态编码。显示模块输入当前金额和状态机输出的找零个数做动态扫描显示刷新频率控制在1ms以内避免闪烁。这里要注意BCD码存金额是我特意选的方案。如果用二进制存金额显示的时候还得做二进制到BCD的转换费资源也容易出错。直接四位BCD表示一位小数显示模块从寄存器里取出各位就能送数码管简单直接。2.3 开发板与工具链选型我在东南大学课设里用的是Altera Cycone系列板子配套Quartus Prime开发环境写Verilog语言。这套组合在高校里非常常见网上资料也多遇到问题基本能搜到答案。如果你用的是Xilinx的板子也没关系代码本身是可移植的只需要注意约束文件格式不同。值得一提的是一开始要选对顶层模块名字和引脚分配。课设板子上的按键、数码管、LED的引脚号在实验指导书里通常有表必须严格按照表格里的引脚名去分配。我见过有同学因为把按键极性搞反了以为按下是高电平实际板子是低电平有效导致一上电就触发投币捣鼓了半个下午。建议开工前先用一个简单的LED流水灯工程测试所有按键和数码管的极性这一步花不了多少时间但能省后面大量的排错时间。3. 核心模块实现细节与调优3.1 按键消抖与脉冲生成按键消抖这个模块看起来简单其实有不少讲究。物理按键在按下和松开的瞬间会有机械抖动持续时间大概在5ms到20ms不等如果不做处理一次按键会被识别成多次投币金额就会乱跳。我用的消抖方案是“边沿检测计数延时确认”。思路是每次采样到按键电平发生变化时启动一个20ms的计数器计数器计满后再采一次电平如果和变化后的电平一致就认为按键状态真的变了然后生成一个单时钟周期的高电平脉冲。这样做的好处是既滤掉了抖动又不会漏掉快速连续按键。一个容易忽略的细节是消抖模块生成脉冲的时机最好在“确认按下”时而不是“确认松开”时。确认按下就产生脉冲这样用户的手指还没抬起来系统就已经响应了手感会好很多也符合售货机“投币立即响铃”的直觉。3.2 状态机的程序结构和状态编码接下来是核心的状态机部分。我的状态定义如下localparam IDLE 4d0; // 空闲等待选商品 localparam SELECT 4d1; // 已选商品等待投币 localparam WAIT_COIN 4d2; // 投币中金额未达商品价 localparam ENOUGH 4d3; // 金额已达或超过商品价可出货 localparam CHANGE 4d4; // 找零中 localparam CANCEL 4d5; // 取消退回推荐用独热码one-hot还是二进制码这个课设规模不大二进制码够用。但如果老师要求你体验一下工程化设计或者你打算把项目扩展成多商品自动售货机独热码在FPGA上有优势——状态译码逻辑简单时序更好收敛代价就是触发器用得多。我的建议是课程设计用二进制码足够答辩时说得出两种编码的优劣就行。三段式的代码框架大致是这样的// 第一段状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else if (clk_en) state next_state; end // 第二段次态组合逻辑 always (*) begin next_state state; case (state) IDLE: if (sel_valid) next_state SELECT; SELECT: if (coin_valid) next_state WAIT_COIN; ... endcase end // 第三段输出逻辑 always (*) begin out_sell 1b0; out_change ...; case (state) ENOUGH: if (confirm) out_sell 1b1; endcase end这里有个细节第二段的次态组合逻辑用always (*)必须要给next_state赋默认值否则会产生锁存器latchLUT资源占用上升不说时序还会出现奇怪问题。我一般习惯在case前先写next_state state;作为默认值然后只改需要跳转的路径这样代码简洁且不易出错。3.3 金额累加与找零算法的硬件实现金额累加模块需要注意的一个点是“金额上限”。我定义了最高9.9元也就是BCD码寄存器的最大值是9_9。每次投币脉冲到来时先判断当前金额加上新币种是否超过上限超过就忽略这次投币并给出一个“拒收”指示。这是老师最容易问到的边界处理之一。找零算法我直接用了贪心因为只找5角所以找零个数等于“当前金额减去商品价格”再除以5角。硬件实现上用一个减法器把当前金额减掉商品价格得到差额然后不断减5角每减一次输出一个找零脉冲同时用LED点亮指示找零次数。这个循环在状态机的CHANGE状态里实现每来一个时钟使能执行一次减法直到差额为0然后回到IDLE状态。if (state CHANGE) begin if (diff 4d5) begin diff diff - 4d5; change_cnt change_cnt 1b1; end else begin state_next IDLE; end end这里有个小技巧不要把找零循环放在一个时钟周期里用组合逻辑算完而是用时钟节拍一个一个减。虽然看起来“慢”但好处是找零过程可以被肉眼看到LED逐个闪烁答辩演示效果特别直观而且在FPGA实现时序也更好。我当时演示的时候老师看到LED一闪一闪地找零直接说“这个设计很真实”。3.4 数码管动态扫描显示显示模块相对独立但容易出问题。三个数码管需要显示的内容分别是商品编号1/2/3、当前金额如2.5显示成2.5、找零次数。动态扫描的原理是所有数码管共用段选线片选线轮流导通每次只点亮一个数码管刷新频率高于人眼临界频率大于50Hz就不会闪烁。我踩过的一个坑是扫描时钟频率太高导致数码管亮度不均匀。频率太高时每个管点亮时间太短亮度不均且有重影频率太低则明显闪烁。实测下来扫描周期在1ms到2ms之间效果最好也就是1kHz到500Hz的扫描频率。我最终选了1ms切换一次片选三个数码管扫描一轮3ms亮度均匀且稳定。always (posedge clk) begin if (scan_cnt 100000) begin // 1ms scan_cnt 0; scan_pos scan_pos 1b1; end else scan_cnt scan_cnt 1b1; end还需要注意数码管段码表共阴和共阳段码刚好相反。这个查板子手册确认即可我一开始没仔细看显示的数字全是反的。4. 仿真、上板调试与常见问题排查4.1 仿真用例怎么设计才能把逻辑测全我始终觉得仿真这一步花的时间越多上板调试就越轻松。我写仿真测试平台时没有只测一条“投够钱出货”的happy path而是把重点放在边界情况上。以下是我整理的测试用例表场景操作序列期望结果正常购买选2元商品投1元1元出货不找零超金额找零选2元商品投1元1元5角5角出货找零2个5角取消退回选3元商品投1元5角按取消不出货金额清零投币上限持续投1元直到超过9.9元第10个1元拒收金额保持9.9先投币后选商品先投1元再选2元商品按需求应不允许保持当前状态快速连按极短时间间隔内双击投币键不误触发两次投币写仿真的时候用initial块加#延时模拟按键时序再把金额、状态输出引出来看波形。我强烈建议把state变量也加到仿真波形里直接看状态跳转是否符合设计图。我第一次仿真时发现取消功能没有生效就是靠看状态机波形定位到“取消那条分支的优先级写得比投币分支低”导致同时按下取消和投币时总进入投币状态。4.2 上板实测那几小时的经历仿真全过之后上板实测还是出了意外。第一个奇怪现象是数码管偶尔会闪过一个不该出现的数字排查了半天发现是复位键没接上拉电阻导致上电瞬间复位信号不稳定系统在随机状态启动。加了一个上电延时复位电路后解决——这个在上板调试中非常常见建议所有板子都做一个上电延时复位。第二个问题是找零指示灯在金额刚到商品价格时也会闪一下这其实是状态机从ENOUGH跳到CHANGE时差额为0但我的组合逻辑里没有对0做判断直接进入CHANGE循环走了循环体一次。修这个bug只加了一个判断条件。这个例子也说明状态机的输出逻辑一定要仔细检查“状态进入条件”和“状态内的动作”是否匹配。第三个问题是按键偶尔会一次触发两次这是消抖计数时长不够导致的。我把消抖时长从10ms调整到30ms后问题消失。不同板子的按键抖动时间不一样这个参数需要实测调整。4.3 常见问题速查表与排错技巧我把整个调试过程遇到的高频问题整理成一个速查表给后面做这个题的同学参考故障现象可能原因排查方向一上电金额乱跳消抖参数太小/按键极性反了先测按键原始电平再调消抖计数时长状态机乱跑、操作没反应复位信号不稳定确认复位按键上拉/下拉增加上电延时复位数码管显示数字不对共阴共阳段码搞反查板子手册确认段码表投币金额显示时有时无扫描频率太高有重影、太低闪烁调整为1ms切换一次的扫描周期取消后金额未清零状态机漏了从SELECT直接回IDLE的路径画完整状态图对代码找零个数少一个差额为0时仍进入找零循环在进入CHANGE状态前判断差额是否为0引脚分配正确但外设不工作顶层模块引脚约束没生效重新编译看Pin Planner是否有未分配引脚告警这里有一个排错思路分享遇到问题先看仿真、再上板。我在上板阶段遇到的所有诡异现象回头在仿真里基本都能复现。如果仿真复现不了那大概率是硬件层面的问题引脚约束、电平标准、复位不稳这时候就要靠示波器或逻辑分析仪看了。4.4 一些能提高答辩质量的小设计点如果你想让这个课设做得更出彩我建议在基础功能之上加两个小设计改动量不大但效果很好。第一个是把“出货”设计成一段出货动画出货信号拉高后对应的LED不是直接亮而是以一定频率闪烁3次再熄灭。这段逻辑状态机里多加两个周期就行但演示时看起来很有“商品从货架掉下来”的仪式感老师印象分直接拉满。第二个是加一个“找零余额提示”如果用户取消退回数码管先显示应退金额并且LED闪烁2秒后才回到空闲状态。这个设计虽然只多了两个状态但体现出了对“用户体验”的思考答辩时可以好好讲讲状态机的可扩展性。最后跟大家说一个我自己摸索出来的经验整个项目里最耗时间的不是写代码而是画状态图。我画状态图画了一个晚上期间反复推演各种输入组合下的行为把状态图改了三版才开始写代码。写代码只用了半天仿真调试用了一天上板半天。很多同学一上来就写代码写着写着发现分支关系理不清又回头改反而更慢。状态图就是程序的设计文档画得越细后面越顺畅。自动售货机这个课设题目虽然经典但每年还是能刷掉一批人大部分原因不是能力不够而是对“状态机”这个核心知识点理解不够透彻。希望这篇记录能让你少走一些弯路。等你的版本跑通了可以试着把商品数量扩展到5种、找零面额增加到1元和5角混合那又是新一轮折腾但也是真正把数字系统设计吃透的开始。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门