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平台是中国移动推出的物联网开放平台,提供了完善的设备管理和数据可视化功能。首先我们需要在平台上创建产品和设备:

  1. 登录OneNET控制台,进入"全部产品服务" → "多协议接入" → "MQTT旧版"

  2. 点击"创建产品",填写产品信息:

    • 产品名称:自定义一个有意义的名称
    • 行业类别:根据实际应用选择
    • 联网方式:WiFi
    • 数据格式:JSON
  3. 产品创建完成后,在设备列表中"添加设备",记录下分配的设备ID和产品ID

  4. 生成设备鉴权信息(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与设备进行交互。小程序端主要实现以下功能:

  1. 用户登录和设备绑定
  2. 实时数据显示(温度、湿度等传感器数据)
  3. 设备控制界面(开关、滑块等控制元素)
  4. 历史数据查询和显示

在小程序端,我们使用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连接,最后再添加数据上报和控制功能。这样分步调试会更容易成功。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐