5 DMA简介

DMA全称direct memory access,即直接存储器访问。

其主要作用是不占用CPU大量资源,在APB、AHB总线上的设备之间以硬件方式高速有效地传输数据。

注意:在ing20系列使能 FLASH_PROGRAM_WITH_DMA 宏时,DMA的通道0会被FLASH写入时占用,当调用flash写入相关接口时,会修改ch0的配置用于flash写入

5.1 功能描述

5.1.1 特点

  • 最多8个DMA通道

  • 最多16个硬件握手请求/确认配对

  • 支持8/16/32/64位宽的数据传输

  • 支持24-64位地址宽度

  • 支持成链传输数据

5.1.2 搬运方式

  • 单次数据块搬运:DMA使用单个通道,一次使能将数据从SRC到DST位置搬运一次

  • 成串多数据块搬运:DMA使用单个通道,一次使能按照DMA链表信息依次将数据从SRC到DST位置搬运多次或循环搬运。

其根本区别是有无注册有效的DMA链表。

5.1.3 搬运类型

  • memory到memory搬运

  • memory到peripheral搬运

  • peripheral到memory搬运

  • peripheral到peripheral搬运

5.1.4 中断类型

  • IntErr:错误中断表示DMA传输发生了错误而触发中断,主要包括总线错误、地址没对齐和传输数据宽度没对齐等。

  • IntAbt:终止传输中断会在终止DMA通道传输时产生。

  • IntTC:TC中断会在没有产生IntErr和IntAbt的情况下完成一次传输时产生。

    注意: 当DMA传输链表有效时,TC中断只会在传输完成时产生,不会在传输过程中产生。

  • IntEachDesc:链转换结束中断。当DMA传输链表有效时,每当一个描述符传输完成时产生。

    注意: 中断触发时会停止DMA传输,需要在中断重新使能通道才会继续进行链式DMA传输。

5.1.5 数据地址类型

  • Increment address

  • Decrement address

  • Fixed address

如果Increment则DMA从地址由小到大搬运数据,相反的Decrement则由大到小搬运。fixed地址适用于外设FIFO的寄存器搬运数据。

5.1.6 数据方式

  • normal mode

  • handshake mode

DMA搬运前需要对数据源和数据目的地址的数据方式进行配置。

数据方式的选择有如下建议:

1. 从内存搬运数据选择normal mode;

2. 从外设FIFO搬运数据选择handshake mode,同时要和外设协商好BurstSize,支持2^n(n = 0-7)大小的BurstSize。

5.1.7 数据位宽

DMA传输要求传输两端的数据类型一致,支持数据类型有:

  • Byte transfer

  • Half-word transfer

  • Word transfer

  • Double word transfer

覆盖所有常见数据类型。

5.1.8 总线仲裁

DMA支持总线仲裁,即多个DMA通道共享总线,互不干扰。当使能多个DMA通道时,DMA会自动进行总线仲裁,使得总线上同时只有一个DMA通道在工作。 为了防止总线单通道阻塞,DMA会根据每个传输的burstSize大小,自动切换通道数据传输,即通道每传输一个burstSize的数据,切换到下一个通道继续传输。

5.2 使用方法

5.2.1 方法概述

首先确认数据搬运需求是单次搬运还是成串搬运,以及搬运类型,即memory和peripheral的关系。

5.2.1.1 单次搬运

1. 注册DMA中断

2. 定义一个DMA_Descriptor变量用来配置DMA通道寄存器

3. 根据数据搬运类型选择DMA寄存器配置接口,正确调用驱动接口配置DMA寄存器

4. 使能DMA通道开始搬运

5.2.1.2 成串搬运

1. 注册一个或多个DMA中断

2. 定义多个DMA_Descriptor变量用来配置DMA通道寄存器

3. 根据数据搬运类型选择DMA寄存器配置接口,正确调用驱动接口配置DMA寄存器

4. 将多个DMA_Descriptor变量首尾相连成串,类似链表

5. 使能DMA通道开始搬运

5.2.2 注意点

  • 定义DMA_Descriptor变量需要8字节对齐,否则DMA搬运不成功

  • 成串搬运如果配置多个DMA中断则需要在每个中断里使能DMA,直到最后一次搬运完成

  • 对于从外设搬运需要确认外设是否支持DMA

  • 建议从外设搬运选择握手方式,并与外设正确协商burstSize

  • burstSize尽量取较大值,有利于减少DMA中断次数提高单次中断处理效率。但burstSize太大可能最后一次不能搬运丢弃较多数据

  • 建议设置从外设搬运总数据量为burstSize的整数倍或采用乒乓搬运的方式

  • 在DMA从外设搬运的情况下,正确的操作顺序是先配置并使能好DMA,再使能外设开始产生数据

5.2.3 握手请求

  • 握手请求:DMA请求握手信号,请求对方传输数据。当进行外设到内存数据传输,内存到外设数据传输以及外设到外设数据传输时,都需要进行握手请求。

  • 握手请求的触发条件:

    DMA用作到外设到FIFO搬运数据时,当FIFO内数据计数增加waterlevel时,外设会发出数据请求,DMA开始将数据从外设FIFO搬运到内存。

    DMA用作FIFO到外设数据传输时,当FIFO内数据计数减小到waterlevel时,DMA会发出数据请求,DMA开始将数据从内存搬运到外设FIFO。

    例如 :

    1. 使用DMA将RX接收数据搬运到内存: UART设置RX FIFO waterlevel为4,当接收到4个字节数据时,UART会发出数据请求,DMA开始将数据从外设FIFO搬运到内存。

    2. 使用DMA将内存搬运到外设触发TX发送:UART设置TX FIFO waterlevel为4,当fifo内的数据计数到达4时,UART会发出数据请求,DMA开始将数据从内存搬运到外设FIFO。

  • 握手请求的配置: 通常情况无需关心,使用默认配置。

    当需要使能的DMA搬运配置为pdm,adc,pwm_channel0,pwm_channel2,qdec_req2外设时,需要手动重新配置握手请求表,再使用对应接口初始化DMA. 详情请参照SYSCTRL DMA规划章节。

5.3 编程指南

5.3.1 驱动接口

  • DMA_PrepareMem2Mem:memory到memory搬运标准DMA寄存器配置接口

  • DMA_PreparePeripheral2Mem:Peripheral到memory搬运标准DMA寄存器配置接口

  • DMA_PrepareMem2Peripheral:memory到Peripheral搬运标准DMA寄存器配置接口

  • DMA_PreparePeripheral2Peripheral:Peripheral到Peripheral搬运标准DMA寄存器配置接口

  • DMA_Reset:DMA复位接口

  • DMA_GetChannelIntState:DMA通道中断状态获取接口

  • DMA_ClearChannelIntState:DMA通道清中断接口

  • DMA_EnableChannel:DMA通道使能接口

  • DMA_AbortChannel:DMA通道终止接口

  • DMA_ConfigSrcBurstSize: DMA配置通道burstSize接口

5.3.2 代码示例

5.3.2.1 单次搬运

下面以memory到memory单次搬运展示DMA的基本用法:

#define CHANNEL_ID  0
char src[] = "hello world!";
char dst[20];
DMA_Descriptor test __attribute__((aligned (8)));
static uint32_t DMA_cb_isr(void *user_data)
{
    uint32_t state = DMA_GetChannelIntState(CHANNEL_ID);
    DMA_ClearChannelIntState(CHANNEL_ID, state);

    printf("dst = %s\n", dst);
    return 0;
}

void DMA_Test(void)
{
    DMA_Reset();
    platform_set_irq_callback(PLATFORM_CB_IRQ_DMA, DMA_cb_isr, 0);
    DMA_PrepareMem2Mem(&test[0],
                       dst,
                       src, strlen(src),
                       DMA_ADDRESS_INC, DMA_ADDRESS_INC, 0);
    DMA_EnableChannel(CHANNEL_ID, &test);
}

最终会在DMA中断程序里面将搬运到dst中的“hello world!”字符串打印出来。

5.3.2.2 成串搬运

下面以memory到memory两块数据搬运拼接字符串展示DMA成串搬运的基本用法:

#define CHANNEL_ID  0
char src[] = "hello world!";
char src1[] = "I am ING20.";
char dst[100];
DMA_Descriptor test[2] __attribute__((aligned (8)));
static uint32_t DMA_cb_isr(void *user_data)
{
    uint32_t state = DMA_GetChannelIntState(CHANNEL_ID);
    DMA_ClearChannelIntState(CHANNEL_ID, state);

    printf("dst = %s\n", dst);
    return 0;
}

void DMA_Test(void)
{
    DMA_Reset();
    platform_set_irq_callback(PLATFORM_CB_IRQ_DMA, DMA_cb_isr, 0);
      test[0].Next = &test[1];    // make a DMA link chain
      test[1].Next = NULL;
    DMA_PrepareMem2Mem(&test[0],
                       dst,
                       src, strlen(src),
                       DMA_ADDRESS_INC, DMA_ADDRESS_INC, 0);
    DMA_PrepareMem2Mem(&test[1],
                       dst + strlen(src),
                       src1, sizeof(src1),
                       DMA_ADDRESS_INC, DMA_ADDRESS_INC, 0);
    DMA_EnableChannel(CHANNEL_ID, &test[0]);
}

最终将会打印出“hello world!I am ING20.”字符串。

5.3.2.3 DMA乒乓搬运

DMA乒乓搬运是一种DMA搬运的特殊用法,其主要应用场景是将外设FIFO中数据循环搬运到memory中并处理。

可实现“搬运”和“数据处理”分离,从而大大提高程序处理数据的效率。

对于大量且连续的数据搬运,如音频,我们推荐选用DMA乒乓搬运的方式。

5.3.2.3.1 DMA乒乓搬运接口

在最新SDK中我们已将DMA乒乓搬运封装成标准接口,方便开发者调用,提高开发效率。

使用时请添加 pingpong.c文件,并包含 pingpong.h文件。

  • DMA_PingPongSetup:DMA乒乓搬运建立接口

  • DMA_PingPongIntProc:DMA乒乓搬运标准中断处理接口

  • DMA_PingPongGetTransSize:获取DMA乒乓搬运数据量接口

  • DMA_PingPongEnable:DMA乒乓搬运使能接口

  • DMA_PingPongDisable:DMA乒乓搬运去使能接口

更多程序开发者可以参考voice_remote_ctrl例程。

5.3.2.3.2 DMA乒乓搬运示例

下面将以最常见的DMA乒乓搬运I2s数据为例展示DMA乒乓搬运的用法。

I2s的相关配置不在本文的介绍范围内,默认I2s已经配置好,DMA和I2s协商burstSize=8。

#include "pingpong.h"
#define CHANNEL_ID  0
DMA_PingPong_t PingPong;

static uint32_t DMA_cb_isr(void *user_data)
{
    uint32_t state = DMA_GetChannelIntState(CHANNEL_ID);
    DMA_ClearChannelIntState(CHANNEL_ID, state);
    
    // call 'DMA_PingPongIntProc' to get the pointer of data-buff.
    uint32_t *rr = DMA_PingPongIntProc(&PingPong, CHANNEL_ID);
    uint32_t i = 0;
    // call 'DMA_PingPongGetTransSize' to kwon how much data in data-buff.
    uint32_t transSize = DMA_PingPongGetTransSize(&PingPong);
    while (i < transSize) {
        // do something with data 'rr[i]'
        i++;
    }

    return 0;
}

void DMA_Test(void)
{
    // call 'DMA_PingPongSetup' to setup ping-pong DMA.
    DMA_PingPongSetup(&PingPong, SYSCTRL_DMA_I2S_RX, 100, 8);
    platform_set_irq_callback(PLATFORM_CB_IRQ_DMA, DMA_cb_isr, 0);
    
    // call 'DMA_PingPongEnable' to start ping-pong DMA transmission.
    DMA_PingPongEnable(&PingPong, CHANNEL_ID);
    I2S_ClearRxFIFO(APB_I2S);
    I2S_DMAEnable(APB_I2S, 1, 1);
    I2S_Enable(APB_I2S, 0, 1);   // Enable I2s finally
}

停止DMA乒乓搬运可以调用以下接口:

void Stop(void)
{
    // call 'DMA_PingPongEnable' to disable ping-pong DMA transmission.
    DMA_PingPongDisable(&PingPong, CHANNEL_ID);
    I2S_Enable(APB_I2S, 0, 0);
    I2S_DMAEnable(APB_I2S, 0, 0);
}