【DMA】DMA实战:用DMA操控外设
目录
DMA实战操作:DMA控制外部外设
通过DMA控制外部外设(DMA控制GPIO翻转)
1. DMA配置的常规步骤
2. 使用DMA翻转LED灯
通过DMA控制外部外设(实战)
第一步、配置时钟和GPIO模式
第二步、配置DMA
第三步、代码实现
DMA对象
DMA搬运数据目标寄存器:
开启DMA搬运数据
第一次尝试
为什么数据没搬运成功?
使用DMA中断搬运数据
注册DMA的中断回调函数
DMA实战操作:DMA控制外部外设
DMA的妙用:
DMA可以将数据搬运到外设的寄存器上,单片机启动时,经常需要配置外设的寄存器,我们可以让DMA去帮助我们配置一些简单外设的寄存器来进行初始化,这样MCU的启动速度、性能就会被加速。这就是DMA的妙用,DMA不仅能帮助我们降低功耗,也可以作为 CPU 的“协程”、“协处理器”帮我们在启动过程当中去优化我们的启动速度。
通过DMA控制外部外设(DMA控制GPIO翻转)
1. DMA配置的常规步骤
-
先确定哪一个DMA更适合我们的应用
-
如果操作GPIOA,通过架构图知道,DMA2可以连接到GPIOA
-
-
初始化DMA
-
触发DMA(明确触发源是自动触发或者是手动触发)
-
知道DMA传输完成:查询方式或中断方式
2. 使用DMA翻转LED灯
-
看下LED是通过哪个GPIO连接的
-
GPIOA_6引脚
-
-
确认GPIO是连接在哪个BUS上的
-
AHB1
-
-
确认哪个总线Master可以和AHB1的外设通信
-
DMA2
-
通过DMA控制外部外设(实战)
第一步、配置时钟和GPIO模式
第二步、配置DMA
这里我们需要把DMA2配置为 Memory To Memory 模式,因为直接挂载在总线AHB(不论AHB1还是AHB2)上的外设都视为“Memory”。(这是一个很小的小窍门)
DMA下方配置图:
FIFO 是一个数据队列,当 CPU 抢总线的时候可以把数据暂存在 FIFO 中,避免数据溢出导致丢数据。
第三步、代码实现
DMA对象
配置好cubemx生成工程后,main.c 中就已经自动生成了一个DMA2对象:
/* Private variables ---------------------------------------------------------*/
DMA_HandleTypeDef hdma_memtomem_dma2_stream0;
跳转到定义,这是将DMA抽象成了一个类:
在函数 MX_DMA_Init(void) 中对DMA对象 hdma_memtomem_dma2_stream0 进行初始化:
/*** Enable DMA controller clock* Configure DMA for memory to memory transfers* hdma_memtomem_dma2_stream0*/
static void MX_DMA_Init(void)
{/* DMA controller clock enable */__HAL_RCC_DMA2_CLK_ENABLE();/* Configure DMA request hdma_memtomem_dma2_stream0 on DMA2_Stream0 */hdma_memtomem_dma2_stream0.Instance = DMA2_Stream0;hdma_memtomem_dma2_stream0.Init.Channel = DMA_CHANNEL_0;hdma_memtomem_dma2_stream0.Init.Direction = DMA_MEMORY_TO_MEMORY;hdma_memtomem_dma2_stream0.Init.PeriphInc = DMA_PINC_DISABLE;hdma_memtomem_dma2_stream0.Init.MemInc = DMA_MINC_DISABLE;hdma_memtomem_dma2_stream0.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;hdma_memtomem_dma2_stream0.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;hdma_memtomem_dma2_stream0.Init.Mode = DMA_NORMAL;hdma_memtomem_dma2_stream0.Init.Priority = DMA_PRIORITY_LOW;hdma_memtomem_dma2_stream0.Init.FIFOMode = DMA_FIFOMODE_ENABLE;hdma_memtomem_dma2_stream0.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;hdma_memtomem_dma2_stream0.Init.MemBurst = DMA_MBURST_SINGLE;hdma_memtomem_dma2_stream0.Init.PeriphBurst = DMA_PBURST_SINGLE;if (HAL_DMA_Init(&hdma_memtomem_dma2_stream0) != HAL_OK){Error_Handler( );}/* DMA interrupt init *//* DMA2_Stream0_IRQn interrupt configuration */HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);}
DMA搬运数据目标寄存器:
而我们如何找到这个寄存器的地址呢?
第一种方法:直接找到GPIOx的基地址,然后 GPIOx基地址 + 偏移量 就是我们要找的寄存器的地址。
第二种方法:
我们找到 GPIOA 的定义:
可以发现 GPIOA 是一个 GPIO_TypeDef 结构体指针
我们来到结构体定义里:
会发现这个GPIO相关寄存器都已经被包括在结构体内了,也就是说,我们直接使用 GPIOA->ODR 就可以访问这个寄存器了,如果要取该寄存器的地址,直接 &(GPIOA->ODR) 。
开启DMA搬运数据
第一次尝试
根据上述理论,我们尝试写下如下代码:
main.c:
// 其他代码...
uint32_t data_gpio[2] = {0xFF,0x00};
// 其他代码...
int main(void)
{// 一些初始化代码// ...while (1){// DMA 写入数据,置位GPIO输出高电平HAL_DMA_Start(&hdma_memtomem_dma2_stream0,(uint32_t)&data_gpio[0],(uint32_t)&(GPIOA->ODR),1);HAL_Delay(10);// DMA 写入数据,置位GPIO输出低电平HAL_DMA_Start(&hdma_memtomem_dma2_stream0,(uint32_t)&data_gpio[1],(uint32_t)&(GPIOA->ODR),1);HAL_Delay(10);}
}
通过逻辑分析仪抓到的电平发现,第一次搬运成功了,后续DMA搬运数据好像没成功
通过 debug 直接观察寄存器我们也发现,确实第二次的DMA搬运没有成功:
第一次DMA搬运:
第二次DMA搬运:
为什么数据没搬运成功?
传输完成后,需要用HAL_DMA_PollForTransfer()去poll一下,清除对应标志位(使标志位就位)来启动下一次的传输。
所以我们要修改上述代码:
while (1)
{HAL_DMA_Start(&hdma_memtomem_dma2_stream0,(uint32_t)&data_gpio[0],(uint32_t)&(GPIOA->ODR),1);// 等待DMA传输完成,超时时间100HAL_DMA_PollForTransfer(&hdma_memtomem_dma2_stream0,HAL_DMA_FULL_TRANSFER,100);HAL_Delay(10);HAL_DMA_Start(&hdma_memtomem_dma2_stream0,(uint32_t)&data_gpio[1],(uint32_t)&(GPIOA->ODR),1);// 等待DMA传输完成,超时时间100HAL_DMA_PollForTransfer(&hdma_memtomem_dma2_stream0,HAL_DMA_FULL_TRANSFER,100);HAL_Delay(10);
}
成功运行:
但是问题是:如果每次DMA传输都要CPU等待,那我DMA不白配置了吗?这还不如直接让CPU配置寄存器呢!
使用DMA中断搬运数据
所以,我们有新的解决方法:用中断的方式进行DMA的传输。即使用 HAL_DMA_Start_IT() 来让DMA搬运数据,在DMA搬运完成后会触发DMA中断。
但是在使用 HAL_DMA_Start_IT() 之前,我们需要配置DMA中断:
生成工程后,我们可以在 stm32f4xx_it.c 文件中找到下面这个函数,这就是 DMA 中断服务函数:
很显然,这个函数会调用 HAL_DMA_IRQHandler() 函数,而在 HAL_DMA_IRQHandler() 函数中,又会调用 DMA 的中断回调函数:
这个函数在哪?在一开始定义的 dma 句柄里:
但是实际上我们并此时没有给这个函数指针挂载对应的函数,我们可以通过 debug 查看 hdma_memtomem_dma2_stream0 对象中此时 XferCpltCallback 指针的值。
明显,此时的 XferCpltCallback 是 NULL 。
如果 XferCpltCallback 是 NULL,就不会进入 DMA 回调函数,因为此时 DMA 是没有回调函数的。但是我们此时需要 DMA 产生一个回调,让DMA传输完成后调用我们的回调函数。
此时,我们就要使用hal库DMA的另一个接口,注册DMA的中断回调函数。
注册DMA的中断回调函数
注册DMA中断回调函数的注册函数叫 HAL_DMA_RegisterCallback()
-
第一个参数是DMA对象的地址
-
第二个参数是你想要注册的回调类型(传输完成/传输一半...),如下图
-
第三个参数是你自己实现的DMA回调函数,把函数地址(即函数名)作为参数传给他后,他会将该函数注册为DMA回调函数
我们只需要注册好自己的回调函数,就可以使用DMA传输完成中断回调函数来操作GPIO的翻转了:
uint32_t data_gpio[2] = {0xFF,0x00};
uint32_t counter = 0;// DMA2_stream0 full transmit callback function
void my_DMA_TC_Callback(DMA_HandleTypeDef *_hdma)
{counter++;HAL_DMA_Start_IT(&hdma_memtomem_dma2_stream0,(uint32_t)&data_gpio[counter%2],(uint32_t)&(GPIOA->ODR),1);
}void main()
{// 其他代码// ...// Register DMA2_stream0 full transmit callbackHAL_DMA_RegisterCallback(&hdma_memtomem_dma2_stream0,HAL_DMA_XFER_CPLT_CB_ID,my_DMA_TC_Callback);// 其他代码// ...while(1){HAL_DMA_Start_IT(&hdma_memtomem_dma2_stream0,(uint32_t)&data_gpio[counter%2],(uint32_t)&(GPIOA->ODR),1);}
}
实验效果:
可以发现,GPIO翻转的非常快速,但是现在还有个问题:我们每次进入DMA中断,不是还是要唤醒CPU吗?这没有达成我们想要的用DMA代替CPU从而实现降低功耗或是解放CPU的目的。
但此时可玩性就多了:我们可以把这个数组改的很大:
因为我们现在是每传输一个字节进入一次中断,我们完全可以把这个数组改大一些,在里面多加几个0xFF、0x00这样的,让他一次多传输几个(或者几十个)字节,配置源地址自增、目标地址不自增,做到超快速翻转GPIO电平:
也可以通过定时器updata触发DMA请求让DMA搬运数据到指定寄存器或者翻转io口电平模拟某种通信协议,从而做到解放CPU。(这个后续文章再讲)