ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于STM32F103C8T6的智能垃圾桶设计与传感器抗干扰实践

基于STM32F103C8T6的智能垃圾桶设计与传感器抗干扰实践

简介:这份 PDF 文档围绕基于 STM32F103C8T6 单片机的智能垃圾桶控制系统展开,面向电子信息、自动化及相关专业学生与单片机开发者,可作为课程设计、毕业设计和智能家居小项目开发的参考。内容从系统总体方案入手,串联超声波测距、红外感应、电机驱动、LCD1602 显示、WT588D 语音播报与继电器消毒等模块,讲清自动翻盖、延时关闭、满溢提醒和按键消毒的控制逻辑与软硬件配合方式,对传感器选型、电路连接和程序流程均有较完整的论述。压缩包内共 1 个 PDF 文件,约 1.94MB,为完整论文式技术文档,含摘要、系统设计、模块分析与总结等部分,便于按章节查阅和摘录。该资源已有 7845 人学习,适合需要快速理解智能垃圾桶实现思路、搭建实物原型或撰写相关设计文档的读者参考借鉴。

1. 一个桶盖的响应速度,决定这套东西能不能落地

很多基于 STM32 的智能垃圾桶设计,图纸上画得漂亮,焊完板子一上电就原形毕露:手伸过去盖板纹丝不动,或者盖板开一半又往回缩,像抽搐一样。这不是玄学,是红外阈值、超声时序和电机供电三件事里至少有一样没处理好。手上这份基于 STM32F103C8T6 单片机的智能垃圾桶设计,把红外感应翻盖、HC-SR04 测满溢、LCD1602 显示容量、WT588D 语音提醒、按键消毒这五条链路都跑通了,它解决的是"人不碰桶"这个很具体的问题,适合做课题设计、想练 STRM32 定时器捕获和比较器电路的从业者。它的难点从来不在算法,而在传感器现场标定和干扰抑制,后者才是决定成败的地方。

2. 方案定型:主控与外设怎么配才不打架

选型这件事,我见过太多人先囤一堆模块再回头硬塞程序。正确的顺序是先数引脚、再算电流、最后定接口,顺序反了后面全是返工。

2.1 主控为什么是 C8T6,而不是更便宜的八位机

这套设计要同时干四件事:跑一路输入捕获测超声波回波宽度、扫一路按键并做消抖、刷 LCD1602、按需触发语音芯片。八位机也能做,但输入捕获的精度和定时器数量会非常紧张,超声测距的时间分辨率直接决定厘米级误差。STM32F103C8T6 是 Cortex-M3 内核,主频 72MHz,64KB Flash、20KB SRAM,LQFP48 封装引出 37 个可用 GPIO,带 3 个通用定时器和 2 个高级定时器,其中 TIM2 做输入捕获、TIM3 做按键扫描节拍、TIM4 留给软件延时,资源正好够分配,不宽裕但也不捉襟见肘。

时钟这块有个细节要说明:外部高速晶振配 8MHz,经过 PLL 九倍频得到 72MHz 系统时钟;RTC 用的那颗是 32.768kHz,不是 32.768MHz。原始文档里写成了 32.768MHz,属于笔误,照那个参数买晶振,RTC 必然不走。血泪经验是:BOM 表里的晶振一定要按频率和负载电容两个参数同时核对,只对频率不对负载电容,起振失败照样是黑匣子。

引脚分配我一般写成表贴在工位上,避免改代码时来回翻原理图。

功能引脚模式备注
HC-SR04 TrigPA0推挽输出触发脉冲 ≥10us
HC-SR04 EchoPA1浮空输入复用 TIM2_CH2 输入捕获
红外感应输入PB0上拉输入低电平=检测到物体
电机驱动 IN1~IN4PB4~PB7推挽输出接 ULN2003
LCD1602 D4~D7PA4~PA7推挽输出四线模式省 IO
LCD1602 RS/RW/ENPB10~PB12推挽输出RW 可接地省一根
WT588D DATAPA9推挽输出一线串口
WT588D BUSYPA10浮空输入忙状态指示
消毒按键PC13上拉输入低电平有效
消毒继电器PB1推挽输出高电平吸合

这张表的用法很直接:先按它接完线,再照它写初始化,最后照它排查。任何一处接反都会在第 5 章那些翻车点里体现出来。

2.2 五个模块的取舍逻辑

红外感应用 RPR220 还是成品避障模块。RPR220 是反射式光电传感器,内部带可见光过滤,体积小、便宜,但它输出的是模拟量,必须配 LM393 比较器把它整形成数字电平,阈值靠一只滑动变阻器现场调。好处是阈值你能自己控制,难点是每一台的阈值都不一样,量产时这是负担,单台打样时这是灵活性。如果是批量做,我会换成带电位器的一体化避障模块,省事;如果是要讲清比较器原理,RPR220 更合适。这套资料用的是两路 RPR220 并联再进 LM393,属于后者。

满溢检测用超声还是称重。称重传感器要装桶底,结构改动大,而且换袋、倒垃圾后零点漂移严重;HC-SR04 装在桶盖内侧朝下打,量的是垃圾堆顶到探头的高度,安装简单、免标定零点,代价是塑料袋、纸团这类吸声材质会让回波变弱。所以超声测满在垃圾桶场景是"够用"而不是"精确",后面 3.3 节会给一个防误判的判据。

翻盖驱动用步进还是舵机。ULN2003 驱动 28BYJ-48 减速步进的方案,成本低、扭矩够,缺点是四相八拍占四个 IO,且必须做加减速,否则启停时整个桶身都在抖;舵机方案只占一个 PWM 口,但 180° 舵机扭力普遍偏小,大盖板压不住。这套设计选了步进,那就得在软件里老老实实补一个梯形加减速。

提示音用 WT588D 还是蜂鸣器。蜂鸣器只能"滴滴",讲不清是满了还是消毒中;WT588D 是可以烧录语音段的语音芯片,一线串口发地址就能播指定句子,交互体验提升明显,代价是多一根控制线和一个烧录步骤。

显示用 LCD1602 还是 OLED。1602 字符够用、强光下可读、驱动简单,缺点是占 6 到 10 根 IO,还得单独调对比度电位器。OLED 只占 I2C 两线,但在走廊这种光照变化大的环境里,镜面反射会让人看不清。这套设计用 1602,是因为它要在户外场景下保持可读性。

2.3 供电拓扑:别让电机和单片机抢同一口饭

整机从一路 5V 适配器进来,分成三条支路:5V 直供 ULN2003 和继电器线圈;5V 经 AMS1117-3.3 稳压给 STM32 和 LCD1602 背光限流电阻;HC-SR04 和 WT588D 吃 5V。关键点是电机和继电器的地必须单点汇到电源入口,不能串在单片机的 GND 走线上,否则电机换相的瞬间,地弹会把 MCU 的复位脚拉低,表现就是"一开盖就死机"。

我一般会在 ULN2003 的 COM 端并一颗 1N4148 续流二极管,在电机电源脚就近并 100uF 电解加 0.1uF 瓷片,在 STM32 每个电源脚并 0.1uF。这三个动作加起来不到五毛钱,但能省掉后面两小时的抓瞎。继电器那一路同理,线圈两端必须并续流二极管,触点侧如果驱动的是紫外灯管,还要考虑启动瞬间的浪涌。

提示:调试阶段用可调电源限流到 300mA 再上电,短路时电源会限流而不是冒烟,这个习惯救过我不止一次。

3. 两路感知落地:把模拟量变成可信的数字量

感知层是这套设计里最容易"看起来正常、实际不可用"的部分。红外决定盖板开不开,超声决定语音叫不叫,两路信号都必须先稳定,再去谈逻辑。

3.1 RPR220 加 LM393:阈值不是一个固定数字

RPR220 的接法是发射管串限流电阻接 5V,接收管集电极接 5V、发射极经负载电阻接地,中间取分压点送 LM393 的正输入端;LM393 的负输入端接一只 10k 滑动变阻器分压出的参考电压。没有物体靠近时,接收管收不到反射光,内阻很大,分压点电压高;有手靠近时,反射光让接收管导通,分压点电压被拉低,比较器翻转,输出从高变低,STM32 读到下降沿就知道有人来了。

这里有两个容易忽略的点。第一是回差,单阈值比较在临界距离上会疯狂抖动,盖板就会反复开合。解决办法是在软件侧做时间滤波:连续 N 次采样都是低电平才认定"有人",连续 M 次都是高电平才认定"人走了",N 取 3、M 取 8 是个不错的起点。第二是环境光,RPR220 内部虽然有滤光片,但正对窗户或正对射灯时,红外分量依然会抬高底噪,把阈值顶穿。实机上我一般会让探头略微朝下倾斜 15° 到 20°,避开直射,同时把参考电压调到"手在 20cm 处刚好翻转"的位置,然后用胶固定电位器——不点胶的电位器,运输途中自己就转走了。

调试时别靠猜。拿万用表直流档量比较器输出脚,手在 5cm、10cm、20cm、30cm 各停三秒,记下翻转点,再把这个距离对应的电位器位置锁死。这一步做完,红外这块基本就不会再出问题。

3.2 HC-SR04 的时序与换算:所有误差都来自这 10 微秒

HC-SR04 的工作流程是:Trig 脚收到一个不小于 10us 的高电平,模块内部自动发 8 个 40kHz 的脉冲串,同时 Echo 脚拉高;脉冲遇到障碍物返回被接收后,Echo 拉低。Echo 高电平持续的时间就是声波往返时间,距离按下面这个式子算:

测量距离(cm)= Echo 高电平时间(us)× 0.034 ÷ 2

0.034 是声速 340m/s 换算成 cm/us 的结果,除 2 是因为时间包含了往返。常温下声速其实在 331 到 340m/s 之间随温度变化,20℃ 时约 343m/s,用 0.034 会带来 1% 左右的系统误差,对垃圾桶满溢判断来说完全无所谓,但如果要做毫米级项目,就得补温度补偿。

参数典型值说明
供电5V3.3V 下回波幅度不足,容易被误判
量程2cm ~ 400cm小于 2cm 是盲区,回波和发射混叠
测量角度约 15°安装时要让声锥基本正对垃圾表面
精度约 3mm这是模块标称,实测受表面材质影响更大
最小测量周期60ms周期太短会收到上一次的残余回波

最小周期这条必须重视。很多人把测距放在主循环里狂刷,结果读到一堆乱跳的值,原因就是上一个脉冲串还没衰减完,下一个就发出去了。60ms 是安全线,也就是说超声波每秒最多测 16 次,对垃圾桶完全够用,别贪快。

3.3 安装位置与盲区:把判满的规则定死

桶深假设 36cm,探头装在盖板内侧正中央朝下。第一次上电,空桶状态下读到的距离接近 36cm(减去盖板到探头的几厘米);随着垃圾堆高,读数逐渐变小。判满逻辑不能写成"读数小于 X 就算满",因为桶里扔了个斜插的纸盒,回波可能从盒顶反弹回来,读数突然变小,语音就误报了。

我一般用双条件:连续 5 次采样读数都小于 12cm,且 5 次之间的波动小于 3cm,才判定满溢并触发语音。12cm 这个值对应"垃圾离桶口还剩 12cm",留出了套袋翻边的空间。波动门限是为了过滤掉单次异常回波。另外,探头周围 2cm 内不能有遮挡物,盖板上的加强筋如果正好在声锥里,会形成固定回波,读数永远是一个偏小的常数,这个坑非常隐蔽。

安装角度上,探头轴线尽量垂直于桶底平面。倾斜会让声锥打在内壁上形成多次反射,读数比实际偏小,而且不同高度的垃圾偏小量还不一样,标定都标不准。装配时我用一个直角垫块定位,压紧后再打胶,比目测靠谱得多。

4. 软件骨架:状态机、捕获中断与显示刷新

软件部分的价值不在写多少行,而在把硬件的不确定性挡在逻辑层外面。主循环只做三件事:采数据、跑状态机、刷显示,任何阻塞超过 20ms 的操作都挪进中断或分时任务。

4.1 主程序用一个四状态机收口

系统状态定义为:待机、开盖、满溢报警、消毒。上电初始化后进待机,红外有效沿触发进入开盖,开盖持续 5 秒无人则自动关闭并回到待机;待机期间每 60ms 采一次超声,满足 3.3 节的双条件就进满溢报警,播报语音并把状态锁存,直到超声读数回升到 20cm 以上(说明垃圾被取走)才解锁;任何状态下按下消毒键,先强制关盖,再吸合继电器 30 秒,然后回到待机。

这个状态机的关键约束是"同一时刻只允许一个执行器动作"。开盖过程中不接受消毒指令,消毒过程中不响应红外,否则步进电机和继电器同时动作,电源纹波会把 MCU 拉复位。

/* main.c —— 主循环状态机(标准库,Keil5 MDK) */ typedef enum { ST_IDLE, ST_OPEN, ST_FULL, ST_STERILIZE } sys_state_t; int main(void) { uint16_t dist_cm; sys_state_t state = ST_IDLE; SystemInit(); /* 72MHz */ Delay_Init(); HCSR04_Init(); IR_Init(); Motor_Init(); /* PB4~PB7 + TIM4 节拍 */ LCD1602_Init(); Voice_Init(); Key_Init(); LCD1602_ShowStr(0, 0, "Smart Bin Ready "); while (1) { switch (state) { case ST_IDLE: dist_cm = HCSR04_ReadCm(); /* 内部自带 60ms 节流 */ LCD1602_ShowNum(1, 0, dist_cm); /* 第二行显示实时距离 */ if (IR_Detected()) state = ST_OPEN; if (IsFull(dist_cm)) state = ST_FULL; if (Key_Sterilize()) state = ST_STERILIZE; break; case ST_OPEN: Motor_Open(); /* 梯形加减速开盖 */ if (WaitNoPerson(5000)) /* 5s 无人则关盖 */ { Motor_Close(); state = ST_IDLE; } break; case ST_FULL: Voice_Play(ADDR_FULL); /* 播“垃圾已满,请及时处理” */ LCD1602_ShowStr(1, 0, "FULL! "); if (HCSR04_ReadCm() > 20) state = ST_IDLE; /* 取走垃圾后解锁 */ break; case ST_STERILIZE: Motor_Close(); Relay_On(); /* 继电器吸合,启动消毒 */ Delay_ms(30000); Relay_Off(); state = ST_IDLE; break; } } }

这段代码的逻辑很朴素:状态只在一处被修改,执行器动作集中在分支里,避免"边开盖边判断满溢"这种互相打架的写法。参数上要注意三点:WaitNoPerson(5000)的 5000 是毫秒,改这个值就是改关盖延时,实测 4 到 6 秒体验最好,太短人还没走开盖就关了,太长则白耗电;ADDR_FULL是语音芯片里烧录的地址,必须和烧录时填的一致;满溢解锁门限 20cm 要大于判满门限 12cm,中间留 8cm 的迟滞,防止在临界点反复进出报警。

4.2 超声波捕获:10 微秒触发与 1 微秒计数

测距的精度上限由定时器分辨率决定。把 TIM2 预分频设成 71,72MHz 分频后计数频率正好 1MHz,一个计数就是 1us,对应 0.017cm,远高于模块本身的 3mm 精度,够用。

/* hcsr04.c —— PA0 触发,PA1/TIM2_CH2 捕获回波高电平宽度 */ #include "stm32f10x.h" static volatile uint16_t s_t_rise = 0; /* 上升沿计数值 */ static volatile uint16_t s_width = 0; /* 高电平宽度,单位 us */ static volatile uint8_t s_done = 0; /* 一次测量完成标志 */ void HCSR04_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tb; TIM_ICInitTypeDef ic; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; /* Trig 推挽输出 */ gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); GPIO_ResetBits(GPIOA, GPIO_Pin_0); gpio.GPIO_Pin = GPIO_Pin_1; /* Echo 浮空输入,兼 TIM2_CH2 */ gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); tb.TIM_Prescaler = 71; /* 72MHz/72 = 1MHz,1 计数 = 1us */ tb.TIM_Period = 0xFFFF; /* 65.5ms 溢出,足够测 11m */ tb.TIM_CounterMode = TIM_CounterMode_Up; tb.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tb); ic.TIM_Channel = TIM_Channel_2; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x03; /* 必须开滤波,否则毛刺会误捕获 */ TIM_ICInit(TIM2, &ic); TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); TIM_ITConfig(TIM2, TIM_IT_CC2, ENABLE); nvic.NVIC_IRQChannel = TIM2_IRQn; nvic.NVIC_IRQChannelPreemptionPriority = 1; nvic.NVIC_IRQChannelSubPriority = 1; nvic.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&nvic); TIM_Cmd(TIM2, ENABLE); } /* 捕获中断:上升沿记起点并切下降沿,下降沿算宽度并切回上升沿 */ void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET) { if (s_t_rise == 0) { s_t_rise = TIM_GetCapture2(TIM2); TIM2->CCER |= TIM_CCER_CC2P; /* 改为下降沿触发 */ } else { s_width = TIM_GetCapture2(TIM2) - s_t_rise; s_t_rise = 0; s_done = 1; TIM2->CCER &= ~TIM_CCER_CC2P; /* 改回上升沿触发 */ } TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); } } /* 对外接口:返回厘米值,0 表示超时无回波 */ uint16_t HCSR04_ReadCm(void) { uint32_t guard = 0; s_done = 0; GPIO_SetBits(GPIOA, GPIO_Pin_0); for (guard = 0; guard < 72; guard++) __NOP(); /* 约 10us 高电平 */ GPIO_ResetBits(GPIOA, GPIO_Pin_0); guard = 0; while (!s_done && ++guard < 20000) __NOP(); /* 约 8ms 超时等待 */ if (!s_done) return 0; return (uint16_t)((uint32_t)s_width * 17 / 1000); /* us*0.034/2 ≈ us*0.017 */ }

代码里有三个参数决定了它能不能用。TIM_ICFilter = 0x03是输入滤波,不写这一行,杜邦线一长就会捕获到几十纳秒的毛刺,读出来的距离全是几十厘米的乱数。触发脉冲那段用__NOP()空转大约 10us,这是"至少 10us",短了模块不认,长了也没事,但别用Delay_ms(1)这类毫秒级延时去凑,容易把主循环拖慢。最后的超时等待 20000 次循环,是防止桶里没东西、回波彻底丢失时程序卡死在 while 里,返回 0 让上层自己决定怎么处理——状态机里对 0 值应当直接忽略,而不是当成"很近"。

4.3 语音播报与 1602 刷新:两条慢速链路

WT588D 的一线串口时序是这么个意思:DATA 脚平时保持高电平,先拉低 5ms 作为起始,然后按位发送 8 位数据,每位用不同的高电平宽度区分 0 和 1,发完再把 DATA 拉高。具体脉宽各批次手册略有差异,我一般先用示波器抓一次原厂遥控信号,量出高电平宽度再写进延时表,比照着手册猜靠谱。

/* voice.c —— WT588D 一线串口发一个字节,位宽需按手头模块手册标定 */ #define VOICE_DATA_H GPIO_SetBits(GPIOA, GPIO_Pin_9) #define VOICE_DATA_L GPIO_ResetBits(GPIOA, GPIO_Pin_9) #define W_ONE_US 1200 /* “1”的高电平宽度,单位 us */ #define W_ZERO_US 400 /* “0”的高电平宽度,单位 us */ #define W_LOW_US 800 /* 每位之间的低电平 */ void Voice_SendByte(uint8_t data) { uint8_t i; VOICE_DATA_L; /* 起始位,低电平 5ms */ Delay_us(5000); for (i = 0; i < 8; i++) /* 低位先发 */ { VOICE_DATA_H; Delay_us((data & 0x01) ? W_ONE_US : W_ZERO_US); VOICE_DATA_L; Delay_us(W_LOW_US); data >>= 1; } VOICE_DATA_H; /* 回到空闲高电平 */ } /* 播报指定地址的语音段 */ void Voice_Play(uint8_t addr) { if (Voice_IsBusy()) return; /* BUSY 为高说明正在播,直接丢帧防止串音 */ Voice_SendByte(addr); }

这段代码的关键不在逻辑,而在三个延时宏。它们和模块主频、晶振批次强相关,同型号换一批货可能需要微调 ±100us。BUSY 判断是必须的,语音芯片在播放时收到新指令会打断当前语音,表现就是"垃圾已……已满……",很难听。

LCD1602 用四线模式,只占 6 根 IO,刷新频率控制在 200ms 一次就够。刷太快会让整屏闪烁,而且每次写指令都要等忙标志,主循环会被拖住。

/* lcd1602.c —— 四线模式写一个字节,RS=PB10 RW=PB11 EN=PB12 */ static void LCD_WriteNibble(uint8_t nib, uint8_t rs) { GPIO_WriteBit(GPIOB, GPIO_Pin_10, rs ? Bit_SET : Bit_RESET); GPIO_ResetBits(GPIOB, GPIO_Pin_11); /* RW=0 只写 */ GPIO_WriteBit(GPIOA, GPIO_Pin_4, (nib & 0x01) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOA, GPIO_Pin_5, (nib & 0x02) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOA, GPIO_Pin_6, (nib & 0x04) ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOA, GPIO_Pin_7, (nib & 0x08) ? Bit_SET : Bit_RESET); GPIO_SetBits(GPIOB, GPIO_Pin_12); /* EN 上升沿锁存 */ Delay_us(2); GPIO_ResetBits(GPIOB, GPIO_Pin_12); Delay_us(50); /* 等内部处理,不能省 */ } void LCD_WriteByte(uint8_t dat, uint8_t rs) { LCD_WriteNibble(dat >> 4, rs); /* 先高四位 */ LCD_WriteNibble(dat & 0x0F, rs); /* 再低四位 */ }

Delay_us(50)这一行是很多人省掉的地方,省了就会出现"偶尔丢字符""上电花屏",因为 1602 内部执行指令需要时间,四线模式还要两次锁存。这种问题不会稳定复现,最难查。

5. 避坑与排查:这些翻车点我替你踩过

5.1 电气层:复位、抖动与误触发

现象一:手一靠近,盖板开到位的同时单片机重启,屏幕回到初始界面。原因基本可以锁定在电机换相时的电流冲击。ULN2003 驱动步进电机,四相轮流导通,每次切换都会在电源线上产生尖峰,如果电机和 MCU 共用一条细地线,地电位抬升会把 MCU 的 NRST 拉到阈值以下。解决分三步:电机电源单独从电源入口取,地线单点汇合;ULN2003 的 COM 端并 1N4148 续流;MCU 的 NRST 脚并 0.1uF 到地。做完这三步还没好,就量一下 5V 轨在开盖瞬间跌到多少,跌到 4.3V 以下说明电源功率不够,换 2A 的适配器。

现象二:盖板开合时"咔咔"响,走一半停住,或者丢步。28BYJ-48 是减速步进,启动频率上不去。如果代码里直接连续发脉冲,第一拍就丢步,表现就是原地抖。正确做法是梯形加减速:前 20 步每步间隔从 8ms 递减到 2ms,匀速段保持 2ms,末尾 20 步再递增回去。这个改动只多十几行代码,但盖板的动作质感完全不同。另外,步进电机的四相线序接错也会抖,红线和各相的顺序要按说明书对,接反了电机只会原地振动不转。

现象三:白天好好的,晚上灯一开,桶盖自己反复开合。红外阈值被环境光顶穿了。RPR220 虽然有滤光片,但白炽灯和射灯的红外分量很足。解决方式是软件时间滤波加物理遮光:连续 3 次采样为低才认定有人,同时给探头加一个 3mm 长的黑色热缩管做遮光筒,只留正向视场。这个改动成本几毛钱,效果比调电位器明显得多。

现象四:继电器一吸合,LCD1602 显示乱码。继电器线圈是感性负载,断电瞬间的反向电动势会顺着控制线窜回 MCU 的 IO。除了线圈并续流二极管,控制线最好走远离电机的路径,必要时在继电器驱动三极管的基极串一个 1k 电阻限流。乱码还有一个可能原因是 1602 的 RW 脚悬空,飞线时忘了接,读操作时序错乱,同样表现为随机花屏。

5.2 逻辑层:误报满、盖板弹跳与语音串音

现象五:桶是空的,语音却一直喊"垃圾已满"。先看探头是不是被盖板加强筋挡住了,这种情况读数会固定在一个偏小的常数上。用串口把每次测距值打出来,如果一直是 9cm 左右不动,就是固定回波。其次是量一下供电,HC-SR04 在 3.3V 下工作,回波脉宽会变窄甚至丢失,HCSR04_ReadCm返回 0,如果上层把 0 当成"很近"处理,就会误判满溢。所以判满函数里必须显式排除 0 值和大于量程的值。

现象六:人站在桶前不动,盖板开了关、关了开。红外输出在临界距离上抖动,或者关盖延时太短。检查顺序是:先把红外判定改成计数器滤波,再看WaitNoPerson的延时值是不是只有 1 到 2 秒。我一般会把红外灵敏度调到"手在 20cm 处触发",比调到最灵敏要稳得多,因为最灵敏意味着人走过走廊都会被误触发。

现象七:语音播报只播了半句就断,或者干脆不响。两个原因。一是 BUSY 判断缺失,主循环每 60ms 就发一次播放指令,芯片被反复打断;二是 DATA 线的位宽和模块不匹配,发出去的数据被芯片判成无效帧。排查方法是先断开 MCU,用按键手动触发一次语音,确认模块本身和喇叭没问题,再去查时序。这个"先分离故障域"的习惯能省掉大量时间。

现象八:消毒键按一下,继电器吸合好几次。按键抖动,加上消毒流程里没有做状态锁。机械按键的抖动时间通常在 5 到 20ms,软件上要 20ms 消抖;同时进入消毒状态后应当屏蔽按键输入,直到流程结束。另外,消毒用的紫外灯管不能在人靠近时点亮,这一步必须在硬件上和软件上双重保证:软件上消毒前强制关盖,硬件上把灯管装在盖板内侧并有行程开关串联。

6. 验证方法:把"能用"变成几个能测的数

到这一步,功能都通了,但"能用"和"稳定"之间还差一轮量化验证。我一般不看"感觉还行",只看下面这几个数。

指标测试方法合格线
翻盖响应时间手进入检测区到盖板完全抬起,秒表或高速摄像≤1.2s
关盖延时误差手离开到盖板启动关闭5s±0.5s
满溢判定一致性桶内放不同形状垃圾 20 次误报 ≤1 次
连续开合寿命自动循环 200 次无丢步、无复位
待机电流万用表串在电源入口≤80mA(含屏背光)

翻盖响应时间这项,最容易暴露问题。如果测出来超过 1.5 秒,八成是超声测距阻塞了主循环,因为HCSR04_ReadCm里有最长 8ms 的等待,加上 60ms 的节流,本身不该有明显延迟;真正的元凶往往是 LCD 刷新太频繁,或者语音播报时用了死延时。把所有延时都改成状态机分时之后,这个数会明显改善。

满溢一致性测试有个土办法:分别用纸团、塑料袋、平整的纸盒三种填充物各测若干次,记录误报率。塑料袋最容易被漏检,因为它吸声,回波弱,读数会偏大,表现为"明明满了却不报警"。如果这一项不过关,就把判满门限从 12cm 提高到 15cm,用一点余量换稳定性,这个取舍在实际产品里很常见。

还有一项容易被忽略:断电重启后的状态恢复。消毒进行到一半断电,重新上电后继电器必须处于断开状态,盖板必须处于关闭状态。这要求在初始化函数里先把继电器和电机引脚全部置为安全电平,再去初始化外设。没有这一步,上电瞬间继电器可能吸合,紫外灯直接亮起来,是很危险的事情。

踩过最疼的一次坑,是整机装配完之后才发现超声探头被盖板的塑料卡扣挡了两厘米,空桶读数永远是 11cm,语音从早叫到晚。拆开重新定位花了一晚上。从那以后我每次装探头,都会先不装盖板单独跑一遍测距,把空桶、半满、满溢三个状态的距离值全部记在纸上,装完再复测一遍,两次数据对得上才算过。这个习惯后来帮我拦下过好几次结构干涉的问题,值得你也在自己的板子上强制走一遍。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进