本篇基于DMA学习理解的基础上( 往期文章)再次深入使用串口DMA,使用到了DMA,实现了串口接收不定长数据和串口接收定长数据。对DMA使用配置和程序运行的过程做了详细的理解和分析。 文末附工程
好久没用STM32CUBEMX工具了所以这次顺便用了一下,所以这篇文章代码都是是基于HAL库写的,头痒痒的感觉要长脑子了。
DMA的使用
1、STM32CUBEMX配置
按照我自己的习惯,使用STM32CUBEMX的时候先无脑配置好时钟:
点①处展开所有项目,然后选择②处RCC,右侧出现配置界面,在③处与你自己的板子上外接的晶振相匹配,我的板子外部晶振有两个,一个是HSE外部高速8MHz的,另一个是LSE外部低速的32.768KHz的所以这里两个框的下拉都选择了有振荡器。可以看到最右边的芯片上的引脚因为你的配置都已经做出了相应的更改变为绿色的了。下一步:
RCC引脚配置好了后,点击①处开始配置时钟,点击②处输入你的HSE时钟频率,点击③处将HSE作为PLL的输入,再点击④处使用PLL作为SYSCLK系统时钟的频率输入,这之间过程中的参数除了②处的需要你手动更改,其他都不需要动,将②③④都操作好了后,点击⑤处输入168然后回车,它会自动根据你设置的系统时钟频繁进行调整。至此时钟配置结束,下一步:
点击①处返回配置界面,点击②处SYS,在③处配置好DEBUG选择,我用的是ST-Link所以选择了Serial Wire,其他DEBUG方式可以百度下。下面的Timebase Source是选择一个产生基准的时钟,这里选择了SysTick系统滴答时钟。在HAL库中有一个函数叫HAL_Delay是一个延时函数,这里选择的时钟就会作为这个函数的时间来源,其实本质上就是通过定时器来做延时。下一步:
我们配置一个串口,点击①处串口1,点击②处配置为异步全双工的,然后在③处可以设置波特率,奇偶校验等参数。下一步:
配置好一个最基础的串口后我们增加DMA功能,点击①处设置DMA,点击②处ADD,然后在点击③处选择DMA请求为串口1的接收,CUBEMX会自动给你找到对应的Stream数据流通道以及设置好传输方向等基础信息,与标准库相比确实省力一些。然后再④处可以设置DMA模式(循环还是普通)、目的和源地址是否自增,还可以设置FIFO,具体参数的含义可以参照(往期文章)里面都有详细的讲解参数的含义。下一步:
最后检查一下设置中断,点击①处NVIC,在右侧界面可以看到设置过的中断,在②处可以看到已经有了串口1中断以及DMA2数据流2中断,我们知道使用DMA可以不用开启DMA中断,此时可以点击③处然后可以把DMA2中断取消,这里我们先按默认的来开启DMA中断,这里我还配置了中断优先级可以在后面的框框里修改。最后生成一下代码就OK了,直接来看程序。
2、CUBEMX程序解读
HAL库生成的代码必然包含两个固定的函数HAL_Init和SystemClock_Config,这里我们不关心只看本节代码的核心,先看一下 MX_DMA_Init():
/**
* Enable DMA controller clock
*/
void MX_DMA_Init(void)
{
/* DMA controller clock enable */
__HAL_RCC_DMA2_CLK_ENABLE();
/* DMA interrupt init */
/* DMA2_Stream2_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
}
由于我们开启了DMA中断所以这个函数开启了DMA时钟和设置数据流中断应该很好理解。
MX_USART1_UART_Init():
/* USART1 init function */
void MX_USART1_UART_Init(void)
{
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;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
}
这个函数主要是对串口参数的初始化,波特率、奇偶校验等参数,里面最核心的函数是HAL_UART_Init,函数处理过程如下:
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
{
1、检查参数的合法性
2、HAL_UART_MspInit(huart);对底层硬件初始化
3、将设置好的参数填入寄存器
}
接下来我们看初始化中调用的HAL_UART_MspInit,这个函数也是CUBEMX给我们配置好我们可以不用做修改的:
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
/* USER CODE BEGIN USART1_MspInit 0 */
/* USER CODE END USART1_MspInit 0 */
/* USART1 clock enable */
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**USART1 GPIO Configuration
PA9 ------> USART1_TX
PA10 ------> USART1_RX
*/
GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
/* USART1 DMA Init */
/* USART1_RX Init */
hdma_usart1_rx.Instance = DMA2_Stream2;
hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
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_NORMAL;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);
/* USART1 interrupt Init */
HAL_NVIC_SetPriority(USART1_IRQn, 1, 1);
HAL_NVIC_EnableIRQ(USART1_IRQn);
/* USER CODE BEGIN USART1_MspInit 1 */
/* USER CODE END USART1_MspInit 1 */
}
}
里面是对串口引脚以及DMA的设置,配置了串口的中断,最后点睛的一句,将串口的接收和DMA捆绑在一起:
__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);
可以看到到目前为止所有的配置都已经完成了,STM32CUBEMX帮我们实现了配置参数以及配置中断等操作,留给我们的就剩下开启中断以及对中断后逻辑部分的处理。最后再看一下stm32f4xx_it.c文件下拉到最后可以看到CUBEMX把中断的函数都预留写好了,串口中断使用了HAL_UART_IRQHandler,DMA中断使用了HAL_DMA_IRQHandler,这两个函数的作用是判断当前的中断类型是什么,然后根据中断的类型调用对应中断类型回调函数:
/**
* @brief This function handles USART1 global interrupt.
*/
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 */
/* USER CODE END USART1_IRQn 1 */
}
/**
* @brief This function handles DMA2 stream2 global interrupt.
*/
void DMA2_Stream2_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream2_IRQn 0 */
/* USER CODE END DMA2_Stream2_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_usart1_rx);
/* USER CODE BEGIN DMA2_Stream2_IRQn 1 */
/* USER CODE END DMA2_Stream2_IRQn 1 */
}
例如在HAL_UART_IRQHandler中,如果当前的串口中断是接收中断则会调用UART_Receive_IT,在这个函数中判断接收状态,最后会调用HAL_UART_RxCpltCallback回调函数,用户可以通过重定义这个回调函数实现自己的功能,类似的还可以看到里面还有发送完成中断的回调函数HAL_UART_TxCpltCallback。
在DMA中断处理HAL_DMA_IRQHandler中,我们主要关心的是传输完成中断,因此需要我们去编写回调函数。
分析到这里大家应该知道几点:
1、CUBEMX将外设的配置以及中断的配置都写好了
2、用户需要自己去开启需要的中断,以及编写对应的中断回调函数
3、不定长数据接收程序解读(IDLE+DMA)
首先我们理清一下思路,我们需要实现的是串口接收不定长数据,因为每帧的数据都是不定长的,所以我们无法将接收到的数据个数作为结束的标志,这里由于我们使用DMA所以考虑最常见的方法就是利用串口空闲中断IDLE。简单解释一下原理,每当一帧数据被发送到串口后,会通过DMA将数据接收进来,最后没数据了会进入串口空闲中断,在中断中可以重新配置DMA开启下一次接收和设置标志位提示接收完成。所以不定长接收可以不需要DMA中断,但是正好趁这次机会我们可以使用一下DMA中断,因此还是保留了DMA中断。
第一步开启串口空闲中断:
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除空闲中断标志位
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);//空闲中断
注意必须先清除一次中断标志不然会一直卡在中断里
第二步配置DMA接收的长度并注册DMA回调函数:
HAL_UART_Receive_DMA(&huart1,Rxbuf,10);
这里参数解释一下,Rxbuf是我的DMA接收数组,10是指一次最大可以接收10个字节。在这个函数内部可以看到如下四行代码:
/* Set the UART DMA transfer complete callback */
huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;
/* Set the UART DMA Half transfer complete callback */
huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt;
/* Set the DMA error callback */
huart->hdmarx->XferErrorCallback = UART_DMAError;
/* Set the DMA abort callback */
huart->hdmarx->XferAbortCallback = NULL;
这四行就是注册函数,分别是DMA传输完成、半传输完成、DMA错误中断、第四个可以不关心。我们这次用到的就是DMA传输完成函数UART_DMAReceiveCplt,在这函数内部注册了真正的回调函数为HAL_UART_RxCpltCallback,可以看到和串口接收完成中断的回调函数一致,因此我们在编写这个函数的时候需要用句柄去判断当前的中断类型。
我们将第一步第二步的三行代码放在MX_USART1_UART_Init,初始化中。回到stm32f4xx_it.c的USART1_IRQHandler,我们编写一下处理空闲中断的逻辑:
/**
* @brief This function handles USART1 global interrupt.
*/
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 */
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)==1)//是空闲中断产生
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除空闲中断标志位
HAL_UART_DMAStop(&huart1);//关闭DMA
Rxlen=10-__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
HAL_UART_Receive_DMA(&huart1,Rxbuf,10);//重新使能
Rxflag=2;
}
/* USER CODE END USART1_IRQn 1 */
}
当产生串口空闲中断时,此时DMA已经搬运完成了所有数据都储存在Rxbuf中,我们先清除串口空闲标志,然后计算接收到的数据长度Rxlen,最后把接收完成标志Rxflag置位。这过程中需要关闭DMA然后重新配置长度,是为了下一次还能正常启动DMA。可以验证一下:
发送数据后进入串口接收中断里,DMA会自动将数据存放到Rxbuf中,再计算出接收的长度最后通过串口再次显现发送的数据,至此实现了接收不定长数据。
4、定长数据接收程序解读(DMA中断的使用)
使用DMA中断时我们把IDLE先关闭,我们实现当串口收到10个字节数据后进入DMA中断然后将数据输出,好处就是不需要一个字节一个字节的进入串口中断,减少了中断的频率,适用于接收定长数据。
先将串口IDLE中断注释了,然后编写一下回调函数HAL_UART_RxCpltCallback
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)//DMA接收满 处理
{
Rxflag=1;
HAL_UART_Receive_DMA(&huart1,Rxbuf,10);
}
}
函数很简单我们直接放到usart.c里就好了。很好理解,由于我们设置的时候设置了10个字节长度,所以当DMA接收到10个字节后,会触发DMA中断DMA2_Stream2_IRQHandler,在HAL_DMA_IRQHandler函数里会触发DMA传输完成回调函数HAL_UART_RxCpltCallback,由于我们配置的时候是配置DMA模式为单次不是循环模式,因此只需要重新再配置一下DMA接收就好了,验证一下:
5、小结
好久没用到STM32CUBEMX感觉变得亲切了很多
工程审核过后会附上链接: 工程文件🔗