目录

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传输完成,超时时间100
      HAL_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传输完成,超时时间100
      HAL_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 callback
    HAL_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。(这个后续文章再讲)

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐