开源STM32数据中心环境监控系统:低成本补盲温湿度水浸烟雾门磁供电监测

发布时间:2026/9/17 23:21:56
开源STM32数据中心环境监控系统:低成本补盲温湿度水浸烟雾门磁供电监测 机房这摊事最怕的从来不是服务器自己坏而是它坏的时候你完全不知道周围发生了什么。我见过太多小机房、边缘机柜、实验室机架服务器本身双电源、RAID、冗余风扇配得齐齐整整结果隔壁空调冷凝水管堵了地板下积了两厘米水等到运维发现的时候UPS 电池仓已经泡了。也有相反的情况——机柜门被保洁阿姨顺手打开忘了关空调出风口直吹某个位置的进风温度低到 15℃机柜另一侧却因为风道短路常年 32℃。这类事故的共同点是出事之前没有任何人收到过告警。我这次要拆解的这套STM32 数据中心环境监控系统就是为了补上这个盲区。它做的事情很朴素用一颗 STM32 做主控挂上温湿度、水浸、烟雾、门磁、供电电压这几路传感器把数据按固定周期采回来本地做阈值判断超限就本地声光告警同时通过 RS485/Modbus 或者以太网把数据上送到上位机。整套东西源码和原理图全部开源硬件成本两百块出头你可以直接照着原理图打板也可以拿现成的 STM32F103C8T6 核心板加快捷模块先搭一版验证逻辑。适合谁看一类是做嵌入式、想找一个能把 GPIO、ADC、I2C、UART、定时器、看门狗全部串起来的完整项目练手的同学另一类是真的有个小机房要管、预算又批不下来的运维和 IT 负责人。先说清楚这套东西不是替代专业动环监控的专业机房动环有专门的主机和传感器总线功能、可靠性、认证都是另一个量级。它真正的定位是低成本补盲在几十平米以内、机柜数量不多、没有集中监控预算的场景里用极低的成本把最关键的几个风险点盯住。下面我把整个项目的设计思路、原理图关键细节、代码实现、调试过程和我自己踩过的坑一层一层拆开来讲。1. 项目整体设计与方案选型拆解1.1 机房环境里真正会出事的那几个量很多人一开始做这类项目第一反应是多装几个传感器越多越好。我一开始也这么想后来发现方向偏了。监控系统的价值不在于测得多而在于测的那几路刚好对应真实事故。我复盘过自己经历过和听同行讲过的机房事故归下来其实就那么几类对应的监测量非常集中。第一类是温度异常。来源包括空调停机、制冷剂泄漏、风道被挡、机柜内风扇故障、以及最隐蔽的风道短路——机房整体温度正常但某个机柜进风口温度超标。所以温度测点不能只放一个至少要有机房环境和机柜进风两个位置。第二类是湿度异常。湿度过高凝结水会直接威胁电路板而且往往是漏水的前兆湿度过低静电风险陡增冬天北方机房很常见。所以湿度要和温度一起测用的是同一颗传感器边际成本几乎为零。第三类是漏水。空调冷凝水、加湿器管路、楼上管道渗漏这是最容易被忽视又最致命的一类。水浸传感器必须放在地板下、空调内机下方、水管接头附近这几个点。第四类是烟雾。这个不用多说早发现一分钟损失差一个量级。第五类是门禁状态。机柜门、机房门的开合既关系到安全也关系到风道。门常开会导致局部温度漂移长期开着门还意味着有人在无记录状态下接触设备。第六类是供电状态。这个容易被漏掉但特别关键市电断了、UPS 切换了、电压跌落了都需要第一时间知道。测一路交流电压或者直流母线电压成本很低价值很高。我把这六类需求整理成一张表作为整个项目的需求基线后面所有硬件选型和软件逻辑都是围绕它展开的。监测项典型测点位置建议传感器接口类型采样周期环境温湿度机房中部、回风口SHT30 / SHT31I2C5 s机柜进风温湿度机柜前门下部SHT30I2C5 s漏水地板下、空调下方电极式水浸探头GPIO / 比较器1 s烟雾吊顶、机柜顶部MQ-2 类半导体烟感ADC10 s门禁机房门、机柜门干簧管 / 霍尔GPIO 中断事件触发供电电压配电箱、UPS 输出分压 ADCADC1 s这张表看起来简单但它决定了后面原理图的接口分配和代码里的任务调度节奏。新手最容易犯的错是拍脑袋决定温度 1 秒采一次结果 I2C 总线被占满同时传感器自身的热漂移也会让读数偏高。1.2 主控为什么落在 STM32F103 上选型这一步我纠结过一阵候选有三个方向STM32F103C8T6、ESP32、树莓派。最后落回 STM32理由不是性能最强而是匹配场景。先说 ESP32。它自带 Wi-Fi看起来上云很方便但它有一个在机房场景下很尴尬的问题Wi-Fi 是射频在金属机柜密集的环境里信号衰减非常严重而且 2.4G 频段在机房里本身就是被各种设备占满的。更关键的是可靠性——ESP32 跑 FreeRTOS 加网络栈长时间运行的稳定性远不如裸机或者轻量 RTOS 的 STM32断电恢复后的联网重连、DHCP 超时这些逻辑都要自己兜住。做玩具可以做需要连续跑一年的监控节点我心里没底。再说树莓派。算力强、能跑 Python、能接数据库但它本质是 Linux 系统SD 卡挂掉是经典问题断电重启有概率起不来而且功耗和成本都比 MCU 方案高一个量级。放在机房里你等于又往里塞了一台小型服务器本身成了需要被监控的对象。STM32F103C8T6 的胜出理由就很清晰了裸机或轻量 RTOS 就能跑完所有任务没有操作系统和文件系统断电后上电即可恢复正常不存在卡在某个状态起不来的问题。它外设够用——2 个 I2C、3 个 USART、2 个 SPI、2 个 12 位 ADC、足够多的 GPIO还能用 IWDG 独立看门狗兜底。价格上核心板十几块钱自己打板加上所有元件也就六七十。功耗低5V 供电下整机几十毫安可以直接挂在机柜的 USB 口上。当然它也有短板主要是没有原生以太网 MAC。解决办法有两个一是走 RS485 总线用 Modbus RTU 协议上送到一台集中器这是我最推荐的方案因为 RS485 差分传输在机房里抗干扰能力强一根双绞线能挂 32 个节点二是需要以太网时选 STM32F407它自带 MAC配 LAN8720 PHY 芯片或者干脆用 W5500 这种 SPI 转以太网的硬件协议栈芯片SPI 挂上去就能用开发量小很多。这两个方案我在源码里都留了适配层切换起来不算麻烦。1.3 一套采集-判断-上送的分层结构整个系统的软件结构我拆成三层这个分层方式是我做了几个类似项目之后总结出来的好处是每一层单独测试都容易出问题也好定位。最下面是采集层。它只负责一件事把原始数据从外设读进来做必要的换算和校验然后把结果丢进一个全局的数据缓冲区。这一层里包含 I2C 读 SHT30、ADC 采样烟感和电压、GPIO 读水浸和门磁。它不判断好坏不告警不通信纯粹是搬运。中间是判断层。它按固定周期扫描数据缓冲区把每个值跟阈值对比用带迟滞的状态机判断是否需要告警维护告警等级和消抖计数器。这一层是系统的大脑逻辑必须清晰、可重入、不阻塞。最上面是通信层。它只负责把数据打包成 Modbus 寄存器或者自定义帧通过 UART/以太网发出去同时接收上位机的查询请求。它不关心数据是怎么来的、合不合法。三层之间用全局结构体和标志位交互不直接用队列——裸机上用队列反而增加复杂度。数据缓冲区做成一个结构体数组每个通道有当前值、上次值、状态、计数器这几个字段结构清晰调试的时候直接在 Keil 的 Watch 窗口里看就行。这种傻但稳的设计在监控类项目里比花哨的架构靠谱得多。2. 硬件原理图里那些容易翻车的地方2.1 最小系统与供电链路最小系统这块原理图上东西不多但每一个都值得说。主控 STM32F103C8T68MHz 无源晶振做 HSE两个匹配电容按晶振规格书给的负载电容反推。举个例子如果晶振标称 CL 20pFPCB 上单边杂散电容按 3~5pF 估那么 C1 C2 ≈ 2 × (20 − 4) 32pF这个值在实际布板后可以微调晶振起振异常时优先怀疑这两个电容。RTC 用的 32.768kHz 晶振配 12.5pF 负载电容时单边电容通常在 10pF 上下注意这个晶振对走线长度很敏感尽量靠近芯片。复位脚 NRST 上拉 10kΩ 到 3.3V并联 100nF 到地这个电容既有滤波作用也保证上电时有足够的复位低电平时间。BOOT0 一定下拉 10kΩ 到地否则上电可能进不了用户程序——这个坑我踩过现象是烧录完程序不跑查了半天以为是芯片锁了其实是 BOOT0 悬空被干扰拉高了。供电链路是我最想强调的部分。系统对外是 12V 或者 5V 输入内部需要 3.3V。新手最常见的做法是 12V 经 LM2596 降到 5V再用 AMS1117-3.3 线性稳到 3.3V。逻辑上没问题但有个物理事实绕不过去AMS1117 是线性稳压器它把多余的压差全部变成热。5V 到 3.3V如果整机电流 300mA那么发热功率就是 (5 − 3.3) × 0.3 0.51WSOT-223 封装在密闭机柜里温度能上到 80℃ 以上长期运行就是隐患。我的做法是全部走开关电源12V 先用 MP1584 或 MP2359 这类同步降压模块降到 5V效率 90% 以上3.3V 再单独用一颗 MP2359 或者 SY8089 从 5V 降下来轻载效率高静态电流小发热基本可以忽略。如果图省事用现成模块那就买那种带同步整流的小型降压板别用老式的 LM2596 加 AMS1117 组合长期跑。去耦电容也要说清楚。每个 VDD 引脚旁边必须有一颗 100nF 陶瓷电容这个电容不是有就行它的位置比数量更重要——必须紧贴引脚走线越短越好地回流路径越小越好。VBAT 单独接 3.3V 或者纽扣电池。整板电源入口再并一颗 10μF 钽电容和一颗 100μF 电解电容用来吸收大电流突变的低频波动。模拟电源 VSSA/VDDA 单独用磁珠或者 0Ω 电阻从数字电源隔离过来再配 100nF 1μFADC 精度会明显改善。2.2 传感器接入与接口分配表接口分配这件事如果一开始不规划好后面加传感器的时候会很痛苦。我的原则是同类型、低速率、需要隔离的传感器走 I2C需要连续采样、有模拟特性的走 ADC开关量走 GPIO 加中断长距离传输走 RS485。SHT30 温湿度传感器走 I2C1地址 0x44ADDR 脚接地或 0x45ADDR 接 VDD。如果要在同一条总线上挂两个 SHT30机房环境一个、机柜进风一个就把其中一个的 ADDR 拉高另一个拉低两个地址不冲突。SDA/SCL 各配 4.7kΩ 上拉到 3.3V注意这两个上拉电阻不能省也不能用太小的值否则总线电容负载大时上拉太强会导致上升沿过冲。MQ-2 烟感输出走 ADC1 的通道 0它的模拟输出随烟雾浓度变化但我必须提醒一件事MQ-2 需要预热冷机刚上电时读数完全不可信通常要预热 24 小时以上才能稳定上电后前几分钟的读数偏高代码里要做预热带过滤。另外它测的是可燃气体总量不是专测烟雾用作早期预警可以别当作消防认证设备用。水浸传感器我建议用比较器方案而不是纯 GPIO 读取。电极式探头本质是两个电极有水时电阻从兆欧级降到几十千欧级可以直接配合上拉电阻读 GPIO但电极长期通直流会电解腐蚀几个月下来电极就黑了。更好的做法是用交流驱动或者干脆选带内部交流驱动的成品水浸模块输出干接点或者电平信号STM32 端只需要读一个 GPIO。如果自己用比较器LM393 配一个电位器调阈值输出接 GPIO 并加 10kΩ 上拉。门磁用干簧管或者霍尔开关接 GPIO 并开启外部中断。这里有个必须做的动作加 RC 硬件消抖。门磁在开关瞬间会有几十毫秒的抖动如果软件里不做处理一次开门会产生一串中断告警记录里会出现十几条重复事件。硬件上串 10kΩ 电阻并联 100nF 电容配合软件里 50ms 的消抖窗口基本就干净了。接口分配整理成表如下外设STM32 资源引脚建议备注SHT30-1I2C1PB6/PB7地址 0x44环境温湿度SHT30-2I2C1PB6/PB7地址 0x45机柜进风MQ-2 烟感ADC1_IN0PA0需预热加 RC 滤波供电电压ADC1_IN1PA1分压后接入水浸GPIO / EXTIPA2上拉输入低电平有效门磁GPIO / EXTIPA3上拉输入低电平有效RS485USART2PA2/PA3与 EXTI 需重新分配调试串口USART1PA9/PA10打印日志SWD 下载SWDIO/SWCLKPA13/PA14保留调试口表里 PA2/PA3 的冲突是真实存在的我在实际项目里就把水浸和门磁改到了 PB0/PB1把 PA2/PA3 留给 USART2。这种资源冲突在画原理图阶段一定要用表格过一遍画完再改就得飞线了。2.3 模拟量采集与分压计算模拟量这块我拿供电电压监测这个最典型的例子讲透。假设要监测一路 12V 直流母线STM32 的 ADC 参考电压是 3.3V输入绝不能被拉到 3.3V 以上所以必须分压。选 R1 100kΩ上臂R2 10kΩ下臂。分压比 (R1 R2) / R2 110 / 10 11。输入 12V 时ADC 端电压 12 / 11 ≈ 1.0909V安全落在 3.3V 以内。如果母线可能冲到 24V那就得改分压比最坏情况下 ADC 端电压 24 / 11 ≈ 2.18V仍然安全。12 位 ADC 在 VREF 3.3V 时的量化单位是 3.3 / 4096 ≈ 0.8057mV。刚才 1.0909V 对应的码值大约是 1.0909 / 0.0008057 ≈ 1354。反推公式就是 Vin code × 3.3 / 4096 × 11。等效到母线电压的分辨率是 0.8057mV × 11 ≈ 8.86mV对监测 12V 系统来说完全够用。但光有分压电阻不行还有三件事必须做。第一加 RC 低通滤波。在分压点和 ADC 引脚之间串 10kΩ再对地并 100nF截止频率 f 1 / (2π × 10000 × 100e-9) ≈ 159Hz能把开关电源的高频纹波压下去。第二加钳位保护。用一颗 BAT54S 或者两个肖特基二极管把 ADC 输入钳在 0 到 3.3V 之间防止母线瞬态过压打坏 ADC 引脚。第三分压电阻的阻值不能太大。STM32 的 ADC 输入阻抗有要求源阻抗太大会导致采样不充分、读数偏低一般建议源阻抗控制在 10kΩ 量级以内100k 10k 的组合等效源阻抗约 9.1kΩ是可以接受的但采样时间要设长一点比如 239.5 个 ADC 周期。软件端再做一次滑动平均滤波取 16 次采样去掉最大最小值后求平均读数会很稳。这个滤波我不建议用复杂的卡尔曼16 点滑窗对监控场景足够了。2.4 告警输出与隔离保护告警输出有两路本地声光蜂鸣器 LED和干接点继电器。蜂鸣器用有源蜂鸣器5V 或者 3.3V 供电用一个 S8050 三极管驱动基极串 1kΩ发射极和集电极之间一定要并一颗续流二极管1N4148 或 1N4007方向是阴极接电源正极。很多人省这颗二极管短期内没事但蜂鸣器是感性负载关断瞬间的反向电动势会打到三极管时间长了会击穿。继电器同理而且继电器的线圈电流更大续流二极管绝对不能省。继电器的干接点输出我做了光耦隔离。原因很直接继电器控制的可能是外部的报警灯、门禁系统甚至消防联动这些外部回路的地电位跟你系统的地不一定相同直接连会把干扰引进来严重时烧主控。用 PC817 或者更快的 6N137把控制侧和负载侧地完全隔开。继电器输出侧再加一个 TVS 管抑制外部线上的浪涌。RS485 接口同样要防护。收发器用 SP3485 或者 MAX3485DE/RE 引脚用 GPIO 控制A/B 线各串一个 10Ω 电阻再各加一颗 TVS比如 SMBJ6.5CA到地终端匹配用 120Ω 电阻注意这个电阻只在总线两端各放一个中间节点不要放否则总线负载过重通信距离会大幅缩短。电源入口的防护也别省自恢复保险丝、TVS 管、共模电感这三样东西加起来成本不到两块钱但能挡掉很多现场意外。机房里大型设备的启停会在电源线上产生很大的浪涌没有防护MCU 会随机复位。3. 软件架构与核心代码怎么写3.1 定时采集与任务调度裸机跑多任务我的方案是SysTick 提供 1ms 时基任务用软件定时器轮询触发。不开 RTOS因为任务数量少、实时性要求也不高裸机反而更好调试。具体做法是定义一个任务结构体数组每个任务有周期、上次执行时间、回调函数指针。主循环里遍历这个数组比较当前 tick 和上次执行时间到点了就调用回调并更新上次时间。这种写法避免了HAL_Delay阻塞主循环也不会有多个任务互相抢占的问题。// 任务调度核心1ms 时基轮询式软定时器 typedef struct { uint32_t period_ms; // 任务周期 uint32_t last_tick; // 上次执行时刻 void (*handler)(void); // 任务回调 } task_t; static task_t tasks[] { {1000, 0, task_speed_sample}, // 水浸、门磁1s {1000, 0, task_voltage_sample}, // 供电电压1s {5000, 0, task_th_sample}, // 温湿度5s {10000, 0, task_smoke_sample}, // 烟雾10s {500, 0, task_alarm_check}, // 告警判断0.5s {100, 0, task_comm_poll}, // 通信轮询0.1s }; void scheduler_run(void) { uint32_t now HAL_GetTick(); for (uint8_t i 0; i sizeof(tasks) / sizeof(task_t); i) { if ((now - tasks[i].last_tick) tasks[i].period_ms) { tasks[i].last_tick now; tasks[i].handler(); } } }这个写法有个细节值得说now - last_tick用的是无符号减法即使HAL_GetTick()在 49 天左右回绕也不会出错这是处理 tick 溢出的标准做法。新手常写成if (now last_tick period)一旦 tick 回绕就会卡死。IWDG 独立看门狗是必须开的。配置上LSI 典型频率 40kHz预分频设为 64那么计数时钟是 40000 / 64 625Hz每个计数周期 1.6ms重装值填 1250 得到 2 秒超时。喂狗放在scheduler_run()之外的主循环里也就是所有任务跑完一轮才喂一次。这样只要任何一个任务卡死超过 2 秒看门狗就会复位系统。这个设计思路很重要——喂狗的位置决定了看门狗保护的范围。如果把喂狗放在定时器中断里那主循环卡死也不会复位看门狗等于白开。static void iwdg_init(void) { hiwdg.Instance IWDG; hiwdg.Init.Prescaler IWDG_PRESCALER_64; hiwdg.Init.Reload 1250; // 40000/64 625Hz - 625 个计数为 1s HAL_IWDG_Init(hiwdg); } // 主循环喂狗在最后任何一个任务卡死都会触发复位 while (1) { scheduler_run(); HAL_IWDG_Refresh(hiwdg); }3.2 SHT30 驱动与 CRC 校验SHT30 走 I2C用标准库或者 HAL 都行。它有两种读取模式我选单次测量、无时钟拉伸、高重复性命令字是 0x2C06。发送命令后要等大约 15ms 才有数据然后读 6 个字节温度高、温度低、温度 CRC、湿度高、湿度低、湿度 CRC。这里最容易忽略的是CRC 校验。I2C 总线上如果受到干扰读回来的数据可能整体偏移或者某位翻转没有 CRC 你根本发现不了只会看到一个看起来合理但其实是错的数值。SHT30 每个测量字后面都跟一个 CRC 字节多项式 0x31初值 0xFF必须校验。// SHT30 CRC-8 校验多项式 0x31初值 0xFF static uint8_t sht30_crc8(const uint8_t *data, uint8_t len) { uint8_t crc 0xFF; for (uint8_t i 0; i len; i) { crc ^ data[i]; for (uint8_t b 0; b 8; b) { crc (crc 0x80) ? ((crc 1) ^ 0x31) : (crc 1); } } return crc; } // 单次读取温湿度返回 0 表示成功 int sht30_read(float *temp, float *humi) { uint8_t cmd[2] {0x2C, 0x06}; uint8_t buf[6]; if (HAL_I2C_Master_Transmit(hi2c1, 0x44 1, cmd, 2, 100) ! HAL_OK) return -1; HAL_Delay(15); if (HAL_I2C_Master_Receive(hi2c1, 0x44 1, buf, 6, 100) ! HAL_OK) return -1; if (sht30_crc8(buf[0], 2) ! buf[2]) return -2; // 温度 CRC 失败 if (sht30_crc8(buf[3], 2) ! buf[5]) return -3; // 湿度 CRC 失败 uint16_t raw_t (buf[0] 8) | buf[1]; uint16_t raw_h (buf[3] 8) | buf[4]; *temp -45.0f 175.0f * raw_t / 65535.0f; *humi 100.0f * raw_h / 65535.0f; return 0; }温度换算公式T -45 175 × S / 65535湿度RH 100 × S / 65535这两个是数据手册给的注意温度公式里的 -45 偏置忘了会差出几十度。另外 I2C 地址在 HAL 库调用时要左移一位0x44 1这个也是新手高频错误。读取失败时的处理策略也很关键。我的做法是连续失败 3 次才把该通道标记为故障单次失败直接丢弃用上一次的有效值顶替同时累加错误计数。因为 I2C 偶发失败太常见了如果一次失败就告警运维会被误报淹没最后干脆不看告警了这比没有告警更糟。3.3 告警状态机与迟滞判断告警逻辑如果只是简单的if (value threshold) alarm 1;在现场会被骂死。原因是临界抖动温度在阈值上下浮动 0.1℃告警会疯狂开关继电器哒哒响上位机记录刷屏。正确的做法是双阈值迟滞 持续时间确认。以温度上限 30℃ 为例开启告警的阈值设 30℃解除告警的阈值设 28℃中间 2℃ 是死区。同时要求超限状态连续保持 3 个采样周期也就是 15 秒才真正触发告警避免瞬时扰动误报。// 带迟滞和持续时间确认的告警状态机 typedef struct { float value; // 当前值 float hi_on; // 上限触发阈值 float hi_off; // 上限解除阈值 uint8_t confirm_cnt; // 持续计数 uint8_t need_cnt; // 需要的持续次数 uint8_t state; // 0正常 1告警 } alarm_ch_t; void alarm_update(alarm_ch_t *ch) { if (ch-state 0) { if (ch-value ch-hi_on) { if (ch-confirm_cnt ch-need_cnt) { ch-state 1; ch-confirm_cnt 0; alarm_report(ch); } } else { ch-confirm_cnt 0; } } else { if (ch-value ch-hi_off) { if (ch-confirm_cnt ch-need_cnt) { ch-state 0; ch-confirm_cnt 0; alarm_report(ch); } } else { ch-confirm_cnt 0; } } }这套逻辑同时适用于温度上限、湿度上下限、电压跌落、烟雾浓度等所有通道。门磁和水浸不一样它们是事件型量检测到就触发但仍然要做消抖用 50ms 的软件滤波窗口滤掉抖动。告警输出也要分级。我分了三级一级是提示只有 LED 慢闪二级是警告LED 快闪加本地蜂鸣器短鸣三级是严重蜂鸣器长鸣加继电器输出。分级的意义在于运维能一眼看出严重程度不用去翻日志。3.4 Modbus RTU 上送与 CRC16上位通信我选 Modbus RTU 跑在 RS485 上主要原因是它足够简单、足够通用市面上的组态软件、PLC、甚至自己写的 Python 脚本都能对接不用为每个项目重新写协议解析。Modbus 帧格式很固定从站地址、功能码、数据、CRC16 校验。我用功能码 0x03读保持寄存器把采集到的数据映射到寄存器地址上上位机定时轮询就行。CRC16 的多项式是 0xA001初值 0xFFFF注意它是低字节在前。// Modbus RTU CRC16多项式 0xA001初值 0xFFFF uint16_t modbus_crc16(const uint8_t *buf, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ buf[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) { crc 1; crc ^ 0xA001; } else { crc 1; } } } return crc; }寄存器映射我这样安排0x0000 是机房温度乘以 10整数传输避免浮点解析麻烦0x0001 是湿度乘 100x0002 是机柜温度乘 100x0003 是供电电压乘 100单位伏0x0004 到 0x0007 分别是四个告警位的位掩码0x0008 是系统状态字。上位机读 9 个寄存器就拿到全部信息。RS485 收发切换的时序是个坑。发送前拉高 DE发完最后一个字节后不能立刻拉低 DE要等 UART 的 TC传输完成标志置位确认移位寄存器里的最后一个字节真正发出去了再拉低 DE 切回接收。如果提前拉低最后一个字节的高位会被截断上位机收到的帧 CRC 永远错。正确做法是用HAL_UART_Transmit之后等__HAL_UART_GET_FLAG(huart2, UART_FLAG_TC)或者用中断发送并在 TC 中断里切换 DE。还有个细节Modbus RTU 靠 3.5 个字符时间的静默间隔来分帧波特率 9600 时大概是 4ms。接收端如果不用 DMA 空闲中断很容易把一帧拆成两帧。我的做法是 UART 开 DMA 循环接收加空闲中断空闲中断触发时说明一帧结束这时候去算长度、验 CRC非常稳。4. 从零搭建的完整实操过程4.1 环境搭建与烧录开发环境我推荐STM32CubeMX Keil MDK5 ST-Link这个组合。CubeMX 负责图形化配置引脚、时钟树、外设生成 HAL 库初始化代码Keil 负责写业务逻辑和编译下载。虽然有人吐槽 HAL 库效率低、代码臃肿但对监控类项目来说它的开发速度和可维护性远比那点效率重要。CubeMX 里的关键配置我列一下。时钟树部分HSE 选 8MHz 晶振PLL 倍频到 72MHz 作为 SYSCLKAPB1 分频到 36MHz注意 APB1 最高 36MHz超了外设会工作异常APB2 保持 72MHz。调试接口选 Serial Wire一定要选否则下载一次之后芯片的 SWD 口被占用下次就下不进去了得用 BOOT0 拉起的方式救回来。I2C1 设成标准模式 100kHz 或者快速模式 400kHz我建议先用 100kHz 调试稳定后再提速。USART1 设 115200 做日志口USART2 设 9600 做 Modbus。ADC1 开两个通道扫描模式加 DMA 循环搬运。生成代码后先别急着写业务。第一步是确认时钟跑对了在main里点一个 GPIO用示波器或者逻辑分析仪量翻转频率或者用HAL_GetTick()让 LED 精确一秒翻转一次用手机秒表对一下。这个看似多余的步骤能排除掉一半的玄学问题——很多莫名其妙的通信异常根子都是时钟没配对。烧录用 ST-LinkSWD 四线3.3V、GND、SWDIO、SWCLK。Keil 里 Debug 选 ST-Link DebuggerFlash Download 里勾上 Reset and Run下载完自动运行。如果提示 No target connected先查供电再查 SWDIO/SWCLK 有没有接反最后查芯片是不是被读保护了。读保护的情况可以用 STM32CubeProgrammer 做全片擦除解开。如果手上没有 ST-Link用 USB 转 TTL 走串口 ISP 下载也行BOOT0 拉高复位用 Flash Loader Demonstrator 或者 STM32CubeProgrammer 的串口模式下载下完把 BOOT0 拉回低电平再复位。这条路慢一点但不需要额外买调试器。4.2 分段验证先点灯再单传感器最后联调我特别反对一种做法把所有代码写完硬件焊完然后一次性上电调试。出了问题你面对的是十几个可能的故障点逐个排除能耗掉一整周。正确的方式是分段验证每一段都有明确的通过标准。第一段最小系统。只焊主控、晶振、复位、BOOT0、电源、SWD 口。上电后用 ST-Link 能不能识别到芯片能识别说明最小系统活了。然后烧一个 LED 闪烁程序确认程序能跑起来、时钟配置正确。第二段电源链路。不焊主控只焊电源部分输入 12V用万用表量 5V 和 3.3V 输出。空载和带载分别量一次带载用一个 100Ω 电阻模拟。电压偏差控制在 ±3% 以内用示波器看一下纹波峰峰值超过 100mV 就要查布局和电容。第三段单独验证每个传感器。SHT30 接上写个最小程序每秒打印一次温湿度用手捂住传感器看数值有没有反应哈一口气看湿度会不会升。MQ-2 上电后记录它 24 小时内的读数漂移曲线确定预热时间和基线值。水浸探头拿一杯水滴上去看能不能触发。门磁拿磁铁靠近离开看中断计数对不对。这一段最重要的是记录每个传感器的原始基线数据后面设阈值全靠它。第四段通信。不用实际总线先把 USB 转 TTL 接到 USART2用电脑上的 Modbus 调试助手比如 Modbus Poll当主站STM32 当从站读寄存器。能稳定读出数据、CRC 校验通过、连续读一万次不出错通信这一层就算过了。第五段联调。接上 RS485 收发器用真实双绞线连接长度按现场情况来测试误码率。同时把所有传感器一起跑上观察有没有资源冲突、有没有互相干扰。这个流程走下来通常三到五天能出一版稳定可用的样机。比一次性上电省的时间多得多。4.3 阈值标定与老化测试阈值这事别照抄网上的数字。我见过有人直接抄温度上限 35℃结果机柜进风口正常就在 34℃天天告警。阈值必须基于自己现场的基线数据来定。标定方法是让系统连续记录 72 小时的原始数据不做任何告警判断把温度、湿度、电压、烟雾基线的最大值、最小值、平均值、标准差都算出来。然后按平均值 ± 3 倍标准差作为初版阈值再根据业务要求微调。机房环境温度的行业通行参考是 18~27℃这是一般设备厂商建议的运行区间湿度一般控制在 40%~60% RH上限不超过 60% 主要是防止结露下限不低于 40% 是为了控制静电。但这些是大方向具体到你的机房还是要看现场。比如你的机房本来就偏热空调设定 24℃那上限阈值定在 28℃ 加 2℃ 迟滞就比较合理定 30℃ 就太晚了定 25℃ 又会天天误报。电压阈值也一样。12V 系统欠压告警设 11.0V解除设 11.5V过压告警设 13.5V解除设 13.0V。这些数字要结合你的电源类型来定开关电源和电池直供的容忍范围完全不同。老化测试这一步我建议至少跑72 小时。观察三件事一是看门狗有没有触发复位可以在复位后往备份寄存器写一个标志重启后打印出来就知道有没有复位过二是内存有没有泄漏虽然是裸机但看栈的使用情况、全局变量的变化三是告警有没有误报把告警日志导出来数一数。72 小时零复位、零误报才算可以往现场装。5. 踩过的坑与常见问题速查5.1 传感器读数异常的排查顺序温湿度读数不对这是被问得最多的问题。我总结了一个固定的排查顺序按这个顺序走九成问题能在十分钟内定位。第一步先排除物理因素。SHT30 靠近 MCU、LDO、DC-DC 这些发热源读数必然偏高。我实测过传感器离一颗满载的 AMS1117 两厘米温度读数能高 3℃ 以上。解决办法是传感器远离热源必要时用排线引出去或者开槽做热隔离。另外要注意传感器本身自发热也存在采样周期越密越明显所以 5 秒一次比 1 秒一次读数更准。第二步查 I2C 通信。用逻辑分析仪抓一次完整的读写波形看地址对不对、ACK 有没有、CRC 校验过不过。没有逻辑分析仪就在代码里打印每次读写的返回值连续失败说明是硬件问题偶发失败说明是干扰或者时序问题。第三步查上拉电阻和总线电容。SDA/SCL 没有上拉通信会完全不通上拉太弱比如用了 100kΩ上升沿太慢高速率下会出错上拉太强比如 1kΩ功耗大而且可能过冲。4.7kΩ 是 3.3V、100kHz 到 400kHz 场景下的通用值。第四步查换算公式。温度公式里的 -45 偏置、65535 的除数、HAL 库地址左移一位这三个地方错任何一个读数都会离谱但看起来很合理。第五步查电源。3.3V 纹波大、电压偏低都会让传感器工作异常。用示波器在传感器电源脚上量交流耦合看纹波。我把常见现象和对应原因整理成速查表现象最可能原因排查动作完全读不到地址错、无上拉、供电缺失逻辑分析仪抓波形量电源读数偏高 3℃ 以上靠近热源、自发热挪位置降采样率数值随机跳动CRC 未校验、总线干扰打开 CRC 校验缩短走线偶发通信失败上拉不当、总线电容大换 4.7kΩ 上拉降速率数值固定不变读取函数未被调用打断点看任务调度湿度长期 100%探头结露加透气膜避免直吹5.2 通信不稳与看门狗复位RS485 通信不稳现场最常见的原因有三个。一是没有共地。RS485 虽然是差分传输对共模干扰有抑制能力但共模电压超出收发器范围一般是 -7V 到 12V就会误码。长距离布线必须把 A/B 和地线一起走或者用带隔离的收发器。二是终端电阻乱放。120Ω 只在总线两端各放一个中间节点放了会让总线阻抗下降信号幅度变小。三是DE 切换时序错前面讲过了。看门狗复位排查思路是反过来的先看复位标志。STM32 的 RCC 寄存器里有 IWDG 复位标志上电后先读出来打印确认是不是真的看门狗触发的。确认是看门狗之后再找哪个任务卡住了。方法很简单在scheduler_run()里每个任务执行前后各翻转一个空闲 GPIO用逻辑分析仪看哪个任务的脉冲消失了就锁定是哪个任务卡死了。我自己遇到过两次看门狗复位一次是 I2C 读取时总线被拉死HAL_I2C_Master_Transmit里等超时卡了 2 秒以上另一次是 Modbus 接收用了阻塞式读取上位机不发数据就一直在等。两次的解决办法都是给所有可能阻塞的操作加超时I2C 超时设 10msUART 接收全部改成 DMA 空闲中断。这是裸机开发的铁律任何while等待都必须有超时退出。5.3 长期运行与现场部署注意现场部署有几个坑我在实验室完全没遇到装到现场才暴露。第一个是静电和雷击。机房的接地情况和实验室不一样有时候机柜接地电阻很大。我的做法是 RS485 和电源入口都加 TVS外壳接大地传感器线用屏蔽线并且屏蔽层单端接地。第二个是凝露。机房湿度控制失效的时候设备表面会结露尤其是夜里温度下降的时候。PCB 上做三防漆处理接口用带密封的端子能显著提升存活率。第三个是取电位置。别从服务器机柜的 PDU 取电那里可能随时被维护动作断电。最好从机房照明回路或者独立的监控电源取电条件允许再加个小的后备电池或者超级电容撑过短时断电避免频繁重启。第四个是物理固定。水浸探头要放在真正会积水的最低点别随手丢在地板上。温湿度传感器要装在能代表整体环境的位置别塞在机柜最底层角落那里温度永远比机房平均高好几度。第五个是标签和记录。每个探头贴标签写上位置和通道号原理图、接线表、寄存器映射表都留一份纸质文档在机房里。这事看起来老土但等你半年后去维护面对一团线你会感谢当时的自己。6. 可以往下扩展的几个方向这套东西跑通之后能扩展的地方很多我按投入产出比排个序供你参考。最划算的扩展是加一路 4G 或者以太网上报。现有的 RS485 方案需要上位机主动轮询如果上位机本身在机房内机房整体断电的时候它也一起挂了。加一个独立的联网上报通道把告警直接推到短信或者邮件等于给系统加了一层保险。用 W5500 走以太网是最省事的SPI 接口挂上去硬件协议栈不占 CPU。第二划算的是加历史存储。STM32F103 内部 Flash 只有 64KB 到 128KB存不了多少数据。外挂一颗 W25Q64 或者 AT24C 系列的 EEPROM把每分钟的数据存下来出事故之后可以回溯曲线这个价值比实时告警还大。我在另一个项目里用 W25Q128 存了两周的分钟级数据后来定位一次空调间歇性故障全靠它。再往后是多节点组网。一个机房往往需要三到六个测点用 RS485 总线挂多个从站地址分别设 1 到 6主机统一轮询。这时候要注意总线拓扑必须手拉手串联不能星型分叉分叉会引起阻抗不匹配反射严重。最后一个方向是本地显示。加一块 0.96 寸 OLED 或者 2.4 寸 TFT把当前温湿度、告警状态直接显示在设备上运维巡检的时候扫一眼就知道有没有问题不用开电脑看上位机。这个投入很小体验提升很明显OLED 走 I2C 挂到已有的总线上就行注意地址别和 SHT30 冲突SSD1306 默认地址 0x3C和 0x44、0x45 不冲突。这套监控系统我自己在三个小机房里跑了两年多最长的一个连续运行 700 多天没有复位过中间抓到过两次真实的空调故障和一次水管渗漏每次都比人发现得早。硬件成本算下来单点不到两百块最贵的其实是那几个传感器主控反而是最便宜的。如果你手头正好有这样的小机房要管或者想找一个能覆盖 GPIO、ADC、I2C、UART、DMA、看门狗、中断、状态机的完整练手项目这套源码加原理图直接拿来改就行改的时候记得先把电源和地处理好剩下的都好说。