实验硬件:STM32F103ZET6;0.96寸OLED(128×64);ESP8266,DHT11;CS创世 SD NAND;LED;KEY

        硬件实物图:


效果图:

       引脚连接:

OLED模块引脚:

VCC --> 3.3V

GND --> GND

SCL --> PB10

SDA --> PB11

ESP8266模块引脚:

VCC --> 3.3V

GND --> GND

RX--> PB10

TX --> PB11

RST --> PB9

EN --> PB7

DHT11传感器引脚:

VCC --> 3.3V

GND --> GND

DATA-->PE0

一、物联网
1.1 物联网简介
       物联网(Internet of Things,简称IoT)是指通过各种信息传感器、射频识别技术、全球定位系统、红外感应器、激光扫描器等各种装置与技术,实时采集任何需要监控、 连接、互动的物体或过程,采集其声、光、热、电、力学、化学、生物、位置等各种需要的信息,通过各类可能的网络接入,实现物与物、物与人的泛在连接,实现对物品和过程的智能化感知、识别和管理。物联网是一个基于互联网、传统电信网等的信息承载体,它让所有能够被独立寻址的普通物理对象形成互联互通的网络 。

        总而言之,物联网就是利用现代互联网技术实现端对端的数据互联与控制。
1.2 物联网开发
        目前,物联网开发的形式是多种多样的。总的来说,一般都需借助特定的网络服务平台为基础实现数据的上传与下发(如果只考虑内网是可以不需要的,比如ESP32CAM)。

        实力雄厚或者有一定背景的公司通常考虑可能自建网络协议服务器,专属服务自家的物联网产品开发。当然,也有不少企业会选择借助他人网络服务平台去实现自家的物联网开发。

        这里比较著名的网络服务平台有:中国移动旗下的OneNet、阿里巴巴旗下阿里云以及机智云平台。

平台分析:

机智云:机智云作为物联网开发服务平台的元老,一直致力于完善和搭建快速高效的服务机制,其有一套自己快速开发适配的物联网实现流程。但是,在笔者使用的过程中也存在着一些弊端。比如:

        (1)其需要给ESP8266等WIFI模块刷上自家的固件才可使用;

        (2)状态极其不稳定,很容易断联或者死活连不上;

        (3)受限于开发模式,对于产品自我开发有一定限制;

OneNet和阿里云平台:这2大平台背靠强大的资源和技术支持,其服务稳定。设定的开发框架也更多样化,可以提供开发者更多的发挥空间。

OneNet服务平台:
阿里云物联网平台:
二、OneNet平台使用
        从多元化和产品稳定性方面考虑,作者将以中国移动旗下的OneNet服务平台为案例进行教学讲解。(其实本来打算以机智云出一篇案例的,结果后来发现之前能正常联动的MCU和APP动不动就宕机。后来,索性直接就以OneNet这个框架更开放的平台为案例教学)

2.1 OneNet准备
1、注册OneNet平台账号(网址:OneNET - 中国移动物联网开放平台 (10086.cn));
2、 登入后选择控制台,进入后点击全部产品服务,选择多协议接入;(我们使用MQTT,既可以上传数据也可以下发数据控制,而且都是免费的)
3、选择MQTT(旧版)之后添加产品,按照自己实际需求填写产品内容;
4、点击所创建的产品,添加几个设备(免费版用户上限10个设备)
5、注意设备ID,鉴权信息以及接入方式这3个属性;
6、关于数据流模块可以设置,可以不设置,反正最后通讯正常的情况下会收到需要的数据流;

2.2 OneNet调试
在设置好OneNet平台设备后,其实可以借助该平台自带的API调试工具进行调试检测(前提:下位机已经成果接通了)。

这里的调试使用API函数的介绍和使用可以参考文档中心(一个合格的嵌入式工程师是一定需要学会自己去查看技术支持文档,而且OneNet提供的文档内容还是非常详尽的)。

OneNet技术文档网址:OneNET - 中国移动物联网开放平台 (10086.cn)

网络协议通讯关键函数

服务器或上位机查询读取设备历史数据:

API函数:

请求方式:GET

URL:

http://api.heclouds.com/devices/device_id/datapoints

服务器或上位机下发主题报文(控制下位机):

API函数:

请求方式:POST

URL: http://api.heclouds.com/mqtt?topic=xxx

        以上2个网络通讯的API函数至关重要,就是实现常规情况下OneNet物联网开发的关键性技术支持。(情况允许的条件下,建议读者朋友们去好好研读一下技术文档,将会为之后的开发大大助力)


三、下位机外设驱动
3.1 ESP8266模块
        作者采用的ESP8266模块为ESP8266NodeMCU,是需要进行烧入AT固件,才能实现目标网络通讯。作为常见的物联网开发模块,ESP8266的出现大大降低了物联网开发的难度系数,也普及了物联网的发展。

        AT指令最早在蓝牙模块上接触过,所谓AT指令实质上就是一些起控制作用的特殊字符串。模块可以通过AT指令控制搭配使用源代码API函数开发,总体开发速度快,难度较低。

        不同厂商芯片的AT固件可能有所不同,但是指令基本一致(作者使用的是乐鑫的)。

说明:由于篇幅有限,这里就不和大家单独详细介绍AT指令。指令的详细参数及使用说明请参考官方文档:ESP8266 AT指令集。
3.2 OLED模块
        本项目中0.96寸OLED模块的使用仅为显示DHT11传感器采集到的温湿度信息,以此来对比是否和服务器端以及上位机APP端的数据一致性。对其使用有不是太了解的读者朋友可以参考,作者另一篇基础教学博客:(2条消息) 【强烈推荐】基于stm32的OLED各种显示实现(含动态图)_混分巨兽龙某某的博客-CSDN博客_oled显示图片程序

        本项目的代码都是基于作者以前基础教学上的项目代码搭建而成,保证读者朋友可以实现快速复现。

3.3 DHT11模块
        本项目中DHT11为下位机MCU采集周围环境温度和湿度的传感器,当然,条件允许的情况下还可以附加很多环境传感器(比如:烟雾传感器,环境光传感器,二氧化碳传感器等等)。当然得益于OneNet平台的布局,本项目教学的底层逻辑支持读者朋友的自我DIY,实现自主化的物联网产品设计。

        DHT11模块驱动参考博客:基于stm32的太空人温湿度时钟项目——DHT11(HAL库)_混分巨兽龙某某的博客-CSDN博客

3.4 KEY和LED
        KEY和LED都是源于作者正点原子精英版开发板上自备的(如果和作者同款开发板移植开发将会特别简单快速),属于最基本的GPIO操作相信各位应该都是掌握的

特别注意:

        (1)这里的KEY按键从设计逻辑上就可以看出应该是需要采用外部中断的;

        (2)KEY按下之后会改变LED的亮灭状态,为了同步上位机此时的LED状态,所以需要触发串口通讯中断(考虑嵌套中断情况时候中断优先级的安排)。

四、CubeMX配置
1、RCC配置外部高速晶振(精度更高)——HSE;
2、SYS配置:Debug设置成Serial Wire(否则可能导致芯片自锁);
3、TIM2配置:由上面可知DHT11的使用需要us级的延迟函数,HAL库自带只有ms的,所以需要自己设计一个定时器;
4、I2C2配置:作为OLED的通讯方式;
5、UART1和UART3配置:MCU分别与电脑和ESP8266通讯(记得开启串口通信中断);

6、设置KEY0按键PE4为外部中断(根据自己的开发板来确定)

7、GPIO配置:PE0设置为DHT11的DATA端,PE5为LED,并且设置ESP8266的EN和RST(PB7和PB9);

8、时钟树配置
五、代码与解析
5.1 OLED与DHT11模块代码
        受篇幅限制OLED与DHT11部分的代码,这里就不展示了。如果有不懂这部分原理与代码的读者朋友可以参考本人的另一篇博客。博客地址:基于stm32的太空人温湿度时钟项目——DHT11(HAL库)_混分巨兽龙某某的博客-CSDN博客

5.2 ESP8266模块代码
        ESP8266部分的代码主要是借助串口通讯AT指令与ESP8266模块(刷入AT固件的)与OneNet平台进行信息交互(包含ESP8266初始化、数据发送,指令发送和数据缓存清除等)。

esp8266.h代码:
#ifndef _ESP8266_H_
#define _ESP8266_H_

#include "main.h"
#include "usart.h"
#include<string.h>
#include<stdio.h>
#include<stdbool.h>

#define     ESP8266_WIFI_INFO                "AT+CWJAP=\"NJUST\",\"768541ly\"\r\n"          //连接上自己的wifi热点:WiFi名和密码
#define     ESP8266_ONENET_INFO                "AT+CIPSTART=\"TCP\",\"183.230.40.39\",6002\r\n" //连接上OneNet的MQTT

#define     OK                        0            //接收完成标志
#define     OUTTIME                1            //接收未完成标志



void ESP8266_Clear(void);           //清空缓存

void ESP8266_Init(void);            //esp8266初始化


_Bool ESP8266_SendCmd(char *cmd, char *res);//发送数据

unsigned char *ESP8266_GetIPD(unsigned short timeOut);
void ESP8266_SendData(unsigned char *data, unsigned short len);

#endif
esp8266.c代码:
#include "esp8266.h"

unsigned char ESP8266_Buf[128];                         //定义一个数组作为esp8266的数据缓冲区
unsigned short esp8266_cnt = 0, esp8266_cntPre = 0;     //定义两个计数值:此次和上一次
unsigned char a_esp8266_buf;

/**
  * @brief esp8266初始化
  * @param 无
  * @retval 无
  */
void ESP8266_Init(void)
{
  ESP8266_Clear();
       
        printf("1. 测试AT启动\r\n");            //AT:测试AT启动
        while(ESP8266_SendCmd("AT\r\n", "OK"))
                HAL_Delay(500);
       
        printf("2. 设置WiFi模式(CWMODE)\r\n");        //查询/设置 Wi-Fi 模式:设置WiFi模式为Station模式
        while(ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK"))
                HAL_Delay(500);
       
        printf("3. AT+CWDHCP\r\n");     //启用/禁用 DHCP
        while(ESP8266_SendCmd("AT+CWDHCP=1,1\r\n", "OK"))
                HAL_Delay(500);
       
        printf("4. 连接WiFi热点(CWJAP)\r\n");        
        while(ESP8266_SendCmd(ESP8266_WIFI_INFO, "GOT IP"))
                HAL_Delay(500);
       
        printf("5. 建立TCP连接(CIPSTART)\r\n");
        while(ESP8266_SendCmd(ESP8266_ONENET_INFO, "CONNECT"))
                HAL_Delay(500);
       
        printf("6. ESP8266 Init OK\r\n");
}   


/**
  * @brief  清空缓存
  * @param  无
  * @retval 无
  */
void ESP8266_Clear(void)
{
    memset(ESP8266_Buf, 0, sizeof(ESP8266_Buf));    //将数组中的元素全部初始化为0,
}   

/**
  * @brief  等待接收完成
  * @param  无
  * @retval OK:表示接收完成;OUTTIME:表示接收超时完成
  *         进行循环调用,检测接收是否完成
  */
_Bool ESP8266_WaitRecive(void)
{
    if(esp8266_cnt == 0)                                                         //如果当前接收计数为0 则说明没有处于接收数据中,所以直接跳出,结束函数
                return OUTTIME;
               
        if(esp8266_cnt == esp8266_cntPre)                                //如果上一次的值和这次相同,则说明接收完毕
        {
                esp8266_cnt = 0;                                                        //清0接收计数
                       
                return OK;                                                                    //返回接收完成标志
        }
        else                                            //如果不相同,则将此次赋值给上一次,并返回接收未完成标志
    {        
        esp8266_cntPre = esp8266_cnt;                               

        return OUTTIME;                                                               
    }
}

/**
  * @brief 发送命令
  * @param cmd:表示命令;res:需要检查的返回指令
  * @retval 0:表示成功;1:表示失败
  */
_Bool ESP8266_SendCmd(char *cmd, char *res)
{
       
        unsigned char timeOut = 200;

        HAL_UART_Transmit(&huart3, (unsigned char *)cmd, strlen((const char *)cmd),0xffff);

        while(timeOut--)
        {
                if(ESP8266_WaitRecive() == OK)                                                        //如果收到数据
                {
                        printf("%s",ESP8266_Buf);
                        if(strstr((const char *)ESP8266_Buf, res) != NULL)                //如果检索到关键词,清空缓存
                        {
                                ESP8266_Clear();                                                       
                               
                                return 0;
                        }
                }
                HAL_Delay(10);
        }
        return 1;
}

/**
  * @brief 数据发送
  * @param data:待发送的数据;len:待发送的数据长度
  * @retval 无
  */
void ESP8266_SendData(unsigned char *data, unsigned short len)
{

        char cmdBuf[32];
       
        ESP8266_Clear();                                                                //清空接收缓存
        sprintf(cmdBuf, "AT+CIPSEND=%d\r\n", len);                //发送命令,sprintf()函数用于将格式化的数据写入字符串
        if(!ESP8266_SendCmd(cmdBuf, ">"))                                //收到‘>’时可以发送数据
        {
                HAL_UART_Transmit(&huart3, data, len,0xffff);                //发送设备连接请求数据
        }
}

/**
  * @brief 获取平台返回的数据
  * @param 等待的时间
  * @retval 平台返回的数据,不同网络设备返回的格式不同,需要进行调试,如:ESP8266的返回格式为:"+IPD,x:yyy",x表示数据长度,yyy表示数据内容
  */
unsigned char *ESP8266_GetIPD(unsigned short timeOut)
{

        char *ptrIPD = NULL;
       
        do
        {
                if(ESP8266_WaitRecive() == OK)                                                                //如果接收完成
                {
                        ptrIPD = strstr((char *)ESP8266_Buf, "IPD,");                                //搜索“IPD”头
                        if(ptrIPD == NULL)                                                                                        //如果没找到,可能是IPD头的延迟,还是需要等待一会,但不会超过设定的时间
                        {
                                //UsartPrintf(USART_DEBUG, "\"IPD\" not found\r\n");
                        }
                        else
                        {
                                ptrIPD = strchr(ptrIPD, ':');                                                        //找到':'
                                if(ptrIPD != NULL)
                                {
                                        ptrIPD++;
                                        return (unsigned char *)(ptrIPD);
                                }
                                else
                                        return NULL;
                               
                        }
                }
               
                HAL_Delay(5);                                                                                                        //延时等待
        } while(timeOut--);
       
        return NULL;                //超时还未找到,返回空指针
}


/**
  * @brief 串口2收发中断回调函数
  * @param
  * @retval
  */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

        if(esp8266_cnt >= 255)  //溢出判断,超过一个字节
        {
                esp8266_cnt = 0;
                memset(ESP8266_Buf,0x00,sizeof(ESP8266_Buf));
                HAL_UART_Transmit(&huart3, (uint8_t *)"数据溢出", 10,0xFFFF);        

        }
        else
        {
                ESP8266_Buf[esp8266_cnt++] = a_esp8266_buf;   //接收数据转存
       
        }
       
        HAL_UART_Receive_IT(&huart3, (uint8_t *)&a_esp8266_buf, 1);   //再开启接收中断
}
代码总结:

        ESP8266模块的代码基于HAL库实现,主要是利用AT指令去使下位机(STM32+ESP8266)连接上WIFI,并且与OneNet平台进行MQTT协议通信(TCP连接IP地址和对应端口)。

特别注意:

        使用ESP8266进行通讯时,当数据量较大的时候一定要编写缓存清除代码(否则,很有可能出现死机等情况)。当然,这个时候可以搭配CS创世 SD NAND(又叫贴片式 TF卡/贴片式SD卡) 去存储传输的数据流。同时,利用这些保存在SD卡中的数据,可以在下位机制作精美的数据历史信息UI,极大的拓展了产品价值。