☰
FPGA课设实战:豆浆机面板状态机设计,从按键消抖到数码管扫描
2026/10/19 12:33:45 网站建设 项目流程

简介:面向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 = 3'b000; localparam HEATING = 3'b001; localparam GRIND = 3'b010; localparam BOILING = 3'b011; localparam FINISH = 3'b100;

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 <= 1'b0; led_work <= 1'b0; end else begin case (next_state) IDLE: led_work <= 1'b0; FINISH: begin led_work <= 1'b0; buzzer <= 1'b1; // 完成提示音 end default: led_work <= 1'b1; endcase end end

这里特别说明一下计时完成的信号来源:每个工作状态都有一个独立的秒计数器,用clk_1hz驱动,达到阈值后产生一个单周期的xxx_cnt_done脉冲。这个脉冲既作为状态机的跳转条件,又作为下一状态计数器复位的标志,信号设计上做到了一个条件只干一件事。

果汁模式比其他模式少一个煮沸阶段,所以 GRIND 状态多了一个分支:判断模式是果汁就直接跳到 FINISH。这种分支在课设答辩时经常被问到,代码里注释写清楚,参数设置就能讲明白。

状态机的计时参数放在模块头部,方便统一修改:

parameter SOY_HEAT_TIME = 8'd60; // 豆浆加热 60 秒 parameter SOY_GRIND_TIME = 8'd30; // 豆浆研磨 30 秒 parameter SOY_BOIL_TIME = 8'd40; // 豆浆煮沸 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 <= 1'b1; key_sync2 <= 1'b1; 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 <= 20'd0; key_stable <= 1'b1; end else begin if (key_sync2 != key_stable) cnt <= cnt + 1'b1; else cnt <= 20'd0; if (cnt == 20'd999_999) begin key_stable <= key_sync2; cnt <= 20'd0; end end end // 边沿检测:稳定值变化时产生一个时钟周期脉冲 always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) key_pulse <= 1'b0; 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 <= 16'd0; clk_1k <= 1'b0; end else begin if (cnt_1k == 16'd24999) begin cnt_1k <= 16'd0; clk_1k <= ~clk_1k; end else begin cnt_1k <= cnt_1k + 1'b1; end end end always @(posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt_1hz <= 26'd0; clk_1hz <= 1'b0; end else begin if (cnt_1hz == 26'd24_999_999) begin cnt_1hz <= 26'd0; clk_1hz <= ~clk_1hz; end else begin cnt_1hz <= cnt_1hz + 1'b1; end end end endmodule

cnt_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 板子,第一步永远是查复位引脚和时钟引脚,第二步才是打开代码,第三步就写这个最小验证脚本,确认状态机能跑通之后才碰外设。这套习惯帮我少烧了很多块板子,也希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询