前言

代码和函数有部分更新,想要思路是没有变化的。My_usart.c和My_usart.h修改后代码可读性更好,移植更方便。

STM32串口通信–(基于HAL库|自定义命令调试)
之前写过一篇基于标准库的串口通信和匿名上位机使用的教程,但是这个上位机的发送功能(结合蓝牙模块)是比较好用的,但接收功能配置起来麻烦,用来串口发送数据调试麻烦,只能发送8位的数据,使用起来很不方便。因此写下这篇,有方便的API接口函数,移植起来方便,使用起来也简单

比较难理解的就是自定义的数据帧的协议的书写,包括头帧和尾帧以及中间分格的帧,但是如果不能理解,移植过来使用也是很简单的。

如果是发送数据,需要显示波形,可以看我之前写的博客。使用匿名上位机,这个上位机使用起来也比较方便,比较适合调试。

匿名V7上位机简单教程

正片开始
为什么要使用协议,不使用协议不是更简单吗

使用协议的其中一个原因是,为了保证数据准确性,也就是减少错误率,使得判断更为精准。如果自己写过比较简单的串口接收和判断的函数(无协议),遇到比较多数据的情况,很容易引起误判。

一、STM32cubemx配置

这里用正点原子STM32F103ZETX的板子作为实例
1
主要是配置串口接收中断,发送部分在main中,采取阻塞的方式发送,这里从只是进行串口的配置

使用PA9,PA10,默认配置 (一般都是使用默认配置,波特率115200) , 勾选全局中断(使用接收中断)
在这里插入图片描述

串口中断一定要勾选,优先级按自己按照需要安排

在这里插入图片描述

串口通信的STM32cubemx串口通信配置到此,接下来是在keil进行的串口代码的书写

二、Keil中断基本配置

主要是使用下面的三个函数,发送,接收,回调函数。

//在stm32f1xx_hal_uart.c中的三个函数基本配置就差不多完成了
//接收中断函数
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
//回调函数
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
//阻塞发送函数
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

在这里插入图片描述

(1)基本配置main函数种的配置,只是加上了HAL_UART_Receive_IT()的函数

extern void SystemClock_Config(void);
char S_uart_temp=0;
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */ 	
  HAL_UART_Receive_IT(&huart1,&S_uart_temp,0X01);//必须使能这个,每次接受完一次数据就要重新调用这个函数;//必须使能这个,每次接受完一次数据就要重新调用这个函数
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

(2)在main函数下面使用回调函数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    HAL_UART_Transmit(&huart1,ch,1,0xFFFF);//阻塞方式打印
	HAL_UART_Receive_IT(&huart1,&S_uart_temp,0X01
}

到此,基本的通信配置完成,做完前期工作,STM32能够使用串口进行通信,下面是协议的书写,也是重点。

三、自定义协议配置

(1)输出调试信息配置
在进行协议配置前,我们先对printf进行重定向,方便调试和输出信息,也就是使用在HAL库中使用printf,加入以下代码即可

#include "stdio.h"
#ifdef __GNUC__
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
  //Usart_sent_data((uint8_t *)ch);可以使用这个函数,也可以不用,建议使用,方便移植printf
  HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,0xFFFF);//阻塞方式打印
  return ch;
}

(2)对两个函数进行重新写,方便阅读和移植

//函数名:  Usart_get_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    中断串口接收单个字符
//输入参数:无
//返回值:  类型(char)
//          返回接收字符
//修改记录:
//=================================================================
char Usart_get_data(void)
{
	HAL_UART_Receive_IT(&huart1,&S_uart_temp,0X01);//必须使能这个,每次接受完一次数据就要重新调用这个函数;
	return S_uart_temp;
}
//函数名:  Usart_sent_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    串口输出单个字符
//输入参数:uint8_t *ch
//					需要发送单个字符的地址
//返回值:  类型(void)
//修改记录:
//=================================================================
void Usart_sent_data(uint8_t *ch)
{
	HAL_UART_Transmit(&huart1,ch,1,0xFFFF);//阻塞方式打印
}

(3)协议说明
这个是通信层面的协议,只是包括头帧和尾帧的协议。发送数据的一方必须使用此协议,才能进行有效的通信。例如,发送方发送数据为"(PID=2.22)",这样包括头帧 ‘(’,和尾帧 ‘)’,STM32才能知道有效数据是PID=2.22。没有的话,数据一概视为垃圾数据,不处理。

初始化配置

先对初始化一些变量,方便对数据管理,这里使用结构体进行管理。

#define JUDGE_DATA_MAX_LEN 20 //判断数据有效容量
#define Max_BUFF_Len 0XFF//定义的最大数据容量

//为了方便阅读,使用下面的宏定义
#define S_Uartx_Rx     usartx_struct_ptr->Uartx_Rx
#define	S_Uartx_Tx     usartx_struct_ptr->Uartx_Tx
#define	S_Uartx_Len    usartx_struct_ptr->Uartx_Len
#define	S_Uartx_Sta    usartx_struct_ptr->Uartx_Sta
#define	S_head_flag    usartx_struct_ptr->head_flag
#define	S_teal_flag    usartx_struct_ptr->teal_flag
#define S_Uartx_Buffer usartx_struct_ptr->Uartx_Buffer
#define S_uart_temp    usartx_struct_ptr->usrt_temp_data
#define S_command_flag usartx_struct_ptr->command_flag

//自定义头帧,尾帧,命令使用方式,如果想改的话,把宏定义修改即可
#define HEAL_FLAGE '('
#define TAIL_FLAGE ')'
#define COMMAND_FLGAHE '\0'//如果算是使用命令模式的话,可以把这个宏改为'\0',使用'='是调参模式,方便调参
//串口管理结构体
typedef struct
{
	uint8_t Uartx_Buffer[Max_BUFF_Len];//缓冲数组
	uint8_t usrt_temp_data;//保存的单个数据
	int  UartX_Sta;//发送数据标志位
	int  Uartx_Rx;//状态标志位
	int  Uartx_Tx;//数据头
	int  Uartx_Len;//有效数据长度
	int  Uartx_Sta;//发送数据标志位
	char head_flag;//头帧标志位
	char teal_flag;//尾帧标志位
	char command_flag;//命令标志
}Usart_struct;

Usart_struct usartx_struct;
Usart_struct *usartx_struct_ptr=&usartx_struct;//结构体指针

void Usart_srtuct_init(void)
{
	usartx_struct_ptr->usrt_temp_data=0;//保存的单个数据
	usartx_struct_ptr->Uartx_Rx=0;//状态标志位
	usartx_struct_ptr->Uartx_Tx=0;//数据头
	usartx_struct_ptr->Uartx_Len=0;//有效数据长度
	usartx_struct_ptr->Uartx_Sta=0;//发送数据标志位
	usartx_struct_ptr->head_flag=HEAL_FLAGE;
	usartx_struct_ptr->teal_flag=TAIL_FLAGE;
	usartx_struct_ptr->command_flag=COMMAND_FLGAHE;
}

下面是比较关键的数据处理的部分,也就是判断改数据是不是符合自己的协议,数据是否有效,若是符合头帧和尾帧格式的,将数据保存下来。判断有效数据的函数在回调函数 (中断)里面调用,因为需要对每一个数据都进行判断。

额外说明:是通过Uart3_Rx来判断位置的,每一次到来就Uart3_Rx+1,具体已经在注释中有说明了

//函数名:  Usart_judge_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    接收一个数据,并对数据进行判断
//输入参数:uint8_t *ch
//		  需要进行判断的一个数据
//返回值:  类型(void)
//修改记录:
//=================================================================

void Usart_judge_data(char *usart_data)
{	
	
		//例如数据是1123(PID=3.33),头帧:S_Uartx_Tx=5,执行5遍函数后,尾帧是14		S_Uartx_Buffer[S_Uartx_Rx]=*usart_data;
		S_Uartx_Rx++; //向下加1,方便保存,也就是原来的数据是未减之前的位置
		S_Uartx_Rx &= Max_BUFF_Len; //FIFO最大接收数据,防止溢出		
		if((S_Uartx_Buffer[S_Uartx_Rx-1])==S_head_flag)//找到头'('所在的位置
			S_Uartx_Tx = S_Uartx_Rx-1; //保存头帧的位置
		 
		if((S_Uartx_Buffer[S_Uartx_Tx] == S_head_flag&&(S_Uartx_Buffer[S_Uartx_Rx-1] == S_teal_flag))) //检测到头帧的情况下检测到尾帧')' 
   		 { 
			 //注意尾帧就是S_Uartx_Rx-1
      		 S_Uartx_Len = (S_Uartx_Rx-1)- 			(S_Uartx_Tx)+1; //有效数据长度(尾减头,包括头帧和尾帧) 
   		     S_Uartx_Sta=1;//标志位 
   		 }
}

//回调函数中调用Usart_judge_data
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart==&huart1)
	{
	//Usart_sent_data(&S_uart_temp);
		Usart_deal_data((char *)&S_uart_temp);
		Usart_get_data();
	}
}

(4)处理数据

这里面的数据处理,只是为了方便调试参数,比如调试PID,当然接收命令也是同理,这里只是以自定义命令,调试信息为例子

先对自定义的命令做出判断,判断哪一些是命令,那一些是需要调参的数据,这里我用的是 **'='进行判断,’=‘**后面的就是有效的数字。

//函数名:  judge_command
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    判断命令到哪里是有效的,并返回命令长度
//输入参数:const char *str
//		      传入命令,也就是字符串
//返回值:  类型(int)
//          返回命令长度,方便判断后面的有效内容的位置
//修改记录:
//=================================================================
int judge_command(const char *str)
{
	int count = 0;
	while (*str !='=')//'='是判断的位置,也是命令停止标志
	{
		count++;
		str++;
		if(count>=JUDGE_DATA_MAX_LEN) //防止一直在while循环,如果一直找不到命令停止标准就20次循环后结束
		{
			printf("input data error");
			break;
		}
	}
	count++;//把=也加上
	return count;
}

实际有效内容的处理,主要是比较函数strncmp和字符转换函数atof的使用,把两个头文件包含进来即可

#include "stdlib.h"
#include "string.h"
//函数名:  Usart_deal_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    传入需要判断的字符串,例如,格式:(内容=数字)
//输入参数:const char *str,写入参数的地址
//		      传入命令,也就是字符串和指令的地址
//返回值:  类型(void)
//注意:    需要修改宏定义,来使用#define //COMMAND_FLGAHE来选择使用哪种方式,如果是调参模式
//则parameter传入调试参数的数字,通过它来判断指令是否正确和判断值有没有修改成功。如果是命令模式,如果命令正确是真,则parameter为1.0,否则为0,方便判断。如果是指令模式,指令正确,parameter的值就改变,否则不变方便调试。
//实例:调参模式下,pid_p=Usart_deal_data(pid_p=0.111) printf("pid_p=%f",pid_p),一般在主函数中调用或者其他的发送线程调用
//修改记录:          2021/08/03,写完该函数
//					2021/08/04,把不能调用多条命令的BUG修改完
//					2021/08/05,把传入参数修改了,使之能正确的修改参数
//					2021/08/08,修改指令模式下的返回值
//=================================================================
void Usart_deal_data(const char *sdata,float *parameter)
{
	char floatdaer[15]={0};//临时保存内容数据
	int str_len=0;//除命令外有效数据长度
    float temp_data=0;
		
	if(S_Uartx_Sta) //有效数据来了
	{
		if(S_command_flag=='=')//调参使用
		{
			str_len=judge_command(sdata);//把命令结束的位置保存下来
			//printf("command_len=%d,",str_len);
		
			if(strncmp((const char *)&S_Uartx_Buffer[S_Uartx_Tx+1],sdata,str_len)==0)	//判断传来的数据是否符合命令,比较命令是否符合
			{
				printf("%s\r\n",&S_Uartx_Buffer[S_Uartx_Tx]);
				strncpy(floatdaer,(const char *)&S_Uartx_Buffer[(S_Uartx_Tx+str_len+1)],(S_Uartx_Len-str_len+1));//floatdate保存等于号后六位作为字符串
				//printf("%s",floatdaer);
				temp_data=atof(floatdaer);//字符串转换成为浮点型数字,遇到数字或者+-号转换开始,遇到非数字,转换结束,也就是遇到尾标转换结束
				*parameter=temp_data;//保存传入的指令参数
				printf("temp_data=%lf\r\n",*parameter);
				//重新清零
				S_Uartx_Rx = 0; 
				S_Uartx_Tx = 0; 
				S_Uartx_Sta = 0; 
			}

		}
		else//命令使用,如果是符合命令,返回1.0 ,假的返回0
		{
			str_len=judge_command(sdata);//把命令结束的位置保存下来
			//printf("command_len=%d,",str_len);
			//printf("%s",&S_Uartx_Buffer[S_Uartx_Tx]);
			if(strncmp((const char *)&S_Uartx_Buffer[S_Uartx_Tx+1],sdata,str_len-1)==0)	//判断传来的数据是否符合命令,比较命令是否符合
			{			
				printf("%s\r\n",&S_Uartx_Buffer[S_Uartx_Tx]);		
				printf("True\r\n");
					//重新清零
				*parameter=1.0;//指令为真
				S_Uartx_Rx = 0; 
				S_Uartx_Tx = 0; 
				S_Uartx_Sta = 0; 
			}
			else
			{
			*parameter=0;//指令为假
			}	
		}
	}
}


(4)调用说明

自己把命令传入这个参数,调用这个函数就能很好地使用了。

建议在main中使用,同时使用printf进行调试信息的输出

int main(void)
{
  /* USER CODE BEGIN 1 */
	float temp=0;
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  Usart_srtuct_init();//结构体初始化
  Usart_get_data();//必须使能这个,每次接受完一次数据就要重新调用这个函数
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
	Usart_deal_data("PID_P=",parameter);
	Usart_deal_data("PID_I=",parameter);
	Usart_deal_data("PID_D=",parameter);
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

输出如下:支持写入多条命令,支持写入命令加参数的模式进行调参,会有写入命令的输出,以及返回数值的输出,如果命令失败不正确,则没有返回输出

调参模式下的输出:

在这里插入图片描述在这里插入图片描述

命令模式下使用:和调参命令一致,只是没有输入调参的数字

在这里插入图片描述

2、移植说明

提示:如果不想理解这么麻烦,只是想用,移植的时候也是很简单的,把全部东西给复制过去,然后把串口配置好,把发送串口发送单个数据的函数给修改,再把指定函数放到中断里面就行了,最后调用判断函数。

改这两个函数,只是把里面发送和接收的函数给改了就行了
void Usart_sent_data(uint8_t *ch)
{
	HAL_UART_Transmit(&huart1,ch,1,0xFFFF);//阻塞方式打印,把这个修改了
}

char Usart_get_data(void)
{
	HAL_UART_Receive_IT(&huart1,&S_uart_temp,0X01);//必须使能这个,每次接受完一次数据就要重新调用这个函数,把这个修改了
	return S_uart_temp;
}
把这两个函数放在中断,或者回调函数里面就行了
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart==&huart1)
	{
		Usart_judge_data((char *)&S_uart_temp);
		Usart_get_data();/这个用于获取数据,并把数据保存在S_uart_temp,所以,要先调用这个,在调用Usart_judge_data,因为此处已经在main中事先调用过一次了,所以放在后面,等下次调用。
	}
}

最为好用的函数就是Usart_deal_data()。使用时把宏定义修改好,选择命令或者调参的模式,然后,自己把命令传入这个参数,调用这个函数就能很好地使用了。这里的命令是任意的,喜欢传入什么命令就传入什么命令,这个命令也是可以的"f*ck you"

修改宏:

指定选择方式,需要不需要哪种方式注释掉就好了,两种方式只能选择一种哦
#define COMAND_MODE 
//#define PARAMETER_MODE


#ifdef COMAND_MODE

#define COUNT_FLAH '\0'

#else
				COUNT_FLAH '='
#endif

调用命令函数float Usart_deal_data(const char *sdata);

//使用例子
//调参模式,把宏定义改了
float parem;
//调用函数,传入命令"PID_P=",'='后面的数字是需要输入的,例如在上位机这样输入,返回11.111,打印输入值
Usart_deal_data("PID_P=11.111",&parem);
//命令模式,把宏定义改了
//调用函数,传入命令"PID_P=",'='后面的数字是需要输入的,例如在上位机这样输入,如果符合命令,返回1.0,打印True
Usart_deal_data("PID_P",&parem);
if(parem==1.0) print("RIGHT COMMAND \r\n");

调参模式下的输出:

在这里插入图片描述

命令模式下使用:和调参命令一致,只是没有输入调参的数字

在这里插入图片描述

3、完整的串口相关代码代码(不包cubemx已经帮我们已经配置好的代码,代码分别在自己创建的My_Usart.c和My_Usart.h

main.c的文件已经囊括了本文%90的内容了

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * <h2><center>&copy; Copyright (c) 2021 STMicroelectronics.
  * All rights reserved.</center></h2>
  *
  * This software component is licensed by ST under BSD 3-Clause license,
  * the "License"; You may not use this file except in compliance with the
  * License. You may obtain a copy of the License at:
  *                        opensource.org/licenses/BSD-3-Clause
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "My_Usart.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* USER CODE BEGIN PFP */


/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
extern void SystemClock_Config(void);
int main(void)
{
  /* USER CODE BEGIN 1 */
	float tt=0;
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  My_Usart_init();//调用相关初始化函数
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
		Usart_deal_data("PID_P=",&tt);
		printf("tt=%f\r\n",tt);
		HAL_Delay(1000);
		//tt=Usart_deal_data("PID_D=");
		//printf("tt=%f\r\n",tt);
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart==&huart1)
	{
	//Usart_sent_data(&S_uart_temp);
		Usart_judge_data((char *)&S_uart_temp);
		Usart_get_data();
	}
}
/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
void 
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/

My_Usart.c文件

#include "hc05.h"
#include "stdlib.h"
#include "string.h"
#include "usart.h"
#include "stdio.h"


PUTCHAR_PROTOTYPE
{
	//Usart_sent_data((uint8_t *)ch);
  HAL_UART_Transmit(&huart1,(uint8_t *)&ch,1,0xFFFF);//阻塞方式打印
  return ch;
}

#endif

//自定义头帧,尾帧,命令结束,如果想改的话,把宏定义修改即可
#define HEAL_FLAGE '('
#define TAIL_FLAGE ')'

//指定选择方式,需要不需要哪种方式注释掉就好了,两种方式只能选择一种哦
#define COMAND_MODE 
//#define PARAMETER_MODE


#ifdef COMAND_MODE

#define COUNT_FLAH '\0'

#else
		COUNT_FLAH '='
#endif

//蓝牙串口结构体
Usart_struct usartx_struct;
Usart_struct *ustu_ptr=&usartx_struct;//结构体指针


//串口结构体初始化
void Usart_srtuct_init(void)
{
	ustu_ptr->Usrt_temp_data=0;//保存的单个数据
	ustu_ptr->Uartx_Rx=0;//状态标志位
	ustu_ptr->Uartx_Tx=0;//数据头
	ustu_ptr->Uartx_Len=0;//有效数据长度
	ustu_ptr->Uartx_Sta=0;//发送数据标志位
	ustu_ptr->head_flag=HEAL_FLAGE;
	ustu_ptr->teal_flag=TAIL_FLAGE;
	ustu_ptr->command_flag=COUNT_FLAH;
}

//HC05初始化
void Hc05_init()
{
	Usart_srtuct_init();
	Usart_get_data();	
}

//======================================================================//
//函数名:  Usart_sent_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    串口输出len个字符
//输入参数:uint8_t *ch,字符长度
//					需要发送单个字符的地址
//返回值:  类型(void)
//修改记录:
//======================================================================//
void Usart_sent_data(uint8_t *ch,uint8_t len)
{
	HAL_UART_Transmit(&huart1,ch,len,0xFFFF);//阻塞方式打印
}
//函数名:  judge_command
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    判断命令到哪里是有效的,并返回命令长度
//输入参数:const char *str
//		      传入命令,也就是字符串
//返回值:  类型(int)
//          返回命令长度,方便判断后面的有效内容的位置
//修改记录:
//=================================================================
int judge_command(const char *str)
{
	int count = 0;
	while (*str !=COUNT_FLAH)//是判断的位置,也是命令停止标志
	{
		count++;
		str++;
		if(count>=JUDGE_DATA_MAX_LEN) //防止一直在while循环,如果一直找不到命令停止标准就20次循环后结束
		{
			//printf("input data error");
			break;
		}
	}
	count++;//把=也加上
	return count;
}
//函数名:  Usart_get_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    中断串口接收单个字符
//输入参数:无
//返回值:  类型(char)
//          返回接收字符
//修改记录:
//=================================================================
char Usart_get_data(void)
{
	HAL_UART_Receive_IT(&huart1,&ustu_ptr->Usrt_temp_data,0X01);//必须使能这个,每次接受完一次数据就要重新调用这个函数;
	return ustu_ptr->Usrt_temp_data;
}
//函数名:  Usart_judge_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    接收一个数据,并对数据进行判断
//输入参数:uint8_t *ch
//		  需要进行判断的一个数据
//返回值:  类型(void)
//修改记录:
//=================================================================
void Usart_judge_data(char *usart_data)
{	
		
		//例如数据是1123(PID=3.33),头帧:ustu_ptr->Uartx_Tx=5,执行5遍函数后,尾帧是14
		//UART3_GetChar(Uart3_Buffer[Uart3_Rx]);
		ustu_ptr->Uartx_Buffer[ustu_ptr->Uartx_Rx]=*usart_data;
		ustu_ptr->Uartx_Rx++; //向下加1,方便保存,也就是原来的数据是未减之前的位置
		ustu_ptr->Uartx_Rx &= 0XFF; //FIFO最大接收数据,防止溢出		
		if((ustu_ptr->Uartx_Buffer[ustu_ptr->Uartx_Rx-1])==ustu_ptr->head_flag)//找到头'('所在的位置
			ustu_ptr->Uartx_Tx = ustu_ptr->Uartx_Rx-1; //保存头帧的位置
		 
		if((ustu_ptr->Uartx_Buffer[ustu_ptr->Uartx_Tx] == ustu_ptr->head_flag&&(ustu_ptr->Uartx_Buffer[ustu_ptr->Uartx_Rx-1] == ustu_ptr->teal_flag))) //检测到头帧的情况下检测到尾帧')' 
    { 
			 //注意尾帧就是ustu_ptr->Uartx_Rx-1
       ustu_ptr->Uartx_Len = (ustu_ptr->Uartx_Rx-1)- (ustu_ptr->Uartx_Tx)+1; //有效数据长度(尾减头,包括头帧和尾帧) 
       ustu_ptr->Uartx_Sta=1;//标志位 
    }
}

//函数名:  Judge_recv
//作者:    Silent Knight
//日期:    2022-02-18
//功能:    提供一个接口判断是否一个命令是否已经到达
//输入参数: void
//返回值:   1 已经到达, 0 还未到达
//=================================================================
int Judge_recv(void)
{
	return ustu_ptr->Uartx_Sta;
}
//函数名:  Usart_deal_data
//作者:    Silent Knight
//日期:    2021-08-4
//功能:    传入需要判断的字符串,例如,格式:(内容=数字)
//输入参数:const char *str,写入参数的地址
//		      传入命令,也就是字符串和指令的地址
//返回值:  类型(void)
//注意:    需要修改宏定义,来使用#define //COMMAND_FLGAHE来选择使用哪种方式,如果是调参模式
//则parameter传入调试参数的数字,通过它来判断指令是否正确和判断值有没有修改成功。如果是命令模式,如果命令正确是真,则parameter为1.0,否则为0,方便判断。如果是指令模式,指令正确,parameter的值就改变,否则不变方便调试。
//实例:调参模式下,pid_p=Usart_deal_data(pid_p=0.111) printf("pid_p=%f",pid_p),一般在主函数中调用或者其他的发送线程调用
//修改记录:          2021/08/03,写完该函数
//					2021/08/04,把不能调用多条命令的BUG修改完
//					2021/08/05,把传入参数修改了,使之能正确的修改参数
//					2021/08/08,修改指令模式下的返回值
//=================================================================
void Usart_deal_data(const char *sdata,double *parameter)
{
	char floatdaer[15]={0};//临时保存内容数据
	int str_len=0;//除命令外有效数据长度
  double temp_data=0;
		
	if(Judge_recv()) //有效数据来了
	{
		if(ustu_ptr->command_flag=='=')//调参使用
		{
			str_len=judge_command(sdata);//把命令结束的位置保存下来
			//printf("command_len=%d,",str_len);
		
			if(strncmp((const char *)&ustu_ptr->Uartx_Buffer[ustu_ptr->Uartx_Tx+1],sdata,str_len)==0)	//判断传来的数据是否符合命令,比较命令是否符合
			{
				//printf("%s\r\n",&ustu_ptr->Uartx_Rx[ustu_ptr->Uartx_Tx]);
				strncpy(floatdaer,(const char *)&ustu_ptr->Uartx_Buffer[(ustu_ptr->Uartx_Tx+str_len+1)],(ustu_ptr->Uartx_Len-str_len+1));//floatdate保存等于号后六位作为字符串
				//printf("%s",floatdaer);
				temp_data=atof(floatdaer);//字符串转换成为浮点型数字,遇到数字或者+-号转换开始,遇到非数字,转换结束,也就是遇到尾标转换结束
				*parameter=temp_data;//保存传入的指令参数
				//printf("temp_data=%lf\r\n",*parameter);
				//重新清零
				ustu_ptr->Uartx_Rx = 0; 
				ustu_ptr->Uartx_Tx = 0; 
				ustu_ptr->Uartx_Sta = 0; 
			}

		}
		else//命令使用,如果是符合命令,返回1.0 ,假的返回0
		{
			str_len=judge_command(sdata);//把命令结束的位置保存下来
			//printf("command_len=%d,",str_len);
			//printf("%s",&ustu_ptr->Uartx_Rx[ustu_ptr->Uartx_Tx]);
			if(strncmp((const char *)&ustu_ptr->Uartx_Buffer[ustu_ptr->Uartx_Tx+1],sdata,str_len-1)==0)	//判断传来的数据是否符合命令,比较命令是否符合
			{			
				//printf("%s\r\n",&ustu_ptr->Uartx_Rx[ustu_ptr->Uartx_Tx]);		
				//printf("True\r\n");
					//重新清零
				*parameter=1.0;//指令为真
				ustu_ptr->Uartx_Rx = 0; 
				ustu_ptr->Uartx_Tx = 0; 
				ustu_ptr->Uartx_Sta = 0; 
			}
			else
			{
			*parameter=0;//指令为假
			}	
		}
	}
}


包括头文件My_Usart.h

#ifndef __MY_USART_H__
#define __MY_USART_H__

//加入该宏定义使用printf函数
#define JUDGE_DATA_MAX_LEN 20
#define Max_BUFF_Len 0XFF//定义的最大数据容量


//串口管理结构体
typedef struct
{
	rt_uint8_t  Uartx_Buffer[Max_BUFF_Len];//缓冲数组
	rt_uint8_t  Usrt_temp_data;//保存的单个数据
	rt_uint8_t  Uartx_Rx;//状态标志位
	rt_uint8_t  Uartx_Tx;//数据头
	rt_uint8_t  Uartx_Len;//有效数据长度
	rt_uint8_t  Uartx_Sta;//发送数据标志位
	char head_flag;//头帧标志位
	char teal_flag;//尾帧标志位
	char command_flag;//命令标志
}Usart_struct;

//HC05结构体指针
extern Usart_struct usartx_struct;
extern Usart_struct *usartx_struct_ptr;

void Hc05_init(void);
void Usart_srtuct_init(void);
void Usart_sent_data(rt_uint8_t *ch,rt_uint8_t len);
int  Judge_command(const char *str);
char Usart_get_data(void);
void Usart_judge_data(char *usart_data);
void Usart_deal_data(const char *sdata,double *parameter);
void Usart_sent_data(rt_uint8_t *ch,rt_uint8_t len);
int Judge_recv(void);

#endif


四、注意

上位机发送的数据是使用英文输入法,使用中文输入法是不对的,还有输入的命令的严格区分大小写的,这样和下位机的程序一致,记得把头帧和尾帧加上。

建议使用Windous10 的串口调试助手,这个比较好用,在微软的商店就能下载

五、更高级实现

由于这篇半年前写的,代码和思路都不够成熟,算是一个初级的版本,有兴趣想要看更高级的实现(更加复杂和难看懂),但使用起来更方便。功能和这篇文章实现的功能是一样的,但更加人性化和效率更高。

文章就放在这里啦,可以自取

基于循环队列的自定义串口协议