《DMG474开发指南_V1.1》第二十五章 DMA实验

第二十五章 DMA实验


       本章,我们将介绍STM32G474的DMA。我们将利用DMA来实现串口数据传送,并在LCD模块上显示当前的传送进度。

       本章分为如下几个小节:

       25.1 DMA简介

       25.2 硬件设计

       25.3 程序设计

       25.4 下载验证


       25.1 DMA简介

       DMA,全称为:Direct Memory Access,即直接存储器访问。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,能使CPU的效率大为提高。

       STM32G474最多有2个DMA控制器(DMA1和DMA2)。每个通道都专用于管理来自一个或多个外设的内存访问请求。每个DMA都包含一个仲裁器,用于处理DMA请求之间的优先级。

       STM32G474的DMA有以下一些特性:

       ① 单AHB主总线架构。

       ② 支持外设到内存、内存到外设、内存到内存和外设到外设数据传输。

       ③ 作为源和目标访问片上存储器映射设备,如Flash内存,SRAM,AHB和APB外设。

       ④ 所有DMA通道均可独立配置:

         1,每个通道都与来自外围设备的DMA请求信号相关联,或与内存到内存传输中的软件触发器相关联,此配置将由软件完成;

         2,请求的优先级可通过软件进行编程(每个通道有4级:非常高、高、中、低),与此同时,还可通过硬件进行编程;

         3,源和目标的传输大小是独立的,源地址和目标地址必须与数据大小对齐;

         4,支持循环缓冲区管理;

         5,要传输的可编程数据数:0到65535;

       ⑤ 可以为DMA1控制器生成单个中断请求,中断请求可以由三个DMA事件中的任何一个引起:传输完成、传输一半或传输错误。

       ⑥ 可以为DMA2控制器每个通道生成中断请求。每个中断请求都是由三个DMA事件中的任何一个引起的:传输完成、传输一半或传输错误。


       25.1.1 DMA框图

       STM32G474有两个DMA控制器,DMA1和DMA2,本章,我们仅针对DMA1进行介绍。

       下面先来学习DMA控制器框图,通过学习DMA控制器框图会有一个很好的整体掌握,同时对之后的编程也会有一个清晰的思路。STM32G474的DMA控制器框图如图25.1.1.1所示:


图25.1.1.1 DMA控制器框图


       图中,我们标记了5处位置,它们分别是:

       ① DMA控制器的主从机编程接口,通过该接口可以对DMA的相关控制寄存器进行设置,从而配置DMA,实现不同的功能。

       ② DMA控制器的外设和存储器接口,用于访问相关外设和存储器。

       ③ DMA通道:CH1~CH8。

       ④ DMA控制器的仲裁器,用于仲裁1~8请求源的优先级,保证数据有序传输。

       ⑤ DMA请求信号。


       25.1.2 DMA寄存器

       l DMA中断状态寄存器(DMA_ISR)

       DMA中断状态寄存器,该寄存器用于管理(读取)通道1~8的中断状态,描述如图25.1.2.1所示:


图25.1.2.1 DMA_LISR寄存器


       如果开启了DMA_ISR中这些位对应的中断,则在达到条件后就会跳到中断服务函数里面去,即使没开启,我们也可以通过查询这些位来获得当前DMA传输的状态。这里我们常用的是TCIFx位,即DMA传输完成与否标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的操作来清除。

       l DMA中断标志清除寄存器(DMA_IFCR)

       DMA中断标志清除寄存器,该寄存器用于清除通道1~8的中断标志,描述如图25.1.2.2所示:


图25.1.2.2 DMA_LIFCR寄存器


       DMA_IFCR的各位就是用来清除DMA_ISR的对应位的,通过写1清除。在DMA_ISR被置位后,我们必须通过向该位寄存器对应的位写入1来清除。

       第三个是DMA通道x配置寄存器(DMA_CCRx)(x=1~8,下同)。该寄存器的我们在这里就不贴出来了,见《STM32G4xx参考手册_V7(英文版).pdf》第12.6.3小节(417页)。该寄存器控制着DMA的很多相关信息,包括数据宽度、外设及存储器的宽度、优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以DMA_SxCR是DMA传输的核心控制寄存器。

       第四个是DMA通道x数据项数寄存器(DMA_CNDTRx)。这个寄存器控制DMA通道x的每次传输所要传输的数据量。其设置范围为0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为0的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前DMA传输的进度。特别注意,这里是数据项数目,而不是指的字节数。比如设置数据位宽为16位,那么传输一次(一个项)就是2个字节。

       第五个是DMA通道x的外设地址寄存器(DMA_CPARx)。该寄存器用来存储STM32G474外设的地址,比如我们使用串口1,那么该寄存器必须写入0x40013828(USART1_TDR的地址)。如果使用其他外设,修改成相应外设的地址就行了。

       最后一个是DMA通道x的存储器地址寄存器(CMARx),该寄存器和DMA_CPARx差不多,但是是用来放存储器的地址的。比如我们使用SendBuf[7800]数组来做存储器,那么我们在DMA_SxM0AR中写入&SendBuff就可以了。


        25.2 硬件设计


       1. 例程功能

       每按下按键KEY0,串口1就会以DMA方式发送数据,同时在LCD上面显示传送进度。打开串口调试助手,可以收到DMA发送的内容。LED0闪烁用于提示程序正在运行。


       2. 硬件资源


       1)LED灯

              LED0 – PE0

       2)独立按键

              KEY0 – PE12

       3)串口1(PB6/PB7连接在板载USB转串口芯片CH340上面)

       4)正点原子1.3寸TFTLCD模块(SPI接口)


       3. 原理图

       DMA属于STM32G474内部资源,通过软件设置好就可以了。


        25.3 程序设计


       25.3.1 DMA的HAL库驱动

       DMA在HAL库中的驱动代码在stm32g4xx_hal_dma.c文件(及其头文件)中。

       1. HAL_DMA_Init函数

       DMA的初始化函数,其声明如下:

HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);

       l 函数描述:

       用于初始化DMA1,DMA2。

       l 函数形参:

       形参1是DMA_HandleTypeDef结构体类型指针变量,其定义如下:

typedef struct __DMA_HandleTypeDef
{
  DMA_Channel_TypeDef            *Instance;  /* 寄存器基地址 */
  DMA_InitTypeDef                Init;       /* DAM通信参数 */
  HAL_LockTypeDef               Lock;        /* DMA锁对象 */
  __IO HAL_DMA_StateTypeDef   State;      /* DMA传输状态 */
  void                             *Parent;    /* 父对象状态,HAL库处理的中间变量 */
void (*XferCpltCallback)( struct __DMA_HandleTypeDef *hdma);/*DMA传输完成回调*/
/* DMA一半传输完成回调 */
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);  
/*DMA传输错误回调*/
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
/* DMA传输中止回调 */
  void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);
  __IO uint32_t      ErrorCode;              /* DMA存取错误代码 */
  DMA_TypeDef      *DmaBaseAddress;  /* DMA通道基地址 */
uint32_t       ChannelIndex;          /* DMA通道索引 */
  DMAMUX_Channel_TypeDef   *DMAmuxChannel;                                   
  DMAMUX_ChannelStatus_TypeDef  *DMAmuxChannelStatus;    
  uint32_t       DMAmuxChannelStatusMask;  
  DMAMUX_RequestGen_TypeDef   *DMAmuxRequestGen;
  DMAMUX_RequestGenStatus_TypeDef *DMAmuxRequestGenStatus;
  uint32_t       DMAmuxRequestGenStatusMask;
}DMA_HandleTypeDef;

       这个结构体内容比较多,上面已注释中文翻译,下面列出几个成员说明一下。

       Instance:是用来设置寄存器基地址。

       Parent:是HAL库处理中间变量,用来指向DMA通道外设句柄。

       其他成员变量是HAL库处理过程状态标识变量,这里就不做过多讲解。

       接下来我们重点介绍Init,它是DMA_InitTypeDef结构体类型变量,该结构体定义如下:

typedef struct
{
  uint32_t Request;               /* 通道请求 */     
  uint32_t Direction;             /* 传输方向,例如存储器到外设DMA_MEMORY_TO_PERIPH */ 
  uint32_t PeriphInc;             /* 外设(非)增量模式,非增量模式DMA_PINC_DISABLE */  
  uint32_t MemInc;                /* 存储器(非)增量模式,增量模式DMA_MINC_ENABLE */  
  uint32_t PeriphDataAlignment; /* 外设数据大小:8/16/32位 */
  uint32_t MemDataAlignment;    /* 存储器数据大小:8/16/32位 */
  uint32_t Mode;                  /* 模式:外设流控模式/循环模式/普通模式 */    
  uint32_t Priority;            /* DMA优先级:低/中/高/非常高 */  
}DMA_InitTypeDef;

       我们通过该结构体成配置DMA_CCRx寄存器和DMA_CNDTRx寄存器的相应位。

       l 函数返回值:

       HAL_StatusTypeDef枚举类型的值。

       以DMA的方式传输串口数据的配置步骤

       1)使能DMA时钟。

       DMA的时钟使能是通过AHB1ENR寄存器来控制的,这里我们要先使能时钟,才可以配置DMA相关寄存器。HAL库方法为:

__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */

       2)初始化DMA。

       调用HAL_DMA_Init函数初始化DMA的相关参数,包括配置通道,外设地址,存储器地址,传输数据量等。

       HAL库为了处理各类外设的DMA请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接DMA和外设句柄。例如要使用串口DMA发送,所以方式为:

__HAL_LINKDMA(&g_uart1_handle, hdmatx, g_dma_handle);

       其中g_uart1_handle是串口初始化句柄,我们在usart.c中定义过了。g_dma_handle是DMA初始化句柄。hdmatx是外设句柄结构体的成员变量,在这里实际就是g_uart1_handle的成员变量。在HAL库中,任何一个可以使用DMA的外设,它的初始化结构体句柄都会有一个DMA_HandleTypeDef指针类型的成员变量,是HAL库用来做相关指向的。hdmatx就是DMA_HandleTypeDef结构体指针类型。

       这句话的含义就是把g_uart1_handle句柄的成员变量hdmatx和DMA句柄g_dma_handle连接起来,是纯软件处理,没有任何硬件操作。

       这里我们就点到为止,如果大家要详细了解HAL库指向关系,请查看本实验宏定义标识符__HAL_LINKDMA的定义和调用方法,就会很清楚了。

       3)使能串口的DMA发送,启动传输。

       串口1的DMA发送实际是串口控制寄存器CR3的位7来控制的,在HAL库中操作该寄存器来使能串口DMA发送的函数为HAL_UART_Transmit_DMA。

       这里大家需要注意,调用该函数后会开启相应的DMA中断,对于本章实验,我们是通过查询的方法获取数据传输状态,所以并没有做中断相关处理,也没有编写中断服务函数。

       HAL库还提供了对串口的DMA发送的停止,暂停,继续等操作函数:

HAL_StatusTypeDef HAL_UART_DMAStop(UART_HandleTypeDef *huart);    /* 停止 */
HAL_StatusTypeDef HAL_UART_DMAPause(UART_HandleTypeDef *huart);   /* 暂停 */
HAL_StatusTypeDef HAL_UART_DMAResume(UART_HandleTypeDef *huart);  /* 恢复 */

       4)查询DMA传输状态。

       在DMA传输过程中,我们要查询DMA传输通道的状态,使用的方法是通过检测DMA寄存器的相关位实现:

__HAL_DMA_GET_FLAG(&g_dma_handle, DMA_FLAG_TC1);

获取当前传输剩余数据量:

__HAL_DMA_GET_COUNTER(&g_dma_handle);

       同样,我们也可以设置对应的DMA数据流传输的数据量大小,函数为:

__HAL_DMA_SET_COUNTER (&g_dma_handle, 1000);

       DMA相关的库函数我们就讲解到这里,大家可以查看HAL库手册详细了解。

       5)DMA中断使用方法。

       HAL库提供了通用DMA中断处理函数HAL_DMA_IRQHandler,在该函数内部,会对DMA传输状态进行分析,然后调用相应的中断处理回调函数(例如串口):

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);     /* 发送完成回调函数 */
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);/* 发送一半回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);     /* 接收完成回调函数 */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);/* 接收一半回调函数 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart);      /* 传输出错回调函数 */


       25.3.2 程序流程图


图25.3.2.1 DMA实验程序流程图


       25.3.3 程序解析


       1. DMA驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考A盘资料本实验对应源码。DMA驱动源码包括两个文件:dma.c和dma.h。

       dma.h头文件只有函数得声明,就不解释了,我们直接介绍dma.c的程序。下面是与DMA初始化相关的函数,其定义如下:

/**
 * @brief       串口TX DMA初始化函数
 * @note        这里的传输形式是固定的, 这点要根据不同的情况来修改
 *               从存储器 -> 外设模式/8位数据宽度/存储器增量模式
 *
 * @param       dma_channel_handle : DMA通道
 * @retval      无
 */
void dma_init(DMA_Channel_TypeDef *dma_channel_handle)
{ 
    __HAL_RCC_DMAMUX1_CLK_ENABLE();
    __HAL_RCC_DMA1_CLK_ENABLE();
 
/* 将DMA与USART1联系起来(发送DMA) */
    __HAL_LINKDMA(&g_uart1_handle, hdmatx, g_dma_handle); 
 
    /* Tx DMA配置 */
    g_dma_handle.Instance = dma_channel_handle;    /* DMA通道选择 */
    g_dma_handle.Init.Request = DMA_REQUEST_USART1_TX;  /* DMA请求选择 */
    g_dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH;  /* 存储器到外设 */
    g_dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;   /* 外设非增量模式 */
g_dma_handle.Init.MemInc = DMA_MINC_ENABLE;    /* 存储器增量模式 */
/* 外设数据长度:8位 */
g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; 
/* 存储器数据长度:8位 */
    g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;       
    g_dma_handle.Init.Mode = DMA_NORMAL;     /* 外设流控模式 */
    g_dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;  /* 中等优先级 */
    HAL_DMA_DeInit(&g_dma_handle);   
    HAL_DMA_Init(&g_dma_handle);
}

       该函数是一个通用的DMA配置函数,DMA1/DMA2的所有通道,都可以利用该函数配置,不过有些固定参数可能要适当修改(比如位宽,传输方向等)。该函数在外部只能修改DMA通道号,更多的其他设置只能在该函数内部修改。对照前面的配置步骤的详细讲解来分析这部分代码即可。


       2. main.c代码

       main.c代码如下:

const uint8_t TEXT_TO_SEND[]={"正点原子 STM32 DMA 串口实验"}; /* 要循环发送的字符串 */
#define SEND_BUF_SIZE       (sizeof(TEXT_TO_SEND)+2)*200  /* 发送数据长度*/
uint8_t g_sendbuf[SEND_BUF_SIZE];        /* 发送数据缓冲区 */
 
int main(void)
{
    uint8_t  key = 0;
    uint16_t i, k;
    uint16_t len;
    uint8_t  mask = 0;
    float pro = 0;    /* 进度 */
 
    HAL_Init();                 /* 初始化HAL库 */
    sys_stm32_clock_init(85, 2, 2, 4, 8);  /* 设置时钟,170Mhz */
    delay_init(170);                           /* 延时初始化 */
    usart_init(115200);                      /* 串口初始化为115200 */
    led_init();                               /* 初始化LED */
    lcd_init();                               /* 初始化LCD */
    key_init();                               /* 初始化按键 */
 
    dma_init(DMA1_Channel1);    /* 初始化DMA */
 
    lcd_show_string(10, 10, 140, 32, 32, "STM32", RED);
    lcd_show_string(10, 42, 140, 24, 24, "DMA TEST", RED);
    lcd_show_string(10, 66, 140, 24, 24, "ATOM@ALIENTEK", RED);
    lcd_show_string(10, 96, 200, 24, 24, "KEY0:Start", RED);
 
    len = sizeof(TEXT_TO_SEND);
    k = 0;
    
    for (i = 0; i < SEND_BUF_SIZE; i++)    /* 填充ASCII字符集数据 */
    {
        if (k >= len)    /* 入换行符 */
        {
            if (mask
            {
                g_sendbuf[i] = 0x0a;
                k = 0;
            }
            else
            {
                g_sendbuf[i] = 0x0d;
                mask++;
            }
        }
        else    /* 复制TEXT_TO_SEND语句 */
        {
            mask = 0;
            g_sendbuf[i] = TEXT_TO_SEND[k];
            k++;
        }
    }
 
    i = 0;
 
    while (1)
    {
        key = key_scan(0);
        if (key == KEY0_PRES)   /* KEY0按下 */
        {
            printf("\r\nDMA DATA:\r\n");
            lcd_show_string(10, 130, 200, 16, 16, "Start Transimit....", BLUE);
            lcd_show_string(10, 150, 200, 16, 16, "   %", BLUE);    /* 显示百分号 */
 
/* 开始一次DMA传输! */
            HAL_UART_Transmit_DMA(&g_uart1_handle, g_sendbuf, SEND_BUF_SIZE); 
 
            /* 等待DMA传输完成,此时我们来做另外一些事情,比如点灯。
             * 实际应用中,传输数据期间,可以执行另外的任务 
*/
            while (1)
            {
/* 等待DMA1通道1传输完成 */
                if (__HAL_DMA_GET_FLAG(&g_dma_handle, DMA_FLAG_TC1)) 
                {
/* 清除DMA1通道1传输完成标志 */
                    __HAL_DMA_CLEAR_FLAG(&g_dma_handle, DMA_FLAG_TC1); 
                    HAL_UART_DMAStop(&g_uart1_handle);  /* 传输完成以后关闭串口DMA */
                    break;
                }
                pro =__HAL_DMA_GET_COUNTER(&g_dma_handle);/* 获取当前还剩余多少数据 */
                len = SEND_BUF_SIZE;       /* 总长度 */
                pro = 1 - (pro / len);      /* 得到百分比 */
                pro *= 100;         /* 扩大100倍 */
                lcd_show_num(10, 150, pro, 3, 16, BLUE);
            }
            lcd_show_num(10, 150, 100, 3, 16, BLUE);   /* 显示100% */
/* 提示传送完成 */
            lcd_show_string(10, 130, 200, 16, 16, "Transimit Finished!", BLUE);
        }
 
        i++;
        delay_ms(10);
 
        if (i == 20)
        {
            LED0_TOGGLE();  /* LED0闪烁,提示系统正在运行 */
            i = 0;
        }
    }
}

       main函数的流程大致是:先初始化发送数据缓冲区g_sendbuf的值,然后通过KEY0开启串口DMA发送,在发送过程中,通过__HAL_DMA_GET_COUNTER(&g_dma_handle)获取当前还剩余的数据量来计算传输百分比,最后在传输结束之后清除相应标志位,提示已经传输完成。


        25.4 下载验证

       将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如图25.4.1所示:


图25.4.1 DMA实验测试图


       我们打开串口调试助手,然后按KEY0,可以看到串口显示如图25.4.2所示的内容:


图25.4.2 串口收到的数据内容


       可以看到串口收到了开发板发送过来的数据,同时可以看到TFTLCD上显示了进度等信息,如图25.4.3所示:


25.4.3 DMA串口数据传输中


       至此,我们整个DMA实验就结束了,希望大家通过本章的学习,掌握STM32G474的DMA使用。DMA是个非常好的功能,它不但能减轻CPU负担,还能提高数据传输速度,合理的应用DMA,往往能让你的程序设计变得简单。


请使用浏览器的分享功能分享到微信等