STM32物联网实战:ESP8266通过OneNET MQTT协议实现微信小程序远程控制完整代码解析
1. 项目概述与硬件准备
大家好,今天我来分享一个超实用的STM32物联网项目:通过ESP8266 WiFi模块,借助OneNET平台的MQTT协议,实现微信小程序远程控制STM32设备的完整方案。这个项目特别适合想要入门物联网开发的嵌入式爱好者,我会把每个步骤都讲得清清楚楚,让你轻松复现整个项目。
先来看看我们需要准备哪些硬件设备:
- 一块STM32开发板(我用的STM32F103C8T6,性价比超高)
- ESP8266 WiFi模块(ESP-01S就行,便宜又好用)
- 一些基础的外设:LED灯、温湿度传感器等(用于实际控制演示)
- USB转串口模块(用于调试和烧录程序)
软件环境准备:
- Keil MDK或者STM32CubeIDE(根据个人喜好选择)
- OneNET物联网平台账号(免费注册使用)
- 微信开发者工具(用于小程序端开发)
这个项目的核心思路是:STM32通过串口与ESP8266通信,ESP8266负责连接WiFi和OneNET平台,微信小程序作为控制端通过OneNET平台向设备发送控制指令。整个数据流是双向的:STM32可以上传传感器数据到云端,小程序也能下发控制命令到设备。
2. ESP8266模块配置与AT指令解析
ESP8266模块是这个项目中连接网络的关键组件,我们需要通过AT指令来配置它。先来看看基本的AT指令序列:
// WiFi连接AT指令
#define WIFI_SSID "你的WiFi名称"
#define WIFI_PASSWORD "你的WiFi密码"
#define WIRELESS_WIFI_INFO "AT+CWJAP=\"" WIFI_SSID "\",\"" WIFI_PASSWORD "\"\r\n"
// OneNET MQTT配置指令
#define ONENET_MQTT_USERCFG_INFO "AT+MQTTUSERCFG=0,1,\"设备名称\",\"产品ID\",\"Token\",0,0,\"\"\r\n"
#define ONENET_MQTT_SERVER_INFO "AT+MQTTCONN=0,\"mqtts.heclouds.com\",1883,1\r\n"
在实际代码中,我编写了一个可靠的AT指令发送函数,这个函数处理了重试机制和超时判断:
uint8_t MQTT_Send_Command(char *cmd, char *res, uint8_t MaxSendCount, uint8_t Clear_Buffer, uint16_t timeout_ms)
{
uint32_t start_time = GetTick();
uint8_t retry_count = 0;
while (retry_count < MaxSendCount) {
Usart2_SendString(cmd); // 发送AT指令
printf("发送指令: %s", cmd);
uint32_t send_time = GetTick();
while ((GetTick() - send_time) < timeout_ms) {
if (MQTT_Get_Receive_Flag() == 1) {
if (strstr((char*)MQTT_RxBuffer, res) != NULL) {
printf("指令执行成功\n");
if (Clear_Buffer) MQTT_Clear_Buffer();
return 1;
}
}
delay_ms(10);
}
retry_count++;
printf("第%d次重试...\n", retry_count);
}
return 0;
}
这里有个实用小技巧:ESP8266模块有时会返回"busy"状态,这时候需要稍作延迟再重试。我在实际测试中发现,每条AT指令之间最好有100-200ms的间隔,这样稳定性会大大提高。
3. OneNET平台配置详解
OneNET平台是中国移动推出的物联网开放平台,提供了完善的设备管理和数据可视化功能。首先我们需要在平台上创建产品和设备:
-
登录OneNET控制台,进入"全部产品服务" → "多协议接入" → "MQTT旧版"
-
点击"创建产品",填写产品信息:
- 产品名称:自定义一个有意义的名称
- 行业类别:根据实际应用选择
- 联网方式:WiFi
- 数据格式:JSON
-
产品创建完成后,在设备列表中"添加设备",记录下分配的设备ID和产品ID
-
生成设备鉴权信息(Token),这是设备连接平台的身份凭证
在代码中,我们需要配置这些平台参数:
#define ONENET_MQTT_PRODUCT_ID "你的产品ID"
#define ONENET_MQTT_DEVICE_NAME "你的设备名称"
#define ONENET_MQTT_TOKEN "你的设备Token"
平台还提供了详细的数据流管理功能,我们可以定义多个数据点(Data Stream)来存储不同类型的传感器数据。比如温度、湿度、设备状态等。小程序端就是通过这些定义好的数据流来与设备进行数据交互的。
4. MQTT协议通信机制解析
MQTT(Message Queuing Telemetry Transport)是一种轻量级的发布/订阅模式消息传输协议,特别适合物联网应用。在我们的项目中,主要用到了以下几个主题(Topic):
// 设备属性上报主题
#define ONENET_MQTT_PUBTOPIC "AT+MQTTPUBRAW=0,\"$sys/产品ID/设备名称/thing/property/post\""
// 设备属性设置主题(用于接收控制指令)
#define ONENET_MQTT_SET_TOPIC "AT+MQTTSUB=0,\"$sys/产品ID/设备名称/thing/property/set\",0\r\n"
// 命令响应主题
#define ONENET_MQTT_REPLY_TOPIC "AT+MQTTSUB=0,\"$sys/产品ID/设备名称/thing/property/post/reply\",0\r\n"
MQTT通信的质量等级(QoS)设置也很重要:
- QoS 0:最多交付一次,可能丢失消息
- QoS 1:至少交付一次,可能重复消息
- QoS 2:精确交付一次,保证消息不重复不丢失
对于我们的控制场景,建议使用QoS 1,既能保证指令可靠送达,又不会太复杂。
在实际编码中,我设计了一个灵活的数据上报机制:
void MQTT_Publish_Data(void)
{
char jsonBuffer[512] = {0};
uint16_t bufferPos = 0;
// 构建JSON数据
bufferPos += snprintf(jsonBuffer + bufferPos, sizeof(jsonBuffer) - bufferPos, "{\"id\":\"%d\",\"params\":{", GetTick());
for (uint8_t i = 0; i < DATA_ARRAY_SIZE; i++) {
switch (onenet_data_array[i]->type) {
case TYPE_BOOL:
bufferPos += snprintf(jsonBuffer + bufferPos, sizeof(jsonBuffer) - bufferPos,
"\"%s\":%s%s", onenet_data_array[i]->name,
onenet_data_array[i]->value.bool_value ? "true" : "false",
(i < DATA_ARRAY_SIZE - 1) ? "," : "");
break;
case TYPE_INT:
bufferPos += snprintf(jsonBuffer + bufferPos, sizeof(jsonBuffer) - bufferPos,
"\"%s\":%d%s", onenet_data_array[i]->name,
onenet_data_array[i]->value.int_value,
(i < DATA_ARRAY_SIZE - 1) ? "," : "");
break;
// 其他数据类型处理...
}
}
bufferPos += snprintf(jsonBuffer + bufferPos, sizeof(jsonBuffer) - bufferPos, "}}");
// 发送数据
char atCommand[256];
snprintf(atCommand, sizeof(atCommand), "%s,%d,0,0\r\n", ONENET_MQTT_PUBTOPIC, bufferPos);
if (MQTT_Send_Command(atCommand, ">", 1, 1, 100)) {
Usart2_SendString(jsonBuffer);
printf("数据上报成功: %s\n", jsonBuffer);
}
}
5. STM32与ESP8266串口通信实现
STM32与ESP8266之间通过串口进行通信,我使用了USART2接口。首先需要初始化串口:
void uart2_init(uint32_t baud)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置TX引脚(PA2)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置RX引脚(PA3)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置串口参数
USART_InitStructure.USART_BaudRate = baud;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART2, &USART_InitStructure);
// 使能接收中断
USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
NVIC_EnableIRQ(USART2_IRQn);
USART_Cmd(USART2, ENABLE);
}
串口中断处理函数负责接收ESP8266返回的数据:
void USART2_IRQHandler(void)
{
if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) {
uint8_t data = USART_ReceiveData(USART2);
MQTT_Receive_Callback(data); // 处理接收到的数据
USART_ClearITPendingBit(USART2, USART_IT_RXNE);
}
}
数据接收回调函数实现了简单的数据帧解析:
void MQTT_Receive_Callback(uint8_t byte)
{
if (MQTT_RxDataCnt < RX_BUFFER_SIZE) {
MQTT_RxBuffer[MQTT_RxDataCnt++] = byte;
}
// 检测帧结束标志\r\n
if (MQTT_RxDataCnt >= 2 &&
MQTT_RxBuffer[MQTT_RxDataCnt-2] == '\r' &&
MQTT_RxBuffer[MQTT_RxDataCnt-1] == '\n') {
MQTT_Status.receiveDataFlag = 1;
}
}
在实际调试中,我发现ESP8266的响应时间有时不太稳定,所以建议设置合理的超时时间。对于简单的AT指令(如ATE0),500ms的超时足够;对于WiFi连接指令,建议设置5-10秒的超时;MQTT连接则需要更长的超时时间。
6. 微信小程序与控制指令处理
微信小程序作为用户控制界面,通过OneNET平台的API与设备进行交互。小程序端主要实现以下功能:
- 用户登录和设备绑定
- 实时数据显示(温度、湿度等传感器数据)
- 设备控制界面(开关、滑块等控制元素)
- 历史数据查询和显示
在小程序端,我们使用OneNET提供的JavaScript SDK来简化开发:
// 小程序端MQTT连接配置
const mqttOptions = {
host: 'mqtts.heclouds.com',
port: 1883,
clientId: '小程序客户端ID',
username: '产品ID',
password: '鉴权信息'
};
// 发布控制指令
function publishControlCommand(deviceId, command) {
const topic = `$sys/{产品ID}/${deviceId}/thing/property/set`;
const message = {
id: Date.now().toString(),
version: "1.0",
params: command
};
mqttClient.publish(topic, JSON.stringify(message));
}
在STM32端,我们需要解析和处理小程序下发的控制指令:
void MQTT_Processing_Command(void)
{
char *jsonData = strstr((char*)MQTT_RxBuffer, ONENET_MQTT_REVEIVE_SET_TOPIC);
if (jsonData) {
jsonData = strchr(jsonData, '{');
if (jsonData) {
uint32_t cmd_id = MQTT_Get_Command_Id(jsonData);
MQTT_Receiver_Command_Extract(jsonData);
MQTT_Receive_Command_Respond(cmd_id, 200, "success");
MQTT_Command_Control();
}
}
}
指令解析函数使用cJSON库来处理JSON格式的数据:
void MQTT_Receiver_Command_Extract(char *jsonData)
{
cJSON *json_obj = cJSON_Parse(jsonData);
if (!json_obj) return;
cJSON *params = cJSON_GetObjectItem(json_obj, "params");
if (params) {
// 解析窗帘控制
cJSON *item = cJSON_GetObjectItem(params, "ChuangLian_Control");
if (item && cJSON_IsBool(item)) {
ChuangLian_Control.value.bool_value = item->valueint;
printf("窗帘控制: %s\n", item->valueint ? "打开" : "关闭");
}
// 解析灯光亮度
item = cJSON_GetObjectItem(params, "DiaoDeng_Control");
if (item && cJSON_IsNumber(item)) {
DiaoDeng_Control.value.int_value = item->valueint;
printf("灯光亮度: %d%%\n", item->valueint);
}
}
cJSON_Delete(json_obj);
}
7. 数据上报与状态同步机制
为了实现设备状态的实时同步,我设计了一个双向的数据流机制。设备定期上报状态数据,同时随时响应小程序的查询请求。
数据上报函数:
void device_data_report(void)
{
static uint32_t last_report_time = 0;
uint32_t current_time = GetTick();
// 每30秒上报一次数据
if (current_time - last_report_time >= 30000) {
update_sensor_data(); // 更新传感器数据
MQTT_Publish_Data(); // 上报数据到平台
last_report_time = current_time;
printf("定时数据上报完成\n");
}
}
传感器数据更新函数模拟了真实传感器的数据变化:
void update_sensor_data(void)
{
// 模拟温度变化:20.0°C - 30.0°C
static uint8_t trend = 0;
if (trend == 0) {
temperature.value.float_value += 0.5;
if (temperature.value.float_value >= 30.0) trend = 1;
} else {
temperature.value.float_value -= 0.5;
if (temperature.value.float_value <= 20.0) trend = 0;
}
// 模拟湿度变化:40% - 80%
humidity.value.float_value = 60.0 + 20.0 * sin(GetTick() / 60000.0);
// 设备状态计数
sensor_status.value.int_value++;
}
为了处理网络异常情况,我还实现了一个简单的重连机制:
void network_check_handler(void)
{
static uint32_t last_check_time = 0;
uint32_t current_time = GetTick();
if (current_time - last_check_time >= 10000) { // 每10秒检查一次
if (strstr((char*)MQTT_RxBuffer, "WIFI DISCONNECT")) {
printf("WiFi连接断开,尝试重连...\n");
WiFi_Init();
}
if (strstr((char*)MQTT_RxBuffer, "+MQTTDISCONNECTED")) {
printf("MQTT连接断开,尝试重连...\n");
MQTT_Config();
}
last_check_time = current_time;
MQTT_Clear_Buffer();
}
}
8. 实战调试与常见问题解决
在实际项目中,调试是非常重要的一环。我总结了一些常见的调试技巧和问题解决方法:
1. ESP8266连接WiFi失败
- 检查WiFi名称和密码是否正确
- 确保WiFi信号强度足够
- 尝试在AT指令中增加重试次数
2. MQTT连接OneNET失败
- 检查产品ID、设备名称和Token是否正确
- 确认网络时间同步(MQTT鉴权对时间敏感)
3. 数据上报失败
- 检查JSON格式是否正确
- 确认数据流名称在平台上已创建
4. 控制指令无响应
- 检查主题(Topic)设置是否正确
- 确认小程序和设备使用相同的产品ID
我在代码中添加了详细的调试信息输出,方便定位问题:
void MQTT_System_Handler(void)
{
if (MQTT_Status.error_code != 0) {
printf("系统错误代码: 0x%04X\n", MQTT_Status.error_code);
if (MQTT_Status.error_code & ERROR_WiFi_CONNECT) {
printf("WiFi连接错误,请检查网络配置\n");
}
if (MQTT_Status.error_code & ERROR_MQTT_CONNECT) {
printf("MQTT连接错误,请检查平台配置\n");
}
if (MQTT_Status.error_code & ERROR_PUBLISH) {
printf("数据发布错误,请检查数据格式\n");
}
}
}
还有一个实用的调试技巧:使用串口助手同时监控STM32和ESP8266的通信。这样可以清楚地看到AT指令的发送和响应过程,快速定位通信问题。
在实际部署时,建议增加看门狗机制,防止程序死机:
void IWDG_Init(void)
{
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
IWDG_SetPrescaler(IWDG_Prescaler_256);
IWDG_SetReload(0xFFF);
IWDG_ReloadCounter();
IWDG_Enable();
}
// 在主循环中定期喂狗
void main(void)
{
// 初始化代码...
while (1) {
device_data_report();
network_check_handler();
IWDG_ReloadCounter(); // 喂狗
delay_ms(100);
}
}
这个项目我前前后后调试了大概一周时间,最大的体会是:物联网项目成功的关键在于稳定可靠的通信机制。无论是硬件层面的串口通信,还是网络层面的MQTT协议,都需要充分考虑各种异常情况的处理。
最后给想要复现这个项目的朋友一个建议:先从最简单的功能开始,比如先实现WiFi连接,再实现MQTT连接,最后再添加数据上报和控制功能。这样分步调试会更容易成功。
更多推荐
所有评论(0)