FPGA豆浆机面板设计实战:Verilog状态机与按键消抖教程

发布时间:2026/10/18 2:29:01
FPGA豆浆机面板设计实战:Verilog状态机与按键消抖教程 简介面向FPGA课程设计任务这份压缩包提供了基于Verilog的豆浆机控制面板完整方案适合电子信息类本科生、自学FPGA的开发者参考。资源围绕豆浆机六档功能选择、按键操作、LED状态指示、数码管倒计时、温度检测与干烧/溢出蜂鸣报警等模块展开并用LED点阵模拟电机和加热器工作状态涵盖HDL源码、Quartus工程文件、测试仿真文件及配套说明文档。包内188个文件以tdf、vhd、v等硬件描述语言文件为核心附带qsf/qpf工程配置、sof/pof下载文件、vwf仿真波形和docx文档整体约4.51MB目录结构清晰。资源包还包含完整工程约束与下载配置可直接在开发板上综合、布局布线并验证设计效果。已有813人学习下载适合需要完整理解FPGA豆浆机控制器设计流程、模块划分与EDA工具操作的读者。1. 把豆浆机面板搬进 FPGA一个小课设里被反复折腾的时序逻辑深夜的实验室里几个同学围着一块开发板同时沉默。按键按下一次数码管上的模式编号却跳了两个档状态机刚跑起来倒计时数字又乱跳。用单片机做这个豆浆机面板本不难但课设偏偏要求 FPGA HDL。这份压缩包里是一份完整的 FPGA 课程设计作品——豆浆机面板设计用 Verilog 在入门级开发板上实现了模式选择、阶段倒计时、启动暂停取消、蜂鸣器提示这一整套操作逻辑。它算不上复杂工程但状态机、按键消抖、数码管动态扫描、计时分频这些时序逻辑的典型问题全都占了。适合正在做课设的学生也能给第一次写状态机的 FPGA 初学者留一份能跑通、能动手改的参考。2. 项目全貌面板资源、目录结构与数据流2.1 面板功能拆解三个按键、四个状态、五种模式豆浆机面板的“表面功夫”是按键和显示但真正决定设计成败的是背面的状态流转。这套设计把面板资源分成三块输入是按键输出分成状态呈现和声音提示。板上只用了三个独立按键分别是模式切换 MODE、启动暂停 START、取消 CANCEL没有做矩阵键盘。课设场景下用户真正需要的操作就这么多按键越多状态机要处理的异常分支越多复杂度会成倍上升。按键之外的输出资源需要提前交代清楚因为后面的模块全在为这三样东西服务外设资源数量/形式在这个面板里的职责按键3 个独立按键模式选择、启动暂停切换、取消退出数码管4 位七段数码管前两位显示当前模式编号后两位显示剩余秒数LED3 个独立 LED指示浸泡、研磨、煮浆保温三个阶段的进度蜂鸣器1 个有源蜂鸣器按键反馈短鸣、流程完成长鸣豆浆程序内置三种模式干豆、湿豆、米糊每种模式对应一条独立的时间线。例如干豆模式是浸泡 15 秒、研磨 20 秒、煮浆 25 秒、保温 15 秒湿豆模式则把浸泡阶段去掉直接进入研磨。真实的豆浆机时间以分钟为单位但课设需要在仿真和现场演示时快速看到完整流程所以这套设计把时间压缩到了秒级参数全部集中在一个模块里想改回分钟级只需改常量。状态流转是这套面板设计的核心。整个系统只有四个状态空闲 IDLE、运行 RUN、暂停 PAUSE、完成 DONE。空闲时按 MODE 在三种程序之间循环切换按 START 从空闲进入运行运行中按 START 在运行与暂停之间切换按 CANCEL 直接回空闲所有阶段跑完后进入完成状态蜂鸣器响一段时间再按任意键回空闲。我整理文档时手绘过这个状态转移图比写一大段文字管用得多动手改代码前建议你也先画一张。2.2 目录结构与模块划分每个文件干什么一眼看清压缩包里的 RTL 代码按模块拆开每个文件对应一个可独立仿真的单元顶层只有例化没有逻辑。目录结构大致如下fpga_soymilk_panel/ ├── rtl/ │ ├── top_panel.v // 顶层模块例化所有子模块 │ ├── key_debounce.v // 按键消抖与边沿检测 │ ├── panel_fsm.v // 主状态机与阶段流转 │ ├── stage_timer.v // 阶段计时器输出剩余秒数 │ ├── seg_display.v // 数码管扫描显示驱动 │ ├── buzzer_ctrl.v // 蜂鸣器发声控制 │ └── clk_div.v // 分频产生 1kHz 与 1Hz 脉冲使能 ├── sim/ │ └── tb_top_panel.v // 顶层仿真测试文件 └── constraint/ └── panel_pins.xdc // 引脚约束文件这个设计的核心思路是“单时钟、脉冲使能”。系统只有一个主时钟分频模块不产生新时钟而是产生两个脉冲使能信号1kHz 给数码管动态扫描1Hz 给秒计时。这样设计的原因我在第 5 章会专门展开简单说就是多时钟域是 FPGA 课设里最常见的翻车点用脉冲使能可以把风险压到最低。数据流走向也值得先讲清楚按键信号进key_debounce消抖并提取边沿然后送入panel_fsm状态机状态机根据当前状态和按键事件决定下一拍的状态同时把状态输出到stage_timer、seg_display、buzzer_ctrl。计时器倒计时结束会产生一个完成信号回传给状态机驱动状态跳转。整个环路里没有任何组合逻辑直接驱动外部输出所有输出都打了一拍寄存器这是这套代码能稳定烧进板子的关键。3. 核心模块拆解从按键按下到数码管跳数字的完整链路3.1 按键消抖两级同步与边沿检测机械按键按下的瞬间电平会在 5ms 到 20ms 内反复抖动。如果直接采样按键电平状态机会被一次按压触发多次。常见处理是延时消抖我的做法是连续采样稳定若干毫秒再确认有效并输出一个单周期的按键事件脉冲。完整的消抖模块如下module key_debounce #( parameter CLK_FREQ 50_000_000, parameter DEBOUNCE_MS 20 ) ( input wire clk, input wire rst_n, input wire key_in, // 按键原始电平按下为低 output wire key_press // 单周期脉冲按下有效时拉高一个时钟 ); reg [1:0] key_sync; reg [23:0] cnt; reg stable_level; reg stable_level_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_sync 2b11; // 复位后默认高电平 stable_level 1b1; stable_level_d 1b1; end else begin key_sync {key_sync[0], key_in}; // 两级同步打拍 if (key_sync[1] stable_level) begin // 电平未变化 cnt 0; end else begin cnt cnt 1b1; if (cnt (CLK_FREQ / 1000 * DEBOUNCE_MS - 1)) begin stable_level key_sync[1]; cnt 0; end end stable_level_d stable_level; end end assign key_press stable_level_d ~stable_level; // 检测稳定电平的下降沿 endmodule代码里有三个值得注意的参数点。第一DEBOUNCE_MS默认 20ms。如果板子的按键抖动时间更长在例化时覆盖这个参数即可不用改动消抖逻辑本身。第二CLK_FREQ必须与系统实际时钟频率一致否则消抖时间会与预期不符。第三最后输出的是下降沿脉冲对应“按下”这个动作松开时不会产生第二个事件这样能避免一次按压导致两次模式切换的问题。3.2 主状态机三段式写法的模块化实现状态机采用标准三段式第一段做状态寄存器次态更新第二段做状态转移条件判断第三段做输出逻辑。这样把“下一拍跳到哪”和“当前拍输出什么”拆开仿真时看波形能快速定位是时序问题还是组合逻辑问题。核心代码段如下module panel_fsm #( parameter MODE_NUM 3 // 支持的程序模式数量 ) ( input wire clk, input wire rst_n, input wire key_mode, input wire key_start, input wire key_cancel, input wire timer_done, // 当前阶段计时结束 output reg [1:0] state, output reg [1:0] mode, // 当前选择的模式编号 output reg [1:0] stage, // 当前所处的阶段编号 output reg work_en, // 计时器使能 output reg beep_en // 蜂鸣器使能 ); localparam IDLE 2d0, RUN 2d1, PAUSE 2d2, DONE 2d3; always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else case (state) IDLE: begin if (key_start) state RUN; else state IDLE; end RUN: begin if (key_cancel) state IDLE; else if (key_start) state PAUSE; else if (timer_done) state DONE; else state RUN; end PAUSE: begin if (key_cancel) state IDLE; else if (key_start) state RUN; else state PAUSE; end DONE: begin if (key_start || key_cancel) state IDLE; else state DONE; end default: state IDLE; endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) begin mode 2d1; stage 2d0; end else if (state IDLE) begin if (key_mode) mode mode 1b1; // 空闲时切换模式 stage 2d0; end else if (state RUN timer_done) begin stage stage 1b1; // 阶段推进 end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin work_en 1b0; beep_en 1b0; end else begin work_en (state RUN); beep_en (state DONE); end end endmodule容易被新手忽略的是模式切换只能在 IDLE 状态下进行启动之后key_mode被完全忽略。这样设计的目的是防止运行过程中误触模式键导致参数整体错乱。阶段推进条件timer_done在每个阶段结束时拉高一个周期状态机在这一拍既跳状态也推进 stage两件事同步发生避免跨时钟握手问题。第三段输出逻辑里work_en只在 RUN 状态为高PAUSE 和 DONE 时都被拉低。这样计时器模块不需要关心状态机的细节只要看到work_en有效就开始倒计时无效就保持当前计数值。所有控制信号都由状态寄存器同步产生不存在组合逻辑毛刺这也是“输出打拍”的具体应用。3.3 阶段计时器1Hz 使能脉冲与倒计时计时模块根据当前模式和阶段决定本阶段总秒数并在每个 1Hz 脉冲下减一。时间参数全部放在常量表里整个项目修改运行时长只需要动这一处module stage_timer #( parameter MODE_NUM 3, parameter STAGE_NUM 4 ) ( input wire clk, input wire rst_n, input wire [1:0] mode, input wire [1:0] stage, input wire work_en, input wire pulse_1hz, // 1Hz 脉冲使能 output reg timer_done, // 本阶段结束 output reg [7:0] remain_sec // 剩余秒数 ); reg [7:0] stage_sec; always (*) begin case (mode) 2d1: case (stage) // 干豆 2d0: stage_sec 8d15; 2d1: stage_sec 8d20; 2d2: stage_sec 8d25; default: stage_sec 8d15; endcase 2d2: case (stage) // 湿豆没有浸泡 2d0: stage_sec 8d0; 2d1: stage_sec 8d20; default: stage_sec 8d25; endcase default: stage_sec 8d12; // 米糊 endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) begin remain_sec 0; timer_done 0; end else if (!work_en) begin remain_sec stage_sec; timer_done (stage_sec 0) ? 1b1 : 1b0; end else if (pulse_1hz) begin if (remain_sec 0) begin timer_done 1b1; end else begin remain_sec remain_sec - 1b1; timer_done (remain_sec 1) ? 1b1 : 1b0; end end end endmodule这个模块的写法承上启下。work_en拉低时模块把本阶段总秒数装载到remain_sec同时如果总秒数为 0湿豆模式没有浸泡阶段会立刻产生timer_done让状态机在下一拍就把 stage 推进到下一阶段相当于跳过空阶段。remain_sec 1时会在下一个 1Hz 脉冲上拉高timer_done完成信号的到来和倒计时归零是同一拍状态机跳转时看到的剩余值始终一致不会出现“显示 0 却迟迟不跳阶段”的错位。3.4 数码管扫描与蜂鸣器控制四位共阴数码管要稳定显示必须动态扫描同一时刻只点亮一位以人眼无法分辨的频率轮流点亮四位。1kHz 脉冲使能在这里派上用场每来一个脉冲切换一位扫描周期 4ms肉眼上看四位同时常亮。关键代码如下module seg_display #( parameter SCAN_FREQ 1000 ) ( input wire clk, input wire rst_n, input wire [7:0] remain_sec, // 剩余秒数 input wire [1:0] mode, // 当前模式编号 output reg [3:0] seg_sel, // 位选 output reg [7:0] seg_data // 段码 ); reg [1:0] scan_cnt; reg [3:0] number; always (*) begin case (scan_cnt) 2d0: begin seg_sel 4b0001; number mode / 10; end 2d1: begin seg_sel 4b0010; number mode % 10; end 2d2: begin seg_sel 4b0100; number remain_sec / 10; end default: begin seg_sel 4b1000; number remain_sec % 10; end endcase end always (posedge clk or negedge rst_n) begin if (!rst_n) scan_cnt 0; else scan_cnt scan_cnt 1b1; // 扫描位切换 end // 段码译码根据 number 查表输出七段码 // 开发板共阴/共阳极不同段码表按硬件实际接线填写 always (*) begin case (number) 4d0: seg_data 8h3F; 4d1: seg_data 8h06; // 其余数字查表略 default: seg_data 8h00; endcase end endmodule这里我把扫描计数和段码译码分成两块。scan_cnt递增可以用外部 1kHz 脉冲门控也可以直接用主时钟计数分频两种写法在这个模块里都成立差别在于前者天然避免分频时钟带来的时序收敛问题。段码表必须按自己开发板的共阴共阳接法修改这一节属于“拿到板子应该先确认的硬件常识”引脚接法错了显示就会乱码。蜂鸣器控制更简单功能上只区分两种声音按键反馈短鸣约 100ms完成长鸣约 3 秒。模块内部用一个计数器区分两种时长在 DONE 状态拉高使能计时结束自动关断。需要特别注意的是有源蜂鸣器给高电平就会响无源蜂鸣器需要输出特定频率的方波买板子时一定要确认类型否则完成不响会让人怀疑人生。4. 搭建工程与运行验证从新建项目到波形仿真的完整流程4.1 工程创建与引脚约束三个关键步骤拿到源码第一件事不是读代码而是先把工程建起来跑一遍仿真。主流 EDA 工具都采用类似的组织方式新建工程后把 rtl 目录下的 .v 文件全部添加进去然后写约束文件把顶层信号映射到开发板物理引脚。最容易出错的地方不在语法而在引脚号的来源——必须对照开发板原理图看数码管的位选和段选分别接在哪个 FPGA 引脚上不能照搬别人工程里的约束。引脚约束文件的关键内容长这样具体引脚号要换成自己板子实际编号set_property PACKAGE_PIN E3 [get_ports clk] set_property PACKAGE_PIN F6 [get_ports key_mode] set_property PACKAGE_PIN F7 [get_ports key_start] set_property PACKAGE_PIN G6 [get_ports key_cancel] set_property PACKAGE_PIN A10 [get_ports {seg_sel[0]}] set_property PACKAGE_PIN A9 [get_ports {seg_sel[1]}] create_clock -period 20.000 -name sys_clk [get_ports clk]Vivado 系的工程用 XDC 格式上面这段 tcl 直接可用如果是 Quartus 系换成 QSF 里的set_location_assignment逻辑是等价的。create_clock这条约束虽然对课设小工程不是必须的但加了对后续时序收敛和调试都有帮助是养成好习惯的起点。约束完成后跑综合如果报引脚复用或未定义错误九成是引脚编号写错回来再对一遍原理图。4.2 仿真测试三个关键测试用例仿真测试文件建议直接调用顶层模块把所有按键事件合成一段激励序列用$display或波形查看器观察状态机变化。测试至少要覆盖三件事正常走完一个流程、运行中取消、运行中暂停再恢复。下面是一段典型的“选择湿豆模式→启动→完成”激励initial begin rst_n 0; key_mode 1; key_start 1; key_cancel 1; repeat(4) #20 rst_n 1; // 按一次 MODE切到模式 2湿豆 key_mode 0; #20; key_mode 1; repeat(2000000) (posedge clk); // 等待消抖确认 // 按 START 进入运行 key_start 0; #20; key_start 1; // 观察 timer_done 和 stage 的变化 end注意这里按键极性按“按下为低有效”处理与市面上大多数开发板一致。仿真时 20ms 消抖在 50MHz 时钟下是 100 万个周期为了加快仿真可以把DEBOUNCE_MS在 testbench 里覆盖为 1ms或者用defparam修改。这招能让仿真速度明显提升也是我调试时最常用的手段。看波形的重点位置有三个启动那一拍state从 IDLE 变 RUNwork_en同步拉高阶段结束时timer_done和stage是否在同一拍更新取消时state回到 IDLEremain_sec是否重新装载。这些点确认无误后再上板基本不会遇到行为层面的意外。4.3 上板调试流程与现象判断仿真通过后上板依然可能遇到现象和设计对不上的情况这部分最耗时。我的调试顺序是先确认时钟再验证按键再看显示最后听蜂鸣器。具体做法可以在顶层加一个计数器让 LED 周期闪烁如果 LED 都不闪问题多半在时钟或复位如果 LED 正常而按键没反应就把按键信号放到在线逻辑分析仪里抓波形确认按键事件脉冲有没有产生。另一个常见现象是上板后数码管亮度不均有的位明显暗。这要么是扫描频率不对要么是位选驱动能力不足。前者改分频参数后者需要位选输出加三极管驱动。硬件问题拿软件调不回来只能认栽。这类现象会单独在第 5 章列出来方便对照自查。5. 常见问题与排查五个坑的记录与检讨5.1 按键一次按下触发多次切换现象按一下 MODE模式编号从 1 直接跳到 3相当于一次按压产生了两次事件。原因消抖逻辑用了电平稳定判断但没有把边沿检测放在正确位置或者消抖时间设置太短抖动尚未结束就认为按下有效。解决把消抖确认时间提高到 20ms 以上并且最后输出必须是stable_level_d ~stable_level的单周期脉冲而不是直接输出稳定电平。我把这个写法固化成模板之后抄这套代码的人很少再在这个问题上翻车。5.2 数码管整片闪烁且亮度不均现象四位数字都亮但整体在快速闪动低位尤其昏暗。原因扫描频率低于 50Hz 人眼就会察觉闪烁分频计数器写错导致扫描周期变成 40ms 就会出现这种效果。位选引脚没有加驱动管每位点亮时间太短也会亮度不均。解决先量扫描脉冲频率确认在 1kHz 附近再检查位选硬件电路共阴数码管的位选高电平需要足够的驱动能力。5.3 状态机进入未知状态显示乱码现象数码管显示毫无规律像随机数字按键全部失效。原因状态编码匹配但 case 分支不完整或者状态寄存器被亚稳态污染。更隐蔽的是有人把状态机写成两段式输出逻辑里放了复杂组合判断让毛刺被外部电路当成有效信号。解决用三段式写法case 里必须写default: state IDLE;。所有异步输入进状态机前先打两拍同步防止亚稳态进入时序逻辑。5.4 蜂鸣器长鸣不停止现象完成状态时蜂鸣器开始响但几秒后还在响按键回空闲也停不下来。原因蜂鸣器使能信号来自状态机的组合逻辑判断状态已经跳走组合输出却没有同步变化或者完成长鸣的计数器复位条件不完善。解决让蜂鸣器使能由寄存器输出状态转移的同时清除计数。更稳妥的做法是在蜂鸣器模块内部判断state DONE并且复位时强制关闭使能输出。5.5 仿真一跑就通上板就失灵现象testbench 里波形完美烧进板子现象全乱像是两个程序。原因仿真时钟是理想时钟真实时钟存在抖动更常见的是分频写法有问题——用分频后的时钟去驱动另一组 always等于人为制造了新的时钟域跨时钟域的握手在课设里几乎没人做对。解决全工程只保留一个主时钟所有“分频”都做成脉冲使能而不是直接生成子时钟。这是 2.2 节强调过的设计思路也是这套工程能稳定上板的重要原因。从那以后我做 FPGA 相关的板卡项目任何模块的时钟都坚持用主时钟加门控使能这套习惯救了我很多次。6. 二次开发改出你自己的豆浆机面板拿到这套工程最值得动手改的是三个方向调整时间参数、增加预约功能、更换显示载体。时间参数最简单stage_timer里已经把三种模式的阶段秒数做成常量表把秒级改成分钟级只需改stage_sec的赋值。改的时候注意remain_sec的位宽当前是 8 位最大能表示 255 秒如果某阶段要跑 90 分钟就必须把位宽扩到 16 位否则综合时报位宽截断上板后倒计时会突然归零。预约功能是课设答辩时容易加分的改造。思路是在状态机里增加一个 PRE 状态进入预约状态后先用 MODE 设定启动倒计时按 START 后从预约倒计时走完自动进入 RUN。这个改动涉及状态机加一个状态、计时器增加一个预约计时通道、数码管复用显示逻辑大概三四十行代码。建议动手前先在纸上画一版新的状态转移表把进入 PRE 和离开 PRE 的事件标清楚再逐段替换现有代码。另一个实用改造是把数码管换成 LCD 或者串口打印调试信息。现在的 FPGA 工具链普遍支持在线逻辑分析仪直接在运行时观察状态机跳转比盯着数码管猜现象效率高得多。替换时保持顶层接口不变只换seg_display的内部实现顶层代码一行不用动。这也反映出当初按模块划分的价值。验证方法上坚持“先仿真、后上板、再从现象反推”的顺序。每次改动后至少做一次定向仿真把涉及的状态转移和计时边界全覆盖确认没有引入新问题再烧板子。上板时一次只验证一个改动点别同时改多处否则现象不对时根本不知道应该改哪里。有个习惯是从这套豆浆机面板做完之后养成的每次拿到一个带控制逻辑的 FPGA 工程都强制自己先列一张状态转移表把每个输入事件在每个状态下的跳转目标写清楚再动手写 RTL。画表这十分钟看着像浪费时间实际上能把后面调试的时间省掉一半。如果你打算在这个工程上做二次开发也建议从画状态转移表开始希望帮到你。本文还有配套的精品资源点击获取