上一次我们通过HAL库的串口中断回调函数,基本能够实现简单的不定长度读写收发的功能。这一次用DMA来实现,先了解一下DMA。
DMA 直接存储器存取用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。
DMA详解传送门:https://blog.csdn.net/Cheatscat/article/details/79476806
CUBEMX中的设置大概如下,其他默认值(记得要开启串口中断)
关于DMA串口读写收发功能的实现,有两种可用的方法。
第一种,
如果只是为了实现用DMA的功能,在cubemx中设置好之后,在main函数中添加
uint8_t aTxBuffer[] = "SENDING DATA USING USART1 with DMA\r\n";//(最好写到全局中) HAL_UART_Transmit_DMA(&huart1,aTxBuffer,sizeof(aTxBuffer));// DMA发生数据
串口调试助手能够在复位之后直接收到送出的数据。
亦或者使用(一)中普通的串口中断回调函数的方法使用DMA,使用HAL_UART_Receive_DMA(&huart1,aRxBuffer,sizeof(aRxBuffer)) 放在main函数中进行DMA的接收(等待接收)。在回调函数中使用receive和trainsmit函数互相调用如(一)。但是该方法有个很重要的缺点,就是在DMA的应用中,无法做到跟串口中断回调那样子的效果,因为
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)和HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)这两个函数有区别。
HAL_UART_Receive_IT这个函数在接收数据的时候可以是一个字一个字的读取,每读完一个自动关闭串口接收。
HAL_UART_Receive_DMA则是要读完一串一帧数据之后才能够执行相关函数功能,所以DMA只有在接收到期待长度的数据时才触发中断。
如果DMA的期待长度为1时,一次性发送不定长数据时会导致接收一个丢失下一个的情况,例如发送123456会接收到135,可能是DMA发送时无法及时接收接下来的数据。
这种解决方法有上位机发送数据时最好是补全至定长后发送,或采用空闲中断的方式 即使数据长度没有达到期望,只要一段时间未接收到数据即进入中断。
第二种,
就是采用DMA空闲中断方式,对应事件标志为IDLE,检测到空闲线路时,该位由硬件置 1,生成中断。
还能够在中断函数中读取DMA中的功能,如获取已读和未读数据的长度。整体流程如下:
- 1、开启串口DMA接收
- 2、串口收到数据,DMA不断传输数据到存储buf
- 3、一帧数据发送完毕,串口暂时空闲,触发串口空闲中断
- 4、在中断服务函数中,可以计算刚才收到了多少个字节的数据
- 5、解码存储buf,清除标志位,开始下一帧接收
先上手动改过的源码
uint8_t aTxBuffer[] = "*********SENDING DATA USING USART1 with DMA***********\r\n"; volatile uint8_t rx_len=0; //接收数据长度 volatile uint8_t recv_end_flag=0; //接收完成标记位 uint8_t aRxBuffer1[100]; //接收缓存 char BUFFER_SIZE=100; //不定长数据的最大长度,设置为100
main函数里面:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); HAL_UART_Receive_DMA(&huart1,aRxBuffer1,BUFFER_SIZE);// 启动DMA接收
while (1) { if(recv_end_flag ==1) { // printf("rx_len=%d\r\n",rx_len);//打印接收长度 HAL_UART_Transmit(&huart1,aRxBuffer1, rx_len,200);//接收数据打印出来 for(uint8_t i=0;i<rx_len;i++) { aRxBuffer1[i]=0;//清接收缓存 } rx_len=0;//清除计数 recv_end_flag=0;//清除接收结束标志位 } HAL_UART_Receive_DMA(&huart1,aRxBuffer1,BUFFER_SIZE);// 启动DMA接收 /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ }
stm32f1xx_it.c文件中:
void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ 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->SR; //清除状态寄存器SR,读取SR寄存器可以实现清除SR寄存器的功能 temp = huart1.Instance->DR; //读取数据寄存器中的数据 HAL_UART_DMAStop(&huart1); // temp = hdma_usart1_rx.Instance->CNDTR;// 获取DMA中未传输的数据个数,NDTR寄存器分析见下面 rx_len = BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数 recv_end_flag = 1; // 接受完成标志位置1 } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }
即可实现功能。
先定义缓存数据的数组,可以更改大小。在声明变量的时候用到了volatile关键字, 主要是因为volatile 关键字提醒编译器定义的变量是易变的,编译后的程序每次需要存储或读取该变量时,会直接从变量地址读取数据。在中断或多线程中使用volatile关键字可以避免不同优化等级时程序出错,提高程序的鲁棒性。
所以若是在多文件并且经常用到的变量需要用这个关键字。在别的.c文件中调用这些变量的时候有两种方法,一是在相应的.h文件中声明然后在别的.c文件中添加引用。二则是采用extern关键字,全局变量在外部使用时要用到extern关键字。
extern volatile uint8_t rx_len; extern volatile uint8_t recv_end_flag; extern uint8_t aRxBuffer1[100]; extern char BUFFER_SIZE;
要用到空闲中断方式,即要开启IDLE中断。
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
再用DMA的串口接收函数启动DMA接收。
从上位机接收数据后进行一个空闲中断函数,通过IDLE的标志位进行判断
void USART1_IRQHandler(void) { /* USER CODE BEGIN USART1_IRQn 0 */ 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->SR; //清除状态寄存器SR,读取SR寄存器可以实现清除SR寄存器的功能 temp = huart1.Instance->DR; //读取数据寄存器中的数据 HAL_UART_DMAStop(&huart1); // temp = hdma_usart1_rx.Instance->CNDTR;// 获取DMA中未传输的数据个数,NDTR寄存器分析见下面 rx_len = BUFFER_SIZE - temp; //总计数减去未传输的数据个数,得到已经接收的数据个数 recv_end_flag = 1; // 接受完成标志位置1 } /* USER CODE END USART1_IRQn 0 */ HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ /* USER CODE END USART1_IRQn 1 */ }
其中CNDTR是未传输的数据数量,有些地方是NDTR, CNDTR 寄存器在 DMA 通道结构体中定义了 CNDTR 寄存器,这个不同的芯片HAL库里面定义的命名有点不同。
寄存器说明如上。所以可以得到已接收数据的长度,这个对于不定长度的读写十分的重要。得到接收数据的长度之后,就可以进行操作了。在while中。
while (1) { if(recv_end_flag ==1) { // printf("rx_len=%d\r\n",rx_len);//打印接收长度 HAL_UART_Transmit(&huart1,aRxBuffer1, rx_len,200);//接收数据打印出来 for(uint8_t i=0;i<rx_len;i++) { aRxBuffer1[i]=0;//清接收缓存 } rx_len=0;//清除计数 recv_end_flag=0;//清除接收结束标志位 } HAL_UART_Receive_DMA(&huart1,aRxBuffer1,BUFFER_SIZE);// 启动DMA接收 /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ }
将接受到的数据通过Transmit函数发送出来到上位机。最后重新启动DMA接收函数。
OK.