简介这是一份基于 STM32 的智能垃圾桶设计资源面向物联网项目开发者、嵌入式学习者和高校课程设计人群可支撑从传感器选型到软硬件联调的完整流程。智能垃圾桶是物联网技术在生活中的典型应用方案重点包括红外传感器实现无接触开盖LED 屏实时显示状态温湿度传感器采集环境数据称重传感器触发超重报警气体传感器识别异味与可燃气体。软件部分采用 C/C 与 STM32 标准库或 HAL 框架涵盖外设驱动、数据处理和无线通信扩展思路。资源包为 zip 压缩包整体约 333KB文件类型明细未单独列出适合快速下载与二次修改。目前已有 6885 人学习下载尤其适合需要完成电子设计课题或入门智能硬件实战的读者借助其中的代码结构、传感器接口和控制逻辑可快速搭建出具备无接触投放、环境监测和满载提醒能力的智能垃圾桶原型。1. 基于STM32的智能垃圾桶到底在解决什么问题市区道路旁每天被清运车拉走的垃圾桶相当一部分并没有装满真正满溢、异味外溢的桶又常常因为没人巡到而滞留很久。智能垃圾桶要解决的不是“自动翻盖”这个表面动作而是让“桶内有多少垃圾、什么时候需要清运、设备是否正常”变成可实时查看的数据。STM32在这类场景里几乎是标准答案外设齐全、芯片生态成熟、低功耗档位完整社区能查到从传感器驱动到云端接入的完整参考。C负责HAL库和寄存器操作C负责对象封装和状态机分层两层混编完全可行。适合毕设、物联网竞赛以及准备做小批量试产的人读完能拿到一条从硬件选型到现场运行的落地路径。2. 智能垃圾桶的硬件选型与传感器原理从F103到L071怎么选第一个要决定的不是单片机型号而是传感器组合。传感器决定结构设计、电池容量和误报率也决定了整个软件状态机怎么写。2.1 满溢检测的传感器选型与量程精度2.1.1 满溢检测到底用超声波还是称重绝大多数智能垃圾桶设计会用超声波测距把探头固定在桶盖上沿朝下测垃圾表面到探头的距离。HC-SR04是最常见的选择量程在2cm到400cm精度标称±3mm实际受被探测面影响很大对于平整的纸箱和塑料袋读数稳定遇到蓬松织物或者泡沫超声会被吸收回波衰减明显。另一个选项是称重方案用HX711配合应变式压力传感器一次称量就能知道垃圾重量不会骗人。缺点是机械结构复杂垃圾桶要有承重支架传感器要承受偏心载荷投递过程中还容易跳动。如果按真实垃圾桶尺寸来看桶深50cm左右超声波测距量程只需覆盖10~100cm。盲区是关键垃圾填满到距探头很近时HC-SR04会误读为一个随机小距离所以在软件里要设置“测量距离小于3cm视为无效”的规则或者在结构上把探头埋在保护管内。2.1.2 开盖触发、告警与环境的几个坑开盖触发用PIR人体红外模块静态电流约15μA贴着桶壁安装时要用热缩管把菲涅尔透镜周围包住避免桶身反光造成误触发。户外场景建议换成JSN-SR04T这类防水超声波探头探头部与主板分离可以通过开孔安装但它的响应时间比HC-SR04慢通常一次测量要几十毫秒所以不适合用于快速连续测距。既然涉及舵机开门还要预留蜂鸣器和LED户外亮度传感器可以自动切换夜间指示灯但这部分并非必需。以下是常用传感器对比表方便在初期划定范围。传感器检测方式量程/参数工作功耗主要坑点HC-SR04超声回波2~400cm约10mA探头不耐水汽软垃圾吸音JSN-SR04T防水超声20~600cm约10mA响应慢探头线带干扰HX711应变片称重5/10/20kg可选约2mA偏心载荷、震动、温漂PIR红外模块人体红外感应角约100°约15μA红外被衣物遮挡时不触发这四种组合足够覆盖绝大多数毕业设计和试产需求。错误做法是把超声波、称重、红外全部堆上去然后每个传感器都调不好按产品定义取舍选两个主传感器反而更稳定。2.2 STM32最小系统自制的晶振电容计算与启动配置既然是自己做板子就要自己算晶振电容。HSE用无源8MHz晶振时负载电容不匹配会导致不起振、频率偏或者批量性死机。经验公式是C_L (C1 × C2) / (C1 C2) C_stray当两个匹配电容相等即C1 C2 Cx时Cx ≈ 2 × (C_L - C_stray)。C_stray是PCB走线和引脚寄生电容一般取3~5pF。若晶振规格书上给出C_L 16pF取C_stray 4pF则Cx 24pF工程上22pF或24pF都能接受。板子空焊、走线过长电容稍微匹配不上并不致命真正致命的是HSE Ready标志永远不置位这时候应先查晶振两脚波形而不是怀疑固件。时钟树配置建议HSE选8MHzPLL Source HSEPLLM8PLLN72PLLP2主频72MHz。生成代码后先打印SystemCoreClock确认等于72000000再继续做外设。对“STM32芯片包安装”和“Keil5兼容C51和STM32安装”这类问题只需要记住MDK打开工程时按Pack Installer安装对应型号的DFPC51和STM32两个工具链使用不同目录互不干扰。样机从F103换到L071时在CubeMX里Change Device重新生成工程外设映射会随芯片改变大部分HAL代码可复用但必须逐项检查引脚复用和中断向量。2.3 引脚分配建议与调试口预留下面是一张适合STM32F103C8T6加两路串口、一路PWM、一路输入捕获的引脚分配表。功能引脚外设/模式说明超声波TrigPA0GPIO Output10μs高电平触发超声波EchoPA1TIM2_CH1 Input Capture输入捕获测脉宽5V需分压到3.3V调试串口TX/RXPA9/PA10USART1115200 8N1日志输出WiFi/4G模块PA2/PA3USART2AT指令交互默认115200舵机PWMPA8TIM1_CH150Hz周期0.5~2.5ms脉宽蜂鸣器PB0GPIO Output低电平驱动PIR人体传感器PB1GPIO EXTI1上升沿中断唤醒HX711 DOUT/SCKPB4/PB5GPIO Output软件时序读取SWD调试PA13/PA14SWDIO/SWCLK下载和串口无关Echo引脚要注意HC-SR04工作在5V逻辑回波高电平也是5V直接进STM32的PA1会长期超压。分压电阻取1kΩ和2kΩ串联把电压降到3.3V左右。PIR模块一般输出3.3V或5V接PB1前确认模块供电电压。HX711的DOUT是推挽输出可以接受3.3V读取不必额外处理。3. 用C/C搭建STM32智能垃圾桶工程CubeMX初始化与C混合编译3.1 CubeMX生成工程时的时钟、外设配置与芯片支持包先用STM32CubeMX建工程芯片选STM32F103C8T6调试接口选SWD。RCC里HSE选Crystal/Ceramic Resonator时钟树按2.2节设置到72MHz。GPIO部分配置PA0、PA1、PB0、PB1等PA1映射到TIM2_CH1用于输入捕获PA8映射到TIM1_CH1用于PWM这样GPIO设置会自动调整为复用模式。USART1设置为1152008位数据无校验1位停止位使能中断或DMA。如果后期想用低功耗唤醒尽量把串口挂在支持LPUART的引脚上或者选择支持从STOP模式唤醒的USART。生成代码时Toolchain选MDK-ARM打开Keil前先通过Pack Installer安装STMicroelectronics STM32F1xx Device Support Package否则工程会报“No STM32 target”。Keil5同时装过C51和STM32的情况很常见两者编译器不同安装路径分开就没有冲突。工程里如果遇到编译能过、下载时报“RDDI-DAP Error”通常不是软件问题而是SWDIO接线太长或者目标板供电不稳。3.1.1 Keil里最容易踩的配置顺序推荐顺序是先配置时钟树再配置外设最后生成代码。如果先把外设配好再改时钟HAL库的SystemClock_Config会被CubeMX重新生成之前手工改过的寄存器配置容易丢。生成完工程后不要直接从main.c开始写先烧一个LED闪烁或者USART打印锤实最小系统。3.2 Keil、STM32CubeIDE与VSCodeCMake的C支持差异很多STM32教程默认用C写但标题既然带了C/C它真正的价值在于把硬件操作封装成对象之后状态机和上层业务会清晰很多。开发环境对C的支持差别比较大。工作流C标准适合场景我一般怎么选Keil MDKC11可用传统量产、同事习惯Keil快速验证HAL库行为STM32CubeIDEGCC C17个人开源、混合编译项目智能垃圾桶默认首选VSCode CMake arm-none-eabi-gccC17命令行党、CI集成有自动化构建需求时CubeIDE默认生成的工程是C语言把main.c改成main.cpp之后HAL库里的stm32f1xx_hal.h等头文件需要在cpp里编译其中C函数声明必须用extern C包裹。CMSIS 4.5以后已经使用__STATIC_INLINE等兼容写法大部分头文件可以直接在C下包含但HAL库源码本身还是C链接时由编译器按头文件的声明方式区分。因此最稳妥的做法是.c文件保留给HAL和启动代码自己的业务逻辑写在.cpp文件调用C接口的地方统一加extern C。VSCode配合CMake方案最大的成本是启动文件和链接脚本都要自己配好处是可以用C17里更现代的容器和模板。智能垃圾桶这种外设不多、代码量几千行的项目CubeIDE在开发和调试上综合体验更好。3.2.1 三种工作流如何迁移从Keil工程迁移到CubeIDE不需要重写代码只要在CubeMX里选Toolchain为STM32CubeIDE重新生成一次工程把自写源文件拷进Src目录再处理一下头文件路径。VSCode方案建议先让CubeIDE生成CMakeLists.txt再用这个文件接入CMake不要手工从零写链接脚本。3.3 用C把传感器封装成对象回调转发与模板泛型一个值得抄写的模式是把传感器定义成抽象基类让上层代码面向接口而不是面向引脚。// sensor.hpp #pragma once #include stm32f1xx_hal.h class DistanceSensor { public: virtual ~DistanceSensor() default; virtual float ReadDistanceCm() 0; virtual void Recalibrate(float offset) 0; };// ultrasonic.cpp #include ultrasonic.hpp Ultrasonic::Ultrasonic(GPIO_TypeDef* trig_port, uint16_t trig_pin, TIM_HandleTypeDef* htim) : trig_port_(trig_port), trig_pin_(trig_pin), htim_(htim) {} void Ultrasonic::TriggerPulse() { HAL_GPIO_WritePin(trig_port_, trig_pin_, GPIO_PIN_SET); delay_us(10); // 自实现微秒延时可用DWT或SysTick HAL_GPIO_WritePin(trig_port_, trig_pin_, GPIO_PIN_RESET); } float Ultrasonic::ReadDistanceCm() { float speed_mm_us (331.4f 0.606f * temp_c_) / 1000.0f; return (echo_time_us_ * speed_mm_us) / 2.0f / 10.0f; }构造时传入GPIO端口和定时器句柄而不是把HAL句柄作为全局变量到处引用。构造函数在C里不要调用HAL外设初始化因为静态对象的构造顺序不确定应当由main.c先完成HAL_Init和所有外设的配置再调用一个app_init函数显式构造传感器对象。中断回调转发是另一个容易出现链接错误的点。HAL回调是弱符号直接放到.cpp里会因C名字修饰导致回调不被覆盖必须包一层。// irq_router.cpp extern C void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef* htim) { if (htim-Instance TIM2) { UltrasonicOnCapture(htim); } }UltrasonicOnCapture是可以重载的C函数由主程序注册。回调里只做时间戳保存和标志位置位不做滤波、不做浮点运算、不做串口打印。如果想把引脚号作为编译期参数减少运行时分支可以写成模板。template GPIO_TypeDef* Port, uint16_t Pin inline void SetPinActive(bool active) { HAL_GPIO_WritePin(Port, Pin, active ? GPIO_PIN_SET : GPIO_PIN_RESET); }模板在O2优化下会内联与手写寄存器操作的代码没有差别。4. STM32智能垃圾桶核心功能实现超声波测距、满溢判断与无线告警4.1 超声波测距公式与温度补偿超声波从发射到接收的时间要除以2才是单程距离。声音在空气中的速度随温度变化按20°C约343m/s计算即0.343mm/μs很多代码直接取0.34mm/μs误差在1%以内。温度每升高10°C速度大约增加6m/s户外冬天和夏天之间可能差几十毫米。补偿公式为距离(mm) 单程时间(μs) × (331.4 0.606 × T) / 1000使用TIM2输入捕获的简化代码如下。把Echo上升沿和下降沿分别捕获在下降沿中断里读取计数器值就是高电平持续时间。static volatile uint32_t echo_us 0; static uint8_t echo_wait_falling 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef* htim) { if (htim-Instance ! TIM2) return; if (echo_wait_falling 0) { __HAL_TIM_SET_CAPTUREPOLARITY(htim2, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); __HAL_TIM_SET_COUNTER(htim2, 0); echo_wait_falling 1; } else { echo_us HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_1); echo_wait_falling 0; } }这段代码需要把TIM2计数器配置为1MHz计数。若PSC71在72MHz时钟下预分频后正好是1MHz计数器每μs加1echo_us直接就是微秒数。每次发Trig之前要把捕获极性设回上升沿、清echo_wait_falling否则上一轮回波没结束时状态会错。两次测量间隔不小于100ms给回波留足结束时间。4.1.1 输入捕获模式的边界与防呆Echo高电平最长约38ms对应6.5米左右。输入捕获定时器如果周期设成65535且计数频率1MHz65.535ms就会溢出垃圾桶场景够用。如果测量目标超过4米建议使能溢出中断并在溢出时直接判定“无回波”。4.2 舵机开盖状态机与PWM参数舵机控制使用50Hz PWM即20ms周期脉宽1.0ms对应0°2.0ms对应180°。SG90这类舵机要避免在开机瞬间上电否则会在中间位置抖动并拉低系统电压。上电时先把PWM脉宽设到0°对应的1.0ms等200ms后再发送动作命令。MG995扭矩大但静态电流高垃圾桶开盖扭矩要求不高时SG90足够。软件上不要用延时去等待舵机到位改成状态机更合适。状态进入条件行为退出条件IDLE初始化/PIR无触发低功耗待机停止PWMPIR中断OPENPIR上升沿舵机旋转到开盖角开始计时开盖完成后保持5秒CLOSE保持时间到舵机回0°到位后回IDLEFULL_ALERT距离连续低于阈值蜂鸣器循环禁止投递人工复位或远程清空核心代码片段void LidController::Run() { switch (state_) { case State::IDLE: if (pir_irq_) { servo_.SetAngle(open_angle_); state_ State::OPEN; opened_ms_ NowMs(); } break; case State::OPEN: if (full_alert_) { servo_.SetAngle(0); state_ State::FULL_ALERT; } else if (NowMs() - opened_ms_ keep_open_ms_) { servo_.SetAngle(0); state_ State::CLOSE; } break; case State::CLOSE: if (servo_.AtTarget()) state_ State::IDLE; break; default: break; } }状态机不阻塞主循环每50ms调用一次Run()即可。NowMs()直接用HAL_GetTick()不要在中断里做状态迁移只置标志。4.3 满溢阈值判定滑动窗口与连续确认满溢判定不能只量一次距离因为有人投递时手臂划过探头也会产生“已满”假象。常见做法是维护一个长度为N的滑动均值数组再结合连续确认。static float history[5] {0}; static uint8_t index 0; float PushAndFilter(float sample) { history[index] sample; index (index 1) % 5; float sum 0; for (uint8_t i 0; i 5; i) sum history[i]; return sum / 5; }滑动均值对瞬时毛刺有抑制作用但滤不掉一次持续1~2秒的误投递所以还要结合“连续判定”逻辑滤波后距离低于FULL_THRESH_CM连续3次才进入满溢状态一旦距离回升到阈值以上2cm立即清除累计次数。阈值按桶深80%计算比如桶深50cm剩余有效距离约45cm满溢阈值取15~20cm即可。阈值太接近探头会被垃圾表面不规则尖峰干扰。4.4 无线告警USART2、ESP8266与MQTT透传数据上报是智能垃圾桶与普通垃圾桶的区别所在。常见路线是STM32发送AT指令由ESP8266完成连接和MQTT透传下面指令在多数带MQTT透传的AT固件上可用。ATCWMODE1 ATCWJAPSSID,PASSWORD ATMQTTUSERCFG0,1,clientid,user,pass,0,0, ATMQTTCONN0,broker.example.com,1883,1 ATMQTTPUB0,device/trash/001,1,1,{\level\:82,\temp_c\:25.3,\bat_mv\:3720}broker.example.com是占位替换成自己平台的接入点。第四条指令的retain位设置为1Broker会保留这条消息后台新订阅者上线就能马上看到当前垃圾桶状态。STM32侧只需要调用HAL_UART_Transmit发送AT命令并解析模块返回的OK/ERROR不要在STM32里实现MQTT协议栈。上报策略按事件和心跳结合心跳每5分钟上报一次level满溢、打开、关闭、电量低等事件立即上报。满溢事件里带上level:100后台收到后生成清运工单。串口日志独立放在USART1与USART2的AT数据链路分开。5. STM32智能垃圾桶低功耗设计如何靠两节18650跑一个月5.1 低功耗模式选型与唤醒源智能垃圾桶的一个重要指标是电池续航。STM32三种典型模式功耗差异很大。模式典型电流唤醒源唤醒后RAM备注SLEEP1mA~几十mA任意中断保留时钟未关闭功耗高STOP10~30μA外部中断/RTC/LPUART保留恢复后时钟需重新配置STANDBY约2μARTC/复位/唤醒引脚丢失程序重新从头执行方案逻辑是PIR需要24小时待命但STM32不一定需要。把PIR接到EXTI引脚STM32常态停在STOP模式PIR上升沿一到就唤醒整个系统。RTC闹钟作为第二唤醒源每隔几分钟唤醒一次测一次桶内距离用于补报数据和检测异常。进入STOP模式前做3件事停掉所有非必要外设关闭PWM输出把外部模块断电。void EnterStopMode(void) { HAL_TIM_PWM_Stop(htim1, TIM_CHANNEL_1); HAL_UART_DeInit(huart2); WiFi_Module_PowerOff(); // 通过MOS管切断 Servo_PowerOff(); __HAL_RCC_PWR_CLK_ENABLE(); HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后必须恢复时钟 SystemClock_Config(); HAL_ResumeTick(); }HAL_PWR_EnterSTOPMode把CPU和大部分外设时钟关掉RAM保持供电。唤醒后PLL恢复之前如果HSE是PLL源不是每个芯片都能一次成功恢复所以唤醒后要调SystemClock_Config并在配置失败后加一个降级到HSI的逻辑保证设备不会因振荡器冷启动失败而死机。HAL_SuspendTick()和HAL_ResumeTick()之间不要调用阻塞型HAL外设函数。5.2 外设掉电顺序与GPIO电平设计低功耗设计里最容易忽略的是GPIO悬空。进入STOP模式前所有接外部传感器的引脚都要是确定的电平USART2接到ESP8266如果模块供电已切断RX悬空时可能通过内部保护二极管反向倒灌电流。解决方法是配置为推挽输出低电平或接10kΩ下拉电阻后再关闭外设。蜂鸣器也一样保持低电平关闭状态不要用高电平关闭的接法。舵机是隐性耗电大户。SG90在没收到PWM信号时电机会试图保持最后位置耗电可达几十毫安。如果它的电源和STM32共用3.3V整机睡眠电流会非常难看。正确做法是让PWM先停在0°脉宽再用一个N-MOS管或负载开关断开舵机电源。开盖时先上电等200ms稳定再输出目标角度。5.2.1 常见倒灌路径排查实测时如果STOP电流远高于数据手册值先把所有外设排线拔掉看电流是否回落。如果回落逐一插回每插一个就复测一次。最容易中招的三个位置是USART_RX到WiFi模块、Echo到超声波、舵机信号线没通过三极管隔离。5.3 供电链路与一天的功耗预算供电链路建议用两节18650并联通过DC-DC降到5V再经过LDO降到3.3V给STM32和其他外设。DC-DC的效率比LDO在高压差时高很多放电曲线末端也能维持稳定的3.3V缺点是纹波稍大如果用到HX711要留意DC-DC纹波是否进入称重信号。一天的功耗可以这样估算待机35μA持续24小时约0.84mAh超声测量每次10ms、10mA按每天500次计算约0.014mAh开盖每次1秒、PWM输出平均8mA每天200次约0.44mAh上报每次500ms、模块50mA按20次算约0.14mAh。合计加上待机电流约1.4mAh/天。两节18650按4000mAh、70%可用容量计算理论续航超过两年所以真正的矛盾不在续航而是WiFi模块待机漏电和电池低温自放电。6. 校准、调试与远程运维把STM32智能垃圾桶样机变成可维护产品6.1 超声波三步标定与线性插值把垃圾桶放在水平地面用纸箱等已知高度的块状物在桶内建立3个基准距离比如20cm、40cm、60cm。每个基准距离采20次取中位数填入校准表。typedef struct { float raw_cm; float true_cm; } CalPoint; static const CalPoint cal_table[] { {19.8f, 20.0f}, {41.5f, 40.0f}, {63.2f, 60.0f}, }; float ApplyCalibration(float raw_cm) { if (raw_cm cal_table[0].raw_cm) { return raw_cm (cal_table[0].true_cm - cal_table[0].raw_cm); } for (uint8_t i 1; i 3; i) { if (raw_cm cal_table[i].raw_cm) { float k (cal_table[i].true_cm - cal_table[i-1].true_cm) / (cal_table[i].raw_cm - cal_table[i-1].raw_cm); return cal_table[i-1].true_cm k * (raw_cm - cal_table[i-1].raw_cm); } } return raw_cm; }校准表放在Flash里重新标定后写回不要每次开机插USB输入。若手边没有高精度量块用A4纸按长度折叠替代也可以精度足够。6.2 串口日志、看门狗与死机现场重定向printf到USART1是最快的现场调试手段在C工程里要加extern C。extern C { int fputc(int ch, FILE* f) { HAL_UART_Transmit(huart1, (uint8_t*)ch, 1, 100); return ch; } }开发阶段打印原始测量值发布阶段只打印状态切换。如果担心设备在户外死机开启IWDG独立看门狗喂狗点放在主循环而不是中断确保主流程阻塞时能复位。复位后读RCC_CSR的复位标志把“上电复位”还是“看门狗复位”记进日志能区分代码死循环和低压复位。6.3 远程阈值调整与校准系数存储运维端希望能远程调整满溢阈值。在MQTT下行订阅device/trash/001/cfg收到JSON后解析并写入Flash。{full_threshold_cm:18,keep_open_ms:4000}写入Flash注意先解锁、擦除页、再写写完读回校验。把校准系数和唯一设备ID放在同一个Flash扇区量产工位只需要按一次“校准并保存”按钮后续固件升级不要擦除这个扇区否则所有设备都要重新校准。若样机总是凌晨死机先检查低温下晶振匹配电容和LDO压差这两个元件能造成完全不同的两种现场问题。本文还有配套的精品资源点击获取