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 进行介绍。
- DMA 存储器总线:DMA 通过该总线来执行 存储器数据 的传入和传出。
- DMA 外设总线:DMA 通过该总线访问 AHB 外设 或执行 存储器间 的数据传输。
① DMA 控制器的从机编程接口,通过该接口可以对 DMA 的相关控制寄存器进行设置,从而配置 DMA,实现不同的功能。
② DMA 控制器的外设接口,用于访问相关外设,特别的,当外设接口设置的访问地址是内存地址的时候,DMA 就可以工作在内存到内存模式了。
③ DMA 控制器的 FIFO 区,每个数据流(总共 8 个数据流)都有一个独立的 FIFO,可以实现存储器接口到外设接口之间的数据长度非对齐传输。
④ DMA 控制器的存储器接口,用于访问外部存储器,特别的当存储器地址是外设地址的时候,可以实现类似外设到外设的传输效果。
⑤ DMA 控制器的仲裁器,用于仲裁数据流 0~7 的请求优先级,保证数据有序传输。
⑥ DMA 控制器数据流多通道选择,通过 DMA_SxCR 寄存器控制,每个数据流有多达 8个通道请求可以选择。
外设的 8 个请求独立连接到每个通道,由 DMA_SxCR 控制数据流选择哪一个通道,每个数据流有 8 个通道可供选择,每次只能选择其中一个通道进行 DMA 传输。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_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_LIFCR 的各位就是用来清除 DMA_LISR 的对应位的,通过写 1 清除。在 DMA_LISR 被置位后,我们必须通过向该位寄存器对应的位写入 1 来清除。DMA_HIFCR 的使用同 DMA_LIFCR 类似。
5.3、DMA数据流x配置寄存器
该寄存器控制着 DMA 的很多相关信息,包括数据宽度、外设及存储器的宽度、优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以 DMA_SxCR 是 DMA 传输的核心控制寄存器。
5.4、DMA数据流x数据项数寄存器
这个寄存器控制 DMA 数据流 x 的每次传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少,当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前 DMA 传输的进度。特别注意,这里是数据项数目,而不是指的字节数。比如设置数据位宽为 16 位,那么传输一次(一个项)就是 2 个字节。
5.5、DMA 数据流x外设地址寄存器
该寄存器用来存储 STM32F4 外设的地址,比如我们使用串口 1,那么该寄存器必须写入 0x40011004(USART1_TDR 的地址)。如果使用其他外设,就修改成相应外设的地址就行了。
5.6、DMA数据流x存储器地址寄存器
由于 STM32F407 的 DMA 支持双缓存,所以存储器地址寄存器有两个:DMA_SxM0AR 和 DMA_SxM1AR,其中 DMA_SxM1AR 仅在双缓冲模式下,才有效。
六、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);
形参 hdma 是 DMA_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 通道外设句柄。
StreamBaseAddress 和 StreamIndex 是 数据流基地址 和 索引号,这个是 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);
形参 hdma 是 DMA 句柄,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);
}