DMA 能完成外设到内存的 或 内存的外设的数据搬运,不用CPU参与。
下面以串口发送为例,说明操作步骤:
1、初始化通道的CCR寄存器
DMA有x个通道,每个通道都有一组寄存器
1、数据传输方向
2、循环模式------是否循环操作
3、外设地址增量
4、内存地址增量
5、外设数据宽度 8位,16位还是32位
6、内存地址宽度
7、通道优先级
8、是否是 存储器到存储器------复位后为0,表示外设与存储器通信,所以HAL这位不设置
初始化就是配置上面这8条。
用STM32CubeMX生成的DMA初始化代码 HAL_DMA_Init(DMA_HandleTypeDef *hdma) 如下:
/* Prepare the DMA Channel configuration */
tmp |= hdma->Init.Direction |
hdma->Init.PeriphInc | hdma->Init.MemInc |
hdma->Init.PeriphDataAlignment | hdma->Init.MemDataAlignment |
hdma->Init.Mode | hdma->Init.Priority;
/* Write to DMA Channel CR register */
hdma->Instance->CCR = tmp;
旧版的FW_V3.3.0库还多初始化了3个参数
1、数据长度
2、外设地址
3、内存地址
tmpreg |= DMA_InitStruct->DMA_DIR | DMA_InitStruct->DMA_Mode |
DMA_InitStruct->DMA_PeripheralInc | DMA_InitStruct->DMA_MemoryInc |
DMA_InitStruct->DMA_PeripheralDataSize | DMA_InitStruct->DMA_MemoryDataSize |
DMA_InitStruct->DMA_Priority | DMA_InitStruct->DMA_M2M;
/* Write to DMAy Channelx CCR */
DMAy_Channelx->CCR = tmpreg;
/*--------------------------- DMAy Channelx CNDTR Configuration ---------------*/
/* Write to DMAy Channelx CNDTR */
DMAy_Channelx->CNDTR = DMA_InitStruct->DMA_BufferSize;
/*--------------------------- DMAy Channelx CPAR Configuration ----------------*/
/* Write to DMAy Channelx CPAR */
DMAy_Channelx->CPAR = DMA_InitStruct->DMA_PeripheralBaseAddr;
/*--------------------------- DMAy Channelx CMAR Configuration ----------------*/
/* Write to DMAy Channelx CMAR */
DMAy_Channelx->CMAR = DMA_InitStruct->DMA_MemoryBaseAddr;
2、我们STM32CubeMX生成的DMA初始化代码以后,还不能用DMA,因为数据从哪里搬到哪里,搬多少个字节还没有初始化。HAL库是在我们调用类似HAL_UART_Transmit_DMA()这样的函数的时候才初始化这几个参数的。
如果想实现旧版一样的功能,可以直接在上面的初始化代码中加入:
hdma_usart1_tx.Instance->CNDTR=0;
hdma_usart1_tx.Instance->CPAR = USART1_DR_Base;
hdma_usart1_tx.Instance->CMAR = (uint32_t)Com_Data.SendBuff;
SET_BIT(huart->Instance->CR3, USART_CR3_DMAT);//使能DMA发送,CR3是串口1中的寄存器,不是DMA中的寄存器
在HAL_UART_Transmit_DMA()函数中,调用HAL_DMA_Start_IT()---->DMA_SetConfig()来设置源地址,目标地址,传输字节数。
以上配置完以后,最后需要EN通道:
旧版EN的方法:DMA_Cmd (DMA1_Channel4,ENABLE);//即把CCR的最后一位置1
hal库EN的方法:在HAL_DMA_Start_IT()—>__HAL_DMA_ENABLE(hdma);
3、将串口与DMA这两个变量关联起来
在初始化HAL_UART_MspInit()----->HAL_DMA_Init()以后,有一条语句:
__HAL_LINKDMA(huart,hdmarx,hdma_usart1_rx);
//相当于
huart->hdmarx=&(hdma_usart1_rx);
hdma_usart1_rx.Parent=huart;
这条只是把串口变量huart1 与 DMA变量hdma_usart1_rx相互连接起来,像链表一样。后面在串口模块中使用huart->hdmarx.Init.Direction
就可以访问DMA变量中的方向参数;在DMA模块中使用huart=hdma_usart1_rx.Parent;huart->TxXferCount=0;
就可以设置串口发送的字节数。
4、通过仿真可以查看地址
通过查数据手册知道DMA的基地址是0x4002 0000
通道5即串口接收通道,偏移地址是0x08+20*(通道号-1)=88=0x58
5、串口DMA HAL库的使用
在Init_Uart(void)初始化函数中调用下来两个函数
A、MX_DMA_Init();-------只设置了DMA中断优先级
B、MX_USART1_UART_Init()
------1)、设置串口和DMA相关寄存器:HAL_UART_Init(&huart1)
------------a,设置底层:HAL_UART_MspInit(huart);
------------------(1)串口引脚初始化
------------------(2)DMA通道初始化,即初始化传输的8个参数:HAL_DMA_Init(&hdma_usart1_rx)
------------------(3)把串口变量和DMA变量连接起来:__HAL_LINKDMA(huart,hdmarx,hdma_usart1_rx)
------------------(4)设置串口中断优先级
------------b,设置串口波特率,停止位等:UART_SetConfig(huart);
------------c,设置串口CR2 CR3寄存器
------------d,EN串口:__HAL_UART_ENABLE(huart);
------2)、使能串口空闲中断:__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
------3)、开始DMA接收:HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buffer, RX_LEN);
-------------------这个函数里会设置中断回调函数
下面详细分析其中重要的步骤:
A、设置DMA通道中断的优先级
static void MX_DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 6, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}
(2)DMA通道初始化
设置DMA通道的函数在文件stm32f1xx_hal_msp.c中,而DMA通道变量是定义在用户的c文件中,所以要在stm32f1xx_hal_msp.c的最前面把DMA变量声明为外部变量
extern DMA_HandleTypeDef hdma_usart1_rx;
extern DMA_HandleTypeDef hdma_usart1_tx;
(3)为了用串口变量访问DMA变量,或用DMA变量访问串口变量,把它们连接起来
在DMA变量中,定义了一个 *Parent;指针
以下语句把它两个变量相互连接起来:
__HAL_LINKDMA(huart,hdmarx,hdma_usart1_rx);
//相当于
huart->hdmarx=&(hdma_usart1_rx);
hdma_usart1_rx.Parent=huart;
2)、串口空闲中断
串口接收数据后,当一个空闲帧被检测到时(一个byte的高电平(空闲)状态),如果IDLEIE位被设置将产生一个中断
3)、DMA串口接收
首先,定义一个数组用来接收串口数据,数组的长度要设置大于些
#define RX_LEN 30
static char rx_buffer[RX_LEN];
其次,调用HAL_UART_Receive_DMA(&huart1, (uint8_t*)rx_buffer, RX_LEN);来接收数据
再次,当DMA接收到一些字节时,突然有一个空闲字节,那就会产生空闲中断,在串口中断函数HAL_UART_IRQHandler(UART_HandleTypeDef *huart)中增加空闲中断代码
//如果是空闲中断
tmp_flag = __HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE);
tmp_it_source= __HAL_UART_GET_IT_SOURCE(huart, UART_IT_IDLE);
if((tmp_flag != RESET) && (tmp_it_source != RESET))
{
__HAL_UART_CLEAR_IDLEFLAG(huart);//清除标志
tmp_flag=huart->Instance->SR;//读SR可以实现清除状态寄存器
tmp_flag=huart->Instance->DR;
HAL_UART_DMAStop(huart);
tmp_flag=huart->hdmarx->Instance->CNDTR;//获取未传输数据个数
__HAL_DMA_DISABLE(huart->hdmarx);//关闭DMA接收
HAL_UART_RxIdleCallback(tmp_flag);//把未接收字节数传过去
__HAL_DMA_ENABLE(huart->hdmarx);//开启DMA接收,在传未接收字节数时,先关DMA,防止又接收到数据,改变了未接收字节数
}
}
在空闲中断中,获取剩余未接收的字节数tmp_flag,开始我们设置的是接收RX_LEN个字节,两个相减就得到已接收的字节数。处理接收到的字节后,重新启动DMA接收。
//串口接收完回调函数
void HAL_UART_RxIdleCallback(uint32_t rx_count)
{
uart_pak data;
uint32_t rx_cou;
rx_cou=RX_LEN-rx_count;//算出已接收字节数
strcpy(data.str,rx_buffer);
data.strLen = rx_cou;
printFromISR(&data);//输出
HAL_UART_Receive_DMA(&huart1, (uint8_t *)rx_buffer, RX_LEN);
}
串口中断函数是在stm32f1xx_hal_uart.c中,它的空闲中断中会调用HAL_UART_RxIdleCallback(uint32_t tmp_flag)回调函数 来处理接收到的数据,回调函数是用户在其它文件中定义的,所以在文件的开始要把它个函数声明为外部函数。
在使用函数的c文件开始声明外部函数,有一个好处,就是这个外部函数可以写在任何其它c文件里。而在头文件中声明,必须包含相应的头文件。