代码和函数有部分更新,想要思路是没有变化的。My_usart.c和My_usart.h修改后代码可读性更好,移植更方便。
之前写过一篇基于标准库的串口通信和匿名上位机使用的教程,但是这个上位机的发送功能(结合蓝牙模块)是比较好用的,但接收功能配置起来麻烦,用来串口发送数据调试麻烦,只能发送8位的数据,使用起来很不方便。因此写下这篇,有方便的API接口函数,移植起来方便,使用起来也简单
比较难理解的就是自定义的数据帧的协议的书写,包括头帧和尾帧以及中间分格的帧,但是如果不能理解,移植过来使用也是很简单的。
如果是发送数据,需要显示波形,可以看我之前写的博客。使用匿名上位机,这个上位机使用起来也比较方便,比较适合调试。
匿名V7上位机简单教程
为什么要使用协议,不使用协议不是更简单吗
使用协议的其中一个原因是,为了保证数据准确性,也就是减少错误率,使得判断更为精准。如果自己写过比较简单的串口接收和判断的函数(无协议),遇到比较多数据的情况,很容易引起误判。
这里用正点原子STM32F103ZETX的板子作为实例
主要是配置串口接收中断,发送部分在main中,采取阻塞的方式发送,这里从只是进行串口的配置
使用PA9,PA10,默认配置 (一般都是使用默认配置,波特率115200) , 勾选全局中断(使用接收中断)
串口中断一定要勾选,优先级按自己按照需要安排
串口通信的STM32cubemx串口通信配置到此,接下来是在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)
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 */
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit(&huart1,ch,1,0xFFFF);//阻塞方式打印
HAL_UART_Receive_IT(&huart1,&S_uart_temp,0X01
}
到此,基本的通信配置完成,做完前期工作,STM32能够使用串口进行通信,下面是协议的书写,也是重点。
在进行协议配置前,我们先对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;
}
//函数名: 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);//阻塞方式打印
}
这个是通信层面的协议,只是包括头帧和尾帧的协议。发送数据的一方必须使用此协议,才能进行有效的通信。例如,发送方发送数据为"(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();
}
}
这里面的数据处理,只是为了方便调试参数,比如调试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;//指令为假
}
}
}
}
自己把命令传入这个参数,调用这个函数就能很好地使用了。
建议在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 */
}
输出如下:支持写入多条命令,支持写入命令加参数的模式进行调参,会有写入命令的输出,以及返回数值的输出,如果命令失败不正确,则没有返回输出
调参模式下的输出:
命令模式下使用:和调参命令一致,只是没有输入调参的数字
提示:如果不想理解这么麻烦,只是想用,移植的时候也是很简单的,把全部东西给复制过去,然后把串口配置好,把发送串口发送单个数据的函数给修改,再把指定函数放到中断里面就行了,最后调用判断函数。
改这两个函数,只是把里面发送和接收的函数给改了就行了
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
#define 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");
调参模式下的输出:
命令模式下使用:和调参命令一致,只是没有输入调参的数字
main.c的文件已经囊括了本文90%的内容了
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* © Copyright (c) 2021 STMicroelectronics.
* All rights reserved.
*
* 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 */
Hc05_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 "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 的串口调试助手,这个比较好用,在微软的商店就能下载
由于这篇半年前写的,代码和思路都不够成熟,算是一个初级的版本,有兴趣想要看更高级的实现(更加复杂和难看懂),但使用起来更方便。功能和这篇文章实现的功能是一样的,但更加人性化和效率更高。
文章就放在这里啦,可以自取
基于循环队列的自定义串口协议