0. 前言

        最近学完STM32裸机和RTOS后,正在学习一些网络知识,在嵌入式设备比较轻量级的就是MQTT协议了,在没有MQTT协议基础知识的情况下,需要去移植官方的MQTT库,太繁琐,还有另一种方法就是用ESP8266的AT指令,新版固件已经可以支持MQTT协议了,使用起来很方便。

        点灯教程千千万,今天学的是一门高阶点灯功法:远程点灯。本文将记录笔者最近的点灯历程,将STM32采集的温湿度数据(来自DHT11)通过ESP8266模块基于MQTT协议发送数据到ONENET物联网平台,再做一个微信小程序连接ONENET提供的API接口下发点灯指令给单片机实现远程点灯。

        学会了远程点灯,在此基础上,把板子上的LED换成继电器,继电器再连接家用电灯,那不就可以远程控制家里的电灯了吗,简单的智能家居就可以实现了。

1. DHT11的配置

1.1 硬件原理图

        DHT11时一款温湿度传感器芯片,收发数据只需要一根总线,这根总线是开漏输出的,芯片释放总线时会被上拉电阻拉高。

1.2 软件设计

        每次发送起始信号都需要先拉低总线18~30ms,再拉高总线高10~35us

        DHT11的单总线协议,不是读取到3.3V就是高电平。而是高电平持续26~28us则表示接收到0,持续70us则接收到1。

        传输的数据格式包含40位数据,8位的校验位就等于前面32位数据的总和,再取低八位。单片机读取数据后把前32位数据求和,取低八位,再与传来的8位校验位进行对比,如果不一样就能判断传输数据有问题了。

DHT11.c

#include "dht11.h"
#include "Delay.h"

// 模式定义
#define DHT11_MODE_OUT 1
#define DHT11_MODE_IN  0

// 局部函数声明
static void DHT11_Mode(uint8_t mode);
static uint8_t DHT11_Check(void);

/**
 * @brief 初始化DHT11
 * 
 * @return uint8_t @param 1:初始化失败,未检测到DHT11的存在
 *                  @param 0:初始化成功,检测到DHT11的存在
 */
uint8_t DHT11_Init(void)
{
    uint8_t retry = 0;
    GPIO_InitTypeDef  GPIO_InitStructure;

    // 配置GPIO
    RCC_APB2PeriphClockCmd(DHT11_GPIO_CLK, ENABLE);
    GPIO_InitStructure.GPIO_Pin         = DHT11_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode        = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed       = GPIO_Speed_50MHz;
    GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure);

    // 设置初始状态
    GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN);

    // 发送起始信号
    DHT11_RST();
    // 等待DHT11的回应
    retry = DHT11_Check();
    return retry;
}

/**
 * @brief 复位DHT11,发送起始信号
 * 
 */
void DHT11_RST(void)
{
    // 设置为GPIO输出模式
    DHT11_Mode(DHT11_MODE_OUT);
    // 拉低总线18~30ms
    DHT11_Low;
    Delay_ms(20);
    // 拉高总线高10~35us
    DHT11_High;
    Delay_us(13);
}
 
/**
 * @brief 读取DHT11一位数据
 * 
 * @return uint8_t 读取到的位,1或0
 */
uint8_t DHT11_ReadBit(void)
{
    uint8_t timeout = 0, max_timeout = 100;

    // 读取总线电平:总线被拉低50us,表示主机开始接收数据
    while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) && timeout < max_timeout)//等待变为低电平
    {
        timeout++;
        Delay_us(1);
    }
    timeout = 0;

    // 读取总线电平,高电平持续26~28us则表示接收到0,持续70us则接收到1
    while(!GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) && timeout < max_timeout)//等待变高电平
    {
        timeout++;
        Delay_us(1);
    }

    // 40us后读取总线状态,判断接收到的位是0还是1
    Delay_us(40);
    if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)) return 1;
    else return 0;
}

/**
 * @brief 读取DHT11一个字节
 * 
 * @return uint8_t 读到的数据
 */
uint8_t DHT11_ReadByte(void)
{        
    uint8_t i, dat = 0;

    for (i = 0; i < 8; i++)
    {
        // 高位优先
        dat <<= 1;
        dat |= DHT11_ReadBit();
    }
    return dat;
}

/**
 * @brief 读取DHT11温湿度数据
 * 
 * @param temp 温度
 * @param humi 湿度
 * @return uint8_t @param 1:读取数据失败
 *                  @param 0:读取数据成功
 */
uint8_t DHT11_ReadData(float *temp, float *humi)
{
    uint8_t buff[5];// 湿度整数+湿度小数+温度整数+温度小数+校验位
    uint8_t i;

    // 发送起始信号
    DHT11_RST();
    // 等待响应
    if (DHT11_Check() != 0) return 1;
    
    // 读取40位数据
    for (i = 0; i < 5; i++)
    {
        buff[i] = DHT11_ReadByte();
    }
    // 校验数据 = 温湿度数据总共32位之和,取低8位
    if ((buff[0] + buff[1] + buff[2] + buff[3]) != buff[4]) return 1;
    
    // 处理温度负数情况
    int16_t temp_raw = buff[2];
    if (temp_raw & 0x80) {
        temp_raw = -(temp_raw & 0x7F);
    }
    // 转换数据:组合整数和小数部分
    *humi = (float)buff[0] + buff[1] * 0.1f;
    *temp = (float)temp_raw + buff[3] * 0.1f;

    return 0;
}


/**
 * @brief 等待DHT11的回应
 * 
 * @return uint8_t @param 1:未检测到DHT11的存在
 *                  @param 0:检测到DHT11的存在
 */
static uint8_t DHT11_Check(void)
{
    uint8_t timeout = 0, max_timeout = 100;

    // 设置为GPIO输入模式,读取来自DHT11的响应信号
    DHT11_Mode(DHT11_MODE_IN);

    // 总线被拉低80us,之后被拉高80us,如果没有检测到这个信号则认为DHT11不存在
    while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) && timeout < max_timeout)
    {
        timeout++;
        Delay_us(1);
    };
    if (timeout >= max_timeout) return 1;
    else timeout = 0;
    while (!GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) && timeout < max_timeout)
    {
        timeout++;
        Delay_us(1);
    };
    if (timeout >= max_timeout) return 1;
    return 0;
}

/**
 * @brief 设置DHT11的工作模式
 * 
 * @param mode  DHT11_MODE_IN:  输入模式
 *              DHT11_MODE_OUT: 输出模式
 */
static void DHT11_Mode(uint8_t mode)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    
    GPIO_InitStructure.GPIO_Pin     = DHT11_GPIO_PIN;
    GPIO_InitStructure.GPIO_Speed   = GPIO_Speed_50MHz;

    if (mode == DHT11_MODE_OUT)
    {
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    }
    else if (mode == DHT11_MODE_IN)
    {
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    }
    GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStructure);
}

DHT11.h

#ifndef __DHT11_H
#define __DHT11_H
 
#include "stm32f10x.h"

// GPIO宏定义
#define DHT11_GPIO_PORT  GPIOA
#define DHT11_GPIO_PIN   GPIO_Pin_11
#define DHT11_GPIO_CLK   RCC_APB2Periph_GPIOA

// GPIO快捷操作宏定义
#define DHT11_Low  GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN)
#define DHT11_High GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN)

// 初始化DHT11
uint8_t DHT11_Init(void);

// 复位DHT11,发送起始信号
void DHT11_RST(void); 

// 读取温湿度数据
uint8_t DHT11_ReadData(float *temp, float *humi);
uint8_t DHT11_ReadByte(void);
uint8_t DHT11_ReadBit(void);
 
#endif

main.c

#include "stm32f10x.h"
#include "Delay.h"
#include "dht11.h"
#include "OLED.h"

int main()
{
    OLED_Init();
    DHT11_Init();
    float temp = 0, humi = 0;
    OLED_ShowString(1, 1, "Temp:");
    OLED_ShowString(2, 1, "Humi:");

    while (1)
    {
        DHT11_ReadData(&temp, &humi);//数据获取
        Delay_ms(1000);
        OLED_ShowNum(1, 6, temp, 2); //显示温度
        OLED_ShowNum(2, 6, humi, 2); //显示湿度
    }
}

        在主循环中每秒读取一次温湿度数据。

1.3 调试现象

2. esp8266的配置

2.1 模块引脚

在正常启动模式下,只需要接VCC、GND、TXD、RXD四个引脚即可。

2.2 ONENET平台配置

  • 开通认证:注册登录后,进入控制台,需要认证个人信息后开通

  • 创建产品:进入设备管理服务,点击创建产品

  • 填入信息:重点是MQTT协议,OneJson格式,WIFI自定义方案

  • 添加设备:就是添加实际的物理设备,在添加后会将之前创建的产品与设备联系起来,然后通过设备ID去访问,设备ID需要复制保存后续使用

完成:

  • 设置物模型:给产品添加参数,我需要上传温湿度,所以设置一个温度和湿度的参数属性,来保存单片机上传的数据

  • 设置参数类型:数据的类型和范围,重点是标识符,后面单片机访问温度参数的时候是通过标识符访问的,而不是功能名称。

        我添加了三个参数,分别保存温湿度,控制LED灯,其中LED灯后续需要用于从平台下发数据到单片机,实现远程点灯。

  • 查看产品信息:产品ID,设备密钥,这两个很重要,需要保存起来。

  • 生成token:另外需要使用官方提供的token生成工具生成用户登录密码,也需要保存起来。

        官方工具下载地址:token生成工具

        打开后需要填入设备信息:

其中

  • resproducts/{你的产品id}/devices/{你的设备名称}
  • et:到期时间戳,随便填,填久一点
  • key:设备密钥

填完点击生成,复制下面的内容先保存起来,将要作为后续的登录密码

到此平台的内容已经配置完成,可以通过属性栏查看当前平台收集到的数据

上面会显示标识符和对应的数据,在刚开始没有数据时都显示undefined

2.3 软件设计

        现在我要实现的功能是stm32采集的温湿度数据上传到ONENET平台,同时单片机等待平台下发的点灯指令。大体思路就是先初始化ESP8266打开STA模式,连接一下附近的WIFI热点,连接云平台,在主循环里不断采集温湿度并发布到平台,以及等待云平台下发的点灯指令。

2.3.1 ESP8266初始化

  • 配置串口空闲中断:首先要配置好WIFI传输使用的两根串口线,我使用串口空闲中断,所以配置DMA模式,开启中断。
#include "esp8266.h"
#include "Delay.h"
#include "stdio.h"
#include "string.h"
#include <stdlib.h>
#include "usart.h"

/**************** 静态函数声明 ****************/
static void WIFI_USART_Send_Char(uint8_t ch);

/*************** 静态变量声明 ***************/
static uint8_t WIFI_RX_FLAG = 0;            // WIFI接收数据完成标志位
static uint32_t WIFI_RX_LEN = 0;            // WIFI当前接收到的数据长度
static uint8_t WIFI_AP_Link_Num = 0;        // AP模式下连接热点的设备数量
static uint8_t WIFI_TCP_Link_Num = 0;       // AP模式下连接TCP服务器的设备数量

/****** 数组声明 ******/
uint8_t WIFI_RX_BUFF[WIFI_RX_LEN_MAX];   // WIFI接收数据缓冲区

/**
 * @brief 串口参数配置
 * 
 * @param Baud 波特率
 */
static void WIFI_USART_Config(uint32_t Baud)
{
    /* 开启时钟 */
    WIFI_USART_CLK_ENABLE();
    WIFI_USART_GPIO_CLK_ENABLE();

    /* 初始化GPIO */
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.GPIO_Pin   = WIFI_USART_TX_Pin;    // 发送
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(WIFI_USART_TX_Port, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin   = WIFI_USART_RX_Pin;  // 接收
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_IPU;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(WIFI_USART_RX_PORT, &GPIO_InitStruct);

    /* 初始化USART参数 */
    USART_InitTypeDef USART_InitStruct = {0};
    USART_InitStruct.USART_BaudRate            = Baud;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode                = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStruct.USART_Parity              = USART_Parity_No;
    USART_InitStruct.USART_StopBits            = USART_StopBits_1;
    USART_InitStruct.USART_WordLength          = USART_WordLength_8b;
    USART_Init(WIFI_USART, &USART_InitStruct);

    /* 配置USART的发送/接收中断 */
    USART_ITConfig(WIFI_USART, USART_IT_IDLE, ENABLE);

    /* 配置DMA */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    DMA_DeInit(WIFI_DMA_CH);
    DMA_InitTypeDef DMA_InitStruct = {0};
    DMA_InitStruct.DMA_M2M                = DMA_M2M_Disable;
    DMA_InitStruct.DMA_DIR                = DMA_DIR_PeripheralSRC;
    DMA_InitStruct.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStruct.DMA_MemoryBaseAddr     = (uint32_t)WIFI_RX_BUFF;
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&WIFI_USART->DR;
    DMA_InitStruct.DMA_BufferSize         = WIFI_RX_LEN_MAX;
    DMA_InitStruct.DMA_Mode               = DMA_Mode_Normal;
    DMA_InitStruct.DMA_Priority           = DMA_Priority_VeryHigh;
    DMA_Init(WIFI_DMA_CH, &DMA_InitStruct);

    /* 初始化NVIC */
    NVIC_InitTypeDef NVIC_InitStruct = {0};
    NVIC_InitStruct.NVIC_IRQChannel                   = WIFI_IRQ;
    NVIC_InitStruct.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority        = 3;
    NVIC_Init(&NVIC_InitStruct);

    /* 开启USART */
    USART_DMACmd(WIFI_USART, USART_DMAReq_Rx, ENABLE);
    USART_Cmd(WIFI_USART, ENABLE);
    DMA_Cmd(WIFI_DMA_CH, ENABLE);
}
  • 需要注意的是DMA这里,先开启时钟再复位DMA。注意DMA的通道数不能随意配置,手册规定USART3的DMA接收通道包括DMA1通道3,那就只能使用通道3。
    /* 配置DMA */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    DMA_DeInit(WIFI_DMA_CH);
  • 封装串口指令:然后封装好串口发送字符和字符串的函数。

/**
 * @brief WIFI发送一个字节数据
 * 
 * @param ch 要发送的字符数据
 */
static void WIFI_USART_Send_Char(uint8_t ch)
{
    uint32_t timeOut = 100000;
    if (USART_GetFlagStatus(WIFI_USART, USART_FLAG_TXE) == SET)
        USART_SendData(WIFI_USART, (uint8_t)ch);

    // 等待发送数据缓冲区标志置位
    while( RESET == USART_GetFlagStatus(WIFI_USART, USART_FLAG_TXE) ){
        timeOut--;
        if (timeOut==0){
            break;
        }
    } 
}

/**
 * @brief WIFI发送一个字符串数据
 * 
 * @param str 字符串首地址
 */
uint8_t WIFI_USART_Send_String(uint8_t *str)
{
    if (str == NULL) return 0; // 地址为空跳出
    while (*str) // 值为空跳出
    {
        WIFI_USART_Send_Char(*str++);// 高位优先
    }
    return 1;
}

/**
 * @brief 清除WIFI串口接收的数据
 * 
 */
void WIFI_Clear_RX_BUFF(void)
{
    memset(WIFI_RX_BUFF, 0, WIFI_RX_LEN_MAX);
    WIFI_RX_LEN = 0;
    WIFI_RX_FLAG = 0;
}

/**
 * @brief 读取WIFI串口接收的数据
 * 
 * @param buff 数组首地址,用户提供的缓冲区地址
 * @param len 要读取的数据长度
 * @return uint8_t 1 ---- 成功;0 ---- 失败
 */
uint8_t WIFI_RX_BUFF_Read(uint8_t *buff, uint16_t len)
{
    if (buff == NULL || len == 0 || WIFI_RX_FLAG == 0) return 0; // 地址为空或长度为0或没有接收到数据,跳出
    if (len > WIFI_RX_LEN) return 0; // 请求读取的长度超过接收的数据长度,跳出

    memcpy(buff, WIFI_RX_BUFF, len); // 从接收缓冲区复制数据到用户提供的缓冲区
    return 1;
}

/**
 * @brief 通过WIFI发送AT指令,并查看WIFI模块是否返回想要的数据
 * 
 * @param cmd 发送的AT指令
 * @param ack 想要的应答
 * @param waitms 等待应答的时间
 * @param cnt 等待应答多少次
 * @return uint8_t 返回1则表示得到了想要的应答,否则不是。
 */
uint8_t WIFI_Send_Cmd(char *cmd,char *ack, uint16_t waitms, uint8_t cnt)
{
    uint32_t timeOut = waitms * 10000, ackCnt = cnt;

    while (ackCnt--)
    {
        timeOut = waitms * 10000;
        WIFI_Clear_RX_BUFF();  // 清空接收缓冲区

        // 发送AT指令
        WIFI_USART_Send_String((uint8_t*)cmd);

        /* 超时等待应答 */
        while (WIFI_RX_FLAG == 0) {
            timeOut--;
            if (timeOut == 0) break;
        }

        /* 判断应答执行指令 */
        if( WIFI_RX_FLAG == 1 )
        {
            WIFI_RX_FLAG = 0;
            WIFI_RX_LEN = 0;
            // 查找是否有想要的应答
            if( strstr((char*)WIFI_RX_BUFF, ack) != NULL )
            {
                memset( WIFI_RX_BUFF, 0, sizeof(WIFI_RX_BUFF));
                return 1;
            }
            // 清除接收的数据
            memset( WIFI_RX_BUFF, 0, sizeof(WIFI_RX_BUFF) );
        }
        WIFI_RX_FLAG = 0;
        WIFI_RX_LEN = 0;
    }

    return 0;
}
  • 这里的WIFI_Send_Cmd函数后续将会用于发布AT指令给esp8266
  • 配置初始化函数:我目前只使用STA模式,那么AP模式的代码可以忽略。

/**
 * @brief 初始化WIFI模块ESP01S
 * 
 * @param mode WIFI模式:参考 @WIFI_Mode_t
 * @return uint8_t 返回配置成功标志
 *                  1 ---- 成功
 *                  0 ---- 失败
 */
uint8_t WIFI_ESP8266_Init(WIFI_Mode_t mode)
{
    uint8_t ret = 0;

    WIFI_USART_Config(115200);//默认波特率为115200
    if (mode == WIFI_MODE_AP)       ret = WIFI_MODE_AP_Init();
    else if (mode == WIFI_MODE_STA) ret = WIFI_MODE_STA_Init();

    return ret;
}

/**
 * @brief 开启AP模式,即模块开启热点让手机连接。
 * 手机通过WIFI模块默认的IP地址192.168.4.1和设置的端口号,进行连接
 * 
 * @return uint8_t 1 ---- 配置成功;否则配置失败
 */
uint8_t WIFI_MODE_AP_Init(void)
{
    uint8_t ret = 0;
    char send_buff[200];

    /* 初始化缓冲区 */
    memset(WIFI_RX_BUFF, 0, WIFI_RX_LEN_MAX);

    Delay_ms(1000); // 等待模块上电稳定
    /* 复位 */
    ret = WIFI_Send_Cmd("AT+RST\r\n", "ready", 500, 1);

    /* 测试指令:AT\r\n */
    ret = WIFI_Send_Cmd("AT\r\n", "OK", 10, 1);
    if (ret == 0) goto exit_init;

    /* 配置WIFI AP模式 */
    ret = WIFI_Send_Cmd("AT+CWMODE_CUR=2\r\n", "OK", 30, 1);
    if (ret == 0) goto exit_init;

    /* 设置wifi账号与密码 */
    sprintf(send_buff, "AT+CWSAP=\"%s\",\"%s\",11,4\r\n", AP_WIFISSID, AP_WIFIPASS);
    ret = WIFI_Send_Cmd(send_buff, "OK", 30, 1);
    if (ret == 0) goto exit_init;

    /* 开启多个连接 */
    ret = WIFI_Send_Cmd("AT+CIPMUX=1\r\n", "OK", 50, 1);
    if (ret == 0) goto exit_init;

    /* 开启TCP服务器设置端口号为8080 */
    ret = WIFI_Send_Cmd("AT+CIPSERVER=1,8080\r\n", "OK", 500, 1);
    if (ret == 0) goto exit_init;

exit_init:
    printf("WIFI_MODE_AP_Init: ret = %d\r\n", ret);

    return ret;
}

/**
 * @brief 获取WIFI接收标志位
 * 
 * @return uint8_t 返回 1 ---- 已经接收到数据
 *                      0 ---- 未接受到数据
 * @note 该标志位在中断中被置位,在获取数据函数中被清除
 */
uint8_t WIFI_GetRxFlag()
{
    return WIFI_RX_FLAG;
}

uint8_t WIFI_ClearRxFlag()
{
    WIFI_RX_FLAG = 0;
    return 1;
}

/**
 * @brief 获取WIFI接收数据长度
 * 
 * @return uint32_t 
 */
uint32_t WIFI_GetRxLen()
{
    return WIFI_RX_LEN;
}


/********************************* WIFI STA模式 *********************************/
/**
 * @brief 配置WIFI模块为STA模式
 * 
 */
uint8_t WIFI_MODE_STA_Init(void)
{
    uint8_t res = 0;// 返回错误码

    WIFI_Send_Cmd("AT+RST\r\n", "ready", 500, 1);
    Delay_ms(2000); // 等待模块上电稳定

    // 测试指令AT
    res = WIFI_Send_Cmd("AT\r\n", "OK", 100, 3);
    if (res == 0) {
        res = 11;
        goto exit_init;
    }
    Delay_ms(500);

    // 配置WIFI STA模式
    res = WIFI_Send_Cmd("AT+CWMODE=1\r\n", "OK", 300, 3);
    if (res == 0) {
        res = 12;
        goto exit_init;
    }
    Delay_ms(500);

exit_init:
    printf("WIFI_MODE_STA_Init: res = %d\r\n", res);
    
    return res;
}
  • 重点关注WIFI_MODE_STA_Init这个函数,先发送复位指令,测试指令,最后用AT+CWMODE=1把当前模式设置为STA。这个指令设置0时进入AP模式,设置为2时进入AP模式和STA模式两种,也就是既可以连接他人的WIFI热点,自己也开启WIFI热点供他人连接。
  • 实现串口中断函数逻辑

        串口空闲中断在串口每次接收完一段长长的数据之后都会进入,但是在每次接收完一个字节数据之后都会进入串口中断函数,所以在执行功能前要进行判断一下当前是空闲中断还是接收一个字节后产生的中断。


/**
 * @brief 连接WIFI的串口中断服务函数
 * 
 */
void WIFI_IRQHandler(void)
{
    if (USART_GetITStatus(WIFI_USART, USART_IT_IDLE) == SET)
    {
        volatile uint32_t temp, count;

        /* 清空标志位 */
        temp = USART_ReceiveData(WIFI_USART);
        count = DMA_GetCurrDataCounter(WIFI_DMA_CH);
        if (count < WIFI_RX_LEN_MAX)
        {
            /* 接收数据完成后产生的空闲 */
            /* 重置DMA传输计数器 */
            DMA_Cmd(WIFI_DMA_CH, DISABLE);

            /* 记录接收个数:总长度减去剩余数量即为已经传输的数量 */
            WIFI_RX_LEN = WIFI_RX_LEN_MAX - count;
            
            /* 数据处理 */
            WIFI_RX_BUFF[WIFI_RX_LEN] = '\0';   // 字符串结尾补 '\0'
            WIFI_RX_FLAG = SET;                 // 接收完成标志置位
            // WIFI_CheckState();                  // 检测接收到的数据包以执行对应的指令

            // printf("WIFI_RX_BUFF = %s\r\n", WIFI_RX_BUFF);

            /* 重启DMA */
            DMA_SetCurrDataCounter(WIFI_DMA_CH, WIFI_RX_LEN_MAX);
            DMA_Cmd(WIFI_DMA_CH, ENABLE);
        } else {
            /* 发送数据完成后产生的空闲 */
        }
    }
}
  • 清空标志位:只需要读取一下串口的接收数据寄存器就可以触发硬件自动清除标志位,不需要手动清除。之所以使用volatile 来定义是因为接收的数据来自硬件寄存器,编译器会自动优化导致读取到的数据不对,使用volatile可以防止编译器优化。
        volatile uint32_t temp, count;

        /* 清空标志位 */
        temp = USART_ReceiveData(WIFI_USART);
        count = DMA_GetCurrDataCounter(WIFI_DMA_CH);

注意:

  • 每次DMA运输完数据后都要手动关掉DMA,重置当前的DMA_SetCurrDataCounter再进行数据转移,DMA_SetCurrDataCounter设置的是它下次传输能够传输的最大长度,直接赋值给串口缓冲区的长度即可,表示下次传输最多能传满整个缓冲区。
  • 读取完之后设置一个标志位WIFI_RX_FLAG ,后续通过判断这个标志位来识别是否接收到OneNet平台的回复或者下发的数据。
            /* 重置DMA传输计数器 */
            DMA_Cmd(WIFI_DMA_CH, DISABLE);

            /* 记录接收个数:总长度减去剩余数量即为已经传输的数量 */
            WIFI_RX_LEN = WIFI_RX_LEN_MAX - count;
            
            /* 数据处理 */
            WIFI_RX_BUFF[WIFI_RX_LEN] = '\0';   // 字符串结尾补 '\0'
            WIFI_RX_FLAG = SET;                 // 接收完成标志置位
            // WIFI_CheckState();                  // 检测接收到的数据包以执行对应的指令

            // printf("WIFI_RX_BUFF = %s\r\n", WIFI_RX_BUFF);

            /* 重启DMA */
            DMA_SetCurrDataCounter(WIFI_DMA_CH, WIFI_RX_LEN_MAX);
            DMA_Cmd(WIFI_DMA_CH, ENABLE);

2.3.2 ONENET平台的连接

        我已经开启了WIFI的STA模式,但还没有连接任何WIFI热点,我需要连接热点,然后通过MQTT协议与平台通信,ESP8266的最新固件已经支持这个功能,在此之前要更新一下固件。

        直接访问安信可官方,链接如下 安信可固件相关

        我下载的是v2.2.0版本固件,固件和说明文档最好都要下载,不同版本的固件,支持MQTT,但是AT指令的参数顺序会有所不同,后面遇到问题可以看文档。

下载两个文件

固件烧录需要官方提供的烧录工具:下载地址: 烧录工具

        打开软件进行烧录,只会烧录勾选的地方,从地址0x00开始烧录,注意接线,IO0要接地,表示进入烧录模式。先断开3v3供电,再点击ERASE擦除,然后插上3V3,上电后就会擦除旧版本的固件了,然后再断开供电,点一下START,重新供电,就能烧录成功了。下面有进度条显示就表示成功。

ESP8266 USB转TTL
3V3 3V3
GND GND
TX RX
RX TX
IO0 GND

烧录完后重新接线,接回最初的接线。

  • 连接ONENET平台:现在开始准备ONENET的连接,代码如下
#include "onenet.h"
#include "stdio.h"
#include "string.h"
#include "Delay.h"
#include "cJSON.h"
#include "led.h" 

/* AT指令下WIFI连接信息 */
char wifi_info[] = "AT+CWJAP=\"" WIFISSID "\",\"" WIFIPASS "\"\r\n";
char onenet_info[] = "AT+MQTTCONN=0,\"mqtts.heclouds.com\",1883,0\r\n";
/* AT+MQTTCONN=<LinkID>,<host>,<port>,<reconnect> 
    <LinkID>: 连接ID,取值范围0~4,当前模块仅支持0
    <host>: MQTT服务器地址,详见OneNet平台文档,当前为mqtts.heclouds.com
    <port>: MQTT服务器端口号,详见OneNet平台文档,一般为1883
    <reconnect>: 是否自动重连,0-否,1-是
*/

/**
 * @brief 连接OneNet平台的函数
 * 
 * @return int8_t 返回1 ---- 连接成功,其他均为错误码
 */
int8_t ONENET_Connect(void)
{
    int8_t res = 0;
    char AT_CMD[250] = {0};

    // 测试指令AT
    WIFI_Send_Cmd("AT\r\n", "OK", 100, 3);

    // 连接热点
    // printf("正在连接热点%s\r\n%s\r\n", WIFISSID, WIFIPASS);
    sprintf(AT_CMD, "AT+CWJAP=\"%s\",\"%s\"\r\n", WIFISSID, WIFIPASS);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 3000, 3);
    if (res == 0) {
        res = 12;
        goto exit_init;
    }
    Delay_ms(1000);
    memset(AT_CMD, 0, sizeof(AT_CMD));// 清0数组,备用

    // 设置DHCP
    // printf("正在设置DHCP\r\n");
    // res = WIFI_Send_Cmd("AT+CWDHCP=1,1\r\n", "OK", 500, 3);
    // if (res == 0) {
    //     res = 13;
    //     goto exit_init;
    // }

    // 配置MQTT连接
    sprintf(AT_CMD, "AT+MQTTUSERCFG=0,1,\"%s\",\"%s\",\"%s\",0,0,\"\"\r\n", ONENET_DEVID, ONENET_PROID, ONENET_AUTH_INFO);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);
    if (res == 0) {
        res = 15;
        goto exit_init;
    }
    memset(AT_CMD, 0, sizeof(AT_CMD));// 清0数组,备用
    Delay_ms(1000);

    // 连接到MQTT平台
    printf("正在连接物联平台: %s\r\n", onenet_info);
    WIFI_Send_Cmd(onenet_info, "OK", 5000, 5);  // 连接到OneNet服务器
    Delay_ms(1000);
    if (res == 0) {
        res = 16;
        goto exit_init;
    }

    // 订阅云平台回应主题:能够在每次发布数据后如果出错,云平台将回应错误原因,便于调试。
    memset(AT_CMD, 0, sizeof(AT_CMD));
    sprintf(AT_CMD, "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/post/reply\",0\r\n", ONENET_PROID, ONENET_DEVID);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);
    if (res == 0) {
        res = 17;
        goto exit_init;
    }
    Delay_ms(1000);

    // 订阅主题:能够在手机APP上修改设备属性,例如修改LED开关状态
    memset(AT_CMD, 0, sizeof(AT_CMD));
    sprintf(AT_CMD, "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/set\",0\r\n", ONENET_PROID, ONENET_DEVID);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);
    if (res == 0) {
        res = 18;
        goto exit_init;
    }

exit_init:
    printf("ESP8266 Init OK: %d\r\n", res);

    return res;
}

        这里先使用AT+CWJAP连接热点,后面连接的参数分别是你要连接到wifi热点的账号密码,字符串形式

    // 连接热点
    printf("正在连接热点%s\r\n%s\r\n", WIFISSID, WIFIPASS);
    sprintf(AT_CMD, "AT+CWJAP=\"%s\",\"%s\"\r\n", WIFISSID, WIFIPASS);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 3000, 3);

        然后使用AT+MQTTUSERCFG配置用户属性

参数如下:

  • LinkID连接ID号,官方手册只支持0
  • scheme:填1,表示通过TCP来传输MQTT协议
  • cliend_id客户端ID,指的就是设备ID,字符串形式
  • username用户名,指的就是产品ID,字符串形式
  • password密码,指的就是之前用token工具生成的那一串version开头的,字符串形式
  • cert_key_ID证书ID,模块只支持0
  • CA_ID:模块仅支持0
  • path:资源路径,不需要,但也要赋值,直接给两个双引号表示空字符串即可。
    // 配置MQTT连接
    sprintf(AT_CMD, "AT+MQTTUSERCFG=0,1,\"%s\",\"%s\",\"%s\",0,0,\"\"\r\n", ONENET_DEVID, ONENET_PROID, ONENET_AUTH_INFO);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);

        然后连接到MQTT平台,使用AT+MQTTCONN,可以建立连接

char onenet_info[] = "AT+MQTTCONN=0,\"mqtts.heclouds.com\",1883,0\r\n";    
    // 连接到MQTT平台
    printf("正在连接物联平台: %s\r\n", onenet_info);
    WIFI_Send_Cmd(onenet_info, "OK", 5000, 5);  // 连接到OneNet服务器

参数如下:

  • LinkID:官方手册只支持0
  • scheme:填1,表示通过TCP来传输MQTT协议
  • host:MQTT代理的域名,在ONENET官方文档有描述,字符串形式
  • port:端口号,在ONENET官方文档有描述,一般为1883
  • reconnect:是否自动重连,填0就是不重连

        host和port在文档中此处可以查看(文档会更新,会不一样)

  • 订阅回复:连接MQTT代理后可以订阅云平台发布的回复消息,这样的话,每次发布消息到云平台时,如果参数错误,单片机会接收到错误消息,提示错误的内容是哪,调试的时候就可以直接从串口缓冲区查看错误信息。
    // 订阅云平台回应主题:能够在每次发布数据后如果出错,云平台将回应错误原因,便于调试。
    memset(AT_CMD, 0, sizeof(AT_CMD));
    sprintf(AT_CMD, "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/post/reply\",0\r\n", ONENET_PROID, ONENET_DEVID);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);

        比如我发送id键值对:

{
	"id":"1"
}

        他就会回复,需要参数。因为我创建产品时设置了三个参数,上传数据的时候至少要带有一个参数,才能发送成功。

        如果发送如下内容:

{
	"id": "123",
	"params": {
		"temp": {
			"value": 27
		},
		"hum": {
			"value": 66
		}
	}
}

        就会回复:发送成功

        用于检查发送的消息是否准确,这就是订阅回复的作用。

  • 订阅主题:接下来就要订阅主题了,使用AT+MQTTSUB
    // 订阅主题:能够在手机APP上修改设备属性,例如修改LED开关状态
    memset(AT_CMD, 0, sizeof(AT_CMD));
    sprintf(AT_CMD, "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/set\",0\r\n", ONENET_PROID, ONENET_DEVID);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);

参数如下:

  • LinkID:官方手册只支持0
  • topic主题,想要订阅哪个主题就填哪个,ONENET平台有规定发布的格式,字符串形式:

                $sys/{你的产品ID}/{你的设备ID}/thing/property/set

  • qos服务质量,填0,表示不重要,发送消息后不用等待接收方回应。如果是1或2的话要多次回应,适用于需要保证数据传输到位的场景。

2.3.3 单片机发布数据到云平台

  • 发布数据:接下来要发送温湿度数据到云平台,使用AT+MQTTPUB

/**
 * @brief 向OneNet平台发布数据的函数
 * 
 * @param temp 要发送的温度数据
 * @param humi 要发送的湿度数据
 * @return int8_t 为1表示发布成功,其他为错误码
 */
int8_t Publish_DHT11_Data(float temp, float humi)
{
    int8_t res = 0;
    char cmd[512];

    // 发布格式:AT+MQTTPUB=<link_ID>,<"topic">,<"data">,<QoS>,<retain>
    // 实际发送的例如:AT+MQTTPUB=0,"$sys/br7KEdD3U4/temp_hum/thing/property/post","{\"id\":\"13\"\,\"params\":{\"hum\":{\"value\":70}\,\"temp\":{\"value\":27.0}}}",0,0
    // 发现一个坑,逗号也需要转义,使用\,
    sprintf(cmd, "AT+MQTTPUB=0,\"$sys/%s/%s/thing/property/post\",\"{\\\"id\\\":\\\"12\\\"\\,\\\"params\\\":{\\\"temp\\\":{\\\"value\\\":%.1f}\\,\\\"hum\\\":{\\\"value\\\":%.1f}}}\",0,0\r\n",
        ONENET_PROID, ONENET_DEVID, temp, humi);
    res = WIFI_Send_Cmd(cmd, "OK", 2000, 5);

    return res;
}

其中参数如下:

  • LinkID:官方手册只支持0
  • topic主题,想要发布到哪个主题就填哪个,ONENET平台有规定发布的格式,字符串形式:

                $sys/{你的产品ID}/{你的设备ID}/thing/property/post 

  • data:要发送的数据,以字符串形式发送字典格式数据到服务器中
  • qos服务质量,填0,表示不重要,发送消息后不用等待接收方回应。如果是1或2的话要多次回应,适用于需要保证数据传输到位的场景。
  • retain保留消息,作用是发送数据后保留一次最新的消息,等订阅者订阅之后自动会发送给他。给0即可,不使用。

        发布时要发送json格式的数据,在单片机里只能通过字符串形式去发布json格式,因为这里我们使用sprintf函数去把字符串给格式化,所以双引号之前要用反斜杠转义一下,反斜杠要用两个反斜杠来表示。

    sprintf(cmd, "AT+MQTTPUB=0,\"$sys/%s/%s/thing/property/post\",\"{\\\"id\\\":\\\"12\\\"\\,\\\"params\\\":{\\\"temp\\\":{\\\"value\\\":%.1f}\\,\\\"hum\\\":{\\\"value\\\":%.1f}}}\",0,0\r\n",
        ONENET_PROID, ONENET_DEVID, temp, humi);

        举个例子,esp8266实际收到的数据如下:每个引号和逗号之前都会有反斜杠。之前我逗号之前没加反斜杠导致一直返回错误,这是一个需要注意的地方。

AT+MQTTPUB=0,"$sys/br7KEdD3U4/temp_hum/thing/property/post","{\"id\":\"13\"\,\"params\":{\"hum\":{\"value\":66}\,\"temp\":{\"value\":27}}}",0,0

2.3.4 单片机接收到云平台下发的数据

        单片机订阅了主题之后,每当有别的设备向云平台发送温湿度数据,单片机也会接收到,为了让单片机不错过数据接收到时机,要在主循环中轮询检测云平台下发的消息。

代码如下:每次调用这个函数都会获取一下串口接收的标志位,单片机一旦接收完数据,触发了串口空闲中断,在中断函数里我们把WIFI接收标志位置位了,然后通过WIFI_GetRxFlag函数返回来,我们通过读取这个标志来判断是否接受到消息。接收到消息之后才会进行json格式数据的解析

/**
 * @brief 处理来自云端的消息
 * 
 * @return int8_t 为1表示成功处理消息,0表示没有新消息,其他为错误码
 */
int8_t MCU_ProcessMsgFromCloud(void)
{
    int8_t ret = (int8_t)WIFI_GetRxFlag();
    uint32_t WIFI_RX_LEN = WIFI_GetRxLen();

    // 如果没有接收到数据,直接返回0
    if (ret == 0)   return 0;
    else            WIFI_ClearRxFlag();

    // 定义一个指向接收缓冲区的字符串指针,防止直接操作全局数组导致混乱
    char *buf = (char*)WIFI_RX_BUFF;

    // 检查是否是订阅主题的消息:
    // 收到"+MQTTSUBRECV:0,"开头的消息,表示有设备向订阅的主题发送了数据
    if (strstr(buf, "+MQTTSUBRECV:0,\"") == NULL) {
        WIFI_Clear_RX_BUFF();
        ret = 11;
        return ret;
    }

    // 定位JSON起始位置(第一个 '{')
    char *json_str = strchr(buf, '{');
    if (json_str == NULL) {
        WIFI_Clear_RX_BUFF();
        ret = 12;
        return ret;
    }

    // 解析JSON
    cJSON *root = cJSON_Parse(json_str);
    if (root == NULL) {
        const char *error_ptr = cJSON_GetErrorPtr();
        if (error_ptr != NULL)
            printf("JSON parse error: %s\n", error_ptr);
        WIFI_Clear_RX_BUFF();
        ret = 13;
        return ret;
    }

    // 提取 params 对象
    cJSON *params = cJSON_GetObjectItem(root, "params");
    if (params == NULL) {
        // printf("No params object\n");
        cJSON_Delete(root);
        WIFI_Clear_RX_BUFF();
        ret = 14;
        return ret;
    }
    // 提取 LED 属性(布尔值)
    cJSON *led = cJSON_GetObjectItem(params, "LED");
    if (led != NULL && cJSON_IsBool(led)) {
        if (cJSON_IsTrue(led)) {
            LED_B_ON;
            printf("LED ON\n");
        } else {
            LED_B_OFF;
            printf("LED OFF\n");
        }
    } else {
        printf("LED property not found or not bool\n");
    }

    // 释放cJSON对象
    cJSON_Delete(root);
    WIFI_Clear_RX_BUFF();
    ret = 1; // 成功处理消息
    return ret;
}
  • 数据检查:收到"+MQTTSUBRECV:0,"开头的消息,表示有设备向订阅的主题发送了数据,云平台会下发数据到单片机,接下来为了解析json数据,先把指针移动到json数据的开头,也就是指向第一个花括号的位置。这里需要注意的是:先定义了个指针变量buf,指向WIFI接收缓冲区首地址,而不是直接使用首地址,因为直接使用的话在调用strstr函数时会出问题。
    // 如果没有接收到数据,直接返回0
    if (ret == 0)   return 0;
    else            WIFI_ClearRxFlag();

    // 定义一个指向接收缓冲区的字符串指针,防止直接操作全局数组导致混乱
    char *buf = (char*)WIFI_RX_BUFF;

    // 检查是否是订阅主题的消息:
    // 收到"+MQTTSUBRECV:0,"开头的消息,表示有设备向订阅的主题发送了数据
    if (strstr(buf, "+MQTTSUBRECV:0,\"") == NULL) {
        WIFI_Clear_RX_BUFF();
        ret = 11;
        return ret;
    }

    // 定位JSON起始位置(第一个 '{')
    char *json_str = strchr(buf, '{');
    if (json_str == NULL) {
        WIFI_Clear_RX_BUFF();
        ret = 12;
        return ret;
    }
  • json数据解析:与发送数据到云平台一样,接收到的数据都是json格式,需要使用cJSON库,从官方GitHub下载,只需下载两个文件“cJSON.h"、"cJSON.c"即可。

        首先使用cJSON_Parse函数对接收到的字符串进行解析,获得一个json对象。需要注意的是,函数内使用了动态内存分配,在使用完之后要手动释放内存。

    // 解析JSON
    cJSON *root = cJSON_Parse(json_str);
    if (root == NULL) {
        const char *error_ptr = cJSON_GetErrorPtr();
        if (error_ptr != NULL)
            printf("JSON parse error: %s\n", error_ptr);
        WIFI_Clear_RX_BUFF();
        ret = 13;
        return ret;
    }

        然后要获取参数项,我们之前提到过,ONENET平台发布的json数据包括idparams的键值对,id可以随意取,只要不同设备不相同id就可以了。我们首先要调用cJSON_GetObjectItem函数,获取params的键值对,params之内又包含温度、湿度、LED的键值对,我们需要再从params中取出LED键值对,再进一步判断LED是1还是0就可以了,布尔类型,非1即0,为1则调用点灯函数。就可以实现远程点灯了。

    // 提取 params 对象
    cJSON *params = cJSON_GetObjectItem(root, "params");
    if (params == NULL) {
        // printf("No params object\n");
        cJSON_Delete(root);
        WIFI_Clear_RX_BUFF();
        ret = 14;
        return ret;
    }

    // 提取 LED 属性(布尔值)
    cJSON *led = cJSON_GetObjectItem(params, "LED");
    if (led != NULL && cJSON_IsBool(led)) {
        if (cJSON_IsTrue(led)) {
            LED_B_ON;
            printf("LED ON\n");
        } else {
            LED_B_OFF;
            printf("LED OFF\n");
        }
    } else {
        printf("LED property not found or not bool\n");
    }
  • 释放json内存

        cJSON库内使用了malloc来调用内存分配函数,使用完后要调用free函数去释放内存并置空。在cJSON库中有专门的函数来释放内存,调用cJSON_Delete即可。

    // 释放cJSON对象
    cJSON_Delete(root);
    WIFI_Clear_RX_BUFF();
    ret = 1; // 成功处理消息
    return ret;

2.3.5 主循环逻辑

        在主循环中每隔一秒采集一次温湿度数据并发布到ONENET平台,同时轮询解析来自平台下发的LED开关灯指令即可。

#include "stm32f10x.h"
#include "Delay.h"
#include "led.h" 
#include "dht11.h"
#include "OLED.h"
#include "usart.h"
#include "esp8266.h"
#include "onenet.h"

float temp = 0, humi = 0;
uint8_t state = 0;

int main()
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    LED_Init();
    OLED_Init();
    DHT11_Init();
    USART_Config(115200);
    WIFI_ESP8266_Init(WIFI_MODE_STA);
    ONENET_Connect();

    OLED_ShowString(1, 1, "Temp:");
    OLED_ShowString(2, 1, "Humi:");

    DHT11_ReadData(&temp, &humi);//数据获取
    Publish_DHT11_Data(temp, humi);
    OLED_ShowNum(1, 6, temp, 2); //显示温度
    OLED_ShowNum(2, 6, humi, 2); //显示湿度

    while (1)
    {
        DHT11_ReadData(&temp, &humi);//数据获取
        // OLED_ShowNum(3, 1, Publish_DHT11_Data(temp, humi), 1);
        Publish_DHT11_Data(temp, humi);
        Delay_ms(1000);
        OLED_ShowNum(1, 6, temp, 2); //显示温度
        OLED_ShowNum(2, 6, humi, 2); //显示湿度
        MCU_ProcessMsgFromCloud();

        // printf("Temp:%.2fC  Humi:%.2f%%\n", temp, humi);
    }
}

2.4 调试现象

  • 初始化调试:下载烧录后,打开手机热点,再给单片机通电,打开串口助手查看连接情况

        res为1则说明正常,其他值均为错误码。那么初始化已经成功。

  • 发布调试:接下来查看云平台是否接收到单片机端发布的温湿度数据,打开属性栏查看

        与OLED显示的一致,只不过OLED我使用的函数是显示整数数字,所以小数点被忽略了。

那么发布数据也成功了。

  • 订阅调试:最后看一下接收的数据。目前还没做APP,所以用ONENET平台提供的调试器来下发LED指令给单片机。

        打开设备调试栏,选择应用模拟

        打勾LED,选择发送true

        此时右侧返回两个结果,发送成功和响应超时,可能是因为我们只写接收数据的代码,没有再接收之后发出响应导致的。不重要,数据确实被单片机接收到了。

        注意:有时候发送失败,可能是你发送的时候,单片机正好执行了发布数据温湿度的代码,发布与订阅冲突了,或者是别的原因,重试几次就行。

  • 实际现象:观察实物,蓝色LED已点亮。红色是电源灯,本来就是亮的。

2.5 本次调试的完整代码

esp8266.c

#include "esp8266.h"
#include "Delay.h"
#include "stdio.h"
#include "string.h"
#include <stdlib.h>
#include "usart.h"

/**************** 静态函数声明 ****************/
static void WIFI_USART_Send_Char(uint8_t ch);

/*************** 静态变量声明 ***************/
static uint8_t WIFI_RX_FLAG = 0;            // WIFI接收数据完成标志位
static uint32_t WIFI_RX_LEN = 0;            // WIFI当前接收到的数据长度
static uint8_t WIFI_AP_Link_Num = 0;        // AP模式下连接热点的设备数量
static uint8_t WIFI_TCP_Link_Num = 0;       // AP模式下连接TCP服务器的设备数量

/****** 数组声明 ******/
uint8_t WIFI_RX_BUFF[WIFI_RX_LEN_MAX];   // WIFI接收数据缓冲区

/**
 * @brief 串口参数配置
 * 
 * @param Baud 波特率
 */
static void WIFI_USART_Config(uint32_t Baud)
{
    /* 开启时钟 */
    WIFI_USART_CLK_ENABLE();
    WIFI_USART_GPIO_CLK_ENABLE();

    /* 初始化GPIO */
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.GPIO_Pin   = WIFI_USART_TX_Pin;    // 发送
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(WIFI_USART_TX_Port, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Pin   = WIFI_USART_RX_Pin;  // 接收
    GPIO_InitStruct.GPIO_Mode  = GPIO_Mode_IPU;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(WIFI_USART_RX_PORT, &GPIO_InitStruct);

    /* 初始化USART参数 */
    USART_InitTypeDef USART_InitStruct = {0};
    USART_InitStruct.USART_BaudRate            = Baud;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode                = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStruct.USART_Parity              = USART_Parity_No;
    USART_InitStruct.USART_StopBits            = USART_StopBits_1;
    USART_InitStruct.USART_WordLength          = USART_WordLength_8b;
    USART_Init(WIFI_USART, &USART_InitStruct);

    /* 配置USART的发送/接收中断 */
    USART_ITConfig(WIFI_USART, USART_IT_IDLE, ENABLE);

    /* 配置DMA */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    DMA_DeInit(WIFI_DMA_CH);
    DMA_InitTypeDef DMA_InitStruct = {0};
    DMA_InitStruct.DMA_M2M                = DMA_M2M_Disable;
    DMA_InitStruct.DMA_DIR                = DMA_DIR_PeripheralSRC;
    DMA_InitStruct.DMA_MemoryInc          = DMA_MemoryInc_Enable;
    DMA_InitStruct.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
    DMA_InitStruct.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
    DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStruct.DMA_MemoryBaseAddr     = (uint32_t)WIFI_RX_BUFF;
    DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&WIFI_USART->DR;
    DMA_InitStruct.DMA_BufferSize         = WIFI_RX_LEN_MAX;
    DMA_InitStruct.DMA_Mode               = DMA_Mode_Normal;
    DMA_InitStruct.DMA_Priority           = DMA_Priority_VeryHigh;
    DMA_Init(WIFI_DMA_CH, &DMA_InitStruct);

    /* 初始化NVIC */
    NVIC_InitTypeDef NVIC_InitStruct = {0};
    NVIC_InitStruct.NVIC_IRQChannel                   = WIFI_IRQ;
    NVIC_InitStruct.NVIC_IRQChannelCmd                = ENABLE;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 3;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority        = 3;
    NVIC_Init(&NVIC_InitStruct);

    /* 开启USART */
    USART_DMACmd(WIFI_USART, USART_DMAReq_Rx, ENABLE);
    USART_Cmd(WIFI_USART, ENABLE);
    DMA_Cmd(WIFI_DMA_CH, ENABLE);
}

/**
 * @brief WIFI发送一个字节数据
 * 
 * @param ch 要发送的字符数据
 */
static void WIFI_USART_Send_Char(uint8_t ch)
{
    uint32_t timeOut = 100000;
    if (USART_GetFlagStatus(WIFI_USART, USART_FLAG_TXE) == SET)
        USART_SendData(WIFI_USART, (uint8_t)ch);

    // 等待发送数据缓冲区标志置位
    while( RESET == USART_GetFlagStatus(WIFI_USART, USART_FLAG_TXE) ){
        timeOut--;
        if (timeOut==0){
            break;
        }
    } 
}

/**
 * @brief WIFI发送一个字符串数据
 * 
 * @param str 字符串首地址
 */
uint8_t WIFI_USART_Send_String(uint8_t *str)
{
    if (str == NULL) return 0; // 地址为空跳出
    while (*str) // 值为空跳出
    {
        WIFI_USART_Send_Char(*str++);// 高位优先
    }
    return 1;
}

/**
 * @brief 清除WIFI串口接收的数据
 * 
 */
void WIFI_Clear_RX_BUFF(void)
{
    memset(WIFI_RX_BUFF, 0, WIFI_RX_LEN_MAX);
    WIFI_RX_LEN = 0;
    WIFI_RX_FLAG = 0;
}

/**
 * @brief 读取WIFI串口接收的数据
 * 
 * @param buff 数组首地址,用户提供的缓冲区地址
 * @param len 要读取的数据长度
 * @return uint8_t 1 ---- 成功;0 ---- 失败
 */
uint8_t WIFI_RX_BUFF_Read(uint8_t *buff, uint16_t len)
{
    if (buff == NULL || len == 0 || WIFI_RX_FLAG == 0) return 0; // 地址为空或长度为0或没有接收到数据,跳出
    if (len > WIFI_RX_LEN) return 0; // 请求读取的长度超过接收的数据长度,跳出

    memcpy(buff, WIFI_RX_BUFF, len); // 从接收缓冲区复制数据到用户提供的缓冲区
    return 1;
}

/**
 * @brief 通过WIFI发送AT指令,并查看WIFI模块是否返回想要的数据
 * 
 * @param cmd 发送的AT指令
 * @param ack 想要的应答
 * @param waitms 等待应答的时间
 * @param cnt 等待应答多少次
 * @return uint8_t 返回1则表示得到了想要的应答,否则不是。
 */
uint8_t WIFI_Send_Cmd(char *cmd,char *ack, uint16_t waitms, uint8_t cnt)
{
    uint32_t timeOut = waitms * 10000, ackCnt = cnt;

    while (ackCnt--)
    {
        timeOut = waitms * 10000;
        WIFI_Clear_RX_BUFF();  // 清空接收缓冲区

        // 发送AT指令
        WIFI_USART_Send_String((uint8_t*)cmd);

        /* 超时等待应答 */
        while (WIFI_RX_FLAG == 0) {
            timeOut--;
            if (timeOut == 0) break;
        }

        /* 判断应答执行指令 */
        if( WIFI_RX_FLAG == 1 )
        {
            WIFI_RX_FLAG = 0;
            WIFI_RX_LEN = 0;
            // 查找是否有想要的应答
            if( strstr((char*)WIFI_RX_BUFF, ack) != NULL )
            {
                memset( WIFI_RX_BUFF, 0, sizeof(WIFI_RX_BUFF));
                return 1;
            }
            // 清除接收的数据
            memset( WIFI_RX_BUFF, 0, sizeof(WIFI_RX_BUFF) );
        }
        WIFI_RX_FLAG = 0;
        WIFI_RX_LEN = 0;
    }

    return 0;
}

/**
 * @brief 初始化WIFI模块ESP01S
 * 
 * @param mode WIFI模式:参考 @WIFI_Mode_t
 * @return uint8_t 返回配置成功标志
 *                  1 ---- 成功
 *                  0 ---- 失败
 */
uint8_t WIFI_ESP8266_Init(WIFI_Mode_t mode)
{
    uint8_t ret = 0;

    WIFI_USART_Config(115200);//默认波特率为115200
    if (mode == WIFI_MODE_AP)       ret = WIFI_MODE_AP_Init();
    else if (mode == WIFI_MODE_STA) ret = WIFI_MODE_STA_Init();

    return ret;
}

/**
 * @brief 开启AP模式,即模块开启热点让手机连接。
 * 手机通过WIFI模块默认的IP地址192.168.4.1和设置的端口号,进行连接
 * 
 * @return uint8_t 1 ---- 配置成功;否则配置失败
 */
uint8_t WIFI_MODE_AP_Init(void)
{
    uint8_t ret = 0;
    char send_buff[200];

    /* 初始化缓冲区 */
    memset(WIFI_RX_BUFF, 0, WIFI_RX_LEN_MAX);

    Delay_ms(1000); // 等待模块上电稳定
    /* 复位 */
    ret = WIFI_Send_Cmd("AT+RST\r\n", "ready", 500, 1);

    /* 测试指令:AT\r\n */
    ret = WIFI_Send_Cmd("AT\r\n", "OK", 10, 1);
    if (ret == 0) goto exit_init;

    /* 配置WIFI AP模式 */
    ret = WIFI_Send_Cmd("AT+CWMODE_CUR=2\r\n", "OK", 30, 1);
    if (ret == 0) goto exit_init;

    /* 设置wifi账号与密码 */
    sprintf(send_buff, "AT+CWSAP=\"%s\",\"%s\",11,4\r\n", AP_WIFISSID, AP_WIFIPASS);
    ret = WIFI_Send_Cmd(send_buff, "OK", 30, 1);
    if (ret == 0) goto exit_init;

    /* 开启多个连接 */
    ret = WIFI_Send_Cmd("AT+CIPMUX=1\r\n", "OK", 50, 1);
    if (ret == 0) goto exit_init;

    /* 开启TCP服务器设置端口号为8080 */
    ret = WIFI_Send_Cmd("AT+CIPSERVER=1,8080\r\n", "OK", 500, 1);
    if (ret == 0) goto exit_init;

exit_init:
    printf("WIFI_MODE_AP_Init: ret = %d\r\n", ret);

    return ret;
}

/**
 * @brief 复位WIFI模块
 * 
 * @return uint8_t 标志 1 ---- 成功
 *                      0 ---- 失败
 */
uint8_t WIFI_RESET()
{
    uint8_t ret = 0;

    ret = WIFI_Send_Cmd("AT+RST\r\n", "ready", 500, 1);
    return ret;
}

/**
 * @brief 向WIFI模块发送测试指令AT
 * 
 * @return uint8_t 标志 1 ---- 模块正常
 *                      0 ---- 模块异常
 */
uint8_t WIFI_Test()
{
    uint8_t ret = 0;
    ret = WIFI_Send_Cmd("AT\r\n", "OK", 10, 1);
    return ret;
}

/**
 * @brief 设置WIFI模块的账号和密码
 * 
 * @param ssid WIFI账号名称:如果为NULL,表示不修改WIFI的名称
 * @param password WIFI的密码
 * @return uint8_t 标志 1 ---- 成功
 *                      0 ---- 失败
 */
uint8_t WIFI_SetPassword(char *ssid, char *password)
{
    uint8_t ret = 0;
    char send_buff[200] = {0};
    if (ssid == NULL) sprintf(send_buff, "AT+CWSAP=\"%s\",\"%s\",11,4\r\n", AP_WIFISSID, password);
    else sprintf(send_buff, "AT+CWSAP=\"%s\",\"%s\",11,4\r\n", ssid, password);
    ret = WIFI_Send_Cmd(send_buff, "OK", 30, 1);

    return ret;
}

/**
 * @brief WIFI模块在AP模式下开启TCP服务器
 * 
 * @param WIFIPort 需要设置的服务器端口号
 * @return uint8_t 标志 1 ---- 成功
 *                      0 ---- 失败
 */
uint8_t WIFI_StartTCP(uint16_t WIFIPort)
{
    uint8_t ret = 0;
    char sendBuff[200] = {0};

    sprintf(sendBuff, "AT+CIPSERVER=1,%d\r\n", WIFIPort);
    WIFI_Send_Cmd("AT+CIPMUX=1\r\n", "OK", 50, 1); // 开启多连接模式
    ret = WIFI_Send_Cmd(sendBuff, "OK", 50, 1); // 开启服务器

    return ret;
}

/**
 * @brief 设置WIFI模式
 * 
 * @param mode WIFI模式:参考 @WIFI_Mode_t
 * @return uint8_t 返回配置成功标志
 *                  1 ---- 成功
 *                  0 ---- 失败
 */
uint8_t WIFI_SetMode(WIFI_Mode_t mode)
{
    switch (mode)
    {
        case WIFI_MODE_STA:
            return WIFI_Send_Cmd("AT+CWMODE=1\r\n", "OK", 30, 1); 
        
        case WIFI_MODE_AP:
            return WIFI_Send_Cmd("AT+CWMODE=2\r\n", "OK", 30, 1); 

        case WIFI_MODE_AP_STA:
            return WIFI_Send_Cmd("AT+CWMODE=3\r\n", "OK", 30, 1);
        default:
            return 0;
    }
}

/**
 * @brief 检测数据包,执行对应的命令
 * 
 */
void WIFI_CheckState()
{
    /* 不是+IPD,开头的数据都要清空缓冲区 */
    /* 实时记录设备的连接个数 */
    if (WIFI_RX_FLAG == SET)
    {
        if( strstr((char*)WIFI_RX_BUFF, "+STA_CONNECTED") != NULL ) {
            /* 热点连接数量增加 */
            if (WIFI_AP_Link_Num+1 >= 0 && WIFI_AP_Link_Num+1 <= 4) WIFI_AP_Link_Num++;
            WIFI_Clear_RX_BUFF();
        } else if( strstr((char*)WIFI_RX_BUFF, "+STA_DISCONNECTED") != NULL ) {
            /* 热点连接数量增加 */
            if (WIFI_AP_Link_Num-1 >= 0 && WIFI_AP_Link_Num-1 <= 4) WIFI_AP_Link_Num--;
            WIFI_Clear_RX_BUFF();
        } else if( strstr((char*)WIFI_RX_BUFF, "+LINK_CONN") != NULL ) {
            /* TCP服务器连接数量增加 */
            if (WIFI_TCP_Link_Num+1 >= 0 && WIFI_TCP_Link_Num+1 <= 4) WIFI_TCP_Link_Num++;
            WIFI_Clear_RX_BUFF();
        } else if( strstr((char*)WIFI_RX_BUFF, "CLOSED") != NULL ) {
            /* TCP服务器连接数量增加 */
            if (WIFI_TCP_Link_Num-1 >= 0 && WIFI_TCP_Link_Num-1 <= 4) WIFI_TCP_Link_Num--;
            WIFI_Clear_RX_BUFF();
        } else if (strstr((char*)WIFI_RX_BUFF, "+IPD,") != NULL) {
            /* 数据包:不需要清除缓冲区 */
        } else if (strstr((char*)WIFI_RX_BUFF, "AT") != NULL){
            /* AT指令:不需要清除缓冲区 */
        } else {
            /* 无效数据需要清空缓冲区 */
            WIFI_Clear_RX_BUFF();   
        }
    }
}

/**
 * @brief 获取连接WIFI热点的设备数量
 * 
 * @return uint8_t 连接WIFI热点的设备数量,范围:1~5
 */
uint8_t WIFI_GetAPLinkNum()
{
    return WIFI_AP_Link_Num;
}

/**
 * @brief 获取连接TCP服务器的设备数量
 * 
 * @return uint8_t 连接TCP服务器的设备数量,范围:1~5
 */
uint8_t WIFI_GetTCPLinkNum()
{
    return WIFI_TCP_Link_Num;
}

/**
 * @brief 获取WIFI接收标志位
 * 
 * @return uint8_t 返回 1 ---- 已经接收到数据
 *                      0 ---- 未接受到数据
 * @note 该标志位在中断中被置位,在获取数据函数中被清除
 */
uint8_t WIFI_GetRxFlag()
{
    return WIFI_RX_FLAG;
}

uint8_t WIFI_ClearRxFlag()
{
    WIFI_RX_FLAG = 0;
    return 1;
}

/**
 * @brief 获取WIFI接收数据长度
 * 
 * @return uint32_t 
 */
uint32_t WIFI_GetRxLen()
{
    return WIFI_RX_LEN;
}

/**
 * @brief 解析设备发送过来的数据
 * 
 * @param ap_parameter 要将数据保存的地址
 * @return uint8_t 1:有设备发送过来数据
 *                  0:没有设备发送过来数据
 * @note device_id最大5个  //+IPD,1,4:abcd
 */
uint8_t WIFI_AP_GetData(AP_PARAMETER *ap_parameter)
{
    char buff[50];
    char *test;

    uint16_t i=0;

    /* 检测标志位,再获取数据 */
    if (WIFI_RX_FLAG)
    {
        if( strstr((char*)WIFI_RX_BUFF,"+IPD,") != NULL )
        {
            test = strstr((char*)WIFI_RX_BUFF,"+IPD,");

            // 记录设备ID号
            strncpy(buff, test+5, 1);
            buff[1] ='\0';
            ap_parameter->device_id = atoi(buff); // 把字符串转换成整数
            // printf("device_id = %s\r\n",buff);

            // 记录发送过来的数据长度
            strncpy(buff, test+7, strcspn(test+7, ":") );
            buff[ strcspn(test+7, ":") ] ='\0';
            // printf("device_data = %s\r\n",buff);
            ap_parameter->device_datalen = atoi(buff);
            // printf("device_datalen = %s\r\n",buff);

            // 记录发送过来的数据
            memset(buff, 0, sizeof(buff));
            while(test[i++]!=':') {
                if (i >= WIFI_RX_LEN_MAX) break;
            };
            strncpy(buff, test+i, strcspn(test+i, "\r") );
            // printf("device_data = %s\r\n",buff);
            strcpy((char*)ap_parameter->device_data, buff);

            // 清除串口缓存
            WIFI_Clear_RX_BUFF();
            return 1;
        }
    }
    return 0;
}

/**
 * @brief AP模式下,WIFI发送数据至TCP客户端(连接AP模式下热点的设备)
 * 
 * @param id 向第几个客户端发送数据
 * @param data 要发送的数据(字符串形式)
 * @return uint8_t 0=发送失败   1=发送成功
 * @note AP模式下使用
 */
uint8_t WIFI_AP_SendTCPClient(uint8_t id, char* data)
{
    uint8_t send_buf[40] = {0};

    sprintf((char*)send_buf, "AT+CIPSEND=%d,%d\r\n", id, strlen(data));
    if(WIFI_Send_Cmd((char*)send_buf,">", 50, 1))
    {
        WIFI_USART_Send_String((unsigned char *)data);
        return 1;
    }
    return 0;
}

/********************************* WIFI STA模式 *********************************/
/**
 * @brief 配置WIFI模块为STA模式
 * 
 */
uint8_t WIFI_MODE_STA_Init(void)
{
    uint8_t res = 0;// 返回错误码

    WIFI_Send_Cmd("AT+RST\r\n", "ready", 500, 1);
    Delay_ms(2000); // 等待模块上电稳定

    // 测试指令AT
    res = WIFI_Send_Cmd("AT\r\n", "OK", 100, 3);
    if (res == 0) {
        res = 11;
        goto exit_init;
    }
    Delay_ms(500);

    // 配置WIFI STA模式
    res = WIFI_Send_Cmd("AT+CWMODE=1\r\n", "OK", 300, 3);
    if (res == 0) {
        res = 12;
        goto exit_init;
    }
    Delay_ms(500);

exit_init:
    printf("WIFI_MODE_STA_Init: res = %d\r\n", res);
    
    return res;
}

/**
 * @brief 连接WIFI的串口中断服务函数
 * 
 */
void WIFI_IRQHandler(void)
{
    if (USART_GetITStatus(WIFI_USART, USART_IT_IDLE) == SET)
    {
        volatile uint32_t temp, count;

        /* 清空标志位 */
        temp = USART_ReceiveData(WIFI_USART);
        count = DMA_GetCurrDataCounter(WIFI_DMA_CH);
        if (count < WIFI_RX_LEN_MAX)
        {
            /* 接收数据完成后产生的空闲 */
            /* 重置DMA传输计数器 */
            DMA_Cmd(WIFI_DMA_CH, DISABLE);

            /* 记录接收个数:总长度减去剩余数量即为已经传输的数量 */
            WIFI_RX_LEN = WIFI_RX_LEN_MAX - count;
            
            /* 数据处理 */
            WIFI_RX_BUFF[WIFI_RX_LEN] = '\0';   // 字符串结尾补 '\0'
            WIFI_RX_FLAG = SET;                 // 接收完成标志置位
            // WIFI_CheckState();                  // 检测接收到的数据包以执行对应的指令

            // printf("WIFI_RX_BUFF = %s\r\n", WIFI_RX_BUFF);

            /* 重启DMA */
            DMA_SetCurrDataCounter(WIFI_DMA_CH, WIFI_RX_LEN_MAX);
            DMA_Cmd(WIFI_DMA_CH, ENABLE);
        } else {
            /* 发送数据完成后产生的空闲 */
        }
    }
}

esp8266.h

#ifndef __ESP01S_H
#define __ESP01S_H

#include "stm32f10x.h"

/* WIFI模式的类型定义 */
typedef enum{
    WIFI_MODE_STA,
    WIFI_MODE_AP,
    WIFI_MODE_AP_STA
} WIFI_Mode_t;

/* WIFI网络传输协议的类型定义 */
typedef enum{
    WIFI_NetPro_TCP,
    WIFI_NetPro_UDP,
    WIFI_NetPro_SSL,
} WIFI_NetPro_t;

/* WIFI被连接时设备的ID号 */
typedef enum{
	Multiple_ID_0 = 0,      /* 多连接模式下连接ID号可选范围:0~4*/
	Multiple_ID_1 = 1,
	Multiple_ID_2 = 2,
	Multiple_ID_3 = 3,
	Multiple_ID_4 = 4,
	Single_ID_0 = 5,        /* 单连接模式下连接ID号为0 */
} WIFI_Connected_ID_t;

/* WIFI在AP模式下的加密方式 */
typedef enum{
	OPEN = 0,
	WEP = 1,
	WPA_PSK = 2,
	WPA2_PSK = 3,
	WPA_WPA2_PSK = 4,
} WIFI_AP_PsdMode_t;

/* WIFI所使用的串口号 */
#define WIFI_USART                          USART3

/* WIFI功能时钟宏定义 */
#define WIFI_USART_CLK_ENABLE()             RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE)
#define WIFI_USART_GPIO_CLK_ENABLE()        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE)

/* WIFI引脚:RX、TX宏定义 */
#define WIFI_USART_TX_Pin                   GPIO_Pin_10
#define WIFI_USART_TX_Port                  GPIOB
#define WIFI_USART_RX_Pin                   GPIO_Pin_11
#define WIFI_USART_RX_PORT                  GPIOB

/* WIFI中断宏定义 */
#define WIFI_IRQ                    USART3_IRQn
#define WIFI_IRQHandler             USART3_IRQHandler

/* WIFI串口所用DMA通道宏定义 */
#define WIFI_DMA_CH                 DMA1_Channel3
#define WIFI_DMA_IRQ                DMA1_Channel3_IRQn
#define WIFI_DMA_IRQHandler         DMA1_Channel3_IRQHandler

/* WIFI单次接收数据的最大缓存长度 */
#define WIFI_RX_LEN_MAX             512

/* WIFI接收数据缓冲区 */
extern uint8_t WIFI_RX_BUFF[];

/****************************   STA模式    ****************************/
/* STA模式下需要的WIFI模块需要连接的热点名称和密码 */
#define WIFISSID                   "iQOO9"
#define WIFIPASS                   "88888888"

/****************************   AP模式    ****************************/

/* AP模式下,WIFI模块的热点名称和密码 */
#define AP_WIFISSID                         "ESP01S"
#define AP_WIFIPASS                         "12345678"

/**
 * @brief TCP服务器和端口:
 * @note TCP服务器默认IP为192.168.4.1,端口号在AP模式初始化时已被设置为8080
 */

/* WIFI传输的数据结构体(AP模式) */
typedef struct
{
    uint8_t device_id;          // 设备ID
    uint8_t device_datalen;     // 数据长度
    uint8_t device_data[200];   // 数据缓冲区
}AP_PARAMETER;

/****************************   函数声明    ****************************/
/* 初始化函数 */
uint8_t WIFI_ESP8266_Init(WIFI_Mode_t mode);    // WIFI模块初始化
uint8_t WIFI_MODE_AP_Init(void);                // AP模式初始化
uint8_t WIFI_MODE_STA_Init(void);               // STA模式初始化

/* 获取数据的函数 */
uint8_t WIFI_AP_GetData(AP_PARAMETER *ap_parameter);

/* 发送数据的函数 */
uint8_t WIFI_AP_SendTCPClient(uint8_t id, char* data);
uint8_t WIFI_USART_Send_String(uint8_t *str);
uint8_t WIFI_Send_Cmd(char *cmd,char *ack, uint16_t waitms, uint8_t cnt);

/* 获取WIFI模块连接数量的函数 */
uint8_t WIFI_GetAPLinkNum();
uint8_t WIFI_GetTCPLinkNum();

/* 设置WIFI模块属性的函数 */
uint8_t WIFI_RESET();
uint8_t WIFI_Test();
uint8_t WIFI_SetPassword(char *ssid, char *password);
uint8_t WIFI_SetMode(WIFI_Mode_t mode);

/* 启动TCP服务 */
uint8_t WIFI_StartTCP(uint16_t WIFIPort);

/* 清空接收缓冲区 */
void WIFI_Clear_RX_BUFF(void);
uint8_t WIFI_ClearRxFlag();

/* 获取接收标志和长度 */
uint8_t WIFI_GetRxFlag();
uint32_t WIFI_GetRxLen();

/* 读取WIFI接收数据缓冲区 */
uint8_t WIFI_RX_BUFF_Read(uint8_t *buff, uint16_t len);

#endif // !__ESP01S_H

onenet.c

#include "onenet.h"
#include "stdio.h"
#include "string.h"
#include "Delay.h"
#include "cJSON.h"
#include "led.h" 

/* AT指令下WIFI连接信息 */
char wifi_info[] = "AT+CWJAP=\"" WIFISSID "\",\"" WIFIPASS "\"\r\n";
char onenet_info[] = "AT+MQTTCONN=0,\"mqtts.heclouds.com\",1883,0\r\n";
/* AT+MQTTCONN=<LinkID>,<host>,<port>,<reconnect> 
    <LinkID>: 连接ID,取值范围0~4,当前模块仅支持0
    <host>: MQTT服务器地址,详见OneNet平台文档,当前为mqtts.heclouds.com
    <port>: MQTT服务器端口号,详见OneNet平台文档,一般为1883
    <reconnect>: 是否自动重连,0-否,1-是
*/

/**
 * @brief 连接OneNet平台的函数
 * 
 * @return int8_t 返回1 ---- 连接成功,其他均为错误码
 */
int8_t ONENET_Connect(void)
{
    int8_t res = 0;
    char AT_CMD[250] = {0};

    // 测试指令AT
    WIFI_Send_Cmd("AT\r\n", "OK", 100, 3);

    // 连接热点
    // printf("正在连接热点%s\r\n%s\r\n", WIFISSID, WIFIPASS);
    sprintf(AT_CMD, "AT+CWJAP=\"%s\",\"%s\"\r\n", WIFISSID, WIFIPASS);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 3);
    if (res == 0) {
        res = 12;
        goto exit_init;
    }
    Delay_ms(1000);
    memset(AT_CMD, 0, sizeof(AT_CMD));// 清0数组,备用

    // 设置DHCP
    // printf("正在设置DHCP\r\n");
    // res = WIFI_Send_Cmd("AT+CWDHCP=1,1\r\n", "OK", 500, 3);
    // if (res == 0) {
    //     res = 13;
    //     goto exit_init;
    // }

    // 配置MQTT连接
    sprintf(AT_CMD, "AT+MQTTUSERCFG=0,1,\"%s\",\"%s\",\"%s\",0,0,\"\"\r\n", ONENET_DEVID, ONENET_PROID, ONENET_AUTH_INFO);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);
    if (res == 0) {
        res = 15;
        goto exit_init;
    }
    memset(AT_CMD, 0, sizeof(AT_CMD));// 清0数组,备用
    Delay_ms(1000);

    // 连接到MQTT平台
    printf("正在连接物联平台: %s\r\n", onenet_info);
    WIFI_Send_Cmd(onenet_info, "OK", 5000, 5);  // 连接到OneNet服务器
    Delay_ms(1000);
    if (res == 0) {
        res = 16;
        goto exit_init;
    }

    // 订阅云平台回应主题:能够在每次发布数据后如果出错,云平台将回应错误原因,便于调试。
    memset(AT_CMD, 0, sizeof(AT_CMD));
    sprintf(AT_CMD, "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/post/reply\",0\r\n", ONENET_PROID, ONENET_DEVID);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);
    if (res == 0) {
        res = 17;
        goto exit_init;
    }
    Delay_ms(1000);

    // 订阅主题:能够在手机APP上修改设备属性,例如修改LED开关状态
    memset(AT_CMD, 0, sizeof(AT_CMD));
    sprintf(AT_CMD, "AT+MQTTSUB=0,\"$sys/%s/%s/thing/property/set\",0\r\n", ONENET_PROID, ONENET_DEVID);
    res = WIFI_Send_Cmd(AT_CMD, "OK", 5000, 5);
    if (res == 0) {
        res = 18;
        goto exit_init;
    }

exit_init:
    printf("ESP8266 Init OK: %d\r\n", res);

    return res;
}

/**
 * @brief 向OneNet平台发布数据的函数
 * 
 * @param temp 要发送的温度数据
 * @param humi 要发送的湿度数据
 * @return int8_t 为1表示发布成功,其他为错误码
 */
int8_t Publish_DHT11_Data(float temp, float humi)
{
    int8_t res = 0;
    char cmd[512];

    // 发布格式:AT+MQTTPUB=<link_ID>,<"topic">,<"data">,<QoS>,<retain>
    // 实际发送的例如:AT+MQTTPUB=0,"$sys/br7KEdD3U4/temp_hum/thing/property/post","{\"id\":\"13\"\,\"params\":{\"hum\":{\"value\":70}\,\"temp\":{\"value\":27.0}}}",0,0
    // 发现一个坑,逗号也需要转义,使用\,
    sprintf(cmd, "AT+MQTTPUB=0,\"$sys/%s/%s/thing/property/post\",\"{\\\"id\\\":\\\"12\\\"\\,\\\"params\\\":{\\\"temp\\\":{\\\"value\\\":%.1f}\\,\\\"hum\\\":{\\\"value\\\":%.1f}}}\",0,0\r\n",
        ONENET_PROID, ONENET_DEVID, temp, humi);
    res = WIFI_Send_Cmd(cmd, "OK", 2000, 5);

    return res;
}

/**
 * @brief 向OneNet平台发布数据的函数,适用于发布任意主题和数据,参数为字符串形式
 * 
 * @param topic 主题
 * @param json_data JSON格式的数据
 * @return int8_t 为1表示发布成功,其他为错误码
 */
int8_t Publish_Data(char *topic, char *json_data)
{
    int8_t res = 0;
    char cmd[512];

    // 发布格式:AT+MQTTPUB=<link_ID>,<"topic">,<"data">,<QoS>,<retain>
    sprintf(cmd, "AT+MQTTPUB=0,\"%s\",\"%s\",0,0\r\n", topic, json_data);

    res = WIFI_Send_Cmd(cmd, "OK", 2000, 5);
    return res;
}

/**
 * @brief 处理来自云端的消息
 * 
 * @return int8_t 为1表示成功处理消息,0表示没有新消息,其他为错误码
 */
int8_t MCU_ProcessMsgFromCloud(void)
{
    int8_t ret = (int8_t)WIFI_GetRxFlag();
    uint32_t WIFI_RX_LEN = WIFI_GetRxLen();

    // 如果没有接收到数据,直接返回0
    if (ret == 0)   return 0;
    else            WIFI_ClearRxFlag();

    // 定义一个指向接收缓冲区的字符串指针,防止直接操作全局数组导致混乱
    char *buf = (char*)WIFI_RX_BUFF;

    // 检查是否是订阅主题的消息:
    // 收到"+MQTTSUBRECV:0,"开头的消息,表示有设备向订阅的主题发送了数据
    if (strstr(buf, "+MQTTSUBRECV:0,\"") == NULL) {
        WIFI_Clear_RX_BUFF();
        ret = 11;
        return ret;
    }

    // 定位JSON起始位置(第一个 '{')
    char *json_str = strchr(buf, '{');
    if (json_str == NULL) {
        WIFI_Clear_RX_BUFF();
        ret = 12;
        return ret;
    }

    // 解析JSON
    cJSON *root = cJSON_Parse(json_str);
    if (root == NULL) {
        const char *error_ptr = cJSON_GetErrorPtr();
        if (error_ptr != NULL)
            printf("JSON parse error: %s\n", error_ptr);
        WIFI_Clear_RX_BUFF();
        ret = 13;
        return ret;
    }

    // 提取 params 对象
    cJSON *params = cJSON_GetObjectItem(root, "params");
    if (params == NULL) {
        // printf("No params object\n");
        cJSON_Delete(root);
        WIFI_Clear_RX_BUFF();
        ret = 14;
        return ret;
    }

    // 提取 LED 属性(布尔值)
    cJSON *led = cJSON_GetObjectItem(params, "LED");
    if (led != NULL && cJSON_IsBool(led)) {
        if (cJSON_IsTrue(led)) {
            LED_B_ON;
            printf("LED ON\n");
        } else {
            LED_B_OFF;
            printf("LED OFF\n");
        }
    } else {
        printf("LED property not found or not bool\n");
    }

    // 释放cJSON对象
    cJSON_Delete(root);
    WIFI_Clear_RX_BUFF();
    ret = 1; // 成功处理消息
    return ret;
}

onenet.h

#ifndef __ONENET_H
#define __ONENET_H

#include "stm32f10x.h"
#include "esp8266.h"

/* MQTT连接信息:产品ID、设备ID、鉴权信息(产品密钥) */
#define ONENET_PROID		    "br7KEdD3U4"    // 产品ID
#define ONENET_DEVID		    "temp_hum"      // 设备名称
#define ONENET_AUTH_INFO	    "version=2018-10-31&res=products%2Fbr7KEdD3U4%2Fdevices%2Ftemp_hum&et=2058245361&method=md5&sign=%2FykqYKB6fsRkyHBt7Osdtw%3D%3D"  //鉴权信息token

/************************** 相关函数 **************************/
/* 连接OneNet平台的函数 */
int8_t ONENET_Connect(void);

/* 向OneNet平台发布数据的函数 */
int8_t Publish_DHT11_Data(float temp, float humi);
int8_t Publish_Data(char *topic, char *json_data);

/* 处理来自云平台的消息的函数 */
int8_t MCU_ProcessMsgFromCloud(void);

#endif

main.c

#include "stm32f10x.h"
#include "Delay.h"
#include "led.h" 
#include "dht11.h"
#include "OLED.h"
#include "usart.h"
#include "esp8266.h"
#include "onenet.h"

float temp = 0, humi = 0;
uint8_t state = 0;

int main()
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    LED_Init();
    OLED_Init();
    DHT11_Init();
    USART_Config(115200);
    WIFI_ESP8266_Init(WIFI_MODE_STA);
    ONENET_Connect();

    OLED_ShowString(1, 1, "Temp:");
    OLED_ShowString(2, 1, "Humi:");

    DHT11_ReadData(&temp, &humi);//数据获取
    Publish_DHT11_Data(temp, humi);
    OLED_ShowNum(1, 6, temp, 2); //显示温度
    OLED_ShowNum(2, 6, humi, 2); //显示湿度

    while (1)
    {
        DHT11_ReadData(&temp, &humi);//数据获取
        // OLED_ShowNum(3, 1, Publish_DHT11_Data(temp, humi), 1);
        Publish_DHT11_Data(temp, humi);
        Delay_ms(1000);
        OLED_ShowNum(1, 6, temp, 2); //显示温度
        OLED_ShowNum(2, 6, humi, 2); //显示湿度
        MCU_ProcessMsgFromCloud();

        // printf("Temp:%.2fC  Humi:%.2f%%\n", temp, humi);
    }
}

3. APP制作

        学习嵌入式感觉没有必要去深入学习前后端开发的知识,所以我选择用微信小程序来控制小灯,只涉及一些简单的html、css、JavaScript语法。页面用html和css来制作,数据传输显示的逻辑部分用JavaScript来编写,主要是从云平台读取温湿度数据和下发指令到云平台,云平台再发布指令到单片机设备。

3.1 工具材料下载

3.1.1 微信开发者工具:下载地址

        进入小程序开发工具,点击中间加号创建

        填信息,选择测试号,会自动生成AppID,选JS基础模板。

        左侧文件目录包括pages文件夹,app.js文件等等。其中pages文件夹下包含两个页面,index和logs,我们准备用index页面来显示温湿度数据,不用logs页面(也可以用logs来显示个人主页)。右侧会显示一个登陆页面,是js基础模板自带的,页面显示的代码在index.wxml文件,可以删掉全部内容。

3.1.2 图标下载:阿里巴巴矢量图标库

        准备制作类似于下图所示的页面。制作页面要下载一些图片素材,包括数据主页、个人主页、温度、湿度和小灯的图标。其中数据主页和个人主页都要分别下载黑色的和高亮的图标。我们要实现的功能是当前显示的页面如果是数据主页的话,数据主页的图标应该高亮显示。所以要分别准备两种颜色图标。

        在阿里巴巴图标矢量库内搜索一个 home,作为主页图标

        随便下载一个

        调一下颜色,再下载一个作为高亮显示图标

        搜user,个人主页,分别下载灰色的图标,和激活后蓝色的图标

        依此类推,继续下载湿度、温度、LED灯的图标即可。

3.2 UI界面绘制

3.2.1 绘制底部菜单栏

        在app.json下添加以下内容,这是小程序的全局配置文件,用来设置一些基础信息。

{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "window": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "环境监测",
    "navigationBarBackgroundColor": "#ffffff"
  },
  "style": "v2",
  "componentFramework": "glass-easel",
  "sitemapLocation": "sitemap.json",
  "lazyCodeLoading": "requiredComponents",
  "tabBar": {
    "list": [{
      "pagePath": "pages/index/index",
      "text": "",
      "iconPath": "static/home.png",
      "selectedIconPath": "static/home_light.png"
    },
    {
      "pagePath": "pages/logs/logs",
      "text": "",
      "iconPath": "static/user.png",
      "selectedIconPath": "static/user_light.png"
    }]
  }
}

        pages段指定数据主页和个人主页两个页面在工程文件夹中的路径,不用加文件后缀,会自动识别。

  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],

        window指定窗口的导航栏,文本颜色为黑色,小程序名称为“环境监测”,背景颜色为白色。


  "window": {
    "navigationBarTextStyle": "black",
    "navigationBarTitleText": "环境监测",
    "navigationBarBackgroundColor": "#ffffff"
  },

        tabBar指定底部导航栏,其下的list列表用于指定两个页面的图标路径。

  "tabBar": {
    "list": [{
      "pagePath": "pages/index/index",
      "text": "",
      "iconPath": "static/home.png",
      "selectedIconPath": "static/home_light.png"
    },
    {
      "pagePath": "pages/logs/logs",
      "text": "",
      "iconPath": "static/user.png",
      "selectedIconPath": "static/user_light.png"
    }]
  }

        没点击图标之前显示iconPath路径上的图标,图标是黑色的;点击之后会变成selectedIconPath路径上的图标,图标会变蓝色。

        保存编译,可以看到一个基础界面。

3.2.2 绘制顶部天气栏

         在index.wxss下创建一个page-container类选择器,设置上下左右内边距为40个像素。当页面使用这个类时。显示的页面与屏幕边界距离40个像素。

.page-container {
  padding: 40rpx;
}


/* 页面顶部 */
.header {
  background-color:rgb(48, 152, 207);
  color: antiquewhite;
  border-radius: 50rpx;
  padding: 32rpx 64rpx;
}

.header .header-air {
  display: flex;
  justify-content: space-between;
  font-size: 30rpx;
}

.header .header-title {
  display: flex;
  justify-content: space-between;
  font-size: 100rpx;
}

.header .header-ad {
  font-size: 24rpx;
  margin-top: 24rpx;
}

         index.wxml文件上显示的内容是我们能够看见的,我们先在上面添加几个字,再调整wxss文件中的参数,使页面美观。

<!--index.wxml-->
<view class="page-container">
  <view class="header">
    <view class="header-air">
      <view>
        空气质量 - 优
      </view>
      <view>
        海南 - 海口
      </view>
    </view>
    <view class="header-title">
      <view>
        38
      </view>
    </view>
    <view class="header-ad">
      <view>
        今天天气针不戳
      </view>
    </view>
  </view>
</view>

        保存编译,就可以看到天气栏。

3.2.3 绘制数据卡片

        在index.wxss内添加

/* 数据卡片 */
.data {
  margin-top: 60rpx;
  display: grid;
  justify-content: center;
  grid-template-columns: repeat(auto-fill, 300rpx);
  grid-gap: 50rpx;
}
.data .data-card {
  background-color: white;
  height: 120;
  box-shadow: rgb(13, 12, 12) 0 0 8rpx;
  border-radius: 48rpx;
  display: flex;
  padding: 40rpx;
  justify-content: space-between;
}

.data .data-card .data-card-icon {
  height: 100rpx;
  width: 100rpx;
}

.data .data-card .data-card-title {
  font-size: 36rpx;
}

.data .data-card .data-card-value {
  font-size: 40rpx;
  font-weight: bold;
}

        在index.wxml下添加

  <view class="data">
    <!-- 温度框 -->
    <view class="data-card">
      <image class="data-card-icon" src="/static/temp.png" />
      <view>
        <view class="data-card-title">
          温度
        </view>
        <view class="data-card-value">
          0 ℃
        </view>
      </view>
    </view>
    <!-- 湿度框 -->
    <view class="data-card">
      <image class="data-card-icon" src="/static/hum.png" />
      <view>
        <view class="data-card-title">
          湿度
        </view>
        <view class="data-card-value">
          60 %
        </view>
      </view>
    </view>
    <!-- 小灯框 -->
    <view class="data-card">
      <image class="data-card-icon" src="/static/led.png" />
      <view>
        <view class="data-card-title">
          小灯
        </view>
        <view class="data-card-value">
          <switch checked="" bindchange="" />
        </view>
      </view>
    </view>
  </view>

        完成UI界面,但还没与真实的物理设备连接,显示的温湿度数据都是固定的

3.3 连接MQTT服务端

        数据部分涉及简单的JavaScript知识,在index.js文件下实现连接MQTT代理的逻辑。

// index.js
const apiBaseUrl = 'https://iot-api.heclouds.com'
const productId = 'br7KEdD3U4'
const deviceName = 'temp_hum'
const authInfo = 'version=2018-10-31&res=products%2Fbr7KEdD3U4%2Fdevices%2Ftemp_hum&et=2058245361&method=md5&sign=%2FykqYKB6fsRkyHBt7Osdtw%3D%3D'

Page({
  data: {
    client: null,
    temp: 0,
    hum: 0,
    LED: false,
    onenetData: [],       // 用来存储设备属性值的数组  
  },

  onLoad(options) {  
    // 获取设备数据和状态  
    this.fetchOnenetData();  
    // 使用setInterval时,确保this指向正确,并定时更新设备状态和数据  
    setInterval(() => {
      this.fetchOnenetData();
    }, 1000); // 每秒更新一次
  },

  onLEDChange(event) {
    const that = this;
    const sw = event.detail.value; // true/false
    console.log('开关状态变化:', sw);

    // 改变界面上的开关状态
    that.setData({ LED: sw });
    // 构造下发指令的请求
    wx.request({
      url: 'https://iot-api.heclouds.com/thingmodel/set-device-property',
      method: 'POST',
      header: {
        'Authorization': authInfo,
        'Content-Type': 'application/json'
      },
      data: {
        product_id: productId,
        device_name: deviceName,
        params: {
          LED: sw
        }
      },
      success(res) {
        console.log('下发指令响应:', res.data);
        if (res.data && res.data.code === 0) {
          wx.showToast({
            title: sw ? '开灯成功' : '关灯成功',
            icon: 'success'
          });
        } else {
          wx.showToast({
            title: '操作失败',
            icon: 'none'
          });
          // 失败时回滚开关状态
          that.setData({ LED: !sw });
        }
      },
      fail(err) {
        console.error('下发指令失败', err);
        wx.showToast({
          title: '网络错误',
          icon: 'none'
        });
        // 失败时回滚开关状态
        that.setData({ LED: !sw });
      }
    });
  },
  fetchOnenetData() {  
    wx.request({  
      url: `${apiBaseUrl}/thingmodel/query-device-property?product_id=${productId}&device_name=${deviceName}`,
      method: "GET",
      header: {
        'Authorization': authInfo
      },
      success: (res) => {
        // 1. 判断接口调用是否成功
        if (res.data.code !== 0) {
          console.log('API返回错误:', res.data.msg);
          return;
        }
        // 2. 获取属性列表数组
        const properties = res.data.data || [];
        // 3. 遍历属性,提取需要的值
        let temp = 0, hum = 0, led = false;
        properties.forEach(item => {
          const id = item.identifier;
          const value = item.value;  // value 是字符串需要转为数据

          if (id === 'temp') {
            temp = parseFloat(value);  // 转换为数字
          } else if (id === 'hum') {
            hum = parseFloat(value);
          } else if (id === 'LED') {
            if (value === 'true' || value === '1') {
              led = true;
            } else {
              led = false;
            }
          }
        });
  
        // 4. 更新页面数据
        this.setData({
          temp: temp,
          hum: hum,
          // LED: led,
          onenetData: res.data   // 保留原始数据备用
        });
        // console.log('解析后温度:', temp, '湿度:', hum, 'LED:', led);
      },
      fail: (err) => {  
        console.log("OneNET数据请求失败");  
        console.error(err);
      }
    });
  },
})

        这里指定的是产品ID、设备ID、token工具生成的内容。其中apiBaseUrl 是由官方文档决定的,详见官方文档。目前链接的是我这个,官方会不断更新。

const apiBaseUrl = 'https://iot-api.heclouds.com'
const productId = 'br7KEdD3U4'
const deviceName = 'temp_hum'
const authInfo = 'version=2018-10-31&res=products%2Fbr7KEdD3U4%2Fdevices%2Ftemp_hum&et=2058245361&method=md5&sign=%2FykqYKB6fsRkyHBt7Osdtw%3D%3D'

        这里初始化一些变量,温湿度和小灯状态是我们所必需的。

  data: {
    client: null,
    temp: 0,
    hum: 0,
    LED: false,
    onenetData: [],       // 用来存储设备属性值的数组  
  },

        onenetData用于保存从来字云平台发布的数据。官方文档有描述,发布的数据包含list列表,列表内容包含各个属性的信息。小程序读取到数据之后我们要进行解析。

        onLoad函数在每次页面加载时都会执行,每秒钟执行一次fetchOnenetData函数,在函数内读取数据并解析。

  onLoad(options) {  
    // 获取设备数据和状态  
    this.fetchOnenetData();  
    // 使用setInterval时,确保this指向正确,并定时更新设备状态和数据  
    setInterval(() => {
      this.fetchOnenetData();
    }, 1000); // 每秒更新一次
  },

        这个函数首先会从官方提供的API接口,获取数据;从数据列表中找到温湿度数据并加载到变量temp、hum、LED中。

  fetchOnenetData() {  
    wx.request({  
      url: `${apiBaseUrl}/thingmodel/query-device-property?product_id=${productId}&device_name=${deviceName}`,
      method: "GET",
      header: {
        'Authorization': authInfo
      },
      success: (res) => {
        // 1. 判断接口调用是否成功
        if (res.data.code !== 0) {
          console.log('API返回错误:', res.data.msg);
          return;
        }
        // 2. 获取属性列表数组
        const properties = res.data.data || [];
        // 3. 遍历属性,提取需要的值
        let temp = 0, hum = 0, led = false;
        properties.forEach(item => {
          const id = item.identifier;
          const value = item.value;  // value 是字符串需要转为数据

          if (id === 'temp') {
            temp = parseFloat(value);  // 转换为数字
          } else if (id === 'hum') {
            hum = parseFloat(value);
          } else if (id === 'LED') {
            if (value === 'true' || value === '1') {
              led = true;
            } else {
              led = false;
            }
          }
        });
  
        // 4. 更新页面数据
        this.setData({
          temp: temp,
          hum: hum,
          // LED: led,
          onenetData: res.data   // 保留原始数据备用
        });
        // console.log('解析后温度:', temp, '湿度:', hum, 'LED:', led);
      },
      fail: (err) => {  
        console.log("OneNET数据请求失败");  
        console.error(err);
      }
    });
  },

        这三个变量将用来显示到页面中。回到文件index.wxml,把原先固定的温湿度数值修改成这三个变量。

        其中LED处的代码要稍微修改,修改switch语句,页面会不断检查LED变量来决定开关左滑还是右滑,当用户点击开关发生变化时,会调用onLEDChange函数。

        在onLEDChange下执行从小程序发布数据到云平台,发布点灯/关灯指令。

  onLEDChange(event) {
    const that = this;
    const sw = event.detail.value; // true/false
    console.log('开关状态变化:', sw);

    // 改变界面上的开关状态
    that.setData({ LED: sw });
    // 构造下发指令的请求
    wx.request({
      url: 'https://iot-api.heclouds.com/thingmodel/set-device-property',
      method: 'POST',
      header: {
        'Authorization': authInfo,
        'Content-Type': 'application/json'
      },
      data: {
        product_id: productId,
        device_name: deviceName,
        params: {
          LED: sw
        }
      },
      success(res) {
        console.log('下发指令响应:', res.data);
        if (res.data && res.data.code === 0) {
          wx.showToast({
            title: sw ? '开灯成功' : '关灯成功',
            icon: 'success'
          });
        } else {
          wx.showToast({
            title: '操作失败',
            icon: 'none'
          });
          // 失败时回滚开关状态
          that.setData({ LED: !sw });
        }
      },
      fail(err) {
        console.error('下发指令失败', err);
        wx.showToast({
          title: '网络错误',
          icon: 'none'
        });
        // 失败时回滚开关状态
        that.setData({ LED: !sw });
      }
    });
  },

        使用官方提供的APi接口来发送数据,同时单片机设备端需要做出回应,否则会返回响应超时。

        所以单片机端代码要添加响应函数,响应格式必须是json格式

/**
 * @brief 回复属性设置结果
 * @param id 原消息中的id(可为NULL)
 */
void Reply_PropertySet(char *id)
{
    char topic[128];
    char payload[128];
    char cmd[512];

    sprintf(topic, "$sys/%s/%s/thing/property/set_reply", ONENET_PROID, ONENET_DEVID);

    if (id != NULL) {
        sprintf(payload, "{\\\"id\\\":\\\"%s\\\"\\,\\\"code\\\":200\\,\\\"msg\\\":\\\"success\\\"}", id);
    } else {
        sprintf(payload, "{\\\"code\\\":200\\,\\\"msg\\\":\\\"success\\\"}");
    }

    sprintf(cmd, "AT+MQTTPUB=0,\"%s\",\"%s\",0,0\r\n", topic, payload);
    WIFI_Send_Cmd(cmd, "OK", 2000, 5);
}

        同为以下内容,但是设备端响应时id必须与接收云平台下发数据时的id一致。

        所以在解析数据时要解析id号,要在MCU_ProcessMsgFromCloud函数内添加:

    // 提取 id(用于回复)
    cJSON *id_item = cJSON_GetObjectItem(root, "id");
    char *id_str = NULL;
    if (cJSON_IsString(id_item) && id_item->valuestring != NULL)
        id_str = id_item->valuestring;

        添加到图中所示位置:

        接下来要修改主循环的逻辑了,在原先的代码中引入定时器,每2s发送一次温湿度数据到云平台,在主循环中轮询检查接收到的点灯指令并解析。防止发布数据和接收数据产生冲突,因为共用一个串口,每次发送完后还要等待响应。

#include "stm32f10x.h"
#include "Delay.h"
#include "led.h" 
#include "dht11.h"
#include "OLED.h"
#include "usart.h"
#include "esp8266.h"
#include "onenet.h"
#include "tim.h"

float temp = 0, humi = 0;
uint8_t state = 0, onenetLinkFlag = 0;

int main()
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    LED_Init();
    OLED_Init();
    DHT11_Init();
    USART_Config(115200);
    WIFI_ESP8266_Init(WIFI_MODE_STA);
    onenetLinkFlag = ONENET_Connect();

    OLED_ShowString(1, 1, "Temp:");
    OLED_ShowString(2, 1, "Humi:");
    OLED_ShowString(3, 1, "Link:");
    if (onenetLinkFlag == 1) OLED_ShowString(3, 6, "OK");
    else OLED_ShowString(3, 6, "NO");
    Timer_Init();

    while (1)
    {
        MCU_ProcessMsgFromCloud();
        if (state == 1) {
            DHT11_ReadData(&temp, &humi);//数据获取
            Publish_DHT11_Data(temp, humi);
            OLED_ShowNum(1, 6, temp, 2); //显示温度
            OLED_ShowNum(2, 6, humi, 2); //显示湿度
            state = 0;
        }
        // printf("Temp:%.2fC  Humi:%.2f%%\n", temp, humi);
    }
}

void TIM_CallBack()
{
    /* 2s发送一次数据 */
    state = 1;
}

3.4 调试现象

        现在观看效果:上电,开热点,OLED显示OK则表示连接MQTT成功。

        小程序端能看到温湿度在变化,尝试打开小灯,控制台会打印日志,打印来自设备的响应信息,表示成功。

        板子上的蓝灯也亮了

        试试关灯,返回结果也表示成功。

3.5 发布小程序

        测试号不能发布,如果要发布小程序的话需要注册一个新账号,还要备案、身份认证,所以我只发布体验版的小程序,首先要进入小程序注册页面注册一个账号:地址:

        注册完成后复制小程序appID

        回到微信开发工具点击详情页面,修改appID号,把复制的新ID粘贴进去

        点击右上角上传就可以发布小程序了。

        指定发布版本

        发布完成,可以回到小程序网页

        把刚刚发布的版本修改成体验版就可以通过二维码发送给身边的朋友使用了。也可以不使用体验版,直接点击提交审核,然后认证信息就行了。

4. 问题总结

        学习这个远程点灯的过程中遇到了很多问题。笔者经常忘记STM32给串口用的DMA通道是固定的,在手册中指定的通道才能使用,因为这个问题导致我用串口空闲中断的时候没接收到数据。其次就是在发送AT指令的时候电源供电问题,WIFI模块要单独供电,和开发板隔离开,不然老是电源不稳自动复位。发送指令的时候,忘记引号要加反斜杠,令我不解的是逗号也要加反斜杠,这个问题之前一直困扰我,问deepseek,他也没发觉。直到我拿出串口转TTL,直接串口助手发AT指令给WiFi模块,不同的指令一条条尝试才发觉这个问题。

4.1 数据包粘包问题

        使用串口DMA不定长接收缓冲区的时候,每次向WIFI模块发送AT指令都会收到的回复,模块回复的时候如果出现网络不稳或者其他因素可能导致接收的回复结果被拆分掉,一次长长的数据被DMA分两次搬运,本次接收信息的尾部数据与下次接收到的信息黏在一起。设备没有等到WIFI模块的响应结果。解决方法之一就是在超时时间内持续接收数据,拼接成完整字符串后再查找期望应答,可以用滴答定时器来记录时间。第二个方法就是重复发送多次指令,每发送完一次都检查一下缓冲区有没有应答结果。第一种方法好点吧,我用的第二种。

4.2 设备响应超时问题

        每次云平台下发数据到设备时设备都需要返回一个响应消息,设备不反悔消息的话云平台会返回响应超时错误。下发点灯指令的时候,如果设备正在采集温湿度,或者正在向平台发送数据,就会造成冲突。所以我引入定时器,每2秒钟读取一下温湿度再上传,在主循环中循环等待下发的点灯指令。但不是最佳方法,也会有几率发生冲突。

源码地址:通过网盘分享的文件:STM32IOT.rar
链接: https://pan.baidu.com/s/1bDg3mkO3ZCqF3fiu9LUkJg?pwd=zyx4 提取码: zyx4

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐