
1. 为什么我要做这个衣柜除湿控制器南方回南天那段时间我家衣柜里的衣服摸上去总是潮乎乎的皮衣上甚至长出了白毛。市面上带除湿功能的衣柜动辄三四千而一个半导体除湿模块加个控制器成本不到一百块。我本身做嵌入式开发手头常备STM32F103C8T6最小系统板干脆自己搭一套。这个项目的核心目标很明确用STM32实时采集衣柜内的温湿度当湿度超过阈值时自动启动除湿模块湿度降下来后自动停止同时把数据和控制状态显示在OLED屏幕上。整套东西包括代码、原理图、仿真三部分适合正在学STM32的嵌入式新手、需要做毕业设计的学生以及想给家里衣柜加个智能除湿功能但不想花大价钱的动手派。我选STM32F103C8T6的原因很直接价格便宜十几块钱一片、资料丰富、外设够用。DHT11负责温湿度采集SSD1306 OLED做本地显示继电器模块控制半导体除湿片再加一个按键用于手动切换模式。整个系统不需要联网也不需要手机App上电就能独立运行可靠性比那些依赖云端的方案高得多。下面我会从硬件选型、原理图设计、代码架构、仿真验证、实际调试五个维度把整个项目拆开讲清楚。每个环节我都会说明为什么这样选、这样写以及我在实操中踩过的坑。2. 硬件选型与原理图设计中的关键决策2.1 主控芯片为什么锁定STM32F103C8T6市面上做温湿度控制的可选方案很多STC89C52、ESP32、Arduino Nano都能干这活。我最终选STM32F103C8T6主要基于三点考虑。第一是ADC和定时器资源。虽然这个项目用DHT11是单总线数字信号不需要ADC但后续如果要加土壤湿度传感器或者其他模拟量传感器STM32的12位ADC直接就能用不用换主控。定时器方面我需要一个定时器做系统滴答一个做DHT11的微秒级延时STM32的TIM资源完全够用。第二是中断优先级管理。DHT11的读取对时序要求很严格如果中断处理不当读出来的数据全是0或者全是1。STM32的NVIC支持嵌套中断我可以把DHT11的读取放在高优先级把OLED刷新放在低优先级避免时序被打断。第三是生态和成本。STM32F103C8T6的核心板在某宝上十块钱出头ST-Link调试器二十多块Keil MDK的STM32芯片包免费。相比之下ESP32虽然自带WiFi但功耗高而且这个项目不需要联网多出来的功能反而增加调试复杂度。注意STM32F103C8T6的Flash是64KBSRAM是20KB。如果你的代码里加了字库或者大量字符串常量Flash可能会不够用。我建议OLED显示用简单的ASCII字符不要上中文点阵字库否则64KB很快就被吃光。2.2 DHT11的时序陷阱与上拉电阻选择DHT11是单总线器件一根数据线完成双向通信。它的通信过程是这样的主机拉低总线至少18ms作为起始信号然后释放总线DHT11响应后再发送40位数据8位湿度整数8位湿度小数8位温度整数8位温度小数8位校验和。这里有个很多人忽略的细节DHT11的数据线必须接一个4.7kΩ到10kΩ的上拉电阻。我一开始图省事用了STM32内部的上拉电阻约40kΩ结果读出来的数据十次有三次校验失败。换成外部4.7kΩ上拉后连续读一百次都没有出错。原因是内部上拉太弱总线在释放后上升沿不够陡峭DHT11识别不到高电平。另外DHT11的采样周期不能低于1秒。我试过500ms读一次数据会漂移。官方手册写的是1Hz实际用下来2秒读一次最稳。代码里我用一个软件定时器控制采样间隔避免频繁读取导致传感器发热影响精度。2.3 继电器驱动电路为什么用光耦隔离控制半导体除湿片通断我用的是5V继电器模块。这里有个关键设计STM32的GPIO输出电流最大20mA而继电器线圈的吸合电流通常在60-80mA直接驱动会烧IO口。所以必须加驱动电路。我选的是光耦三极管的方案。光耦用PC817三极管用S8050继电器线圈两端反并联一个1N4148续流二极管。光耦的作用是把STM32的GPIO和继电器线圈完全隔离避免继电器吸合瞬间产生的反向电动势通过地线串扰到MCU导致复位或者死机。实测中我发现如果不用光耦继电器吸合的瞬间OLED屏幕会闪一下有时候STM32还会重启。加了光耦之后这个问题彻底消失。续流二极管也不能省否则三极管关断时线圈产生的反向电压会击穿三极管。2.4 原理图绘制在嘉立创EDA中的实操要点原理图我用嘉立创EDA画的免费而且元件库全。画图时有几个地方容易出错。电源部分STM32F103C8T6的VDD和VDDA都要接3.3V每个电源引脚旁边放一个100nF的去耦电容。我一开始只放了两个结果ADC读数跳动很大。后来在每个VDD引脚旁边都加了100nFVDDA旁边加了1uF100nF读数才稳定下来。晶振部分外部8MHz晶振配两个20pF的负载电容。负载电容的值要根据晶振手册来我用的晶振要求是20pF所以选了两个20pF的电容。如果电容选大了起振会变慢选小了频率会偏。BOOT引脚BOOT0通过10kΩ电阻接地BOOT1也接地。这样上电后从主Flash启动不需要手动切换。SWD调试接口SWDIO和SWCLK引出来接4针排针方便用ST-Link下载和调试。我建议把NRST也引出来有时候程序跑飞了需要硬件复位。画完原理图后一定要用ERC电气规则检查过一遍。嘉立创EDA的ERC会提示未连接的引脚、电源冲突等问题。我第一版原理图就是忘了把DHT11的VCC接到3.3VERC直接报错省去了打板后才发现问题的麻烦。3. 代码架构从裸机到状态机的设计思路3.1 主循环为什么用状态机而不是延时阻塞很多STM32入门教程写温湿度控制主循环里直接while(1){ 读DHT11; 判断; 控制继电器; 延时1秒; }。这种写法在简单场景下能用但一旦要加按键扫描、OLED刷新、串口打印就会互相阻塞。比如DHT11读取需要至少20ms的时序这期间按键按下去没反应OLED也不刷新。我用的是时间片轮询状态机的架构。主循环里维护一个1ms的滴答计数器每个任务根据自己的时间片执行。DHT11每2秒读一次OLED每200ms刷新一次按键每10ms扫描一次继电器状态每500ms检查一次。这样各个任务互不干扰按键响应灵敏OLED刷新流畅。状态机方面我把系统分成四个状态IDLE空闲、DEHUMIDIFY除湿中、MANUAL_ON手动开启、MANUAL_OFF手动关闭。湿度超过阈值自动进入DEHUMIDIFY湿度降到阈值以下回到IDLE。按键可以强制切换到手动模式。状态切换时继电器动作同时OLED显示当前状态。3.2 DHT11驱动代码的微秒延时实现DHT11的时序要求微秒级延时STM32的HAL库提供的HAL_Delay()是毫秒级不够用。我用的是SysTick定时器做微秒延时。具体做法配置SysTick为1us中断一次在中断里对一个全局变量usTicks自增。需要延时的时候记录当前usTicks然后循环等待直到差值达到目标值。代码如下volatile uint32_t usTicks 0; void SysTick_Handler(void) { usTicks; } void delay_us(uint32_t us) { uint32_t start usTicks; while ((usTicks - start) us); }这里有个坑usTicks必须声明为volatile否则编译器优化后可能把它缓存到寄存器里导致死循环。另外SysTick中断优先级要设成最低避免打断DHT11的时序。DHT11的读取函数里起始信号拉低18ms然后释放等待DHT11响应。响应信号是80us低电平80us高电平之后是40位数据。每一位数据以50us低电平开始高电平持续26-28us表示0持续70us表示1。代码里用delay_us配合GPIO读取来判断每一位的值。3.3 OLED显示驱动的页地址模式与刷新策略SSD1306 OLED我用的是I2C接口4针VCC、GND、SCL、SDA。I2C速率设成400kHz刷新一屏128x64的数据大约需要20ms。显示内容我分了三行第一行显示当前温度和湿度第二行显示系统状态IDLE/DEHUMIDIFY/MANUAL第三行显示继电器开关状态和运行时间。刷新策略是局部刷新只更新变化的部分而不是整屏重写。比如温度没变就不重写温度那一行。这样可以把刷新时间从20ms降到5ms左右减少对DHT11时序的干扰。OLED的I2C地址通常是0x78写地址或0x3C7位地址。我遇到过买回来的模块地址是0x7A的调试了半天才发现。建议先用I2C扫描程序确认地址再写驱动。3.4 继电器控制的安全逻辑迟滞比较与最小运行时间继电器控制看似简单湿度高于阈值开低于阈值关。但实际用起来有两个问题。第一个是阈值抖动。如果阈值设60%湿度在59.9%和60.1%之间波动继电器会频繁开关咔咔响不说还容易坏。我加了5%的迟滞湿度超过60%开启降到55%以下才关闭。这样继电器不会在阈值附近反复动作。第二个是压缩机保护。虽然半导体除湿片没有压缩机但频繁通断也会缩短寿命。我加了最小运行时间继电器一旦吸合至少运行3分钟才能断开一旦断开至少等待1分钟才能再次吸合。这个逻辑用状态机里的计时器实现代码里用两个变量relayOnTime和relayOffTime记录。提示如果你用的是压缩机型除湿模块最小运行时间和最小停机时间必须严格遵守否则压缩机容易损坏。半导体除湿片虽然没那么娇贵但加了这个逻辑后继电器寿命明显延长。4. 仿真验证在Proteus里跑通整个系统4.1 Proteus仿真环境的搭建与元件替换打板之前我先在Proteus里把整个系统跑了一遍。Proteus的好处是能模拟DHT11的时序响应还能看到OLED的显示效果。元件清单STM32F103C8T6、DHT11、SSD1306Proteus里用I2C OLED模型、继电器用RELAY-SPDT、按键、电阻电容若干。Proteus自带的STM32模型可以直接加载Keil编译出来的hex文件。这里有个坑Proteus的DHT11模型和实物时序有差异。仿真里DHT11响应很快实物上电后需要1秒稳定时间。所以仿真通过不代表实物一定通过仿真只能验证逻辑正确性时序余量还是要以实物为准。4.2 仿真中验证状态机切换与继电器动作仿真里我重点验证了三个场景。场景一湿度从50%上升到65%。DHT11模型输出湿度值逐渐增加当超过60%时状态机从IDLE切换到DEHUMIDIFY继电器吸合OLED显示DEHUMIDIFY。湿度降到55%以下后状态机回到IDLE继电器断开。场景二手动按键切换。按下按键状态机从IDLE切换到MANUAL_ON继电器强制吸合。再按一次切换到MANUAL_OFF继电器断开。再按一次回到IDLE。场景三最小运行时间验证。在DEHUMIDIFY状态下即使湿度瞬间降到55%以下继电器也要等3分钟才断开。仿真里我把时间参数调小3秒验证逻辑正确后再改回3分钟。仿真跑通后我把hex文件烧到实物上一次成功。仿真最大的价值是提前发现逻辑错误比如状态机漏了某个切换条件或者继电器控制逻辑写反了。这些问题在仿真里改代码只要几秒钟在实物上就要重新烧录、重新接线。4.3 仿真与实物的差异哪些能信哪些不能信仿真能验证的GPIO逻辑电平、I2C通信协议、状态机切换逻辑、OLED显示内容。仿真不能验证的DHT11的实际时序余量、继电器的电磁干扰、电源纹波、OLED的实际亮度、按键的机械抖动。我的经验是仿真通过后实物调试的重点放在电源和信号完整性上。比如继电器吸合时电源电压跌落多少DHT11数据线有没有振铃OLED的I2C波形是否干净。这些用示波器一看便知仿真里完全看不到。5. 实物调试中踩过的五个坑5.1 坑一DHT11读出来全是0xFF第一版代码烧进去后串口打印的温湿度全是255。用逻辑分析仪抓DHT11数据线的波形发现主机拉低18ms后释放DHT11根本没有响应。排查过程先查硬件DHT11的VCC是3.3VGND接地数据线接PA0上拉电阻4.7kΩ。硬件没问题。再查代码发现delay_us函数里usTicks没有加volatile编译器优化后把while循环优化掉了实际延时远小于18ms。加上volatile后波形正常数据读取成功。这个坑的教训是涉及硬件时序的变量必须加volatile。编译器不知道这个变量会被中断修改会做出错误的优化。5.2 坑二OLED显示乱码DHT11读出来后OLED上显示的温度是乱码。查I2C波形发现SCL和SDA的上升沿很缓像是被电容拖慢了。原因是我用的杜邦线太长20cm线间电容导致I2C信号边沿变缓。把杜邦线换成10cm的短线后波形明显改善显示正常。如果必须用长线可以把I2C速率从400kHz降到100kHz或者加I2C缓冲器。另一个可能是OLED的初始化序列不对。SSD1306的初始化命令有几十条漏一条或者顺序错了都会导致显示异常。我建议直接用厂商提供的初始化代码不要自己手写。5.3 坑三继电器吸合导致STM32复位继电器吸合的瞬间STM32会重启。用示波器看3.3V电源发现继电器吸合时电源电压从3.3V跌到2.8V持续约10ms。STM32的欠压复位阈值是2.0V左右2.8V虽然没到复位阈值但可能触发了内部BOR。解决方案在继电器模块的电源输入端并联一个1000uF的电解电容吸收吸合瞬间的电流冲击。同时在STM32的3.3V输入端加一个100uF的钽电容。改完后继电器吸合时电源电压只跌到3.1VSTM32不再复位。如果还不行就把继电器模块的电源和STM32的电源完全分开用两个独立的稳压器。继电器的5V用LM7805STM32的3.3V用AMS1117两者共地但不共电源。5.4 坑四按键抖动导致状态误切换按键用的是普通的轻触开关按下时会有5-10ms的抖动。一开始没做消抖按一次键状态机切换了好几次。消抖方案有两种硬件消抖和软件消抖。硬件消抖是在按键两端并联一个0.1uF的电容软件消抖是检测到按键按下后延时20ms再读一次。我用的是软件消抖在10ms的按键扫描任务里连续三次读到按下才确认。这样既能消抖又不增加硬件成本。代码里用一个keyCount变量计数读到按下就加一读到释放就清零加到3才触发状态切换。5.5 坑五长时间运行后湿度读数漂移系统连续运行一周后湿度读数比实际值偏高10%左右。用标准湿度计对比确认是DHT11漂移了。DHT11的精度是±5%RH长期在高湿环境下工作传感器表面会凝结水膜导致读数偏高。解决办法是定期加热除湿。DHT11内部有一个加热电阻但手册里没写怎么控制。我的做法是每24小时让继电器强制运行10分钟把衣柜内的湿度降下来同时DHT11也顺便被“烘干”了。如果湿度长期高于90%DHT11的读数会完全失效。这种情况下建议换SHT30或者AHT20这两款是I2C接口精度更高抗结露能力也更强。AHT20的价格只比DHT11贵两三块钱但性能好很多。6. 从原型到成品外壳、供电与长期运行建议6.1 外壳选型与传感器开孔位置原型验证通过后我把它装进了一个塑料防水盒100x68x50mm。盒子正面开孔装OLED屏幕侧面开孔装DHT11和按键背面开孔走电源线。DHT11的开孔位置有讲究不能靠近继电器和电源模块否则继电器发热会影响温度读数。我把DHT11放在盒子顶部继电器放在底部中间隔了5cm。实测温度读数比放在一起时低了1.5度。OLED屏幕用热熔胶固定在开孔处边缘打胶密封防止湿气进入盒子内部。按键用防水轻触开关或者干脆用磁吸遥控避免开孔。6.2 供电方案5V适配器还是锂电池衣柜里通常没有插座所以供电是个问题。我试过两种方案。方案一5V/2A适配器USB线。优点是简单插上就能用。缺点是要从衣柜外面拉线进来不太美观。如果衣柜附近有插座这个方案最省事。方案二18650锂电池升压模块。优点是无线可以放在衣柜任何位置。缺点是续航短2000mAh的电池只能撑两天。如果加太阳能板成本又上去了。我最终用的是方案一在衣柜背板上钻了一个8mm的孔把USB线穿进来孔周围用密封胶封住。适配器放在衣柜顶上线沿着背板走基本看不见。6.3 长期运行的数据记录与阈值调整系统跑了一个月后我加了一个简单的数据记录功能每10分钟把温湿度值通过串口打印出来用Python脚本保存到CSV文件。分析数据后发现衣柜内的湿度在晴天是55%左右雨天能到75%。所以我把除湿阈值从60%调到了65%这样晴天基本不启动雨天才会工作省电不少。如果你不想用串口可以在代码里加一个SD卡模块把数据写到SD卡里。或者用STM32的内部Flash模拟EEPROM存最近100条记录。不过内部Flash的擦写次数只有1万次左右频繁写会坏建议用外部EEPROM如AT24C02。阈值调整的逻辑我写成了可配置的长按按键3秒进入设置模式短按增加阈值再长按保存到Flash。这样不用重新烧录就能改参数。6.4 后续扩展方向从单机到联网这个项目目前是单机运行后续如果想联网有几个方向。加ESP-01S WiFi模块通过串口和STM32通信把温湿度数据上传到本地服务器或者手机App。ESP-01S价格便宜十几块但需要额外的3.3V电源而且AT指令调试比较麻烦。加蓝牙模块HC-05手机靠近时自动连接适合近距离查看数据。HC-05的功耗比WiFi低但传输距离只有10米左右。加NRF24L01多个衣柜之间组网一个主机收集所有从机的数据。NRF24L01的功耗极低适合电池供电的场景。我个人的建议是如果只是自己用单机版本足够了。联网版本适合有多個衣柜或者需要远程监控的场景。但联网会增加调试复杂度建议先把单机版本跑稳再考虑加联网功能。7. 代码与原理图的开源说明整套项目的代码和原理图我已经整理好了。代码用Keil MDK 5编译基于标准外设库不是HAL库因为标准库的代码量更小Flash占用更低。原理图是嘉立创EDA格式可以直接导出Gerber打板。代码结构分成了五个文件main.c主循环和状态机、dht11.cDHT11驱动、oled.cOLED驱动、relay.c继电器控制、key.c按键扫描。每个文件都有详细的注释关键参数用宏定义放在头文件里方便修改。如果你要复现这个项目我建议按这个顺序来先在Proteus里跑通仿真确认逻辑正确然后画原理图、打板、焊接最后烧录代码用串口打印调试信息。遇到问题先查电源和信号完整性再查代码逻辑。大部分问题都是硬件引起的代码问题反而少。注意DHT11的驱动代码里微秒延时函数依赖SysTick中断。如果你用了RTOSSysTick会被RTOS占用需要改用其他定时器做微秒延时。我用的是裸机没有这个问题。这个项目从构思到成品大概花了两周时间其中一半时间在调试硬件。如果你有STM32基础一周应该能搞定。没有基础的话建议先看江科大的STM32教程把GPIO、定时器、中断、I2C这几个外设搞明白再来做这个项目会顺畅很多。