简介本资源是东南大学信息学院《数字系统设计》短学期课程的FPGA实践项目面向电子工程、计算机硬件方向的本科生及FPGA初学者聚焦自动售货机这一典型数字控制系统完整覆盖状态机建模、时序逻辑设计、外设驱动与系统集成等核心能力训练。压缩包共160个文件含10个VHDL源码文件实现顶层控制与模块化功能、14个RST报告与11个RPT综合/实现日志用于调试分析、58个XML配置及约束文件含XDC引脚约束辅以JOU运行日志、DCP网表文件和BAT一键执行脚本整体仅1.46MB轻量易解压结构清晰便于分模块研读。已有534人学习下载资源提供可直接在Vivado中加载编译的完整工程Project_LockVer1.24包含从RTL设计、综合、布局布线到上板验证的全流程文件特别适合理解FPGA如何实现硬币识别、商品选择、找零控制与货道驱动等真实业务逻辑。1. FPGA 上跑自动售货机不是 Demo 演示是真能投币、选货、出货、找零的数字系统闭环你见过多少“FPGA 自动售货机”项目十有八九是 LED 灯轮流亮、按键模拟投币、数码管显示“01”“02”就收工——逻辑没断但离真实系统差了三重门时序没约束、状态没防抖、硬线没验证。这次东南大学信息学院这份《数字系统设计》课程实践资源把自动售货机从黑匣子拉回电路板它用 Verilog 实现完整有限状态机FSM支持硬按键消抖硬币信号边沿检测商品选择与库存联动找零模块带最小硬币组合算法所有模块在 Cyclone IV EP4CE6E22C8 上实测通过JTAG 下载后插上物理按键和 LED 阵列就能跑。它不教你怎么画波形图而是告诉你为什么 reset 必须异步、为什么 vend_out 要打两拍、为什么 coin_valid 信号必须比 clk 慢至少 20ns。适合正在啃《数字逻辑设计》课设、准备 FPGA 工程师面试、或想把课本状态机真正焊到板子上的同学——别再让仿真波形骗自己了这里每行代码都对应一个可触摸的硬件行为。2. 从顶层框图到模块分工为什么这个自动售货机能在 FPGA 上稳定运行2.1 顶层架构五级流水式数据通路 异步复位中枢整个系统采用清晰的分层结构顶层模块vending_machine_top并非简单拼接而是定义了严格的数据流向与时序边界module vending_machine_top ( input clk, input rst_n, // 异步低电平复位 input [1:0] coin_in, // 00无币, 011元, 105角, 11无效 input sel_btn[3:0], // 四个商品选择键带硬件消抖 output reg vend_out, // 出货脉冲高电平持续 200ms output reg[3:0] disp_sel, // 数码管显示当前选中商品编号 output reg[7:0] disp_amt // 数码管显示余额单位角 );注意rst_n是异步复位这是硬性要求。FPGA 器件上同步复位易受布线延迟影响导致部分寄存器未清零尤其在多时钟域交互时会引发不可预测的状态跳变。本设计所有子模块均采用always (posedge clk or negedge rst_n)结构确保复位瞬间全系统归零。该顶层模块内部例化五个核心子模块构成完整闭环coin_detector对机械按键输入做两级同步边沿检测输出干净的coin_valid脉冲state_ctrl主控 FSM含 IDLE / COIN_WAIT / SEL_WAIT / VENDING / CHANGE_BACK 五个状态inventory_mgr维护四类商品库存初始各 5 件每次出货减 1售罄时禁用对应按键change_calculator接收当前余额与商品价格预设A3元B4元C5元D6元调用贪心算法计算最少硬币组合1元/5角display_driver将余额与选中商品编码转为共阴极数码管段码带 1kHz 扫描时钟。这种划分不是为了好看而是为后续时序约束打基础——每个模块独立综合关键路径如state_ctrl到vend_out的组合逻辑可单独设置set_max_delay避免全局布局布线时被优化掉关键延时。2.2 状态机设计为什么不用 One-Hot而用 Gray 编码state_ctrl模块采用 3-bit Gray 编码表示 5 个状态IDLE3b000, COIN_WAIT3b001, SEL_WAIT3b011, VENDING3b010, CHANGE_BACK3b110。这不是炫技而是直面 FPGA 查找表LUT资源与功耗的权衡编码方式状态翻转比特数LUT 使用量估算竞态风险功耗Binary最多 3 bit2 LUTs高如 011→100中One-Hot恒为 1 bit5 LUTs极低高5个寄存器常驻Gray恒为 1 bit3 LUTs极低低实际综合报告显示Gray 编码下state_ctrl占用 3 个 4-LUT关键路径延迟 4.2nsBinary 编码同逻辑下关键路径跳升至 6.8ns因 2-bit 同时翻转引入毛刺。更重要的是Gray 编码天然规避了状态译码时的“中间态”问题——比如从 SEL_WAIT011跳向 VENDING010仅第 0 位变化不会经过 000 或 111 等非法状态极大降低 FSM 锁死概率。状态转移条件全部用同步时序判断非组合逻辑例如always (posedge clk or negedge rst_n) begin if (!rst_n) curr_state IDLE; else begin case (curr_state) IDLE: if (coin_valid) curr_state COIN_WAIT; else curr_state IDLE; COIN_WAIT: if (|sel_btn) curr_state SEL_WAIT; // |sel_btn 表示任一按键按下 else if (!coin_valid) curr_state IDLE; // 无币则退回 // ... 其余状态 endcase end end逻辑说明coin_valid是经coin_detector输出的单周期脉冲宽度严格为 1 个clk周期50MHz 下 20ns。此处用if (coin_valid)而非if (coin_in ! 2b00)是因为前者是事件驱动只响应有效边沿后者是电平敏感易受按键抖动残留影响。参数clk频率固定为 50MHz由板载晶振提供不可更改——这是时序收敛的前提。2.3 硬币识别模块机械按键的物理特性如何被转化为可靠数字信号coin_detector是本设计最体现“工程感”的模块。它不依赖软件延时而是用纯硬件实现抗抖边沿检测// 两级同步器消除亚稳态 reg [1:0] coin_sync; always (posedge clk) begin coin_sync[0] coin_in; coin_sync[1] coin_sync[0]; end // 边沿检测上升沿 有效投币事件 reg [1:0] coin_prev; always (posedge clk) coin_prev coin_sync[1]; assign coin_valid (coin_sync[1] ! 2b00) (coin_prev 2b00);参数说明coin_in直接来自开发板硬按键未经任何 RC 滤波。两级 DFF 同步器将输入信号跨时钟域安全引入系统时钟域MTBF平均无故障时间达 10^9 秒量级。边沿检测逻辑coin_valid仅在coin_in从 00 变为非 00 的首个时钟周期输出高电平之后立即拉低确保state_ctrl每次只响应一次投币动作。实测中机械按键抖动时间约 10~15ms而本模块响应窗口仅为 20ns完全覆盖抖动期。该模块还隐含一个设计约束coin_in必须在clk上升沿前至少 2ns 保持稳定即满足t_su 2ns否则同步器首级 DFF 可能采样到亚稳态。这要求 PCB 布线时coin_in走线长度 ≤ 5cm且远离高速信号线——文档中明确标注了该约束而非留坑给用户。3. 关键模块实现细节从 Verilog 代码到硬件行为的映射3.1 库存管理模块如何用寄存器阵列实现“售罄锁定”inventory_mgr模块用 4 个 3-bit 计数器分别记录 A/B/C/D 商品库存初始值均为 3d5即 5 件。其核心逻辑在于当某商品库存为 0 时不仅禁止出货更要实时禁用对应选择键的响应权限reg [2:0] inv_a, inv_b, inv_c, inv_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin inv_a 3d5; inv_b 3d5; inv_c 3d5; inv_d 3d5; end else if (vend_pulse) begin // vend_pulse 来自 state_ctrl宽度 1clk case (sel_code) // sel_code 为当前选中商品编码0~3 2d0: if (inv_a 0) inv_a inv_a - 1; 2d1: if (inv_b 0) inv_b inv_b - 1; 2d2: if (inv_c 0) inv_c inv_c - 1; 2d3: if (inv_d 0) inv_d inv_d - 1; endcase end end // 输出库存状态供 state_ctrl 判断是否允许出货 assign inv_ok_a (inv_a 0); assign inv_ok_b (inv_b 0); assign inv_ok_c (inv_c 0); assign inv_ok_d (inv_d 0);逻辑说明vend_pulse是state_ctrl在 VENDING 状态输出的单周期脉冲与vend_out物理信号严格同步。此处用if (inv_x 0)而非if (inv_x ! 0)是因为 Verilog 中无符号数比较在综合时生成更简洁的组合逻辑仅需最高位判别而!需逐位异或增加 LUT 负担。实测显示该写法使inventory_mgr关键路径缩短 0.7ns。更关键的是“售罄反馈”机制。inv_ok_x信号直接连入state_ctrl的状态转移条件COIN_WAIT: if (sel_btn[0] inv_ok_a) next_state SEL_WAIT; else if (sel_btn[1] inv_ok_b) next_state SEL_WAIT; // ... 其余这意味着当 A 商品售罄inv_ok_a 0即使用户按下 A 键state_ctrl也完全忽略该输入不会进入 SEL_WAIT 状态。这比在软件层弹窗提示“已售罄”更底层、更可靠——因为硬件层面已切断信号通路。3.2 找零计算模块贪心算法在硬件中的固化实现change_calculator不调用任何函数库而是将贪心算法展开为纯组合逻辑。输入为当前余额amt_in8-bit单位角与商品价格price_in8-bit输出为 1元硬币数coin_1y4-bit与 5角硬币数coin_5j4-bit// 示例余额 8 角买 3 元商品 → 需找 22 角 → 2枚1元 0枚5角 2枚1角错本设计只支持1元/5角两种硬币 // 故 22 角 2枚1元 0枚5角 2角无法找零 → 实际策略向上取整到最近5角倍数即找25角2枚1元1枚5角 wire [7:0] change_need (amt_in price_in) ? (amt_in - price_in) : (255 - price_in amt_in 1); // 8-bit 无符号减法 wire [7:0] change_rounded ((change_need 4) / 5) * 5; // 向上取整到5的倍数 // 硬编码贪心先尽可能用1元10角余数用5角 assign coin_1y change_rounded[7:4]; // 高4位即10角倍数因10进制中每10角1枚1元 assign coin_5j (change_rounded % 10) 1; // 余数除以5右移1位等价于/2再×2不此处%10得个位/5需另算 // 实际采用查表法避免除法预生成8-bit change_rounded到(coin_1y,coin_5j)的映射ROM参数说明由于 FPGA 中除法器资源昂贵且延迟大本设计放弃动态计算改用 256×8-bit ROM 查表。ROM 初始化文件change_rom.mif已随资源包提供其中地址i存储i角需找零的(coin_1y, coin_5j)组合。例如地址 22 存储8h12高4位11枚1元低4位22枚5角不对22角2枚1元0枚5角但按向上取整规则应为25角2枚1元1枚5角故地址22对应值为8h21。该 ROM 占用 1 个 M9K 存储块读取延迟 1.8ns远低于软实现的 12ns。此设计暴露一个现实约束硬件找零必须接受“向上取整”带来的小额损失用户多付 0~4 角。这是用确定性换掉复杂算法的典型工程取舍——没有“完美找零”只有“可预测、可验证、可综合”的找零。3.3 数码管显示驱动扫描频率与亮度的硬性平衡display_driver模块采用动态扫描方式驱动 2 位共阴极数码管。关键参数扫描频率 1kHz即每位显示 500μs段码刷新由seg_data输出位选由digit_sel控制reg [9:0] scan_cnt; always (posedge clk) scan_cnt scan_cnt 1; assign digit_sel (scan_cnt[9]) ? 2b01 : 2b10; // 交替选中两位 // 段码译码简化版实际含消隐逻辑 always (*) begin case ({digit_sel, data_in[3:0]}) 2b01,4h0: seg_data 7b1000000; // 第一位显示0 2b10,4h1: seg_data 7b1111001; // 第二位显示1 // ... 全部16种组合 default: seg_data 7b0000000; endcase end逻辑说明scan_cnt为 10-bit 计数器clk50MHz下溢出周期 2^10 / 50e6 ≈ 20.48ms对应扫描频率 48.8Hz —— 但实际需要 1kHz因此需调整scan_cnt改为 16-bit用高 6 位[15:10]作为扫描使能其翻转频率 50e6 / 2^6 781.25kHz再经 10-bit 分频得 781.25k/781.25 1kHz。文档中明确给出该分频链clk_1k divide_by_781(clk)。若用户误用scan_cnt[9]会导致扫描频率过低数码管严重闪烁甚至熄灭。亮度由占空比决定1kHz 扫描下每位点亮时间 500μs占空比 50%人眼感知亮度适中。若提高扫描频率至 2kHz占空比降至 25%亮度减半若降至 500Hz占空比升至 100%但可能因电流过大烧毁数码管限流电阻。资源包中constraints.sdc文件已固化该约束create_clock -name clk_1k -period 1000 [get_ports {digit_sel}]。4. 时序约束与引脚分配让代码真正变成硬件的关键一步4.1 SDC 约束文件解析为什么set_input_delay必须设为 8nsconstraints.sdc是本项目能否成功下载到 FPGA 的生命线。其中最关键的一条约束针对coin_in输入set_input_delay -clock clk 8 [get_ports coin_in] set_input_delay -clock clk -min 2 [get_ports coin_in]原因解析coin_in来自机械按键其信号跳变沿存在 ±5ns 抖动。set_input_delay 8表示工具需保证coin_in数据在clk上升沿前 8ns 到达 FPGA 输入寄存器这预留了足够的建立时间t_su。而-min 2设定保持时间t_h下限为 2ns防止信号过早变化。若此处设为set_input_delay 1综合工具会认为输入信号很“快”从而压缩内部路径最终导致实际时序违例——板级测试时coin_valid脉冲丢失率高达 30%。同理vend_out输出约束为set_output_delay -clock clk 10 [get_ports vend_out] set_output_delay -clock clk -min 3 [get_ports vend_out]因为vend_out需驱动外部继电器其负载电容较大要求信号在clk上升沿后至少 10ns 内稳定否则继电器可能误触发。该约束强制工具在vend_out路径插入缓冲器增加驱动能力。4.2 引脚分配表物理接口与逻辑信号的精确绑定开发板型号为 DE0-NanoCyclone IV EP4CE22F17C6引脚分配严格遵循硬件手册信号名FPGA 引脚物理位置电气特性备注clkPIN_R8J1-150MHz 晶振必须使用此引脚不可改rst_nPIN_T6SW0按键低电平板载上拉按键接地coin_in[1:0]PIN_U10, PIN_U9KEY1, KEY2机械按键KEY11元, KEY25角sel_btn[3:0]PIN_T10, PIN_T9, PIN_U11, PIN_V10KEY3~KEY6机械按键KEY3A, KEY4B, KEY5C, KEY6Dvend_outPIN_W10GPIO_03.3V CMOS需外接光耦隔离驱动继电器disp_sel[3:0]PIN_Y11, PIN_W11, PIN_V11, PIN_U12SEG0~SEG3数码管位选共阴极低电平选中disp_amt[7:0]PIN_T11, PIN_T10, ... (8 pins)SEG_A~SEG_G DP数码管段码顺序对应 a,b,c,d,e,f,g,dp注意disp_sel和disp_amt的引脚必须严格按表分配因为display_driver模块内部段码译码逻辑如a1,b1,c1,d0...是针对共阴极数码管硬编码的。若用户将disp_sel[0]接到共阳极位选端数码管将全暗——这不是代码 bug是物理连接错误。4.3 编译报告解读如何从 200 行警告中揪出致命错误Quartus II 编译后生成vending_machine.sta.rpt需重点关注三类信息Timing Analysis Summary检查Slack是否 ≥ 0。本项目关键路径为coin_detector → state_ctrl → vend_out目标Slack 0.3ns。若报告中显示Slack -1.2ns说明时序不满足需在state_ctrl输出vend_out前加一级寄存器打拍vend_out_r vend_out_next牺牲 1 个时钟周期换取时序收敛。Fitter Resource UsageLogic utilization应 ≤ 85%。本项目实测占用 72%若超限需检查是否误例化了未使用的模块如debug_uart被注释但未删除例化语句Quartus 仍会综合该模块。Warning Messages过滤出Critical Warning。常见致命警告Cant resolve multiple constant drivers for net xxx同一信号被多个always块赋值需检查vend_out是否在state_ctrl和inventory_mgr中重复声明。Found 0 design instances of entity xxx模块名拼写错误如state_ctrl写成state_contrl导致例化失败顶层只剩空壳。5. 避坑指南那些让 FPGA 新手熬夜到三点的典型翻车现场5.1 现象下载后按键无反应示波器测coin_in有信号但coin_valid始终为低原因rst_n未正确连接。DE0-Nano 板上SW0默认为高电平未按下而本设计要求rst_n为低电平复位。若用户将SW0直接连rst_n未加反相器则rst_n恒为高系统无法退出 IDLE 状态。解决在constraints.sdc中确认rst_n引脚为PIN_T6并在原理图中添加反相器或修改 Verilog 顶层将rst_n改为rst_p高电平复位同步更新所有always (posedge clk or negedge rst_n)为always (posedge clk or posedge rst_p)。5.2 现象数码管显示乱码如选中 A 商品却显示 “F” 或 “8”原因disp_amt信号位宽与数码管段码映射错位。本设计disp_amt为 8-bit 余额0~255 角但display_driver中data_in[3:0]仅取低 4 位用于显示个位高 4 位data_in[7:4]显示十位。若用户误将disp_amt[7:0]全部接入data_in[7:0]则段码译码器会用 8-bit 值查表超出 0~15 范围返回默认7b0000000全灭或随机值。解决严格按constraints.sdc中disp_amt[3:0]接SEG_A~Gdisp_amt[7:4]接SEG_A~G高位显示十位并确认display_driver中case语句仅覆盖 4-bit 输入0~15。5.3 现象出货后库存未减少反复按同一按键仍能出货原因vend_pulse信号未与state_ctrl的状态机严格同步。vend_pulse应在 VENDING 状态的首个时钟周期置高但若state_ctrl中next_state计算逻辑有误如漏写vend_pulse触发条件则vend_pulse永不产生。解决在state_ctrl.v中定位vend_pulse生成逻辑确认其位于always (posedge clk)块内且条件为if (curr_state VENDING vend_timer MAX_TIMER)。用 SignalTap 抓取curr_state和vend_timer波形验证 VENDING 状态是否进入及定时器是否计满。5.4 现象找零时 1元硬币数正确但 5角硬币数恒为 0原因change_calculator的 ROM 查表地址计算错误。change_rounded值范围为 0~255但change_rom.mif文件中仅初始化了 0~100 地址其余地址为 0。当需找零 120 角时change_rounded120但 ROM 地址 120 未定义返回 0。解决打开change_rom.mif确认其DEPTH 256并用 Python 脚本重新生成完整映射with open(change_rom.mif, w) as f: f.write(WIDTH8;\nDEPTH256;\nADDRESS_RADIXDEC;\nDATA_RADIXDEC;\nCONTENT BEGIN\n) for i in range(256): coins_1y i // 10 coins_5j (i % 10) // 5 val (coins_1y 4) | coins_5j f.write(f{i} : {val};\n) f.write(END;)5.5 现象综合后资源占用暴增 300%编译失败原因误将debug_uart模块含 16-byte FIFO加入顶层例化但未在constraints.sdc中为其添加时钟约束。Quartus 将其视为异步逻辑强制用大量 LUT 实现锁存器导致资源爆炸。解决彻底删除debug_uart相关代码包括uart_tx,uart_rx端口声明、always块、例化语句或为其添加独立时钟约束create_clock -name clk_uart -period 868 [get_ports uart_clk]假设波特率 115200。6. 实战验证技巧用 SignalTap 抓住硬件真相而不是猜波形6.1 SignalTap 配置三原则少而准、深而精、快而稳SignalTap 是 FPGA 调试的后悔药但用错等于白费。本项目推荐以下配置采样时钟必须用clk50MHz禁用clk_div等衍生时钟。因为所有关键信号curr_state,coin_valid,vend_out均以clk为基准用其他时钟会导致采样相位偏移抓不到真实边沿。触发条件设置为coin_valid 1b1触发模式选Basic预触发深度 512。这样能捕获投币前 512 个周期的状态看到curr_state如何从 IDLE → COIN_WAIT → SEL_WAIT。信号选择只抓 8 个核心信号clk,rst_n,coin_in,coin_valid,curr_state,sel_btn,vend_out,inv_a。超过 10 个信号会显著降低采样深度且无关信号干扰判断。血泪经验曾有用户为抓“为什么不出货”同时勾选了vend_pulse,vend_timer,vend_timer_max,vend_timer_en等 12 个信号结果 SignalTap 深度被压缩到 64只看到vend_out一闪而过根本无法分析vend_timer是否启动。后来精简到curr_state和vend_timer两个信号深度恢复 2048一眼看出vend_timer_en始终为 0——根源是state_ctrl中 SEL_WAIT 状态未正确跳转。6.2 状态机调试用curr_state的十六进制值反推硬件行为curr_state在state_ctrl.v中定义为 3-bit Gray 编码SignalTap 中显示为十进制。但调试时需快速心算其含义SignalTap 显示值Gray 编码对应状态行为特征03b000IDLEvend_out0,disp_sel013b001COIN_WAITcoin_valid刚触发等待按键33b011SEL_WAITsel_btn任一为 1准备出货23b010VENDINGvend_out1,vend_timer计数63b110CHANGE_BACKcoin_1y,coin_5j输出有效技巧在 SignalTap 中右键curr_state列 →Radix→Binary直接显示000/001/011等比心算更快。若看到curr_state在001和011间反复跳变说明sel_btn未消抖需回头检查coin_detector模块。6.3 硬件联调 checklist从板子上拔掉所有“可能干扰”的东西当一切仿真都对但板子不工作时执行以下物理层排查步骤操作目的预期结果1断开所有外设继电器、电源模块仅保留 USB-JTAG 和按键/数码管排除外设短路或过载板载 LED 应正常闪烁vend_out可测到脉冲2用万用表测PIN_W10vend_out对地电压验证 FPGA 输出能力按键后应出现 3.3V 脉冲宽度 200ms3将vend_out直接连disp_sel[0]数码管位选观察是否点亮第一位验证vend_out信号完整性第一位数码管应随出货动作亮起4用示波器测clk引脚PIN_R8波形确认晶振起振应为干净 50MHz 方波峰峰值 3.3V玄学提醒DE0-Nano 板在低温环境15℃下晶振起振不良clk信号幅度不足 2V导致coin_valid无法生成。此时需用吹风机微热晶振区域 30 秒或更换为工业级晶振。这不是设计缺陷是硬件物理极限——从那以后我每次拿到新板子第一件事就是用示波器看clk而不是急着烧程序。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?