基于STM32的智能衣柜:湿度监测与远程控制实战
简介:基于STM32单片机开发的智能衣柜程序工程包,面向嵌入式开发者和智能家居项目爱好者,适合作为课程设计、毕业设计或入门实战参考。程序集成了WIFI通信、温湿度与空气质量及烟雾检测、手机APP远程监控和超阈值自动报警,搭配LCD屏本地显示,整体功能链完整,能够实现环境状态的实时感知和异常预警。压缩包共121个文件,大小约2.96MB,以C源码、H头文件为主,另有Keil工程文件(uvprojx)、烧录用的hex文件、bat辅助脚本和txt说明;源码中包含了STM32外设驱动与gizwits协议相关代码,可直接导入Keil工程进行编译和烧录。目前已有218人学习下载,资源目录结构清晰,便于逐步理解传感器采集、WIFI数据上送、阈值判断与报警联动等关键环节。通过研究这套工程,可以掌握STM32多外设协同开发的方法,并依据实际需求灵活扩展控制逻辑,如联动空气净化器或通风系统,从而打造更完整的智能衣柜方案。
1. 基于STM32的智能衣柜程序:把回南天衣柜发霉变成一条串口指令
我在南方待过三个回南天,每年二三月衣柜背板都会长一层灰绿色的霉点。换干燥剂、放樟脑球都是治标,真正有用的是把湿度传感器和排风扇装进柜子,让单片机自己决定什么时候吹风、什么时候加热。这套基于STM32F103C8T6的智能衣柜程序就是这么来的:DHT11或SHT30测温湿度,ESP8266接WIFI,风扇和PTC通过继电器控制,柜内湿度超70%自动除湿,手机上点一下就开关通风。源码和接线图都在资源包里,嵌入式入门者能照着复现,做过温湿度控制又想加远程的人,改成酒柜、鞋柜、宠物粮柜的防潮方案只是换阈值的事。
2. 主控选型与硬件接线:为什么F103C8T6够用,以及每个引脚该接什么
2.1 为什么不用51也不用ESP32
很多第一次做智能衣柜的人会在这三个选择之间反复纠结,我直接说结论:这个场景下STM32F103C8T6是性价比最稳的选项,比它更老和比它更强的都不如它顺手。
先说51单片机。51的学习资料确实最多,但做这种带WIFI的程序会非常吃力:单片机要同时处理温湿度读取时序、PWM调速、UART协议解析,还要给ESP8266转发数据,8位机的主频和RAM都不够从容,稍微开几个环形缓冲数组,编译就开始蹦警告。再者,传统8051内核没有硬件多通道ADC,要外扩ADC芯片又多一层成本和故障点。不是说51不行,而是"智能衣柜"这种多传感器加通信的需求,已经超出了51最舒服的舒适区。
反过来看ESP32。双核240MHz、内置WIFI和蓝牙,性能完全过剩,但代价也很直接:开发环境重、引脚多反而乱、模块单价是F103C8T6的三倍以上,整机待机功耗也被拉高,衣柜门一关频繁唤醒反而费电。衣柜需要的只是一颗温湿度传感器、一个光敏电阻、两个继电器和一路PWM,F103C8T6的64KB Flash和20KB RAM绰绰有余。
我一般用标准外设库(SPL)写这类工程,代码量小、编译快,Keil里十几秒就能出镜像;如果你习惯HAL库或者用VSCode配EIDE插件看代码,资源里也附了对应的CubeMX工程,函数封装不同但架构完全一致。这里提醒一句:选标准库还是HAL不是重点,重点是别让WIFI协议栈的逻辑侵入主控程序——WIFI的脏活让ESP8266去扛,STM32只管采集、控制和协议帧。
2.2 传感器与执行器接线:一张引脚分配表
先把整机接线列成表,照着插基本不会打架:
| 模块 | 芯片/型号 | 通信方式 | STM32引脚 | 供电 |
|---|---|---|---|---|
| 温湿度 | DHT11 / SHT30 | 单总线 / I2C | PB0 或 PB8-PB9 | 3.3V |
| 光照 | 光敏电阻分压 | ADC | PA0(ADC1_IN0) | 3.3V |
| 电源电压 | 电阻分压 | ADC | PA1(ADC1_IN1) | 3.3V |
| 风扇 | 5V直流风机 | PWM | PA6(TIM3_CH1) | 5V,MOS或ULN2003 |
| PTC加热 | 220V PTC | 继电器 | PB12 | 220V |
| LED灯带 | 12V灯带 | 继电器 | PB13 | 12V |
| 状态屏 | OLED SSD1306 | I2C | PB8/PB9 | 3.3V |
| WIFI模块 | ESP8266-01S | UART | PA2/PA3(USART2) | 3.3V独立LDO |
| 调试串口 | USB转TTL | UART | PA9/PA10(USART1) | 3.3V |
两个点特别容易翻车。第一,ESP8266-01S发射瞬间电流能飙到300mA以上,直接挂在STM32开发板的3.3V上会造成瞬时跌落,表现为模块随机重启、发数据乱码。我一般给它单独配一颗AMS1117-3.3(输入5V),地线还是和系统共地,射频和主控互不干扰。第二,DHT11的单总线协议对边沿时序极其敏感,数据线超过20cm就会出现读值异常,必须加4.7kΩ上拉到3.3V;预算多五块钱换成SHT30走I2C,稳定性会好一个量级,两种传感器的驱动我都放进资源里,用哪个都行。
提示:如果柜体是金属材质,ESP8266天线别贴着铁板安装,至少隔2cm,否则信号强度会掉一个数量级,这是很多人"卧室里就断连"的隐藏原因。
2.3 ADC通道切换与PWM初始化要点
衣柜要采集两个模拟量:光敏电阻分压和电源电压。STM32F1的ADC是多通道共用一个采样保持电容,初始化时不要开扫描模式,按通道逐个配置、逐个转换最稳。下面这段取自资源里的HAL版本工程:
void ADC_ReadMultiChannel(uint16_t *light_val, uint16_t *vbat_val) { ADC_ChannelConfTypeDef sConfig = {0}; /* 通道0:光敏电阻分压,挂在PA0 */ sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; /* 高源阻抗用慢采样 */ HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) *light_val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); /* 通道1:电源电压分压,挂在PA1 */ sConfig.Channel = ADC_CHANNEL_1; HAL_ADC_ConfigChannel(&hadc1, &sConfig); HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) *vbat_val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); }这段代码的关键在于每次切换通道前重新调用HAL_ADC_ConfigChannel,让内部逻辑回到正确的通道状态;采样时间用55.5周期,对光敏电阻这种高阻抗源是必须的,改成1.5周期会拿到明显偏低且跳动的值。如果你想用DMA连续扫描模式,通道顺序和采样时间必须在CubeMX里一次配准,运行期改顺序很容易采到上一个通道的残留。
风扇调速用TIM3_CH1做PWM输出,频率我设25kHz。这个频率下直流风机的啸叫基本听不到,10kHz以下的高频噪音在安静的卧室里会非常明显。初始化部分在工程里已经配好,注意CubeMX里TIM3的初始Pulse给0,避免上电瞬间风扇猛转。
3. WIFI接入与指令解析:ESP8266透传、巴法云MQTT与状态机
3.1 两种WIFI方案:AT透传和MQTT固件怎么选
ESP8266接入WIFI有两条主流路线。第一条是刷官方AT固件,STM32通过串口发AT+CWMODE、AT+CIPSTART这类命令建立TCP连接,再自己在应用层组MQTT报文。这条路的缺点是STM32被字符串处理缠住——AT固件回显不稳定时,strstr匹配"OK"偶尔会失败,一旦失败整个链路就卡死。第二条是给ESP8266刷MQTT透传固件,比如巴法云提供的MQTT固件,模块自己维护TCP连接和心跳,STM32的串口里只需要收发自定义协议帧。
我推荐第二条,实测下来代码量能砍掉一半。巴法云这类物联网平台的免费配额对个人家庭设备足够用,微信小程序端的SDK是现成的,手机远程开关衣柜就是调一个接口的事。如果你不想依赖云平台,ESP8266也可以刷成TCP透传服务器,手机和衣柜在同一局域网内直接连,断外网也能用,代价是没有远程推送。
3.2 自定义串口协议:帧头、帧尾与CRC校验
不管走云还是走本地TCP,STM32和ESP8266之间的串口协议一定要自己定,不要直接传"ON"、"OFF"这种裸字符串。裸字符串解析简单,但扩展性为零:加设备ID要改协议,加参数要改协议,加校验更要改。我用的帧格式固定6字节:帧头0xA5、设备ID、命令字、数据字节、校验、帧尾0x5A,校验是前三个字节的异或。下面这段按标准外设库的写法贴,HAL版本在工程里等价实现:
/* USART2接收中断:单片机只认协议帧,不认字符串 */ static uint8_t rx_buf[16]; static uint8_t rx_index = 0; static volatile uint8_t cmd_ready = 0; typedef struct { uint8_t dev_id; uint8_t cmd; /* 0x01开风扇 0x02关风扇 0x03开PTC 0x04关PTC */ uint8_t data; /* 预留参数,比如定时时长 */ } WardrobeCmd; void USART2_IRQHandler(void) { uint8_t byte; if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { byte = USART_ReceiveData(USART2); if (byte == 0xA5) { rx_index = 0; rx_buf[rx_index++] = byte; /* 帧头无条件重新计数 */ } else if (rx_index > 0) { rx_buf[rx_index++] = byte; if (rx_index >= 6) { if (rx_buf[5] == 0x5A) { /* 校验:设备ID、命令、数据异或后等于第4字节 */ if ((rx_buf[1] ^ rx_buf[2] ^ rx_buf[3]) == rx_buf[4]) cmd_ready = 1; } rx_index = 0; } } } } uint8_t Wardrobe_GetCmd(WardrobeCmd *out) { uint8_t ret = 0; if (cmd_ready) { out->dev_id = rx_buf[1]; out->cmd = rx_buf[2]; out->data = rx_buf[3]; cmd_ready = 0; ret = 1; } return ret; }接收中断里的逻辑不算复杂,但有一个细节值得记:0xA5出现的位置不一定是帧起点,可能是上一帧的尾部数据恰好是0xA5,所以收到帧头时要无条件把rx_index清零重来,宁丢一帧也不组错帧。校验用异或,低成本且够用,资源里也给了CRC8查表法版本,数据完整度要求更高的场景直接替换。
3.3 主循环状态机:自动除湿、低温防霉、手动模式怎么编排
衣柜的控制逻辑我用状态机来写,四个状态:待机、通风除湿、低温加热、手动。状态切换只发生在主循环里,串口指令可以打断自动状态,但自动状态不会反过来打断手动状态,这条规则能避免很多"衣柜自己动起来了"的惊吓。
typedef enum { STATE_STANDBY, /* 待机:只监控不动作 */ STATE_VENT, /* 通风:湿度高,风扇低速转 */ STATE_HEAT, /* 加热:温度低且湿度高,PTC启动 */ STATE_MANUAL /* 手动:App控制,状态机不自动切换 */ } WardrobeState; int main(void) { /* 省略时钟、GPIO、ADC、定时器初始化 */ WardrobeState state = STATE_STANDBY; uint16_t humidity = 0; int16_t temperature = 0; uint8_t fan_duty = 0; while (1) { humidity = DHT11_ReadHumidity(); temperature = DHT11_ReadTemperature(); /* 读到0xFFFF或0xFF表示传感器通信失败,本次跳过控制 */ if ((humidity != 0xFFFF) && (temperature != 0xFF)) { switch (state) { case STATE_STANDBY: if (humidity > 70) state = STATE_VENT; else if (temperature < 12) state = STATE_HEAT; break; case STATE_VENT: fan_duty = (humidity - 60) * 2; /* 湿度越高转速越快 */ if (fan_duty > 100) fan_duty = 100; TIM3_SetDuty(fan_duty); if (humidity < 55) { /* 降到55%以下退出 */ TIM3_SetDuty(0); state = STATE_STANDBY; } break; case STATE_HEAT: if ((humidity < 50) && (temperature > 15)) state = STATE_STANDBY; break; case STATE_MANUAL: break; /* 手动模式只响应串口指令 */ } } WardrobeCmd cmd; if (Wardrobe_GetCmd(&cmd)) { switch (cmd.cmd) { case 0x01: TIM3_SetDuty(80); state = STATE_MANUAL; break; case 0x02: TIM3_SetDuty(0); state = STATE_STANDBY; break; case 0x03: Relay_On(RELAY_PTC); break; case 0x04: Relay_Off(RELAY_PTC); break; } } delay_ms(100); /* 主循环100ms周期,湿度变化感知足够快 */ } }两个参数值得展开。湿度70%触发、55%退出,中间留了15%的滞回区,没有滞回的话风扇会在临界点来回启停,继电器和风机都会短命。转速映射用(湿度-60)*2的整数运算,湿度80%给40%占空比,90%给60%,比启停控制平顺得多;如果柜子里衣服挂得特别多,把这个系数从2改成3,除湿更快但噪音也更大。手动模式刻意设计成"不自动退出",避免你晚上打开衣柜拿衣服时PTC突然自己启动。
协议帧里带了设备ID,为后续扩展留了口子:假如做双柜联动,同一套串口帧可以直接发给两个从机,甚至可以把这个ID扩展成CAN通信的节点地址。衣柜这种柜门开关频繁、干扰源多的场景,CAN的差分信号比UART更能扛,资源里附了从UART切CAN的适配层说明,需要时按图改线就行。
4. 避坑排查:湿度误报、WIFI乱码与ADC通道切换的翻车记录
下面的五条都是我在实板上跑出来的血泪经验,照着排查比对着文档猜快得多。
4.1 湿度显示99%恒定,风扇狂转不停
现象:DHT11读回来的湿度永远是99.9%,偶尔蹦出一个正常值,下一秒又跳回99,风扇因为湿度超阈值一直在转。 原因:DHT11单总线协议靠主机拉低后的电平脉宽来传数据,任何一位判断错都会导致数据错位。最常见的两个根因是数据线没加上拉电阻,或者杜邦线超过20cm导致边沿变缓;其次是主机拉低时间不够,从机应答没被正确捕获。 解决:数据线加4.7kΩ上拉到3.3V,线长压到15cm以内;主机拉低时间从18ms加到25ms,位0和位1的判定阈值从50us放宽到60us。我用示波器对比过,加上拉后从机应答的下降沿明显更陡,读值就稳定了,别把这种问题当玄学。如果你换了SHT30还有类似现象,就不是时序而是I2C地址冲突,检查总线上是否挂了两个相同地址的器件,比如SHT30默认地址0x44和某些EEPROM撞车。
4.2 ESP8266透传数据乱码、偶发断连
现象:App下发指令能执行,但回传的状态字符串夹乱码,运行半小时后模块彻底失联,断电重启才恢复。 原因:两个叠加。一是ESP8266-01S发射瞬间工作电流很大,板载3.3V稳压扛不住,电压跌破3.15V时射频数据就开始出错;二是模块和STM32串口没有共地,电平参考点不一致。 解决:ESP8266单独用一颗AMS1117-3.3供电,输入5V,输出端加100uF和0.1uF电容滤波;串口线共地。初始化时先把波特率统一到115200再进透传,别让模块自己协商。回传乱码还有一个隐蔽来源:STM32的USART发送如果和printf重定向混用同一个串口,两个发送源互相打断就会在帧中间插入杂字节。调试信息单独用USART1,业务数据固定USART2,物理分开最省心。
4.3 ADC切换通道后读数错乱,永远取到上一路的值
现象:PA0读完光敏电阻后切到PA1读电源电压,连续十次读到的都是PA0的残留值,交换读取顺序也一样。 原因:STM32F103的ADC切换通道后需要建立时间,采样保持电容没有充满就启动转换时,采到的是上一个通道的电荷。源阻抗越高的电路越明显,光敏电阻分压网络恰恰是高阻。 解决:切换通道后延时至少1ms再启动转换,采样时间设55.5周期,转换完Stop掉,下次再重来一遍。2.3节那段代码就是这么写的。顺带说一句,"ADC读出来为什么总是一个值"是群里出现频率最高的提问之一,排查顺序永远是先看通道切换延时,再看采样时间,最后才怀疑硬件。
4.4 继电器吸合瞬间单片机复位重启
现象:继电器一吸合,OLED闪一下,程序从头跑,日志里能看到复位记录。 原因:继电器线圈是感性负载,吸合瞬间反电动势沿电源线倒灌,把3.3V波形砸出毛刺;如果负载是220V,触点通断时的尖峰更猛,甚至会通过驱动管基极串回GPIO。 解决:继电器线圈两端并联1N4148续流二极管,方向反着接,阴极朝电源正;STM32板电源入口加100uF电解电容配0.1uF陶瓷电容。条件允许的话直接用带光耦隔离的继电器模块,把功率地和逻辑地彻底切开,这是最稳的做法,也是我后面几版一直沿用的习惯。
4.5 定时器中断里做浮点和串口发送,运行十分钟后卡死
现象:系统刚上电一切正常,十分钟后无响应,看门狗也喂不活。 原因:PWM占空比计算用了float类型,还在定时器中断回调里调用printf。STM32F103没有硬件浮点单元,float运算是库函数模拟的,一个中断周期里做浮点和阻塞串口发送,中断堆叠起来就把系统拖死了。 解决:占空比计算全部改整数,就是3.3节里的(湿度-60)*2;中断回调里只置标志位,真正的打印动作放到主循环处理。这条经验放之四海皆准:中断里永远只做最必要的事,状态翻出去,到主循环再处理。
5. 验证技巧:串口回环加一盏LED把整机流程一次性跑通
5.1 先用串口助手模拟App,再上云
很多人在接线完成后直接连巴法云调远程,结果一次失败分不清是WIFI问题、协议问题还是硬件问题。我的一般做法是先不上WIFI:用USB转TTL把电脑串口和STM32的USART2接在一起,串口助手按协议帧手动发指令,看风扇和PTC动不动、状态屏显示对不对。这一步把"云"这个黑匣子摘掉,错在哪一层一眼就知道。
手动帧通过后,再用Python脚本把全部指令回归一遍,顺便验证响应帧:
import serial, time ser = serial.Serial('COM3', 115200, timeout=1) def build_frame(dev_id, cmd, data): crc = dev_id ^ cmd ^ data # 与单片机端校验算法一致 return bytes([0xA5, dev_id, cmd, data, crc, 0x5A]) test_cases = [ (0x01, 0x01, 0x00), # 开风扇 (0x01, 0x02, 0x00), # 关风扇 (0x01, 0x03, 0x00), # 开PTC (0x01, 0x04, 0x00), # 关PTC ] for dev_id, cmd, data in test_cases: ser.write(build_frame(dev_id, cmd, data)) time.sleep(0.3) resp = ser.read(32) print(f"cmd=0x{cmd:02X} -> resp={resp.hex(' ')}")build_frame里的校验算法和单片机端必须完全一致,异或顺序错了会一条都过不了。跑完这个脚本,协议层基本可以信任,之后再把ESP8266接入,出问题只需要怀疑WIFI链路。
5.2 逻辑分析仪看DHT11时序和PWM波形
协议过了以后,测试传感器和PWM。逻辑分析仪的CH0夹在DHT11数据线上,CH1夹在PA6的PWM输出上。DHT11的波形能清楚看到40位数据里的0和1脉宽,脉宽不均匀、边沿有毛刺就是上拉或线长问题,不用靠猜。PWM波形看频率和占空比是否随湿度指令变化,25kHz用万用表频率档也能量,但逻辑分析仪能同时确认两路信号有没有互相串扰。
整机测试时建议把ST-Link接上,用STM32CubeIDE下载程序并开在线调试。很多人问"下载失败",十有八九是BOOT0跳线没接对或者驱动没装好,接上ST-Link点下载,在状态机切换处打断点,走到哪一步一目了然。程序跑起来后,把板载LED接到PB13继电器指示位,App发一条开LED的指令,秒级确认链路通断。OLED滚动显示中文状态时,源码字符串编码和字模编码不一致也会看着像乱码,先查是不是编码问题,别误判成WIFI故障。
从那以后我每次做这种带WIFI的设备,都强制自己先走一遍"串口手测协议、脚本回归、逻辑分析仪看波形"三关,再碰云平台。这套流程看着多花半小时,实际能把联调时间缩短一个下午,出问题了永远知道黑匣子在哪一层。这套智能衣柜的源码、接线图和协议文档都在资源包里,跟着上面的步骤走一遍就能复现,希望帮到你。
本文还有配套的精品资源,点击获取