基于51单片机的智能垃圾桶:自动开盖与垃圾分类状态机实现
简介:这套基于51单片机的分类智能垃圾桶资源,定位为电子设计竞赛或课程设计的完整参考方案,适合单片机初学者、职校学生及毕业设计人员,帮助快速实现垃圾自动分类与桶盖智能化控制。系统硬件以STC89C52单片机为核心,搭配双桶结构、红外人体检测、超声波测距、直流电机与蜂鸣器模块;软件采用状态机模型,设计了待机、开盖、等待关盖等五种状态,支持按钮触发开盖、人体离开延时一秒自动关盖、十秒超时保护,以及桶满蜂鸣器与LED报警,PWM控制电机正反转实现稳定开关盖。资源包共43个文件,压缩包大小约642KB,主要包含Keil工程C语言源代码、Proteus仿真工程、AD原理图、PDF说明文档、Hex烧录文件等,涵盖从电路设计、程序编写到仿真验证的完整链条。已有100人学习下载,适合直接作为课设或竞赛底稿,在此基础上修改传感器逻辑或扩展更多分类即可。
1. 分类智能垃圾桶的本质:51单片机如何管好一套机械动作
小区里的四色垃圾桶,本质上靠人脑判断垃圾类别,而把“自动开盖–自动分类–自动复位”这套动作交给单片机来做,考验的就不再是传感器贵不贵,而是资源极其有限的51单片机怎么把一堆外设调度得不出错。这个项目基于STC89C52这级别的51单片机,把超声波测距、TCS3200颜色识别、两路SG90舵机组装成一台能感应开盖、按垃圾袋颜色/金属属性分拣的智能垃圾桶。它的价值在于让你完整走一遍嵌入式小系统闭环:从IO分配、PWM脉冲标定,到状态机设计、实物排错,全在一块MCU上落地。适合刚入门51但想练全链路的学生,也适合做毕设想快速成型再谈扩展的开发者。你不需要一上来啃多高深的理论,按下面的结构把每块拆开做,就能复现一台可用的样机。
2. 硬件选型与电路设计:从元器件清单到IO分配
2.1 元器件清单与选型理由
做这个项目,我不建议一上来就手工焊最小系统板。虽然直接画原理图没有错,但STC89C52这类DIP封装自己焊,虚焊概率不低,烧录失败时你很难分清是电路问题还是程序问题,最后全归到硬件玄学里,白白耗掉一个下午。常见做法是买一块现成的51核心板,带USB转串口、电源稳压和复位按键那种,把精力放在外设连接上。
外设端的模块清单可以按这张表备料:
| 模块 | 型号/规格 | 数量 | 用途 |
|---|---|---|---|
| 主控 | STC89C52RC核心板 | 1 | 逻辑调度 |
| 超声波 | HC-SR04 | 1 | 人体靠近检测 |
| 舵机 | SG90 9g | 2 | 桶盖开合、分拣挡板翻转 |
| 颜色识别 | TCS3200 | 1 | 按袋子颜色分类 |
| 金属接近 | LJ12A3-4-Z/BX | 1 | 检测金属罐,可选 |
| 显示 | LCD1602 | 1 | 显示当前状态 |
| 按键 | 轻触开关 | 3 | 手动分类、复位、校准 |
| 蜂鸣器 | 有源蜂鸣器 | 1 | 完成提示 |
| 电源 | 5V/3A适配器 | 1 | 主控与舵机分开供电 |
选TCS3200而不是摄像头模组,理由很实在:51的资源跑不动图像处理,TCS3200输出的是频率信号,单片机只需要数脉冲,不需要读像素,十几块钱的模块配合遮光罩就可以稳定工作。超声波选HC-SR04,是因为它集成发射、接收和信号调理,Trig一个IO触发、Echo一个IO返回,精度对这个场景完全够。舵机选两路SG90,一路负责桶盖开合,另一路负责投掷后把垃圾从暂存斗推入对应分类仓。如果只做“自动开盖”不要求分类动作,一路舵机也够,但要做完整的分类流程,两路机械自由度更从容。
2.2 IO分配与电路连接要点
IO分配按这张表来,接线和查线都好办:
| 外设 | 引脚 | 说明 |
|---|---|---|
| 超声波 Trig | P2.0 | 输出10us以上触发脉冲 |
| 超声波 Echo | P3.2/INT0 | 接中断口,便于计时 |
| 舵机1 信号 | P1.0 | 桶盖 |
| 舵机2 信号 | P1.1 | 挡板/推杆 |
| TCS3200 S0/S1 | P1.4/P1.5 | 频率缩放选择 |
| TCS3200 S2/S3 | P1.6/P1.7 | 颜色通道选择 |
| TCS3200 OUT | P3.5/T1 | 定时器1计数模式测频 |
| LCD 数据/控制 | P0口 + P2.7 | 显示 |
| 按键 | P3.4/P3.6/P3.7 | 手动分类、复位、校准 |
| 金属接近 | P2.4 | 高电平触发 |
| 蜂鸣器 | P2.3 | 状态提示 |
电路连接上三个习惯很重要。第一,所有模块的地必须和单片机共地,尤其舵机,信号线、电源地、板子地三根不共地,整个输入输出就悬空了,这在实际制作里是“程序没毛病但就是不动”的头号元凶。第二,舵机电源不要从核心板的VCC排针直接拉,SG90启动瞬间能抽走接近800mA电流,核心板上的低压差稳压器扛不住,我会让5V适配器先进入舵机供电排针,再从排针引一路到核心板Vin,两个电源轨共地但不过板载稳压,这样分流干净。第三,Echo引脚输出是5V电平,做3.3V单片机项目需要分压,但51核心板是5V供电,直接连接即可,不用加转换。
2.3 Proteus原理图画法与网络检查
仿真原理图里,电源网络要画清楚。很多Proteus仿真51单片机项目翻车,就翻在Power Sources工具里放了两个5V电源符号,看起来电压都对,实际电源网络没有合并,单片机一个网络、舵机一个网络,仿真时舵机模型根本得不到驱动。正确做法是只用一个VCC符号,所有5V器件都挂在同一个网络名下,GND同理。
器件模型映射方面,单片机选AT89C52,与STC89C52引脚兼容,仿真运行没问题;舵机用Proteus自带的SERVO模型;LCD用LM016L替代,这个模型和1602字符屏行为一致,方便验证显示逻辑。TCS3200在Proteus里有自带模型,S0、S1、S2、S3、OUT引脚的时序和实物基本一致,可以直接拿来做颜色读取仿真。超声波模型如果版本里没有HC-SR04,就用两个虚拟源模拟Trig和Echo时序,或者干脆在仿真阶段把测距函数返回值强制设成固定距离,先验证状态机跳转,传感器性能放到实物阶段再调。仿真的价值在于验证逻辑,不在于验证传感器物理特性,把这两件事分开,你就不容易被仿真牵着走。
2.4 电源与下载电路:先解决能不能跑再谈智能
下载电路大多数现成核心板已经带了CH340串口芯片和冷启动时序,我要提醒的是手动下载的物理顺序。有些散件图清单上画了一个独立下载按钮串在VCC和地之间,按下去整板断电再上电,效果和“先点下载再上电”一样,但手要快,别一按不松就一直复位,STC-ISP那边会一直等待握手,松手太慢会错过时序窗口。如果核心板上没有独立下载开关,手动断电再上电是最稳的,别只按复位键,有些板子复位后CH340还占着串口,握手照样失败。
晶振频率我统一用11.0592MHz,这个频率下9600波特率可以整除,串口打印调试不会累计误差。12MHz也能跑,但波特率有约0.16%偏差,短帧没问题,长帧偶尔乱码,到时候你很难分清是程序bug还是波特率偏差,调试体验很差。电源侧,5V/3A适配器是底线,纯USB供电只有500mA,带两个舵机加一个超声波模块,开盖瞬间电压就跌破4.5V,单片机复位、舵机抖动会轮番出现。
3. 状态机驱动的分类逻辑:距离阈值与舵机PWM的两个关键参数
3.1 为什么不直接排main函数,要绕一道状态机
很多初学者写这个项目时,喜欢把代码排成顺序流程:先测距,距离近就开盖,等三秒,识别颜色,再关盖。听着顺畅,跑起来就出问题——超声波测距函数里有一段几十毫秒的等待,舵机动作又要几十毫秒,蜂鸣器、LCD刷新都挤在同一循环里,按键扫描周期被拖长到几百毫秒,按一下没反应、按两下才执行,整台机器看起来像卡住。
这正是51单片机状态机在这个项目里最值得落地的场景。状态机不是玄学,它让程序明确知道自己当前处于哪一个环节,只在当前状态下执行该执行的事,状态切换靠条件触发,而不是靠延时硬拖。对垃圾桶来说,五个状态就能把业务闭环描述清楚:空闲巡检、开盖等待、投掷倒计时、分类判定、机械复位。每个状态只做一件事,代码结构清晰,加功能也不容易牵一发动全身。
3.2 五态主循环代码:能直接进Keil的骨架
下面这段是工程里的主循环骨架,Keil5建工程的步骤和普通51单片机新建工程一致,C51编译器下选择STC89C52后直接编译运行:
// 五个状态的枚举定义 typedef enum { S_IDLE = 0, // 空闲:超声波周期性巡检 S_OPEN, // 开盖:舵机1转到开盖角度 S_WAIT, // 等待:投掷窗口倒计时 S_JUDGE, // 判定:颜色/金属传感器读值 S_RESET // 复位:舵机回中,回到空闲 } SYS_STATE; SYS_STATE state = S_IDLE; unsigned int wait_cnt = 0; // 投掷窗口倒计时,单位100ms unsigned char lid_done = 0; // 开盖动作完成标志 unsigned char steer_done = 0; // 分拣动作完成标志 void task_loop(void) { switch (state) { case S_IDLE: // 超声波测距,低于阈值说明有人靠近 if (sonar_get_distance_mm() < 400) { state = S_OPEN; servo1_set_angle(45); // 开盖角度按实测标定 } break; case S_OPEN: if (lid_done) { // 舵机到位标志 state = S_WAIT; wait_cnt = 30; // 30 * 100ms = 3秒投掷窗口 } break; case S_WAIT: if (wait_cnt) wait_cnt--; // 由系统时基递减 else state = S_JUDGE; break; case S_JUDGE: classify_and_route(); // 内部读传感器并转舵机2 state = S_RESET; break; case S_RESET: if (steer_done) { servo1_set_angle(90); // 关盖复位 state = S_IDLE; } break; } }这段代码的核心是区分“当前状态在发生什么”和“下一个状态做什么”。注意S_JUDGE没有等任何延时,classify_and_route函数内部不能为了读传感器而长时间阻塞,TCS3200的频率计数窗口最多10ms,足够完成。wt_cnt由系统时基递减,所谓系统时基,是定时器0产生10ms中断,软计数器累加10次后把wait_cnt减一,这样主循环里就不需要delay,按键和LCD刷新不会被拖住。
参数说明:距离阈值400mm是经验值。放在楼道这类人流量小的场景,可以降到300mm,减少路过误触发;放在人流量大的公共区域,建议取400到500mm。wait_cnt=30在100ms时基下就是3秒,这个窗口要大于人的平均投掷动作,又要小于路人停留时间,我实测3秒比较舒服。开盖角度45度是我常用桶盖转轴位置的一个中间值,具体要按你的机械结构重新标定。
3.3 舵机PWM:角度不是算出来的,是标出来的
SG90这类模拟舵机的控制协议是统一的:周期20ms,高电平脉宽0.5ms对应0度,2.5ms对应180度,中间近似线性。但到实物上坑就来了——每一只舵机的零度漂移不一样,有的在0.5ms时已经微微打颤,有的要到0.55ms才安静,用角度公式硬算出来的脉宽不可靠。
我的做法是先写一张脉冲宽度分度表,把常用角度对应的定时器计数直接填进去,不在代码里做乘除换算。以下是用定时器1做PWM的骨架:
// 定时器1产生100us分度中断,系统频率11.0592MHz // 重装初值 65536 - 11059200/12/100 = 0xDC00 // 20ms周期 = 200个分度 #define PWM_0D 5 // 0.5ms -> 实测约0度 #define PWM_90D 15 // 1.5ms -> 中位 #define PWM_180D 25 // 2.5ms -> 实测约180度 unsigned int pwm_high_us; // 当前高电平宽度,单位100us void servo1_set_angle(unsigned char angle) // 0/45/90/180切换 { switch (angle) { case 0: pwm_high_us = PWM_0D + 1; break; case 45: pwm_high_us = PWM_0D + 3; break; case 90: pwm_high_us = PWM_90D; break; default: pwm_high_us = PWM_180D; break; } }这里没有做线性换算,只做了档位映射。表面上不如公式优雅,但实物调试时改的只是一个常量数组,而不是在中断里算浮点。PWM的产生由定时器1中断完成:每100us进一次中断,累加到pwm_high_us时拉低信号,累加到200时重新拉高并且计数器清零。两路舵机可以共用一个周期中断,输出脚在中断里分别处理,P1.0和P1.1互不影响。
每次拿到新舵机,我习惯先通过串口发0度和180度两个指令,确认它在0.5ms时不打颤、2.5ms时不堵转,然后再标定45度和90度。堵转现象是舵机嗡嗡响但臂不动,这是脉宽超出了实际行程导致的,不是舵机坏了,回到限位内就恢复。舵机臂上的螺丝要留一点虚位,拧太死会让扭矩全吃在齿轮箱上,开盖动作显得无力,还容易加速齿轮磨损。
3.4 分类判定:颜色传感器的比值法,不读绝对频率
TCS3200输出的是一串方波,输出频率和入射光强度近似成正比。直接读“红色通道频率是多少”来判断颜色会翻车,因为环境光强弱对绝对频率影响太大,上午十点和下午三点读同一个袋子,频率能差一倍。稳定做法是先标定黑、白两个基准,再算比值。
// TCS3200输出接P3.5,定时器1工作在计数模式 // 10ms窗口内统计外部脉冲个数 unsigned int tcs_read(unsigned char ch) { set_channel(ch); // 按S2/S3选择红绿蓝通道 TH1 = 0; TL1 = 0; // 清零计数 TR1 = 0; // 停止计数 TR1 = 1; // 启动 delay10ms(); // 阻塞10ms,可接受 TR1 = 0; // 停止 return TH1 * 256 + TL1; } // 比值 = (目标频率 - 黑基准) / (白基准 - 黑基准) * 100 unsigned char freq_ratio(unsigned char ch) { unsigned int f = tcs_read(ch); return (unsigned char)((f - black_base[ch]) * 100u / (white_base[ch] - black_base[ch])); }参数要点:S0和S1配置为20%频率缩放,也就是S0=1、S1=0,让输出频率落在单片机容易捕获的范围,10ms窗口内计数值不会溢出,也能明显区分三个颜色通道。白基准校准是把白色纸板放在传感器正前方10mm处,按下校准键,程序把三个通道读数存进white_base数组;黑基准用黑色垃圾袋堵住镜头读一次。判定时红色袋子通常在红色通道比值最高,蓝色袋子在蓝色通道比值最高,取三个比值里的最大者作为分类结果。如果加了金属接近开关,判定逻辑里金属优先级要最高,防止易拉罐表面反光被误判成蓝色或白色袋子。
4. Keil工程到Proteus仿真:可抄作业的完整步骤
4.1 Keil5建工程与烧录文件生成
拿到资源包后,第一步不是打开代码就编译,而是核对Keil的C51支持。Keil5安装流程和51单片机常规流程一致,安装MDK后再补装C51支持包,新建工程时芯片型号选STC89C52;如果型号列表里没有,选ATMEL的AT89C52也可以正常编译,烧录时再到STC-ISP里选STC89C52RC,两边的寄存器定义差异由烧录工具层面覆盖,不影响hex运行。
新建工程时建议拆三个目录:App放主状态机文件,Drv放每个传感器的驱动C文件,Lib放公共头文件和延时函数。这个分法和江科大51单片机笔记里常见的那种单文件套路不一样,好处在后遗症少:后续要换超声波模块或者改舵机驱动时,不需要在几千行的主文件里翻找,直接替换Drv目录下的对应文件。工程配置里两个关键选项:Output页勾选Create HEX File,编译后才生成可烧录的hex;Target页的时钟频率不改也没关系,因为代码没有依赖编译器内置的时钟宏。编译通过后再看警告,C51里常见的“unreachable code”多数是switch-case里break之后还跟了语句,不影响运行,但建议清掉,免得掩盖真正的越界错误。
编译生成hex后,打开STC-ISP,选择串口号和波特率,加载hex文件,点下载按钮后立刻给单片机断电再上电。这个“先点下载再上电”的顺序要练成肌肉记忆,失败时软件会提示“握手失败,请重新上电”,这时候按一次板载复位键多半能补上。STC-ISP程序在STC官网可以找到,官方渠道下载即可,不需要额外付费。
4.2 Proteus仿真:器件映射与固件加载
Proteus仿真51单片机是这个项目最方便先看效果的一步。仿真工程里需要核对四个器件映射:
| 器件 | Proteus模型 | 说明 |
|---|---|---|
| 单片机 | AT89C52 | 与STC89C52引脚兼容 |
| 超声波 | HC-SR04模型或虚拟信号源 | 模型缺失时用虚拟脉冲模拟Echo |
| 舵机 | SERVO | 信号接P1.0,电源接+5V |
| 颜色识别 | TCS3200 | Proteus自带模型 |
仿真里最容易卡住的是舵机模型。Proteus自带的SERVO模型只响应0.5ms到2.5ms脉宽,并且对初始状态有个“转到固定角度然后保持”的设定。如果你直接把P1.0置成高电平想“看舵机动不动”,会发现模型纹丝不动。正确做法是先把虚拟示波器接在P1.0上,确认波形周期20ms、高电平1.5ms左右,再把舵机信号线接上,这时舵机臂会转到中位附近。这其实也是实物调试要过的第一关,PWM波形不对,机械就永远不对。
超声波模型有些版本也需要手动处理。Trig拉高后如果Echo一直没有响应,先看模型的Trig识别电平,部分模型要求5V整电平,P2.0口直接输出高电平理论上没问题,但仿真版本太老会有兼容问题。实在不行就换思路:仿真阶段用一个可调电压源模拟Echo波形,或者干脆把主循环里的sonar_get_distance_mm函数单独摘出来,用串口虚拟终端验证状态跳转。仿真的价值是验证状态机和IO配置,不是测传感器性能,分清楚这一点,就不会在仿真里硬磕好几个小时。
4.3 实物联调:从点亮最小系统到完整流程
实物联调和仿真完全不同,仿真过了不代表硬件能跑,我按三层推进来调试。第一步,只烧最小系统测试程序,串口回显一个固定字符串,波特率9600。能回显说明下载电路、晶振、串口三条链路都对,这是后面所有调试的地基。这套代码在资源包里单独放了一份,只有几十行,排查问题时不掺杂其他外设干扰。
第二步,单独跑舵机标定程序,把两个舵机分别转到0度、90度、180度,听有没有打齿声,看有没有抖动。这一步会暴露最多的硬件问题:舵机供电不足、PWM周期不对、信号线虚接,全在这一层暴露。两个舵机都要单独过一遍,不要只测一路就跳过。
第三步才挂超声波和颜色传感器。超声波先固定距离测三组数,手放在桶盖上方的固定位置,看串口打印的距离值是否稳定,浮动超过3cm就要查供电和接线。颜色传感器先做白基准和黑基准校准,再拿红色袋子测,确认分类结果对得上。每一层都有独立串口打印,不要急着把所有传感器一次性挂上,否则出了问题只能全拔重插,效率极低。
串口打印格式建议统一成单行文本:state=3 dist=230 cls=2 servo=15。state是状态枚举值,dist是超声波距离,cls是分类结果,servo是当前舵机脉宽档位。这样调试时一眼能看出程序卡在哪、传感器读到什么、机械动作在哪个环节掉链子。仿真里的虚拟终端也可以验证同样的打印,波特率与程序一致即可;如果打印粘连,把虚拟终端里RX和TX极性对调再看。
5. 避坑记录:舵机抖动、伪触发与电源乱象
做硬件项目,问题往往不从代码逻辑里来,而是从电源、波形、接线这些容易被当成黑匣子的小地方冒出来。以下五条是这个项目里最典型的坑,每一条我都踩过,按现象、原因、解决三步写清楚。
5.1 舵机“嗡嗡”抖动不动作
现象:上电后舵机臂左右小幅度颤抖,或者发出高频嗡嗡声,用手摸臂还跟着抖,程序里发的0度指令没有效果。
原因:PWM周期不对是最常见的。SG90要求20ms周期,有些例程把周期做成了10ms,舵机内部电路检测脉宽时会把第二个脉冲误认为新指令,于是不断在两个角度之间挣扎。另一个常见原因是供电电压低于4.5V,舵机控制芯片进入欠压状态,伴随核心板LED亮度闪烁。
解决:先量P1.0波形,确认周期在20ms左右。没有示波器,就用串口打印周期常量,把pwm_period宏强制改成20ms再试。供电侧,舵机单独接5V/2A电源,主控和舵机共地但不同电源轨,再在舵机电源两端并一个470uF电解电容。从那以后我每次拿到新舵机,都先干这两件事:量周期、查供电,再谈角度准不准。
5.2 超声波距离乱跳,手没靠近就开盖
现象:串口打印的距离值在几十厘米到二三百厘米之间跳,人站着不动,垃圾桶偶尔自己开盖。
原因:Echo引脚接在普通IO上,用查询方式计时,主循环里同时跑LCD刷新和状态机时,查询延时被拉长,计时读取不准。Echo悬空时引脚噪声也被当成有效回波,纯查询模式下没有边沿锁存,一个毛刺就可能误判距离。
解决:Echo必须接在P3.2/INT0中断口。触发Trig后把外部中断配置为上升沿触发,进中断就启动定时器0开始计数,然后查询Echo引脚变低时停止定时器。STC89C52的外部中断不支持自动锁存双沿,所以结束沿用查询,但查询只在Echo为低时做,不会像纯查询那样把整个计时拖乱。回波高电平持续时间小于100us的丢弃,大于25ms的也丢弃,对应测距超过4.3米,这两条滤波规则写在中断处理里,不影响主循环。距离阈值保持在400mm以内,太远的触发一律不动作。
5.3 下载握手失败,或者烧录一半卡住
现象:STC-ISP下载时前面正常,进度条走到一半卡住,或者板子工作一阵后,再次下载经常提示握手失败。
原因:STC的串口下载依赖复位时序和串口数据的配合。如果板子上没有一键下载电路,手动断电上电时电容放电不彻底,单片机没有真正复位,握手就失败。另外CH340驱动版本过旧会偶发时序问题,表现就是时好时坏。
解决:下载时把单片机电源彻底断开1秒再上电,不要只按复位键,我见过太多只按复位导致反复失败的案例。驱动换成CH340最新版,有必要的话换一根数据线试试,USB线里只接了电源线不接数据线的劣质线在实验室里非常常见。下载线固定好,下载时不动它,跑程序时也不碰它,排除接触不良这个变量。
5.4 颜色识别白天正常、傍晚乱判
现象:下午调试一切正常,晚上灯光下红色袋子被识别成绿色,蓝色袋子识别成黑色。
原因:TCS3200直接受环境光影响,晚上荧光灯的频闪和色温变化让绝对频率读数完全偏移。白天标定的白基准,到晚上已经不对了,比值计算直接溢出或错乱。
解决:给传感器做遮光罩,用黑色吸光海绵或热缩管围一圈,让镜头前只有垃圾袋的反射光。白基准和黑基准要在固定安装位置、固定供电电压下标定,之后不要换角度。判定代码里还要加保护逻辑:三个通道比值全部低于15%时按“无法识别”处理,蜂鸣器响两声提醒,而不是强行归到某一类。把误判变成“未知”,比硬性判错要好得多,这是这个项目里最值得加的一行判断。
5.5 舵机关盖无力,垃圾卡在桶口
现象:开盖正常,但关盖时舵机臂到半途不动,嗡嗡两声又弹回,垃圾袋卡在桶沿。
原因:两路舵机共用同一个电源时,分拣挡板动作和关盖动作时间重叠,电流峰值叠加,瞬间压降跌破4.3V。另一个原因是舵机臂和盖子的连接螺丝拧得过紧,机械阻力变大,SG90扭矩本来就只有1.8kg·cm,阻碍一大就罢工。
解决:在状态机里把两个机械动作错开,S_RESET状态先让舵机2复位,延时300ms后再让舵机1关盖,峰值电流错峰。电源侧换成5V/3A适配器,每个舵机电源引脚直接焊一个100uF高频电容,就近吸收瞬态电流。舵机臂固定时用M2螺丝留一点余量,不要用胶水把舵机臂和旋臂粘死,粘死之后换角度标定只能拆齿轮箱,后悔药都没有。
6. 进阶技巧:五态状态机重构与低功耗两个惯用手法
6.1 为什么我要把线性流程改成状态机
第一次写完这个项目时,代码里一排排delay,功能能跑,但想加一个“异味检测”就整个人都不好了——新功能的检测函数一加进去,原来的开盖时序就被拖乱,按键也失灵。后来我把整个流程重构为五个状态,收益是实打实的:主循环执行时间稳定在几十毫秒内,任何传感器的等待都拆到子状态里,按键扫描和LCD刷新不再被拖住;新加传感器只需要在对应状态下追加一个条件,不需要重排整个main;调试时打印state值,卡在哪一步一目了然,不用对着现象猜逻辑。
这个习惯我从那个项目之后一直保留,不管做矩阵键盘密码锁还是做电子秤,都先画状态转移表再写代码。具体到实现上,就是把S_WAIT这一类带时间的状态单独用系统时基计数驱动,任何状态下主循环里不允许出现超过30ms的阻塞。状态表画清楚,代码只是翻译,后期维护省掉大量翻代码的时间。
6.2 低功耗与掉电唤醒
垃圾桶不一定总放在插座旁边,用电池供电时,STC89C52可以进入掉电模式,待机功耗压到微安级。进入掉电前把所有外设关闭,蜂鸣器停振、LCD背光关掉,然后置位PCON的PD位。唤醒用外部中断,把按键接到P3.2/INT0,配置为低电平触发,按下时唤醒芯片。
void enter_sleep(void) { // 外设全部关闭,再进入掉电模式 P0 = 0x00; P1 = 0x00; P2 = 0x00; LCD_backlight_off(); PCON |= 0x02; // 进入掉电模式,等待外部中断唤醒 // 唤醒后从这里继续执行 state = S_IDLE; system_timer_reset(); }注意掉电模式下定时器停走,唤醒后系统时基要从头累计,否则投掷窗口计时会变短。进入掉电前把state打到S_IDLE,唤醒后外设重新初始化LCD和传感器,再回到正常巡检流程。
说一个我自己的教训:做这个项目时,我一度把状态刷新全写进看门狗喂狗函数里,想着“一边喂狗一边刷新状态”,结果看门狗超时把状态机重置,垃圾桶半夜自己“咔哒”开盖,屋里人吓一跳。后来我把喂狗单独放在空闲态的末尾,其他状态里只喂狗不做事,再用串口打印开机原因码区分上电复位和看门狗复位,再也没有半夜诈尸过。从那以后我每次调带看门狗的项目,都强制先打印一段原因码,搞清楚芯片是被谁唤醒的,再往下查。希望帮到你。
本文还有配套的精品资源,点击获取