简介:一套基于51单片机的智能电饭锅系统设计方案,面向单片机初学者、电子爱好者及课程设计/毕业设计人群。系统支持煲汤、煮粥、快炖、慢炖、保温五种模式,配合矩阵键盘设定预约时间(1~10小时可调),并通过数码管实时显示工作状态;同时具备防干烧检测和蜂鸣报警功能,整体逻辑完整、贴近实际应用。压缩包共36个文件,约1.19MB,涵盖C语言源码、Proteus仿真、原理图、流程图、元件清单以及HEX烧录文件等,从开发调试到硬件制作所需的关键资料均已包含。目前已有117人学习,适合以“51单片机+矩阵键盘+数码显示”为课题的项目参考,可直接对照仿真与源码理解按键扫描、定时预约、温度检测与报警控制的实现思路。
1. 电饭锅控制系统的难点不在加热,而在时序管理
做电饭锅控制,很多人第一反应是继电器驱动加热管,把温度拉起来就完事。真正写代码才发现,麻烦全在别处:4×4矩阵键盘要处理煲汤、煮粥、快炖、慢炖、保温五种模式切换,数码管要动态刷新显示预约倒计时,DS18B20单总线协议要跟加热逻辑抢CPU时间,还要防干烧报警。整个系统是一个典型的多任务裸机调度场景——中断、定时器、按键扫描、温度采样全部挤在一个51内核里。
这套基于STC89C52(兼容AT89S52)的电饭锅方案,包含完整C51源码、Proteus 8仿真工程(ISIS仿真.DSN)、Altium Designer原理图(Sheet1.SchDoc)、流程图和元件清单。适合三类人:做课程设计需要直接复现的学生,想把矩阵键盘、数码管、DS18B20、继电器这几样外设串成一个完整逻辑的嵌入式初学者,以及想看看别人怎么用状态机管理多模式时序的从业者。下面按硬件架构、按键输入、核心控制、仿真调试四个层面拆开讲。
2. 硬件架构与Proteus仿真电路搭建
2.1 系统组成与外设选型
整个控制系统以51单片机为控制核心,外设分四路:4×4矩阵键盘作为输入设备,6位共阴数码管(或4位,取决于位选扩展)作为显示设备,DS18B20作为锅底温度传感器,继电器控制加热管通断。报警部分用有源蜂鸣器,防干烧检测直接复用DS18B20的实时温度——当温度超过100℃且持续一段时间,判定为干烧。
选型上需要注意两点。第一,温度传感器用DS18B20而不是热敏电阻,是因为电饭锅需要长时间稳定监测,DS18B20的数字输出可以直接挂在P3.7上,省去ADC采样电路,Proteus仿真时也方便直接读取温度值。第二,数码管显示必须用动态扫描而不是静态显示,否则6位数码管需要48个IO口,51单片机根本不够用。动态扫描用P0口做段选,P2口做位选,通过74HC245或三极管驱动提高驱动能力。
下面是用到的核心元件清单(从配套的元件清单.xlsx中整理):
| 元件 | 型号/参数 | 数量 | 作用 |
|---|---|---|---|
| 单片机 | STC89C52 | 1 | 主控 |
| 温度传感器 | DS18B20 | 1 | 锅底温度检测 |
| 数码管 | 共阴6位 | 1 | 时间/温度显示 |
| 键盘 | 4×4矩阵 | 1 | 模式选择/时间设定 |
| 继电器 | 5V单路 | 1 | 加热管通断 |
| 蜂鸣器 | 有源5V | 1 | 干烧/完成报警 |
| 晶体 | 12MHz | 1 | 主时钟 |
| 驱动芯片 | 74HC245 | 2 | 段选/位选驱动 |
2.2 数码管动态扫描电路设计
P0口作为段选口,接数码管的a-g和dp引脚。这里有一个典型的坑:P0口是开漏输出,内部没有上拉电阻,直接接数码管段选会出现高电平拉不上去、显示亮度不均的问题,所以必须在P0口加排阻上拉到VCC。Proteus仿真时可以省掉排阻,但实物焊接时不能省。
位选信号我一般用P2.0~P2.5控制六个PNP三极管(实物用8550),基极通过4.7k电阻接单片机IO口,集电极接数码管公共端。动态扫描时同一时刻只点亮一位数码管,按2ms间隔循环扫描6位,利用人眼视觉暂留形成稳定显示。刷新频率不能低于100Hz整体刷新率(6位×2ms=12ms周期,约83Hz),否则会看到明显闪烁。
2.3 加热与温度检测回路
加热回路用继电器控制220V加热管,单片机P3.4输出高电平驱动三极管S8050,三极管导通后继电器线圈得电,触点闭合接通加热管。Proteus仿真里用的是220V交流电源和电阻丝模型,不用考虑隔离问题,但实物要注意继电器续流二极管(1N4007)必须反向并联在线圈两端,否则关断瞬间的反向电动势会击穿三极管。
DS18B20的数据线DQ接P3.7,需要外接4.7k上拉电阻到VCC。单总线协议对时序要求严苛,12MHz晶振下每个时隙的延时参数要按数据手册计算,后面第4章会给实际可用的延时函数。
3. 矩阵键盘扫描与按键功能映射
3.1 4×4矩阵键盘的扫描原理
矩阵键盘省IO的核心思想是行列复用:4根行线(Row)接P1.0~P1.3,4根列线(Col)接P1.4~P1.7。读取时先给所有行线写低电平,然后读列线电平,如果某一列被拉低,说明该列上有按键按下——这是「列扫描」的第一步,确定按键所在的列。再反转方向,给列线写低电平,读行线,确定所在行。行列交叉点就是按下的按键。
这个方案相比独立按键,16个按键只占8个IO口。但代价是扫描代码复杂度上升,并且必须处理「多键同时按下」的情况——本项目的功能设计上不需要组合键,所以代码里只识别第一个检测到的按键,其余忽略。
3.2 消抖与连击处理
按键按下瞬间会产生约5~10ms的机械抖动,直接读取电平状态会导致一次按下被识别成多次。常见做法有两种:延时消抖和状态机消抖。延时消抖简单粗暴——检测到电平变化后等待10ms再读一次,两次结果一致才确认有效。状态机消抖更优雅,通过定时器中断每2ms采样一次,连续采样三次状态一致才认为稳定,适合系统里已有定时器中断的场景。
我用的消抖实现如下,基于主循环内的软延时:
// 按键扫描函数,返回按键键值,0表示无按键 unsigned char KeyScan(void) { unsigned char row, col, key_value = 0; unsigned char temp; unsigned char key_state = 0; // 第一步:行线置低,读列线 P1 = 0xF0; // P1.4~P1.7输出0,P1.0~P1.3拉高(输入模式) temp = P1 & 0xF0; // 读P1.4~P1.7 if (temp != 0xF0) // 有按键按下 { DelayMs(10); // 消抖 temp = P1 & 0xF0; // 再次读取确认 if (temp != 0xF0) { // 确定列号 for (col = 0; col < 4; col++) { if ((temp & 0x10) == 0) break; temp <<= 1; } // 第二步:反转扫描,确定行号 P1 = 0x0F; // 列线置低,行线拉高 temp = P1 & 0x0F; for (row = 0; row < 4; row++) { if ((temp & 0x01) == 0) break; temp >>= 1; } key_value = row * 4 + col + 1; // 按键编号1~16 key_state = 1; } } // 等待按键释放,防止一次按下多次触发 if (key_state) { while ((P1 & 0xF0) != 0xF0); // 松手检测 DelayMs(5); // 释放消抖 } return key_value; }这段代码的执行流程是:行线置低扫描列线——如果检测到有列线为低,说明有按键按下,延时10ms再次确认消除机械抖动;确认后记录列号,再反转行列方向扫描行线,得到行号;组后等待按键释放,避免按住时反复触发。key_value的计算方式是row * 4 + col + 1,刚好映射到1~16的键值。
这里有一个参数值得注意:DelayMs(10)的10ms是经验值,机械按键的抖动周期一般在5~10ms,取10ms可以覆盖绝大多数情况。如果你用的是触摸按键或者薄膜按键,抖动时间更短,可以缩到5ms。而松手检测的while循环没有超时保护,实际工程中如果出现按键卡死(弹簧失效)会导致主循环卡在这里,更健壮的做法是加一个超时计数器强制退出。
3.3 按键功能分配
16个按键按功能分为三组,映射关系如下:
| 按键编号 | 功能 | 说明 |
|---|---|---|
| 1~5 | 模式选择 | 四种汤、两种粥、快炖、慢炖 |
| 6~8 | 时间调整 | 增加、减少、确认 |
| 9 | 保温切换 | 开启/关闭保温 |
| 10 | 预约设置 | 进入预约时间调整 |
| 11~16 | 保留 | 可扩展定时/菜单 |
模式选择采用「按下立即切换并记忆」的方式:按下模式键后,数码管对应位显示当前模式编号,同时把模式码写入EEPROM(51内部暂无EEPROM时用外部AT24C02,或者干脆用RAM变量,掉电丢失但课设够用)。预约时间调整逻辑是:按下预约键进入预约模式,此时数码管显示当前预约小时数,按加/减键在1~10小时范围内调整,按确认键生效。
4. 核心控制逻辑:状态机驱动加热、保温与防干烧
4.1 系统状态定义与状态流转
电饭锅控制系统本质是一个有限状态机,状态切换由按键和温度共同触发。定义六个状态:待机(IDLE)、预约等待(TIMER_WAIT)、加热烹饪(COOKING)、保温(KEEP_WARM)、防干烧报警(ALARM)、完成停机(FINISH)。状态之间的跳转规则是这个项目代码里最重要的逻辑骨架。
// 状态枚举定义 typedef enum { IDLE, // 待机 TIMER_WAIT, // 预约等待 COOKING, // 加热烹饪中 KEEP_WARM, // 保温中 ALARM, // 干烧报警 FINISH // 烹饪完成 } SysState; SysState current_state = IDLE; unsigned int cook_time_min; // 烹饪剩余时间(分钟) unsigned int reserve_hour; // 预约小时数(1~10小时)状态跳转的核心逻辑在主循环中,按状态分支执行:
void SystemLoop(void) { unsigned char key = KeyScan(); switch (current_state) { case IDLE: if (key >= 1 && key <= 5) { // 选择了模式,进入预约等待或直接开始 select_mode(key); current_state = COOKING; cook_time_min = get_time_by_mode(key); } break; case TIMER_WAIT: // 预约倒计时,每秒减1,到0进入烹饪 if (reserve_hour == 0) { current_state = COOKING; } break; case COOKING: // 读取温度,检测干烧 temp_val = ReadDs18b20(); if (temp_val >= 100) { current_state = ALARM; break; } // 烹饪时间到,切换到保温或完成 if (cook_time_min == 0) { if (keep_warm_enabled) { current_state = KEEP_WARM; } else { current_state = FINISH; } } break; case KEEP_WARM: // 保温上下限控制,低于90℃加热,高于100℃停止 temp_val = ReadDs18b20(); if (temp_val <= 90) HEATER_ON = 1; if (temp_val >= 100) HEATER_ON = 0; break; case ALARM: BEEP = 0; // 蜂鸣器响 HEATER_ON = 0; // 断开加热 if (key == 16) { // 按任意键解除报警 BEEP = 1; current_state = IDLE; } break; } }状态机的价值在于把复杂度分散到每个状态里,每个状态只处理自己该做的事。比如COOKING状态只关心「时间有没有到、温度有没有超限」,不需要知道预约等待阶段是怎么计时的。ALARM状态下只允许按键解除报警,防止用户误操作。这种写法对后期维护和功能扩展都友好——要加一个「蒸煮」模式,只需要增加一个模式参数,不需要改状态机骨架。
4.2 定时器配置与预约时间计算
所有时间基准都来自定时器T0中断。12MHz晶振下,定时器T0每50ms中断一次,20次中断累加为1秒。这个1秒基准再分发给两个计数器:一个是预约倒计时(单位秒,累加到3600秒为1小时),另一个是烹饪倒计时(单位分钟,60秒减1分钟)。
// 定时器T0初始化:模式1(16位定时),50ms中断 void Timer0_Init(void) { TMOD &= 0xF0; TMOD |= 0x01; // T0工作在方式1,16位定时 TH0 = (65536 - 50000) / 256; // 50ms定时初值 TL0 = (65536 - 50000) % 256; ET0 = 1; // 开T0中断 EA = 1; // 开总中断 TR0 = 1; // 启动定时器 } // 定时器中断服务函数 void Timer0_ISR(void) interrupt 1 { static unsigned int ms_counter = 0; static unsigned int sec_counter = 0; TH0 = (65536 - 50000) / 256; // 重装初值 TL0 = (65536 - 50000) % 256; ms_counter += 50; if (ms_counter >= 1000) { // 1秒到达 ms_counter = 0; sec_counter++; // 预约倒计时(每秒刷新) if (current_state == TIMER_WAIT && reserve_sec > 0) { reserve_sec--; if (reserve_sec == 0) { current_state = COOKING; // 预约时间到,开始烹饪 } } // 烹饪倒计时(每60秒减1分钟) if (current_state == COOKING && cook_time_min > 0) { if (sec_counter >= 60) { sec_counter = 0; cook_time_min--; } } // 数码管显示刷新(每秒更新显示内容) UpdateDisplay(); } }预约时间的核心计算公式:reserve_sec = reserve_hour * 3600。用户在按键界面设置的小时数(1~10)在按下确认键时换算成秒存入reserve_sec。这个设计避免了对小时和分分别计数带来的复杂判断——全部统一到秒,到点判断只做一个reserve_sec == 0就够了。
4.3 DS18B20温度读取与防干烧判定
DS18B20使用单总线协议,操作时序分三步:初始化复位、写ROM指令、读温度数据。这个项目的Ds18b20.c文件中实现了完整的时序函数,核心代码如下:
// DS18B20复位,返回1表示存在 bit DS18B20_Reset(void) { bit presence; DQ = 0; DelayUs(480); // 复位脉冲,480us低电平 DQ = 1; DelayUs(60); // 等待60us presence = DQ; // 读取存在脉冲 DelayUs(420); // 时隙剩余时间 return presence; } // 读取单字节,从低位到高位 unsigned char DS18B20_ReadByte(void) { unsigned char i, data_byte = 0; for (i = 0; i < 8; i++) { data_byte >>= 1; DQ = 0; // 读时隙开始 _nop_(); DQ = 1; // 释放总线 _nop_(); if (DQ) { data_byte |= 0x80; } DelayUs(60); // 时隙持续60us } return data_byte; } // 启动温度转换(12位分辨率,最大转换时间750ms) void DS18B20_StartConvert(void) { DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM检测 DS18B20_WriteByte(0x44); // 启动温度转换 } // 读取温度值,返回整数型温度(单位:℃) int DS18B20_GetTemp(void) { unsigned char temp_low, temp_high; int temperature; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 temp_low = DS18B20_ReadByte(); temp_high = DS18B20_ReadByte(); temperature = (temp_high << 8) | temp_low; temperature >>= 4; // 只取整数部分,忽略小数 return temperature * 10; // 返回温度值,单位0.1℃,用于和设定值比较 }温度值处理有一个关键点:DS18B20的12位温度数据是带符号的,低4位是小数部分。只取整数部分的方式是右移4位,但要注意先拼接成16位再右移,不能分别移动高低字节——否则负数温度会出错。temperature * 10的目的是把整数温度放大10倍,这样防干烧判定「温度≥100.0℃」就可以用整型比较temp_val >= 1000,避免浮点数运算(51单片机做浮点运算非常慢,占用大量ROM)。
防干烧判定的策略是:持续检测温度,如果温度达到100℃(水的沸点),说明锅底水烧干了,进入ALARM状态。但直接100℃就报警有点灵敏,因为汤水沸腾时锅底温度很容易短暂超过100℃。我实现的逻辑是在COOKING状态连续读到3次温度≥100℃才触发报警,每次读取间隔1秒,相当于连续3秒超温才算干烧,有效避免了误报。
4.4 PWM式加热控制和保温策略
加热控制不是简单的继电器通断,而是采用类似PWM的占空比控制:以10秒为一个控制周期,根据当前温度和目标温度的差值,决定加热时间在这个周期内的占比。举例来说,设定目标温度95℃煮粥,当前温度85℃,温差10℃,加热占空比为70%,即10秒里加热7秒、停3秒。这个实现让继电器不会频繁通断(频繁通断会打火减少寿命),又比纯开关控制保温效果平稳。
加热控制的决策表如下(自动调整占空比逐级递减,避免温度过冲):
| 当前温度 | 加热时间/10s周期 | 说明 |
|---|---|---|
| < 目标-20℃ | 10s | 全功率加热 |
| 目标-20℃ ~ 目标-10℃ | 8s | 90%占空比 |
| 目标-10℃ ~ 目标-5℃ | 6s | 60%占空比 |
| 目标-5℃ ~ 目标 | 3s | 30%占空比 |
| ≥ 目标+2℃ | 0s | 停止加热 |
保温状态复用这套逻辑,目标温度设定为90℃即可。用定时器中断里的pwm_counter变量计数,每100ms累加一次,10秒一个周期共100个计数点,加热时间映射成计数阈值。这套方案在Proteus里仿真效果明显——温度曲线平滑,不像简单通断控制那样上下剧烈震荡。
5. Proteus仿真调试:从原理图到联调的关键细节
5.1 仿真模型连接与常见报错处理
打开配套的ISIS仿真.DSN文件后,应看到完整的电路连接。如果自己从原理图重新搭仿真,最关键的是确认芯片型号和电源网络标签:51单片机选AT89C52,DS18B20在Proteus元件库中的名称是DS18B20(实际是DALLAS公司的模型),数码管可选7SEG-MPX6-CA(共阳)或7SEG-MPX6-CC(共阴),注意与你代码里的段码表方向一致。
搭建完成后的首要验证步骤是加一个while(1);空程序编译下载运行,检查数码管是否全灭、按键是否有电平变化。如果在Proteus里程序完全跑不起来,优先检查三个地方:
- 单片机晶振频率是否设置为12MHz(双击单片机芯片设置Clock Frequency)。代码里的
DelayUs(480)等延时参数全部按12MHz计算,如果仿真用的频率是11.0592MHz,DS18B20的时序会偏移,经常出现温度读不出来、显示FF或者85℃的情况。 - 电源网络是否完整。Proteus仿真图上0V网标注为Ground,VCC标注为Power,如果元件电源脚没有接对,数码管永远不会亮。
- 复位电路是否正确。简单起见可以用
RST = 0直接接地——Proteus仿真不需要上电复位脉冲,但实物电路必须有RC复位电路。
5.2 DS18B20在Proteus仿真的特殊处理
Proteus自带的DS18B20模型和实物有一个细微差别:实物转换温度需要750ms(12位分辨率),Proteus模型可以设置响应延迟。如果发现仿真时温度长时间不变,检查代码里两次读取温度之间的间隔是否足够。我写的DS18B20_StartConvert()后面会跟一个DelayMs(750)等待转换完成,仿真时用DelayMs(100)就够了,因为Proteus模型的转换时间是「仿真时间」,不是真实时间。
另一个仿真特有的坑:Proteus的DS18B20模型初始化不依赖严格的复位时序,但A51代码里如果复位失败会直接跳过温度读取,导致温度永远显示0。判断代码卡在哪个环节的方法是在Keil里全速运行后暂停,查看current_state变量和PC指针位置。如果PC指针反复在DS18B20_Reset()内部循环,说明仿真模型的时序响应和真实芯片有差异,需要把延时调大(DelayUs(480)改到DelayUs(600))。
5.3 Keil与Proteus联调的效率技巧
推荐用Keil的Debug模式配合Proteus联调。在Keil中配置Debug选项卡,选择Proteus VSM Simulator作为仿真器,设置远程调试端口8001,然后在Proteus里启用Remote Debug Monitor。这样可以在Keil里下单步、看变量、打断点,同步观察Proteus里的硬件状态变化——比如按下矩阵键盘第1键时,Keil的变量窗口能看到key_value变为1,数码管实时显示对应模式。
联调时最值得跟踪的变量有三个:current_state(看状态是否按预期跳转)、temp_val(看DS18B20读取是否正常)、cook_time_min(看倒计时是否准确)。我一般会在每个状态跳转点设置条件断点,比如current_state == ALARM时断下,观察触发报警时的温度值——这样可以精确判断防干烧阈值设置的是偏早还是偏晚。
5.4 矩阵键盘仿真时的上拉电阻必要性
Proteus里矩阵键盘直接用按键元件(BUTTON)连接P1口,行线和列线之间没有上拉电阻。但51单片机的P1口是准双向口,内部有上拉电阻,所以仿真时能正常工作。实物电路上,P1口虽然内部有弱上拉,但抗干扰能力差,建议在P1口外加4.7k排阻上拉到VCC,防止按键引线过长时误触。
另外注意矩阵键盘的行列扫描顺序要和代码保持一致。我在2.2节代码里用的方向是行线接P1.0~P1.3、列线接P1.4~P1.7,按键编号第1行第1列是row=0, col=0,对应键值1。如果你在Proteus里画电路时把行列接反了,按键功能就会错乱——按「开始」触发了「保温」。检查方法:在Keil里给KeyScan()加断点,按下某个按键看返回的键值是不是和设计一致,不匹配就把电路的行列对调或修改row * 4 + col + 1中的行列权重。
最后补充一个实机与仿真差异的点:Proteus里继电器、数码管、蜂鸣器的驱动电流都是理想模型,不需要考虑单片机IO口驱动能力不足的问题。但实机上P0口外接数码管段选必须加上拉电阻,继电器不能直接接IO口,这些在最终出实物前一定要对照元件清单.xlsx把驱动电路补全。
本文还有配套的精品资源,点击获取