用STM32F103和ESP8266做个微信小程序温湿度监控(附完整Keil工程)
STM32F103与ESP8266构建微信小程序温湿度监控系统实战指南
在智能家居和工业物联网快速发展的今天,实时环境监测已成为许多应用场景的基础需求。本文将手把手带您完成一个完整的物联网项目——基于STM32F103和ESP8266的温湿度监控系统,并通过微信小程序实现远程数据可视化。不同于简单的教程罗列,我们将重点关注项目架构设计、代码模块化以及实际开发中可能遇到的各种"坑"和解决方案。
1. 项目整体架构与硬件选型
一个完整的物联网系统通常分为感知层、传输层和应用层三个部分。在本项目中,我们选择的硬件组合既考虑了性价比,也确保了系统的稳定性和扩展性。
核心硬件组件:
- STM32F103C8T6:作为主控制器,这款Cortex-M3内核的MCU具有72MHz主频、64KB Flash和20KB RAM,完全满足我们的需求且性价比极高
- ESP8266-01S:负责Wi-Fi连接和MQTT通信,相比更昂贵的ESP32,在简单物联网应用中已经足够
- DHT11温湿度传感器:虽然精度一般(湿度±5%,温度±2℃),但对于大多数家庭和办公环境监测已经足够
- 0.96寸OLED显示屏:用于本地数据显示,方便调试和现场查看
- USB转TTL模块:用于程序烧录和调试
硬件连接示意图:
| STM32引脚 | 连接组件 | 备注 |
|---|---|---|
| PA0 | DHT11数据线 | 需上拉电阻 |
| PB6/PB7 | I2C SCL/SDA | 连接OLED |
| PA2/PA3 | USART2_TX/RX | 连接ESP8266 |
| PC13 | LED指示灯 | 系统状态指示 |
注意:ESP8266的CH_PD引脚需要接高电平,GPIO0在烧录时需要拉低,正常工作时需拉高
2. 开发环境搭建与基础驱动编写
2.1 Keil工程配置
首先我们需要建立一个完整的Keil工程框架,良好的工程结构能显著提高开发效率:
Project/ ├── CMSIS/ // 内核支持文件 ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ // HAL库 │ └── BSP/ // 板级支持包 ├── Middlewares/ │ ├── OLED/ // OLED驱动 │ └── DHT11/ // 传感器驱动 ├── Application/ │ ├── Inc/ // 头文件 │ └── Src/ // 源文件 └── MDK-ARM/ // Keil工程文件关键配置步骤:
- 在Keil中新建工程,选择STM32F103C8器件
- 配置时钟树,确保系统时钟为72MHz
- 开启USART2用于ESP8266通信
- 配置I2C1用于OLED显示
- 启用定时器TIM3用于DHT11时序控制
2.2 传感器驱动开发
DHT11虽然简单,但时序要求严格。以下是经过优化的读取函数:
#define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_0 uint8_t DHT11_Read_Data(uint8_t *temperature, uint8_t *humidity) { uint8_t data[5] = {0}; uint8_t i, j; // 主机拉低至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); // 等待从机响应 if(!DHT11_Wait_State(GPIO_PIN_RESET)) return 1; if(!DHT11_Wait_State(GPIO_PIN_SET)) return 1; // 读取40位数据 for(i=0; i<5; i++) { for(j=0; j<8; j++) { if(!DHT11_Wait_State(GPIO_PIN_RESET)) return 1; uint32_t start = HAL_GetTick(); while(HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)) { if(HAL_GetTick()-start > 50) return 1; } start = HAL_GetTick(); uint8_t duration = 0; while(!HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN)) { if(HAL_GetTick()-start > 50) return 1; } duration = HAL_GetTick() - start; data[i] <<= 1; if(duration > 30) data[i] |= 1; } } // 校验数据 if(data[4] != (data[0]+data[1]+data[2]+data[3])) return 1; *humidity = data[0]; *temperature = data[2]; return 0; }OLED显示驱动建议使用现成的SSD1306库,但需要进行适当优化以减小内存占用:
void OLED_ShowString(uint8_t x, uint8_t y, const uint8_t *str, uint8_t size) { while(*str != '\0') { if(x > 120) { x = 0; y += size; } OLED_ShowChar(x, y, *str, size); x += size/2; str++; } }3. ESP8266与MQTT协议实现
3.1 ESP8266固件选择与AT指令配置
建议使用安信可提供的AT固件,支持MQTT协议且稳定性较好。以下是关键的初始化流程:
void ESP8266_Init(void) { uint8_t retry = 3; while(retry--) { ESP8266_SendCmd("AT", "OK", 1000); ESP8266_SendCmd("AT+CWMODE=1", "OK", 1000); ESP8266_SendCmd("AT+CWJAP=\"SSID\",\"password\"", "OK", 5000); ESP8266_SendCmd("AT+CIPMUX=0", "OK", 1000); ESP8266_SendCmd("AT+CIPSTART=\"TCP\",\"mqtt.server.com\",1883", "OK", 3000); if(ESP8266_SendCmd("AT+CIPSTATUS", "CONNECTED", 1000)) break; } }实际项目中,我们需要处理各种异常情况,比如Wi-Fi断开重连:
void ESP8266_Check_Connection(void) { static uint32_t last_check = 0; if(HAL_GetTick() - last_check < 10000) return; last_check = HAL_GetTick(); if(!ESP8266_SendCmd("AT+CIPSTATUS", "CONNECTED", 1000)) { ESP8266_SendCmd("AT+CIPCLOSE", "OK", 1000); ESP8266_Init(); } }3.2 MQTT协议实现要点
虽然可以使用现成的MQTT库,但理解协议原理对调试很有帮助。MQTT连接的基本流程:
- 发送CONNECT报文建立连接
- 发送SUBSCRIBE报文订阅主题
- 定期发送PINGREQ保持连接
- 通过PUBLISH报文发布数据
以下是CONNECT报文的构建示例:
void MQTT_Connect(void) { uint8_t buffer[128]; uint8_t *ptr = buffer; // Fixed header *ptr++ = 0x10; // CONNECT // Remaining length uint8_t rem_len = 12 + 2 + strlen(MQTT_CLIENT_ID) + 2 + strlen(MQTT_USER) + 2 + strlen(MQTT_PASS); *ptr++ = rem_len; // Protocol name *ptr++ = 0x00; *ptr++ = 0x04; memcpy(ptr, "MQTT", 4); ptr += 4; // Protocol level *ptr++ = 0x04; // MQTT 3.1.1 // Connect flags *ptr++ = 0xC2; // CleanSession=1, WillFlag=0, Username=1, Password=1 // Keep alive *ptr++ = 0x00; *ptr++ = 0x3C; // 60 seconds // Payload *ptr++ = 0x00; *ptr++ = strlen(MQTT_CLIENT_ID); memcpy(ptr, MQTT_CLIENT_ID, strlen(MQTT_CLIENT_ID)); ptr += strlen(MQTT_CLIENT_ID); *ptr++ = 0x00; *ptr++ = strlen(MQTT_USER); memcpy(ptr, MQTT_USER, strlen(MQTT_USER)); ptr += strlen(MQTT_USER); *ptr++ = 0x00; *ptr++ = strlen(MQTT_PASS); memcpy(ptr, MQTT_PASS, strlen(MQTT_PASS)); ptr += strlen(MQTT_PASS); ESP8266_SendData(buffer, ptr - buffer); }4. 微信小程序开发与数据可视化
4.1 小程序基础框架搭建
微信小程序开发需要先注册开发者账号并安装开发者工具。项目基本目录结构:
miniprogram/ ├── pages/ │ ├── index/ // 主页面 │ └── history/ // 历史数据页面 ├── components/ │ └── chart/ // 自定义图表组件 ├── utils/ │ └── mqtt.js // MQTT连接工具 └── app.json // 全局配置关键配置文件app.json:
{ "pages": [ "pages/index/index", "pages/history/history" ], "window": { "navigationBarTitleText": "温湿度监控", "navigationBarBackgroundColor": "#1E90FF" }, "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "实时数据", "iconPath": "images/home.png", "selectedIconPath": "images/home-active.png" }, { "pagePath": "pages/history/history", "text": "历史记录", "iconPath": "images/history.png", "selectedIconPath": "images/history-active.png" } ] } }4.2 MQTT连接实现
小程序端使用WebSocket连接MQTT服务器,推荐使用Eclipse Paho的JavaScript客户端:
// utils/mqtt.js const MQTT = require('./paho-mqtt.min.js'); let client = null; function connectMQTT(callback) { client = new MQTT.Client('mqtt.server.com', 8083, 'client_' + Math.random().toString(16).substr(2)); client.onConnectionLost = function(response) { console.log('Connection lost: ' + response.errorMessage); }; client.onMessageArrived = function(message) { callback(JSON.parse(message.payloadString)); }; client.connect({ onSuccess: function() { console.log('MQTT Connected'); client.subscribe('sensor/data'); }, useSSL: true, userName: 'username', password: 'password' }); } module.exports = { connectMQTT };4.3 数据可视化实现
使用ECharts for WeChat实现专业级图表展示:
// pages/history/history.js import * as echarts from '../../ec-canvas/echarts'; Page({ data: { ec: { lazyLoad: true } }, onLoad: function() { this.ecComponent = this.selectComponent('#mychart-dom-line'); this.initChart(); }, initChart: function() { this.ecComponent.init((canvas, width, height) => { const chart = echarts.init(canvas, null, { width: width, height: height }); const option = { tooltip: { trigger: 'axis' }, legend: { data: ['温度', '湿度'] }, xAxis: { type: 'category', data: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'] }, yAxis: { type: 'value', axisLabel: { formatter: '{value}' } }, series: [ { name: '温度', type: 'line', data: [22, 21, 23, 25, 26, 24, 22, 21], smooth: true }, { name: '湿度', type: 'line', data: [45, 50, 55, 60, 58, 52, 48, 45], smooth: true } ] }; chart.setOption(option); return chart; }); } });5. 系统优化与常见问题解决
5.1 低功耗设计技巧
对于电池供电的应用,功耗优化至关重要:
- 将STM32设置为休眠模式,定时唤醒采集数据
- 调整ESP8266的休眠策略,仅在发送数据时唤醒
- 降低传感器采样频率
- 关闭不必要的LED指示灯
关键代码实现:
void Enter_Stop_Mode(uint32_t seconds) { // 配置唤醒引脚 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = WAKEUP_PIN; GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(WAKEUP_PORT, &GPIO_InitStruct); // 配置RTC唤醒定时器 HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, seconds*8, RTC_WAKEUPCLOCK_RTCCLK_DIV16); // 进入停止模式 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后重新配置时钟 SystemClock_Config(); }5.2 常见问题排查指南
问题1:ESP8266无法连接Wi-Fi
- 检查SSID和密码是否正确
- 确保路由器不是5GHz频段(ESP8266只支持2.4GHz)
- 尝试缩短Wi-Fi密码长度
- 检查电源是否稳定(建议3.3V 500mA以上)
问题2:MQTT连接频繁断开
- 增加Keep Alive时间(建议60秒以上)
- 实现PINGREQ/PINGRESP机制
- 检查网络信号强度
- 服务器端可能需要调整超时设置
问题3:DHT11读取失败
- 检查接线是否正确,特别是上拉电阻
- 确保供电电压稳定(3.3V-5V)
- 调整时序延迟,不同MCU可能需要微调
- 避免在中断中读取DHT11
问题4:小程序无法显示实时数据
- 检查MQTT服务器地址和端口是否正确
- 确认订阅的主题与发布主题一致
- 检查WebSocket是否启用(wss://)
- 验证JSON数据格式是否正确
5.3 项目扩展思路
基础功能实现后,可以考虑以下扩展方向:
- 添加多传感器支持(如CO2、PM2.5监测)
- 实现历史数据存储与分析
- 增加报警功能(微信消息推送)
- 开发多设备管理功能
- 加入OTA固件升级能力
- 实现本地和远程双控制模式
对于历史数据存储,可以使用腾讯云的云开发数据库:
// 存储数据到云数据库 const db = wx.cloud.database(); function saveData(temp, humi) { db.collection('sensor_data').add({ data: { temperature: temp, humidity: humi, timestamp: new Date() } }); } // 查询历史数据 function queryHistory(days) { const start = new Date(); start.setDate(start.getDate() - days); return db.collection('sensor_data') .where({ timestamp: _.gte(start) }) .orderBy('timestamp', 'desc') .get(); }