这两天好好整理了一下STM32的串口通信,主要测试DMA方式发送与接收,以及配合串口空闲中断接收不定长数据。前后在F103和F767上都测试通过了。不过依然有一些问题想不明白,算了不甩它,暂且先能实现功能就好。
本文环境:
- Keil MDK5.14
- STM32CubeMX6.2.1
- 开发板/芯片:正点原子精英板F103ZET6/正点原子阿波罗F767IGT6
实现功能:
- 串口DMA发送
- 串口DMA+空闲中断接收不定长数据
下载链接:
- STM32F1串口DMA与空闲中断接收不定长数据.zip
- STM32F767串口DMA与空闲中断接收不定长数据.zip
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_Transmit
与HAL_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中配置工程,系统、时钟等跳过,只看串口和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方式发送。串口调试助手现象如图。
至此功能完成。
总结一下几点雷区。
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模式即可。
F1系列板子串口DMA发送完成中断函数必须写,F7系列经测试可以不写。
串口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
函数。