简介面向FPGA课程设计需求的一份豆浆机面板设计资料包使用Verilog描述豆浆机控制器覆盖五谷、湿豆、干豆、果汁、米糊、玉米汁六档按键模式选择以及LED状态显示、启动电热器与电机、温度监测、4位数码管计时、溢出和干烧蜂鸣报警等功能。压缩包共188个文件约4.51MB以tdf、vhd、v等硬件描述源码为核心辅以qsf、qpf工程文件、sof配置文件、docx文档和仿真波形文件可还原完整Quartus设计流程。内含详细的HDL代码、工程配置、部分备份版本以及双拨码开关模拟干烧/溢出报警、LED点阵模拟电机与加热器状态等实现细节同时配有系统框图说明便于对照学习模块划分、按键消抖、状态机设计及报警逻辑。已有813人学习下载适合正在完成同类课设或希望掌握FPGA控制器设计思路的学生参考。1. 豆浆机面板课程设计为什么用FPGA做这个看似“简单”的面板FPGA课设拿到“豆浆机面板设计”这个题时很多人第一反应是这不就是一个按键加显示用单片机写几十行代码就能串通但题目里真正考察的点是“用HDL在FPGA上完成面板的整体控制”从按键消抖到状态机再到加热、电机、报警输出全部要在同一块逻辑器件里并行调度。这个方案的完整价值在于它不是只给出一个能亮的“显示壳子”而是把豆浆机的运行流程做成一个可仿真、可综合、可上板的状态机闭环。适合正在做FPGA课程设计的同学也适合需要快速交付一个小型控制面板方案的人。你照着这份结构调整代码和报告比从零开始摸索省掉大半试错时间。2. 总体架构设计五个模块与三段式状态机的职责划分2.1 模块划分与数据流五个模块各管一件事先把整个面板拆开再决定怎么写代码。我一般会按外设边界把系统分成五个模块每个模块只做一件事调试的时候哪个环节挂掉可以单独验证。这五个模块分别是clk_div时钟分频模块、key_filter按键消抖模块、mocha_fsm核心状态机、display_driver显示驱动模块、output_ctrl输出控制模块。时钟分频模块把开发板上的50MHz系统时钟分成1kHz扫描时基、1Hz状态灯时基供其他模块使用。按键消抖模块对启动、停止、模式切换三个按键做消抖输出稳定的电平信号和单周期触发脉冲。核心状态机模块接收按键和温度比较器输入按豆浆机流程切换状态并输出加热、电机、蜂鸣器的控制寄存器值。显示驱动模块读取状态机和模式寄存器的值动态扫描两位数码管显示出当前正在执行的工序。输出控制模块把状态机的输出映射到真实引脚处理极性反转和驱动能力问题防止直接驱动继电器导致拉不高电平。整体数据流是单向的按键输入经过消抖后触发生状态机状态机再向显示模块和输出模块送出控制值。由于FPGA并行执行按键扫描、状态转移、数码管刷新互不阻塞这正是用HDL做控制面板优于顺序执行单片机的核心优势。实际做课程设计时不建议再引入总线协议把各模块用顶层一行行例化信号直连结构清晰且容易向评审老师讲解。2.2 核心状态机定义五个状态与控制信号真值表豆浆机的流程不是随便写的每个状态都要对应实际工作步骤。在这个方案中我把主状态机设计为五个状态待机、加热浸泡、搅打、煮沸、完成报警。待机状态下按下启动键进入加热浸泡温度达到阈值后进入搅打搅打计时结束回到煮沸煮沸结束进入完成报警此时只有按下停止键才能回到待机。状态中文含义加热输出电机输出蜂鸣器IDLE待机000PREHEAT加热浸泡100BLEND搅打010BOIL煮沸100DONE完成报警001这张表非常实用仿真波形一出来可以直接和真值对照答辩时老师问“为什么煮沸和加热都用同一个输出”我们就能回答因为这两个工序本来就需要加热丝导通区别只在持续时间表里明确列出来就避免了混淆。状态转移采用三段式状态机原因是课程设计最常见的问题就是一段式写法把组合逻辑和时序逻辑混在一起输出出现毛刺三段式把次态逻辑、状态寄存器和输出寄存器分开上板行为和仿真波形基本一致是风险最低的写法。2.3 时钟分频与扫描时基用时钟使能替代分频时钟时钟分频不能随意写参数要根据具体开发板时钟频率计算。假设系统时钟为50MHz需要产生一个1kHz按键采样时钟计数器就要计数到49999需要产生一个10kHz显示扫描时钟计数器就要计数到4999。很多同学在这里直接写多个always生成不同频率的“新时钟”然后把这些分频时钟接到模块的posedge上。这种做法会在模块之间引入跨时钟域问题按键消抖模块偶尔会采集到亚稳态数据上板表现就是时好时坏。更稳的做法是保留全局时钟在一个模块里产生时钟使能脉冲。比如每50000个周期拉高一个clk_1k_en其他模块仍然使用posedge clk_50m内部再用if(clk_1k_en)判断是否执行逻辑。这样所有寄存器都由同一个时钟驱动避免跨时钟域抖动。按键消抖时间我习惯取10ms因为机械按键的抖动通常不超过5ms10ms窗口既能覆盖抖动又不会让按键响应迟钝到让用户觉得卡数码管动态扫描刷新率则取1kHz到4kHz每位停留0.25ms到1ms视觉上稳定不闪烁。3. 硬件描述语言实现按键消抖、状态机与显示驱动代码3.1 按键消抖模块10ms稳定窗口加边沿提取module key_debounce ( input wire clk_50m, input wire rst_n, input wire key_in, // 按键引脚按下为低 output wire key_pressed // 消抖后的下降沿脉冲 ); reg [19:0] cnt; // 50MHz 下计数到 499999 为 10ms reg stable_low; // 持续低电平后的稳定标志 reg stable_low_d; // 打一拍用于边沿检测 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin cnt 20d0; stable_low 1b1; end else if (key_in 1b0) begin if (cnt 20d499_999) begin cnt 20d0; stable_low 1b0; // 连续低电平超过 10ms判定为一次有效按下 end else begin cnt cnt 1b1; end end else begin cnt 20d0; stable_low 1b1; end end always (posedge clk_50m or negedge rst_n) begin if (!rst_n) stable_low_d 1b1; else stable_low_d stable_low; end assign key_pressed ~stable_low stable_low_d; endmodule这段代码的要点有两个一是计数窗口直接由系统时钟计数得到不需要额外分频二是用stable_low_d打一拍后与stable_low做与非产生一个单时钟周期的下降沿脉冲。key_pressed适合作为状态机的触发信号而stable_low适合作为停止键的电平判断信号。如果把key_pressed和stable_low混用很容易出现状态机一次跳多个状态的问题这一点在第4章会专门展开。3.2 核心状态机三段式写法次态逻辑、状态寄存与计数控制module mocha_fsm ( input wire clk_50m, input wire rst_n, input wire start_pulse, // 启动键消抖后的脉冲 input wire stop_level, // 停止键消抖后的电平按下为低 input wire [1:0] mode, // 功能选择00干豆 01湿豆 10米糊 input wire temp_high, // 温度比较器输出超过阈值拉高 output reg [2:0] state, output reg heat_en, output reg motor_en, output reg alarm_en ); localparam IDLE 3d0; localparam PREHEAT 3d1; localparam BLEND 3d2; localparam BOIL 3d3; localparam DONE 3d4; reg [2:0] next_state; reg [31:0] cnt; reg [31:0] cnt_max; // 次态组合逻辑 always (*) begin next_state state; case (state) IDLE: if (start_pulse) next_state PREHEAT; PREHEAT: if (stop_level 1b0) next_state IDLE; else if (temp_high) next_state BLEND; BLEND: if (stop_level 1b0) next_state IDLE; else if (cnt cnt_max) next_state BOIL; BOIL: if (stop_level 1b0) next_state IDLE; else if (cnt cnt_max) next_state DONE; DONE: if (stop_level 1b0) next_state IDLE; default: next_state IDLE; endcase end // 状态寄存器与时间计数器 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin state IDLE; cnt 32d0; end else begin state next_state; if (next_state ! BLEND next_state ! BOIL) cnt 32d0; else if (cnt cnt_max) cnt 32d0; else cnt cnt 1b1; end end // 不同模式对应不同搅拌/煮沸时间 always (*) begin case (mode) 2b00: cnt_max 32d10_000_000; // 干豆约 10 秒 2b01: cnt_max 32d20_000_000; // 湿豆约 20 秒 default: cnt_max 32d15_000_000; // 米糊约 15 秒 endcase end // 输出寄存器 always (*) begin heat_en 1b0; motor_en 1b0; alarm_en 1b0; case (state) PREHEAT, BOIL: heat_en 1b1; BLEND: motor_en 1b1; DONE: alarm_en 1b1; endcase end endmodule需要注意最后一段输出赋值使用的是组合逻辑它把状态值直接转化为输出电平。如果对输出毛刺敏感可以再把这些输出在时序块里打一拍形成寄存器输出。计数器部分之所以用next_state判断是否清零是因为状态转移发生在时钟沿前一刻用当前state判断会造成计数溢出后多计一拍。课程设计里如果能讲清这一处答辩时会让老师认为是真理解不是照抄。3.3 显示驱动模块动态扫描与段码译码module display_driver ( input wire clk_1k, input wire rst_n, input wire [2:0] state, input wire [1:0] mode, output reg [6:0] seg, output reg [1:0] sel ); reg [3:0] seg_num; // 1kHz 扫描时钟下只切换两位数码管每位刷新率 500Hz always (posedge clk_1k or negedge rst_n) begin if (!rst_n) sel 2b01; else sel ~sel; end always (*) begin case (sel) 2b01: begin case (mode) 2b00: seg_num 4d1; // 干豆 2b01: seg_num 4d2; // 湿豆 default: seg_num 4d3; // 米糊 endcase end 2b10: begin case (state) 3d0: seg_num 4d0; 3d1: seg_num 4d1; 3d2: seg_num 4d2; 3d3: seg_num 4d3; default: seg_num 4d4; endcase end default: seg_num 4d0; endcase end always (*) begin case (seg_num) 4d0: seg 7b0111111; 4d1: seg 7b0000110; 4d2: seg 7b1011011; 4d3: seg 7b1001111; 4d4: seg 7b1100110; default: seg 7b0111111; endcase end endmodule段码表对应的是共阴极数码管段选低有效或高有效要根据开发板型号调整。动态扫描的核心是“每个瞬间只点亮一位靠视觉暂留让两位看起来同时显示”。如果刷新率低于100Hz人眼会明显看到闪烁如果刷新率太高驱动管功耗会增加。1kHz扫描、每位500Hz是比较均衡的选择。课程设计里显示这一块翻车概率最高不是段码顺序错就是位选极性反建议先把一位点亮确认段码表规则后再接两位。4. 常见问题与排查最容易让课设翻车的五个点4.1 按键按下没反应或一按跳多格现象按一次启动键状态机直接从待机跳到煮沸中间两个状态完全看不到或者按键偶尔完全没反应。原因通常是两个一是没有消抖按键机械抖动产生的多个脉冲被状态机当成多次输入二是把消抖后的稳定电平直接当作脉冲信号使用状态机在同一个持续低电平期间反复触发。解决方式是把按键模块输出的stable_low和key_pressed严格分开状态跳转只用key_pressed停止判断才用stable_low。同时在testbench里模拟一段带抖动的按键波形确认消抖窗口能过滤掉前5ms的杂波。4.2 状态机停在某个状态加热或电机信号拉不高现象仿真中状态转移完全正常但板子一旦进入加热状态就再也退不出来必须断电才能恢复。原因多出现在一段式状态机中阻塞赋值同时更新了状态变量和输出变量导致下一拍组合逻辑读到的是新状态转移条件被覆盖。解决方式是严格采用三段式次态逻辑用组合逻辑、状态寄存器用非阻塞赋值、输出逻辑单独处理。另一个高发原因是没有在每个状态的转移分支里加入停止键判断停止信号自然失效。我在检查时习惯把状态机和输出模块分开编译看波形时先看state变化再看heat_en是否跟随能快速定位是哪一段出了问题。4.3 数码管乱码、显示闪烁或亮度不均现象第一位显示的数字是错误的或者某个数码管明显比另一个暗再或者数字整体在跳变。原因通常是段码表和实际连线顺序不一致动态扫描的位选信号没有连接驱动管直接接到共阴极端口导致电流不够刷新率太低造成闪烁也是常见问题。解决方式分三步走先只点亮一位逐个段码点亮确认a到g的对应关系接着单独测试位选信号极性确认是低有效还是高有效最后把扫描时钟调到1kHz以上。如果开发板的数码管是多位的建议对照原理图而不是只看板子丝印这一步能省掉大量无效实验。4.4 仿真波形正确但板子上电后完全没有反应现象testbench里所有信号都符合预期下载到板子后LED不亮、数码管不显示、按键按下无响应。这类问题的根源通常不在逻辑而在工程配置。第一复位极性反了板上复位按键是高电平有效而代码里写的是rst_n低电平复位第二引脚约束文件缺失或者引脚号填错逻辑根本没有连接到物理管脚第三时钟输入没有使用专用的全局时钟引脚导致内部时钟无法正常进入。排查顺序建议是先烧一个最简单的LED翻转工程确认开发板和下载链路没有问题再逐步添加按键和显示模块。遇到上板问题不要先怀疑状态机先怀疑时钟和复位。4.5 继电器或电机驱动信号电平不够导致输出抖动现象out_heat信号在逻辑分析仪上为1但外接的三极管开关不动作继电器反复吸合弹开声音听起来像在“哆嗦”。原因是FPGA引脚输出电流极其有限一般只有几毫安直接驱动继电器线圈远远不够另外驱动电路可能是低电平导通而状态机输出的是高电平有效。解决方式是在output_ctrl模块里做最终的极性适配如果驱动板要求低电平有效就在顶层取反驱动管基极必须串一个限流电阻继电器线圈两端要反向并联二极管吸收关断时的反向电动势。这一条往往要到联调阶段才能暴露建议在课程设计一开始就把输出极性规划清楚。5. 验收与答辩跑通演示脚本与应对评审提问5.1 验收演示脚本答辩现场的时间有限不能拿着开发板随意按键要提前准备好固定步骤。我的习惯是先演示待机状态数码管显示“01”代表干豆模式、待机状态然后按模式切换键数码管变为“02”说明功能选择生效按下启动键后加热指示灯亮数码管显示“11”用一根跳线把温度比较信号接到高电平状态机进入搅打数码管显示“12”同时电机指示灯亮再模拟搅打结束状态机进入煮沸显示“13”最后状态机自动跳到完成报警蜂鸣器发声显示“14”此时按下停止键回到待机“01”。每一步都对照面板上的状态灯和数据码评委一眼就能看到逻辑闭环。5.2 答辩提问预判与回答思路老师最常问的问题有三个为什么用状态机而不是计数器如果出现异常状态怎么办按键消抖时间为什么选10ms回答思路分别是状态机把豆浆机的工艺流程直接映射为有限个阶段控制逻辑可读性强计数器只能做时间累加不适合表达阶段切换异常状态下组合逻辑中的default分支会回到待机保证系统不会死在未知状态机械按键抖动一般不超过5ms10ms是留了余量的经验值同时兼顾响应速度。我负责过好多次课程设计辅导每次带学生演示前都会做同一件事把testbench仿真波形导出成图片和板子每一步按键操作放在一起对照检查。这个动作看着简单但真的能发现“仿真过了但上板不稳”的隐藏坑。从那以后我每次遇到涉及状态机的项目都强制自己走一遍仿真和实板对照的流程希望帮到你。本文还有配套的精品资源点击获取