资讯详情

FPGA课设实战:豆浆机面板状态机设计,从按键消抖到数码管扫描

📅 2026/10/19 12:33:46 | 华诺云谱 👁 阅读
FPGA课设实战:豆浆机面板状态机设计,从按键消抖到数码管扫描
简介面向FPGA课程设计场景这份以豆浆机面板为载体的完整工程包适合电子类本科生或自学开发者作为数字逻辑综合项目的入门参考。设计基于Verilog HDL实现覆盖六档功能按键选择、LED状态指示、两个拨码开关触发的干烧与溢出报警、温度传感器监测以及数码管剩余时间显示等模块并用LED点阵模拟电机和加热器运行状态系统框图与工作原理解析清晰。压缩包共188个文件主要包括tdf、vhd、v等硬件描述源码qsf、qpf等工程配置文件sof、pof等下载文件以及docx设计说明文档和仿真波形文件整体仅4.51MB结构归类明确。该资源已有813人学习下载具备一定参考热度。读者可从工程源码和报告文档中掌握FPGA状态机设计、按键消抖、报警信号处理和数码管动态扫描等实用技巧也可直接借助Quartus工程在开发板上复现验证对完成课程设计或备赛都很有帮助。1. 豆浆机面板课设按下启动键之后真正的战争才开始实验室里 A 同学调了一下午豆浆机面板数码管就是不亮最后发现是复位引脚悬空。这种画面在 FPGA 课程设计周里几乎每年都要上演几回。豆浆机面板听起来就是几个按键加一块数码管实际拆开看它是一台完整的状态机按键输入、模式切换、分阶段计时、倒计时显示、蜂鸣器提示这些模块叠在一起才是课设拿分的全部筹码。这份资源就是一套跑通的豆浆机面板 HDL 工程从按键消抖到状态流转再到显示刷新代码和仿真文件都齐适合正在做类似课设的学生也适合想完整走一遍时序逻辑项目的开发者。2. 拆开面板工程包顶层模块与四个子模块的分工2.1 顶层模块的接口先看清楚下载下来的工程包里最重要的文件是顶层模块所有逻辑都从这一个文件展开。我习惯先看顶层例化了哪些子模块再顺着信号往里面走。这套豆浆机面板的顶层信号不算复杂但每个引脚都有明确用途。module top_soymilk ( input wire clk_50m, // 板载 50MHz 时钟 input wire rst_n, // 复位按键低有效 input wire key_start, // 启动/停止键 input wire key_mode, // 模式选择键豆浆/米糊/果汁 output reg seg_sel, // 数码管位选 output reg seg_data, // 数码管段选 output wire buzzer, // 蜂鸣器输出 output wire led_power, // 电源指示灯 output wire led_work // 工作状态灯 );信号分为三组时钟与复位一组按键输入一组显示与输出一组。seg_sel和seg_data是动态扫描数码管的两个关键信号前者决定当前点亮哪一位后者决定这一位显示什么数字。蜂鸣器和 LED 是状态机的直接输出不同状态对应不同的输出组合。顶层只做例化和信号连接真正干活的是下面的子模块。这种写法在工程里很常见课设评审老师看到这种结构也会认为你对模块化设计有概念比把所有逻辑堆在一个 always 块里要规范得多。2.2 子模块拆到了什么粒度工程包里的源码按功能拆成了四个模块每个模块一个文件职责边界非常清晰模块名职责关键输出clk_div时钟分频1kHz 扫描时钟、1Hz 计时时钟key_debounce按键消抖与边沿检测单周期脉冲state_machine豆浆机工作状态流转状态编码、计时使能display_driver数码管动态扫描与显示数据生成位选、段选clk_div把 50MHz 分频成两路低频时钟一路给数码管扫描一路给计时器用。key_debounce解决机械按键抖动问题输出干净的按键脉冲。state_machine是核心控制整个豆浆机在空闲、加热、研磨、煮沸、完成之间切换。display_driver把剩余时间翻译成数码管能显示的数据。这样拆的好处是每个模块都能单独仿真。我调状态机的时候不需要关心显示直接给输入看状态跳转就行调显示的时候又可以用固定状态值来测互不干扰。如果你是第一次做这种规模的课设照着这个粒度拆模块能少走很多弯路。2.3 顶层例化的接线逻辑顶层模块内部把四个子模块串起来中间信号需要在顶层声明。这套代码里的接线顺序是按键经过消抖模块产生脉冲状态机接收脉冲后改变状态和计时值计时值送给显示模块显示模块驱动数码管。wire key_start_pulse; wire key_mode_pulse; wire [2:0] current_state; wire [7:0] remain_sec; clk_div u_clk_div ( .clk_50m (clk_50m), .rst_n (rst_n), .clk_1k (clk_1k), .clk_1hz (clk_1hz) ); key_debounce u_key_start ( .clk_50m (clk_50m), .rst_n (rst_n), .key_in (key_start), .key_pulse (key_start_pulse) ); key_debounce u_key_mode ( .clk_50m (clk_50m), .rst_n (rst_n), .key_in (key_mode), .key_pulse (key_mode_pulse) ); state_machine u_state_machine ( .clk_50m (clk_50m), .rst_n (rst_n), .clk_1hz (clk_1hz), .key_start (key_start_pulse), .key_mode (key_mode_pulse), .state_out (current_state), .remain_sec(remain_sec), .buzzer (buzzer), .led_work (led_work) ); display_driver u_display ( .clk_1k (clk_1k), .rst_n (rst_n), .remain_sec (remain_sec), .state (current_state), .seg_sel (seg_sel), .seg_data (seg_data) );注意按键模块例化了两次因为启动键和模式键都需要消抖处理。这种复用方式也提醒你写代码的时候尽量把通用逻辑抽出来做成可例化的模块一份消抖代码两处用比复制两遍再改信号名要容易维护得多。remain_sec是秒计数器的输出范围 0 到 99传给显示模块后拆成十位和个位。current_state是状态编码显示模块拿到它之后决定数码管是显示模式还是显示剩余时间这个设计把显示逻辑和状态逻辑解耦了。3. 状态机是豆浆机的灵魂五个状态怎么设计才合理3.1 工作流程拆解与状态编码豆浆机的工作过程不是按下启动就一路跑到结束中间每个阶段都对应一个独立状态。这套工程把流程拆成了五个状态空闲、加热、研磨、煮沸、完成。每个状态的进入条件和退出条件都写得很明确这也是评审老师最喜欢追问的部分。五个状态的编码按顺序排列用三位二进制就够了。编码顺序尽量跟工作流程保持一致方便后面排错的时候快速定位问题状态。豆浆机正常流程是空闲状态按下启动进入加热加热计时结束进入研磨研磨结束进入煮沸煮沸结束进入完成完成之后可以手动停止回到空闲。模式选择键只在空闲状态下有效三种模式的区别在于每个阶段的总时长不同。做豆浆模式时加热时间长做果汁模式时没有煮沸阶段这个差异也用状态机实现——不同模式下同一状态的计时阈值不同。状态编码定义如下localparam IDLE 3b000; localparam HEATING 3b001; localparam GRIND 3b010; localparam BOILING 3b011; localparam FINISH 3b100;3.2 三段式状态机的写法和计时逻辑这套工程用的是三段式写法第一段做状态寄存器的时序更新第二段用组合逻辑算下一状态和输出条件第三段做输出寄存。相比一段式写法三段式把时序逻辑和组合逻辑分开了代码更容易读也方便加复位和使能信号课设代码写成这样比较稳。状态机的核心代码给出参考实际工程包里的代码与这个结构一致// 第一段状态寄存器 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 第二段组合逻辑算下一个状态 always (*) begin next_state state; case (state) IDLE: begin if (key_start_pulse) next_state HEATING; end HEATING: begin if (mode MODE_JUICE heat_cnt_done) next_state GRIND; else if (mode ! MODE_JUICE heat_cnt_done) next_state GRIND; end GRIND: begin if (grind_cnt_done) next_state BOILING; else if (mode MODE_JUICE) next_state FINISH; end BOILING: begin if (boil_cnt_done) next_state FINISH; end FINISH: begin if (key_start_pulse) next_state IDLE; end default: next_state IDLE; endcase end // 第三段输出寄存器 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin buzzer 1b0; led_work 1b0; end else begin case (next_state) IDLE: led_work 1b0; FINISH: begin led_work 1b0; buzzer 1b1; // 完成提示音 end default: led_work 1b1; endcase end end这里特别说明一下计时完成的信号来源每个工作状态都有一个独立的秒计数器用clk_1hz驱动达到阈值后产生一个单周期的xxx_cnt_done脉冲。这个脉冲既作为状态机的跳转条件又作为下一状态计数器复位的标志信号设计上做到了一个条件只干一件事。果汁模式比其他模式少一个煮沸阶段所以 GRIND 状态多了一个分支判断模式是果汁就直接跳到 FINISH。这种分支在课设答辩时经常被问到代码里注释写清楚参数设置就能讲明白。状态机的计时参数放在模块头部方便统一修改parameter SOY_HEAT_TIME 8d60; // 豆浆加热 60 秒 parameter SOY_GRIND_TIME 8d30; // 豆浆研磨 30 秒 parameter SOY_BOIL_TIME 8d40; // 豆浆煮沸 40 秒改参数就能调节整个流程的时长仿真的时候把 60 改成 5可以快速跑完整个流程不用等真实的一分钟。这个技巧对课设调试特别实用我在调状态机的时候都是先把所有时间参数缩小十倍验证完功能再改回真实值。3.3 状态机代码的仿真验证状态机是否正确不能靠上板之后肉眼判断得先在仿真里把状态跳转测一遍。上一节提到把时间参数缩小后仿真时间可以从数百秒缩短到数十秒这样能在合理时间内看到完整流程。验证时重点关注几点按下启动键之后状态是否从 IDLE 跳到 HEATING加热计满之后是否进入 GRIND果汁模式下是否跳过 BOILING 直接进 FINISH完成状态按下启动键是否回到 IDLE。这些跳转点覆盖了所有状态转移路径只要全通过状态机的逻辑就是可靠的。4. 外设接口的时序处理消抖、扫描与输出驱动4.1 按键消抖延时确认比计数可靠机械按键按下和松开的瞬间会产生几十毫秒的抖动FPGA 时钟是纳秒级的直接采样会把一次按键识别成很多次。消抖的本质是延迟确认信号变化之后等待一段时间如果电平稳定就认为按键动作有效。这套工程用计数器实现延时时间常设 20ms 左右。典型的消抖模块写法如下module key_debounce ( input wire clk_50m, input wire rst_n, input wire key_in, output reg key_pulse ); reg [19:0] cnt; reg key_sync1; reg key_sync2; reg key_stable; // 两级同步器处理跨时钟域 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin key_sync1 1b1; key_sync2 1b1; end else begin key_sync1 key_in; key_sync2 key_sync1; end end // 20ms 延时确认 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_stable 1b1; end else begin if (key_sync2 ! key_stable) cnt cnt 1b1; else cnt 20d0; if (cnt 20d999_999) begin key_stable key_sync2; cnt 20d0; end end end // 边沿检测稳定值变化时产生一个时钟周期脉冲 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) key_pulse 1b0; else key_pulse key_stable; end // 实际使用时取 key_pulse 上升沿作为按键事件 endmodule计数器上限999_999对应 50MHz 时钟下的 20ms 延时。按键电平不稳定时计数器持续累加只有稳定后的电平让计数器完整跑完key_stable才更新。这套工程把按键消抖做成了独立模块启动键和模式键各例化一份互不干扰。两级同步器是必须的因为按键输入来自板子上的物理引脚理论上和你内部时钟没有确定的相位关系。不加同步器直接采偶发情况下会出现亚稳态导致状态机莫名其妙跳状态。这一步在课设里容易被省略但评审问起来你能答出原因就是加分项。4.2 数码管动态扫描刷新率与分频参数豆浆机面板上的数码管用来显示剩余秒数多位共阴数码管如果每个位单独控制需要很多引脚所以采用动态扫描方案同一时间只点亮一位轮流点亮所有位利用人眼视觉暂留形成完整显示。扫描频率太低会闪烁太高又会浪费资源一般控制在 1kHz 到 2kHz 之间每位停留 1ms。clk_div模块把 50MHz 分频为 1kHz 时钟的逻辑如下module clk_div ( input wire clk_50m, input wire rst_n, output reg clk_1k, output reg clk_1hz ); reg [15:0] cnt_1k; reg [25:0] cnt_1hz; always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_1k 16d0; clk_1k 1b0; end else begin if (cnt_1k 16d24999) begin cnt_1k 16d0; clk_1k ~clk_1k; end else begin cnt_1k cnt_1k 1b1; end end end always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_1hz 26d0; clk_1hz 1b0; end else begin if (cnt_1hz 26d24_999_999) begin cnt_1hz 26d0; clk_1hz ~clk_1hz; end else begin cnt_1hz cnt_1hz 1b1; end end end endmodulecnt_1k计数到 24999 翻转一次完整周期 50000 个时钟正好是 1kHz。cnt_1hz计数到 24999999 翻转周期 1 秒。分频参数的计算口诀是周期数等于时钟频率除以目标频率再除以二因为每次翻转需要两个周期。显示模块拿到 1kHz 时钟后在四个数码管位之间轮询每 1ms 切换一次位选信号同时把对应位的段选数据送到数据线上。轮询一圈 4ms刷新率 250Hz肉眼看不出闪烁。4.3 蜂鸣器与 LED有源还是无源决定驱动方式蜂鸣器驱动有个容易踩坑的地方有源蜂鸣器内部自带振荡源给高电平就响适合做提示音无源蜂鸣器需要外部提供指定频率的方波才能发声一般用来做旋律。这套豆浆机面板用的是有源蜂鸣器状态机里只要给一个高电平就能响。完成状态的提示音不需要一直响持续一两秒就够了。实现方式是在状态机里加一个提示音计时器蜂鸣器拉高之后开始计数计满阈值自动拉低。果汁模式因为流程短提示音触发逻辑和豆浆模式一样不会出现模式间的音调差异。LED 的驱动就简单多了电源指示灯常亮工作状态灯在非空闲状态亮起。两个灯用led_power和led_work分开控制上电之后led_power由外部直接拉高led_work由状态机输出。建议把 LED 驱动也放在状态机模块里避免顶层信号满天飞。5. 课设排坑实录仿真通过不等于上板能用5.1 复位信号悬空导致全板无响应现象仿真波形一切正常烧录到板子上之后数码管不亮、按键无反应、LED 全灭。原因开发板的复位按键没有连接到 FPGA 的全局复位引脚或者工程里没有做引脚约束复位信号悬空所有时序逻辑的初值不可控状态乱套。解决查看板卡原理图确认复位按键对应的 FPGA 引脚在约束文件里把rst_n绑定到正确位置。如果板子确实没有独立复位按键可以在顶层写一个上电自动复位模块用一个小计数器在起振后产生一个几十毫秒的低电平复位脉冲。提示拿到任何一块不熟悉的板子第一件事查原理图确定时钟引脚和复位引脚的位置再开始写代码。5.2 数码管亮度不均有的位明显偏暗现象四位数码管显示内容正确但其中一位亮度比其他位低很多或者有轻微闪烁。原因动态扫描时每一位的点亮时间不均匀。可能是位选切换逻辑里有一位停留时间特别短或者扫描时钟分频参数设定不合理导致某一位的占空比偏低。解决检查位选状态机的轮询逻辑确保每一位停留的时钟周期数完全一致。合理的扫描周期是每位 1ms 到 2ms低于 0.5ms 亮度会明显不足。如果板子上的数码管是共阳的还要注意位选信号的有效电平是低电平段选数据也得对应取反。5.3 按键按一下状态跳了好几步现象按下启动键豆浆机从空闲状态直接跳到了研磨状态中间加热阶段没走完。原因按键消抖时间设置过长或过短边沿检测逻辑把一次按键抖动识别成了多个脉冲。更隐蔽的情况是消抖产生的是一次电平变化而不是单周期脉冲状态机一个时钟周期内采到了多次条件满足。解决把消抖模块的最终输出改成严格的单周期脉冲也就是在key_stable变化沿后只产生一个时钟周期的高电平。同时检查按键按下到状态跳变之间是否有其他信号干扰比如计时脉冲恰好也在同一时刻到达。调完代码后在仿真里反复按几次按键每次按键只能产生一个脉冲。5.4 蜂鸣器声音刺耳持续不停现象完成后蜂鸣器一直响按停止键也没用或者声音明显发破。原因状态机在 FINISH 状态停留时如果提示音计时器没有正确复位高电平一直保持蜂鸣器就不会停。发声刺耳可能是因为有源蜂鸣器的驱动电流过大需要进行限流处理。解决提示音逻辑不要直接由状态机状态控制单独用一个计数器进入 FINISH 状态时启动计数计满 1 秒后自动拉低蜂鸣器信号。硬件层面的限流电阻阻值可以参考板卡设计一般串联 100 到 330 欧姆电阻限流即可。5.5 仿真状态正常上板后卡在煮沸状态现象加热和研磨阶段都正常到了煮沸状态之后数码管不再变化按键也失去响应。原因煮沸状态的计时阈值没有被正确加载。可能是模式选择逻辑中有信号没有同步或者计时器复位逻辑只在某一状态有效导致煮沸计时器无法启动。也可能是秒脉冲clk_1hz在极少数情况下丢了一拍累计误差越来越大。解决把冷热两套参数分开调试。先用最短参数验证状态跳转确认煮沸状态计时器能正常累加再改回真实参数。同时检查clk_1hz的生成逻辑确保它是干净的 1Hz 方波而不是占空比异常的信号。如果开发板的晶振频率不是精确的 50MHz分频出来的 1Hz 会有轻微偏差但不会造成状态卡死卡死一定还有别的原因。6. 上板前的验证技巧一套 testbench 把状态机测透与其反复烧录看现象不如先把时间参数缩小写一套完整的仿真把状态机所有路径跑一遍。我每次拿到这类面板设计工程都会先做三步第一步检查复位所有寄存器的初值第二步给固定按键序列跑功能第三步用$display打印每拍的状态和关键计数值。仿真脚本的骨架可以这样组织initial begin rst_n 0; clk_50m 0; key_start 1; key_mode 1; #100 rst_n 1; // 选择果汁模式验证跳过煮沸的路径 key_mode 0; #20000 key_mode 1; #20000; // 启动 key_start 0; #20000 key_start 1; // 持续观察时间到果汁流程结束 #100_000_000; // 停止回到空闲 key_start 0; #20000 key_start 1; #20000; $display(all state transitions verified); $finish; end always #10 clk_50m ~clk_50m; // 在每个状态跳变时打印信息 always (posedge clk_50m) begin if (current_state ! prev_state) begin $display(time%0t state%0d, $time, current_state); prev_state current_state; end end时间参数缩小之后仿真跑完整个果汁流程只需要几十个仿真周期波形里能清楚看到 IDLE、HEATING、GRIND、FINISH 四个状态的跳变时序。打印信息配合波形查看状态跳转是否干净利落、有没有多余的毛刺一眼就能看出来。从那以后我每次拿到一块陌生的 FPGA 板子第一步永远是查复位引脚和时钟引脚第二步才是打开代码第三步就写这个最小验证脚本确认状态机能跑通之后才碰外设。这套习惯帮我少烧了很多块板子也希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑