news 2026/9/8 23:51:18

FPGA自动售货机课设:从状态机设计到上板调试的完整实践指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA自动售货机课设:从状态机设计到上板调试的完整实践指南

简介:这是东南大学信息学院短学期数字系统设计课程的自动售货机项目,以FPGA为硬件平台,完整展示从输入信号解析(硬币检测、按键选择)、核心逻辑控制(金额累计、商品选择、找零退款)到输出驱动(显示、货道电机)的闭环设计。面向正在修读数字电路与硬件描述语言课程的高年级本科生,也适合准备FPGA综合实践的开发者。包内共160个文件,整体仅1.46MB,主要源码为VHDL文件,另有xdc约束、xpr工程、rpt报告、log日志以及bat/tcl脚本,便于在Vivado中直接打开工程并复现综合、实现与仿真流程。目前已有532人学习。压缩包还保留了四次dcp网表和多份jou备份,可对比不同设计阶段的资源占用与时序结果;顶层模块划分清晰,便于拆解状态机、按键消抖和分频逻辑,也可借鉴其模块化思路迁移到其他数字系统设计任务中,是深入理解FPGA开发流程和短学期综合实践的参考范本。 东南大学信息学院的数字系统设计课设,自动售货机算是出镜率最高的题目之一了。每年到了课程后期,实验室里讨论最多的就是它——有人觉得状态机绕,有人觉得显示和按键配合起来麻烦,还有人仿真全通过、上板就黑屏。我当年做这个题目的时候也折腾了不少时间,后来帮学弟学妹调过几版,踩过的坑和总结出来的经验都有不少。这篇就围绕自动售货机这个课设,从方案设计、模块划分、状态机实现到上板调试,把关键环节捋一遍。

1. 选题门道:为什么自动售货机是数字系统设计的“黄金课设”

1.1 一个课设题目背后的知识体系

先说结论:自动售货机这题,几乎把数字系统设计课程里的核心知识点全串起来了。

它不是一个纯逻辑题,而是典型的“数据通路 + 控制通路”结合的题目。你既要处理货币输入、商品价格、找零金额这些数据,又要设计一套状态机来管理整个交易流程。说得直白点,数据怎么走、什么时候走、走到哪,全是状态机说了算。

具体来说,这个题目覆盖了这些内容:

  • 有限状态机(FSM)的设计与编码,包括状态转移条件、输出逻辑
  • 时序逻辑设计,寄存器的使用、同步与异步处理
  • 分频电路设计,板载时钟一般几十MHz,而数码管扫描、按键采样根本用不了这么高的频率
  • 按键消抖,机械按键的抖动是硬件问题,处理不好状态机直接就乱了
  • 数码管动态扫描显示,涉及刷新率、位选段选时序
  • 简单的运算电路,比如累计金额、找零计算

有些要求高的版本还会让你加“最多投币金额限制”“售罄指示”“取消退币”这些功能,难度直接再上一个台阶。

1.2 从需求文档到数字系统的常规拆解路径

课设拿到手,第一件事绝对不要打开 Vivado 或者 Quartus 写代码,先看需求,然后把需求翻译成状态和条件。

拿最常见的规格来说:售货机卖三种饮料,价格分别是 2 元、3 元、4 元,支持投 1 元、5 元、10 元,选货后判断金额是否足够,够则出饮料并找零,不够则提示继续投币或取消退币。

翻译成数字系统需求就是:

  • 投币输入需要编码,比如两位二进制区分 1、5、10 三种面额
  • 累计金额寄存器需要做加法,上限可能需要限制,防止一直投下去溢出
  • 选货信号对应三种商品,每种的单价不同
  • 金额足够时触发“出货”“找零”两个动作,找零金额 = 累计金额 - 单价
  • 状态机管理的核心是“空闲 → 投币 → 选货 → 出货找零 → 回到空闲”这条主流程

这一步做完,整个系统的框架就有了。急着写代码的人,后面大概率要反复删改。

2. 系统方案与模块划分:先画图再写代码

2.1 总体架构:数据通路与控制通路

我的习惯是画一张模块框图,把顶层模块拆成几个子模块再动手。自动售货机系统的经典拆分方式如下。

  • 顶层模块,负责实例化所有子模块并连线
  • 按键消抖模块,处理投币按键、选货按键、确认/取消按键
  • 分频模块,生成数码管扫描时钟、按键采样时钟
  • 状态机模块,这是控制通路的核心
  • 金额计算模块,负责累计投币金额和找零计算
  • 数码管显示模块,显示当前金额、商品价格或提示信息

数据通路的流向是:按键面额 → 金额寄存器 → 比较器(与单价比较)→ 找零计算器 → 数码管显示。控制通路的流向是:按键事件 → 状态机状态转移 → 控制信号(出货、找零、金额清零)。

这两个通路之间是相互配合的关系,状态机给出控制信号,数据通路执行运算,运算结果(比如金额够不够)又作为状态转移的条件。很多人写代码的时候把这两个东西混在一起,状态寄存器里面既存状态又存金额,最后状态转移逻辑又长又乱,排错的时候特别痛苦。

2.2 核心模块划分与接口设计

模块划分之后,每个模块的接口先定义清楚,再各写各的。这里给一份我常用的接口清单,可以作为参考。

模块主要接口作用
key_debounceclk, rst_n, key_in, key_pulse消抖并输出单周期脉冲
clk_divclk, rst_n, clk_ms, clk_scan分频输出毫秒级和扫描级时钟
vending_fsmclk, rst_n, coin_pulse, sel_pulse, price, amount_enough状态转移与输出控制
money_calcclk, rst_n, coin_value, add_en, clear, change_value金额累计与找零输出
seg_displayclk, rst_n, data_in, seg, sel数码管动态扫描显示

每个子模块的功能尽量单一,接口信号尽量少。一个常见的问题是模块之间通信信号命名混乱,比如金额模块的输出有的叫 total,有的叫 sum,到了顶层连线的时候两个人各写各的,对不上。建议信号命名统一,例如金额累计用amount_sum,找零用change_out,清一色用小写加下划线,顶层连线时一眼就能看出意思。

2.3 开发环境与目标板卡选型

东南大学信息学院的数字系统设计课一般用的是 FPGA 开发板,主流是 Xilinx 的 Artix-7 系列,EDA 工具是 Vivado。也有部分往年用的是 Altera 的 Cyclone IV 配合 Quartus,核心流程大同小异,但语法细节和引脚约束文件的写法有些区别。

我建议这个项目用 Verilog 实现,原因很简单:课设代码量不大,Verilog 更灵活,而且 Vivado 对 Verilog 的支持非常成熟。VHDL 在类型系统上更严谨,但对这个体量的项目来说,优势体现不出来,写起来还啰嗦。

板卡上的外设要提前确认:有几个按键、几位数码管、时钟频率是多少、是高电平有效还是低电平有效。这些信息决定了你的代码细节。比如按键按下时,输入是高电平还是低电平,直接决定了消抖模块里采样后取反还是不变。

3. 状态机设计:自动售货机的“大脑”

3.1 需求状态与状态转移条件

状态机是这个项目的核心,设计好了整个系统就稳了。我习惯把状态画成状态转移图,虽然课设不强制交这个图,但画一遍之后,写代码的速度会快很多。

以一个带“取消退币”的版本为例,状态可以这样设计:

  • IDLE:空闲状态,等待投币。有投币脉冲,进入COIN_IN;无操作则一直停留
  • COIN_IN:投币累计状态。每次投币累加金额;投币结束可以选货,进入SELECT
  • SELECT:选货状态。按选货键之后,比较金额是否足够。足够则进入DISPENSE,不够则停留在SELECT并给出“金额不足”提示
  • DISPENSE:出货找零状态。输出出货信号、计算找零,执行完成后回到IDLE

如果要求支持“取消退币”,则COIN_IN状态下还有一个取消按键的转移条件,触发后退币并回到IDLE

有些实现还会加一个WAIT_CHANGE状态,把“出货”和“找零”拆开执行,这样控制逻辑更清晰,也方便后续加“找零不足”提示。我个人建议状态宁可多一个,也不要在一个状态里做太多事。

3.2 状态编码方式选择

状态编码有三种常见方式:二进制编码、格雷码、独热码。

课设规模下,我推荐独热码。原因很直接:状态数量少(一般 4 到 6 个状态),独热码每个状态只有一个 bit 为 1,状态转移逻辑就是很简单的比较和置位,不容易出错,而且综合出来的电路时序也干净。二进制编码虽然寄存器用得少,但译码逻辑复杂,排查状态乱跳的时候很难一眼看出当前是哪个状态。

独热码的代价是多用几个触发器,这在 FPGA 上根本不是事儿,资源完全够。课设评审老师问起来,还可以顺势答一条:独热码以寄存器资源换取组合逻辑的简洁,这在 FPGA 设计中很常见。

3.3 三段式状态机模板

状态机的写法我强烈推荐三段式,这是业界标准实践,也可以说是数字设计里最成熟的状态机模板。

第一段是时序逻辑,完成状态寄存器的更新。第二段是组合逻辑,完成次态判断,也就是根据当前状态和输入信号决定下一个状态。第三段是输出逻辑,根据当前状态和输入信号产生输出控制信号。

用 Verilog 写出来大致是这个样子:

// 第一段:状态寄存器 always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= IDLE; else current_state <= next_state; end // 第二段:次态组合逻辑 always @(*) begin case (current_state) IDLE: begin if (coin_pulse) next_state = COIN_IN; else next_state = IDLE; end // 其他状态同理 endcase end // 第三段:输出逻辑 always @(*) begin case (current_state) DISPENSE: dispense = 1'b1; default: dispense = 1'b0; endcase end

这里有个特别要注意的细节:次态逻辑和输出逻辑都用组合逻辑,但为什么输出逻辑不直接跟状态寄存器绑定成时序逻辑,这样可以避免输出比状态晚一个周期,从而在仿真的时候出现输出滞后?实际用的时候,第三段输出如果用<=在时钟沿打一拍,输出天然会比状态慢一拍。有些场景允许,但如果你是控制出货信号,晚一拍可能就导致出货只维持了一个周期而不是一个完整周期,上板后现象非常奇怪。所以第三段输出我统一用组合逻辑,控制信号随状态立即变化。

注意:复位信号的极性、同步复位还是异步复位,必须和板卡的复位按键一致。很多板上复位按键按下去是低电平,代码里写negedge rst_n,这没问题。写反了的表现是:上电后系统直接不在初始状态,状态机随机乱跑。

4. 关键外设模块的实现细节

4.1 按键消抖:别让抖动毁掉状态机

机械按键按下和释放的时候,触点会来回弹跳几毫秒到十几毫秒。如果不做消抖,状态机一次投币动作可能被触发十几次,金额直接爆表,这是上板后最常遇到的诡异现象之一。

消抖的思路是:检测到按键电平变化后,等一段时间再确认电平是否稳定。常用的方法是延时采样,检测到按下沿之后延时 10ms 到 20ms,在稳定期再采样一次,如果仍是按下电平,就认为按键有效。

这个延时从哪里来?有的同学直接用一个计数器在状态机里延时,while循环等 20ms。这在单片机里可以,在 FPGA 里不行,组合逻辑里用while是不可综合的,如果目标是竞赛,就得认真考虑。

正确做法是独立做一个消抖模块,用计数器打拍延时。我用的模板是在时钟驱动下,当按键输入变化时,计数器清零重新计数;当计数器计到设定值且按键电平稳定不变,就输出一个单周期脉冲。

always @(posedge clk or negedge rst_n) begin if (!rst_n) begin key_reg <= 1'b1; cnt <= 0; end else if (key_in != key_reg) begin key_reg <= key_in; cnt <= 0; end else if (cnt == DEBOUNCE_CNT) begin // 稳定后根据边沿产生脉冲 key_pulse <= (key_in != key_reg); // 注意这里需要额外处理 end else begin cnt <= cnt + 1; end end

实际实现里,建议用两级寄存器先把key_in同步到时钟域,再做边沿检测和延时消抖,避免亚稳态传播。这是仿真看不出来,只有上板后偶发乱跳才会发现的问题。

4.2 分频与时钟处理:怎样生成扫描时钟

板载时钟一般很高,我当年用的板子是 50MHz,即 20ns 周期。这个频率直接给数码管扫描用太高了,需要分频。

分频的目的是生成两个不同频率的时钟:按键采样用的 1kHz 左右的时钟,以及数码管扫描用的 1kHz 到 2kHz 左右的时钟。数码管动态扫描频率太快则切换太快,肉眼看到的是亮度不均;太慢则能看到闪烁,一般 1kHz 左右比较合适,也就是每一位点亮 1ms 左右。

分频的做法是用计数器,到达计数值就翻转输出或者产生一个使能脉冲。例如 50MHz 分频得到 1kHz,计数值就是 50000,需要注意计数起点是 0,所以计到 49999 时清零。

reg [15:0] cnt; reg clk_1k; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cnt <= 16'd0; clk_1k <= 1'b0; end else if (cnt == 16'd49999) begin cnt <= 16'd0; clk_1k <= ~clk_1k; end else begin cnt <= cnt + 1'b1; end end

还有一个做法是产生时钟使能信号而非翻转时钟,在建模中叫作 clock enable,这是一种更好的做法。分频后的时钟再用posedge clk_1k去驱动多个模块,很容易出现时钟偏移和时序问题。更稳妥的方案是始终保持系统主时钟,分频模块只生成一个使能脉冲,比如每 1ms 产生一个高电平脉冲en_1k,所有时序逻辑仍然用posedge clk,而用en_1k作为条件判断。

这种做法的好处是:整个设计只有一个时钟域,没有跨时钟域问题,综合出来的时序报告也干净得多。我就是被一个时钟不同步的问题教育过之后改过来的。

4.3 数码管显示:加分项往往在这里

自动售货机至少要显示当前累计金额,好的实现还会显示商品价格、找零金额、提示信息。不要把显示功能堆到状态机里,独立成模块最好。

动态扫描的原理是:某一时刻只点亮一位数码管,通过位选信号选择当前点亮的是第几位,段选信号输出这位要显示的内容,然后快速切换到下一位。利用人眼的视觉暂留效应,看起来像是同时点亮了所有位。

扫描频率、位选切换节奏是重点。比如 4 位数码管,1ms 切换一位,4ms 完成一个轮回,刷新率 250Hz,不会有闪烁感,显示亮度也够。

一个常见的问题是扫描频率和状态机时钟冲突。如果你用同一个时钟既做状态机又做扫描,状态机每个周期都在变,而扫描也要每个周期切换位选,两者会产生奇怪的干扰。我最后是在系统时钟下先用分频使能产生一个scan_switch信号,只有这个信号的脉冲到来时才切换位选,状态机其他时间照常跑。

4.4 找零算法:用减法器还是查表

找零金额的计算,在多数课设规格下,只是一个组合逻辑减法:change = amount_sum - price。但如果你要处理“找零硬币面额组合”,比如 1 元、5 角、1 角几种币值分别出多少个,就需要一个简单的贪心算法或者查表。

贪心算法的思路是:优先用大面额找零,依次减下去。在数字逻辑里实现就是一组减法器和比较器串起来。

另一种更省资源的做法是查表。因为单价和投币组合是有限的,把所有可能的找零方案提前算好存进 ROM 或 case 语句里,输出直接查表。查表的好处是逻辑简单、时序好,坏处是需求变化时表的改动比较麻烦。

我建议:如果课设只要求显示找零金额,用组合减法就够了。如果要求“退回相应数量的硬币”,用查表,逻辑一目了然,评审老师也容易看懂。

always @(*) begin case (change_value) 4'd1: change_coin = 4'b0001; // 找回1元硬币 4'd2: change_coin = 4'b0010; // 找回2元硬币 // 根据实际币值设计编码 default: change_coin = 4'b0000; endcase end

提示:找零计算的结果在状态机里一定要锁存住,不能在回到 IDLE 状态以后因为金额累加寄存器清零而跟着清零。一个简单的做法是在DISPENSE状态把找零结果打一拍到独立的寄存器,显示模块只读这个寄存器的值。

5. 仿真验证与板上调试实录

5.1 Testbench 怎么搭才有效率

仿真不是走过场,这个项目的调试效率很大程度上取决于 Testbench 写得好不好。

我的建议是,不要只测一条“投币-选货-出货”的快乐路径,要把异常路径也覆盖到。比如投币金额不足就选货、投币过程中按取消、连续按两次同一商品按键、投币超过上限。这些情况在状态机设计时会直接影响转移条件,而转移条件恰恰是bug重灾区。

一个简单有效的 Testbench 写法是分段激励,用initial begin按时间顺序给不同的激励组合。比如:

initial begin rst_n = 0; #100 rst_n = 1; // 投1元 coin = 2'b01; #20 coin = 2'b00; // 再投1元 #100 coin = 2'b01; #20 coin = 2'b00; // 选3元商品(此时金额只有2元,应提示不足) sel = 2'b10; #20 sel = 2'b00; // 取消退币 cancel = 1'b1; #20 cancel = 1'b0; end

仿真时重点看两个方面:一是状态机状态是否按预期跳转,二是输出信号(出货、找零)的时序是否在正确的状态和正确的周期出现。

我看波形的时候习惯把状态信号导出来,观察各状态停留的周期数是否符合预期。如果状态在某个输入下没有跳转,或者跳了一个不该去的状态,先查次态组合逻辑的case分支有没有遗漏。

5.2 常见问题与排查技巧速查表

上板调试是另一个世界,很多仿真里永远不会出现的问题,一上板全冒出来了。我这里整理了一个问题速查表,都是我实际遇到过或者帮别人排查过的。

现象可能原因排查方向
投币一次金额加好几次按键没有消抖,或消抖参数不对检查消抖模块,延时是否足够,是否输出单周期脉冲
上电后状态机不工作复位极性不对,或初始状态不对检查复位按键电平、上电复位信号
数码管显示乱跳或闪烁扫描频率不合适,或时钟使能处理不当检查扫描分频参数,尝试改用时钟使能方式
显示亮度不均扫描每一位停留时间不一致检查位选切换逻辑是否均匀
找零金额经常少1元组合电平瞬间变化被采样到找零结果锁存到独立寄存器,用状态机时序控制
选货后不出货状态机输出逻辑用的是时序逻辑,输出晚了一拍输出逻辑改用组合逻辑或提前一拍输出
个别按键时灵时不灵按键信号没有同步,有亚稳态打两拍同步,再做消抖

上板调试最有效的手段是看板上的 LED 指示。我在状态机里预留了一组调试 LED,把当前状态用二进制编码输出到 LED 上。状态跳转对不上?看 LED 就知道状态卡在哪了。这个小技巧在评审答辩的时候讲出来,老师会觉得你的调试思路非常清晰。

如果遇到仿真正常、上板不正常的疑难问题,还有一个经典排查法:用 Vivado 的 ILA(集成逻辑分析仪)抓内部信号。把状态、按键脉冲、金额寄存器抓进去,上板后触发条件设好,就能看到内部真实的波形。这比对着现象瞎猜效率高得多。

6. 扩展思路:从一个课设到完整系统

自动售货机这个题目做完之后,很多同学就交差结束了,其实这个题目完全可以作为课程设计的一个起点,往多个方向延伸。

如果你对 SoC 方向感兴趣,可以把控制逻辑做成一个 AXI 外设,挂到 MicroBlaze 软核上,用 C 语言控制出货逻辑,FPGA 做显示和按键接口。这个方案直接把数字系统设计课程和嵌入式课程的内容打通了,做出来的东西复杂度和完整度都不在一个量级。

如果你对低功耗设计感兴趣,可以尝试给不同模块做时钟门控,统计各状态的翻转率,对比优化前后的功耗差异。这个方向在课设里很少见,如果你做出来,绝对比同组的同学多一个亮点。

如果你对验证方法学感兴趣,可以用 SystemVerilog 给这个自动售货机搭一个 UVM 验证环境,把所有功能覆盖点写全。这在数字系统设计课里算是降维打击,但真的有人这样做过,而且效果很好。

我自己做这个题目最大的体会是:数字系统设计和软件编程最大的不同在于,所有的事情都是并行的、时序的,状态机的状态转移不是顺序执行的 if-else,而是每一个时钟沿大家都在动。理解了这个区别,后面学计算机组成、操作系统、体系结构都会顺很多。自动售货机这个题目,就是帮你建立这种感觉的绝佳载体。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:50:00

MATLAB图像处理实战:菌落自动计数与分割算法详解

简介&#xff1a;这是一份面向生物医学图像处理与机器学习初学者的MATLAB实用项目&#xff0c;目标是从培养皿平板琼脂图像中自动识别并统计细菌菌落数量&#xff0c;替代传统人工计数&#xff0c;提升实验效率并降低主观误差。资源包共含5个文件&#xff0c;总大小仅174KB&…

作者头像 李华
网站建设 2026/9/8 23:49:32

软件控制时钟调制扩展(SC-CME)检测机制详解与实战验证

做系统级电磁兼容测试时候&#xff0c;最怕的就是辐射发射&#xff08;Radiated Emission&#xff09;测试在半程突然冒出一个窄带尖峰&#xff0c;频率不高不低&#xff0c;恰好在民用标准限值附近挣扎。排查到最后&#xff0c;往往不是电源滤波的问题&#xff0c;也不是结构屏…

作者头像 李华
网站建设 2026/9/8 23:49:28

APM飞控源码怎么读?一文掌握ArduPilot源码地图与二次开发

简介&#xff1a;APM飞控源码是针对多旋翼无人机控制系统的开源学习资料&#xff0c;基于ArduCopter 3.2.1版本进行特定裁剪&#xff0c;去掉冗余模块&#xff0c;聚焦APM/Pixhawk飞控核心功能&#xff0c;适合无人机开发者、嵌入式爱好者及航模玩家从源码层面掌握飞行控制原理…

作者头像 李华
网站建设 2026/9/8 23:48:38

类型转换的四种面孔:从显式到隐式的安全实践

1. 这不是玄学&#xff0c;是类型转换在暗处拉扯代码的缰绳 “排查了一周的灵异 Bug&#xff0c;真相是一个不起眼的类型转换”——这句话刚在技术群刷出来&#xff0c;我就下意识摸了摸后颈。不是因为害怕&#xff0c;而是肌肉记忆&#xff1a;那种头皮发紧、咖啡因失效、日志…

作者头像 李华