当前位置: 首页 > news >正文

【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配置的常规步骤

  1. 先确定哪一个DMA更适合我们的应用

    1. 如果操作GPIOA,通过架构图知道,DMA2可以连接到GPIOA

  2. 初始化DMA

  3. 触发DMA(明确触发源是自动触发或者是手动触发)

  4. 知道DMA传输完成:查询方式或中断方式

2. 使用DMA翻转LED灯

  1. 看下LED是通过哪个GPIO连接的

    1. GPIOA_6引脚

  2. 确认GPIO是连接在哪个BUS上的

    1. AHB1

  3. 确认哪个总线Master可以和AHB1的外设通信

    1. 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。(这个后续文章再讲)

http://www.dtcms.com/a/389745.html

相关文章:

  • 深入理解传输层协议:UDP 与 TCP 的核心原理与应用
  • 教育行业数字化资料管理:构建安全合规、高效协同的一体化知识共享平台
  • Smart Launcher安卓版(安卓桌面启动器):安卓设备的智能启动器
  • Ansible如何写Callback 插件
  • 自动化测试框架需要具备哪些功能?
  • Pix2Pix中的对抗损失与L1损失:高频细节与低频结构的平衡艺术
  • mkcert生成证书本地或内网使用https
  • 【Python】关于移除Conda中已搭建环境的相关问题
  • 基于SpringBoot+Vue的校园兼职管理系统(WebSocket及时通讯、地图API、Echarts图形化分析)
  • 【K8S默认容器运行时】
  • Makefile学习(二)- 语法(变量、伪目标)
  • Winform自定义无边框窗体
  • 文献综述是什么?怎么写好一篇综述?
  • CLIP:开启多模态AI新时代的密钥(上)
  • @[TOC](位运算) # 常见位运算总结
  • 【Block总结】sMLP,全新的“稀疏MLP”模块|即插即用|原模型改进
  • TDengine IDMP 基本功能——数据可视化(4. 仪表盘)
  • 亚信安全与中国联通共同打造的联通联信一体化安全检测与响应平台亮相网安周
  • 短脉冲计数
  • 铝厂天车PLC远程调试解决方案:御控物联网网关赋能工业智造新生态
  • CPU-GPU预处理流程的核心和优化关键 格式流转
  • 混元开源之力:spring-ai-hunyuan 项目功能升级与实战体验
  • 基于开源AI大模型、AI智能名片与S2B2C商城小程序的社群入群仪式设计研究
  • HookConsumerWidget 深入理解
  • Django多数据库实战:Mysql从逻辑隔离到跨库外键问题的解决方案
  • SQL Server索引优化:从原理到实战的完整指南
  • 前端-Vue自定义指令
  • 深度学习调参核心:PyTorch学习率调整策略全解析(一)(附系列PPT关键要点)
  • 如何在保证质量的前提下,快速完成一份 PPT?
  • AssemblyScript 入门教程(3)AssemblyScript 项目搭建与实战入门