34. DMA传输数据

一、DMA简介

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

  STM32F407 最多有 2 个 DMA 控制器(DMA1 和 DMA2),两个 DMA 控制器总共有 16 个数据流。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个 DMA 请求的优先权。

  STM32F407 的 DMA 有以下一些特性:

  • 双 AHB 主总线架构,一个用于存储器访问,另一个用于外设访问。

  • 仅支持 32 位访问的 AHB 从编程接口。

  • 每个 DMA 控制器有 8 个数据流,每个数据流有多达 8 个通道(请求)。

  • 每个数据流有单独的四级 32 位先进先出存储器缓冲区(FIFO),可用于 FIFO 模式或直接模式。

  • 通过硬件可以将每个数据流配置为:

    • 支持外设到存储器、存储器到外设和存储器到存储器传输的常规通道。
    • 支持在存储器方双缓冲的双缓冲区通道。
  • 8 个数据流中的每一个都连接到专用硬件 DMA 通道(请求)。

  • DMA 数据流请求之间的优先级可用软件编程(4 个级别:非常高、高、中、低),在软件优先级相同的情况下可以通过硬件决定优先级(例如,请求 0 的优先级高于请求 1)。

  • 每个数据流也支持通过软件触发存储器到存储器的传输(仅限 DMA2 控制器)。

  • 可供每个数据流选择的通道请求数多达 8 个。此选择可由软件配置,允许多个外设启动 DMA 请求。

  • 要传输的数据项的数目可以由 DMA 控制器或外设管理:

    • DMA 流控制器:要传输的数据项的数目是 1 到 65535,可用软件编程。
    • 外设流控制器:要传输的数据项的数目未知并由源或目标外设控制,这些外设通过硬件发出传输结束的信号。
  • 独立的源和目标传输宽度(字节、半字、字):源和目标的数据宽度不相等时,DMA 自动封装/解封必要的传输数据来优化带宽。这个特性仅在 FIFO 模式下可用。

  • 对源和目标的增量或非增量寻址。

  • 支持 4 个、8 个和 16 个节拍的增量突发传输。突发增量的大小可由软件配置,通常等于外设 FIFO 大小的一半。

  • 每个数据流都支持循环缓冲区管理。

  • 5 个事件标志(DMA 半传输、DMA 传输完成、DMA 传输错误、DMA FIFO 错误、直接模式错误),进行逻辑或运算,从而产生每个数据流的单个中断请求。

二、DMA框图

  STM32F407 有两个 DMA 控制器,DMA1 和 DMA2。这里,我们仅针对 DMA1 进行介绍。

STM32F4系统架构

  • DMA 存储器总线:DMA 通过该总线来执行 存储器数据 的传入和传出。
  • DMA 外设总线:DMA 通过该总线访问 AHB 外设 或执行 存储器间 的数据传输。

DMA框图

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

  ② DMA 控制器的外设接口,用于访问相关外设,特别的,当外设接口设置的访问地址是内存地址的时候,DMA 就可以工作在内存到内存模式了。

  ③ DMA 控制器的 FIFO 区,每个数据流(总共 8 个数据流)都有一个独立的 FIFO,可以实现存储器接口到外设接口之间的数据长度非对齐传输。

  ④ DMA 控制器的存储器接口,用于访问外部存储器,特别的当存储器地址是外设地址的时候,可以实现类似外设到外设的传输效果。

  ⑤ DMA 控制器的仲裁器,用于仲裁数据流 0~7 的请求优先级,保证数据有序传输。

  ⑥ DMA 控制器数据流多通道选择,通过 DMA_SxCR 寄存器控制,每个数据流有多达 8个通道请求可以选择。

DMA数据流通道选择

  外设的 8 个请求独立连接到每个通道,由 DMA_SxCR 控制数据流选择哪一个通道,每个数据流有 8 个通道可供选择,每次只能选择其中一个通道进行 DMA 传输。DMA1 和 DMA2 各数据流通道映射具体见下所示:

DMA1各数据流通道映射表

DMA2各数据流通道映射表

每个外设请求都会占用一个通道,相同外设请求可以占用不同数据流通道。

请求使用某个数据流的通道,该数据流其它通道不被选择,不可用。

三、DMA仲裁器

  一个 DMA 控制器对应 8 个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。如果我们需要同时使用同一个 DMA 控制器(DMA1 或 DMA2)多个外设请求时,那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断了。

  仲裁器管理数据流方法分为两个阶段。第一阶段属于 软件阶段,我们在配置数据流时可以通过寄存器设定它的优先级别,具体配置 DMA_SxCR 寄存器 PL[1:0] 位,可以设置为非常高、高、中 和低四个级别。第二阶段属于 硬件阶段,如果两个或以上数据流软件设置优先级一样,则它们优先级取决于数据流编号,编号越低越具有优先权,比如数据流 2 优先级高于数据流 3。

大容量芯片中,DMA1 控制器拥有高于 DMA2 控制器的优先级。

多个请求通过逻辑或输入到 DMA 控制器,只能有一个请求有效。

四、DMA传输数据

  DMA2 支持全部 三种传输模式,而 DMA1 只有 外设到存储器存储器到外设 两种模式。模式选择可以通过 DMA_SxCR 寄存器的 DIR[1:0] 位控制,进而将 DMA_SxCR 寄存器的 EN 位置 1 就可以使能 DMA 传输。

  在 DMA_SxCR 寄存器的 PSIZE[1:0] 和 MSIZE[1:0] 位分别指定外设和存储器数据宽度大小,可以指定为字节(8 位)、半字(16 位)和字(32 位),我们可以根据实际情况设置。直接模式要求外设和存储器数据宽度大小一样,实际上在这种模式下 DMA 数据流直接使用 PSIZE,MSIZE 不被使用。

  DMA 数据流 x 外设地址 DMA_SxPAR(x 为 0 ~ 7) 寄存器用来指定外设地址,它是一个 32 位数据有效寄存器。DMA 数据流 x 存储器 0 地址 DMA_SxM0AR(x 为 0 ~ 7) 寄存器和 DMA 数据流 x 存储器 1 地址 DMA_SxM1AR(x 为 0 ~ 7) 寄存器用来存放存储器地址,其中 DMA_SxM1AR 只用于双缓冲模式,DMA_SxM0AR 和 DMA_SxM1AR 都是 32 位数据有效的。

  当选择外设到存储器模式时,即设置 DMA_SxCR 寄存器的 DIR[1:0] 位为 “00”,DMA_SxPAR 寄存器为外设地址,也是传输的源地址,DMA_SxM0AR 寄存器为存储器地址,也是传输的目标地址。对于存储器到存储器传输模式,即设置 DIR[1:0] 位为 “10” 时,采用与外设到存储器模式相同配置。而对于存储器到外设,即设置 DIR[1:0] 位为 “01” 时,DMA_SxM0AR 寄存器作为为源地址,DMA_SxPAR 寄存器作为目标地址。

五、DAM常用寄存器

5.1、DMA中断状态寄存器

  DMA 中断状态寄存器,该寄存器总共有 2 个:DMA_LISR 和 DMA_HISR,每个寄存器管理 4 数据流(总共 8 个),DMA_LISR 寄存器用于管理数据流 0 ~ 3,而 DMA_HISR 用于管理数据流 4 ~ 7。这两个寄存器各位描述都完全一模一样,只是管理的数据流不一样。

DMA低中断状态寄存器

DMA高中断状态寄存器

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

5.2、DMA中断标志清除寄存器

  DMA 中断标志清除寄存器,该寄存器同样有 2 个:DMA_LIFCR 和 DMA_HIFCR,同样是每个寄存器控制 4 个数据流,DMA_LIFCR 寄存器用于管理数据流 0 ~ 3,而 DMA_HIFCR 用于管理数据流 4 ~ 7。这两个寄存器各位描述都完全一模一样,只是管理的数据流不一样。

DMA低中断标志清零寄存器

DMA高中断标志清零寄存器

  DMA_LIFCR 的各位就是用来清除 DMA_LISR 的对应位的,通过写 1 清除。在 DMA_LISR 被置位后,我们必须通过向该位寄存器对应的位写入 1 来清除。DMA_HIFCR 的使用同 DMA_LIFCR 类似。

5.3、DMA数据流x配置寄存器

DMA数据流x配置寄存器

  该寄存器控制着 DMA 的很多相关信息,包括数据宽度、外设及存储器的宽度、优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以 DMA_SxCR 是 DMA 传输的核心控制寄存器。

5.4、DMA数据流x数据项数寄存器

DMA数据流x数据项数寄存器

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

5.5、DMA 数据流x外设地址寄存器

DMA数据流x外设地址寄存器

  该寄存器用来存储 STM32F4 外设的地址,比如我们使用串口 1,那么该寄存器必须写入 0x40011004(USART1_TDR 的地址)。如果使用其他外设,就修改成相应外设的地址就行了。

5.6、DMA数据流x存储器地址寄存器

  由于 STM32F407 的 DMA 支持双缓存,所以存储器地址寄存器有两个:DMA_SxM0AR 和 DMA_SxM1AR,其中 DMA_SxM1AR 仅在双缓冲模式下,才有效。

DMA数据流x存储器0地址寄存器

DMA数据流x存储器1地址寄存器

六、DMA配置步骤

6.1、使能DMA时钟

  使能对应的 DMA 时钟。

#define __HAL_RCC_DMA1_CLK_ENABLE()  do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMA1EN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMA1EN);\
                                        UNUSED(tmpreg); \
                                         } while(0U)
#define __HAL_RCC_DMA2_CLK_ENABLE()     do { \
                                        __IO uint32_t tmpreg = 0x00U; \
                                        SET_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMA2EN);\
                                        /* Delay after an RCC peripheral clock enabling */ \
                                        tmpreg = READ_BIT(RCC->AHB1ENR, RCC_AHB1ENR_DMA2EN);\
                                        UNUSED(tmpreg); \
                                          } while(0U)

6.2、配置DMA工作参数

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

HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);

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

typedef struct __DMA_HandleTypeDef
{
    DMA_Stream_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传输完成回调
    void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);          // DMA一半传输完成回调
    void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);            // DMA传输完整的Memory1回调
    void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);        // DMA传输半完全内存回调
    void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);             // DMA传输错误回调
    void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);             // DMA传输中止回调

    __IO uint32_t ErrorCode;                                                    // DMA存取错误代码
    uint32_t StreamBaseAddress;                                                 // DMA通道基地址
    uint32_t StreamIndex;                                                       // DMA通道索引 
}DMA_HandleTypeDef;

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

#define DMA1_Stream0        ((DMA_Stream_TypeDef *) DMA1_Stream0_BASE)
#define DMA1_Stream1        ((DMA_Stream_TypeDef *) DMA1_Stream1_BASE)
#define DMA1_Stream2        ((DMA_Stream_TypeDef *) DMA1_Stream2_BASE)
#define DMA1_Stream3        ((DMA_Stream_TypeDef *) DMA1_Stream3_BASE)
#define DMA1_Stream4        ((DMA_Stream_TypeDef *) DMA1_Stream4_BASE)
#define DMA1_Stream5        ((DMA_Stream_TypeDef *) DMA1_Stream5_BASE)
#define DMA1_Stream6        ((DMA_Stream_TypeDef *) DMA1_Stream6_BASE)
#define DMA1_Stream7        ((DMA_Stream_TypeDef *) DMA1_Stream7_BASE)

#define DMA2_Stream0        ((DMA_Stream_TypeDef *) DMA2_Stream0_BASE)
#define DMA2_Stream1        ((DMA_Stream_TypeDef *) DMA2_Stream1_BASE)
#define DMA2_Stream2        ((DMA_Stream_TypeDef *) DMA2_Stream2_BASE)
#define DMA2_Stream3        ((DMA_Stream_TypeDef *) DMA2_Stream3_BASE)
#define DMA2_Stream4        ((DMA_Stream_TypeDef *) DMA2_Stream4_BASE)
#define DMA2_Stream5        ((DMA_Stream_TypeDef *) DMA2_Stream5_BASE)
#define DMA2_Stream6        ((DMA_Stream_TypeDef *) DMA2_Stream6_BASE)
#define DMA2_Stream7        ((DMA_Stream_TypeDef *) DMA2_Stream7_BASE)

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

  StreamBaseAddressStreamIndex数据流基地址索引号,这个是 HAL 库处理的时候会自动计算,用户无需设置。

  Init,它是 DMA_InitTypeDef 结构体类型变量,该结构体定义如下:

typedef struct
{
    uint32_t Channel;               // 传输通道
    uint32_t Direction;             // 传输方向
    uint32_t PeriphInc;             // 外设地址是否自增
    uint32_t MemInc;                // 内存地址是否自增
    uint32_t PeriphDataAlignment;   // 外设数据大小
    uint32_t MemDataAlignment;      // 存储器数据大小
    uint32_t Mode;                  // 传输模式
    uint32_t Priority;              // DMA优先级
    uint32_t FIFOMode;              // FIFO模式开启或者禁止
    uint32_t FIFOThreshold;         // FIFO阈值选择
    uint32_t MemBurst;              // 存储器突发模式
    uint32_t PeriphBurst;           // 外设突发模式
}DMA_InitTypeDef;

  成员 Channel 用来 指定传输通道,可选值如下:

#define DMA_CHANNEL_0                 0x00000000U    /*!< DMA Channel 0 */
#define DMA_CHANNEL_1                 0x02000000U    /*!< DMA Channel 1 */
#define DMA_CHANNEL_2                 0x04000000U    /*!< DMA Channel 2 */
#define DMA_CHANNEL_3                 0x06000000U    /*!< DMA Channel 3 */
#define DMA_CHANNEL_4                 0x08000000U    /*!< DMA Channel 4 */
#define DMA_CHANNEL_5                 0x0A000000U    /*!< DMA Channel 5 */
#define DMA_CHANNEL_6                 0x0C000000U    /*!< DMA Channel 6 */
#define DMA_CHANNEL_7                 0x0E000000U    /*!< DMA Channel 7 */

  成员 Direction 用来 指定传输方向,可选值如下:

#define DMA_PERIPH_TO_MEMORY          0x00000000U                   // 外设到内存
#define DMA_MEMORY_TO_PERIPH          ((uint32_t)DMA_SxCR_DIR_0)    // 内存到外设
#define DMA_MEMORY_TO_MEMORY          ((uint32_t)DMA_SxCR_DIR_1)    // 内存到内存

  成员 PeriphInc 用来 设置外设地址是否自增,可选值如下:

#define DMA_PINC_ENABLE               ((uint32_t)DMA_SxCR_PINC)   /*!< Peripheral increment mode enable  */
#define DMA_PINC_DISABLE              0x00000000U                 /*!< Peripheral increment mode disable */

  成员 MemInc 用来 设置内存地址是否自增,可选值如下:

#define DMA_MINC_ENABLE               ((uint32_t)DMA_SxCR_MINC)   /*!< Memory increment mode enable  */
#define DMA_MINC_DISABLE              0x00000000U                 /*!< Memory increment mode disable */

  成员 PeriphDataAlignment 用来 外设数据大小,可选值如下:

#define DMA_PDATAALIGN_BYTE           0x00000000U                  /*!< Peripheral data alignment: Byte     */
#define DMA_PDATAALIGN_HALFWORD       ((uint32_t)DMA_SxCR_PSIZE_0) /*!< Peripheral data alignment: HalfWord */
#define DMA_PDATAALIGN_WORD           ((uint32_t)DMA_SxCR_PSIZE_1) /*!< Peripheral data alignment: Word     */

  成员 MemDataAlignment 用来 存储器数据大小,可选值如下:

#define DMA_MDATAALIGN_BYTE           0x00000000U                  /*!< Memory data alignment: Byte     */
#define DMA_MDATAALIGN_HALFWORD       ((uint32_t)DMA_SxCR_MSIZE_0) /*!< Memory data alignment: HalfWord */
#define DMA_MDATAALIGN_WORD           ((uint32_t)DMA_SxCR_MSIZE_1) /*!< Memory data alignment: Word     */

  成员 Mode 用来 设置传输模式,可选值如下:

#define DMA_NORMAL                    0x00000000U                  // 普通模式
#define DMA_CIRCULAR                  ((uint32_t)DMA_SxCR_CIRC)    // 循环模式
#define DMA_PFCTRL                    ((uint32_t)DMA_SxCR_PFCTRL)  // 外设流控模式

  成员 Priority 用来 DMA 优先级,可选值如下:

#define DMA_PRIORITY_LOW              0x00000000U                 /*!< Priority level: Low       */
#define DMA_PRIORITY_MEDIUM           ((uint32_t)DMA_SxCR_PL_0)   /*!< Priority level: Medium    */
#define DMA_PRIORITY_HIGH             ((uint32_t)DMA_SxCR_PL_1)   /*!< Priority level: High      */
#define DMA_PRIORITY_VERY_HIGH        ((uint32_t)DMA_SxCR_PL)     /*!< Priority level: Very High */

  成员 FIFOThreshold 用来 设置是否开启 FIFO,可选值如下:

#define DMA_FIFOMODE_DISABLE          0x00000000U                 /*!< FIFO mode disable */
#define DMA_FIFOMODE_ENABLE           ((uint32_t)DMA_SxFCR_DMDIS) /*!< FIFO mode enable  */

  成员 FIFOMode 用来 设置 FIFO 的阈值,可选值如下:

#define DMA_FIFO_THRESHOLD_1QUARTERFULL       0x00000000U                  /*!< FIFO threshold 1 quart full configuration  */
#define DMA_FIFO_THRESHOLD_HALFFULL           ((uint32_t)DMA_SxFCR_FTH_0)  /*!< FIFO threshold half full configuration     */
#define DMA_FIFO_THRESHOLD_3QUARTERSFULL      ((uint32_t)DMA_SxFCR_FTH_1)  /*!< FIFO threshold 3 quarts full configuration */
#define DMA_FIFO_THRESHOLD_FULL               ((uint32_t)DMA_SxFCR_FTH)    /*!< FIFO threshold full configuration          */

  成员 MemBurst 用来 设置存储器突发模式,可选值如下:

#define DMA_MBURST_SINGLE             0x00000000U
#define DMA_MBURST_INC4               ((uint32_t)DMA_SxCR_MBURST_0)  
#define DMA_MBURST_INC8               ((uint32_t)DMA_SxCR_MBURST_1)  
#define DMA_MBURST_INC16              ((uint32_t)DMA_SxCR_MBURST)

  成员 PeriphBurst 用来 设置外设突发模式,可选值如下:

#define DMA_PBURST_SINGLE             0x00000000U
#define DMA_PBURST_INC4               ((uint32_t)DMA_SxCR_PBURST_0)
#define DMA_PBURST_INC8               ((uint32_t)DMA_SxCR_PBURST_1)
#define DMA_PBURST_INC16              ((uint32_t)DMA_SxCR_PBURST)

  该函数的返回值是 HAL_StatusTypeDef 枚举类型的值,有 4 个,分别是 HAL_OK 表示 成功HAL_ERROR 表示 错误HAL_BUSY 表示 忙碌HAL_TIMEOUT 表示 超时

typedef enum 
{
    HAL_OK = 0x00U,             // 成功
    HAL_ERROR = 0x01U,          // 错误
    HAL_BUSY = 0x02U,           // 忙碌
    HAL_TIMEOUT = 0x03U         // 超时
} HAL_StatusTypeDef;

6.3、连接DMA和外设

  HAL 库为了处理各类外设的 DMA 请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接 DMA 和外设句柄。

#define __HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD__, __DMA_HANDLE__)               \
                        do{                                                      \
                              (__HANDLE__)->__PPP_DMA_FIELD__ = &(__DMA_HANDLE__); \
                              (__DMA_HANDLE__).Parent = (__HANDLE__);             \
                          } while(0U)

  其中 __HANDLE__ 是外设初始化句柄。__PPP_DMA_FIELD__ 是外设句柄结构体的成员变量名。__DMA_HANDLE__ 是 DMA初始化句柄。

  在 HAL 库中,任何一个可以使用 DMA 的外设,它的初始化结构体句柄都会有一个 DMA_HandleTypeDef 指针类型的成员变量,是 HAL 库用来做相关指向的。__PPP_DMA_FIELD__ 就是 DMA_HandleTypeDef 结构体指针类型。这句话的含义就是把 __HANDLE__ 句柄的成员变量 __PPP_DMA_FIELD__ 和 DMA 句柄 __DMA_HANDLE__ 连接起来,是纯软件处理,没有任何硬件操作。

6.4、使能中断

6.4.1、设置中断优先级分组

  HAL_NVIC_SetPriorityGrouping() 函数是设置中断优先级分组函数。其声明如下:

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup);

  其中,参数 PriorityGroup中断优先级分组号,可以选择范围如下:

#define NVIC_PRIORITYGROUP_0         0x00000007U /*!< 0 bits for pre-emption priority
                                                      4 bits for subpriority */
#define NVIC_PRIORITYGROUP_1         0x00000006U /*!< 1 bits for pre-emption priority
                                                      3 bits for subpriority */
#define NVIC_PRIORITYGROUP_2         0x00000005U /*!< 2 bits for pre-emption priority
                                                      2 bits for subpriority */
#define NVIC_PRIORITYGROUP_3         0x00000004U /*!< 3 bits for pre-emption priority
                                                      1 bits for subpriority */
#define NVIC_PRIORITYGROUP_4         0x00000003U /*!< 4 bits for pre-emption priority
                                                      0 bits for subpriority */

  这个函数在一个工程里基本只调用一次,而且是在程序 HAL 库初始化函数里面已经被调用,后续就不会再调用了。因为当后续调用设置成不同的中断优先级分组时,有可能造成前面设置好的抢占优先级和响应优先级不匹配。如果调用了多次,则以最后一次为准。

6.4.2、设置中断优先级

  HAL_NVIC_SetPriority() 函数是设置中断优先级函数。其声明如下:

void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

typedef enum
{
  DMA1_Stream0_IRQn           = 11,     /*!< DMA1 Stream 0 global Interrupt                                    */
  DMA1_Stream1_IRQn           = 12,     /*!< DMA1 Stream 1 global Interrupt                                    */
  DMA1_Stream2_IRQn           = 13,     /*!< DMA1 Stream 2 global Interrupt                                    */
  DMA1_Stream3_IRQn           = 14,     /*!< DMA1 Stream 3 global Interrupt                                    */
  DMA1_Stream4_IRQn           = 15,     /*!< DMA1 Stream 4 global Interrupt                                    */
  DMA1_Stream5_IRQn           = 16,     /*!< DMA1 Stream 5 global Interrupt                                    */
  DMA1_Stream6_IRQn           = 17,     /*!< DMA1 Stream 6 global Interrupt                                    */
  DMA1_Stream7_IRQn           = 47,     /*!< DMA1 Stream7 Interrupt                                            */

  DMA2_Stream0_IRQn           = 56,     /*!< DMA2 Stream 0 global Interrupt                                    */
  DMA2_Stream1_IRQn           = 57,     /*!< DMA2 Stream 1 global Interrupt                                    */
  DMA2_Stream2_IRQn           = 58,     /*!< DMA2 Stream 2 global Interrupt                                    */
  DMA2_Stream3_IRQn           = 59,     /*!< DMA2 Stream 3 global Interrupt                                    */
  DMA2_Stream4_IRQn           = 60,     /*!< DMA2 Stream 4 global Interrupt                                    */
  DMA2_Stream5_IRQn           = 68,     /*!< DMA2 Stream 5 global interrupt                                    */
  DMA2_Stream6_IRQn           = 69,     /*!< DMA2 Stream 6 global interrupt                                    */
  DMA2_Stream7_IRQn           = 70,     /*!< DMA2 Stream 7 global interrupt                                    */
} IRQn_Type;

  参数 PreemptPriority抢占优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

  参数 SubPriority响应优先级,可以选择范围:0 到 15,具体根据中断优先级分组决定。

6.4.3、使能中断

  HAL_NVIC_EnableIRQ() 函数是中断使能函数。其声明如下:

void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);

  其中,参数 IRQn中断号,可以选择范围:IRQn_Type 定义的枚举类型,定义在 stm32f407xx.h。

6.5、编写中断服务函数

  每开启一个中断,就必须编写其对应的中断服务函数,否则将导致死机(CPU 将找不到中断服务函数)。中断服务函数接口厂家已经在 startup_stm32f407xx.s 中写好了。

void DMA1_Stream0_IRQHandler(void);
void DMA1_Stream1_IRQHandler(void);
void DMA1_Stream2_IRQHandler(void);
void DMA1_Stream3_IRQHandler(void);
void DMA1_Stream4_IRQHandler(void);
void DMA1_Stream5_IRQHandler(void);
void DMA1_Stream6_IRQHandler(void);
void DMA1_Stream7_IRQHandler(void);

void DMA2_Stream0_IRQHandler(void);
void DMA2_Stream1_IRQHandler(void);
void DMA2_Stream2_IRQHandler(void);
void DMA2_Stream3_IRQHandler(void);
void DMA2_Stream4_IRQHandler(void);
void DMA2_Stream5_IRQHandler(void);
void DMA2_Stream6_IRQHandler(void);
void DMA2_Stream7_IRQHandler(void);

  HAL 库为了用户使用方便,提供了一个中断通用入口函数 HAL_DMA_IRQHandler()

6.4、开始DMA传输

HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);

  形参 hdmaDMA 句柄SrcAddress源地址DstAddress目的地址DataLength 是要 传输的数据长度

6.6、查询DMA传输通道的状态

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

#define __HAL_DMA_GET_FLAG(__HANDLE__, __FLAG__)\
(((uint32_t)((__HANDLE__)->Instance) > (uint32_t)DMA2_Stream3)? (DMA2->HISR & (__FLAG__)) :\
 ((uint32_t)((__HANDLE__)->Instance) > (uint32_t)DMA1_Stream7)? (DMA2->LISR & (__FLAG__)) :\
 ((uint32_t)((__HANDLE__)->Instance) > (uint32_t)DMA1_Stream3)? (DMA1->HISR & (__FLAG__)) : (DMA1->LISR & (__FLAG__)))

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

#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->NDTR)

  设置对应的 DMA 数据流传输的数据量大小:

#define __HAL_DMA_SET_COUNTER(__HANDLE__, __COUNTER__) ((__HANDLE__)->Instance->NDTR = (uint16_t)(__COUNTER__))

6.7、使能DMA外设

#define __HAL_DMA_ENABLE(__HANDLE__)      ((__HANDLE__)->Instance->CR |=  DMA_SxCR_EN)

6.8、失能DMA外设

#define __HAL_DMA_DISABLE(__HANDLE__)     ((__HANDLE__)->Instance->CR &=  ~DMA_SxCR_EN)

七、程序源码

7.1、存储器到存储器间数据传输

  DMA2 支持全部 三种传输模式,而 DMA1 只有 外设到存储器存储器到外设 两种模式。DMA内存到内存间的数据传输初始化函数:

DMA_HandleTypeDef g_dma2_handle;

/**
 * @brief DMA内存到内存间的数据传输初始化函数
 * 
 * @param dma_stream DMA数据流,可选值: DMA2_Stream0 ~ DMA2_Stream7
 * @param channel DMA通道,可选值: DMA_CHANNEL_0 ~ DMA_CHANNEL_7
 * @param dataLength 数据长度,可选值: [8, 16, 32]
 * @param mode DMA模式,可选值: [DMA_NORMAL, DMA_CIRCULAR, DMA_PFCTRL]
 * @param priority DMA通道优先级,可选值: [DMA_PRIORITY_LOW, DMA_PRIORITY_MEDIUM, DMA_PRIORITY_HIGH, DMA_PRIORITY_VERY_HIGH]
 */
void DMA_MemoryToMemory_Init(DMA_Stream_TypeDef *dma_stream, uint32_t channel, uint8_t dataLength, uint32_t mode,  uint32_t priority)
{
    __HAL_RCC_DMA2_CLK_ENABLE();

    g_dma2_handle.Instance = dma_stream;                                        // DMA寄存器基地址
    g_dma2_handle.Init.Channel = channel;                                       // DMA通道
    g_dma2_handle.Init.Direction = DMA_MEMORY_TO_MEMORY;                        // 存储器到存储器
    g_dma2_handle.Init.PeriphInc = DMA_PINC_ENABLE;                             // 外设地址增模式
    g_dma2_handle.Init.MemInc = DMA_MINC_ENABLE;                                // 存储器地址增模式
    switch (dataLength)
    {
    case 8:
        g_dma2_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;           // 外设地址长度
        g_dma2_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;              // 存储器地址长度
        break;
    case 16:
        g_dma2_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;       // 外设地址长度
        g_dma2_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;          // 存储器地址长度
        break;
    case 32:
        g_dma2_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;           // 外设地址长度
        g_dma2_handle.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;              // 存储器地址长度
        break;
    default:
        break;
    }
  
    g_dma2_handle.Init.Mode = mode;                                             // DMA模式
    g_dma2_handle.Init.Priority = priority;                                     // DMA通道优先级
    HAL_DMA_Init(&g_dma2_handle);

    if (dma_stream == DMA2_Stream0)
    {
        HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 2, 0);
        HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
    }
}

  DMA2 数据流 0 中断服务函数:

/**
 * @brief DMA2数据流0中断服务函数
 * 
 */
void DMA2_Stream0_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&g_dma2_handle);
}

  自定义的 DMA 传输数据完成回调函数:

/**
 * @brief DMA传输数据完成回调函数
 * 
 * @param hdma DMA句柄
 */
void DMA_CompleteCallback(DMA_HandleTypeDef *hdma)
{
    if (hdma->Instance == DMA2_Stream0)
    {
        dma_is_finished = 1;
    }
}

  main() 函数:

const char src[] = "Hello Sakura!";
char des[15];

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
  
    USART1_Init(115200);
    DMA_MemoryToMemory_Init(DMA2_Stream0, DMA_CHANNEL_0, 8, DMA_NORMAL, DMA_PRIORITY_HIGH);

    // 注册DMA传输完成回调函数
    HAL_DMA_RegisterCallback(&g_dma2_handle, HAL_DMA_XFER_CPLT_CB_ID, DMA_CompleteCallback);

    HAL_DMA_Start_IT(&g_dma2_handle, (uint32_t)src, (uint32_t)des, sizeof(src));

    while (1)
    {
        if (dma_is_finished)
        {
            printf("%s\r\n", des);
            dma_is_finished = 0;
        }
    }
  
    return 0;
}

7.2、存储器到外设间数据传输

DMA_HandleTypeDef g_dma_handle;

/**
 * @brief DMA存储器到外设间的数据传输初始化函数
 * 
 * @param dma_stream DMA数据流,可选值: DMA1_Stream0 ~ DMA1_Stream7, DMA2_Stream0 ~ DMA2_Stream7
 * @param channel DMA通道,可选值: DMA_CHANNEL_0 ~ DMA_CHANNEL_7
 * @param dataLength 数据长度,可选值: [8, 16, 32]
 * @param mode DMA模式,可选值: [DMA_NORMAL, DMA_CIRCULAR, DMA_PFCTRL]
 * @param priority DMA通道优先级,可选值: [DMA_PRIORITY_LOW, DMA_PRIORITY_MEDIUM, DMA_PRIORITY_HIGH, DMA_PRIORITY_VERY_HIGH]
 */
void DMA_MemoryToPeripheral_Init(DMA_Stream_TypeDef *dma_stream, uint32_t channel, uint8_t dataLength, uint32_t mode, uint32_t priority)
{ 
    if ((uint32_t)dma_stream > (uint32_t)DMA2)                                  // 得到当前stream是属于DMA2还是DMA1
    {
        __HAL_RCC_DMA2_CLK_ENABLE();
    }
    else
    {
        __HAL_RCC_DMA1_CLK_ENABLE();
    }

    g_dma_handle.Instance = dma_stream;                                         // 数据流选择
    g_dma_handle.Init.Channel = channel;                                        // 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;                                 // 存储器增量模式
    switch (dataLength)
    {
    case 8:
        g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;            // 外设地址长度
        g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;               // 存储器地址长度
        break;
    case 16:
        g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;        // 外设地址长度
        g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;           // 存储器地址长度
        break;
    case 32:
        g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;            // 外设地址长度
        g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;               // 存储器地址长度
        break;
    default:
        break;
    }
    g_dma_handle.Init.Mode = mode;                                              // DMA模式
    g_dma_handle.Init.Priority = priority;                                      // DMA通道优先级
    g_dma_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;                          // 关闭FIFO模式
    g_dma_handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;                  // FIFO阈值配置
    g_dma_handle.Init.MemBurst = DMA_MBURST_SINGLE;                             // 存储器突发单次传输
    g_dma_handle.Init.PeriphBurst = DMA_PBURST_SINGLE;                          // 外设突发单次传输

    HAL_DMA_Init(&g_dma_handle);
}

  有关时钟配置函数请在 STM32 的时钟系统 篇章查看。

  有关 USART1 的配置请在 串口通信 篇章查看。

  main() 函数:

int main(void)
{
    char src[] = "Hello Sakura!\r\n";

    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
  
    USART1_Init(115200);
    DMA_MemoryToPeripheral_Init(DMA2_Stream7, DMA_CHANNEL_4, 8, DMA_NORMAL, DMA_PRIORITY_HIGH);

    __HAL_LINKDMA(&g_usart1_handle, hdmatx, g_dma_handle);                     // 将DMA与USART1联系起来

    HAL_UART_Transmit_DMA(&g_usart1_handle, (uint8_t *)src, sizeof(src));       // 串口使用DMA发送数据

    while (1)
    {
        if (__HAL_DMA_GET_FLAG(&g_dma_handle, DMA_FLAG_TCIF3_7))                // 等待DMA2_Stream7传输完成
        {
            __HAL_DMA_CLEAR_FLAG(&g_dma_handle, DMA_FLAG_TCIF3_7);              // 清除DMA2_Stream7传输完成标志
            HAL_UART_DMAStop(&g_usart1_handle);                                 // 传输完成以后关闭串口DMA
            break;
        }
    }
  
    return 0;
}

7.3、外设到存储器间数据传输

/**
 * @brief DMA外设到存储器间的数据传输初始化函数
 * 
 * @param dma_stream DMA数据流,可选值: DMA1_Stream0 ~ DMA1_Stream7, DMA2_Stream0 ~ DMA2_Stream7
 * @param channel DMA通道,可选值: DMA_CHANNEL_0 ~ DMA_CHANNEL_7
 * @param dataLength 数据长度,可选值: [8, 16, 32]
 * @param mode DMA工作模式,可选值: [DMA_NORMAL, DMA_CIRCULAR, DMA_PFCTRL]
 * @param priority DMA通道优先级,可选值: [DMA_PRIORITY_LOW, DMA_PRIORITY_MEDIUM, DMA_PRIORITY_HIGH, DMA_PRIORITY_VERY_HIGH]
 */
void DMA_PeripheralToMemory_Init(DMA_Stream_TypeDef *dma_stream, uint32_t channel, uint8_t dataLength, uint32_t mode,  uint32_t priority)
{ 
    if ((uint32_t)dma_stream > (uint32_t)DMA2)                                  // 得到当前stream是属于DMA2还是DMA1
    {
        __HAL_RCC_DMA2_CLK_ENABLE();
    }
    else
    {
        __HAL_RCC_DMA1_CLK_ENABLE();
    }

    g_dma_handle.Instance = dma_stream;                                         // 数据流选择
    g_dma_handle.Init.Channel = channel;                                        // DMA通道选择
    g_dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;                         // 外设到存储器
    g_dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;                             // 外设非增量模式 
    g_dma_handle.Init.MemInc = DMA_MINC_ENABLE;                                 // 存储器增量模式
    switch (dataLength)
    {
    case 8:
        g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;            // 外设地址长度
        g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;               // 存储器地址长度
        break;
    case 16:
        g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;        // 外设地址长度
        g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;           // 存储器地址长度
        break;
    case 32:
        g_dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;            // 外设地址长度
        g_dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;               // 存储器地址长度
        break;
    default:
        break;
    }
    g_dma_handle.Init.Mode = mode;                                              // DMA模式
    g_dma_handle.Init.Priority = priority;                                      // DMA通道优先级
    g_dma_handle.Init.FIFOMode = DMA_FIFOMODE_DISABLE;                          // 关闭FIFO模式
    g_dma_handle.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;                  // FIFO阈值配置
    g_dma_handle.Init.MemBurst = DMA_MBURST_SINGLE;                             // 存储器突发单次传输
    g_dma_handle.Init.PeriphBurst = DMA_PBURST_SINGLE;                          // 外设突发单次传输

    HAL_DMA_Init(&g_dma_handle);
}
posted @ 2023-12-26 17:12  星光樱梦  阅读(25)  评论(0编辑  收藏  举报