FPGA课设经典:豆浆机面板设计全程解析

📅 发布时间:2026/9/8 3:31:48
FPGA课设经典:豆浆机面板设计全程解析 简介这是一份完整的FPGA课程设计工程包面向电子信息类学生与Verilog初学者围绕豆浆机面板的按键选档、温度检测、电机及加热器联动控制等课设要求展开。工程采用模块化设计覆盖五谷、湿豆、干豆、果汁、米糊、玉米汁六档功能结合按键输入、LED状态显示、四位数码管倒计时、蜂鸣器报警以及防溢出/防干烧检测并提供温度传感器与电机驱动的控制思路。压缩包共188个文件约4.51MB以Quartus工程文件qpf/qsf、Verilog/VHDL源文件v/vhd/tdf、编译下载文件sof/pof、仿真波形vwf及编译报告rpt/qmsg为主体同时含docx设计文档与readme说明目录完整便于直接打开工程进行仿真和下载验证。目前已有811人学习下载既能支撑课程设计报告撰写也适合用来理解FPGA状态机、分频计时与模块接口等基础方法并在此基础上二次扩展。 做FPGA课设选豆浆机面板这个题目我是相当推荐的。别看它听起来像个家用电器控制板实际上把状态机、定时器、按键消抖、动态扫描显示、PWM/蜂鸣器驱动这些FPGA入门最核心的知识点全串起来了。做完这个项目你对“时序逻辑到底是怎么跑起来的”这件事理解会比其他同学深一大截。这篇就把我当时做这个项目的完整思路、RTL设计细节、仿真和上板踩坑过程都摊开讲一遍给正在做类似课设或者想练手的人一个参考。1. 需求拆解与设计思路为什么拿豆浆机当课设最合适市面上绝大多数的FPGA课设题要么是流水灯、数码管计数这种“太简单没含金量”的要么是图像处理、以太网这种“直接劝退新手”的。豆浆机面板设计刚好卡在中间难度曲线非常舒服。1.1 豆浆机流程天然就是状态机一台真实的豆浆机工作流程大概是这样的启动后先加热到一定温度然后开始打磨电机转动打磨完再加热煮沸煮沸后进入保温。这个流程本身就是一个典型的状态机——当前处于哪个阶段取决于之前发生了什么、当前的条件是否满足。用FPGA做这个项目核心就是把这个真实的物理流程抽象成有限状态机FSM。这也是FPGA设计的核心思想之一一切控制逻辑本质上都是状态在不同条件下互相跳转。做完这个项目你会发现看其他任何控制类问题都会下意识去想“它的状态有哪些跳转条件是什么”——这个思维习惯比学会几个Verilog语法值钱得多。1.2 设计目标与功能清单我给自己定的目标是做一个“可演示、可交互、有完整反馈”的面板而不是简单模拟几个灯的亮灭。功能分成三层基础功能按键操作启动/取消/模式选择、状态指示LED显示当前阶段、数码管显示倒计时进阶功能蜂鸣器提示音按键音、状态切换音、豆浆/米糊/果蔬汁三种模式对应不同的加热和打磨时间加分功能温度值实时显示在数码管副位温度由FPGA内部模拟产生演示时不需要外接温度传感器功能定下来之后整个系统的边界就清晰了后面的Verilog代码不会写着写着就跑偏。2. 硬件选型与接口规划FPGA板子这块我当时用的是学校实验室常见的板子核心芯片是Xilinx Spartan-6系列的XC6SLX9板子上自带了50MHz晶振、按键、LED、数码管和蜂鸣器。你要是用其他型号比如Altera的Cyclone IV或者国产安路、高云的板子逻辑都一样只是管脚约束XDC/QSF文件需要改一下。2.1 外设资源分配我做的系统对外设的需求和接口规划如下外设用途数量接口类型备注按键启动、取消、模式切换3独立按键接IO口输入需消抖LED显示当前工作阶段4独立IO加热、打磨、煮沸、保温数码管显示模式、倒计时/温度6位动态扫描段选位选蜂鸣器按键音、阶段切换提示音1PWM驱动有源蜂鸣器IO控制无源需PWM温度传感器采集豆浆温度1单总线或ADC演示可用内部模拟替代这里有个关键的经验外设接口规划一定一定先于代码编写。曾经有同学写完了全部RTL代码最后管脚分配的时候发现IO资源不够用或者两个外设复用了同一个引脚还要返工改代码非常痛苦。先把管脚约束文件写好再开始写逻辑才是最省事的顺序。2.2 硬件设计的几个注意点按键一定要设计上拉电阻一般板载已经有了FPGA内部也能配置弱上拉但外部上拉更可靠抗干扰能力强不少。数码管如果是共阳的段选信号需要低电平点亮共阴则反过来。这个极性搞反了显示全乱而且是硬件问题不是代码问题排查起来很花时间。蜂鸣器如果是有源蜂鸣器自带振荡电路只需要给高电平或低电平就能响如果是无源蜂鸣器必须给一定频率的PWM方波才能发声。代码方案差别挺大确定硬件型号再写驱动。3. RTL设计与核心流程实现这一部分是整个课设的大头。我的设计分成了四个模块按键消抖模块debounce、状态机控制模块fsm_ctrl、数码管扫描显示模块seg_display、蜂鸣器驱动模块buzzer。顶层用结构化的方式把四个模块例化连起来。3.1 核心状态机的设计与编码状态定义直接对应豆浆机的物理流程S_IDLE 5b00001 // 待机状态 S_HEAT 5b00010 // 加热状态 S_GRIND 5b00100 // 打磨状态 S_BOIL 5b01000 // 煮沸状态 S_WARM 5b10000 // 保温状态这里用独热码one-hot每个状态对应一个比特位。状态少、逻辑简单的时候独热码综合出来的电路速度快、无毛刺状态多的时候再用二进制码省寄存器资源。这种代码写多了之后你就知道状态编码的选定本身也是一门学问。状态跳转的逻辑我简化成了这样这一段是核心代码的核心always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_IDLE; time_cnt 0; end else begin case (state) S_IDLE: begin time_cnt 0; if (start_key) state S_HEAT; else state S_IDLE; end S_HEAT: begin if (time_cnt HEAT_TIME) begin state S_GRIND; time_cnt 0; end else begin time_cnt time_cnt 1; end end // 其他状态跳转类似... endcase end end这段代码的跳转逻辑是最容易出bug的地方。比如“S_GRIND进入S_BOIL的时机”如果理解成“打磨完直接煮沸”就错了。实际豆浆机是加热-打磨交替进行的打磨完还要再加热一段时间才到煮沸这个层次关系在FSM里面要多设计几个过渡状态。我当时的设计完整的跳转顺序是S_IDLE - S_HEAT - S_GRIND - S_BOIL - S_WARM ^-----------|也就是说打磨S_GRIND完成后要回到加热S_HEAT状态然后再次根据条件判断是进入煮沸还是继续打磨。为了避免无限循环我引入了一个“打磨次数计数器”打磨满3次后从S_GRIND回到S_HEAT时下一次计时结束就直接进S_BOIL。3.2 时间参数的计算从真实时间到计数器溢出这是课设里最容易“蒙圈”的地方。豆浆机加热20分钟FPGA不可能真的等你20分钟去演示所以需要把演示时间压缩。我当时设的系统时钟频率是50MHz驱动状态机的时钟是1kHz1ms一个tick所有时间参数都以这个tick为单位计算。以加热时间为例。如果真实时间是20分钟1200秒映射到1kHz时钟就是1,200,000个tick。这个数值相当大但演示起来太慢。我把时间压缩为HEAT_TIME 3000即3秒钟GRIND_TIME 1000即1秒钟BOIL_TIME 3000WARM_TIME保持长一点比如10秒方便演示保温效果。这里关键逻辑是tick计数器必须有上限判断防止溢出后自动清零又从头开始导致状态机永远跳不出去。// 1kHz tick 产生逻辑 reg [15:0] tick_div; wire tick_1k; always (posedge clk or negedge rst_n) begin if (!rst_n) tick_div 0; else if (tick_div 50000-1) tick_div 0; else tick_div tick_div 1; end assign tick_1k (tick_div 50000-1);用tick_1k作为状态机内部time_cnt的计数使能信号。这样设计的好处是你调时间参数只需要改几个常量不用动整体逻辑后面想演示不同模式也很方便。3.3 按键消抖每个FPGA入门者都要过的一道坎机械按键按下和松开的时候因为物理弹性形变信号会有大约5~20ms的抖动。如果不处理一次按键会被识别成多次触发状态机一顿乱跳整个演示直接翻车。写这个模块的时候我反复和它搏斗过最终稳定方案是这样的always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_dly1 1b1; key_dly2 1b1; end else begin key_dly1 key_in; key_dly2 key_dly1; end end wire key_pulse key_dly2 !key_dly1; // 检测下降沿原理很简单信号连续打两拍。当key_in从高电平变为低电平时key_dly1先变低但key_dly2还是高电平此时key_pulse产生一个脉冲。这段逻辑直接放在状态机模块内部也可以独立成模块复用但要注意这里只是“去除亚稳态抖动”的简化版如果按键抖动超过一个周期还是需要加计数器延时确认。更稳妥的做法是等按键电平稳定20ms再采代码如下reg [19:0] debounce_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) debounce_cnt 0; else if (key_in ! key_stable) begin debounce_cnt 0; key_stable key_in; end else if (debounce_cnt 20d999999) // 延时20ms key_valid key_in; else debounce_cnt debounce_cnt 1; end我的经验是工业级代码用第二个版本计数确认课设演示用第一个版本双触发器同步一般也够但为了演示稳定的效果我建议用计数确认版本一劳永逸。3.4 数码管动态扫描用“视觉暂留”骗过眼睛数码管数量多的时候不可能每个数码管都分配独立的段选线那样太费IO了。标准做法是动态扫描同一时间只点亮一位数码管轮流点亮每一位刷新频率高于人眼视觉暂留频率100Hz以上看起来就全部都在亮。我的刷新逻辑如下reg [2:0] scan_pos; wire [7:0] seg_data; always (posedge clk or negedge rst_n) begin if (!rst_n) scan_pos 0; else if (tick_1k) scan_pos scan_pos 1; end // 位选每次选通某一位 always (*) begin case (scan_pos) 0: begin seg_data mode_disp; seg_sel 6b111110; end 1: begin seg_data time_disp[3:0]; seg_sel 6b111101; end // 其他位类似... endcase end这里的关键参数是刷新频率。我用的1kHz tick作为扫描时钟6位数码管每个位分到大概166Hz的刷新率肉眼完全看不到闪烁。如果刷得太慢比如用10Hz数码管会闪得人眼睛疼。这个频率范围大概是200Hz到2kHz之间最好太快了位选切换会有拖影太慢了闪烁。3.5 蜂鸣器驱动与PWM音调控制蜂鸣器这部分看起来简单但做不好很容易尴尬要么该响的时候不响要么不该响的时候狂响。如果用无源蜂鸣器需要产生特定频率的方波才能发声。我设计了一个简化的音调发生器reg [15:0] pwm_div; reg buzzer_out; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pwm_div 0; buzzer_out 0; end else if (buzzer_en) begin pwm_div pwm_div 1; if (pwm_div (CLK_FREQ / TONE_FREQ / 2)) pulsed_flag ~pulsed_flag; end end实现的时候我让按键按下时蜂鸣器响50ms一个短滴声状态切换时响200ms一个长的提示音两种声音长短差异完全通过状态机里的计数器控制。有一说一这个功能在答辩演示时非常吸睛一下就能让评委知道你的状态机是真的在跳转而不是在跑马灯。4. 仿真验证与板级调试踩过的坑全实录写代码只是第一步真正的时间大头全在仿真和上板调试。我用Vivado自带的仿真器写的testbench没有用复杂的UVM那一套课设场景用不到。4.1 Testbench怎么搭才能验证到核心逻辑我的测试思路是给状态机输入一个“模拟按键启动”的脉冲然后观察state是否按预设顺序跳转以及time_cnt是否在正确时间清零。核心的testbench代码module tb_top; reg clk; reg rst_n; reg start_key; wire [4:0] state; top u_top( .clk(clk), .rst_n(rst_n), .start_key(start_key), .state(state) ); initial begin clk 0; rst_n 0; start_key 1; #100; rst_n 1; #50; start_key 0; // 模拟按键按下 #100; start_key 1; // 模拟按键释放 #10000; $finish; end always #10 clk ~clk; // 50MHz时钟 endmodule仿真波形里重点观察三个点按键按下后状态是否从S_IDLE跳到了S_HEATS_HEAT内time_cnt是否从0涨到3000后溢出清零并跳转S_GRIND是否在设定时间后进入S_BOIL。如果这三个点都对核心逻辑就稳了一大半剩下的是边界条件。第一次仿真时我发现问题time_cnt到达3000之后状态跳到了S_GRIND但time_cnt没清零而是继续递增。这个bug非常隐蔽——代码里清零条件是写在S_HEAT内部的我一看原来是跳转和清零写在了同一个if-else分支里跳转后下一个时钟沿state已经变了但time_cnt还在等计数使能信号。解决办法是每次跳转时单独加一个同步清空而不是依赖主计数器的计算分支。4.2 上板调试中的经典翻车现场仿真通过不代表上板就一切顺利这是我几年下来最大的体会。以下问题是上板调试中遇到过的各位可以照着排查现象一按下按键状态机直接跳过多个状态。原因按键没有消抖或消抖不彻底一次按下被识别成3-5次脉冲。排查方向看按键模块输出有没有毛刺。如果确认消抖模块逻辑没问题建议在testbench里加一个“带5ms抖动的按键模型”在仿真阶段就模拟出来。现象二数码管全亮但显示乱码。原因段选信号极性反转了。共阳数码管的公共端接VCC段选低电平才亮共阴则相反。用示波器量一下或者查板子的原理图。现象三蜂鸣器不响。原因无源蜂鸣器用了直流电平驱动给个1就以为是响的实际上必须给方波。这个现象在代码里特别容易忽略因为RTL仿真里你给个1波形看着就是“高电平”你根本看不出不响。现象四状态机死锁卡在某个状态出不来。原因状态跳转条件不满足或计数器计数异常。可以先检查复位信号和时钟是否正常再在Vivado里用逻辑分析仪ILA看内部state信号这是最直接的方式。4.3 如何在Vivado中高效定位逻辑问题课设调试阶段强烈建议用Vivado的ILA集成逻辑分析仪核可以实时抓取FPGA内部信号。当时系统跑飞了以后我抓的state信号看起来是一堆随机值刚开始完全懵了。后来发现问题是状态机用了独热码多个位同时为1查起来应该是S_IDLE和S_WARM的寄存器同时复位异常时序出了双沿问题。把信号抓出来和预期的时序图做对比绝大部分问题都能暴露出来。上板之后最有效的思路是先确认时钟、复位正常再确认外设接口极性正常最后才开始抓内部逻辑信号不要一上来就怀疑算法写错。5. 答辩亮点与项目扩展建议做完这个设计再回头复盘我觉得有几个点是在答辩时能讲出彩的比如状态编码方式的取舍独热码 vs 二进制码、时间参数如何设计可配置而不是写死常数、模块化设计中接口的划分逻辑。这个项目如果想继续扩展有两条思路我觉得都很有意思。一条是往真实硬件方向走把温度采集模块用真实的DS18B20温度传感器接入代替内部模拟的温度值这样加热阶段的跳转条件就不再是“计时到”而是“温度达到阈值”——这更接近真实豆浆机的工作逻辑也更像工业控制。另外一条是往性能方向走在FPGA里直接用Cordic算法或查找表计算温度值对应的BCD码而不是简单用计数器模拟虽然课设用不上但那些想做高速信号处理的人可以提前感受一下数值计算的硬件实现思路。6. 写在最后的一些经验分享豆浆机面板这个项目其实做了两次才真正跑通。第一次栽在按键消抖上第二次栽在数码管极性上第三次才稳定跑通全流程。要说最大的体会就是做FPGA项目一定不要一上来就对着代码敲顺序应该是先对着豆浆机的物理流程画状态转移图把状态、跳转条件、每个状态下的输出列清楚然后再写代码。另外仿真和上板一定是互相配合的。仿真能帮你确定逻辑功能正确但上板能暴露真实物理世界的问题抖动、极性、干扰。建议仿真阶段就把按键抖动这种外设行为建模出来不要等到上了板再慢慢猜。最后分享一个很多人忽略的小技巧在顶层模块里学着我这样把状态机的当前状态引入一个sys_state信号输出到板载LED上同时用另一个位宽的计数器把时间信息分频到数码管显示。调试的时候一眼就能看出状态机现在停在哪儿、时间走到了多少而不是把全部信息都用串口打印或者逻辑分析仪抓。这个小“调试观察口”的设计能帮你省下好几个小时的排错时间。希望这篇经验能给正在做FPGA课设的人一点参考。把状态机弄明白了、把消抖和扫描这类通用模块吃透了后面再接触PCIe、DDR、以太网这些复杂IP时你至少不慌——因为这些高大上的东西底层玩的最多的还是有限状态机这一套逻辑。本文还有配套的精品资源点击获取