十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

数字系统设计实战:基于FPGA的自动售货机Verilog实现全流程解析

数字系统设计实战:基于FPGA的自动售货机Verilog实现全流程解析 简介面向东南大学信息学院短学期数字系统设计课程的自动售货机FPGA项目是一份聚焦真实业务场景的课程设计资源。项目基于FPGA平台覆盖硬币识别输入、商品选择、支付与找零控制、货道驱动等常见数字系统设计环节适用学习Verilog/VHDL及数字逻辑综合的学生参考。压缩包共160个文件约1.46MB主要包含VHDL源码.vhd、约束文件.xdc、综合实现后生成的DCP网表.dcp、Vivado工程与仿真日志.jou及批处理脚本便于直接打开工程查看代码和还原实现流程。已有532人学习/下载。该资源提供完整工程目录与源代码可作为东南大学“自动售货机”类课程设计的起步模板也适合其他高校数字系统设计实验借鉴通过阅读源码可掌握状态机设计、时序控制、外设接口驱动等方法并参考Vivado下的综合与布局布线脚本快速完成硬件验证。 东南大学信息学院这门“数字系统设计”很多人选自动售货机这个题目觉得它是经典中的经典。但我当年拿到题目时其实有点懵——这不就是课本上一个状态机例子吗有什么好设计的后来真上手做了两周才发现课本上那个三行状态转移图只是冰山一角真正的自动售货机是集输入处理、状态控制、数据计算、显示驱动、时序协调于一身的完整数字系统。这篇就顺着我当时的完整经历从需求分析一直讲到上板验收帮准备做这个题目的学弟学妹把路线趟平。1. 题目需求拆解自动售货机到底考的是哪几件事先看一个典型的题目设定。东南大学信息学院的数字系统设计课程自动售货机这个题目一般是这样描述的设计一个饮料自动售货机控制器能识别5元、10元两种纸币或1元、5元两种硬币具体看实验平台饮料售价8元。当投入金额大于等于8元时出饮料并找零当投入金额不足8元时继续等待投币或退币。系统需支持取消退款操作并用数码管显示当前投入金额和找零金额。1.1 表面需求背后的真实考点这个题目看着简单但里面藏着至少四个层次的考察点输入信号怎么处理。投币不是按键按一下就完事实物平台上往往有抖动、毛刺更别说按键和拨码开关的机械特性了。积分电路、施密特触发器、按键消抖逻辑这些基础模块一个都不能少。核心状态机怎么设计。自动售货机的核心就是一套状态转换逻辑空闲→投币→比较金额→出货并找零→回到空闲。状态定义得好不好直接决定代码好不好写、仿真好不好调。数据通路怎么算。投入金额的累加、售价格比较、找零金额计算这些都用组合逻辑或简单时序逻辑完成。别小看这部分很多人的bug就出在加法器溢出和BCD码转换上。输出怎么驱动。数码管动态扫描显示实时金额、LED灯指示出货状态、蜂鸣器提示操作完成这些外围输出怎么和主状态机协同体现的是从算法到结构的映射能力。1.2 平台和资源要提前确认做这个题目第一步一定不是打开代码编辑器而是翻实验平台手册。信息学院的课程设计通常用Xilinx或Altera的开发板上面会集成按键、拨码开关、数码管、LED、蜂鸣器这些基本外设。不同板子的引脚分配完全不同比如我们当时用的板子数码管是共阴极还是共阳极、按键是高电平有效还是低电平有效如果看都不看直接写代码上板调试时极大概率被点亮逻辑反向折磨一晚上。另外还要确认时钟频率。开发板上的板上时钟一般是一百兆或五十兆而数码管动态扫描和按键消抖需要毫秒级的时钟节拍中间必须做分频。分频模块怎么设计直接关系到后续所有逻辑能否稳定工作。2. 整体架构规划为什么不能一个状态机硬刚到底我先说说自己第一次犯的错。当时觉得这个题就是状态机嘛一个module全写完状态跳转加几个if判断齐活。等到写了两百多行代码准备仿真时发现一个问题当状态机停在“等待投币”状态时投币信号来了累加金额要变但状态机这个always块既要管状态跳转又要管金额加减代码里全是重复的else if分支稍一改动就牵一发动全身仿真波形一行一行翻到头晕。后来痛定思痛参考了学长给的工程结构发现正规的写法是模块化分层——没人真的会在一个module里硬塞全部功能。标准的做法是拆成下面六个模块模块名功能输入输出clk_div将板上高频时钟分频为低频节拍如100Hz、1kHzclk, rst_nclk_100hzkey_debounce对投币、取消、确认按键消抖clk_100hz, key_inkey_out单周期脉冲coin_accumulator投币金额累加及清零控制key_pulse, cancel, resetcoin_sum[7:0]sale_controller主状态机空闲、投币、比较、出货、找零、结束coin_sum, coin_value, confirm状态变量、出货信号、找零金额data_display金额BCD编码及数码管动态扫描显示coin_sum, change_sumseg_sel, seg_datatop_auto_sale顶层例化完成引脚映射和模块互连各外部引脚各外部引脚2.1 模块划分的内在逻辑我当时整理这套划分方式时总结出三条实用原则时钟域单独管。整颗芯片都在跑但没有一个模块能凭空得到自己想要的节拍。先把内部节拍统一规划好后续所有模块的时序关系才说得清楚。状态机只做控制。主状态机管“接下来干什么”不直接干“算钱”“显示”这种事。把计算任务下发给专门模块状态转移图会清晰很多出问题也好定位。数据通路和控制通路分离。金额累加是纯数据流操作按键消抖是纯输入处理显示扫描是纯输出驱动。它们之间通过少量精准的信号握手而非全局变量式的乱接。2.2 顶层模块设计的两张“接线图”顶层模块相当于整个工程的地基外部信号从管脚引进来之后怎么分流内部信号又怎么汇聚到各个子模块我建议动手前先画一张“管脚-信号对照表”再画一张“内部模块互连图”。比如// 顶层模块端口示意伪代码风格具体信号名按平台调整 module auto_sale_top( input clk_50m, // 板上50MHz时钟 input rst_n, // 复位按键 input key_5, // 投入5元对应一个按键 input key_10, // 投入10元 input key_confirm, // 确认购买/出货 input key_cancel, // 取消退款 output [7:0] seg_sel, // 数码管位选 output [7:0] seg_data // 数码管段选 );这样顶层就变得异常清爽——例化五六个子模块管脚一对一顶到模块端口上顶多再加几根wire连内部信号。写完顶层整个工程的“骨架”就已经全部落地后续填充模块内部逻辑时心理压力小很多。3. 核心模块实现与关键Verilog代码思路模块结构定了之后真正的挑战在于把核心逻辑写得既正确又优雅。这块是手工活但有几个关键的实现思路值得展开讲。3.1 按键消抖比你想的更重要一个容易被轻视的坑是按键的机械抖动。物理按键按下和释放的瞬间会产生几毫秒到几十毫秒的高频跳变不处理的话状态机会在“投入5元”这个事件上被触发三四次金额直接累加出错。这是硬件上必现的问题绝非小概率事件。针对这个问题我采用了“定时采样”消抖思路设置一个每隔10ms翻转一次的节拍信号频率100Hz在这个节拍上读取按键电平连续读到两次稳定电平才算有效。原理很简单机械抖动持续时间一般不超过10ms只要连续两次采样一致就认为按键稳定了。// 按键消抖模块关键代码示意适用于单个按键 always (posedge clk_100hz or negedge rst_n) begin if (!rst_n) begin key_reg1 1b1; key_reg2 1b1; end else begin key_reg1 key_in; // 当前采样 key_reg2 key_reg1; // 上一次采样 end end assign key_pulse key_reg1 ~key_reg2; // 下降沿有效则取key_reg2 ~key_reg1上面只是取边沿脉冲更稳妥的消抖还会加一个计数器——连续N次采样都相同才确认电平变化。别嫌这段逻辑简单有相当多同学的板上现象是“投币金额一下加了两个五元”换上完整的消抖逻辑后立刻药到病除。3.2 主状态机的状态定义与转移条件状态机是自动售货机的灵魂。我建议把状态定义得稍微多一些不要用khan连续满5个状态的大状态机硬抗所有情况而是拆成两层第一层是系统级状态空闲、投币、出货、找零、结束第二层是输入处理状态等待按键确认、等待找零完成。这样扩展性更好加需求的时候只改局部不会推倒重来。我当时定义的状态如下localparam IDLE 3b000, // 空闲 WAIT 3b001, // 等待用户投币/选择 JUDGE 3b010, // 比较金额是否足够 SALE 3b011, // 出货信号 GIVE_CHANGE 3b100, // 找零过程 REFUND 3b101; // 取消退款状态转移的边界条件必须非常清晰清零条件投币成功条件购买成功条件找零完成条件。这四条写清楚状态机基本就稳了。有一个容易出错的小细节是从SALE状态到下一个状态的转移必须等到“出货完成”信号拉高才能跳而不是单纯等一个时钟沿——否则出货LED可能只亮了一个周期就被清零。3.3 金额累加与找零计算的陷阱金额用整数二进制还是BCD编码这是每个做这个题的人都会纠结的问题。我当时的做法是内部计算统一用二进制整数因为化合物加法器用整数最顺手只在最后显示时做一次二进制到BCD的转换。如果一开始就全程用BCD5元加5元需要做十进制加法修正显示倒是方便了但控制逻辑会绕不少弯路。具体思路// 二进制金额累加模块部分代码 always (posedge clk_100hz or negedge rst_n) begin if (!rst_n) coin_sum 8d0; else if (cancel_pulse) // 取消退款 coin_sum 8d0; else if (coin_pulse_5) // 投入5元 coin_sum coin_sum 8d5; else if (coin_pulse_10) // 投入10元 coin_sum coin_sum 8d10; end这段代码看着简单但注意两点第一累加操作只能在单周期脉冲到来时执行不能是电平信号否则按键一直按下时会疯狂重复累加第二我在代码块中省略了对8位无符号数溢出的保护实际工程建议给coin_sum预留9位因为多枚10元累加很容易突破255。设计题目里虽然没有要求支持多币种连续投币但稳健性这关迟早要过。找零计算更简单——出货时如果coin_sum 8找零金额就是coin_sum - 8。真正坑的是找零金的合法性判定如果系统支持4种面额但找零只能找5元或10元币种那售价是8元时会面临“无法找零2元”的尴尬。多数课程设计会用“最小面额1元”来规避这个问题但如果你做的题目没有明说一定要在文档里写清楚假设否则验收老师一句“请问有2元零钱吗”就能让你卡壳。3.4 数码管动态扫描与BCD显示数码管的驱动很多同学以为是“复制粘贴模块”的事。NO恰恰是这里最容易出现频率和时序不匹配的问题。常用方案是动态扫描。8个数码管位选信号每隔2~4ms切换一次段选信号跟对应位的BCD码。注意扫描频率不能太低会闪烁也不能太高超出数码管响应带宽造成亮度不均。实测4kHz左右比较合适也就是每位数码管刷新约500Hz肉眼看不到闪烁。这里有一笔账要算清楚假设4个数码管需要刷新频率为240Hz则扫描周期取1kHz左右足够。如果板上时钟50MHz需要分频到1kHz就除以50000这是典型的计数器分频用法。// 动态扫描位选切换示意4位数码管 reg [3:0] disp_data; always (posedge clk_1k) begin if (scan_cnt 2d3) scan_cnt 2d0; else scan_cnt scan_cnt 1b1; case (scan_cnt) 2d0: begin seg_sel 4b1110; disp_data bcd_ones; end 2d1: begin seg_sel 4b1101; disp_data bcd_tens; end 2d2: begin seg_sel 4b1011; disp_data bcd_hundreds; end 2d3: begin seg_sel 4b0111; disp_data bcd_thousands; end endcase endBCD转换可以用连续除法和取模实现在仿真里直接看波形也可以用查找表。对于小金额两位数以内直接维护两个四位寄存器十进制个位、十位每次累加时做十六进制转十进制修正也是一个轻量做法。这个自己在代码里权衡即可。4. 仿真验证与上板调试把坑一个个趟平写代码只是第一步让代码在仿真里听话、在板子上也听话才算真正完成。这一节我把自己踩过的坑和排查思路全列出来。4.1 测试用例设计不要等到上板才后悔很多同学的仿真测试是“把几个按键各按一遍”看着波形跳了就觉得自己写对了。这种验证强度完全不够。我用了一个笨办法把需求列表翻译成测试列表。测试编号操作序列预期结果TC1复位金额显示为0状态空闲TC2连续投3次5元累计金额显示15TC3投5元投10元累计15按确认后出货LED亮一次找零7TC4投5元后立即取消金额清零状态回到空闲TC5投10元后不按确认金额保持10等待继续TC6边沿毛刺干扰金额不误加TC6是我在后面吃亏后加上的——仿真里手动给按键信号加了一个10ns的高脉冲毛刺结果状态机立刻误触发了。如果不设计这个用例消抖模块的缺陷会一路上板成了板上飘忽不定的“幽灵投币”。所以无论时间多紧毛刺用例必须加。4.2 仿真相对于正确波形的重要性用Vivado或ModelSim做仿真的时候除了看波形“对不对”更要看“时序细节”。我拿到状态机波形后会重点盯三个点每次状态跳变是否发生在时钟上升沿后极短的延迟内脉冲信号是否只有一个周期宽不是一高一低反复翻转复位信号释放后所有key_reg有没有进入确定状态。一个容易忽略的细节是复位信号。如果你用了异步复位复位释放时刻必须避开时钟上升沿否则寄存器输出可能出现亚稳态。这个问题在仿真里不太容易暴露但上板后会有极低概率出现“上电状态随机”的问题。我在顶层加了一个上电延迟复位模块用计数器产生一个持续几毫秒的复位信号规避了这个问题。4.3 上板调试硬件平台的“性格”迥异仿真通过只是写在纸面上的胜利真正上板才是见证常识的时刻。有几个高频问题几乎人人会踩数码管乱码或者少位数。大概率是位选信号极性写反了。共阴数码管与共阳数码管的位选有效电平刚好相反一定要对板子的电路原理图确认不能想当然。按键按下去数据跳变两次。若消抖后的key_pulse宽度过长比如保持了几十个时钟周期状态机里面被判断为“连续触发”就投了两次币。解决办法是确保消抖模块输出的是单周期脉冲必要时加一个专门的脉冲展宽/缩短电路。出货信号亮一下就灭根本没来得及看见。这不是错觉是状态机在SALE状态停留時間太短。建议在SALE状态插入一个延时计数器比如维持500ms让LED和蜂鸣器有足够时间被感知。还有一个软性但很重要的建议上板前先把代码里所有引脚绑定检查一遍。我当年把按键接到了Pmod接口上只因为手册的页边注没看清结果所有按键输入全悬浮高电平状态机一上电就在自动投币。用万用表逐一量过通道电平匹配情况再烧录能省下大量不明不白的排查时间。4.4 状态机死锁的排查链路如果你发现板子卡死——表现为无论怎么按键数码管都停在某个值状态机认为自己在等待但用户怎么操作都没反应。这通常不是单点故障而是状态机跳转条件某个始终不成立。我的排查链路是这样的先看仿真波形确认当前状态变量是哪个值找到这个状态下的所有跳转条件逐个标记“是否可能为真”如果跳转条件引用了某个内部脉冲信号去追踪该脉冲信号是否真的产生如果跳转条件引用了串行比较器比如coin_sum 8检查数据通路有没有被复位意外清零最后看状态机是不是出现了非法状态——如果状态机四选一编码为000~101而板上莫名跳到了110或111大概率是同步复位没做好或者状态编码没有写default兜底。第三和第五步命中概率最高。我自己的死锁bug就出自第五步状态机某个状态分支里寄存器被非阻塞赋值同时修改了两个字段导致状态跳到非定义的编码上板子当然“死”了。改法很简单全部case语句写full case并且加上default回IDLE从此再没犯过。5. 踩坑记录做完这个项目的几点教训做完整套流程后回看这两周的收获是巨大的。有些坑属于共性经验写在这里希望后来的同学能直接绕开。5.1 千万不要从“显示模块”开始写代码我见过不少同学习惯先把数码管显示调漂亮再回头写状态机。这个顺序看似直观其实本末倒置。数码管显示是系统的终端它依赖上市售货机内部的金额数据和控制信号。如果主逻辑没定好显示模块大概率会反复推翻重写。正确顺序应该是先定状态机和数据通路再写分频和消抖最后才写显示。显示承担的只是把内部二进制转BCD画蛇添足不如做好配角。5.2 输出要用寄存器打一拍再用这是一个进阶教训。我的系统里出货信号和找零信号是组合逻辑生成的。问题是当时顶层还加了一个“五元找零模拟器”组合逻辑链条直接拉长在仿真里一切正常但上板时一旦组合逻辑输入变化太频繁输出信号上出现毛刺蜂鸣器会在不该叫的时候哼唧一声。后来我把所有输出信号用时钟打了一拍register out即让输出信号经过一级FF再送到管脚毛刺问题彻底消失。虽然带来一个时钟周期的延迟但在这个应用里完全无所谓。这是数字系统从“功能正确”走向“时序可靠”的关键一步。5.3 不要怕推翻自己之前的划分做完第一版后我花了大半天重构了模块边界把原本放在状态机里的change_sum计算全部搬到了独立的组合逻辑模块里。表面看是增加了模块数量但每个模块的行为边界一下子清晰了。重构不丢人只有让每一块逻辑都能被独立看懂后续排错效率才有质的提升。6. 从“过题”到“学懂”还能怎么扩展这个项目题目做完验收过了但如果你想把这门课的价值榨干自动售货机其实是一块很好的跳板后续可以往好几个方向扩展。找零算法强化。常规题目假设找零币种任意如果改成实际找零策略比如优先使用大面额找零就需要设计贪心算法的硬件实现。这是从组合逻辑走向有限状态机数据通路的很好锻炼。引入支付状态机。支持微信/支付宝扫码支付后系统需要和外部握手协议对接这涉及跨时钟域信号同步和握手状态机设计是业界非常看重的基本功。通讯接口。如果能在售货机上接一个UART串口把每一笔交易打印出来系统就从“单机演示”升级到了“数据上报”。很多实验板自带USB转UART写一个简单的发送模块并不难。低功耗与时钟门控。空闲时关闭数码管扫描等有按键操作再唤醒这是在实际嵌入式系统里很实用的设计考量。扩展方向很多但前提是基础模块你确实吃透了。我个人的建议是如果你在仿真和上板调试中都顺风顺水那一定要挑上面一个方向深入试试如果做基础题已经有点吃力那就回过头把状态机仿真、清零复位、显示时序这几个基本功磨扎实这些东西对后续课程比如计算机组成、嵌入式设计、SoC设计都受益无穷。最后掏心窝子说一句自动售货机这种题目看起来简单但它逼着你在“算法设计-硬件映射-时序收敛-实物验证”这条完整链路上走一遍走完了再看数字系统你的视角会和以前完全不同。祝做这个题目的学弟学妹都能顺利跑通如果调试时遇到什么奇怪的bug欢迎多翻手册、多画波形图那一刻的豁然开朗正是这门课送你的最好礼物。本文还有配套的精品资源点击获取
返回列表