本期目标
-
知晓DMA在系统框架下如何与串口配合
-
熟悉STM32CubeMx的相关配置及其意义
-
学习使用DMA将串口接受到的数据存入SRAM中
本文会通过底层寄存器原理配合代码讲解 DMA 与 串口接收全流程,小白也能看懂!
DMA与串口配合原理(不想了解可跳到STM32CubeMx配置)
串口接受数据流程
当通信的设备i传来数据时 , RX引脚会接受到数据 , 然后数据被寄存器传输到Data register 寄存器,等待CPU或者DMA将数据取走

CPU会在这里一直查询 或者是 一个中断 去DR寄存器里将数据取走 ,但是这样是非常占用CPU时间的
如果使用DMA的话 , CPU就不用去管DR寄存器里面的值 , 只要串口传来了数据 , DMA就会把DR寄存器里面的值一个字节一个字节的取走
那么DMA如何知道DR寄存器里有没有值?
DMA_Request
串口在产生数据以后 , 会发送一个DMA_Request的请求
接收到这个请求后DMA就会寻址找到DR寄存器, 然后搬走数据

DMAR寄存器
当这个位被软件置位的时候
当串口来数据了,就会发送DMA request
DM就会去DR寄存器将数据给拿走
如果UART挂载在了APB2总线上

那么DMA就可以不走里面直接从外面拿到数据再存入SRAM里
这样CPU就可以不被搬运数据的活给"困扰" , 解放更多的生产力去干别的事情, 比如说访问其他外设 , 也可以访问SRAM
STM32CubeMx配置
步骤一
设置时钟 

步骤二
配置 串口
使能中断 
设置Request
这样USART接受到数据的时侯, 就会向DMA发送请求 , DMA就会过来取走数据
因为操纵的是一位寄存器, 所以不需要勾上

但是数组地址需要递增所以Memroy需要勾上


Fifo的作用是缓存数据,假如DMA在访问Memroy的时候 , CPU正在占用Memory , 导致DMA的数据无法正常搬运而堵塞, 就可能导致数据丢失 , 而Fifo的作用就是将这一部分被堵塞的数据缓存下来,等到CPU释放了Memory , 再把数据取出来正常搬运
具体可以参考这篇文章
步骤三


点击生成代码
Keil部分
cpp
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
/* USER CODE BEGIN PFP */
此处完成了初始化
cpp
static 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的中断
打开Functions

想要实现DMA串口接受 ,可以在这里找到对应的函数

cpp
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
将其复制
cpp
uint8_t receive_data[20] = {0x00} ;
cpp
HAL_StatusTypeDef ret = 0 ;
HAL_UART_Receive_DMA(&huart1, receive_data, 20);
此时就已经可以开始接受了
调试

添加receive_data到观察窗口观察数据
也可以将其复制到Memory窗口观察
现在打开串口助手
编辑信息准备发送
接受到了数据



但是这里可以看到在反复发送后数据不会继续递增接着存储数据
这就是前面提到的设置了
因为CubeMx设置的是Normal模式
所以到了我们设置的20个字节存满后
地址不会继续递增

那么如果想实现一直循环接受的目的, 就要再次对Cubemx进行设置

也可以将函数搬到while循环里
这样会一直重启DMA的接收
就可以达到一直接收的效果
但是今天的目标不仅经是知道API接口怎么使用,还要知晓其中原理
函数解析
点击函数按下F12跳转函数定义位置
cpp
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
/* Check that a Rx process is not already ongoing */
if (huart->RxState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}
/* Process Locked */
__HAL_LOCK(huart);
/* Set Reception type to Standard reception */
huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;
return (UART_Start_Receive_DMA(huart, pData, Size));
}
else
{
return HAL_BUSY;
}
}
上一篇文章讲过DMA不论跟谁配合 , 结束后都会扔出三个回调出来
-
半满 回调
-
全满 回调
-
DMA erro 回调
相关概念不清楚可以参考下面文章
(新手向详解)通过STM32CubeMx使用STM32F411的DMA发送数据翻转GPIO-CSDN博客
通过STM32CubeMx使用STM32F4系列芯片的DMA中断发送数据翻转GPIO-CSDN博客
你需要向DMA注册回调,让系统知道你具体调用的是哪个回调
但是现在运行这个函数好像什么都办好了
cpp
HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
本质在下面这行代码
cpp
return (UART_Start_Receive_DMA(huart, pData, Size));

cpp
uint32_t *tmp;
huart->pRxBuffPtr = pData;
huart->RxXferSize = Size;
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->RxState = HAL_UART_STATE_BUSY_RX;
/* 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;
/* Enable the DMA stream */
tmp = (uint32_t *)&pData;
HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);
/* Clear the Overrun flag just before enabling the DMA Rx request: can be mandatory for the second transfer */
__HAL_UART_CLEAR_OREFLAG(huart);
/* Process Unlocked */
__HAL_UNLOCK(huart);
这个函数就在里面进行了前面所需要的操做, 已经封装好了,无需你亲自动手
cpp
/* Set the UART DMA transfer complete callback */
huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;
这一行代码就是将这个回调函数挂载到了DMA的全满回调里

点开DMA的类定义不难看到挂载在其中的回调函数 , XferCpltCallback就在其中 , 在传输完成之后进行回调
但是 UART_DMAReceiveCplt是串口的接收完成函数吗?
其实不是
点开Functions

这个函数才是
但是为什么说把UART_DMAReceiveCplt挂载在XferCpltCallback上 就等于调用了这个函数
打开
UART_DMAReceiveCplt函数

往下翻可以看到其最终会调用这个函数
DMAR
这个就是前面提到的DMAR(忘记了或者没了解的可以点击左边目录跳转对应DMAR寄存器的部分)
