一、环境介绍
单片机采用:STM32F103C8T6
上网方式:采用ESP8266,也可以使用其他设备代替,只要支持TCP协议即可。比如:GSM模块、有线网卡等。
开发软件:keil5
硬件连接功能:ESP8266接在STM32的串口3上。通过AT指令与ESP8266进行通信。
注意:本篇文章没有贴ESP8266的底层编程代码,如果不会ESP8266底层编程,请看这里:
https://blog.csdn.net/xiaolong1126626497/article/details/107379554
想了解STM32+ESP8266使用MQTT协议连接OneNET 中国移动服务器的看这里:
https://blog.csdn.net/xiaolong1126626497/article/details/107385118
工程完整源码下载: https://download.csdn.net/download/xiaolong1126626497/15803494
二、实现功能
通过阿里云物联网服务器实现设备数据远程上传、下发,实现数据交互。
在当前使用的开发板上有4盏LED灯、一个蜂鸣器、4个按键。
实现步骤阿里云官方提供了很详细的文档和对应的SDK,可以参考一下。
文档地址:https://iot.console.aliyun.com/lk/document
三、阿里云物联网服务器创建步骤
说明:如果没有账号的话,先点击网页右上角,注册一个账号,并完成实名认证再继续下一步。
产品名称根据自己情况填写。
设备信息根据自己情况填写。
下面参数根据自己情况填写。
四、向服务器上传的数据效果
完成网页端服务器的创建之后,下面使用STM32开发板按下按键通过ESP8266将烟雾传感器数据上传到阿里云服务器。
如果连接成功的话,网页会显示在线状态。
上传的数据可以在这里查看。
五、STM32端的MQTT协议核心代码
代码是标准的MQTT协议代码,实现过程可以参考MQTT协议官方文档。
5.1 mqtt.c代码
#include "aliyun_mqtt.h" char MQTT_ClientID[100]; //MQTT_客户端ID char MQTT_UserName[100]; //MQTT_用户名 char MQTT_PassWord[100]; //MQTT_密码 u8 *mqtt_rxbuf; u8 *mqtt_txbuf; u16 mqtt_rxlen; u16 mqtt_txlen; u8 _mqtt_txbuf[256];//发送数据缓存区 u8 _mqtt_rxbuf[256];//接收数据缓存区 typedef enum { //名字 值 报文流动方向 描述 M_RESERVED1 =0 , // 禁止 保留 M_CONNECT , // 客户端到服务端 客户端请求连接服务端 M_CONNACK , // 服务端到客户端 连接报文确认 M_PUBLISH , // 两个方向都允许 发布消息 M_PUBACK , // 两个方向都允许 QoS 1消息发布收到确认 M_PUBREC , // 两个方向都允许 发布收到(保证交付第一步) M_PUBREL , // 两个方向都允许 发布释放(保证交付第二步) M_PUBCOMP , // 两个方向都允许 QoS 2消息发布完成(保证交互第三步) M_SUBSCRIBE , // 客户端到服务端 客户端订阅请求 M_SUBACK , // 服务端到客户端 订阅请求报文确认 M_UNSUBSCRIBE , // 客户端到服务端 客户端取消订阅请求 M_UNSUBACK , // 服务端到客户端 取消订阅报文确认 M_PINGREQ , // 客户端到服务端 心跳请求 M_PINGRESP , // 服务端到客户端 心跳响应 M_DISCONNECT , // 客户端到服务端 客户端断开连接 M_RESERVED2 , // 禁止 保留 }_typdef_mqtt_message; //连接成功服务器回应 20 02 00 00 //客户端主动断开连接 e0 00 const u8 parket_connetAck[] = {0x20,0x02,0x00,0x00}; const u8 parket_disconnet[] = {0xe0,0x00}; const u8 parket_heart[] = {0xc0,0x00}; const u8 parket_heart_reply[] = {0xc0,0x00}; const u8 parket_subAck[] = {0x90,0x03}; /* 函数功能: 初始化阿里云物联网服务器的登录参数 */ //密码 //clientId*deviceName*productKey# // *替换为DeviceName #替换为ProductKey 加密密钥是DeviceSecret 加密方式是HmacSHA1 //PassWord明文=clientIdmq2_iotdeviceNamemq2_iotproductKeya1WLC5GuOfx //hmacsha1加密网站:http://encode.chahuo.com/ //加密的密钥:DeviceSecret void Aliyun_LoginInit(char *ProductKey,char *DeviceName,char *DeviceSecret) { sprintf(MQTT_ClientID,"%s|securemode=3,signmethod=hmacsha1|",DeviceName); sprintf(MQTT_UserName,"%s&%s",DeviceName,ProductKey); sprintf(MQTT_PassWord,"%s","ebc042f42a9d73ba9ead8456b652e7756895b79d"); } void MQTT_Init(void) { //缓冲区赋值 mqtt_rxbuf = _mqtt_rxbuf; mqtt_rxlen = sizeof(_mqtt_rxbuf); mqtt_txbuf = _mqtt_txbuf; mqtt_txlen = sizeof(_mqtt_txbuf); memset(mqtt_rxbuf,0,mqtt_rxlen); memset(mqtt_txbuf,0,mqtt_txlen); //无条件先主动断开 MQTT_Disconnect(); delay_ms(100); MQTT_Disconnect(); delay_ms(100); } /* 函数功能: 登录服务器 函数返回值: 0表示成功 1表示失败 */ u8 MQTT_Connect(char *ClientID,char *Username,char *Password) { u8 i,j; int ClientIDLen = strlen(ClientID); int UsernameLen = strlen(Username); int PasswordLen = strlen(Password); int DataLen; mqtt_txlen=0; //可变报头+Payload 每个字段包含两个字节的长度标识 DataLen = 10 + (ClientIDLen+2) + (UsernameLen+2) + (PasswordLen+2); //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x10; //MQTT Message Type CONNECT //剩余长度(不包括固定头部) do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 //协议名 mqtt_txbuf[mqtt_txlen++] = 0; // Protocol Name Length MSB mqtt_txbuf[mqtt_txlen++] = 4; // Protocol Name Length LSB mqtt_txbuf[mqtt_txlen++] = 'M'; // ASCII Code for M mqtt_txbuf[mqtt_txlen++] = 'Q'; // ASCII Code for Q mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T mqtt_txbuf[mqtt_txlen++] = 'T'; // ASCII Code for T //协议级别 mqtt_txbuf[mqtt_txlen++] = 4; // MQTT Protocol version = 4 //连接标志 mqtt_txbuf[mqtt_txlen++] = 0xc2; // conn flags mqtt_txbuf[mqtt_txlen++] = 0; // Keep-alive Time Length MSB mqtt_txbuf[mqtt_txlen++] = 100; // Keep-alive Time Length LSB 100S心跳包 mqtt_txbuf[mqtt_txlen++] = BYTE1(ClientIDLen);// Client ID length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(ClientIDLen);// Client ID length LSB memcpy(&mqtt_txbuf[mqtt_txlen],ClientID,ClientIDLen); mqtt_txlen += ClientIDLen; if(UsernameLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(UsernameLen); //username length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(UsernameLen); //username length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Username,UsernameLen); mqtt_txlen += UsernameLen; } if(PasswordLen > 0) { mqtt_txbuf[mqtt_txlen++] = BYTE1(PasswordLen); //password length MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(PasswordLen); //password length LSB memcpy(&mqtt_txbuf[mqtt_txlen],Password,PasswordLen); mqtt_txlen += PasswordLen; } for(i=0;i<10;i++) { memset(mqtt_rxbuf,0,mqtt_rxlen); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); for(j=0;j<10;j++) { delay_ms(50); if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; sprintf((char *)mqtt_rxbuf,"%s",USART3_RX_BUFFER); USART3_RX_FLAG=0; USART3_RX_CNT=0; } //CONNECT if(mqtt_rxbuf[0]==parket_connetAck[0] && mqtt_rxbuf[1]==parket_connetAck[1]) //连接成功 { return 0;//连接成功 } } } return 1; } /* 函数功能: MQTT订阅/取消订阅数据打包函数 函数参数: topic 主题 qos 消息等级 0:最多分发一次 1: 至少分发一次 2: 仅分发一次 whether 订阅/取消订阅请求包 (1表示订阅,0表示取消订阅) 返回值: 0表示成功 1表示失败 */ u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether) { u8 i,j; mqtt_txlen=0; int topiclen = strlen(topic); int DataLen = 2 + (topiclen+2) + (whether?1:0);//可变报头的长度(2字节)加上有效载荷的长度 //固定报头 //控制报文类型 if(whether)mqtt_txbuf[mqtt_txlen++] = 0x82; //消息类型和标志订阅 else mqtt_txbuf[mqtt_txlen++] = 0xA2; //取消订阅 //剩余长度 do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); //可变报头 mqtt_txbuf[mqtt_txlen++] = 0; //消息标识符 MSB mqtt_txbuf[mqtt_txlen++] = 0x01; //消息标识符 LSB //有效载荷 mqtt_txbuf[mqtt_txlen++] = BYTE1(topiclen);//主题长度 MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topiclen);//主题长度 LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topiclen); mqtt_txlen += topiclen; if(whether) { mqtt_txbuf[mqtt_txlen++] = qos;//QoS级别 } for(i=0;i<10;i++) { memset(mqtt_rxbuf,0,mqtt_rxlen); MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); for(j=0;j<10;j++) { delay_ms(50); if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; strcpy((char *)mqtt_rxbuf,(char*)USART3_RX_BUFFER); USART3_RX_FLAG=0; USART3_RX_CNT=0; } if(mqtt_rxbuf[0]==parket_subAck[0] && mqtt_rxbuf[1]==parket_subAck[1]) //订阅成功 { return 0;//订阅成功 } } } return 1; //失败 } //MQTT发布数据打包函数 //topic 主题 //message 消息 //qos 消息等级 u8 MQTT_PublishData(char *topic, char *message, u8 qos) { int topicLength = strlen(topic); int messageLength = strlen(message); static u16 id=0; int DataLen; mqtt_txlen=0; //有效载荷的长度这样计算:用固定报头中的剩余长度字段的值减去可变报头的长度 //QOS为0时没有标识符 //数据长度 主题名 报文标识符 有效载荷 if(qos) DataLen = (2+topicLength) + 2 + messageLength; else DataLen = (2+topicLength) + messageLength; //固定报头 //控制报文类型 mqtt_txbuf[mqtt_txlen++] = 0x30; // MQTT Message Type PUBLISH //剩余长度 do { u8 encodedByte = DataLen % 128; DataLen = DataLen / 128; // if there are more data to encode, set the top bit of this byte if ( DataLen > 0 ) encodedByte = encodedByte | 128; mqtt_txbuf[mqtt_txlen++] = encodedByte; }while ( DataLen > 0 ); mqtt_txbuf[mqtt_txlen++] = BYTE1(topicLength);//主题长度MSB mqtt_txbuf[mqtt_txlen++] = BYTE0(topicLength);//主题长度LSB memcpy(&mqtt_txbuf[mqtt_txlen],topic,topicLength);//拷贝主题 mqtt_txlen += topicLength; //报文标识符 if(qos) { mqtt_txbuf[mqtt_txlen++] = BYTE1(id); mqtt_txbuf[mqtt_txlen++] = BYTE0(id); id++; } memcpy(&mqtt_txbuf[mqtt_txlen],message,messageLength); mqtt_txlen += messageLength; MQTT_SendBuf(mqtt_txbuf,mqtt_txlen); return mqtt_txlen; } void MQTT_SentHeart(void) { MQTT_SendBuf((u8 *)parket_heart,sizeof(parket_heart)); } void MQTT_Disconnect(void) { MQTT_SendBuf((u8 *)parket_disconnet,sizeof(parket_disconnet)); } void MQTT_SendBuf(u8 *buf,u16 len) { USARTx_DataSend(USART3,buf,len); }
5.2 mqtt.h代码
#ifndef __FY_MQTT_H_ #define __FY_MQTT_H_ #include "stm32f10x.h" #include "string.h" #include "stdio.h" #include "stdlib.h" #include "stdarg.h" #include "delay.h" #include "usart.h" #define BYTE0(dwTemp) (*( char *)(&dwTemp)) #define BYTE1(dwTemp) (*((char *)(&dwTemp) + 1)) #define BYTE2(dwTemp) (*((char *)(&dwTemp) + 2)) #define BYTE3(dwTemp) (*((char *)(&dwTemp) + 3)) extern char MQTT_ClientID[100]; //MQTT_客户端ID extern char MQTT_UserName[100]; //MQTT_用户名 extern char MQTT_PassWord[100]; //MQTT_密码 //阿里云用户名初始化 void Aliyun_LoginInit(char *ProductKey,char *DeviceName,char *DeviceSecret); //MQTT协议相关函数声明 u8 MQTT_PublishData(char *topic, char *message, u8 qos); u8 MQTT_SubscribeTopic(char *topic,u8 qos,u8 whether); void MQTT_Init(void); u8 MQTT_Connect(char *ClientID,char *Username,char *Password); void MQTT_SentHeart(void); void MQTT_Disconnect(void); void MQTT_SendBuf(u8 *buf,u16 len); #endif
5.3 main.c代码
#include "stm32f10x.h" #include "led.h" #include "delay.h" #include "key.h" #include "usart.h" #include <string.h> #include "timer.h" #include "bluetooth.h" #include "esp8266.h" #include "aliyun_mqtt.h" //阿里云物联网服务器的设备证书 #define ProductKey "a1GLM2BFQK0" #define DeviceName "iot_mq2" #define DeviceSecret "caa5b2684101072f3dfe0f04688e0a7f" //订阅与发布的主题 #define SET_TOPIC "/sys/a1GLM2BFQK0/iot_mq2/thing/service/property/set" #define POST_TOPIC "/sys/a1GLM2BFQK0/iot_mq2/thing/event/property/post" char mqtt_message[200];//上报数据缓存区 int main() { u32 time_cnt=0; u32 i; u8 key; LED_Init(); BEEP_Init(); KEY_Init(); USART1_Init(115200); TIMER1_Init(72,20000); //超时时间20ms USART2_Init(9600);//串口-蓝牙 TIMER2_Init(72,20000); //超时时间20ms USART3_Init(115200);//串口-WIFI TIMER3_Init(72,20000); //超时时间20ms USART1_Printf("正在初始化WIFI请稍等.\n"); if(ESP8266_Init()) { USART1_Printf("ESP8266硬件检测错误.\n"); } else { USART1_Printf("WIFI:%d\n",ESP8266_STA_TCP_Client_Mode("ChinaNet-wbyq","12345678","a1WLC5GuOfx.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883,1)); } //1. 初始化阿里云登录参数 Aliyun_LoginInit(ProductKey,DeviceName,DeviceSecret); //2. MQTT协议初始化 MQTT_Init(); //3. 连接阿里云服务器 while(MQTT_Connect(MQTT_ClientID,MQTT_UserName,MQTT_PassWord)) { USART1_Printf("阿里云服务器连接失败,正在重试...\n"); delay_ms(500); } USART1_Printf("阿里云服务器连接成功.\n"); //3. 订阅主题 if(MQTT_SubscribeTopic(SET_TOPIC,0,1)) { USART1_Printf("主题订阅失败.\n"); } else { USART1_Printf("主题订阅成功.\n"); } while(1) { key=KEY_Scan(0); if(key==2) { time_cnt=0; sprintf(mqtt_message,"{\"method\":\"thing.event.property.post\",\"id\":\"0000000001\",\"params\":{\"mq2\":55},\"version\":\"1.0.0\"}"); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("发送状态1\r\n"); } else if(key==3) { time_cnt=0; sprintf(mqtt_message,"{\"method\":\"thing.event.property.post\",\"id\":\"0000000001\",\"params\":{\"mq2\":66},\"version\":\"1.0.0\"}"); MQTT_PublishData(POST_TOPIC,mqtt_message,0); USART1_Printf("发送状态0\r\n"); } if(USART3_RX_FLAG) { USART3_RX_BUFFER[USART3_RX_CNT]='\0'; for(i=0;i<USART3_RX_CNT;i++) { USART1_Printf("%c",USART3_RX_BUFFER[i]); } USART3_RX_CNT=0; USART3_RX_FLAG=0; } //定时发送心跳包,保持连接 delay_ms(10); time_cnt++; if(time_cnt==500) { MQTT_SentHeart();//发送心跳包 time_cnt=0; } } }
六、代码参数解释
设备证书在创建设备时保存过,如果没有保存可以在下面的页面里查看对应的值。
订阅的主题在下面页面可以看到。
SET,GET,POST,ERR。 SET 用于设置(一般由单片机端使用), GET 用于获取(一般由 APP 端使用), Post 用于回复机制, ERR 用于错误。
作为单片机端用的最多的两个 TOPIC 就是 SET 与 POST
6.2 MQTT登录的密码、ID、用户名、端口号、域名
MQTT标准的3个参数格式在官方文档有介绍:https://help.aliyun.com/document_detail/140507.html?spm=a2c4g.11186623.6.571.1e417544OGPj2y
密码的组成格式:
1.clientId*deviceName*productKey# 其中: *替换为DeviceName #替换为ProductKey 加密密钥是DeviceSecret 加密方式是HmacSHA1 PassWord明文=clientIdmq2_iotdeviceNamemq2_iotproductKeya1WLC5GuOfx hmacsha1加密网站:http://encode.chahuo.com/ 加密的密钥:DeviceSecret
6.3 上传数据
这是上传数据的格式:
第一个参数是 method:后面所跟的参数可以由物模型看到。
第二个参数id : 因为云端会连接很多个用户,所以他所下发的数据会有一个ID 编号,我们这里任意值都行,我用的 0000000001,这里有要注意的,这个 ID 是多少不重要但是位数一定不能少。
第三个是 params:表示上传的具体数据,根据自己云端订阅的类型上传。
第四个是版本号:可以根据自己实际版本填。