本文开发环境:
- MCU型号:STM32F051R8T6
- IDE环境: MDK 5.25
- 代码生成工具:STM32CubeMx 5.0.1
- HAL库版本:v1.9.0(STM32Cube MCU Package for STM32F0 Series)
本文内容:
- DMA 内存到内存的数据传输
- DMA 内存到串口的数据传输
- DMA 串口数据到内存的传输
文章目录
1. DMA 简介
DMA(直接存储器访问)提供了数据传输功能,如果我们不需要对数据进行处理,只需要原封不动的从一个地址搬移到另一个地址,那么DMA是一个不错的选择,特别在数据量很大的情况,可以大大的减轻内核的负担,再程序实现之前,建议了解一下DMA的基本原理和事件,以便更好的理解配置的过程。关于与DMA数据手册和网上已经有了非常多资源介绍,这里不再赘述。
2. DMA数据内存到内存的搬运
2.1 CubeMx 配置
内存到内存的配置比较简单,我们只需要指定使用哪个DMA通道即可,接着我们再到程序中指定具体的源内存地址和目标内存地址,即可实现DMA的数据搬运功能,首先添加一个DMA通道,具体如下所示:
当我们添加完了以后,就可以开始配置DMA参数,我们依次配置,如下所示:
设置里面有一个Mode,这里只能选择Normal模式,表示每启动一次搬运,则搬运一次后停止,而不是一直不断的搬运。Increment Address 表示搬运数据过程中内存地址是否增长,比如你想要源数组原封不动的搬运到目标数组,那么就需要源地址和目标地址都勾选地址增长,如果你需要把第源数组的第一个值,赋值给目标数组里面所有的元素,则不勾选源地址的地址增长,且勾选目标地址的增长。数据宽度我们都选择Byte,即使选择一字节长度来搬运数据。
2.2 Example
当我们配置好这些以后,就可以生成代码了(当然这建立在你工程的时钟模块等基本配置已经设置好的情况),如果你不清楚STM32CubeMx如何新建一个工程,可以参考一个简单的IO工程配置。接着我们可以使用以下函数来调用一次DMA数据传输:
while (1)
{
/* USER CODE END WHILE */
uint8_t scr_data[20] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19};
uint8_t dst_data[20] = {0};
HAL_Delay(50);
HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)&scr_data, (uint32_t)&dst_data,20);
HAL_DMA_PollForTransfer(&hdma_memtomem_dma1_channel1,HAL_DMA_FULL_TRANSFER,20);
}
当我们运行这段程序后,在HAL_DMA_PollForTransfer()之后打印出dst_data[]数组的值,可以发现和scr_data[]的值一模一样,这证明我本次DMA搬运是成功的。你可以通过硬件调试直接查看数组的值。
这里简单解释一下HAL_DMA_Start()函数和HAL_DMA_PollForTransfert()函数:
2.2.1 HAL_DMA_Start() 函数
HAL_DMA_Start()函数开启了一次DMA传输,从 scr_data 向 dst_data 搬运数据,总共20个字节。当开启这是传输后,DMA将被标记为忙状态,CPU将运行以后的程序。
2.2.2 HAL_DMA_PollForTransfer() 函数
需要注意的是,由于我们CPU没必要也不应该去等DMA传输完成后再运行程序,所以当我们要开始一次新的传输之前,我们需要判断上次开启的DMA是不是已经传输完成了,这时候我们使用 HAL_DMA_PollForTransfer()来查看,第二个参数表示传输的完整度,可以选择Full或者是Half,我们这里选择了Full,只有传输完所有数据了,才认为是传输完成。这部分设计到了DMA传输事件的相关内容。这个函数如果检测到DMA完成了,还会把DMA的状态设置会待续。所以如果我们忘记使用这个函数,就会发现我们的HAL_DMA_Start()只能调用一次,此后每一次,都会返回DMA忙碌的错误状态。
3. 内存到外设的传输
我们可以把数据从内存传输到内存,也可以把数据传输到外设,当然也可以把外设的数据传到了内存。这里以内存到外设为例,把数组的值传到串口发送寄存器中,实现数据自动发送。
3.1 CubeMx 配置
如果我们没有配置串口,会发现添加DMA的时候,没有串口相关的DMA配置供我们选择,所以首先添配置串口(如果你已经配置好了,这一步就可以跳过):
在这里插入图片描述
以上是串口配置基本配置,前文以后介绍,不在赘述,接着我们需要配置DMA相关的参数:
Mode我们选择Normal,即发送一次既可,如果选择Circular,则会一直循环发送。由于我们是把数据发送到串口发送寄存器,所以目标地址不能增长。最后还需要勾选串口的全局中断使能,否则DMA传送只能发送一次(注1),具体操作如下:
3.2 Example
我们使用HAL_UART_Transmit_DMA()发送函数来开启一次串口的DMA传送,具体代码如下:
... ...
uint8_t tx_str[] = "Welcome to Yonas's Blog!\r\n";
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_UART_Transmit_DMA(&huart1,tx_str,sizeof(tx_str));
HAL_Delay(500);
... ...
}
我们定义了tx_str[]字符串,然后就可以使用HAL_UART_Transmit_DMA()函数发送这个字符串了,这里直接只用用sizeof()来计算大小,这样以便我们随意修改字符串数组。
当我们运行以上代码时候,就可以发现程序以500ms的速度向串口打印数据:
3. 外设到内存的传输
3.1 串口中 DMA 的配置
在这个示例中,选择DMA的模式为Circular模式,这是为了每一次串口中断,都可以发生搬运。
3.2 NVIC 中 DMA 的配置
需要开启串口中断和DMA中断,并且勾选生成代码,具体擦做如下:
接着还需要配置 Code generation ,具体操作如下:
3.3 Example
当我们配置好DMA后,就可以进行DMA传送了,以下是一个示例,它们被写在了main.c中:
... ...
uint8_t rx_buf[1]; //接收串口数据存放的数组
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
HAL_UART_Transmit(&huart1,rx_buf,sizeof(rx_buf),200);
}
... ...
void main(void)
{
... ..
HAL_UART_Receive_DMA(&huart1,rx_buf,1); //设置DMA接收到的数据存放在rx_buf中
while(1)
{
... ...
}
}
以上程序示例了一个功能,串口会返回接收到的数据。其原理是,当串口接收到1(注1)个数据以后,DMA将数据搬移到rx_buf(注2)数组中,完成后调用回到函数HAL_UART_RxCpltCallback(),而该函数会将rx_buf数组的值打印出来。
由 HAL_UART_Receive_DMA 第三个参数决定
由 HAL_UART_Receive_DMA 第二个参数决定
4. DMA 的回调函数的理解
这里再简单介绍一下DMA的回到函数,当串口数据传输完成,传输错误,传输一半,DMA 会调用回到函数,以下是HAL库HAL_UART_Transmit_DMA()函数的定义,可以发现库是在这里注册了回调函数:
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
/* Check that a Tx process is not already ongoing */
if(huart->gState == HAL_UART_STATE_READY)
{
if((pData == NULL ) || (Size == 0U))
{
return HAL_ERROR;
}
/* In case of 9bits/No Parity transfer, pData buffer provided as input paramter
should be aligned on a u16 frontier, as data copy into TDR will be
handled by DMA from a u16 frontier. */
if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
{
if((((uint32_t)pData)&1U) != 0U)
{
return HAL_ERROR;
}
}
/* Process Locked */
__HAL_LOCK(huart);
huart->pTxBuffPtr = pData;
huart->TxXferSize = Size;
huart->TxXferCount = Size;
huart->ErrorCode = HAL_UART_ERROR_NONE;
huart->gState = HAL_UART_STATE_BUSY_TX;
/* Set the UART DMA transfer complete callback */
huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt;
/* Set the UART DMA Half transfer complete callback */
huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt;
/* Set the DMA error callback */
huart->hdmatx->XferErrorCallback = UART_DMAError;
/* Set the DMA abort callback */
huart->hdmatx->XferAbortCallback = NULL;
/* Enable the UART transmit DMA channel */
HAL_DMA_Start_IT(huart->hdmatx, (uint32_t)huart->pTxBuffPtr, (uint32_t)&huart->Instance->TDR, Size);
/* Clear the TC flag in the ICR register */
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_TCF);
/* Process Unlocked */
__HAL_UNLOCK(huart);
/* Enable the DMA transfer for transmit request by setting the DMAT bit
in the UART CR3 register */
SET_BIT(huart->Instance->CR3, USART_CR3_DMAT);
return HAL_OK;
}
else
{
return HAL_BUSY;
}
}
从以上代码可以看出,UART_DMATransmitCplt()函数是传输完成的回调函数,进一步跟踪代码可以发现,在本例代码中,它最终调用的是串口发送完成的回调函数,这里虽然用户可以修改,但是非常不建议,毕竟作为CubeMx生成的代码下一次刷新又会改变到。我们可以重新定义串口发送完成回调函数:
/* USER CODE BEGIN 0 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
uint8_t tx_str[] = "Data Transfer completed\r\n";
HAL_UART_Transmit(&huart1,tx_str,sizeof(tx_str),200);
}
/* USER CODE END 0 */
添加这段代码到工程中,比如main.c后,可以发现发送完数据后,串口打印出“Data Transfer completed ”的提示信息,运行程序结果如下: