admin管理员组

文章数量:1540705

目录

一、阿里云物联网平台配置

 二、创建STM32工程

        2.1 MCU及WIFI模组原理框图

         2.2 创建工程及配置

 三、代码设计

        3.1 工程设计

        3.2 printf映射到UART1设计

        3.3 定时器TIM2辅助UART3接收数据代码设计

        3.4 WIFI输出代码设计

        3.5 按键及LED灯驱动实现

        3.6 实现与阿里云物联网平台通信

        3.7 MQTT业务逻辑实现

四、编译及测试

 五、补充


一、阿里云物联网平台配置

        关于阿里云物联网平台上如何配置产品及设备,如何利用MQTT.fx工具及已配置好的产品与设备相关MQTT链接信息,实现MQTT通信连接、订阅及发布测试,请参考本人的博文:

基于IBM纯 c标准MQTT源码,实现纯C语言访问阿里云物联网平台开发案例_py_free的博客-CSDN博客

        假设已经在物联网平台上配置了如下产品及设备

         并通过MQTT.fx工具成功链接上物联网平台及测试了发布、订阅消息

 二、创建STM32工程

        2.1 MCU及WIFI模组原理框图

        MCU框图,调试串口UART1采用PA9、PA10,MCU与WIFI模块采用UART3,使用PB10、PB11引脚,两个按键采用PA0、PA1引脚,两个LED灯采用PB0、PB1引脚。

         WIFI模块为常用的ESP8266芯片,通过与MCU的PB10、PB11引脚串口通信,接收来自MCU的AT指令控制。

         2.2 创建工程及配置

        【1】基于STM32F103C8Tx创建了STM32工程stm32F103C8Tx_mqtt,双击.ioc进入cubeMX配置界面,先配置系统功能,配置Debug调试和TimeBase源如下。

         【2】开启串口UART1和UART3为异步通信模式,配置其串口参数为115200、8、N、1

         开启UART1和UART3的外设中断功能

         串口引脚配置如下:

        【3】配置KEY和LED的四个引脚如下图

         【4】开启RCC功能

         以及设置时钟树如下:

         【5】激活RTC,参数保持默认设置

         【6】设置TIM2,主要是为了辅助UART3接收来自MQTT服务端(阿里云物联网平台)消息,关于TIM定时器参数设置原理以及TIM定时器与串口通信配合的详细信息请参考本专栏的博文:

cubeIDE开发, 定时器TIM与外部中断NVIC实践案例_py_free的博客-CSDN博客

        TIM2设置及参数如下,同时开启TIM2中断功能:

         【7】配置工程及输出生成代码

 三、代码设计

        3.1 工程设计

        本文采用了IBM的MQTT c开源库实现MQTT协议解析,采用ESP8266 WIFI模组,通过AT指令实现MQTT客户端,与阿里云物联网平台通信。MCU通过UART3与WIFI模组串口通信,进而实现与阿里云物联平台通信。

        UART3在数据接收时,采用TIM2辅助,每100毫秒读取一次数据,实现一帧数据接收,而非通过换行等结束标记完成每帧数据接收。数据发送及接收时,逐个字节处理的,一帧内的字节间隔是很小的,前后帧间的帧未字节和后帧最前字节时间间隔要比帧内字节时间间隔大的多。

        3.2 printf映射到UART1设计

         在工程目录下创建源目录ICore,在该目录下创建print目录,同时在print目录下创建print.h和print.c源文件,重新实现syscalls.c文件内相关函数,支持到printf函数。

        print.h

#ifndef PRINT_PRINT_H_
#define PRINT_PRINT_H_

#include "stm32f1xx_hal.h"
#include "stdio.h"//用于printf函数串口重映射
#include <sys/stat.h>

void ResetPrintInit(UART_HandleTypeDef  *huart);

int _isatty(int fd);
int _write(int fd, char* ptr, int len);
int _close(int fd);
int _lseek(int fd, int ptr, int dir);
int _read(int fd, char* ptr, int len);
int _fstat(int fd, struct stat* st);

#endif /* PRINT_PRINT_H_ */

        print.c

#include <_ansi.h>
#include <_syslist.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/times.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>

#include "print.h"

#if !defined(OS_USE_SEMIHOSTING)
#define STDIN_FILENO  0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2

UART_HandleTypeDef *gHuart;

void ResetPrintInit(UART_HandleTypeDef *huart)  {
  gHuart = huart;
  /* Disable I/O buffering for STDOUT  stream, so that
   * chars are sent out as soon as they are  printed. */
  setvbuf(stdout, NULL, _IONBF, 0);
}
int _isatty(int fd) {
  if (fd >= STDIN_FILENO && fd <=  STDERR_FILENO)
    return 1;
  errno = EBADF;
  return 0;
}
int _write(int fd, char* ptr, int len) {
  HAL_StatusTypeDef hstatus;
  if (fd == STDOUT_FILENO || fd ==  STDERR_FILENO) {
    hstatus = HAL_UART_Transmit(gHuart,  (uint8_t *) ptr, len, HAL_MAX_DELAY);
    if (hstatus == HAL_OK)
      return len;
    else
      return EIO;
  }
  errno = EBADF;
  return -1;
}
int _close(int fd) {
  if (fd >= STDIN_FILENO && fd <=  STDERR_FILENO)
    return 0;
  errno = EBADF;
  return -1;
}
int _lseek(int fd, int ptr, int dir) {
  (void) fd;
  (void) ptr;
  (void) dir;
  errno = EBADF;
  return -1;
}
int _read(int fd, char* ptr, int len) {
  HAL_StatusTypeDef hstatus;
  if (fd == STDIN_FILENO) {
    hstatus = HAL_UART_Receive(gHuart,  (uint8_t *) ptr, 1, HAL_MAX_DELAY);
    if (hstatus == HAL_OK)
      return 1;
    else
      return EIO;
  }
  errno = EBADF;
  return -1;
}
int _fstat(int fd, struct stat* st) {
  if (fd >= STDIN_FILENO && fd <=  STDERR_FILENO) {
    st->st_mode = S_IFCHR;
    return 0;
  }
  errno = EBADF;
  return 0;
}

#endif //#if !defined(OS_USE_SEMIHOSTING)

        在ICore目录下创建usart目录,同时在usart目录下创建usart.h和usart.c源文件

        usart.h,huart1为串口UART1,实现与电脑通信,huart3为串口UART3,WIFI模组通信

#ifndef INC_USART_H_
#define INC_USART_H_

#include "stm32f1xx_hal.h" //HAL库文件声明
#include <string.h>//用于字符串处理的库
#include "../print/print.h"//用于printf函数串口重映射

extern TIM_HandleTypeDef htim2;

extern UART_HandleTypeDef huart1;//声明USART1的HAL库结构体
extern UART_HandleTypeDef huart3;//声明USART2的HAL库结构体

#define USART1_REC_LEN  200//定义USART1最大接收字节数
#define USART3_REC_LEN  200//定义USART1最大接收字节数

extern uint8_t  USART1_RX_BUF[USART1_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern uint16_t USART1_RX_STA;//接收状态标记
extern uint8_t USART1_NewData;//当前串口中断接收的1个字节数据的缓存

extern uint8_t  USART3_RX_BUF[USART3_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
extern uint16_t USART3_RX_STA;//接收状态标记
extern uint8_t USART3_NewData;//当前串口中断接收的1个字节数据的缓存

void  HAL_UART_RxCpltCallback(UART_HandleTypeDef  *huart);//串口中断回调函数声明

#endif /* INC_USART_H_ */

        usart.c,重写串口的中断回调函数,huart1采用换行字符判定是否接收结束,而huart3采用TIM2定时器计时判定结束,TIM2会重置接收标记位,huart3接收数据后重新使能TIM2开启下一轮接收。

#include "usart.h"

uint8_t USART1_RX_BUF[USART1_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.
uint16_t USART1_RX_STA=0;//接收状态标记//bit15:接收完成标志,bit14:接收到0x0d,bit13~0:接收到的有效字节数目
uint8_t USART1_NewData;//当前串口中断接收的1个字节数据的缓存

uint8_t USART3_RX_BUF[USART3_REC_LEN];//接收缓冲,最大USART_REC_LEN个字节.
uint16_t USART3_RX_STA=0;//接收状态标记//bit15:接收完成标志,bit14:接收到0x0d,bit13~0:接收到的有效字节数目
uint8_t USART3_NewData;//当前串口中断接收的1个字节数据的缓存

void  HAL_UART_RxCpltCallback(UART_HandleTypeDef  *huart)//串口中断回调函数
{
	if(huart ==&huart1)//判断中断来源(串口1:USB转串口)
    {
       printf("%c",USART1_NewData); //把收到的数据以 a符号变量 发送回电脑
       if((USART1_RX_STA&0x8000)==0){//接收未完成
           if(USART1_RX_STA&0x4000){//接收到了0x0d
               if(USART1_NewData!=0x0a)USART1_RX_STA=0;//接收错误,重新开始
               else USART1_RX_STA|=0x8000;   //接收完成了
           }else{ //还没收到0X0D
               if(USART1_NewData==0x0d)USART1_RX_STA|=0x4000;
               else{
                  USART1_RX_BUF[USART1_RX_STA&0X3FFF]=USART1_NewData; //将收到的数据放入数组
                  USART1_RX_STA++;  //数据长度计数加1
                  if(USART1_RX_STA>(USART1_REC_LEN-1))USART1_RX_STA=0;//接收数据错误,重新开始接收
               }
           }
       }
       HAL_UART_Receive_IT(&huart1,(uint8_t *)&USART1_NewData,1); //再开启接收中断
    }
	if(huart ==&huart3)//判断中断来源(串口3:WIFI模块)//接收完的一批数据,还没有被处理,则不再接收其他数据
	{
		if(USART3_RX_STA<USART3_REC_LEN)//还可以接收数据
		{
			__HAL_TIM_SET_COUNTER(&htim2,0); //计数器清空
			if(USART3_RX_STA==0) //使能定时器2的中断
			{
				__HAL_TIM_ENABLE(&htim2); //使能定时器2
			}
			USART3_RX_BUF[USART3_RX_STA++] = USART3_NewData;//最新接收数据放入数组
		}else
		{
			USART3_RX_STA|=0x8000;//强制标记接收完成
		}

		HAL_UART_Receive_IT(&huart3,(uint8_t *)&USART3_NewData,1); //再开启串口3接收中断
	}
}

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
	UNUSED(huart);
	if(huart ==&huart3)	//判断串口错误回调函数,串口是否为IAP串口函数
	{
		printf("HAL_UART_ErrorCallback->huart3:%lu!\r\n",huart->ErrorCode);
		//HAL_UART_Receive_IT(&huart3,(uint8_t *)&USART3_NewData,1);
		if(HAL_UART_ERROR_ORE&huart->ErrorCode)
			__HAL_UART_FLUSH_DRREGISTER(huart);
	}
}

        3.3 定时器TIM2辅助UART3接收数据代码设计

        在ICore目录下创建tim目录,同时在tim目录下创建tim.h和tim.c源文件

        tim.h

#ifndef TIM_TIM_H_
#define TIM_TIM_H_

#include "main.h"
#include "../usart/usart.h"

extern TIM_HandleTypeDef htim2;

#endif /* TIM_TIM_H_ */

        tim.c,重写定时器到时中断回调函数,重置UART3的接收结束标记,关闭计时

#include "tim.h"

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时器中断回调函数
{
   if(htim ==&htim2)//判断是否是定时器2中断(定时器到时表示一组字符串接收结束)
    {
		USART3_RX_BUF[USART3_RX_STA&0X7FFF]=0;//添加结束符
		USART3_RX_STA|=0x8000;//接收标志位最高位置1表示接收完成
		__HAL_TIM_CLEAR_FLAG(&htim2,TIM_EVENTSOURCE_UPDATE );//清除TIM2更新中断标志
		__HAL_TIM_DISABLE(&htim2);//关闭定时器2
    }
}

        3.4 WIFI输出代码设计

        上述通过TIM2辅助UART3,实现从WIFI模组接收数据,下面设计MCU向WIFI模组发送数据代码,在ICore目录下创建wifi目录,同时在usart目录下创建wifi.h和wifi.c源文件.

        关于MCU与WIFI模组的AT指令通信原理请参考本专栏的博文:

cubeIDE开发, stm32的WIFI通信设计(基于AT指令)_py_free的博客-CSDN博客_uint8_t usart2_newdata=0

        wifi.h

#ifndef WIFI_WIFI_H_
#define WIFI_WIFI_H_

#include "stm32f1xx_hal.h" //HAL库文件声明
#include "../usart/usart.h"
#include "main.h"
#include <string.h>//用于字符串处理的库
#include <stdarg.h>
#include <stdlib.h>
#include "stdio.h"

extern UART_HandleTypeDef huart3;//声明UART2的HAL库结构体

void WIFI_printf (char *fmt, ...); //WIFI模块发送
void WIFI_TCP_SEND (char *fmt, ...);//在TCP模式下的发送数据(不处理返回状态的盲发)

#endif /* WIFI_WIFI_H_ */

        wifi.c,MCU向WIFI模组发送AT指令,本质上就是想UART3写入AT指令字符串。

#include "wifi.h"

//WIFI模块通信,使用UART3,这是专用的printf函数
//调用方法:WIFI_printf("123"); //向USART2发送字符123
void WIFI_printf (char *fmt, ...)
{
	char buff[USART3_REC_LEN+1];  //用于存放转换后的数据 [长度]
	uint16_t i=0;
	va_list arg_ptr;
	va_start(arg_ptr, fmt);
	vsnprintf(buff, USART3_REC_LEN+1, fmt, arg_ptr);//数据转换
	i=strlen(buff);//得出数据长度
	//printf("WIFI_printf len:%02d\r\n",i); //显示操作过程
	if(strlen(buff)>USART3_REC_LEN)i=USART3_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
	HAL_UART_Transmit(&huart3,(uint8_t *)buff,i,0xffff);//串口发送函数(串口号,内容,数量,溢出时间)
//	HAL_StatusTypeDef ret = HAL_UART_Transmit(&huart3,(uint8_t *)buff,i,0xffff);//串口发送函数(串口号,内容,数量,溢出时间)
	HAL_Delay(10);//等待
//	printf("WIFI_printf ret:%02d\r\n",(uint8_t)ret); //显示操作过程
    va_end(arg_ptr);
}
//WIFI模块在TCP模式下的数据发送:TCP发送的规定是先发AT+CIPSEND=数量,等待返回“>“后再发送数据内容。
//调用方法:WIFI_TCP_SEND("123\r\n"); //TCP方式发送字符123和回车换行
void WIFI_TCP_SEND (char *fmt, ...)
{
	char buff[USART3_REC_LEN+1];  //用于存放转换后的数据 [长度]
	uint16_t i=0;
	va_list arg_ptr;
	va_start(arg_ptr, fmt);
	vsnprintf(buff, USART3_REC_LEN+1, fmt, arg_ptr);//数据转换
	i=strlen(buff);//得出数据长度
	if(strlen(buff)>USART3_REC_LEN)i=USART3_REC_LEN;//如果长度大于最大值,则长度等于最大值(多出部分忽略)
	WIFI_printf("AT+CIPSEND=%d\r\n",i);//先发送AT指令和数据数量
	HAL_Delay(100);//等待WIFI模块返回">",此处没做返回是不是">"的判断。稳定性要求高的项目要另加判断。
    HAL_UART_Transmit(&huart3,(uint8_t *)buff,i,0xffff);//发送数据内容(串口号,内容,数量,溢出时间)
    va_end(arg_ptr);
}

//所有USART串口的中断回调函数HAL_UART_RxCpltCallback,统一存放在【USART.C】文件中。

        3.5 按键及LED灯驱动实现

        【1】在ICore目录下创建led目录,同时在led目录下创建led.h和led.c源文件.

        led.h

#ifndef LED_LED_H_
#define LED_LED_H_

#include "stm32f1xx_hal.h" //HAL库文件声明
#include "main.h" //IO定义与初始化函数在main.c文件中,必须引用

void LED_1(uint8_t a);//LED1独立控制函数(0为熄灭,其他值为点亮)
void LED_2(uint8_t a);//LED2独立控制函数(0为熄灭,其他值为点亮)
void LED_ALL(uint8_t a);//LED1~4整组操作函数(低4位的1/0状态对应4个LED亮灭,最低位对应LED1)
void LED_1_Contrary(void);//LED1状态取反
void LED_2_Contrary(void);//LED2状态取反

#endif /* LED_LED_H_ */

        led.c

#include "led.h"

void LED_1(uint8_t a)//LED1独立控制函数(0为熄灭,其他值为点亮)
{
	if(a)HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_SET);
	else HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_RESET);
}
void LED_2(uint8_t a)//LED2独立控制函数(0为熄灭,其他值为点亮)
{
	if(a)HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_SET);
	else HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_RESET);
}
void LED_ALL(uint8_t a)//LED1~2整组操作函数(低2位的1/0状态对应2个LED亮灭,最低位对应LED1)
{
	if(a&0x01)HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_SET);
	else HAL_GPIO_WritePin(GPIOB,LED1_Pin,GPIO_PIN_RESET);
	if(a&0x02)HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_SET);
	else HAL_GPIO_WritePin(GPIOB,LED2_Pin,GPIO_PIN_RESET);
}
void LED_1_Contrary(void){
	HAL_GPIO_WritePin(GPIOB,LED1_Pin,1-HAL_GPIO_ReadPin(GPIOB,LED1_Pin));
}
void LED_2_Contrary(void){
	HAL_GPIO_WritePin(GPIOB,LED2_Pin,1-HAL_GPIO_ReadPin(GPIOB,LED2_Pin));
}

        【2】在ICore目录下创建key目录,同时在key目录下创建key.h和key.c源文件.

        key.h

#ifndef KEY_KEY_H_
#define KEY_KEY_H_

#include "stm32f1xx_hal.h" //HAL库文件声明
#include "main.h" //IO定义与初始化函数在main.c文件中,必须引用

uint8_t KEY_1(void);//按键1
uint8_t KEY_2(void);//按键2

#endif /* KEY_KEY_H_ */

        key.c

#include "key.h"
#include "../delay/delay.h"

uint8_t KEY_1(void)
{
	uint8_t a;
	a=0;//如果未进入按键处理,则返回0
	if(HAL_GPIO_ReadPin(GPIOA,KEY1_Pin)==GPIO_PIN_RESET){//读按键接口的电平
		//HAL_Delay(20);//延时去抖动
		delay_us(20000);//延时去抖动
		if(HAL_GPIO_ReadPin(GPIOA,KEY1_Pin)==GPIO_PIN_RESET){ //读按键接口的电平
			a=1;//进入按键处理,返回1
		}
	}
	while(HAL_GPIO_ReadPin(GPIOA,KEY1_Pin)==GPIO_PIN_RESET); //等待按键松开
	delay_us(20000);//延时去抖动
	return a;
}

uint8_t KEY_2(void)
{
	uint8_t a;
	a=0;//如果未进入按键处理,则返回0
	if(HAL_GPIO_ReadPin(GPIOA,KEY2_Pin)==GPIO_PIN_RESET){//读按键接口的电平
		//HAL_Delay(20);//延时去抖动
		delay_us(20000);//延时去抖动
		if(HAL_GPIO_ReadPin(GPIOA,KEY2_Pin)==GPIO_PIN_RESET){ //读按键接口的电平
			a=1;//进入按键处理,返回1
		}
	}
	while(HAL_GPIO_ReadPin(GPIOA,KEY2_Pin)==GPIO_PIN_RESET); //等待按键松开
	delay_us(20000);//延时去抖动
	return a;
}

        【3】在ICore目录下创建delay目录,同时在delay目录下创建delay.h和delay.c源文件,实现自定义延时函数驱动。

        delay.h

#ifndef DELAY_DELAY_H_
#define DELAY_DELAY_H_

#include "stm32f1xx_hal.h" //HAL库文件声明
void delay_us(uint32_t us); //C文件中的函数声明

#endif /* DELAY_DELAY_H_ */

        delay.c

#include "delay.h"

void delay_us(uint32_t us) //利用CPU循环实现的非精准应用的微秒延时函数
{
    uint32_t delay = (HAL_RCC_GetHCLKFreq() / 8000000 * us); //使用HAL_RCC_GetHCLKFreq()函数获取主频值,经算法得到1微秒的循环次数
    while (delay--); //循环delay次,达到1微秒延时
}

        3.6 实现与阿里云物联网平台通信

        【1】在ICore目录下创建mqtt_aliyun目录,并创建mqtt子目录,将下载的MQTT开源代码放置该子目录,相关源文件如下:

         【2】在mqtt_aliyun目录,并创建hmac子目录,将下载的hmac相关代码放置该子目录,相关源文件如上图。

         【3】在mqtt_aliyun目录,并创建iot子目录,在该子目录下创建iot.h和iot.c源文件,实现IOT的连接、发布、订阅、心跳等功能。

        iot.h

#ifndef IOT_IOT_H
#define IOT_IOT_H

#include "stm32f1xx_hal.h"

extern uint16_t buflen; //临时缓存数量
extern unsigned char buf[200]; //临时缓存数组

uint8_t IOT_connect(void); //IOT物联网平台连接
void IOT_ping(void); //IOT物联网平台PING(心跳包)
uint8_t IOT_subscribe(void);//subscribe主题订阅(订阅成功后才能接收订阅消息)
uint8_t IOT_publish(char* payload);//publish主题发布(参数是发布信息内容,用双引号包含)

#endif

        iot.c,订阅与发布信息及三元组信息,在阿里云物联网平台设备页面查找。

#include "iot.h"
#include "../hmac/utils_hmac.h"
#include "../mqtt/MQTTPacket.h"
#include "../mqtt/transport.h"

//【三元组信息】在下方修改设置您的物联网云平台提供的三元组信息(手动复制正确信息到双引号内)

#define  PRODUCTKEY           "*****" //产品ID(ProductKey)【必须按您的实际情况修改】
#define  PRODUCTKEY_LEN       strlen(PRODUCTKEY) //产品ID长度
#define  DEVICENAME			  "*****" //设备名(DeviceName)【必须按您的实际情况修改】
#define  DEVICENAME_LEN       strlen(DEVICENAME) //设备名长度
#define  DEVICESECRE          "*****" //设备秘钥(DeviceSecret)【必须按您的实际情况修改】
#define  DEVICESECRE_LEN      strlen(DEVICESECRE) //设备秘钥长度

#define  TOPIC_SUBSCRIBE		"/*****/*****/user/get" //订阅权限的地址【必须按您的实际情况修改】
#define  TOPIC_QOS				0  //QoS服务质量数值(0/1)
#define  MSGID					1  //信息识别ID

#define  TOPIC_PUBLISH			"/*****/*****/user/update" //发布权限的地址【必须按您的实际情况修改】

#define  MQTTVERSION			4 //MQTT协议版本号(3表示V3.1,4表示V3.1.1)
#define  KEEPALIVEINTERVAL		120 //保活计时器,服务器收到客户端消息(含心跳包)的最大间隔(单位是秒)

uint16_t buflen=200;
unsigned char buf[200];

char ClientID[128];
uint8_t ClientID_len;

char Username[128];
uint8_t Username_len;

char Password[128];
uint8_t Password_len;

uint8_t IOT_connect()
{
	uint16_t a;
	uint32_t len;
	char temp[128];
	printf("开始连接云端服务器\r\n");
	MQTTPacket_connectData data = MQTTPacket_connectData_initializer;//配置部分可变头部的值
	buflen = sizeof(buf);
	memset(buf,0,buflen);
	memset(ClientID,0,128);//客户端ID的缓冲区全部清零
	sprintf(ClientID,"%s|securemode=3,signmethod=hmacsha1|",DEVICENAME);//构建客户端ID,并存入缓冲区
	memset(Username,0,128);//用户名的缓冲区全部清零
	sprintf(Username,"%s&%s",DEVICENAME,PRODUCTKEY);//构建用户名,并存入缓冲区

	Username_len = strlen(Username);

	memset(temp,0,128);//临时缓冲区全部清零
	sprintf(temp,"clientId%sdeviceName%sproductKey%s",DEVICENAME,DEVICENAME,PRODUCTKEY);//构建加密时的明文
	utils_hmac_sha1(temp,strlen(temp),Password,DEVICESECRE,DEVICESECRE_LEN);//以DeviceSecret为秘钥对temp中的明文,进行hmacsha1加密,结果就是密码,并保存到缓冲区中
	Password_len = strlen(Password);//计算用户名的长度

	printf("ClientId:%s\r\n",ClientID);
	printf("Username:%s\r\n",Username);
	printf("Password:%s\r\n",Password);

	//【重要参数设置】可修改版本号、保活时间
	data.MQTTVersion = MQTTVERSION; //MQTT协议版本号
	data.clientID.cstring = ClientID; //客户端标识,用于区分每个客户端xxx为自定义,后面为固定格式
	data.keepAliveInterval = KEEPALIVEINTERVAL; //保活计时器,定义了服务器收到客户端消息的最大时间间隔,单位是秒
	data.cleansession = 1; //该标志置1服务器必须丢弃之前保持的客户端的信息,将该连接视为“不存在”
	data.username.cstring = Username; //用户名 DeviceName&ProductKey
	data.password.cstring = Password; //密码,工具生成
	
	len = MQTTSerialize_connect(buf, buflen, &data);//构造连接的报文
	transport_sendPacketBuffer(0,buf, len);//发送连接请求

	unsigned char sessionPresent, connack_rc;
	a=0;
	while(MQTTPacket_read(buf, buflen, transport_getdata) != CONNACK || a>1000)//等待胳回复
	{
		HAL_Delay(10);//必要的延时等待
		a++;//超时计数加1
	}
	if(a>1000)NVIC_SystemReset();//当计数超时,则复位单片机

	while(MQTTDeserialize_connack(&sessionPresent, &connack_rc, buf, buflen) != 1 || connack_rc != 0);
	if(connack_rc != 0)
	{
		printf("连接回复:%uc\r\n",connack_rc);
	}
	printf("连接成功!\r\n");
	return 0;//执行成功返回0
}

void IOT_ping(void)//发送心跳包PING(保持与云服务器的连接)
{
	uint32_t len;
	len = MQTTSerialize_pingreq(buf, buflen); //计算数据长度
	transport_sendPacketBuffer(0, buf, len); //发送数据
	HAL_Delay(200);//必要的延时等待
	printf("发送心跳包Ping... ");
}

uint8_t IOT_subscribe(void)//subscribe主题订阅(订阅成功后才能接收订阅消息)
{
	uint32_t len;
	int req_qos = TOPIC_QOS;
	MQTTString topicString = MQTTString_initializer;//定义Topic结构体并初始化
	topicString.cstring = TOPIC_SUBSCRIBE;
	len = MQTTSerialize_subscribe(buf, buflen, 0, MSGID, 1, &topicString, &req_qos);//订阅发送数据编码
	transport_sendPacketBuffer(0, buf, len);
	HAL_Delay(100);//必要的延时等待
	if(MQTTPacket_read(buf, buflen, transport_getdata) == SUBACK) //等待订阅回复
	{
		unsigned short submsgid;
		int subcount;
		int granted_qos;
		MQTTDeserialize_suback(&submsgid, 1, &subcount, &granted_qos, buf, buflen);//回复的订阅确认数据解码
		if(granted_qos != 0) //qos不为0表示订阅成功
		{
			printf("订阅成功 GrantedQoS=%d\r\n", granted_qos);
			return 0; //订阅成功
		}
	}
	printf("订阅失败\r\n");
	return 1; //订阅失败
}

uint8_t IOT_publish(char* payload)//publish主题发布(参数是发布信息内容,用双引号包含)
{
	uint32_t len;
	MQTTString topicString = MQTTString_initializer;//定义Topic结构体并初始化
	topicString.cstring = TOPIC_PUBLISH;
	int payloadlen = strlen(payload);//用函数计算发布信息内容的长度
	printf("发布信息:%.*s\r\n", payloadlen, payload);
	//将要发送的信息payload通过MQTTSerialize_publish编码后用transport_sendPacketBuffer发送给云服务器
	len = MQTTSerialize_publish(buf, buflen, 0, 0, 0, 0, topicString,
								(unsigned char*)payload, payloadlen);//发布数据编码
	transport_sendPacketBuffer(0, buf, len); //发送编码好的最终数据
	HAL_Delay(100);//必要的延时等待
	return 1;
}

        【4】在mqtt_aliyun目录,并创建client子目录,在该子目录下创建client.h和client.c源文件, 发送AT指令给WIFI模组,再通过WIFI模组与阿里云物联网平台建立TCP链接。

        client.h

#include "stm32f1xx_hal.h"

uint8_t client_send_cmd(char *cmd, char *ack, uint16_t waittime);
uint8_t* client_check_cmd(uint8_t *str);
uint8_t client_Connect_IOTServer(void); //连接物联网云服务器IP
uint8_t client_Connect_AP(void); //连接AP路由器
uint8_t client_Connect_Server(void); //连接服务器
uint8_t client_quit_trans(void); //判断指令退出

#endif //CLIENT_CLIENT_H

        client.c,物联网平台域名链接信息在平台的设备页面上面在“MQTT链接信息”获得。该代码通过UART3与WIFI通信,通过AT指令操作WIFI模块,从而建立与物联网平台构建稳定的TCP链接,实现真正的数据发送与读取功能。路由器请填写自己环境的WIFI热点名称及热点密码。

#include "client.h"

#include "string.h"
#include "stdlib.h"
#include "../../usart/usart.h"
#include "../../wifi/wifi.h"

//【网络连接信息】在下方修改设置您的路由器热点和物联网平台IP地址+端口号信息(手动复制正确信息到双引号内)

#define SSID "py_zxj_link_7907_2.4G" //无线路由器热点名称【必须按您的实际情况修改】
#define PASS "*****"   //无线路由器热点密码【必须按您的实际情况修改】

#define IOT_DOMAIN_NAME "*****" //云服务器IP地址【必须按您的实际情况修改】
#define IOT_PORTNUM 	"1883" //云服务器端口号

uint8_t client_send_cmd(char *cmd, char *ack, uint16_t waittime) //client发送指令(底层函数)
{
	uint8_t res = 0;
	USART3_RX_STA = 0;
	memset(USART3_RX_BUF,0,USART3_REC_LEN); //将串口3的缓存空间清0
	WIFI_printf("%s\r\n", cmd); //调用WIFI模块专用的发送函数
	if(waittime) //需要等待应答
	{
		while(--waittime) //等待倒计时
		{
			HAL_Delay(10);//HAL库延时函数
			if(USART3_RX_STA&0x8000) //接收到期待的应答结果
			{
				if(client_check_cmd((uint8_t *)ack))
				{
					printf("回复信息:%s\r\n",(uint8_t *)ack);//反馈应答信息
					break; //得到有效数据
				}
				USART3_RX_STA=0; //串口3标志位清0
			} 
		}
		if(waittime==0)res=1;
	}
	return res;
}

uint8_t* client_check_cmd(uint8_t *str) //client检查指令(底层函数)
{
	char *strx=0;
	if(USART3_RX_STA&0X8000) //接收到一次数据了
	{
		USART3_RX_BUF[USART3_RX_STA&0X7FFF] = 0; //添加结束符
		printf("%s\r\n",(char*)USART3_RX_BUF);
		strx=strstr((const char*)USART3_RX_BUF,(const char*)str);
	} 
	return (uint8_t*)strx;
}

uint8_t client_quit_trans(void) //client退出判断(底层函数)
{
	while((USART3->SR&0X40)==0); //等待发送空
	USART3->DR='+';      
	HAL_Delay(15); //大于串口组帧时间(10ms)
	while((USART3->SR&0X40)==0); //等待发送空
	USART3->DR='+';      
	HAL_Delay(15); //大于串口组帧时间(10ms)
	while((USART3->SR&0X40)==0); //等待发送空
	USART3->DR='+';      
	HAL_Delay(500); //等待500ms
	return client_send_cmd("AT","OK",20); //退出透传判断
}

uint8_t client_Connect_IOTServer(void) //client连接到物联网平台服务器
{
//状态检测
	printf("准备配置模块\r\n");
	HAL_Delay(100);
	client_send_cmd("AT","OK",50);
	printf("准备退出透传模式\n");
	if(client_quit_trans())
	{
		printf("退出透传模式失败,准备重启\r\n");
		return 6;
	}else printf("退出透传模式成功\r\n");
	
	printf("准备关闭回显\r\n");
	if(client_send_cmd("ATE0","OK",50))
	{
		printf("关闭回显失败准备重启\r\n");
		return 1;
	}else printf("关闭回显成功\r\n");
	
	printf("查询模块是否在线\r\n");
	if(client_send_cmd("AT","OK",50))
	{
		printf("模块不在线准备重启\r\n");
		return 1;
	}else printf("设置查询在线成功\r\n");
	
//设置
	printf("准备设置STA模式\r\n");
	if(client_send_cmd("AT+CWMODE=1","OK",50))
	{
		printf("设置STA模式失败准备重启\r\n");
		return 1;
	}else printf("设置STA模式成功\r\n");
	
	printf("准备重启\r\n");
	if(client_send_cmd("AT+RST","OK",50))
	{
		printf("重启失败,准备重启\r\n");
		return 2;
	}else printf("重启成功,等待三秒\r\n");
	
	HAL_Delay(3000);//延时等待WIFI模块就绪

	printf("准备取消自动连接\r\n");
	if(client_send_cmd("AT+CWAUTOCONN=0","OK",50))
	{
		printf("取消自动连接失败,准备重启\r\n");
		return 3;
	}else printf("取消自动连接成功\r\n");
	
	printf("准备链接路由器\r\n");
	if(client_Connect_AP())
	{
		printf("连接路由器失败,等待重启\r\n");
		return 4;
	}else printf("连接路由器成功\r\n");
	
	HAL_Delay(4000);//延时等待WIFI模块就绪

	printf("准备开启DHCP\r\n");
	if(client_send_cmd("AT+CWDHCP=1,1","OK",100))
	{
		printf("开启DHCP失败,准备重启\r\n");
		return 7;
	}else printf("设置DHCP成功\r\n");

//TCP连接
	printf("设置为关闭多路连接\r\n");
	if(client_send_cmd("AT+CIPMUX=0","OK",100))
	{
		printf("关闭多路连接失败,准备重启\r\n");
		return 7;
	}else printf("设置关闭多路连接成功\r\n");

	printf("准备链接服务器\r\n");
	if(client_Connect_Server())
	{
		printf("连接服务器失败,等待重启\r\n");
		return 8;
	}else printf("连接服务器成功\r\n");
	
	printf("准备退出透传模式\n");
	if(client_quit_trans())
	{
		printf("退出透传模式失败,准备重启\r\n");
		return 6;
	}else printf("退出透传模式成功\r\n");
	
	printf("设置为透传模式\r\n");
	if(client_send_cmd("AT+CIPMODE=1","OK",50))
	{
		printf("设置透传失败,准备重启\r\n");
		return 6;
	}else printf("设置透传成功\r\n");

//发送数据
	printf("设置开启透传模式\r\n");
	if(client_send_cmd("AT+CIPSEND","OK",1000))
	{
		printf("开启透传失败,准备重启\r\n");
		return 9;
	}else printf("开启透传成功\r\n");
	
	USART3_RX_STA = 0;
	memset(USART3_RX_BUF,0,USART3_REC_LEN);
	
	return 0; //一切顺利返回0
}

uint8_t client_Connect_AP() //client连接AP设备(无线路由器)
{
	uint8_t i=10;
	char *p = (char*)malloc(50);//分配存储空间的指针

	sprintf((char*)p,"AT+CWJAP=\"%s\",\"%s\"",SSID,PASS);//发送连接AT指令
	while(client_send_cmd(p,"WIFI GOT IP",1000) && i)//循环判断等待连接AP的结果
	{
		printf("链接AP失败,尝试重新连接\r\n"); //连接失败的反馈信息
		i--;
	}
	free(p);//释放分配的空间和指针
	if(i) return 0;//执行成功返回0
	else return 1;//执行失败返回1
}

uint8_t client_Connect_Server() //client连接到服务器
{
	uint8_t i=10;
	char *p = (char*)malloc(50);//分配存储空间的指针
	sprintf((char*)p,"AT+CIPSTART=\"TCP\",\"%s\",\%s",IOT_DOMAIN_NAME,IOT_PORTNUM);
	while(client_send_cmd(p,"CONNECT",1000) && i)
	{
		printf("链接服务器失败,尝试重新连接\r\n");
		i--;
	}
	free(p);//释放分配的空间和指针
	if(i)return 0;//执行成功返回0
	else return 1;//执行失败返回1
}

        【5】实现MQTT应用调用UART3通信实现数据传输transport.c

        transport.c,本文只改写了transport_getdata函数和transport_sendPacketBuffer函数,实现了数据读取与发送。而MQTT的消息发布及订阅通过transport_getdata函数和transport_sendPacketBuffer函数来实现。

#include "stm32f1xx_hal.h"
#include "../../usart/usart.h"
#include "transport.h"

#if !defined(SOCKET_ERROR)
	/** error in socket operation */
	#define SOCKET_ERROR -1
#endif

/**
This simple low-level implementation assumes a single connection for a single thread. Thus, a static
variable is used for that connection.
On other scenarios, the user must solve this by taking into account that the current implementation of
MQTTPacket_read() has a function pointer for a function call to get the data to a buffer, but no provisions
to know the caller or other indicator (the socket id): int (*getfn)(unsigned char*, int)
*/

int transport_sendPacketBuffer(int sock, unsigned char* buf, int buflen)
{
	/*本文改写部分开始*/
	USART3_RX_STA = 0;
	memset(USART3_RX_BUF,0,USART3_REC_LEN);
	HAL_UART_Transmit(&huart3, buf, buflen,1000);//调用串口3发送HAL库函数
	return buflen;
    /*本文改写部分结束*/
}

int transport_getdata(unsigned char* buf, int count)
{
    /*本文改写部分开始*/
	memcpy(buf, (const char*)USART3_RX_BUF, count);
	USART3_RX_STA = 0; //接收标志位清0
	memset(USART3_RX_BUF,0,USART3_REC_LEN);//缓存清0
	return count;
    /*本文改写部分结束*/
}

int transport_getdatanb(void *sck, unsigned char* buf, int count)
{
	return 0;
}

/**
return >=0 for a socket descriptor, <0 for an error code
@todo Basically moved from the sample without changes, should accomodate same usage for 'sock' for clarity,
removing indirections
*/
int transport_open(char* addr, int port)
{
	return 0;
}

int transport_close(int sock)
{
	return 0;
}

        3.7 MQTT业务逻辑实现

        在main.c文件中,添加各自定义驱动的头文件

/* USER CODE BEGIN Includes */
#include "../../ICore/led/led.h"
#include "../../ICore/key/key.h"
#include "../../ICore/delay/delay.h"
#include "../../ICore/print/print.h"//用于printf函数串口重映射
#include "../../ICore/usart/usart.h"
#include "../../ICore/wifi/wifi.h"
#include "../../ICore/mqtt_aliyun/client/client.h"
#include "../../ICore/mqtt_aliyun/mqtt/MQTTPacket.h"
#include "../../ICore/mqtt_aliyun/mqtt/transport.h"
#include "../../ICore/mqtt_aliyun/iot/iot.h"
#include "../../ICore/tim/tim.h"
/* USER CODE END Includes */

        在mian函数中,初始化驱动应用

  /* USER CODE BEGIN 1 */
	uint16_t a=0,b=0;
	int t,qos,payloadinlen; //为下面即将解析的消息定义所需变量
	unsigned char dup,retained;
	unsigned short msgid;
	unsigned char* payloadin;
	MQTTString receiveTopic;
  /* USER CODE END 1 */
-------------------------------------------

  /* USER CODE BEGIN 2 */
  ResetPrintInit(&huart1);//将printf()函数映射到UART1串口上
  HAL_UART_Receive_IT(&huart1,(uint8_t *)&USART1_NewData,1);//开启串口1接收中断
  HAL_UART_Receive_IT(&huart3,(uint8_t *)&USART3_NewData,1); //再开启串口3接收中断
  HAL_TIM_Base_Start_IT(&htim2);//开启定时器中断(必须开启才能进入中断处理回调函数)

//  while(client_Connect_AP());//连接AP无线路由器热点(热点参数在client.h。WIFI模块已保存热点时可屏蔽)
  while(client_Connect_IOTServer());//AT指令连接TCP连接云服务器(IP和端口参数在client.h文件内修改设置)
  while(IOT_connect());//用MQTT协议+三元组信息连接阿里云物联网平台(三元组参数在iot.h文件内修改设置)
  printf("订阅云服务器\r\n");
  HAL_Delay(100);//等待
  IOT_subscribe();//主题订阅(订阅成功后才能接收订阅消息)
  a=0xFFF0; //强制发送心跳包的计数溢出,立即重发心跳包
  LED_1(0);//LED状态初始化 关
  LED_2(0);
  /* USER CODE END 2 */

        在main函数循环体中,实现MQTT连接、订阅、发布、心跳送、接收数据解释出来等功能。

  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  //循环发送心跳包,以保持设备在线
	  HAL_Delay(10);//主循环的间隔延时(防止刷新过快)
	  a++;//计算加1
	  if(a>1000){ //每1000*10毫秒延时发送一次Ping心跳包(保保持与云服务器的连接)
			a=0;//计算标志清0
			IOT_ping();//发送Ping心跳包
			if(MQTTPacket_read(buf, buflen, transport_getdata)==PINGRESP){//判断心跳包是不回复确认
				printf("心跳成功\r\n"); //回复0xD0,0x00时表示心跳成功的回复
			}else {
				printf("心跳失败\r\n");//无回复表示失败
				a=0xFFF0; //强制发送心跳包的计数溢出,立即重发心跳包
				b++;//重启计数加1
				if(b>20) //如果快速发送心跳包20次后无回复,则复位WIFI模块重新连接
				{
					while(client_Connect_IOTServer());//AT指令连接TCP连接云服务器(IP和端口参数在client.h文件内修改设置)
					while(IOT_connect());//用MQTT协议+三元组信息连接阿里云物联网平台(三元组参数在iot.h文件内修改设置)
					a=0;b=0;//计算标志清0
				}
			}
			USART3_RX_STA = 0;//串口3接收标志位清0
	  }
	  //接收云端的订阅消息
	  if(USART3_RX_STA&0x8000) //判断云服务器发布的消息
	  {
		  switch (USART3_RX_BUF[0]/16){//判断接收到的报文类型
			case PUBLISH:
				t = MQTTDeserialize_publish( //对接收的MQTT原始数据进行解码(返回1表示成功,其他值表示错误)
					  &dup, //【得出】重发标志位(0首发,1早前报文的重发)
					  &qos, //【得出】服务质量等级(0最多分发一次,1至少分发一次,2只分发一次)
					  &retained, //【得出】保留位参数
					  &msgid, //【得出】消息ID
					  &receiveTopic, //【得出】订阅主题名
					  &payloadin, //【得出】消息内容
					  &payloadinlen, //【得出】消息长度
					  USART3_RX_BUF, USART3_RX_STA&0x7FFF); //【输入】原始数据缓存(数组+数量)
				if(t){//如果数据正确
				  printf("接收到主题:%.*s  ", receiveTopic.lenstring.len, receiveTopic.lenstring.data);//显示接收主题
				  printf("消息内容:%.*s  ", payloadinlen, payloadin);//显示消息内容的字符串
				  printf("QoS:%d\r\n", qos);//显示接收QoS
				  USART3_RX_STA = 0;//标志位清0
				  //数据控制开发板的程序
				  if(strstr((const char*)payloadin,(const char*)"LED1 ON"))//比对信息内容是不是LED1 ON
				  {
					  LED_1(1);
					  IOT_publish("LED1 ON OK!");//publish主题发布(发送到云平台)
				  }else if(strstr((const char*)payloadin,(const char*)"LED1 OFF"))//同上
				  {
					  LED_1(0);
					  IOT_publish("LED1 OFF OK!");//publish主题发布(发送到云平台)
				  }else if(strstr((const char*)payloadin,(const char*)"LED2 ON"))//同上
				  {
					  LED_2(1);
					  IOT_publish("LED2 ON OK!");//publish主题发布(发送到云平台)
				  }else if(strstr((const char*)payloadin,(const char*)"LED2 OFF"))//同上
				  {
					  LED_2(0);
					  IOT_publish("LED2 OFF OK!");//publish主题发布(发送到云平台)
				  }
				}else{
				  printf("接收订阅消息时出错\r\n");//接收错误时的显示
				}
				break;
			case CONNACK: //连接报文确认
				//插入您的处理程序(也可空置)
				break;
			case SUBACK: //订阅请求报文确认
				//插入您的处理程序(也可空置)
				break;
			case UNSUBACK: //取消订阅报文确认
				//插入您的处理程序(也可空置)
				break;
			default:
				//冗余语句
				break;
		  }
		  USART3_RX_STA = 0;//串口3接收标志位清0
	  }
//按键操作
	  if(KEY_1())//按下KEY1判断【连接云服务器并订阅主题】
	  {
//		  while(client_Connect_AP());//连接AP无线路由器热点(热点参数在client.h。WIFI模块已保存热点时可屏蔽)
		  while(client_Connect_IOTServer());//连接TCP连接云服务器(IP和端口参数在client.h文件内修改设置)
		  while(IOT_connect());//用MQTT协议+三元组信息连接阿里云物联网平台(三元组参数在iot.h文件内修改设置)
		  printf("订阅云服务器\r\n");
		  IOT_subscribe();//主题订阅(订阅成功后才能接收订阅消息)
		  HAL_Delay(100);//等待
	  }

	  if(KEY_2())//按下KEY2判断【向服务器发布信息】
	  {
		  IOT_publish("TEST MQTT&WIFI");//publish主题发布(参数是发布信息内容,用双引号包含)
		  HAL_Delay(100);//等待
	  }
    /* USER CODE END WHILE */

四、编译及测试

        【1】编译,设置输出格式支持

         【2】编译通过

         【3】烧写程序:

         【4】串口工具查看输出log输出内容

         【5】按键KEY2测试发送数据成功在物联网平台接收

         【6】订阅主题消息测试,成功接收到平台发布的消息

 五、补充

【1】源码:https://download.csdn/download/py8105/87302139

【2】请自行调整三元组及链接信息,源码涉及的本人的产品及设备 将会不久删除。需要自己先注册阿里云账户,并开通阿里云物联网平台

【3】如果在线设备数量超过50台或实际项目需要阿里云物联网平台部署,将就不要使用公共案例。这里有阿里云的云产品通用代金券链接,新注册账号可以去领取,在购买某些产品可以使用代金券省一笔费用:https://www.aliyun/minisite/goods?userCode=pfb80n4a

本文标签: 阿里二十八模块案例平台