本项目使用的开发板是正点原子的阿波罗STM32F429开发板,是基于HAL库编写的接入中国移动物联网平台——onenet的项目,用到的通讯模块是esp8266-12F。下面是接入平台的一些步骤及讲解,如果有不懂的地方,大家一起讨论(因为我也是初学者,很多地方也理解得不够透彻,流下了知识贫乏的眼泪)。
实验前准备工作:
首先贴出一些我在本次项目所参考过的一些网址:
https://v.qq.com/x/page/i0814q78no3.html //视频教程地址
https://open.iot.10086.cn/bbs/thread-22449-1-1.html
https://open.iot.10086.cn/bbs/forum.php?mod=viewthread&tid=408&highlight=stm32F4
https://open.iot.10086.cn/develop/global/product/#/device/list?pid=217851
https://www.cnblogs.com/luxiaoguogege/p/10136996.html
https://blog.csdn.net/q361750389/article/details/79362565
https://open.iot.10086.cn/bbs/thread-35887-1-1.html
https://blog.csdn.net/qq_38410730/article/details/86538288 //esp8266与stm32通信
https://www.cnblogs.com/kinging/p/5865649.html //onenet详细介绍
感谢这些前辈的经验以及总结,让我少走了很多弯路。
本次内容包含两部分:
下面开始讲解第一部分的内容。
固件烧写
一般初次使用的esp8266-12F WIFI模块默认烧写了固件,那怎么判断需不需要烧写呢?很简单,使用USB-TTL在串口调试助手上输入AT指令就可以了,若是返回OK等信息,就说明可以直接用。我的WiFi模块快由于以前烧写过机智云的固件,因此这次重新刷了一个固件,下面把用到的几个软件提供给大家:
安信可固件(1wud)
烧写工具(rgme)
串口调试助手(hxo4)
网络调试助手(y9t4)
esp8266-12F与USB-TTL连接线如下:
VCC ----> VCC
TX ----> RX
RX ----> TX
GND ----> GND
进入烧写窗口:
写入AT指令:
依次配置以下指令:
AT
AT+CWMODE=3 //STA+AP模式
AT+RST
AT+CIFSR
AT+CWJAP=“DC”,“14785269” //这个是我的手机热点,当然要提前打开热点才能连上
这里说明一下:
ATK-ESP8266 WIFI 模块,包含:串口无线 AP(COM-AP)、串口无线 STA(COM-STA)和串口无线 AP+STA(COM-AP+STA)这 3 个模式,每个模式又包含 TCP 服务器、 TCP客户端和 UDP 这 3 个子模式。
网络调试:
首先找到自己电脑的IP地址:
然后配置1、2、3(要让电脑和WiFi模块连在同一个热点)
第一次配置IP值时出错,圈圈里多了一个空格,书写一定要严谨,最好自己打IP。可以看到串口调试和网络调试实现了互相通信。网络调试助手可以一直发数据给串口调试助手,但是串口调试助手需要发数据就的每次输入“AT+CIPSEND=4”,出现OK后才可以发数据
在onenet上创建产品信息:
上面的这些随便填一下就行了,主要是要获得产品ID和API-KEY。
最后下载虚拟调试软件:虚拟调试工具(uf9n)
进行虚拟调试:
按步骤完成1、2、3:
可以看到在开发者中心处设备已经接通了:
以上,虚拟调试的部分完成了,前期的调试准备工作也完成了,第一部分结束。
下面开始讲解第二部分的内容:(代码部分)
连接图
简单画了个esp8266WiFi模块、stm32和onenet连接的图:
软件部分要实现的就是驱动esp8266-12F模块和接入EDP协议,下面就是有关程序的一些说明。
esp8266程序的编写
ATK-ESP8266是ALIENTEK推出的一款高性能的UART-WiFi(串口-无线)模块,ATK-ESP8266板载ai-thinker公司的ESP8266模块,该模块通过FCC, CE认证,可直接
用于产品出口欧美地区。
ATK-ESP8266模块采用串口(LVTTL)与MCU(或其他串口设备)通信,内置TCP/IP
协议栈,能够实现串口与WIFI之间的转换。通过ATK-ESP8266模块,传统的串口设备只是需要简单的串口配置,即可通过网络(WIFI)传输自己的数据。
ATK-ESP8266模块支持LVTTL串口,兼容3.3V和5V单片机系统,可以很方便的与你
的产品进行连接。模块支持串口转WIFI STA、串口转AP和WIFI STA+WIFI AP的模式,
从而快速构建串口-WIFI数据传输方案,方便你的设备使用互联网传输数据。
模块与开发板连接如下:
注意:记得检查开发板 P9 的跳线帽, 必须短接: PB11(RX) 和 GBC_TX 以及 PB10(TX) 和 GBC_RX
首先初始化esp8266,用于配置AT指令,使WiFi模块能够正常接网,也就是在代码实现前面第一部分写入AT指令操作:(“AT+CIPSTART=“TCP”,“183.230.40.39”,876\r\n”)
void ESP8266_Init(void)
{
ESP8266_Clear();
UsartPrintf(USART_DEBUG, "1. AT\r\n");
while(ESP8266_SendCmd("AT\r\n", "OK"))
Delay_ms(500);
Usart_Onenet("AT+RST\r\n");
Delay_ms(500);
UsartPrintf(USART_DEBUG, "2. CWMODE\r\n");
while(ESP8266_SendCmd("AT+CWMODE=1\r\n", "OK"))
Delay_ms(500);
UsartPrintf(USART_DEBUG, "3. AT+CWDHCP\r\n");
while(ESP8266_SendCmd("AT+CWDHCP=1,1\r\n", "OK"))
Delay_ms(500);
UsartPrintf(USART_DEBUG, "4. CWJAP\r\n");
while(ESP8266_SendCmd(ESP8266_WIFI_INFO, "OK"))//wifi名称以及密码,在程序开头已定义
Delay_ms(500);
UsartPrintf(USART_DEBUG, "5. CIPSTART\r\n");
while(ESP8266_SendCmd(ESP8266_ONENET_INFO, "CONNECT"))//服务器地址,在程序开头已定义
Delay_ms(500);
UsartPrintf(USART_DEBUG, "6. ESP8266 Init OK\r\n");
}
上面程序中用到了一个串口发送命令函数,该函数定义如下:
_Bool ESP8266_SendCmd(char *cmd, char *res)
{
unsigned short int timeOut = 1000;
Usart_Onenet((char *)cmd);
while(timeOut--)
{
if(ESP8266_WaitRecive() == REV_OK) //如果收到数据
{
//strstr函数功能:用来检索子串在字符中首次出现的位置,原型:char *strstr(char *str,char *substr);参数说明:str为要检索的字符串
//substr为要检索的子串;返回值:返回字符串str中第一次出现子串substr的地址,如果没有检索到子串,则返回NULL
if(strstr((const char *)esp8266_buf, res) != NULL) //如果从esp8266_buf[]中检索到关键词
{
ESP8266_Clear(); //清空缓存
return 0;
}
}
Delay_ms(10);
}
return 1;
接下来还需要定义一个发送数据函数,用于后面上传数据到onenet平台:
void ESP8266_SendData(unsigned char *data, unsigned short len)
{
char cmdBuf[32];
ESP8266_Clear(); //清空接收缓存
sprintf(cmdBuf, "AT+CIPSEND=%d\r\n", len); //发送命令
if(!ESP8266_SendCmd(cmdBuf, ">")) //收到‘>’时可以发送数据
{
Usart_SendString(USART3, data, len); //发送设备连接请求数据
}
}
onenet程序的编写
接下来就是需要建立与平台的连接,这个函数与平台封装的函数基本类似:
_Bool OneNet_DevLink(void)
{
EDP_PACKET_STRUCTURE edpPacket = {NULL, 0, 0, 0}; //协议包
unsigned char *dataPtr;
unsigned char status = 1;
UsartPrintf(USART_DEBUG, "OneNet_DevLink\r\n"
"DEVID: %s, APIKEY: %s\r\n"
,DEVID, APIKEY);
if(EDP_PacketConnect1(DEVID, APIKEY, 256, &edpPacket) == 0) //根据devid 和 apikey封装协议包
{
ESP8266_SendData(edpPacket._data, edpPacket._len); //上传平台
dataPtr = ESP8266_GetIPD(250); //等待平台响应
if(dataPtr != NULL)
{
if(EDP_UnPacketRecv(dataPtr) == CONNRESP)
{
switch(EDP_UnPacketConnectRsp(dataPtr))
{
case 0:UsartPrintf(USART_DEBUG, "Tips: 连接成功\r\n");status = 0;break;
case 1:UsartPrintf(USART_DEBUG, "WARN: 连接失败:协议错误\r\n");break;
case 2:UsartPrintf(USART_DEBUG, "WARN: 连接失败:设备ID鉴权失败\r\n");break;
case 3:UsartPrintf(USART_DEBUG, "WARN: 连接失败:服务器失败\r\n");break;
case 4:UsartPrintf(USART_DEBUG, "WARN: 连接失败:用户ID鉴权失败\r\n");break;
case 5:UsartPrintf(USART_DEBUG, "WARN: 连接失败:未授权\r\n");break;
case 6:UsartPrintf(USART_DEBUG, "WARN: 连接失败:授权码无效\r\n");break;
case 7:UsartPrintf(USART_DEBUG, "WARN: 连接失败:激活码未分配\r\n");break;
case 8:UsartPrintf(USART_DEBUG, "WARN: 连接失败:该设备已被激活\r\n");break;
case 9:UsartPrintf(USART_DEBUG, "WARN: 连接失败:重复发送连接请求包\r\n");break;
default:UsartPrintf(USART_DEBUG, "ERR: 连接失败:未知错误\r\n");break;
}
}
else
{
UsartPrintf(USART_DEBUG, "chf_error !!! \r\n");
}
}
else
{
UsartPrintf(USART_DEBUG, "平台不响应\r\n");
}
EDP_DeleteBuffer(&edpPacket); //删包
}
else
UsartPrintf(USART_DEBUG, "WARN: EDP_PacketConnect Failed\r\n");
return status;
}
建立连接后,再看上传数据接口,该函数也是移植平台封装好的上传数据接口函数 :
void OneNet_SendData(void)
{
EDP_PACKET_STRUCTURE edpPacket = {NULL, 0, 0, 0}; //协议包
char buf[128];
short body_len = 0, i = 0;
UsartPrintf(USART_DEBUG, "Tips: OneNet_SendData-EDP\r\n");
memset(buf, 0, sizeof(buf));
body_len = OneNet_FillBuf(buf); //获取当前需要发送的数据流的总长度
if(body_len)
{
if(EDP_PacketSaveData(DEVID, body_len, NULL, kTypeSimpleJsonWithoutTime, &edpPacket) == 0) //封包
{
for(; i < body_len; i++)
edpPacket._data[edpPacket._len++] = buf[i];
ESP8266_SendData(edpPacket._data, edpPacket._len); //上传数据到平台
UsartPrintf(USART_DEBUG, "Send %d Bytes\r\n", edpPacket._len);
EDP_DeleteBuffer(&edpPacket); //删包
}
else
UsartPrintf(USART_DEBUG, "WARN: EDP_NewBuffer Failed\r\n");
}
}
**body_len = OneNet_FillBuf(buf);**用于在平台上创建数据流,该函数定义如下:
unsigned char OneNet_FillBuf(char *buf)
{
char text[16];
memset(text, 0, sizeof(text));
strcpy(buf, "{");
memset(text, 0, sizeof(text));
sprintf(text, "\"WENDU\":%.2f", temperature/10.0);
strcat(buf, text);
strcat(buf, "}");
return strlen(buf);
}
"WENDU为数据流名称,temperature/10.0为温度值,后面程序会定义。
DS18B20程序的编写
该函数比较常见,原子的例程也有详细的讲解,这里就直接贴出来了:
//复位DS18B20
void DS18B20_Rst(void)
{
DS18B20_IO_OUT(); //设置为输出
DS18B20_DQ_OUT=0; //拉低DQ
delay_us(750); //拉低750us
DS18B20_DQ_OUT=1; //DQ=1
delay_us(15); //15US
}
//等待DS18B20的回应
//返回1:未检测到DS18B20的存在
//返回0:存在
u8 DS18B20_Check(void)
{
u8 retry=0;
DS18B20_IO_IN(); //设置为输入
while (DS18B20_DQ_IN&&retry<200)
{
retry++;
delay_us(1);
};
if(retry>=200)return 1;
else retry=0;
while (!DS18B20_DQ_IN&&retry<240)
{
retry++;
delay_us(1);
};
if(retry>=240)return 1;
return 0;
}
//从DS18B20读取一个位
//返回值:1/0
u8 DS18B20_Read_Bit(void)
{
u8 data;
DS18B20_IO_OUT(); //设置为输出
DS18B20_DQ_OUT=0;
delay_us(2);
DS18B20_DQ_OUT=1;
DS18B20_IO_IN(); //设置为输入
delay_us(12);
if(DS18B20_DQ_IN)data=1;
else data=0;
delay_us(50);
return data;
}
//从DS18B20读取一个字节
//返回值:读到的数据
u8 DS18B20_Read_Byte(void)
{
u8 i,j,dat;
dat=0;
for (i=1;i<=8;i++)
{
j=DS18B20_Read_Bit();
dat=(j<<7)|(dat>>1);
}
return dat;
}
//写一个字节到DS18B20
//dat:要写入的字节
void DS18B20_Write_Byte(u8 dat)
{
u8 j;
u8 testb;
DS18B20_IO_OUT(); //设置为输出
for (j=1;j<=8;j++)
{
testb=dat&0x01;
dat=dat>>1;
if(testb) // 写1
{
DS18B20_DQ_OUT=0;
delay_us(2);
DS18B20_DQ_OUT=1;
delay_us(60);
}
else //写0
{
DS18B20_DQ_OUT=0;
delay_us(60);
DS18B20_DQ_OUT=1;
delay_us(2);
}
}
}
//开始温度转换
void DS18B20_Start(void)
{
DS18B20_Rst();
DS18B20_Check();
DS18B20_Write_Byte(0xcc);// skip rom
DS18B20_Write_Byte(0x44);// convert
}
//初始化DS18B20的IO口 DQ 同时检测DS的存在
//返回1:不存在
//返回0:存在
u8 DS18B20_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE(); //开启GPIOB时钟
GPIO_Initure.Pin=GPIO_PIN_12; //PB12
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
HAL_GPIO_Init(GPIOB,&GPIO_Initure); //初始化
DS18B20_Rst();
return DS18B20_Check();
}
//从ds18b20得到温度值
//精度:0.1C
//返回值:温度值 (-550~1250)
short DS18B20_Get_Temp(void)
{
u8 temp;
u8 TL,TH;
short tem;
DS18B20_Start (); //开始转换
DS18B20_Rst();
DS18B20_Check();
DS18B20_Write_Byte(0xcc); // skip rom
DS18B20_Write_Byte(0xbe); // convert
TL=DS18B20_Read_Byte(); // LSB
TH=DS18B20_Read_Byte(); // MSB
if(TH>7)
{
TH=~TH;
TL=~TL;
temp=0;//温度为负
}else temp=1;//温度为正
tem=TH; //获得高八位
tem<<=8;
tem+=TL;//获得底八位
tem=(double)tem*0.625;//转换
if(temp)return tem; //返回温度值
else return -tem;
}
主函数逻辑简单说明
最后的主函数基本上是对上面定义的函数的调用,由于串口定义那些比较基础,就另外列出来了,主函数大概的思路如下:
程序如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "lcd.h"
#include "ds18b20.h"
#include "pcf8574.h"
//图片
#include "image_2k.h"
#include "tx.h"
//网络协议层
#include "onenet.h"
//网络设备层
#include "esp8266.h"
int led_sta;
short temperature;
void Hardware_Init(void)
{
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
delay_init(180); //初始化延时函数
uart_init(115200); //初始化USART
uart3_init(115200);
LED_Init(); //初始化LED
LCD_Init();
PCF8574_Init(); //初始化PCF8574
PCF8574_ReadBit(BEEP_IO); //初始化LCD
while(DS18B20_Init());
LCD_Clear(YELLOW);
POINT_COLOR=BLACK;
ESP8266_Init(); //初始化ESP8266
UsartPrintf(USART_DEBUG,"Hard_init finish!\r\n");
}
int main(void)
{
unsigned short timeCount = 0; //发送间隔变量
unsigned short timeCount_pic = 0; //发送间隔变量
int whitch_pic=0;
unsigned char *dataPtr = NULL;
u8 x=0;
Hardware_Init();
while(OneNet_DevLink()) //接入OneNET
Delay_ms(500);
LCD_ShowString(10,40,257,32,32,"Hello! OneNet ");
while(1)
{
PCF8574_ReadBit(BEEP_IO); //读取一次PCF8574的任意一个IO,使其释放掉PB12引脚,否则读取DS18B20可能会出问题
temperature=DS18B20_Get_Temp();
if(temperature<0)
{
temperature=-temperature; //转为正数
}
if(++timeCount >= 500) //发送间隔5s
{
UsartPrintf(USART_DEBUG, "OneNet_SendData\r\n");
OneNet_SendData(); //发送数据
timeCount = 0;
ESP8266_Clear();
led_sta=~led_sta;
}
dataPtr = ESP8266_GetIPD(0);
if(dataPtr != NULL)
OneNet_RevPro(dataPtr);
Delay_ms(25);
x++;
if(x==12)x=0;
LED0=!LED0;
}
}