解决STM32串口高速上传数据无法同时接收数据的问题

一. 问题描述

之前做的一个项目中用了STM32F7的串口三做数传用,定时上传机器人数据,上位机(地面站)接收数据在界面显示,同时上位机可以发送数据到机器人,达到调参、校准等目的。数据上传正常,数据下发一直有点问题,时灵时不灵,改参数一会能改一会不能改,就很迷。因为下传数据的链路是上位机串口发送——下位机串口接收——下位机修改参数——下位机上传新的参数——上位机接收——上位机显示新的参数,迷茫的我一直以为是上位机写的出了问题(用的LabWindows多线程),最近有时间总结,终于定位到问题是STM32的串口高速上传时接收数据会出现锁死不能接收

先说结论:造成无法接收的原因是使用了HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)这个函数,造成了串口堵塞,STM32的HAL库函数有时候是真心埋雷。

二. 解决方案

(一)使用字节发送而不是帧发送

经过调试发现是使用HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)这个函数出了问题,我们看看这个函数内部干了什么

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
    
    
	uint16_t *tmp;
	uint32_t tickstart = 0U;

	/* Check that a Tx process is not already ongoing */
	if (huart->gState == HAL_UART_STATE_READY)
	{
    
    
		if ((pData == NULL) || (Size == 0U))
		{
    
    
			return HAL_ERROR;
		}

		/* Process Locked */
		__HAL_LOCK(huart);

		huart->ErrorCode = HAL_UART_ERROR_NONE;
		huart->gState = HAL_UART_STATE_BUSY_TX;

		/* Init tickstart for timeout managment*/
		tickstart = HAL_GetTick();

		huart->TxXferSize = Size;
		huart->TxXferCount = Size;
		while (huart->TxXferCount > 0U)
		{
    
    
			huart->TxXferCount--;
			if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
			{
    
    
				return HAL_TIMEOUT;
			}
			if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
			{
    
    
				tmp = (uint16_t *)pData;
				huart->Instance->TDR = (*tmp & (uint16_t)0x01FFU);
				pData += 2;
			}
			else
			{
    
    
				huart->Instance->TDR = (*pData++ & (uint8_t)0xFFU);
			}
		}
		if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
		{
    
    
			return HAL_TIMEOUT;
		}

		/* At end of Tx process, restore huart->gState to Ready */
		huart->gState = HAL_UART_STATE_READY;

		/* Process Unlocked */
		__HAL_UNLOCK(huart);

		return HAL_OK;
	}
	else
	{
    
    
		return HAL_BUSY;
	}
}

里面有两行代码__HAL_LOCK(huart)__HAL_UNLOCK(huart),注释说是进程上锁和进程解锁,一看这东西就要命,串口上锁了还能进入接收中断么??不好说,感觉就有问题。HAL_UART_Transmit是一次发送一串数据,一旦发送数据量大必然导致不能正常接收。好的,那就换个方法,不按一帧数据发,按单个字节发。

看了看以前的标准库是有单个字节的发送函数的:

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
    
    
  /* Check the parameters */
  assert_param(IS_USART_ALL_PERIPH(USARTx));
  assert_param(IS_USART_DATA(Data)); 
    
  /* Transmit Data */
  USARTx->DR = (Data & (uint16_t)0x01FF);
}

但是HAL库里面没了,没了。。。没关系,自己写一个:

int Uart1SendChar(u8 ch)
{
    
    
    while((USART1->ISR&0X40)==0);//循环发送,直到发送完毕   
	USART1->TDR=(u8)ch; 
    return ch;	
}

这个很简单,就是一直往发送数据寄存器TDR填数据,它就发送出去了。

现在将发送数据改为使用上述函数发送就行了。比如原来是使用

HAL_UART_Transmit(&UART1_Handler, send_buf, 25, 1000);

对应改成

for (i=0;i<25;i++)
{
    
    
	Uart1SendChar(send_buf[i]);
}

再次测试,bingo,接收正常了。

(二)使用DMA发送

第二种方法就是使用DMA了,直接存储访问,它的优势数据传输不经过CPU,节省CPU资源,提高计算速度。关于串口DMA的教程非常之多,这里面一个很烦躁的问题就是中断函数的处理,HAL库真心把我整蒙了。虽然最后也实现了发送和接收不定长数据的功能,但是库没看明白的感觉很糟心。后面计划专门写一篇串口通信/串口DMA通信的中断机制的文章,把HAL库代码干透彻,这里就不细说了。

下面我们实现使用串口1的DMA发送数据与DMA接收不定长数据,然后使用串口2的普通方式将串口1接收的数据打印出来观察。

新建一个工程,在Cubemx中开启串口1、串口2、串口3,同时打开他们的DMA,勾选中断。

在这里插入图片描述
在这里插入图片描述串口1、2、3的配置是一样的,注意选择对应的Sream就行了。

默认生成的代码已经有了串口和DMA的驱动代码。直接调用HAL_UART_Transmit_DMA就行了。

由于要使用串口空闲中断来接收不定长数据,所以要使能串口空闲中断,修改默认的串口初始化函数

static void MX_USART1_UART_Init(void)
{
    
    

	/* USER CODE BEGIN USART1_Init 0 */

	/* USER CODE END USART1_Init 0 */

	/* USER CODE BEGIN USART1_Init 1 */

	/* USER CODE END USART1_Init 1 */
	huart1.Instance = USART1;
	huart1.Init.BaudRate = 115200;
	huart1.Init.WordLength = UART_WORDLENGTH_8B;
	huart1.Init.StopBits = UART_STOPBITS_1;
	huart1.Init.Parity = UART_PARITY_NONE;
	huart1.Init.Mode = UART_MODE_TX_RX;
	huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	huart1.Init.OverSampling = UART_OVERSAMPLING_16;
	huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
	huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
	if (HAL_UART_Init(&huart1) != HAL_OK)
	{
    
    
		Error_Handler();
	}
	/* USER CODE BEGIN USART1_Init 2 */
	__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);		   //使能idle中断
	HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE); //打开DMA接收,数据存入rx_buffer数组中。
	/* USER CODE END USART1_Init 2 */
}

添加了最后的两行代码,就是打开IDLE中断,开始串口DMA传输。

然后添加中断函数,先在cubemx生成的中断文件(stm32F7xxx.it.c)里添加外部数据声明,

extern volatile uint8_t rx_len;
extern volatile uint8_t recv_end_flag;
extern uint8_t rx_buffer[100];
extern char BUFFER_SIZE;

使用的extern,因为定义在了main.c中,在main.c头部增加定义:

volatile uint8_t rx_len;
volatile uint8_t recv_end_flag;
uint8_t rx_buffer[100];
char BUFFER_SIZE;

中断函数直接写在USART1_IRQHandler(void)中,回调函数什么的见鬼去吧。。。

void USART1_IRQHandler(void)
{
    
    
	/* USER CODE BEGIN USART1_IRQn 0 */

	/* USER CODE END USART1_IRQn 0 */
	HAL_UART_IRQHandler(&huart1);
	/* USER CODE BEGIN USART1_IRQn 1 */

	uint32_t tmp_flag = 0;
	uint32_t temp;
	tmp_flag = __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE);    //获取IDLE标志位
	if ((tmp_flag != RESET))      //idle标志被置位
	{
    
    
		__HAL_UART_CLEAR_IDLEFLAG(&huart1);
	    //清除标志位
		temp = huart1.Instance->ISR; 			//清除状态寄存器SR(F0的HAL库USART_TypeDef结构体中名字为ISR:USART Interrupt and status register),读取SR可以清楚该寄存器
		temp = huart1.Instance->RDR; 			//读取数据寄存器中的数据,读取DR(F0中为RDR:USART Receive Data register)
		HAL_UART_DMAStop(&huart1);
		temp = hdma_usart1_rx.Instance->NDTR;   //获取DMA中未传输的数据个数,NDTR寄存器分析见下面
		rx_len = BUFFER_SIZE - temp;       		//总计数减去未传输的数据个数,得到已经接收的数据个数
		recv_end_flag = 1;                 		//接受完成标志位置1
	}
	/* USER CODE END USART1_IRQn 1 */
}

中断函数里面首先获取IDLE空闲中断标志位,然后清除标志位,停止串口DMA传输,获取接收到的数据个数,置位接收完成标志,接收数据的处理放在main函数中。

在main.c中添加串口1和串口2的发送缓冲区:

uint8_t i = 0;
uint8_t count = 0;
uint8_t send_buf[25] = {
    
    0};
uint8_t send[25] = {
    
    0};
send_buf[0] = 0xAA;
send_buf[1] = 0xAA;
for (i = 2; i < 25; i++)
	send_buf[i] = 0x50;

然后修改while (1)循环

while (1)
{
    
    
	/* USER CODE END WHILE */
	HAL_UART_Transmit_DMA(&huart1, send_buf, 25); //开启DMA传输
												  //	HAL_UART_Transmit(&huart1,send_buf,25,1000);//开启DMA传输
	if (recv_end_flag == 1)
	{
    
    
		//printf("rx_len=%d\r\n", rx_len);					//打印接收长度
		count++;
		send[0] = count; 
		HAL_UART_Transmit(&huart2, send, 1, 200);			//接收数据打印出来,标记接收数据的数量
		HAL_UART_Transmit(&huart2, rx_buffer, rx_len, 200); //接收数据打印出来
		HAL_UART_Transmit(&huart2, &send[1], 2, 200); //接收数据打印出来
		for (uint8_t i = 0; i < rx_len; i++)
		{
    
    
			rx_buffer[i] = 0; //清接收缓存
		}
		rx_len = 0;											   //清除计数
		recv_end_flag = 0;									   //清除接收结束标志位
		HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE); //重新打开DMA接收
	}

	HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
	HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);
	HAL_Delay(20);
	/* USER CODE BEGIN 3 */
}

在while循环中通过串口1DMA定时上传数据,通过串口2将串口1接收到的数据打印出来。在计算机上打开两个串口调试助手,分别连接两个串口,查看串口1和串口2发送回来的 数据。如图所示,左侧是串口1发送回的数据,点击发送就可以发数据给板子。右边的是串口2 发送回的数据。点左侧的发送右边会显示出左边发送的数据。

注意到每次都是一帧对的数据,一帧错的数据,好吧我又迷了,迷了。。。调试中。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_30267617/article/details/115276566