Stm32 HAL库 USART(发送+接收)全部采用DMA形式
本文主要参考的是俄国一位大神的文章
1. 例程简介
1.1 DMA
- DMA (直接存储区访问,为实现数据高速在外设寄存器与存储器之间或者存储器与存储器之间传输提供了高效的方法。其他的采用DMA的优点,相信您已经查阅了众多资料,此处就不浪费时间了。
1.2 U(S)ART
- U(S)ART通常可以采用如下几种方式:
- 轮询模式(无DMA,无IRQ):应用程序必须轮询状态位以检查是否已收到新字符并以足够快的速度读取以获取所有字节
- 中断模式(无DMA):UART触发中断,CPU跳转到服务程序以处理数据接收
- DMA模式:DMA用于在硬件级别将数据从USART RX数据寄存器传输到用户存储器。此时不需要应用程序交互,除非必要时按应用程序处理接收的数据
有个国外的帅气大神写的这篇文章,把几种方式的优缺点讲的非常清楚,DMA的不定长方式的使用也讲的很透彻,推荐各位参考
- 本文主要介绍采用HAL库实现USART的DMA方式的发送+接收,本文面向困扰于HAL库DMA方式的小白,仅仅给出最简单的实现方式,帮助他们快速入门,各位大佬们随便看看就好
2. 具体实现步骤
2.1 CubeMx-5.1.0 界面操作
-
引脚(GPIO)配置 如下图所示,小编把TX,RX脚分别称为(
GPS_TX
和GPS_RX
)切勿介怀,注时钟配置,工程等配置参考其他大佬的CubeMx-5.1.0入门教程
-
串口基本参数配置 这里采用的是串口3(USART3),如下图所示:
-
NVIC设置 这里采用的是使能USART3全局中断,如下图所示
-
DMA配置 这里是项目的核心,如下图进行设置
- RX,TX分别采用DMA1 的3和2通道
- RX选择方向为外设到内存,TX选择方向为内存到外设
- RX和TX的增地址均为内存,外设地址不增
- 外设和内存数据宽度均为为byte
- RX,TX模式均为Normal,这里RX不要采用Circle模式,否则等等主程序的判断条件会失效
- 点击
GENERATE CODE
按钮,生成初始化程序:
注 高级设置里面选择 HAL库
生成代码的目录结构为:
2.2 编程展示
- 在
usart.c
文件中,你会看到生成如下三个函数
函数名 | 函数作用 |
---|---|
MX_USART3_UART_Init(..) | 串口的初始化,包括串口的基本参数配置,GPIO,DMA配置等,会自动调用HAL_UART_MspInit(..) 函数,所以,main.c只需要调用该函数进行串口初始化即可,无需再调用下面两个函数 |
HAL_UART_MspInit(..) | 串口底层函数初始化,包括GPIO,DMA等 |
HAL_UART_MspDeInit(..) | 串口解初始化,暂时不知道怎么用 |
请看我生成的代码,里面的设置就对应了我们在CubeMx里面的设置操作,可以仔细对应一下,加深理解
void MX_USART3_UART_Init(void)
{
huart3.Instance = USART3;
huart3.Init.BaudRate = 9600;
huart3.Init.WordLength = UART_WORDLENGTH_8B;
huart3.Init.StopBits = UART_STOPBITS_1;
huart3.Init.Parity = UART_PARITY_NONE;
huart3.Init.Mode = UART_MODE_TX_RX;
huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart3.Init.OverSampling = UART_OVERSAMPLING_16;
huart3.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart3.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart3) != HAL_OK)
{
Error_Handler();
}
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART3)
{
/* USER CODE BEGIN USART3_MspInit 0 */
/* USER CODE END USART3_MspInit 0 */
/* USART3 clock enable */
__HAL_RCC_USART3_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
/**USART3 GPIO Configuration
PD8 ------> USART3_TX
PD9 ------> USART3_RX
*/
GPIO_InitStruct.Pin = GPS_TX_Pin|GPS_RX_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART3;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
/* USART3 DMA Init */
/* USART3_RX Init */
hdma_usart3_rx.Instance = DMA1_Channel3;
hdma_usart3_rx.Init.Request = DMA_REQUEST_2;
hdma_usart3_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart3_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart3_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart3_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart3_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart3_rx.Init.Mode = DMA_NORMAL;
hdma_usart3_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
if (HAL_DMA_Init(&hdma_usart3_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart3_rx);
/* USART3_TX Init */
hdma_usart3_tx.Instance = DMA1_Channel2;
hdma_usart3_tx.Init.Request = DMA_REQUEST_2;
hdma_usart3_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart3_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart3_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart3_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart3_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart3_tx.Init.Mode = DMA_NORMAL;
hdma_usart3_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
if (HAL_DMA_Init(&hdma_usart3_tx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(uartHandle,hdmatx,hdma_usart3_tx);
/* USART3 interrupt Init */
HAL_NVIC_SetPriority(USART3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART3_IRQn);
/* USER CODE BEGIN USART3_MspInit 1 */
/* USER CODE END USART3_MspInit 1 */
}
}
- 在
main.c
里面,主函数可以采用下面的代码形式(方便展示取出无用注释):
int main(void)
{
// 发送测试字符串
uint8_t str[] = "\r\n-------------USART_DMA_Sending------------------\r\n";
// 接收缓存区大小为20
uint8_t recvStr[20] = {0};
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init(); // 初始化串口和晶振相关IO口的时钟
MX_DMA_Init(); // 初始化DMA的时钟,配置RX,TX的中断优先级
//串口的初始化(串口配置,GPIO配置,DMA配置等)
MX_USART3_UART_Init();
/**********************我们添加的代码*****************************/
// 采用DMA发送 str,按照str实际大小发送,不发送字符串末尾的'0'
HAL_UART_Transmit_DMA(&huart3, str, sizeof(str) - 1);
HAL_Delay(1000);
// 采用DMA接收函数,在while函数前要先使能一次DMA接收,存放在recvStr数组中,大小为20
/**
* 该函数能够,使能DMA传输完成中断,半传输完成中断,可以采用在stm32f4xx_it.c文件中
* 对DMA1_Channel3_IRQHandler(void)添加断点观察
* 可以先发送10个字符,再发送10个字符,会观察到两次停到这个地方,这是由于传输完成中断,半传输完成中断的作用
* 因为我们设置的大小为 20
*/
HAL_UART_Receive_DMA(&huart3, (uint8_t *)recvStr, 20);
/***************************************************************/
while (1)
{
/**********************我们添加的代码*****************************/
//循环等待hdma_usart3_rx.State状态的变换,当接收到20个字符的大小后,DMA传输结束
//注:因我们采用的是正常模式,而非循环模式,所以才会这么使用,循环模式下,该标志位貌似不起作用
if(hdma_usart3_rx.State == HAL_DMA_STATE_READY)
{
// 将接收到的字符打印出来进行观察
HAL_UART_Transmit_DMA(&huart3, recvStr, sizeof(recvStr) - 1);
HAL_Delay(1000);
// 清除缓存区内容,方便进行下次接收
memset(recvStr,0,sizeof(recvStr));
// 软件将标志位清零
hdma_usart3_rx.State = HAL_DMA_STATE_BUSY;
// 继续继续下一回合的DMA接收,因为采用的非循环模式,再次调用会再次使能DMA
HAL_UART_Receive_DMA(&huart3, (uint8_t *)recvStr, 20);
}
/***************************************************************/
}
}
3. 实验结果展示
如下图所示,发送了3次20字符的数据,接收共计60个字符的数据,但是频率快了会造成数据丢失,后面慢慢优化吧。