STM32F103_ESP8266基于RTOS移植MQTT

STM32F103_ESP8266基于RTOS移植MQTT

MQTT移植参考韦东山老师视频课:

https://video.100ask.net/p/t_pc/course_pc_detail/big_column/p_627a3ae7e4b01a4851fd107f

一、准备工作

  • 硬件基于洋桃IOT开发板,STM32F103C8T6;

  • 软件基于FreeRTOS_ESP8266_MQTT_SourceCode,已经在CubeMX上配置好FreeRTOS;

  • MQTT使用杰杰作者的MATT源码mqttclient,地址是“https://github.com/jiejieTop/mqttclient.git”

在这里插入图片描述

  1. 图接MCU串口1

在这里插入图片描述

  1. 使用STM32F103_RTOS_MQTT工程在MCU上测试ESP8266,连接WIFI,连接TCP服务器并收发数据:
ESP8266 AT命令:
1、WIFI模式配置,AP模式
AT+CWMODE=1
2、列出当前热点
AT+CWLAP
5、接入热点
AT+CWJAP_DEF="TP-LINK_4522","1234567890"
4、设置连接模式
AT+CIPMUX=0
5、创建TCP连接
AT+CIPSTART="TCP","112.125.89.8",44324
6、发送TCP数据
AT+CIPSEND=5
7、断开TCP连接
AT+CIPCLOSE
8、断开热点
AT+CWQAP

在这里插入图片描述

二、移植mqttclient代码

  1. mqttclient源码目录复制到FreeRTOS_ESP8266_MQTT工程

在这里插入图片描述

  1. mqttclient源码中保留‘common’、‘mqtt’、‘mqttclient’、‘network’、‘platform’目录

在这里插入图片描述

  1. 将各目录中的代码添加到keil工程中(.c和.h)

在这里插入图片描述

‘common’中仅添加mqtt_list.c和random.c文件;‘network’中仅添加nettype_tcp.c、nettype_tls.c、network.c文件

三、编译包含mqttclient的工程

mqttclient源码增加到工程中后对工程进行编译,记录编译报错及解决过程:

  1. 找不到头文件"mqtt_config.h"

在这里插入图片描述

头文件"mqtt_config.h"在test目录下,注释后继续编译

在这里插入图片描述

  1. 找不到mbedtls系列头文件

在这里插入图片描述

自定义MQTT_NETWORK_TYPE_NO_TLS宏,再次编译

在这里插入图片描述

  1. 找不到lwip系列头文件

在这里插入图片描述

韦东山老师移植完成的代码中没有找到lwip系列头文件被注释掉了,注释后继续编译

  1. size_t类型未定义

在这里插入图片描述

包含头文件<stdio.h>,再次编译

在这里插入图片描述

  1. socklen_t类型未定义

在这里插入图片描述

同样增加一个socklen_t的定义,再次编译

在这里插入图片描述

  1. platform_net_socket.c文件中各个接口报错

在这里插入图片描述

删除此文件中的各个接口,后期使用ESP8266 AT命令来实现各个接口,再次编译

在这里插入图片描述

  1. 懂定义参数少了一个括号

在这里插入图片描述

在此宏定义的2个文件处修改一下,再次编译

在这里插入图片描述

在这里插入图片描述

  1. 找不到头文件plooc_class.h

在这里插入图片描述

将plooc_class.h文件路径添加到工程中,再次编译

在这里插入图片描述

  1. 仅支持gnu模式(匿名结构体)错误,语法不支持

在这里插入图片描述

在plooc_class.h中自己定义增加宏定义PLOOC_CFG_REMOVE_MEMORY_LAYOUT_BOUNDARY___USE_WITH_CAUTION___编译通过

参考文章:https://blog.csdn.net/studyingdda/article/details/135428348?spm=1001.2014.3001.5501

在这里插入图片描述

四、编写ESP8266驱动程序

ESP8266驱动程序分成4层,用于隔离底层硬件和网络层:

  • platform_net_socket.c:执行什么AT命令才能连接、收、发网络数据
  • ESP8266:提供AT命令函数
  • UART驱动抽象层:执行UART的写、读(从buffer读)
  • UART硬件驱动:发送UART数据,接收UART数据存入buffer

在这里插入图片描述

1、ESP8266 AT命令代码框架

ESP8266 AT命令发送和接收处理代码中有2个任务,2个信号量:

  • AT命令发送任务:发送完AT命令后以一个超时时间等待信号量1
  • AT命令解析任务:永远等待信号量2,获取信号量2后读取环形buffer中的AT命令响应数据并解析,解析完毕后释放信号量1
  • UART3中断:接收中断中接收ESP8266响应的数据,并存放进环形缓冲区,然后释放信号量2

在这里插入图片描述

2、UART硬件和抽象层相关代码

不使用CubeMX自动生成的USART3_IRQHandler代码,自己定义uart3的中断接收函数

  1. 在mqttclient文件夹里创建hardware文件夹存放uart3硬件和抽象层相关代码,stm32_uart3.c和stm32_uart3.h

在这里插入图片描述

stm32_uart3.h代码:

#ifndef _STM32_UART3_H
#define _STM32_UART3_H

void USART3_Write(char *buf, int len);
void USART3_Read(char *c, int timeout);

#endif

stm32_uart3.c代码:

#include "driver_usart.h"
#include <stdio.h>
#include <ring_buffer.h>

static ring_buffer uart3_buffer;   //创建uart3的环形缓冲buffer
extern UART_HandleTypeDef huart3;

void USART3_IRQHandler(void)
{
	/* 如果发生的是RX中断
	 * 把数据读出来, 存入环形buffer
	 */

	uint32_t isrflags	= READ_REG(huart3.Instance->SR);
	uint32_t cr1its 	= READ_REG(huart3.Instance->CR1);
	char c;
	
    /* UART in mode Receiver -------------------------------------------------*/
    /* USART_SR_RXNE接收不为空; USART_CR1_RXNEIE使能接收中断. */
    if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
    {
      c = huart3.Instance->DR;   //将DR数据寄存器中的数据放入变量c
	  ring_buffer_write(c, &uart3_buffer);   //将c写入环形缓冲区
      return;
    }

}

void USART3_Write(char *buf, int len)
{
	int i = 0;
	while (i < len)
	{
        /* 等待数据发送寄存器空 */
		while ((huart3.Instance->SR & USART_SR_TXE) == 0);
		huart3.Instance->DR = buf[i];
		i++;
	}
}

void USART3_Read(char *c, int timeout)
{
	while (1)
	{
		if (0 == ring_buffer_read((unsigned char *)c, &uart3_buffer))
			return;
		else
		{
		}
	}		
}

  1. 在ModuleDrivers文件夹中增加UART和环形缓冲区相关代码,driver_usart.c、driver_usart.h、ring_buffer.c、ring_buffer.h

在这里插入图片描述

ring_buffer.h代码

/*  Copyright (s) 2019 深圳百问网科技有限公司
 *  All rights reserved
 * 
 * 文件名称:ring_buffer.h
 * 摘要:
 *  
 * 修改历史     版本号        Author       修改内容
 *--------------------------------------------------
 * 2021.8.21      v01         百问科技      创建文件
 *--------------------------------------------------
*/
#ifndef __RING_BUFFER_H
#define __RING_BUFFER_H

#include "stm32f1xx_hal.h"

#define BUFFER_SIZE 1024        /* 环形缓冲区的大小 */
typedef struct
{
    volatile unsigned int pW;           /* 写地址 */
    volatile unsigned int pR;           /* 读地址 */
    unsigned char buffer[BUFFER_SIZE];  /* 缓冲区空间 */
} ring_buffer;

/*
 *  函数名:void ring_buffer_init(ring_buffer *dst_buf)
 *  输入参数:dst_buf --> 指向目标缓冲区
 *  输出参数:无
 *  返回值:无
 *  函数作用:初始化缓冲区
*/
extern void ring_buffer_init(ring_buffer *dst_buf);

/*
 *  函数名:void ring_buffer_write(unsigned char c, ring_buffer *dst_buf)
 *  输入参数:c --> 要写入的数据
 *            dst_buf --> 指向目标缓冲区
 *  输出参数:无
 *  返回值:无
 *  函数作用:向目标缓冲区写入一个字节的数据,如果缓冲区满了就丢掉此数据
*/
extern void ring_buffer_write(unsigned char c, ring_buffer *dst_buf);

/*
 *  函数名:int ring_buffer_read(unsigned char *c, ring_buffer *dst_buf)
 *  输入参数:c --> 指向将读到的数据保存到内存中的地址
 *            dst_buf --> 指向目标缓冲区
 *  输出参数:无
 *  返回值:读到数据返回0,否则返回-1
 *  函数作用:从目标缓冲区读取一个字节的数据,如果缓冲区空了返回-1表明读取失败
*/
extern int ring_buffer_read(unsigned char *c, ring_buffer *dst_buf);

#endif /* __RING_BUFFER_H */

ring_buffer.c代码

/*  Copyright (s) 2019 深圳百问网科技有限公司
 *  All rights reserved
 * 
 * 文件名称:ring_buffer.c
 * 摘要:
 *  
 * 修改历史     版本号        Author       修改内容
 *--------------------------------------------------
 * 2021.8.21      v01         百问科技      创建文件
 *--------------------------------------------------
*/

#include "ring_buffer.h"


/*
 *  函数名:void ring_buffer_init(ring_buffer *dst_buf)
 *  输入参数:dst_buf --> 指向目标缓冲区
 *  输出参数:无
 *  返回值:无
 *  函数作用:初始化缓冲区
*/
void ring_buffer_init(ring_buffer *dst_buf)
{
    /* 唤醒缓冲区初始化,将读写指针设置为0 */
    dst_buf->pW = 0;
    dst_buf->pR = 0;
}

/*
 *  函数名:void ring_buffer_write(unsigned char c, ring_buffer *dst_buf)
 *  输入参数:c --> 要写入的数据
 *            dst_buf --> 指向目标缓冲区
 *  输出参数:无
 *  返回值:无
 *  函数作用:向目标缓冲区写入一个字节的数据,如果缓冲区满了就丢掉此数据
*/
void ring_buffer_write(unsigned char c, ring_buffer *dst_buf)
{
    /* 获取环形缓冲区写指针的下一个位置 */
    int i = (dst_buf->pW + 1) % BUFFER_SIZE;
    /* 
    如果环形缓冲区写指针的下一个位置和读指针不相等代表环形缓冲区未写满,若写满则数据直 
   接丢弃 */
    if(i != dst_buf->pR)    // 环形缓冲区没有写满
    {
        /* 将字符C写到唤醒缓冲区写指针的位置 */
        dst_buf->buffer[dst_buf->pW] = c;
        /* 将环形缓冲区的写指针更新为下一个写位置 */
        dst_buf->pW = i;
    }
}

/*
 *  函数名:int ring_buffer_read(unsigned char *c, ring_buffer *dst_buf)
 *  输入参数:c --> 指向将读到的数据保存到内存中的地址
 *            dst_buf --> 指向目标缓冲区
 *  输出参数:无
 *  返回值:读到数据返回0,否则返回-1
 *  函数作用:从目标缓冲区读取一个字节的数据,如果缓冲区空了返回-1表明读取失败
*/
int ring_buffer_read(unsigned char *c, ring_buffer *dst_buf)
{
    /* 如果环形缓冲区当前的读指针与写指针位置相同表示当前环形缓冲区为空 */
    if(dst_buf->pR == dst_buf->pW)
    {
        return -1;
    }
    else
    {
        /* 将当前环形缓冲区读指针位置的数据传给字符c的地址 */
        *c = dst_buf->buffer[dst_buf->pR];
        /* 将环形缓冲区读指针的位置更新为下一个读位置 */
        dst_buf->pR = (dst_buf->pR + 1) % BUFFER_SIZE;
        return 0;
    }
}

driver_usart.h代码

/*  Copyright (s) 2019 深圳百问网科技有限公司
 *  All rights reserved
 * 
 * 文件名称:driver_usart.h
 * 摘要:
 *  
 * 修改历史     版本号        Author       修改内容
 *--------------------------------------------------
 * 2020.6.6      v01        百问科技      创建文件
 *--------------------------------------------------
*/

#ifndef __DRIVER_USART_H
#define __DRIVER_USART_H

#include "stm32f1xx_hal.h"

/*
 *  函数名:void EnableDebugIRQ(void)
 *  输入参数:无
 *  输出参数:无
 *  返回值:无
 *  函数作用:使能USART1的中断
*/
extern void EnableDebugIRQ(void);
extern void EnableUART3IRQ(void);

/*
 *  函数名:void DisableDebugIRQ(void)
 *  输入参数:无
 *  输出参数:无
 *  返回值:无
 *  函数作用:失能USART1的中断
*/
extern void DisableDebugIRQ(void);

#endif /* __DRIVER_USART_H */

driver_usart.h代码

/*  Copyright (s) 2019 深圳百问网科技有限公司
 *  All rights reserved
 * 
 * 文件名称:driver_usart.c
 * 摘要:
 *  
 * 修改历史     版本号        Author       修改内容
 *--------------------------------------------------
 * 2021.8.21      v01         百问科技      创建文件
 *--------------------------------------------------
*/

#include "driver_usart.h"
#include "usart.h"
#include "main.h"
#include "ring_buffer.h"
#include <stdio.h>

static volatile uint8_t txcplt_flag = 0;    // 发送完成标志,1完成0未完成
static volatile uint8_t rxcplt_flag = 0;    // 接收完成标志,1完成0未完成

static volatile uint8_t rx_data = 0;

/*
 *  函数名:void EnableDebugIRQ(void)
 *  输入参数:无
 *  输出参数:无
 *  返回值:无
 *  函数作用:使能USART1的中断
*/
void EnableDebugIRQ(void)
{
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 1);    // 设置USART1中断的优先级
    HAL_NVIC_EnableIRQ(USART1_IRQn);            // 使能USART1的中断
    
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_TC | UART_IT_RXNE);   // 使能USRAT1的发送和接收中断
}


void EnableUART3IRQ(void)
{
    HAL_NVIC_SetPriority(USART3_IRQn, 15, 0);    // 设置USART3中断的优先级
    HAL_NVIC_EnableIRQ(USART3_IRQn);            // 使能USART3的中断

	huart3.Instance->SR &= ~(USART_SR_RXNE);   //清除数据接收寄存器,否则使能中断后会立刻进入一次中断
    __HAL_UART_ENABLE_IT(&huart3, UART_IT_RXNE);   // 使能USRAT3的接收中断
}

/*
 *  函数名:void DisableDebugIRQ(void)
 *  输入参数:无
 *  输出参数:无
 *  返回值:无
 *  函数作用:失能USART1的中断
*/
void DisableDebugIRQ(void)
{
    __HAL_UART_DISABLE_IT(&huart1, UART_IT_TC | UART_IT_RXNE);      // 失能USRAT1的发送和接收中断
    
    HAL_NVIC_DisableIRQ(USART1_IRQn);   // 失能USART1的中断
}

/*
 *  函数名:int fputc(int ch, FILE *f)
 *  输入参数:ch --> 要输出的数据
 *  输出参数:无
 *  返回值:无
 *  函数作用:printf/putchar 标准输出函数的底层输出函数
*/
int fputc(int ch, FILE *f)
{
    txcplt_flag = 0;
    HAL_UART_Transmit_IT(&huart1, (uint8_t*)&ch, 1);
    while(txcplt_flag==0);
	return ch;
}

/*
 *  函数名:int fgetc(FILE *f)
 *  输入参数:
 *  输出参数:无
 *  返回值:接收到的数据
 *  函数作用:scanf/getchar 标准输出函数的底层输出函数
*/
int fgetc(FILE *f)
{
    char c = 0;
    while(ring_buffer_read((unsigned char *)&c, &test_buffer) != 0);
    return c;
}

/*
 *  函数名:void USART1_IRQHandler(void)
 *  输入参数:无
 *  输出参数:无
 *  返回值:无
 *  函数作用:USART1的中断服务函数
*/
void USART1_IRQHandler(void)
{
    unsigned char c = 0;
    if((USART1->SR &(1<<5)) != 0)   // 判断USART1的状态寄存器的第五位即RXNE位是否被置位
    {
        c = USART1->DR; // RXNE=1,表明DR寄存器有值,就将它读出来保存到临时变量中;
        ring_buffer_write(c, &test_buffer); // 将数据保存到环形缓冲区中
    }
    HAL_UART_IRQHandler(&huart1);   // HAL库中的UART统一中断服务函数,通过形参判断是要处理谁的中断
}

/*
 *  函数名:void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
 *  输入参数:huart --> UART的设备句柄,用以指明UART设备是哪一个UART
 *  输出参数:无
 *  返回值:无
 *  函数作用:HAL库中的UART接收完成回调函数
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)   // 判断进来的是否是USART1这个UART设备
    {
        rxcplt_flag = 1;    // 进入此回调函数表明接收指定长度的数据已经完成,将标志置一
    }
}

/*
 *  函数名:void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
 *  输入参数:huart --> UART的设备句柄,用以指明UART设备是哪一个UART
 *  输出参数:无
 *  返回值:无
*  函数作用:HAL库中的UART发送完成回调函数
*/
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart->Instance == USART1)   // 判断进来的是否是USART1这个UART设备
    {
        txcplt_flag = 1;    // 进入此回调函数表明发送指定长度的数据已经完成,将标志置一
    }
}

在main函数中使用USART3_Write(“AT\r\n”, 4);来测试MCU UART3能否与ESP8266正常通信:

在这里插入图片描述

在这里插入图片描述

3、AT命令发送和解析代码

AT

// 1. 配置 WiFi 模式
AT+CWMODE=3						//	softAP+station	mode

// 2. 连接路由器
AT+CWJAP="SSID","password"		//	SSID	and	password	of	router

// 3. 查询 ESP8266 设备的 IP 地址
AT+CIFSR

// 响应
+CIFSR:APIP,"192.168.4.1"
+CIFSR:APMAC,"1a:fe:34:a5:8d:c6"
+CIFSR:STAIP,"192.168.3.133"
+CIFSR:STAMAC,"18:fe:34:a5:8d:c6"
OK

// 4. ESP8266 设备作为 TCP client 连接到服务器
AT+CIPSTART="TCP","192.168.3.116",8080			 //protocol,	server	IP	and	port

// 5. ESP8266 设备向服务器器发送数据
AT+CIPSEND=4				//	set	date	length	which	will	be	sent,		such	as	4	bytes	
>test						//	enter	the	data,		no	CR

// 响应
Recv	4	bytes
SEND	OK

// 6. 当 ESP8266 设备接收到服务器器发来的数据,将提示如下信息:
+IPD,n:xxxxxxxxxx				//	received	n	bytes,		data=xxxxxxxxxxx	
  1. 在mqttclient文件夹中创建hal文件夹,包括at_uart_hal.c和at_uart_hal.h文件,作用是对USART3_Write(buf, len)和USART3_Read(c, timeout)函数封装一层供上层AT命令函数收发使用,避免更换硬件后上层需要修改

在这里插入图片描述

at_uart_hal.h代码:

#ifndef _AT_UART_H
#define _AT_UART_H

void HAL_AT_Send(char *buf, int len);
void HAL_AT_Secv(char *c, int timeout);

#endif

at_uart_hal.c代码:

#include <stm32_uart3.h>


void HAL_AT_Send(char *buf, int len)
{
	USART3_Write(buf, len);
}

void HAL_AT_Secv(char *c, int timeout)
{
	/* 从环形缓冲区中得到数据 */
	/* 无数据则阻塞 */

	USART3_Read(c, timeout);
}

  1. 对driver_usart.c和driver_usart.h进行修改,目的是使读uart3数据线程安全,当有一个任务使用USART3_Read还未读到数据时先挂起(等待互斥锁),当uart3串口接收中断接收到数据后再释放唤醒此任务(释放互斥锁)继续读取数据,防止在此任务还未读到数据时其他任务再次调用USART3_Read读取uart3数据

driver_usart.h代码:

#ifndef _STM32_UART3_H
#define _STM32_UART3_H

void USART3_Write(char *buf, int len);
void UART3_Lock_Init(void);   //线程安全: USART3_Read没有读到数据时先挂起,读到数据后其他接口才能够调用USART3_Read
void USART3_Read(char *c, int timeout);

#endif

driver_usart.c代码:

#include "driver_usart.h"
#include <stdio.h>
#include <platform_mutex.h>
#include <ring_buffer.h>
  
static ring_buffer uart3_buffer;   //创建uart3的环形缓冲buffer

extern UART_HandleTypeDef huart3;

static platform_mutex_t uart_recv_mutex;

void USART3_IRQHandler(void)
{
	/* 如果发生的是RX中断
	 * 把数据读出来, 存入环形buffer
	 */

	uint32_t isrflags	= READ_REG(huart3.Instance->SR);
	uint32_t cr1its 	= READ_REG(huart3.Instance->CR1);
	char c;
	
    /* UART in mode Receiver -------------------------------------------------*/
    /* USART_SR_RXNE接收不为空; USART_CR1_RXNEIE使能接收中断. */
    if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
    {
      c = huart3.Instance->DR;   //将DR数据寄存器中的数据放入变量c
	  ring_buffer_write(c, &uart3_buffer);   //将c写入环形缓冲区
	  platform_mutex_unlock_from_isr(&uart_recv_mutex);	  
      return;
    }

}

void UART3_Lock_Init(void)
{
	platform_mutex_init(&uart_recv_mutex);
	platform_mutex_lock(&uart_recv_mutex);  // mutex = 0
    ring_buffer_init(&uart3_buffer);
}

void USART3_Write(char *buf, int len)
{
	int i = 0;
	while (i < len)
	{
        /* 等待数据发送寄存器空 */
		while ((huart3.Instance->SR & USART_SR_TXE) == 0);
		huart3.Instance->DR = buf[i];
		i++;
	}
}

void USART3_Read(char *c, int timeout)
{
	while (1)
	{
		if (0 == ring_buffer_read((unsigned char *)c, &uart3_buffer))
			return;
		else
		{
			platform_mutex_lock_timeout(&uart_recv_mutex, timeout);
		}
	}		
}

其中platform_mutex_lock_timeout函数和platform_mutex_unlock_from_isr函数在杰杰MQTT代码中没有实现,因此自己实现:

platform_mutex.h代码:

/*
 * @Author: jiejie
 * @Github: https://github.com/jiejieTop
 * @Date: 2019-12-15 18:31:33
 * @LastEditTime: 2020-04-27 17:04:46
 * @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
 */
#ifndef _PLATFORM_MUTEX_H_
#define _PLATFORM_MUTEX_H_

#include "FreeRTOS.h"
#include "semphr.h"

typedef struct platform_mutex {
    SemaphoreHandle_t mutex;
} platform_mutex_t;

int platform_mutex_init(platform_mutex_t* m);
int platform_mutex_lock(platform_mutex_t* m);
int platform_mutex_lock_timeout(platform_mutex_t* m, int timeout);   /* DCA add */
int platform_mutex_trylock(platform_mutex_t* m);
int platform_mutex_unlock(platform_mutex_t* m);
int platform_mutex_unlock_from_isr(platform_mutex_t* m);   /* DCA add */

int platform_mutex_destroy(platform_mutex_t* m);

#endif

platform_mutex.c代码:

/*
 * @Author: jiejie
 * @Github: https://github.com/jiejieTop
 * @Date: 2019-12-15 18:27:19
 * @LastEditTime: 2020-04-27 22:22:27
 * @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
 */
#include "platform_mutex.h"

int platform_mutex_init(platform_mutex_t* m)
{
    m->mutex = xSemaphoreCreateMutex();
    return 0;
}

int platform_mutex_lock(platform_mutex_t* m)
{
    return xSemaphoreTake(m->mutex, portMAX_DELAY);
}

/* DCA add */
int platform_mutex_lock_timeout(platform_mutex_t* m, int timeout)
{
    return xSemaphoreTake(m->mutex, timeout);
}



int platform_mutex_trylock(platform_mutex_t* m)
{
    return xSemaphoreTake(m->mutex, 0);
}

int platform_mutex_unlock(platform_mutex_t* m)
{
    return xSemaphoreGive(m->mutex);
}

/* DCA add */
int platform_mutex_unlock_from_isr(platform_mutex_t* m)
{
	static BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(m->mutex, &xHigherPriorityTaskWoken);
	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
	return pdTRUE;
}


int platform_mutex_destroy(platform_mutex_t* m)
{
    vSemaphoreDelete(m->mutex);
    return 0;
}

  1. 在mqttclient文件夹中创建at文件夹,包括at_command.c和at_command.h,用于编写AT命令数据的发送与解析函数

在这里插入图片描述

at_command.c代码:

#include "at_command.h"
#include <platform_mutex.h>
#include <at_uart_hal.h>
#include <string.h>
#include <stdio.h>
#include <ring_buffer.h>
#include <mqttclient.h>


#define AT_CMD_TIMOUT 1000
#define AT_RESP_LEN   200


static ring_buffer g_packet_buffer;   //存放网络接收数据payload的唤醒buffer, +IPD,len:data中的data

static platform_mutex_t at_ret_mutex;   //发送AT命令或者网络数据后上锁,数据解析任务完成ESP8266响应数据解析后释放互斥锁
static platform_mutex_t at_packet_mutex;   //读取网络接收数据为空时阻塞,数据解析任务完成网络接收数据解析后释放互斥锁

static int g_at_status;   //发送AT命令后的状态,OK、ERROR、TIMEOUT
static char g_at_resp[AT_RESP_LEN];   //AT命令正确响应后保存ESP8266的返回值数据

/* status
 *   0  - ok
 *   -1 - err
 *   -2 - timeout
 */
void SetATStatus(int status)   //设置AT命令发送后的ESP8266响应状态,AT_OK或者AT_ERR
{
	g_at_status = status; 
}

int GetATStatus(void)      //得到AT命令发送后的ESP8266响应状态,AT_OK或者AT_ERR
{
	return g_at_status;
}

int ATInit(void)     //初始化等待AT命令返回的互斥锁、读取网络数据的互斥锁和存放网络接收数据的环形缓冲区
{
	platform_mutex_init(&at_ret_mutex);
	platform_mutex_lock(&at_ret_mutex);  // mutex = 0

	platform_mutex_init(&at_packet_mutex);
	platform_mutex_lock(&at_packet_mutex);  // mutex = 0

	ring_buffer_init(&g_packet_buffer);
	
	return 0;
}

int ATSendData(unsigned char *buf, int len, int timeout)   //AT+CIPSEND后发送数据的函数
{
	int ret;
	int err;
	
	/* 发送网络数据 */
	HAL_AT_Send((char *)buf, len);

	/* 等待结果 
	 * 1 : 成功得到mutex
	 * 0 : 超时返回
	 */
	ret = platform_mutex_lock_timeout(&at_ret_mutex, timeout);   //等待数据解析任务解析完成释放互斥锁或超时
	if (ret)   //成功得到互斥锁
	{
		/* 判断返回值 */
		/* 存储resp */
		err = GetATStatus();
		return err;
	}
	else   //超时等待
	{
		return AT_TIMEOUT;
	}

}

int ATReadData(unsigned char *c, int timeout)   //读取网络数据的函数
{
	int ret;

	do {
		if (0 == ring_buffer_read((unsigned char *)c, &g_packet_buffer))   //读到一个字符的网络接收数据
			return AT_OK;
		else
		{
			ret = platform_mutex_lock_timeout(&at_packet_mutex, timeout);   //网络数据的环形buffer为空,等待数据解析任务解析完毕后释放互斥锁或者超时
			if (0 == ret)   //超时等待
				return AT_TIMEOUT;
		}
	} while (ret == 1);
	
	return 0;
}

/* eg. buf = "AT+CIPMODE=1"
 *     timeout : ms
 */
int ATSendCmd(char *buf, char *resp, int resp_len, int timeout)   //发送AT命令的函数
{
	int ret;
	int err;
	
	/* 发送AT命令 */
	HAL_AT_Send(buf, strlen(buf));
	HAL_AT_Send("\r\n", 2);

	/* 等待结果 
	 * 1 : 成功得到mutex
	 * 0 : 超时返回
	 */
	ret = platform_mutex_lock_timeout(&at_ret_mutex, timeout);   //发送AT命令后等待数据解析任务解析完ESP8266返回的数据并释放互斥锁
    vTaskDelay(200);
    if (ret)   //成功获取互斥锁,即接收到ESP8266响应的数据
	{
		/* 判断返回值 */
		/* 存储resp */
		err = GetATStatus();
		if (!err && resp)   //resp不为空, resp: 发送AT命令后期望的返回值		
		{
            /* 比较实际返回值与期望返回值,若期望返回值的长度大于最大buffer长度则使用buffer的最大使用长度 */
			memcpy(resp, g_at_resp, resp_len > AT_RESP_LEN ? AT_RESP_LEN : resp_len);
		}
		return err;
	}
	else
	{
		return AT_TIMEOUT;
	}

}

#if 0
static int GetCIPSENDResult(char *buf)
{
	if (g_cur_cmd && strstr(g_cur_cmd, "AT+CIPSEND=") && (buf[0] == '>'))
		return 1;	
	else
		return 0;
}
#endif

static int GetSpecialATString(char *buf)   //获取特殊返回值, IPD是接收到网络数据的数据头
{	
	if (strstr(buf, "+IPD,"))
		return 1;
	else
		return 0;
}

static void ProcessSpecialATString(char *buf)   //处理网络接收数据函数
{
	int i = 0;
	int len = 0;	
	
	/* +IPD,78:xxxxxxxxxx */
	{
		/* 解析出长度 */
		i = 0;
		while (1)
		{
			HAL_AT_Secv(&buf[i], (int)portMAX_DELAY);
			if (buf[i] == ':')
			{
				break;
			}
			else
			{
				len = len * 10 + (buf[i] - '0');   //将接收的到网络数据长度由字符转换为整型
			}
			i++;
		}

		/* 读取真正的网络数据 */
		i = 0;
		while (i < len)
		{
			HAL_AT_Secv(&buf[i], (int)portMAX_DELAY);
			if (i < AT_RESP_LEN)
			{
				/* 把数据放入环形buffer */
				ring_buffer_write(buf[i], &g_packet_buffer);
				
				/* wake up */
				/* 解锁唤醒使用ATReadData读网络数据的任务 */
				platform_mutex_unlock(&at_packet_mutex);		
			}
			i++;
		}
	}
}


void ATRecvParser( void * params)   //解析ESP8266返回数据的任务
{
	char buf[AT_RESP_LEN];   //接收ESP8266数据的buffer
	int i = 0;
	
	while (1)
	{
		/* 读取WIFI模块发来的数据:  使用阻塞方式 */
		HAL_AT_Secv(&buf[i], (int)portMAX_DELAY);
		buf[i+1] = '\0';

		/* 解析结果 */
		/* 1. 何时解析?    
		 * 1.1 收到"\r\n"
		 * 1.2 收到特殊字符: "+IPD,"
		 */
		if (i && (buf[i-1] == '\r') && (buf[i] == '\n'))
		{
			/* 得到了回车换行 */

			/* 2. 怎么解析 */
			if (strstr(buf, "OK\r\n"))
			{
				/* 记录数据 */
				memcpy(g_at_resp, buf, i);
				SetATStatus(AT_OK);
				platform_mutex_unlock(&at_ret_mutex);
				i = 0;
			}
			else if (strstr(buf, "ERROR\r\n"))
			{
				SetATStatus(AT_ERR);
				platform_mutex_unlock(&at_ret_mutex);
				i = 0;
			}       
			else if (strstr(buf, "Recv"))
			{
				SetATStatus(AT_OK);
				platform_mutex_unlock(&at_ret_mutex);
				i = 0;
			}
#if 0            
			else if (GetCIPSENDResult(buf))
			{
				SetATStatus(AT_OK);
				platform_mutex_unlock(&at_ret_mutex);
				i = 0;
			}
#endif            

			i = 0;
		}		
		else if (GetSpecialATString(buf))
		{
			ProcessSpecialATString(buf);
			i = 0;
		}
		else
		{
			i++;
		}

		if (i >= AT_RESP_LEN)
			i = 0;
	}
}


/* 以下代码是参考MqttClient源码中test例程: */
static void topic1_handler(void* client, message_data_t* msg)
{
    (void) client;
    MQTT_LOG_I("-----------------------------------------------------------------------------------");
    MQTT_LOG_I("%s:%d %s()...\r\ntopic: %s\r\nmessage:%s", __FILE__, __LINE__, __FUNCTION__, msg->topic_name, (char*)msg->message->payload);
    MQTT_LOG_I("-----------------------------------------------------------------------------------");
}


void MQTT_Client_Task(void *Param)
{
	int err;
	
    mqtt_client_t *client = NULL;
    mqtt_message_t msg;

    memset(&msg, 0, sizeof(msg));

    mqtt_log_init();

    client = mqtt_lease();
	
    mqtt_set_port(client, "xxx");   //MQTT服务器端口号

    mqtt_set_host(client, "xxx.xxx.xxx.xxx");    //MQTT服务器地址
    mqtt_set_client_id(client, random_string(10));
    mqtt_set_user_name(client, random_string(10));
    mqtt_set_password(client, random_string(10));
    mqtt_set_clean_session(client, 0);

    if (0 != mqtt_connect(client))
    {
		printf("mqtt_connect err\r\n");
		vTaskDelete(NULL);
    }

    err = mqtt_subscribe(client, "mcu_test1", QOS0, topic1_handler);
	if (err)
	{
		printf("mqtt_subscribe topic1 err\r\n");
	}

    err = mqtt_subscribe(client, "topic2", QOS1, NULL);
	if (err)
	{
		printf("mqtt_subscribe topic2 err\r\n");
	}

    err = mqtt_subscribe(client, "topic3", QOS2, NULL);
	if (err)
	{
		printf("mqtt_subscribe topic3 err\r\n");
	}

    msg.payload = "Hello world!";
    msg.qos = 0;
	msg.payloadlen = strlen(msg.payload);

    while (1) {
        /* 每间隔5s向主题mcu_test1发送一次"Hello world!" */
        mqtt_publish(client, "mcu_test1", &msg);
        printf("mqtt_publish mcu_test OK\r\n");
        vTaskDelay(5000);
    }

}

at_command.h代码:

#ifndef __AT_COMMAND_H
#define __AT_COMMAND_H

#define AT_OK        0
#define AT_ERR      -1
#define AT_TIMEOUT  -2

int ATInit(void);
void ATRecvParser( void * params);
void MQTT_Client_Task(void *Param);

/* eg. buf = "AT+CIPMODE=1"
 *     timeout : ms
 */
int ATSendCmd(char *buf, char *resp, int resp_len, int timeout);

int ATSendData(unsigned char *buf, int len, int timeout);
int ATReadData(unsigned char *c, int timeout);

int ATReadPacket(char *buf, int len, int *resp_len, int timeout);


#endif

4、plat_sock网络层相关代码

  1. 主要是对platform_net_socket.c文件中各个接口使用ESP8266的AT命令实现

platform_net_socket.c代码:

/*
 * @Author: jiejie
 * @Github: https://github.com/jiejieTop
 * @Date: 2020-01-10 23:45:59
 * @LastEditTime: 2020-04-25 17:50:58
 * @Description: the code belongs to jiejie, please keep the author information and source code according to the license.
 */
#include "mqtt_log.h"
#include "platform_net_socket.h"
#include "at_command.h"

#define TEST_SSID    "DESKTOP-NKFGPI3"   //WIFI名
#define TEST_PASSWD  "0V6u77{3"   //WIFI密码


/* return : < 0 , err 
 * 0 : ok
 */

int platform_net_socket_connect(const char *host, const char *port, int proto)
{
	int err;
	char cmd[100];

	while (1)
	{
		err = ATSendCmd("AT+CIPCLOSE", NULL, 0, 2000);
		
		/* 1. 配置 WiFi 模式 */
		err = ATSendCmd("AT+CWMODE=3", NULL, 0, 2000);
		if (err)
		{
			printf("AT+CWMODE=3 err = %d\n", err);
			//return err;
		}


		/* 2. 连接路由器 */
		/* 2.1 先断开 */
		err = ATSendCmd("AT+CWQAP", NULL, 0, 2000);
		if (err)
		{
			printf("disconnect AP err = %d\n", err);
			//return err;
		}


		/* 2.2 再连接 */
		err = ATSendCmd("AT+CWJAP=\"" TEST_SSID "\",\"" TEST_PASSWD "\"", NULL, 0, 200000);
		if (err)
		{
			printf("connect AP err = %d\n", err);
			//return err;
			continue;
		}

		/* 3. 连接到服务器 */
		if (proto == PLATFORM_NET_PROTO_TCP)
		{
			/* AT+CIPSTART="TCP","192.168.3.116",8080	 */
			sprintf(cmd, "AT+CIPSTART=\"TCP\",\"%s\",%s", host, port);
		}
		else
		{
			sprintf(cmd, "AT+CIPSTART=\"UDP\",\"%s\",%s", host, port);
		}

		err = ATSendCmd(cmd, NULL, 0, 2000);
		if (err)
		{
			printf("%s err = %d\n", cmd, err);
			continue;
		}

		if (!err)
			break;
		
	}
	
	return 0;
}

/* 暂时不需要用到的函数 */
#if 0
int platform_net_socket_recv(int fd, void *buf, size_t len, int flags)
{
    return 0;
}
#endif

/* 返回得到的字节数 */
int platform_net_socket_recv_timeout(int fd, unsigned char *buf, int len, int timeout)
{
	int i = 0;
	int err;

	/* 读数据, 失败则阻塞 */
	while (i < len)
	{
		err = ATReadData(&buf[i], timeout);
		if (err)
		{
			return 0;
		}
		i++;
	}

	return len;
}

/* 暂时不需要用到的函数 */
#if 0

int platform_net_socket_write(int fd, void *buf, size_t len)
{
	return 0;
}
#endif

int platform_net_socket_write_timeout(int fd, unsigned char *buf, int len, int timeout)
{
	int err;
	char cmd[20];

	sprintf(cmd, "AT+CIPSEND=%d", len);
	err = ATSendCmd(cmd, NULL, 0, timeout);
	if (err)
	{
		printf("%s err = %d, timeout = %d\n", cmd, err, timeout);
		return err;
	}
	err = ATSendData(buf, len, timeout);
	if (err)
	{
		printf("ATSendData err = %d\n", err);
		return err;
	}
	
	return len;
}

int platform_net_socket_close(int fd)
{
	return ATSendCmd("AT+CIPCLOSE", NULL, 0, 2000);
}

/* 暂时不需要用到的函数 */
#if 0

int platform_net_socket_set_block(int fd)
{
	return 0;
}

int platform_net_socket_set_nonblock(int fd)
{
	return 0;
}

int platform_net_socket_setsockopt(int fd, int level, int optname, const void *optval, socklen_t optlen)
{
	return 0;
}

#endif

五、烧录调试

主要记录调试过程中遇到的几个坑:

  • 堆栈空间分配不足导致MQTT_Client_Task任务无法运行、mqtt_yield_thread无法创建
  • mqtt_yield_thread中无法连接Server
  • 无法订阅主题和无法接收消息问题

1、堆栈空间分配问题

  1. 初始FreeRTOSConfig.h文件中默认堆栈空间配置如下:

在这里插入图片描述

  1. Debug时发现程序无法运行到MQTT_Client_Task任务中

在这里插入图片描述

  1. 把HEAP_SIZE调大到10K后继续调试

在这里插入图片描述

  1. 此时可以运行MQTT_Client_Task任务,但是出现mqtt yield thread creat faile

在这里插入图片描述

  1. 调试后猜测是platform_thread_init创建时堆空间不够

在这里插入图片描述

  1. 把堆空间增大到12

在这里插入图片描述

  1. 可以成功创建platform_thread_init任务了,但是出现无法连接Server问题

在这里插入图片描述

2、mqtt_yield_thread无法连接Server问题

  1. 调试后发现是由于client_state_t结构体中state状态不对导致报错的

在这里插入图片描述

  1. 查看mqtt_get_client_state后state状态是初始化状态

在这里插入图片描述

  1. 尝试注释掉判断state的代码

在这里插入图片描述

  1. 竟然发现可以正常发布消息,但是订阅失败

在这里插入图片描述

此处调试很久发现一直解决不了,最后索性注释掉,注释掉后发现是可以正常连接服务器并发布消息,但是无法订阅主题和接收消息!

3、无法订阅主题和无法接收消息问题

  1. 猜测将state判断注释掉可能会影响订阅主题和接收消息,于是取消注释,继续分析原因,发现出现无法连接Server时是进入了硬件错误

在这里插入图片描述

  1. 逐步分析,也是由于在中state状态不对导致需要停止任务导致的

在这里插入图片描述

  1. 搜索整个代码,将state设置为CLIENT_STATE_CONNECTED状态的只有2个地方,其中一个是重新连接时设置的,所以只有一个地方会将state状态设置为CLIENT_STATE_CONNECTED

在这里插入图片描述

  1. 调试发现代码根本无法运行到设置state为连接状态的地方就发生了硬件错误

其中无法理解的是为什么在MQTT_Client_Task任务中(优先级24)创建了mqtt_yield_thread任务(优先级5)后,就直接去运行mqtt_yield_thread了,没能继续运行platform_thread_init下的打印以及设置state状态的位置。

在这里插入图片描述

  1. 手动在platform_thread_init之前将state设置为CLIENT_STATE_CONNECTED

在这里插入图片描述

  1. 再次运行时发现MCU可以成功订阅主题mcu_test1,向主题发送数据并收到自己的数据,同时其他客户端同样能够收到MCU发送的数据,MCU能够收到其他客户端发送的数据,成功调通!

在这里插入图片描述

调试完成的代码以及杰杰的kawaii mqttclient源码:https://download.csdn.net/download/studyingdda/88741258

  • 26
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: STM32F103开发板和ESP8266都是常见的嵌入式系统芯片,其中STM32F103是ST公司推出的32位微控制器,而ESP8266则是一个高性能的Wi-Fi芯片。STM32F103开发板和ESP8266之间的通信可以通过串口通信实现,具体步骤如下: 1.使用STM32F103开发板的外部串口或者软件串口来连接ESP8266的串口,即将ESP8266的RXD引脚连接到STM32F103开发板的TXD引脚上,将ESP8266的TXD引脚连接到STM32F103开发板的RXD引脚上。 2.在STM32F103开发板上编写串口驱动程序,通过配置串口通信参数和发送数据,来实现和ESP8266的通信。 3.使用AT指令来控制ESP8266,AT指令是ESP8266的默认命令集,可以通过串口向ESP8266发送AT指令,来实现对ESP8266的控制和配置。 4.在STM32F103开发板上编写控制程序,通过发送AT指令,来控制ESP8266实现需要的功能,比如连接Wi-Fi网络、发送和接收数据等等。 需要注意的是,在使用ESP8266时,需要根据具体的应用场景和需求,选择合适的ESP8266模块和驱动程序,同时还需要对ESP8266的技术特点和使用方法有一定的了解和掌握,以充分发挥其功能和性能。 ### 回答2: 在将STM32F103开发板与ESP8266无线Wi-Fi模块连接时,首先需要确认开发板与ESP8266的引脚对应关系,确保连接正确。 然后,在STM32F103的代码中,需要使用串口通信方式(例如USART或UART)与ESP8266进行通信。首先需要将串口引脚连接到ESP8266,然后通过代码设置波特率和串口数据传输格式等参数来实现与ESP8266的通信。 在代码中,还需通过AT命令向ESP8266发送指令,如设置Wi-Fi网络名称、密码以及与服务器的连接等。这些指令可以通过串口发送,同时需要进行数据解析和处理。 为了便于开发和调试,在STM32F103代码中还可以加入LED指示灯和调试信息输出等功能,以便于观察系统运行状态。同时,可能还需要进行网络协议处理等其他工作,具体操作与应用场景相关。 综上,驱动ESP8266无线Wi-Fi模块需要对串口通信、AT指令、数据解析等方面进行处理。同时还需要了解并适应应用场景要求,做好相关配置和功能性开发。 ### 回答3: STM32F103是一种高性能的嵌入式系统开发板,可以与ESP8266模组相组合,以实现物联网设备的开发和应用。要驱动ESP8266模组,首先需要确定使用的通信接口。ESP8266可以通过UART、SPI、I2C等接口与STM32F103相连。其中,UART接口是最为常用的连接方式,因为它简单、易用,可以很快地实现数据传输和通信协议。 在使用UART接口连接ESP8266模组和STM32F103开发板时,需要创建一个串口通信接口,并设置相应的波特率、数据位、停止位和校验位等参数。例如,可以使用STM32F103开发板的USART1或USART2串口通道,设置通信参数为波特率9600、数据位8位、停止位1位、无校验位,以此搭建串口设备。 接下来,需要编写ESP8266的相关驱动程序,实现ESP8266模組的联网功能。常见的ESP8266驱动程序库有ESP8266_NONOS_SDK和ESP8266_RTOS_SDK等。这些库可以帮助开发者快速有效地实现ESP8266的驱动,支持WiFi连接、数据传输、远程控制等应用。 在编写完驱动程序后,需要在STM32F103开发板上编写应用程序,实现ESP8266模组和其他传感器、设备的联动控制。例如,可以通过ESP8266模组获取云端数据,通过STM32F103控制继电器、风扇、电机等硬件设备的操作。 综上所述,驱动ESP8266模组需要涉及到多个方面,包括通信接口的选择、驱动程序的编写、应用程序的开发等。只有在不断的实践中,才能更好地理解、掌握这些技术。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

studyingdda

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值