简介:这是一套面向嵌入式初学者与电子类课程设计、毕业设计需求的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 或 AHT20,I2C 接口,数据手册标称精度 ±2%RH、±0.3℃,而且不需要关中断。选型时看三个参数:接口类型、精度、供电范围。SHT30 是 I2C,2.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, 0x44<<1, cmd, 2, 100) != HAL_OK) return HAL_ERROR; // 等待测量完成,SHT30 高重复性模式需要约 15ms osDelay(20); // 读取 6 字节数据:温度高、温度低、温度CRC、湿度高、湿度低、湿度CRC if (HAL_I2C_Master_Receive(&hi2c1, 0x44<<1, 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.20V,ADC 读通道 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。硬件选型上的一个参数,比软件优化半天都管用。希望帮到你。本文还有配套的精品资源,点击获取
简介:这是一套面向嵌入式初学者与电子类课程设计、毕业设计需求的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 或 AHT20,I2C 接口,数据手册标称精度 ±2%RH、±0.3℃,而且不需要关中断。选型时看三个参数:接口类型、精度、供电范围。SHT30 是 I2C,2.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, 0x44<<1, cmd, 2, 100) != HAL_OK) return HAL_ERROR; // 等待测量完成,SHT30 高重复性模式需要约 15ms osDelay(20); // 读取 6 字节数据:温度高、温度低、温度CRC、湿度高、湿度低、湿度CRC if (HAL_I2C_Master_Receive(&hi2c1, 0x44<<1, 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.20V,ADC 读通道 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。硬件选型上的一个参数,比软件优化半天都管用。希望帮到你。本文还有配套的精品资源,点击获取