STM32F103_ESP8266基于RTOS移植MQTT
目录
- STM32F103_ESP8266基于RTOS移植MQTT
- 一、准备工作
- 二、移植mqttclient代码
- 三、编译包含mqttclient的工程
- 四、编写ESP8266驱动程序
- 1、ESP8266 AT命令代码框架
- 2、UART硬件和抽象层相关代码
- 3、AT命令发送和解析代码
- 4、plat_sock网络层相关代码
- 五、烧录调试
- 1、堆栈空间分配问题
- 2、mqtt_yield_thread无法连接Server问题
- 3、无法订阅主题和无法接收消息问题
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”
- 图接MCU串口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代码
- 将
mqttclient
源码目录复制到FreeRTOS_ESP8266_MQTT
工程
mqttclient
源码中保留‘common’、‘mqtt’、‘mqttclient’、‘network’、‘platform’目录
- 将各目录中的代码添加到keil工程中(.c和.h)
‘common’中仅添加mqtt_list.c和random.c文件;‘network’中仅添加nettype_tcp.c、nettype_tls.c、network.c文件
三、编译包含mqttclient的工程
将mqttclient
源码增加到工程中后对工程进行编译,记录编译报错及解决过程:
- 找不到头文件"mqtt_config.h"
头文件"mqtt_config.h"在test目录下,注释后继续编译
- 找不到mbedtls系列头文件
自定义MQTT_NETWORK_TYPE_NO_TLS宏,再次编译
- 找不到lwip系列头文件
韦东山老师移植完成的代码中没有找到lwip系列头文件被注释掉了,注释后继续编译
- size_t类型未定义
包含头文件<stdio.h>,再次编译
- socklen_t类型未定义
同样增加一个socklen_t的定义,再次编译
- platform_net_socket.c文件中各个接口报错
删除此文件中的各个接口,后期使用ESP8266 AT命令来实现各个接口,再次编译
- 懂定义参数少了一个括号
在此宏定义的2个文件处修改一下,再次编译
- 找不到头文件plooc_class.h
将plooc_class.h文件路径添加到工程中,再次编译
- 仅支持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的中断接收函数
- 在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
{
}
}
}
- 在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
- 在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);
}
- 对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;
}
- 在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网络层相关代码
- 主要是对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、堆栈空间分配问题
- 初始FreeRTOSConfig.h文件中默认堆栈空间配置如下:
- Debug时发现程序无法运行到MQTT_Client_Task任务中
- 把HEAP_SIZE调大到10K后继续调试
- 此时可以运行MQTT_Client_Task任务,但是出现mqtt yield thread creat faile
- 调试后猜测是platform_thread_init创建时堆空间不够
- 把堆空间增大到12
- 可以成功创建platform_thread_init任务了,但是出现无法连接Server问题
2、mqtt_yield_thread无法连接Server问题
- 调试后发现是由于client_state_t结构体中state状态不对导致报错的
- 查看mqtt_get_client_state后state状态是初始化状态
- 尝试注释掉判断state的代码
- 竟然发现可以正常发布消息,但是订阅失败
此处调试很久发现一直解决不了,最后索性注释掉,注释掉后发现是可以正常连接服务器并发布消息,但是无法订阅主题和接收消息!
3、无法订阅主题和无法接收消息问题
- 猜测将state判断注释掉可能会影响订阅主题和接收消息,于是取消注释,继续分析原因,发现出现无法连接Server时是进入了硬件错误
- 逐步分析,也是由于在中state状态不对导致需要停止任务导致的
- 搜索整个代码,将state设置为CLIENT_STATE_CONNECTED状态的只有2个地方,其中一个是重新连接时设置的,所以只有一个地方会将state状态设置为CLIENT_STATE_CONNECTED
- 调试发现代码根本无法运行到设置state为连接状态的地方就发生了硬件错误
其中无法理解的是为什么在MQTT_Client_Task任务中(优先级24)创建了mqtt_yield_thread任务(优先级5)后,就直接去运行mqtt_yield_thread了,没能继续运行platform_thread_init下的打印以及设置state状态的位置。
- 手动在platform_thread_init之前将state设置为CLIENT_STATE_CONNECTED
- 再次运行时发现MCU可以成功订阅主题mcu_test1,向主题发送数据并收到自己的数据,同时其他客户端同样能够收到MCU发送的数据,MCU能够收到其他客户端发送的数据,成功调通!
调试完成的代码以及杰杰的kawaii mqttclient源码:https://download.csdn.net/download/studyingdda/88741258