酒窖环境监测听起来是个挺小众的题目,但真把它做完以后,我发现它比很多“炫酷”的智能硬件项目更考验工程能力。先说清楚这个项目是干什么的:用一块STM32主控,把酒窖里的温度、湿度、光照甚至空气状态实时采集起来,本地显示、超限报警、自动控制风扇和加湿器,必要时把数据传到手机或电脑上。这套系统解决的核心问题是:葡萄酒储存对温度和湿度极其敏感,而人不可能24小时蹲在酒窖里盯数据。
适合参考这个方案的人主要有两类。一类是拿它做毕设的电子、嵌入式、物联网方向学生,另一类是真有酒窖、茶仓、档案室或地下室需要做环境监控的开发者。前者可以从中学到一个完整项目的设计流程和代码框架,后者可以把它当原型,改改传感器和阈值就能直接部署。我也把调试过程中踩过的坑、验证过的参数和最终能稳定运行的代码结构都整理在下面了,照着做,大概率能一次跑通。
1. 项目定位:酒窖环境到底要监测什么
1.1 酒窖环境指标与葡萄酒储存的关系
葡萄酒是典型的“娇气”储存品。长期存放过程中,温度波动会引起酒液热胀冷缩,软木塞随之伸缩,密封性变差,加速氧化;湿度太低,软木塞干缩,空气进入酒瓶;湿度太高,瓶塞和酒标发霉。光照则直接影响单宁和色素分解,把酒“晒”得变味。行业里公认的长期储存条件是:温度12~15℃(最理想13℃左右)、相对湿度60%~70%、避光、通风、无异味、无振动。
这里面最容易出问题的是温度和湿度,也是监测系统必须实时盯紧的核心项。光照可以做成“超限报警”而不是持续监测,因为酒窖门开合就是短暂的光照暴露。至于通风,大多数酒窖靠排风扇来解决,监测系统能不控制,但至少要有继电器接排风扇的预留口。异味检测(比如硫化物、霉菌味)属于更高阶的需求,传感器成本高,我建议第一版不做,保留扩展接口。
所以第一版酒窖监测系统的功能边界就定了:实时采集温湿度、光照强度,LCD显示数据,按键设置报警阈值,超限蜂鸣器+LED声光报警,继电器控制排风扇和加湿器,串口或WiFi把数据传出去。这套功能既不复杂到难以实现,又能覆盖酒窖环境管理的80%痛点。
1.2 为什么选STM32作为主控
选题既然叫“基于STM32”,主控选型基本是确定的,但“为什么是它”仍然值得说清楚。STM32F103C8T6是我在这个项目里最终选的型号,原因有三个层面:
一是资源刚好够用。72MHz主频、64KB Flash、20KB RAM,跑一个裸机状态机加上四五个传感器驱动绰绰有余。如果后续加FreeRTOS或MQTT协议栈,Flash和RAM也扛得住。它有2个I2C、3个USART、2个SPI、1个ADC(10个通道)、多个定时器,刚好覆盖温湿度传感器、光照传感器、ESP8266/蓝牙串口、LCD和PWM风扇调速这些外设。
二是工业级稳定性和温度范围。酒窖环境湿度常年60%~85%,主控板要在这个环境下长时间工作。STM32的结温范围是-40~85℃,哪怕机箱散热一般也有足够的余量。相比用ESP32之类的模组,STM32的外围电路可控性更强,抗干扰和电源处理做得好就更稳定。
三是资料和生态。这个因素对新手极其友好。正点原子、野火的例程铺天盖地,从标准库新建工程到HAL库配置,搜索“STM32 + 任何外设”都能找到可参考的代码。调试过程中遇到“delay卡死”“I2C无应答”“ADC切换通道”这类问题,几乎都能搜到现成的踩坑记录,这一点在项目工期紧的时候省了大力气。
1.3 系统整体架构与数据流
整个系统从数据流来看是一条清晰的单向链路:传感器采集 → 主控处理 → 本地交互 → 控制执行/远程上报。传感器端接SHT30温湿度模块、BH1750光照模块、DS18B20温度探头;主控是STM32F103C8T6;交互端是I2C接口的LCD1602或OLED;执行端是继电器驱动的风扇和加湿器,加一个无源蜂鸣器做报警;远程上报预留一个USART接口接ESP8266或HC-05蓝牙模块。
这样的架构好处是模块之间完全解耦。传感器坏了一个不影响其他模块,通信模块不稳定也能用本地LCD顶着。我还特意把主控板和继电器分成两块电路板,传感器全部通过排线外置,这样主控核心板可以放在相对干燥的机箱里,探头才放到酒窖不同位置。酒窖这种高湿环境,系统能不能长期稳定运行,关键就在这种细节上,而不是代码里有没有高大上的算法。
2. 硬件选型与电路设计要点
2.1 传感器和执行机构的选型对比
主传感器我对比过DHT22、SHT30和DS18B20,最后选择SHT30做主力,DS18B20做补充。直接看对比表的参数差异:
| 传感器 | 接口 | 测温范围/精度 | 湿度范围/精度 | 价格区间 | 长期稳定性 |
|---|---|---|---|---|---|
| DHT22 | 单总线 | -40~80℃ / ±0.5℃ | 0~99%RH / ±2~5%RH | 8~15元 | 一般,长期漂移明显 |
| SHT30 | I2C | -40~125℃ / ±0.3℃ | 0~100%RH / ±2%RH | 12~20元 | 好,工业级传感器 |
| DS18B20 | 单总线 | -55~125℃ / ±0.5℃(-10~85℃) | 不支持 | 5~10元 | 好,适合温度探头 |
DHT22不是不能做,但它对手动时序要求高,时序稍微偏一点就偶发错误帧,而且在持续高湿环境下长期漂移肉眼可见。SHT30走I2C总线,带CRC数据校验,读回来的数据可靠性高一个级别。DS18B20则用在另一个场景:酒窖不同位置布点。SHT30只能放一个点,而酒窖内部上下层温差可能超过2℃,我就把DS18B20做成三个探头的多点温度网络,分别测顶层、中层和地面附近温度。DS18B20防水封装探头成本低,一条总线挂多个设备,非常适合多点测温。
光照传感器就简单了。我用BH1750模块,I2C接口直接读出以lux为单位的光照强度,量程1~65535lx,拿来判断酒窖门是否长时间未关、有没有额外光源照射到酒架,逻辑非常直观。如果预算再省一点,光敏电阻加ADC也能做,但读数是0~4095的原始值,换算成lux要自己标定,不推荐。
执行机构选了5V继电器模块控制220V排风扇和加湿器。继电器模块自带光耦隔离和指示灯,但实际测试发现它的5V供电和信号地跟STM32的地连在一起时,继电器吸合的瞬间还是会把3.3V电源拉低几十毫秒。这个问题我做了一版板子才注意到,电流都留到了下一节的电路设计里讲。
2.2 电源、I2C上拉与继电器驱动的关键细节
供电方案是整套系统稳定性的根基。我用了12V/2A的电源适配器进板子,DC-DC降到5V给继电器和风扇供电,再用AMS1117-3.3把5V降到3.3V给STM32和传感器供电。这里踩过的坑是:千万不能让继电器和传感器共用一段从稳压器出来到负载的长走线。继电器吸合瞬间电流可以从几十mA跳到几百mA,传感器电源线上立刻出现电压跌落。解决方法是把5V在接口处分两路,一路走继电器,一路进AMS1117,中间加100uF电解电容和0.1uF陶瓷电容。实测下来,继电器动作时3.3V的纹波控制在50mV以内,MCU再也没复位过。
I2C总线上拉电阻是新手最容易忽略的点。SHT30模块和BH1750模块有些版本板载4.7k上拉电阻,两个模块挂到同一条总线上相当于两个4.7k并联成2.35k。这在100kHz标准模式下没问题,但如果你把I2C开到400kHz快速模式,总线上升沿就会被电容拖慢,偶尔出现通信不稳定。经验做法是:先用万用表确认模块是否自带下拉焊盘、板载上拉,如果没有,就在主控板上统一焊4.7k上拉;如果总线长度超过20cm,比如传感器探头通过排线引到酒窖深处,就把上拉电阻换成2.2k。另外,I2C上拉电阻的选择范围可以按公式估算:最小阻值=(VDD-0.4)/3mA≈967Ω(VDD=3.3V),最大阻值=上升时间/(总线电容×0.8473),100kHz模式下选4.7k安全,400kHz模式建议用2.2k,这两个经验值我在这项目里都验证过。
继电器驱动电路我最后改成了“三极管+续流二极管+光耦”的组合。STM32的GPIO输出能力是3.3V/8mA,直接推继电器线圈不够稳。典型接法是GPIO串一个1k电阻到光耦输入端,光耦输出端接NPN三极管基极,三极管集电极接继电器线圈,线圈两端反向并联1N4007续流二极管。这样MCU的GND和继电器电源GND被光耦隔开,避免了回收干扰。如果继电器驱动负载是风扇电机这类感性负载,还要在继电器触点两端并联RC吸收电路,我用的是100Ω电阻串联0.1uF电容,实测触点火花明显减少,继电器触点寿命也有保障。
2.3 传感器布局与PCB设计注意事项
传感器布局直接决定了监测数据的真实程度。SHT30如果贴在STM32主板上,测出来的温度会比酒窖实际温度高出2~3℃,因为主板上的LDO和MCU本身就是热源。我的做法是主控板和传感器探头彻底分离:SHT30和BH1750装在一个小传感器板上,通过4芯排线(VCC、GND、SCL、SDA)引出到酒窖空中央位置悬空安装;DS18B20三个探头按上中下布在酒架附近。排线长度控制在30cm以内,在这个长度下I2C通信依然稳定,不用改上拉阻值。
PCB设计上,我第一版就做了双面板,但铺地策略有失误。后来改成模拟地和数字地单点连接,在电源入口处用一个0Ω电阻或磁珠隔开。ADC采样模拟量的稳定性明显提升,跳变从原来的±20个码值降到±5个码值以内。还有一个高湿环境下的必要措施:PCB焊完并测试确认没问题后,刷一层三防漆,厚度不用厚,均匀覆盖电路板正面和背面,继电器焊盘和插针位置用胶带遮住不刷。这套系统在湿度长期70%左右的环境里运行两个月,板子没有出现任何爬电或氧化现象。
3. 软件框架与核心代码实现
3.1 工程搭建与基础外设初始化
软件开发我用的STM32CubeMX生成HAL库工程,这是目前效率最高的路径。CubeMX里需要配置的项不多:外部8MHz晶振、PLL倍频到72MHz系统时钟、I2C1走普通模式即可、USART1设为115200-8-N-1、ADC1开启两个通道(分别接空气质量备用接口和电源电压监测)、TIM3_CH2输出PWM用于风扇调速、GPIO配好按键、蜂鸣器、LED、继电器控制引脚。串口上电打印“STM32 Wine Cellar Monitor v1.0”,基本框架就算起来了。
这里要提醒一点:CubeMX生成的HAL_Delay是基于SysTick的。如果你后面要用DWT做微秒级时间戳,或者自己控制SysTick,HAL_Delay和DWT可能产生冲突。我在这个项目里干脆一开始就用DWT实现自己的延时函数,避免后续调试日志和传感器时序互相干扰。DWT延时代码不长:
static void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); }SystemCoreClock在72MHz下,us * 72 用uint32_t存完全没问题,就算延时1秒也才72000000,没有溢出风险。这套延时函数我同时用来做SHT30的等待时间控制、按键消抖、蜂鸣器报警时长等,不再依赖HAL_Delay,排查“delay卡死”这类问题反而少了一个嫌疑点。
串口printf重定向是调试刚需,HAL库下的写法是在工程里加上fputc函数:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }记得在Keil的Target页面勾选“Use MicroLIB”,否则printf会重定向到半主机模式,程序跑起来会卡在串口上。这个坑我见不少初学者踩过:代码编译没问题,下载后printf一调用就死机,十有八九是MicroLIB没勾选。
3.2 多传感器驱动与数据滤波实现
SHT30读取数据的完整流程是:拉低SCL发送0x2C 0x06(高重复度测量)或0x24 0x00(低功耗测量),然后等待10ms左右,再发送I2C读命令读到6字节数据。数据排列是:温度高位、温度低位、温度CRC校验、湿度高位、湿度低位、湿度CRC校验。我直接用硬件I2C读取,配合HAL库,代码长这样:
uint8_t sht30_read_data(float *temp, float *hum) { uint8_t buf[6]; uint16_t tmp_raw, hum_raw; uint8_t cmd[2] = {0x2C, 0x06}; if (HAL_I2C_Master_Transmit(&hi2c1, 0x44 << 1, cmd, 2, 1000) != HAL_OK) return 1; delay_ms(10); if (HAL_I2C_Master_Receive(&hi2c1, 0x44 << 1, buf, 6, 1000) != HAL_OK) return 1; tmp_raw = (buf[0] << 8) | buf[1]; hum_raw = (buf[3] << 8) | buf[4]; *temp = -45.0f + 175.0f * tmp_raw / 65535.0f; *hum = 100.0f * hum_raw / 65535.0f; return 0; }SHT30默认I2C地址是0x44,如果模块上ADDR引脚接高电平则是0x45,记不清的话排查“I2C无应答”问题时要先确认这一位。温度换算公式里的-45和175是数据手册规定的,把16位原始值映射到-45~130℃区间。湿度就是把0~65535映射到0~100%。
数据滤波是我在实测后认真做的。刚上电时SHT30读数会有起伏,尤其是湿度值,有时候会突然跳3~5%RH。只读一次数据直接用,显示界面数值会抖,触发报警也可能因为一个噪声点误报。我用的是滑动平均滤波,缓冲区长度为10次采样,每次采完都重新求平均:
#define FILTER_BUF_LEN 10 float temp_buf[FILTER_BUF_LEN]; float hum_buf[FILTER_BUF_LEN]; uint8_t buf_index = 0; uint8_t buf_filled = 0; void filter_push(float temp, float hum) { temp_buf[buf_index] = temp; hum_buf[buf_index] = hum; buf_index = (buf_index + 1) % FILTER_BUF_LEN; if (buf_index == 0) buf_filled = 1; } float filter_get_temp(void) { int i, cnt = buf_filled ? FILTER_BUF_LEN : buf_index; float sum = 0; if (cnt == 0) return 0; for (i = 0; i < cnt; i++) sum += temp_buf[i]; return sum / cnt; }如果滤波窗口取太大,响应会变慢,传感器断线后要等好久才能反映到显示上。用10次窗口配合1秒采样间隔,就是10秒刷新一次平均值,酒窖环境本身变化很慢,这个响应速度完全够。
ADC部分,如果你要扩展空气质量模拟量传感器或者电源电压监测,要注意“stm32 adc切换通道”这个知识点。CubeMX配置成规则组多通道扫描模式后,HAL库默认一次只能配置一个通道,切换通道时要在规则组里重新设置Rank,而且要等待足够长的采样稳定时间。我实际用的方法很简单:每个采样周期内,先配置通道0采样一次,等10ms,再配置通道1采样一次,连续采三次丢弃第一次,取后两次平均。STM32的ADC采样电容充电需要时间,通道切换后第一次采样结果往往会偏低,丢弃第一次是通用做法。
3.3 主循环状态机与报警联动逻辑
软件主循环没有用复杂的操作系统,就是裸机状态机加1秒周期任务。整个流程可以理解为:采集传感器数据 → 滤波 → 更新LCD → 判断报警阈值 → 控制继电器 → 串口打印日志。伪代码框架:
while (1) { if (sensor_read_ok == 0) { handle_sensor_error(); } else { filter_push(temp, hum); display_update(filter_get_temp(), filter_get_hum(), light_lux); alarm_check(); relay_control(); uart_send_log(); } delay_ms(1000); }报警阈值我用的是“上限+回滞”策略,这是工程上防止继电器频繁动作的关键。比如设置高温报警上限18℃,回滞值0.5℃,那么实际逻辑是:温度超过18.0℃启动排风扇,等温度降到17.5℃才关闭。如果不加回滞,温度恰好卡在18℃上下抖动时,继电器会以秒级频率反复吸合释放,继电器寿命和风扇寿命都会大打折扣。湿度控制逻辑也类似,低于60%RH开启加湿器,高于63%RH关闭,中间留3%的回滞区间。
继电器控制还有一个细节:上电瞬间不要立刻操作继电器。STM32复位到外设初始化完成需要一段时间,如果GPIO默认输出状态不对,继电器可能在上电瞬间乱跳一次。我在main函数开始的地方先把继电器引脚设为确定电平,延时2秒再进入主循环,避免上电误动作。按键阈值设置最好也锁一下,长按按键3秒进入设置模式再允许修改,防止日常误触。
LCD显示这一块我用的是I2C接口的LCD1602,只用了4根线就能显示两行16列字符。这里有个中文显示的坑,如果你买的LCD1602模块支持中文,或者用OLED12864显示中文,需要处理字符编码问题。STM32工程源码里默认是UTF-8编码,而LCD12864字库往往是GB2312/GBK,中文字符串直接扔进去显示出来会是乱码。碰到“stm32 gbk转utf8”这个搜索热词的问题,本质就是这个编码不匹配。解决办法是:要么在显示函数里把UTF-8中文字符串转成GBK,要么直接用取模软件把中文点阵写死成数组。LCD1602这种字符屏更好办,显示英文和数字就完事了,中文状态信息用“T: 13.2C H: 65%”这种简洁格式,反而更清爽。
3.4 串口日志与数据上送
开发和调试阶段,串口日志是唯一能“看到”MCU内部状态的窗口。我固定用了波特率115200,日志格式做了统一:
[TIME 012345] T=13.2C H=65.3% LS=123Lux FAN=ON HUM=OFF ALARM=NO这个格式一行就把所有关键状态打完了,隔1秒打印一次。用串口助手看日志时,配合时间戳功能能直观看到传感器数值变化与报警动作的关系。如果你也想做“精确到微秒”的时间戳,可以用DWT计数器换算,但一般调试场景秒级时间戳就够了。
远程上报我用ESP8266模块,USART2连接,AT固件模式。ESP8266的逻辑是:上电后先发AT测试握手,然后AT+CWJAP连WiFi,再AT+CIPSTART建立TCP连接,最后AT+CIPSEND上传字符串。最简单的方式是搭建一个TCP服务器或者用MQTT转发。如果要上云,可以参考“freertos stm32物联网网关”的思路,在STM32上跑FreeRTOS加MQTT客户端,把数据推给云平台,不过那是第二个版本的扩展内容,第一版只要保证本地功能自洽就足够了。
4. 现场调试中的常见问题与排查实录
4.1 传感器通信故障排查(I2C与单总线)
项目最痛苦的阶段是在酒窖现场调试通信问题。SHT30一段时间后读数不更新,排查步骤很有代表性。第一直觉是硬件接线松了,但重新插拔排线后依然没数据。这时我通过串口打印HAL库返回的错误码,发现是I2C读超时。然后用万用表量SCL和SDA对GND的电压,发现SDA线电压只有0.8V左右,明显不在高电平状态。最后排查出来是传感器板上一个焊盘残留了焊锡,SDA和SCL之间轻微桥接。处理完短路之后,I2C恢复。
DS18B20的排查又是另一套思路。三个探头焊接后只识别出两个,第三个始终搜不到。用示波器看单总线波形,发现在第三个探头的接入点信号幅值只有2.2V左右,上拉到3.3V的电阻被两个探头拉低还能保持高电平,但三个探头并联后总线上拉能力不足。把上拉电阻从4.7k换成1k,三个探头全部识别正常。所以单总线上挂多个DS18B20时,上拉电阻不是固定的,要根据挂载数量调整:一个探头用4.7k没问题,三个探头建议用2.2k或1k。
排查I2C问题时,我还有个经验是看总线上有没有多个设备地址冲突。SHT30地址0x44、BH1750地址0x23,它们不会冲突,但如果两块SHT30模块都默认地址0x44就会打架。这个在软件枚举总线设备时很容易验证:分别发地址,看哪些设备有ACK回包。
4.2 显示、延时与ADC采样的坑
LCD1602白屏或显示乱码是显示环节的高频问题。I2C版LCD1602背部有个蓝色电位器,用来调节对比度,出厂默认角度在多数环境下发白。我第一次调试时忘了这件事,排查了半天接线和驱动代码,最后问题就是电位器没调到位。LCD1602初始化时序有一点要注意:数据手册要求的延时是毫秒级,上电等待时间不能省,否则第二行显示不正常。
延时函数卡死这个问题也在软件调试中出现过。我把DWT延时函数和HAL_Delay混用,结果SysTick中断和DWT的CYCCNT计数概念混淆,导致某个传感器等待循环永远跳出不了。DWT是基于内核调试单元的计数器,和SysTick完全独立,但它不产生中断,无法依赖DWT超时跳转。如果在延时函数里加了“等待标志位”逻辑,一定要确认标志位是在中断里还是主循环里置位的,否则极容易卡死。我的建议是:要么统一用HAL_Delay做所有长时间延时,要么统一用DWT,不要混用。中断服务函数里不要调用任何依赖等待的延时,尤其是HAL_Delay,它本身会等SysTick,中断里用就是死锁。
ADC采样值跳动的问题我在2.3节总结过,但现场还遇到过一种特殊情况:ADC引脚悬空时,数值在4095和0之间随机跳,串口日志里偶尔出现一个巨大的异常值。原因是悬空引脚电压不稳,被内部寄生电容充放电干扰。解决办法是:不用的ADC通道不要启用,必须启用的通道在PCB上一定要接一个10k下拉电阻到GND,把引脚电平钳位在确定状态,否则异常采样值会影响滤波结果。
4.3 实测数据与系统调优记录
系统稳定运行后,我做了48小时连续测试。测试环境是一个半地下酒窖,模拟真实使用状态放置了大约30瓶葡萄酒的储物架,酒窖白天开灯巡检两次,其余时间无人员进出。关键测试结果整理如下:
| 时间点 | 实测温度(℃) | 标准温湿度计温度(℃) | 偏差 | 实测湿度(%RH) | 标准温湿度计湿度(%RH) | 偏差 |
|---|---|---|---|---|---|---|
| 第1小时 | 14.2 | 14.1 | +0.1 | 64.5 | 65.0 | -0.5 |
| 第12小时 | 13.8 | 13.7 | +0.1 | 66.2 | 67.0 | -0.8 |
| 第24小时 | 14.0 | 13.9 | +0.1 | 65.0 | 65.5 | -0.5 |
| 第36小时 | 13.6 | 13.5 | +0.1 | 63.8 | 64.5 | -0.7 |
| 第48小时 | 13.9 | 13.8 | +0.1 | 65.7 | 66.2 | -0.5 |
这个偏差水平对酒窖管理是完全可以接受的,因为酒窖本身就不要求像实验室那样的温湿度精度,重要的是长期稳定性而不是绝对值。系统在整个48小时内没有出现通信中断、死机或误报警,继电器总共动作了9次(排风扇6次、加湿器3次),频率合理。
运行稳定后我还做了两项实测优化。一是把主循环任务从500ms改成1秒周期,传感器读数没变化,但日志输出量少了一半,系统整体功耗还稍微降了一点。二是把报警阈值写入Flash掉电保存,系统断电重启后不用重新设置。这里要特别注意,往Flash里写数据时先擦除整个扇区,并且不要在中断服务函数里执行擦写操作,避免发生“freertos flasah写入被打断”这类问题,也就是擦写过程中被高优先级中断打断导致数据损坏。实际测试断电100次,阈值设置均能正确恢复,说明这个保存方案可行。
结尾:一点个人心得
这个项目做完,我最大的体会是,酒窖环境监测系统真正难的不是代码,而是如何在高湿度、低温和长时间无人维护的环境下保持可靠。每一个看似无聊的设计,比如传感器探头远离主板、继电器功耗隔离、报警回滞、Flash掉电保存,都是在真实场景里被问题逼出来的。如果你也想做类似的项目,我建议第一版别贪多,把温湿度采集、LCD显示、报警、继电器控制这四条链路彻底跑稳,再去折腾WiFi上云和多节点组网。ESP8266通信不稳定的时候,至少你的本地监测系统还能守住基本盘。另外一个小技巧:传感器模块的排线全部用防呆接口(比如PH2.0端子),现场接错线换一根就完事,比直接焊线省心得多。