一. 功能与环境

这两天好好整理了一下STM32的串口通信,主要测试DMA方式发送与接收,以及配合串口空闲中断接收不定长数据。前后在F103和F767上都测试通过了。不过依然有一些问题想不明白,算了不甩它,暂且先能实现功能就好。

本文环境:

  • Keil MDK5.14
  • STM32CubeMX6.2.1
  • 开发板/芯片:正点原子精英板F103ZET6/正点原子阿波罗F767IGT6

实现功能:

  • 串口DMA发送
  • 串口DMA+空闲中断接收不定长数据

下载链接:

二. 串口DMA与空闲中断原理

STM32串口通信包括三种方式,阻塞模式、中断方式与DMA方式。库里面的相关函数如下。

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

一般来说,我们使用阻塞模式的发送与中断方式的接收,即HAL_UART_TransmitHAL_UART_Receive_IT函数,上一篇博客我写了一篇单字节中断接收的例程实现五个串口通信,就是这个原理(参见STM32_HAL库_CubeMx实现STM32F1五个串口通信(单字节中断接收))。接收一个字节进入一次中断,中断中可以判断帧头帧尾。这种方式原理简单,只要按照协议解析即可,但是需要频繁进入中断。DMA方式直接开辟内存与外设之间的数据传输通道,可以减轻CPU的负担,增加数据处理速度。

简单介绍几个函数:

(1) 串口DMA发送函数

HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

函数主要功能是以DAM模式发送pData指针指向的数据中固定长度的数据,并同时设置和使能DMA中断。DMA中断函数最终调用的其实是串口的中断函数。进入到HAL_UART_Transmit_DMA这个函数中可以看到,它将DMA传输完成、半完成、错误的回调函数分别定向到了串口DMA传输完成、半完成、错误的回调函数UART_DMATransmitCplt、UART_DMATxHalfCplt、UART_DMAError
在这里插入图片描述

再进入到UART_DMATransmitCplt函数中可以看到,它最终其实是调用了__weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)这一个串口发送完成的回调函数。我们经常使用的是另一个串口接收完成回调函数__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)所以我们需要做的就是重写 __weak void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)这一个回调函数,进行中断处理。这个回调函数我们要用到

在这里插入图片描述

(2) 串口DMA接收函数

HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

函数说明:此函数的功能在DMA模式下接收大量数据,同时设置DMA线和哪个串口外设连接,以及将DMA线接收到的数据搬 *pData对应地内存中,和上面DMA发送函数一样,此函数同时具有设置和使能DMA中断的功能。

如前所说,一直跳转,可以看到接收完成后会调用串口接收完成回调函数__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)但是请注意我们用不到这个函数,因为我们将要使用到串口空闲中断,中断处理逻辑全部放在空闲中断中

(3) 串口DMA停止函数

HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart)

这个函数在DMA中断中要用到(包括发送和接收),调用这个函数,然后处理数据,之后再重新打开DMA发送与接收。

(4) 查询DMA剩余传输数据个数

__HAL_DMA_GET_COUNTER(__HANDLE__)

这是一个宏定义,定义如下,它是查询NDTR寄存器的值,这个寄存器存储的是DMA传输的剩余传输数量。在接收数据时,用定义的最大传输数量减去这个剩余数量就是已经接收的数据个数。在发送数据时,用定义的最大传输数量减去这个剩余数量就是已经发送的数据个数。

#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->NDTR)

(5) 清除空闲中断标志

__HAL_UART_CLEAR_IDLEFLAG(__HANDLE__)

这也是一个宏定义,在空闲中断中调用。

三. CubeMx配置工程

下面我们在CubeMx中配置工程,系统、时钟等跳过,只看串口和DMA设置这一块。具体的大家可以下载工程来看。

串口DMA配置步骤如图,步骤3那里点击Add添加DMA即可。步骤5内存对齐方式模式Byte,方向Tx和Rx的默认方向,这些都不用管。步骤4模式选择一定注意,Tx选择正常Normal模式,Rx选择循环模式Circular这里我的测试是F7系列选Normal就行,F1系列只能选Circular,否则就会死机,原因我也不知道,哪位道友明白告诉我一声哈!
在这里插入图片描述
在这里插入图片描述
然后点击Generate Code生成代码。

我的习惯是只用Cubemx生成驱动代码,然后移植到自己工程中。所以下面的代码讲解不是Cubemx工程框架哈。

四. 驱动代码与应用程序

(1) DMA时钟与中断初始化

首先新建dma.c和dma.h文件,移植进DMA的代码。代码中有注释就不多说了。

// dam.h文件
#include "sys.h"

#define USART_DMA_TX_BUFFER_MAXIMUM			128    // DMA缓冲区大小
#define USART_DMA_RX_BUFFER_MAXIMUM			128    // DMA缓冲区大小

extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;

void MX_DMA_Init(void);
// dam.c文件
#include "dma.h"

DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;

void MX_DMA_Init(void)
{
  __HAL_RCC_DMA1_CLK_ENABLE();

  HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
  HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}

void DMA1_Channel4_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_usart1_tx);
}

void DMA1_Channel5_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_usart1_rx);
}

(2) 自定义变量与函数

然后在uart.c和uart.h中进行一些参数定义和自定义函数。处理数据会用到,变量和函数的定义都在uart.c文件中,在uart.h中加进行声明。

// uart.h文件
//变量外部声明
extern UART_HandleTypeDef huart1; //UART句柄
extern u8 usart1_rx_buffer[USART_DMA_RX_BUFFER_MAXIMUM]; //串口1的DMA接收缓冲区
extern u8 usart1_tx_buffer[USART_DMA_TX_BUFFER_MAXIMUM]; //串口1的DMA发送缓冲区
extern u8 usart1_rx_flag; //DMA接收成功标志 0,未接收到/1,接收到等待处理
extern u16 usart1_rx_len; //DMA一次空闲中断接收到的数据长度
extern u8 receive_data[USART_DMA_RX_BUFFER_MAXIMUM];	  //DMA接收数据缓存区

void HAL_UART_ReceiveIdle(UART_HandleTypeDef *huart);		//串口空闲中断处处理函数
void UART1_TX_DMA_Send(u8 *buffer, u16 length);				//调用HAL_UART_Transmit_DMA函数进行串口发送
void Debug_printf(const char *format, ...);					//调用UART1_TX_DMA_Send实现格式化输出

(3) 串口DMA初始化函数

// uart.c文件
void uart1_init(u32 bound)
{
	//UART 初始化设置
	huart1.Instance = USART1;
	huart1.Init.BaudRate = bound;
	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;
	HAL_UART_Init(&huart1);
	
	//开启空闲接收中断
	__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
	//打开DMA接收,指定接收缓存区和接收大小
	HAL_UART_Receive_DMA(&huart1, (uint8_t *)&usart1_rx_buffer, USART_DMA_RX_BUFFER_MAXIMUM);
}

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
	//GPIO端口设置
	GPIO_InitTypeDef GPIO_InitStruct;

	if (huart->Instance == USART1)
	{
		__HAL_RCC_USART1_CLK_ENABLE();
		__HAL_RCC_GPIOA_CLK_ENABLE();
	
		GPIO_InitStruct.Pin = GPIO_PIN_9;
		GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

		GPIO_InitStruct.Pin = GPIO_PIN_10;
		GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
		GPIO_InitStruct.Pull = GPIO_NOPULL;
		HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

		  /* USART1 DMA Init */
		/* USART1_RX Init */
		hdma_usart1_rx.Instance = DMA1_Channel5;
		hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
		hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
		hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
		hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
		hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
		hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
		hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
		
		HAL_DMA_Init(&hdma_usart1_rx);
		__HAL_LINKDMA(huart,hdmarx,hdma_usart1_rx);

		/* USART1_TX Init */
		hdma_usart1_tx.Instance = DMA1_Channel4;
		hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
		hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
		hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
		hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
		hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
		hdma_usart1_tx.Init.Mode = DMA_NORMAL;
		hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
				
		HAL_DMA_Init(&hdma_usart1_tx);
		__HAL_LINKDMA(huart,hdmatx,hdma_usart1_tx);

		HAL_NVIC_SetPriority(USART1_IRQn, 3, 3);
		HAL_NVIC_EnableIRQ(USART1_IRQn);
	
	}
}

(4) 串口空闲中断函数处理

// uart.c文件

//串口1中断服务程序
void USART1_IRQHandler(void)
{
	HAL_UART_ReceiveIdle(&huart1);
	HAL_UART_IRQHandler(&huart1); //调用HAL库中断处理公用函数
	
	/* 4.重新打开串口DMA接收 */
	while (HAL_UART_Receive_DMA(&huart1,(u8 *)usart1_rx_buffer, USART_DMA_RX_BUFFER_MAXIMUM)!=HAL_OK) 
}
void HAL_UART_ReceiveIdle(UART_HandleTypeDef *huart)
{
	//当触发了串口空闲中断
    if((__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE) != RESET)) 
    {
		uint32_t tmp_flag = 0;
		uint32_t temp;
		if(huart->Instance == USART1)
		{
			/* 1.清除标志 */
			__HAL_UART_CLEAR_IDLEFLAG(huart); //清除空闲标志

			/* 2.读取DMA */
			HAL_UART_DMAStop(huart); //先停止DMA,暂停接收
			//这里应注意数据接收不要大于 USART_DMA_RX_BUFFER_MAXIMUM
			usart1_rx_len = USART_DMA_RX_BUFFER_MAXIMUM - (__HAL_DMA_GET_COUNTER(&hdma_usart1_rx)); //接收个数等于接收缓冲区总大小减剩余计数
			
			/* 3.搬移数据进行其他处理 */
			memcpy(receive_data, usart1_rx_buffer, usart1_rx_len); 
			usart1_rx_flag = 1; //标志已经成功接收到一包等待处理
		}
    }
}

串口空闲中断的处理顺利在注释中标注出了。首先调用__HAL_UART_CLEAR_IDLEFLAG(huart)清除空闲中断标志,然后调用HAL_UART_DMAStop(huart)停止DMA暂停接受,之后获取接受个数,拷贝接收缓冲区的数据到自定义内存中,等待主函数处理,置位标志位。最后,重新调用HAL_UART_Receive_DMA开启DMA接收。

这里提醒一点, HAL_UART_Receive_DMA(&huart1,(u8 *)usart1_rx_buffer, USART_DMA_RX_BUFFER_MAXIMUM)最好放在 USART1_IRQHandler(void)中断函数的最后进行。如果放在HAL_UART_IRQHandler(&huart1)前面可能出错。 事实上我就出了错。。。

(5) 串口发送函数处理

// uart.c文件
//串口1的DMA发送
void UART1_TX_DMA_Send(u8 *buffer, u16 length)
{
    //等待上一次的数据发送完毕
	while(HAL_DMA_GetState(&hdma_usart1_tx) != HAL_DMA_STATE_READY);
    //while(__HAL_DMA_GET_COUNTER(&hdma_usart1_tx));
	
    //关闭DMA
    __HAL_DMA_DISABLE(&hdma_usart1_tx);

    //开始发送数据
    HAL_UART_Transmit_DMA(&huart1, buffer, length);
}

//串口1的DMA发送printf
void Debug_printf(const char *format, ...)
{
	uint32_t length = 0;
	va_list args;
	
	__va_start(args, format);
	
	length = vsnprintf((char*)usart1_tx_buffer, sizeof(usart1_tx_buffer), (char*)format, args);
	
	UART1_TX_DMA_Send(usart1_tx_buffer, length);
}


void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
	if (huart->Instance == USART1) //如果是串口1
	{
		// 在F7系列是可以不写的,F1必须写
		__HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4); //清除DMA2_Steam7传输完成标志
		HAL_UART_DMAStop(&huart1);		//传输完成以后关闭串口DMA,缺了这一句会死机
	}
}

串口发送调用HAL_UART_Transmit_DMA函数,这里要注意F1的芯片要写发送完成回调函数,在里面关闭DMA,不然会死机。。。 而F7写不写都行的,看来内部机制不一样。当然也可能是我的处理有问题。还是虚心向小伙伴们请教,一起探讨。

UART1_TX_DMA_Send是自定义函数,主要先等待上一帧数据发送完成再发送下一帧数据。Debug_printf函数是自定义函数,实现printf的功能用法。

(6) 主函数

// main.c文件
int main(void)
{
	u8 len;	
	u16 times=0;
	u8 count=0;		

    HAL_Init();                    	 	//初始化HAL库    
    Stm32_Clock_Init(RCC_PLL_MUL9);   	//设置时钟,72M
	delay_init(72);               		//初始化延时函数
	MX_DMA_Init();						//DMA初始化
	uart1_init(115200);					//初始化串口
	uart2_init(115200);					//初始化串口
	uart3_init(115200);					//初始化串口
	uart4_init(115200);					//初始化串口
	uart5_init(115200);					//初始化串口
	LED_Init();							//初始化LED	
	KEY_Init();							//初始化按键
	
    while(1)
    {
		if (usart1_rx_flag)
		{
			count++;
			Debug_printf("接收数据次数: %d\r\n接收数据长度: %d\r\n接收数据内容:%s",count,usart1_rx_len,receive_data);
			usart1_rx_flag = 0;
			usart1_rx_len = 0;
		}
		
		times++;
		if(times%200==0) 
		{ 
			Debug_printf("**********************串口DMA测试************************\r\n");
		}
		if(times%10==0)  LED0=~LED0;//闪烁LED,提示系统正在运行.
		delay_ms(10);   
    }
}

主函数中通过判断接收完成标志位判断是否接收到新一帧数据,然后通过DMA方式发送。串口调试助手现象如图。
在这里插入图片描述至此功能完成。

五. 几点勘误

总结一下几点雷区。

  1. F1系列板子串口DMA接收要设置成循环模式hdma_usart1_rx.Init.Mode = DMA_CIRCULAR,而且要像正常模式一样用,每次中断接收后重新调用 串口DMA接收函数
    HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)。F7系列正常使用DMA_NORMAL模式即可。

  2. F1系列板子串口DMA发送完成中断函数必须写,F7系列经测试可以不写。

  3. 串口DMA不能连续发送两次数据,例如像下面这样写是不行的

    HAL_UART_Transmit_DMA(&huart1, buffer, 20);
    HAL_UART_Transmit_DMA(&huart1, buffer, 20);

第二次调用HAL_UART_Transmit_DMA会返回busy。解决这个问题有两种方式,一种是加延时,等待上一帧数据传输完毕,如下:

    HAL_UART_Transmit_DMA(&huart1, buffer, 20);
    delay_ms(10);
    HAL_UART_Transmit_DMA(&huart1, buffer, 20);

第二种方法是通过查询DMA的状态或者串口传输的状态来判断是否准备好可以发送下一帧数据。如下:

    HAL_UART_Transmit_DMA(&huart1, buffer, 20);
	while(HAL_DMA_GetState(&hdma_usart1_tx) != HAL_DMA_STATE_READY);
    //while(__HAL_DMA_GET_COUNTER(&hdma_usart1_tx));
    HAL_UART_Transmit_DMA(&huart1, buffer, 20);

很可惜的这种方式我试过,并不管用。目前我能测试通过的只有加延时的方式。另一种方法就是所有数据打包一次发送,也就是一个控制周期里只调用一次HAL_UART_Transmit_DMA函数。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐