首页 / 资讯中心 / 文章详情

STM32环境监测系统实战:从传感器选型到低功耗设计的工程化指南

STM32环境监测系统实战:从传感器选型到低功耗设计的工程化指南 ★ FEATURED ARTICLE
简介这是一套面向嵌入式初学者与电子类课程设计、毕业设计需求的STM32环境监测系统完整工程源码围绕温度、湿度、光照、空气质量等参数的采集、处理与上传展开适合具备C语言与单片机基础、希望动手实践ARM Cortex-M开发的读者。压缩包共106个文件约326KB以45个.h头文件与41个.c源文件为主体涵盖外设驱动、数据处理与通信逻辑另含8个.s启动汇编、1个.hex烧录固件、1个.uvprojx与.uvoptx工程文件及若干调试配置可直接用Keil打开编译。内容涉及STM32F10x系列标准外设库包含定时器、ADC、I2C、USART、CAN等模块驱动便于理解传感器接口、数据滤波与串口通信的实现思路。目前已有1225人学习下载可作为课程设计、智能家居或空气质量监测项目的参考方案帮助读者快速搭建硬件框架并掌握固件开发流程。1. 基于stm32的环境监测系统从“能跑”到“敢用”之间差了什么很多人在实验室里用基于stm32的环境监测系统跑通了温湿度采集OLED 上数字一跳就觉得项目成了。可真把它往机房角落、大棚边缘或者仓库墙上一挂三天后数据飘了、五天后再也连不上、第七天屏幕直接花屏——这类“实验室能跑、现场就废”的翻车现场我见过太多次。这个标题背后真正要解决的不是“怎么读 DHT11”而是怎么让一套低成本的 STM32 采集节点在无人值守条件下持续输出可信数据。它适合两类人一类是正在做课程设计或毕业设计、需要把 demo 变成可演示系统的学生另一类是做小型环境监控产品原型、想用最低 BOM 成本验证方案的嵌入式开发者。核心矛盾始终是采样精度、通信可靠性和功耗三者互相拉扯而 STM32 的片上资源刚好卡在“够用但紧张”的临界点上。2. 传感器选型与 STM32 资源分配别让 DHT11 拖垮整个系统2.1 为什么 DHT11 只适合验证不适合落地DHT11 最大的问题是单总线时序对中断极其敏感。STM32 跑 FreeRTOS 或者开了 SysTick 中断后DHT11 读回来的湿度经常是 0 或者 255。这不是代码写错了是时序被中断打断了。常见做法是读 DHT11 时关中断但关中断时间过长又会影响其他任务。我的建议是验证阶段用 DHT11 没问题真要落地就换 SHT30 或 AHT20I2C 接口数据手册标称精度 ±2%RH、±0.3℃而且不需要关中断。选型时看三个参数接口类型、精度、供电范围。SHT30 是 I2C2.4V~5.5V 供电和 STM32 的 3.3V 直接兼容。AHT20 更便宜但封装小手工焊接容易虚焊。BMP280 适合测气压但湿度得另配。如果标题里的“环境监测”包含空气质量那还得加 CCS811 或 SGP30这两个都是 I2C地址不冲突的话可以挂在同一条总线上。2.2 STM32 外设分配表与引脚规划以 STM32F103C8T6 为例资源分配直接决定后面能不能加功能。下面这张表是我一般会先画出来的外设引脚用途备注I2C1PB6/PB7SHT30 OLED挂两个设备地址不同USART1PA9/PA10调试串口打印原始数据USART2PA2/PA3无线模块接 LoRa 或 4G 模块ADC1PA0电池电压检测分压后输入SPI1PA5/PA6/PA7Flash 存储断网时缓存数据GPIOPC13状态指示灯心跳指示这张表的关键在于I2C 和 SPI 不要共用引脚USART1 留给调试USART2 才给通信模块。很多人把调试串口和通信串口搞混结果模块一上电就打印乱码查半天以为是模块坏了。2.3 最小系统搭建与供电去耦STM32 最小系统板市面上很多但用于环境监测时要注意两点一是晶振旁边那两个 20pF 电容不能省二是每个电源引脚旁边必须加 100nF 去耦电容。我见过一个案例A同学用洞洞板搭的板子SHT30 读数每隔几分钟跳一次最后发现是 3.3V 稳压芯片输出端只放了一个 10uF 电解电容高频噪声根本没滤掉。加了一个 100nF 瓷片电容后数据立刻稳了。供电方案上如果是有线供电AMS1117-3.3 够用如果是电池供电建议用低静态电流的 LDO比如 HT7333静态电流只有 4uA。别小看这个STM32 在 Stop 模式下整机电流可以做到 20uA 以内LDO 选错了直接翻倍。3. 从裸机到 FreeRTOS任务划分与采样周期怎么定3.1 任务优先级分配与栈大小估算裸机跑 while(1) 不是不行但一旦要同时处理采样、显示、通信、存储代码就会变成一团乱麻。上 FreeRTOS 之后任务划分我一般这么切// 任务优先级定义数值越大优先级越高 #define TASK_PRIO_SENSOR 4 // 传感器采样周期 2s #define TASK_PRIO_COMM 3 // 通信上报周期 30s #define TASK_PRIO_DISPLAY 2 // OLED 刷新周期 500ms #define TASK_PRIO_STORAGE 1 // Flash 存储事件触发 // 栈大小按经验给传感器任务 256 words通信任务 512 words #define STACK_SENSOR 256 #define STACK_COMM 512 #define STACK_DISPLAY 256 #define STACK_STORAGE 384栈大小不能拍脑袋。通信任务因为要跑协议栈和拼 JSON栈给 512 words 比较稳。传感器任务只是读 I2C 和换算256 够用。如果跑起来发现某个任务 HardFault先查栈溢出把 configCHECK_FOR_STACK_OVERFLOW 打开在 vApplicationStackOverflowHook 里打个断点。3.2 采样周期的确定与数据平滑环境监测不需要毫秒级采样。温度变化本身很慢采样太快除了增加功耗和存储压力没有任何好处。我的经验值温度湿度 2 秒一次空气质量 5 秒一次电池电压 60 秒一次。但 2 秒一次读回来的原始值仍然会跳。这时候需要做滑动平均。下面是一段我常用的滑动平均实现#define WINDOW_SIZE 8 typedef struct { float buf[WINDOW_SIZE]; uint8_t idx; uint8_t count; } MovingAvg; float moving_avg_update(MovingAvg *ma, float new_val) { // 环形缓冲区写入新值 ma-buf[ma-idx] new_val; ma-idx (ma-idx 1) % WINDOW_SIZE; if (ma-count WINDOW_SIZE) ma-count; // 计算当前窗口平均值 float sum 0; for (uint8_t i 0; i ma-count; i) { sum ma-buf[i]; } return sum / ma-count; }这段代码的逻辑是维护一个固定长度的环形缓冲区每次新数据进来覆盖最旧的数据然后求当前窗口内所有数据的算术平均。参数 WINDOW_SIZE 决定了平滑程度8 是我在响应速度和稳定性之间折中的选择。如果发现数据滞后太明显降到 4如果还是跳升到 16但代价是突变响应变慢。3.3 I2C 读取的阻塞与超时处理SHT30 读一次大概需要 15ms如果 I2C 总线被其他任务占用传感器任务就会阻塞。FreeRTOS 里可以用互斥量保护 I2C 总线但更简单的做法是给 I2C 读取加超时。HAL 库的 HAL_I2C_Mem_Read 最后一个参数就是超时时间我一般给 100ms。如果返回 HAL_TIMEOUT不要死等直接标记本次采样失败下次周期再试。HAL_StatusTypeDef sht30_read(float *temp, float *humi) { uint8_t cmd[2] {0x2C, 0x06}; // 高重复性测量命令 uint8_t data[6]; // 发送测量命令超时 100ms if (HAL_I2C_Master_Transmit(hi2c1, 0x441, cmd, 2, 100) ! HAL_OK) return HAL_ERROR; // 等待测量完成SHT30 高重复性模式需要约 15ms osDelay(20); // 读取 6 字节数据温度高、温度低、温度CRC、湿度高、湿度低、湿度CRC if (HAL_I2C_Master_Receive(hi2c1, 0x441, data, 6, 100) ! HAL_OK) return HAL_ERROR; // 温度换算-45 175 * raw / 65535 *temp -45.0f 175.0f * ((data[0] 8) | data[1]) / 65535.0f; // 湿度换算100 * raw / 65535 *humi 100.0f * ((data[3] 8) | data[4]) / 65535.0f; return HAL_OK; }注意 osDelay(20) 这里不能用 HAL_Delay因为 HAL_Delay 是忙等会浪费 CPU。用 osDelay 让出 CPU 给其他任务。CRC 校验这里我省略了实际产品里建议加上SHT30 的 CRC 多项式是 0x31。4. 通信与数据上报MQTT 还是 LoRa怎么选不后悔4.1 有线、WiFi、LoRa、4G 的适用边界通信方案的选择直接决定系统成本和部署难度。我列一个对比表方案模块成本传输距离功耗适用场景以太网低100m高固定机房、有网口WiFi中50m高室内、有路由LoRa中高3~10km低野外、无网覆盖4G Cat.1高无限制中移动场景、有基站如果标题里的“环境监测”是室内场景WiFi 模块比如 ESP-01S 最省事AT 指令透传就行。如果是野外大棚LoRa 是首选但要注意 LoRa 网关也得自己搭成本不低。4G 模块适合数据量小、上报频率低的场景但模块本身功耗大电池供电撑不了太久。4.2 MQTT 报文封装与 JSON 格式约定不管底层用什么传输应用层我建议统一用 MQTT。报文格式用 JSON字段名尽量短省流量。下面是一个上报示例// 拼接 JSON 报文字段名尽量短 char payload[128]; snprintf(payload, sizeof(payload), {\t\:%.1f,\h\:%.1f,\v\:%.2f,\id\:\%s\}, temp, humi, voltage, DEVICE_ID); // MQTT 发布QoS 1非保留消息 mqtt_publish(env/node01/data, payload, strlen(payload), 1, 0);字段含义t 是温度h 是湿度v 是电池电压id 是设备编号。QoS 选 1 保证至少送达一次但要注意 QoS 1 可能重复服务端要做去重。如果网络不稳定QoS 2 开销太大不建议。4.3 断网缓存与补传机制现场网络不可能一直稳定。断网时数据不能丢得先存 Flash等网络恢复再补传。这里有个坑Flash 擦写次数有限不能每条数据都存。我的做法是断网时每 5 分钟存一条网络恢复后按时间顺序补传补传完成后擦除对应扇区。// 断网时写入 Flash 缓存 void cache_data(float t, float h, uint32_t timestamp) { CacheRecord rec {.temp t, .humi h, .ts timestamp}; uint32_t addr CACHE_BASE cache_idx * sizeof(CacheRecord); // 先擦除再写入注意 STM32F1 擦除最小单位是页1KB if (cache_idx % (1024/sizeof(CacheRecord)) 0) { FLASH_ErasePage(addr); } FLASH_ProgramWord(addr, *(uint32_t*)rec.temp); // ... 写入其余字段 cache_idx; }这段代码的关键是擦除策略每写满一页才擦一次减少擦写次数。补传时按 cache_idx 顺序读发一条删一条标记全部发完再统一擦除。5. 避坑与排查那些让系统“猝死”的细节5.1 现象OLED 显示乱码重启后恢复原因I2C 总线被传感器任务和显示任务同时访问没有互斥保护时序错乱。解决给 I2C 操作加互斥量或者把传感器和显示放在同一个任务里串行执行。我一般用互斥量因为显示刷新频率低等一会儿没关系。5.2 现象设备运行几小时后死机看门狗复位原因某个任务栈溢出或者堆碎片化导致 malloc 失败。解决打开栈溢出检测把 configUSE_MALLOC_FAILED_HOOK 设为 1在钩子函数里打印剩余堆大小。如果堆不够把 configTOTAL_HEAP_SIZE 调大或者改用静态分配。5.3 现象电池电压检测值始终偏高原因STM32 的 ADC 参考电压就是 VDDA如果 VDDA 不稳定检测值就不准。解决用内部参考电压 VREFINT 校准。STM32F1 的 VREFINT 典型值是 1.20VADC 读通道 17 可以得到 VREFINT 的转换值反推 VDDA 实际值再换算电池电压。5.4 现象LoRa 模块发送成功但服务端收不到原因LoRa 模块的空中速率和网关不匹配或者频点设错了。解决先用 AT 指令读模块配置确认频点、扩频因子、带宽和网关一致。另外注意天线不能短路我见过一个案例天线座子焊反了模块照样返回发送成功但距离只有几米。5.5 现象Flash 写入后读出来全是 0xFF原因STM32F1 的 Flash 写入前必须先擦除而且擦除后是 0xFF不是 0x00。如果直接写写进去的位只能从 1 变 0不能从 0 变 1。解决每次写入前先擦除对应页或者用双页交替写入的方式一页写满再擦另一页。6. 低功耗进阶把整机电流压到 100uA 以内的三个技巧低功耗是环境监测系统从“能用”到“敢用”的最后一道坎。如果设备要电池供电跑半年平均电流必须控制在 100uA 以内。下面三个技巧是我实际验证过的。第一个技巧用 RTC 唤醒代替 SysTick。STM32 的 Stop 模式下 SysTick 停止只能用 RTC 闹钟或者外部中断唤醒。配置 RTC 每 2 秒产生一次闹钟中断唤醒后采集、上报、再睡。RTC 唤醒的功耗大概 1uA 左右。// 进入 Stop 模式前配置 RTC 闹钟 void enter_stop_mode(uint32_t seconds) { // 设置 RTC 闹钟seconds 秒后唤醒 RTC_SetAlarm(RTC_GetCounter() seconds); // 清除唤醒标志 PWR_ClearFlag(PWR_FLAG_WU); // 进入 Stop 模式调压器低功耗 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后重新配置时钟Stop 模式下 HSE 会停 SystemInit(); }注意唤醒后必须重新调用 SystemInit()因为 Stop 模式下 HSE 停振系统时钟切回了 HSI。如果不重新配置串口波特率会错通信直接挂掉。第二个技巧传感器间歇供电。SHT30 虽然功耗不高但一直上电也有几十微安。用 MOS 管控制传感器电源采集前 10ms 上电采集完立刻断电。这样传感器平均功耗可以降到 1uA 以下。第三个技巧关闭所有未使用的 GPIO 时钟和调试接口。STM32 的 GPIO 时钟默认全开在 Stop 模式下每个 GPIO 都有漏电流。把没用的 GPIO 时钟关掉调试接口在量产固件里禁用又能省十几微安。验证方法很简单在电源正极串一个万用表测平均电流。如果大于 100uA先查 LDO 静态电流再查 GPIO 漏电最后查传感器和通信模块的待机功耗。我一般会先断开通信模块看电流降多少再断开传感器一步步定位。最后说一个我自己的教训早期做低功耗时我总想着把代码优化到极致结果忽略了 LDO 的静态电流。后来换了一颗静态电流 4uA 的 LDO整机电流直接从 200uA 掉到 80uA。硬件选型上的一个参数比软件优化半天都管用。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站