基本原理

玩转 STM32 单片机,肯定离不开串口。串口使用一个称为串行通信协议的协议来管理数据传输,该协议在数据传输期间控制数据流,包括数据位数、波特率、校验位和停止位等。由于串口简单易用,在各种产品交互中都有广泛应用。

但在使用串口通讯的时候,我们并不知道对方会发送多少个数据,也不知道数据什么时候发送完,简单来讲就是:如何确保收到一帧完整的数据?

处理方法

串口发送的数据有长有短,如果没有接收完整,肯定会影响后续业务的处理。为了接收不定长数据,常见的处理方法有:

1. 固定格式

比如双方约定,一帧的数据以 AA BB 开头,以 BB AA 结尾,这样在从机接收数据的时候,一旦收到 AA BB 字符,就知道对方要发来一个数据包了,然后就把后面发来的数据保存起来,直到接收到 BB AA 为止。

这种方法简单高效,但缺点就是需要每个字符都进行判断,浪费 CPU 资源,增加功耗。

2. 接收中断+超时判断

串口接收到一个数据时,就会触发接收中断。但如何判断数据已经发送完了呢?

通常来讲,两帧数据之间,会有个时间间隔。因此,我们可以使用一个计时器,如果在一个固定的时间点里没接收到新的字符,则认为一帧数据接收完成了。

3. 空闲中断

串口在空闲时,也就是说串口在一段时间里没有接收到新数据,则会触发空闲中断。细心的同学应该发现了,空闲中断实际上跟上面的超时判断是一样样的,只不过空闲中断是硬件自带,但超时判断需要我们自己实现。

所以,一旦接收到空闲中断,可以认为接收到一帧完整的数据。

注意:空闲中断并不是所有的 MCU 都具备,一般高端一点的 MCU 才有,低端一些的 MCU 并没有空闲中断。

实验目的

使用串口助手实现串口接收不定长数据。

硬件清单

(1)STM32F103C8T6核心板

(2)ST-Link

(3)USB转TTL(CH340驱动)

具体步骤

        首先我们在之前的STM32串口一个字节收发小实验中进行修改,定义一个保存接收数据的数值里面还需要定义一个接收数据大小,这个数据长度在UART.h中使用宏定义,可以使用128字节或者64字节;还需要定义一个十六位无符号的串口计数值和计数器是否发生变化的变量(计数参考变量),最后定义一个接收数据长度。接下来在串口1服务中断函数中修改:

        把接收到的数据存放在uart1_rx_buf[ ]这个数组中,如何判断数据接收完了呢?就是判断uart1_cnt这个变量是否有变化,没有变化代表数据已经接收完毕。

void USART1_IRQHandler(void)
{
	uint8_t receive_data = 0;
	if(__HAL_UART_GET_FLAG(&uart1_handle,UART_FLAG_RXNE) != RESET)
	{
		if(uart1_cnt >= sizeof(uart1_rx_buf))					//判断接收数据是否超限
			uart1_cnt = 0;
		HAL_UART_Receive(&uart1_handle,&receive_data,1,1000);
		uart1_rx_buf[uart1_cnt++] = receive_data;			   	//保存数据
	}
}

        为了判断计数变量是否变化我们需要另外写一个函数,8位无符号的返回值,没有传入参数。返回的参数都是一些宏定义,这样可以简单明了的知道返回值是什么作用,这些宏定义也放在UART.h中,如果计数变量一直为0,可能是数据传输发生了错误,会返回错误的参数;如果计数变量等于计数参考变量则代表接收数据完毕,我们就把计数值清零返回OK状态。如果不是相等的情况下,我们需要把计数变量赋值给计数参考变量,返回同样是错误,用于下次判断。

/*此函数主要目的就是看uart1_cnt是否有变化*/
uint8_t uart1_wait_receive(void)
{
	if(uart1_cnt == 0)					//接收可能错误
		return UART_ERROR;				//返回数据错误标志位
	
	/*判断是否与上次的CNT一致,一致的话代表CNT不动了代表数据接收完整*/
	if(uart1_cnt == uart1_cntPre)		
	{
		uart1_cnt = 0;						//清零计数变量
		return UART_EOK;					//返回数据正确标志位
	}
	
	uart1_cntPre = uart1_cnt;
	return UART_ERROR;
}

        之后编写一个测试的函数,我们可以在主函数的while中反复调用,如果数据接收为OK状态则打印数据,则我们需要封装printf函数,对此我们还需要格外定义一个头文件#include "stdio.h",之后进行重定向到串口1当中,另外写一个封装printf的函数。接收完整后用printf把数据打印出来,之后需要清空数据函数,否则可能会出现数据的重复。

/*测试函数*/
void uart1_receive_test(void)
{
	if(uart1_wait_receive() == UART_EOK)	    	//数据是否接受完整
	{
		printf("recv: %s\r\n",uart1_rx_buf);		//打印数据
		uart1_rx_clear(); 							//清空数据,方便下次接收
	}
		
}

         在c语言中printf 是可以打印到黑窗口上显示的,但是在我们单片机中使用 printf 的话需要重定向到串口中使用。当调用 printf 时,最终会调用 fputc 来逐个发送字符。这里重写了 fputc,使其通过USART1发送数据。

/*重定向到串口1中,printf会调用fputc*/
int fputc(int ch, FILE *f)
{
/*在USART1的状态寄存器中的第6位 TXE
TXE = 1:表示发送数据寄存器(DR)为空,可以写入新数据。
TXE = 0:表示发送数据寄存器未空,需等待。
循环等待,直到TXE标志位为1(即发送寄存器空闲)
*/
	while((USART1->SR & 0X40) == 0);
	
/*在USART1的数据寄存器中,
将字符 ch 写入数据寄存器,硬件会自动将其发送出去。
*/
	USART1->DR = (uint8_t)ch;

/*返回发送的字符,符合标准库对 fputc 的返回值要求*/
	return ch;
}

        此外需要在KEIL中,把USE McroLIB勾上,因为它专门给MCU弄的一个库函数,我们需要使用。

        这里的串口接收数据长度变量还没用到,下一章实验我们会用到,在这个清空函数中用到了memset我们需要包含头文件#include "string.h"

/* 
ptr:指向要填充的内存区域的指针(起始地址)。

value:要填充的值(以 int 形式传入,但实际按 unsigned char 处理)。

num:要填充的字节数。
*/
void *memset(void *ptr, int value, size_t num);

        sizeof 是 C/C++ 中的一个 编译时运算符(不是函数),用于计算 变量、数据类型或表达式所占的内存大小(字节数)。它的返回值类型是 size_t(一种无符号整数类型,通常是 unsigned int 或 unsigned long)

/*清空接收寄存器函数*/
void uart1_rx_clear(void)
{
	memset(uart1_rx_buf,0,sizeof(uart1_rx_buf));
	uart1_rx_len = 0;
}

UART.c

#include "uart1.h"

uint8_t uart1_rx_buf[UART1_RX_SIZE];			        	//保存接收的数据
uint16_t uart1_rx_len = 0; 									//数据长度
uint16_t uart1_cnt = 0;										//计数器计数变量
uint16_t uart1_cntPre = 0;									//判断计数器是否有变化

/*初始化结构体*/
UART_HandleTypeDef uart1_handle = {0};

void uart1_Init(uint32_t baudrate)
{
	uart1_handle.Instance = USART1;							//选择串口1
	uart1_handle.Init.BaudRate = baudrate;					//设置波特率,用入口参数设置
	uart1_handle.Init.WordLength = UART_WORDLENGTH_8B;		//数据长度为8位数据
	uart1_handle.Init.StopBits = UART_STOPBITS_1;			//1位停止位
	uart1_handle.Init.Parity = UART_PARITY_NONE;			//不启用奇偶校验位
	uart1_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;		//不启用硬件流控制
	uart1_handle.Init.Mode = UART_MODE_TX_RX;    			//同时开启接收和发送
	HAL_UART_Init(&uart1_handle);
}

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
	if(huart->Instance == USART1)
	{
		//打开时钟
		__HAL_RCC_USART1_CLK_ENABLE();						// 使能 USART1 时钟
		__HAL_RCC_GPIOA_CLK_ENABLE();						// 使能 GPIOA 时钟(具体看USART1 的 TX/RX引脚)
		
		//调用GPIO初始化函数
		GPIO_InitTypeDef GPIO_InitStructure;
		GPIO_InitStructure.Mode = GPIO_MODE_AF_PP ;			// 复用推挽输出(用于发送数据 TX)
		GPIO_InitStructure.Pin = GPIO_PIN_9;				// PA9(USART1_TX)
		GPIO_InitStructure.Pull = GPIO_PULLUP;				// 上拉电阻(增强信号稳定性)
		GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;			// 高速模式
		HAL_GPIO_Init(GPIOA,&GPIO_InitStructure);
		
		GPIO_InitStructure.Mode = GPIO_MODE_AF_INPUT;		// 复用输入(用于接收数据 RX)
		GPIO_InitStructure.Pin = GPIO_PIN_10;				// PA10(USART1_RX)
		HAL_GPIO_Init(GPIOA,&GPIO_InitStructure);	

		HAL_NVIC_EnableIRQ(USART1_IRQn);					// 使能 USART1 全局中断
		HAL_NVIC_SetPriority(USART1_IRQn,2,2);				// 设置中断优先级
		
		__HAL_UART_ENABLE_IT(huart,UART_IT_RXNE);	    	// 使能 "接收缓冲区非空" 中断
	}
}

void USART1_IRQHandler(void)
{
	uint8_t receive_data = 0;
	if(__HAL_UART_GET_FLAG(&uart1_handle,UART_FLAG_RXNE) != RESET)
	{
		if(uart1_cnt >= sizeof(uart1_rx_buf))		       	//判断接收数据是否超限
			uart1_cnt = 0;
		HAL_UART_Receive(&uart1_handle,&receive_data,1,1000);
		uart1_rx_buf[uart1_cnt++] = receive_data;			//保存数据
	}
}

/*此函数主要目的就是看uart1_cnt是否有变化*/
uint8_t uart1_wait_receive(void)
{
	if(uart1_cnt == 0)					//接收可能错误
		return UART_ERROR;				//返回数据错误标志位
	
	/*判断是否与上次的CNT一致,一致的话代表CNT不动了代表数据接收完整*/
	if(uart1_cnt == uart1_cntPre)		
	{
		uart1_cnt = 0;			    	//清零计数变量
		return UART_EOK;		    	//返回数据正确标志位
	}
	
	uart1_cntPre = uart1_cnt;
	return UART_ERROR;
}

/*清空接收寄存器函数*/
void uart1_rx_clear(void)
{
	memset(uart1_rx_buf,0,sizeof(uart1_rx_buf));
	uart1_rx_len = 0;
}

/*重定向到串口1中,printf会调用fputc*/
int fputc(int ch, FILE *f)
{
	while((USART1->SR & 0X40) == 0);
	
	USART1->DR = (uint8_t)ch;
	return ch;
}

/*测试函数*/
void uart1_receive_test(void)
{
	if(uart1_wait_receive() == UART_EOK)			//数据是否接受完整
	{
		printf("recv: %s\r\n",uart1_rx_buf);		//打印数据
		uart1_rx_clear(); 							//清空数据,方便下次接收
	}
		
}

UART.h

#ifndef __USART_H__
#define __USART_H__

#include "sys.h"
#include "string.h"
#include "stdio.h"

#define UART1_RX_SIZE 128
#define UART1_TX_SIZE 64

#define UART_EOK			0			//接收成功
#define UART_ERROR			1			//接收错误
#define UART_ETIMEOUT		2			//接收超时
#define UART_EINVAL			3			//接收非法


void uart1_Init(uint32_t baudrate);
void uart1_rx_clear(void);
void uart1_receive_test(void);



#endif


主函数main.c

在while中不断调用测试函数,然后延时10毫秒再检测一次。

#include "sys.h"
#include "delay.h"
#include "uart1.h"


int main(void)
{
    HAL_Init();                             /* 初始化HAL库 */
    stm32_clock_init(RCC_PLL_MUL9); 		/* 设置时钟, 72Mhz */
    

    uart1_Init(115200);
    while(1)
    { 
			uart1_receive_test();
			delay_ms(10);
    }
}

实验现象

        发送一个经典的Hello,World!试试数据的不定长收发,大家可以试试更有趣的~

总结:本文介绍了STM32串口接收不定长数据,包括初始化配置、中断处理和数据收发。希望通过这个小实验能帮你稳固串口通信的知识点。如果有疑问或发现错误,欢迎在评论区留言讨论!

📜 下一篇预告:
本文将介绍《STM32串口接收不定长数据2》的空闲中断用法,教你如何用串口实现不定长数据收发,把空闲中断用法知识点体现在实际的操作上,能更加有趣的稳固知识噢,点击关注不迷路~

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐