FPGA课程设计:豆浆机面板完整方案(状态机、按键消抖、数码管显示)
简介一份基于FPGA的豆浆机面板设计课程设计完整工程面向电子与自动化专业学习Verilog和EDA开发的学生。资源以豆浆机控制为核心实现六档功能选择、拨码开关报警、LED点阵模拟电机与加热器、温度传感器先预热再转动、防溢防干烧、四位数码管显示等采用FPGA主控配合按键、蜂鸣器、电机驱动等外设覆盖按键消抖、状态机、分频计时、显示驱动等典型模块完整展示从需求分析、编码到综合下载的全流程。压缩包共188个文件约4.51MB包含TDF、VHDL、Verilog源码、Quartus配置与烧写文件、仿真波形、设计文档及说明等目录清晰便于按模块查阅。已有813人学习下载适合需要完成类似课设或入门FPGA项目开发的读者可直接参考工程框架与模块划分思路进行二次开发。1. 为什么用FPGA做豆浆机面板而不是单片机FPGA课程设计选豆浆机面板第一反应往往是这玩意儿用单片机不是更简单吗。确实从产品角度讲真实豆浆机的控制面板多数是MCU方案但课程设计的目的是让你在一颗可编程逻辑器件上把组合逻辑、时序逻辑、状态机、分频这些基本功全走一遍而豆浆机面板恰恰是一个能把按键输入、状态跳转、定时计数、显示输出、蜂鸣器控制全部串起来的完整场景。相比流水灯或数码管计数器它有一个明确的产品语义待机、制浆、加热、保温四个阶段每个阶段的面板行为都不一样。这个语义让你写状态机时有据可依而不是为了写状态机而硬造状态。如果你正在选课设题目或者拿到这个题目不知从哪下手这篇方案就是把需求分析→模块划分→代码实现→仿真验证→上板调试→答辩验收整条链路给你铺开照着做就能交出一份结构完整、能跑能讲的FPGA面板设计。2. 需求分析与总体架构先把豆浆机面板到底干什么拆清楚2.1 功能需求按键、显示、蜂鸣器、状态机缺一不可豆浆机面板的核心交互可以归纳为四条一是按键输入至少要有启动/停止、功能选择两个键功能选择用于切换纯豆浆五谷豆浆果蔬汁等模式不同模式对应不同的制浆流程二是状态指示用LED或数码管显示当前状态比如待机显示00制浆倒计时加热显示温度区间保温显示剩余时间三是蜂鸣器提示按键按下响一声短音流程结束响三声长音四是状态机控制这是整块面板的逻辑中枢把待机→制浆→加热→保温→待机的迁移条件定清楚。在实际课设里我一般建议把功能做减法而不是做加法。按键控制在34个模式控制在3种以内显示用两位数码管加三个状态LED就够了。功能太多会让状态机膨胀仿真和上板调试时间成倍增加。你答辩时的加分项不是功能多而是每个功能都有清晰的时序波形和对应的实现说明。2.2 顶层模块划分五个模块各管一件事整体架构按数据流方向切分顶层例化五个模块按键扫描模块负责采集按键并输出消抖后的脉冲状态机模块接收按键脉冲和定时模块的溢出信号输出当前工作状态定时计数模块在制浆和保温阶段产生倒计时显示模块根据状态和计数值驱动两位数码管蜂鸣器控制模块根据按键和状态跳变生成提示音。顶层模块 soymilk_panel ├── key_scan // 按键消抖与边沿检测 ├── fsm_main // 主状态机待机/制浆/加热/保温 ├── timer_cnt // 分频与倒计时 ├── seg_display // 数码管动态扫描 └── buzzer_ctrl // 蜂鸣器驱动波形生成这个划分的逻辑是每个模块只依赖输入信号和少量握手信号不跨模块访问寄存器。状态机和定时器之间通过一个start_pulse和一个timeout_flag通信显示模块只读状态机的状态寄存器和定时器的计数值不反向改写任何控制信号。这样你在调试时任何一个模块出问题都能通过抓信号定位不用在模块间相互拉扯的耦合逻辑里找原因。接口时序上需要约定清楚所有模块统一使用一个50MHz系统时钟通过分频产生1kHz按键扫描时钟和1Hz倒计时基准。按键脉冲宽度为一个按键时钟周期状态机的状态跳变在时钟上升沿同步完成显示模块的数码管扫描刷新频率设在500Hz1kHz之间避免肉眼可见的闪烁。3. 状态机与按键模块实现把最容易翻车的两个设计点夯实3.1 按键消抖与边沿检测不只按一下要按对一下按键模块是面板的外层入口机械按键按下和释放时都会产生十几毫秒的抖动如果直接采电平一次按下会被当成好几次触发。常用的做法是两级处理先做延时消抖再做边沿检测。下面这段代码是典型的计数器消抖方式用一个10ms窗口来判断按键是否真正稳定。module key_scan ( input clk_1k, // 1kHz 扫描时钟 input key_in, // 按键原始输入低有效 input rst_n, output reg key_pulse // 消抖后的按键脉冲高有效 ); reg [3:0] count; // 10ms 计数器1kHz 下计到 9 即 10ms reg key_stable; always (posedge clk_1k or negedge rst_n) begin if (!rst_n) begin count 4d0; key_stable 1b1; key_pulse 1b0; end else if (key_in 1b0) begin // 检测到低电平开始连续计数 if (count 4d9) begin key_stable 1b0; // 连续 10ms 为低视为稳定按下 count 4d0; end else begin count count 1b1; end end else begin // 电平变高立即复位计数 count 4d0; key_stable 1b1; end end // 下降沿检测稳定值为低且前一拍为高时产生脉冲 reg key_stable_d; always (posedge clk_1k or negedge rst_n) begin if (!rst_n) key_stable_d 1b1; else key_stable_d key_stable; end always (*) begin if (~key_stable key_stable_d) key_pulse 1b1; else key_pulse 1b0; end endmodule这里需要说明两个设计选择。第一消抖时间10ms不是随便定的机械按键的抖动时间一般在510ms取10ms可以覆盖大部分按键如果用的按键质量较差可以调整count的上限值到19对应20ms但不要贪大否则按键响应会明显滞后。第二消抖后的key_stable本身还有一次打拍目的是产生下降沿脉冲让状态机只在一个时钟周期内收到一次触发不会因为按键长时间按住而重复触发。这个模块在仿真时最容易被忽略的是复位初值。注意key_stable初始赋的是1b1也就是未按下的稳定态如果初始值给反上电后第一次按键判断会多出一个虚假脉冲。这种问题综合时不报错仿真波形也不明显但上板后表现就是按一次蹦两下。3.2 主状态机四种状态加三个迁移条件状态编码用独热还是顺序状态机模块是整个面板的决策中心。这里我给出一个四状态的定义待机IDLE、制浆GRIND、加热HEAT、保温KEEP。迁移规则是待机下按启动进入制浆制浆倒计时结束进入加热加热达到设定时间进入保温保温倒计时结束回到待机任何时候按下停止键都回到待机。module fsm_main ( input clk_1k, input rst_n, input key_start, // 启动键脉冲 input key_stop, // 停止键脉冲 input grind_done, // 制浆倒计时完成 input heat_done, // 加热完成 output reg [1:0] state, output reg [1:0] state_out // 输出给显示模块的状态编码 ); localparam IDLE 2d0; localparam GRIND 2d1; localparam HEAT 2d2; localparam KEEP 2d3; always (posedge clk_1k or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin case (state) IDLE: if (key_start) state GRIND; else state IDLE; GRIND: if (key_stop) state IDLE; else if (grind_done) state HEAT; else state GRIND; HEAT: if (key_stop) state IDLE; else if (heat_done) state KEEP; else state HEAT; KEEP: if (key_stop || grind_done) state IDLE; else state KEEP; default: state IDLE; endcase end end always (*) begin case (state) IDLE: state_out 2b00; GRIND: state_out 2b01; HEAT: state_out 2b10; KEEP: state_out 2b11; default: state_out 2b00; endcase end endmodule状态编码上我选了2位顺序编码而不是独热码。原因有两个一是这个状态机只有四个状态迁移逻辑简单独热码节省组合逻辑的优势体现不出来二是顺序编码在仿真波形里一眼能读出当前状态值调试时直接看state信号就知道机器跑到哪一步方便对照需求检查。独热码适合几十个状态的大状态机用在豆浆机面板上是给自己找麻烦。状态机的迁移条件里有一个细节值得注意保温结束的条件我写的是key_stop || grind_done这里grind_done复用了一个定时器到期信号。实际项目中更严谨的做法是为每个阶段配独立的定时参数但课设里为了减少定时器实例通常会复用。你要在答辩时主动说明这个复用关系并说清楚保温的保温时长设置为与制浆时长相同是需求假设而不是bug。3.3 定时器分频与倒计时1Hz基准时钟怎么干净地产生定时模块的本质是把50MHz时钟逐级分频到1Hz再在每个1Hz脉冲到来时递减计数。分频有两种写法计数器分频和锁相环分频。课设里用计数器分频就够了但要注意50MHz分到1Hz需要50_000_000分频这个计数器宽度至少要26位2的26次方约6700万仿真时如果用完整分频参数跑一秒要等5000万个时钟周期仿真时间长得无法接受。所以我会在代码里把分频参数做成可通过宏开关切换的形式define SIM_MODE ifdef SIM_MODE parameter CLK_DIV 50; // 仿真时 50 个时钟周期输出一个脉冲 else parameter CLK_DIV 50_000_000; // 上板时 50MHz - 1Hz endif这是一个血泪经验总结下来的写法。刚开始做课设的同学通常直接写死50_000_000结果testbench里跑十分钟波形都不动以为是死循环其实只是仿真时间单位太小。用宏切分参数后仿真时验证逻辑功能上板前把宏注释掉重新综合即可。需要注意ifdef宏定义要在编译时通过defineSIM_MODE传入或者直接在代码里注释切换两种方式都可以但记得上板前必须关掉仿真参数。4. 显示与蜂鸣器模块让面板的状态看得见听得着4.1 数码管动态扫描两位共阳极数码管的刷新时序豆浆机面板的显示模块用两位数码管显示倒计时数值。两位数码管如果每位都接独立的段选信号需要14个引脚这在开发板上往往不够用。标准做法是动态扫描两个数码管共用段选线用位选信号分时点亮扫描频率500Hz以上时人眼会因视觉暂留效应看到两位同时稳定显示。module seg_display ( input clk_1k, // 1kHz 扫描时钟 input rst_n, input [1:0] state_code, // 当前状态编码 input [7:0] counter_value, // 倒计时当前值 output reg [7:0] seg_data, // 段选信号低有效 output reg [1:0] seg_sel // 位选信号低有效 ); reg scan_phase; reg [3:0] current_num; always (posedge clk_1k or negedge rst_n) begin if (!rst_n) scan_phase 1b0; else scan_phase ~scan_phase; end always (*) begin case (scan_phase) 1b0: begin seg_sel 2b10; // 点亮低位 current_num counter_value % 10; end 1b1: begin seg_sel 2b01; // 点亮高位 current_num counter_value / 10; end endcase end always (*) begin case (current_num) 4d0: seg_data 8b1100_0000; 4d1: seg_data 8b1111_1001; 4d2: seg_data 8b1010_0100; 4d3: seg_data 8b1011_0000; 4d4: seg_data 8b1001_1001; 4d5: seg_data 8b1001_0010; 4d6: seg_data 8b1000_0010; 4d7: seg_data 8b1111_1000; 4d8: seg_data 8b1000_0000; 4d9: seg_data 8b1001_0000; default: seg_data 8b1100_0000; endcase end endmodule这个写法里有一个容易踩坑的点seg_sel和seg_data都是组合逻辑输出如果在扫描相位切换的瞬间current_num还没有稳定数码管上会出现短暂的拖影甚至串位。实际现象是倒计时数字快速变化时高位偶尔会闪一下低位的内容。通常解决方法是把seg_sel和seg_data在时钟边沿同步打一拍但打拍会让显示滞后一个周期对面板场景无所谓。课程设计答辩时如果被问到这个现象你就回答组合逻辑竞争冒险问题通过时序同步消除这是标准答案。4.2 蜂鸣器控制按下短鸣和流程结束长鸣的两个计时器蜂鸣器模块相对简单核心是有源蜂鸣器在上电高电平时发声所以只要控制输出电平的持续时间。我会用两个参数SHORT_DUR短鸣100ms和LONG_DUR长鸣500ms通过一个简单的状态机切换静音和发声。module buzzer_ctrl ( input clk_1k, input rst_n, input key_pulse, input flow_done, output reg buzzer_out ); reg [1:0] state; reg [9:0] cnt; localparam IDLE 2d0; localparam SHORT 2d1; localparam LONG 2d2; always (posedge clk_1k or negedge rst_n) begin if (!rst_n) begin state IDLE; cnt 10d0; buzzer_out 1b0; end else begin case (state) IDLE: begin if (key_pulse) begin state SHORT; cnt 10d100; // 100ms buzzer_out 1b1; end else if (flow_done) begin state LONG; cnt 10d500; // 500ms buzzer_out 1b1; end end SHORT, LONG: begin if (cnt 10d0) begin state IDLE; buzzer_out 1b0; end else begin cnt cnt - 1b1; end end endcase end end endmodule这里的设计说明要提出一个实际问题flow_done信号在状态机模块中来自保温结束但保温结束的同时主状态机会跳回IDLE蜂鸣器模块如果在同一个时刻采样到flow_done需要确认该信号至少持续一个时钟周期。我在代码里让flow_done由状态机输出打两拍产生保证脉冲宽度足够被蜂鸣器模块捕获避免响了一声就没了的尴尬。这个细节虽然小但在上板验证时真遇到过属于典型的跨模块握手信号宽度问题。5. 避坑指南FPGA面板设计里最容易翻车的五个地方5.1 数码管扫描频率设置不当导致显示闪烁现象是倒计时数字闪烁明显甚至完全看不清数字。原因有两种一是扫描频率低于50Hz人眼能感知刷新过程二是两个位选信号的占空比严重不均某一位点亮时间过长。解决方法是把扫描时钟设置为500Hz1kHz且每一位点亮的时间严格对半。调整为1kHz后实测没有可见闪烁再用示波器看位选波形确认占空比是50%。5.2 按键消抖时间过长导致按了没反应现象是按键按下去要等一两秒才响应体验很差。原因是消抖计数器上限设置过大比如把10ms窗口写成了1s窗口。解决方法是回查计数器的位宽和使能条件确认在1kHz时钟下计数到9就是10ms不要贪多。这个问题的隐蔽性在于仿真时很难发现因为仿真激励通常把按键抖动模型简化成理想电平只有上板才会暴露。5.3 状态机不判default导致跑飞后无法自恢复现象是连续快速按键后面板进入一个奇怪的模式按键全部失灵。原因是状态机的case语句没有default分支在按键脉冲毛刺或亚稳态干扰下状态机跳到了未定义编码。解决方法是所有case语句补default且default恢复到IDLE。这是一个用半小时就能改完但能省掉一整天调试时间的习惯。5.4 仿真正常但上板数码管乱码现象是testbench波形完全正确但上板后某个数字的段选显示错误比如8显示成了0。原因通常是段选编码表和板子的原理图对不上共阳极和共阴极搞反或者某个段位在板上被反相电路翻转。解决方法是上板前逐个点亮单段而不是直接显示数字。写一个测试用的小模块让每一段轮流亮1秒对照板子上的丝印确认段码表映射关系这个动作必须在综合前完成。5.5 忘记检查时序约束导致综合后频率不达标现象是上板后系统运行一两分钟就死机复位后又能跑。原因是代码里存在过长组合逻辑路径比如显示模块用counter_value / 10这种除法运算综合工具把它展开成大量组合逻辑关键路径延迟超过了时钟周期出现时序违规在50MHz下表现为随机性死机。解决方法是把除法和取模运算改为查表或者用减法器实现综合后查看时序报告确保建立时间余量为正。这个问题的排查思路要形成习惯上板异常先跑时序报告再怀疑逻辑功能。6. 验收与答辩三个步骤证明你的面板设计真的对课设答辩的本质是证明设计正确、实现完整、测试有效所以验收流程要按功能清单核对→仿真波形留证→上板实测录屏三个步骤组织。第一步拿需求文档逐条打勾四个状态迁移是否全部验证按键暂停和取消功能是否可用倒计时显示误差是否在允许范围内。第二步把testbench里关键信号波形保存成图片重点截取状态跳变沿和按键脉冲对齐的时刻这是你答辩时的直接证据。第三步上板后用手机录一段完整操作视频从启动按键开始到保温结束全程记录中间故意按几次停止键证明停止功能在任意状态都有效。答辩时有一个加分技巧主动讲一个你排查过的bug。比如在验证消抖模块时发现当按键非常快地连续按两次时第二次按下会被当成抖动滤掉这是消抖窗口太宽导致的问题最终我把窗口从20ms收敛到10ms并增加了边沿检测。主动暴露问题并给出完整解决链路比一味说我的功能全对可信得多这也是工程汇报和课程汇报的本质区别。从那次课设之后我每次上板前都会强制走一遍先时序报告、再单点测试、最后联调的顺序不再凭感觉改代码。写板级代码这件事玄学性地改参数是最容易翻车的老老实实按流程走反而最快。希望这套方案能帮你在FPGA课设上少走几个弯路把时间花在真正能讲清楚的设计点上。本文还有配套的精品资源点击获取