| 上一篇 | 下一篇 |
|---|---|
| DMA(2) |
目 录
- [7)DMA 相关寄存器介绍(F1)](#7)DMA 相关寄存器介绍(F1))
- [8)相关 HAL 库函数](#8)相关 HAL 库函数)
-
- [① HAL_DMA_Init()](#① HAL_DMA_Init())
- [② HAL_DMA_Start_IT()](#② HAL_DMA_Start_IT())
- [③ HAL_DMA_Start()](#③ HAL_DMA_Start())
- [④ __HAL_LINKDMA():star:](#④ __HAL_LINKDMA():star:)
- [⑤ HAL_UART_Transmit_DMA()](#⑤ HAL_UART_Transmit_DMA())
- [⑥ DMA 外设相关结构体](#⑥ DMA 外设相关结构体)
- 9)配置步骤(以DMA方式传输串口数据)
- [10)实验示例 1(内存到内存)](#10)实验示例 1(内存到内存))
- [11)实验示例 2(内存到外设)](#11)实验示例 2(内存到外设))
- 12)问题
7)DMA 相关寄存器介绍(F1)

其中 x 表示通道号,在 HAL 库函数中,会通过宏定义来区分 DMA1 和 DMA2。比如:DMA1_Channel1 ~ DMA1_Channel7、DMA2_Channel1 ~ DMA2_Channel5。
-
DMA_CCRx 寄存器:
- 设置数据传输方向,外设到内存、内存到外设、内存到内存;
- 设置通道优先级:最高、高、中、低;
- 设置循环模式、单次模式;
- 设置增量模式(内存接收数据一般需要配置为增量模式,接收一个数据后,往后移动一个地址);
- 设置数据宽度(单位可以是字节、半字(2字节)、字(4字节)),当传输和接收的字节宽度不一致时,需查看手册内办法;
- 传输错误中断使能;
- 传输完成中断使能;
- 半传输中断(传输到一半触发中断);
- 通道开启与关闭。
-
DMA_ISR 寄存器:
- 对应上面三种中断(传输错误、半传输完成、传输完成)的标志位,对应位置 1 就是产生,置 0 就是没产生。
- 对于传输完成中断,如果传输长度为 100,但是最终 CNDTR 并没有减到 0 ,那么是不会触发传输完成中断的。
-
DMA_IFCR 寄存器:
- 对应位置 1 即可清除 DMA_ISR 寄存器中的对应位;
-
DMA_CNDTRx 寄存器:
- 低15位有效,即最大数据传输数目为 65535 。
- 非循环模式下传输结束后,要开始新的DMA传输,需要在关闭DMA通道情况下,在该寄存器中重新写入传输数目。
-
DMA_CPARx 寄存器、DMA_CMARx 寄存器
- 存储外设和内存地址
8)相关 HAL 库函数

其中 x 表示通道号,在 HAL 库函数中,会通过宏定义来区分 DMA1 和 DMA2。比如:DMA1_Channel1 ~ DMA1_Channel7、DMA2_Channel1 ~ DMA2_Channel5。
注意:下面前四个函数都属于是初始化的部分。
① HAL_DMA_Init()
声明:HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma)
功能:配置DMA传输的传输方向、增量模式、数据宽度等等
输入参数:DMA句柄
② HAL_DMA_Start_IT()
声明:HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
功能:启动 DMA 传输,并开启中断
输入参数:DMA句柄、源地址、目标地址、传输数据长度
在开启 DMA 传输的同时会开启中断,默认开启的是:
- 传输完成中断(TC)
- 传输错误中断(TE)
半传输中断(HT)仅在用户注册了 XferHalfCpltCallback 回调函数时才开启。
注意:只有在不使用外设(内存到内存)、但需要开启中断的情况下,才会直接调用此函数。
③ HAL_DMA_Start()
声明:HAL_StatusTypeDef HAL_DMA_Start (DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength);
功能:启动 DMA 传输,不开启中断
输入参数:DMA句柄、源地址、目标地址、传输数据长度
注意:只有在不使用外设(内存到内存)、不开启中断的情况下,才会直接调用此函数。
④ __HAL_LINKDMA()⭐️
功能:连接DMA和外设,这样的话,就可以直接使用 HAL_UART_Transmit_DMA() 这些函数,不用再调用DMA句柄了
使用:具体看实验
⑤ HAL_UART_Transmit_DMA()
声明:HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
注意:HAL_UART_Transmit_DMA(...) 函数内部是调用的 HAL_DMA_Start_IT(...) ,不用再显式调用后者了,而且每次重新传输需要重新配置传输长度的活也在这个函数内部完成了。
类似 HAL_UART_Transmit_DMA(...) 的函数是存放于各个外设的库中的,DMA的库中只有一些底层的函数;并且这些函数中都内置了 HAL_DMA_Start_IT(...) 函数或 HAL_DMA_Start(...) 函数,不要重复调用了。
⑥ DMA 外设相关结构体
DMA_HandleTypeDef:
c
typedef struct __DMA_HandleTypeDef
{
DMA_Channel_TypeDef *Instance /* DMA通道基地址 */
DMA_InitTypeDef Init /* DMA初始化结构体 */
... // 其他的不重要
} DMA_HandleTypeDef;
其中 DMA_InitTypeDef 也是个结构体:
c
typedef struct
{
uint32_t Direction /* DMA传输方向 */
uint32_t PeriphInc /* 外设地址(非)增量 */
uint32_t MemInc /* 存储器地址(非)增量*/
uint32_t PeriphDataAlignment /* 外设数据宽度 */
uint32_t MemDataAlignment /* 存储器数据宽度 */
uint32_t Mode /* 操作模式 */
uint32_t Priority /* DMA通道优先级 */
} DMA_InitTypeDef;
9)配置步骤(以DMA方式传输串口数据)
以DMA方式传输串口数据,以这个为例
-
使能DMA时钟
c__HAL_RCC_DMA1_CLK_ENABLE -
初始化DMA
c__HAL_LINKDMA函数连接DMA和外设!!!! HAL_DMA_Init函数初始化DMA相关参数 -
使能串口的DMA发送,启动传输
cHAL_UART_Transmit_DMA // 这个函数在 UART 的库中在这个函数中会调用
HAL_DMA_Start_IT(...)函数,开启DMA传输(本质上第三步就是开启DMA传输)
查询DMA传输状态(⭐️)
__HAL_DMA_GET_FLAG查询通道传输状态__HAL_DMA_GET_COUNTER获取当前传输剩余数据量
DMA中断使用(⭐️)
HAL_NVIC_EnableIRQ()HAL_NVIC_SetPriority()- 编写中断服务函数 xxx_IRQHandler,注意这里是 DMA 的中断(DMA1_Channel1_IRQHandler这样),不是外设的中断服务函数。
- (下面两个实验示例都没有开启中断,DMA 中断会在下一章 ADC 章节中使用到)
- (在单片机中,DMA 通常是没有专用 MSP 回调函数的,所以其中断的开启通常是在初始化函数中调用这两个函数完成的,默认开启的是传输完成中断)
10)实验示例 1(内存到内存)
实验内容:按下按键KEY0,实现内部RAM中内存到内存的数据传输。
dma.c 和 dma.h 需要自己创建
dma.c 文件代码如下:
c
#include "dma.h"
#include "exti.h"
#include "delay.h"
#include "led.h"
#include "key.h"
DMA_HandleTypeDef dma_handle; /* DMA句柄 */
/**
* @brief DMA初始化函数
* @param DMAx_CHy : DMA的通道, DMA1_Channel1 ~ DMA1_Channel7, DMA2_Channel1 ~ DMA2_Channel5
* @note 从存储器 -> 存储器模式、8位数据宽度(字节)、存储器增量模式
* @retval 无
*/
void DMA_Init(DMA_Channel_TypeDef* DMAx_CHy)
{
/* 时钟使能 */
if ((uint32_t)DMAx_CHy > (uint32_t)DMA1_Channel7) /* 大于DMA1_Channel7, 则为DMA2的通道了 */
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
}
/* DMA配置 */
dma_handle.Instance = DMAx_CHy; /* USART1_TX使用的DMA通道为: DMA1_Channel4 */
dma_handle.Init.Direction = DMA_MEMORY_TO_MEMORY; /* 存储器到存储器模式 */
dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 存储器数据宽度:字节(8位) */
dma_handle.Init.PeriphInc = DMA_PINC_ENABLE; /* 外设增量模式,这里实质是存储器增量模式 */
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; /* 外设数据宽度:字节(8位),这里实质是存储器数据宽度 */
dma_handle.Init.Mode = DMA_NORMAL; /* 单次传输模式,内存到内存流向下是一次性传输完 */
dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&dma_handle);
}
/**
* @brief DMA传输数据长度修改函数
* @param 传输数据长度 cndtr
* @note 必须要先调用DMA_Init()函数才可以使用
* @retval 无
*/
void DMA_cndtr_change(uint16_t cndtr)
{
__HAL_DMA_DISABLE(&dma_handle);
dma_handle.Instance->CNDTR = cndtr;
// DMA1_Channel1->CNDTR=cndtr; // 使用 DMA1_Channel1 的话可以代替上一行代码
__HAL_DMA_ENABLE(&dma_handle);
}
dma.h 文件代码如下:
c
#ifndef _DMA_H
#define _DMA_H
#include "sys.h"
extern DMA_HandleTypeDef dma_handle; /* DMA句柄 */
void DMA_Init(DMA_Channel_TypeDef* DMAx_CHy);
void DMA_cndtr_change(uint16_t cndtr);
#endif
main.c 文件代码如下(注意 HAL_DMA_Start() 函数的位置和使用方式):
c
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "string.h"
#include "stdio.h"
#include "uart4.h"
#include "dma.h"
#include "string.h"
// ====================================================================
uint8_t key_value = 0; // 按键扫描返回值
uint8_t src_buf[10] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}; // 源数组
uint8_t dest_buf[10] = {0}; // 目标数组
extern DMA_HandleTypeDef dma_handle; /* DMA句柄 */
// ====================================================================
int main(void)
{
HAL_Init(); // 初始化HAL库
Stm32_Clock_Init(RCC_PLL_MUL9); // 设置时钟,72M
delay_init(72); // 初始化延时函数
LED_Init(); // 初始化LED
KEY_Init(); // 按键初始化
uart_init(115200); // 串口1初始化
usart2_init(115200); // 串口2初始化
uart4_init(115200); // 串口4初始化
DMA_Init(DMA1_Channel1); // DMA初始化(内存到内存)
HAL_DMA_Start(&dma_handle, (uint32_t)src_buf, (uint32_t)dest_buf, 0); // 开启DMA传输
while(1)
{
key_value = KEY_Scan(0);
if(key_value==KEY0_PRES)
{
// 清空目标数组
memset(dest_buf,0,10);
printf("dest_buf清零,元素为:");
for(int i=0;i<10;i++)
{
printf("%d",dest_buf[i]);
}
printf("\r\n");
// 重新设置传输数据长度(每次重新传输前都要重设)
DMA_cndtr_change(10);
// 等待传输完成
while(1)
{
if (__HAL_DMA_GET_FLAG(&dma_handle, DMA_FLAG_TC1))
{
__HAL_DMA_CLEAR_FLAG(&dma_handle, DMA_FLAG_TC1);
printf("传输完成,元素为:");
for(int i=0;i<10;i++)
{
printf("%d",dest_buf[i]);
}
printf("\r\n");
break;
}
}
}
LED0=!LED0;
delay_ms(500);
}
}
实验结果:

当然,也可以不用繁琐的printf来验证,可以直接debug(选中dest_buf,然后右键添加到watch2之后,它自动添加的是dest_buf[10],双击变量名,将[10]删掉再回车,就可以下拉查看所有元素了):

11)实验示例 2(内存到外设)
实验内容:使用DMA的方式通过串口 1 发送数据
通过查询通道映射表可以看到,串口 1 的 TX 对应 DMA1 的通道 4 :

dma.c文件代码如下:
c
#include "dma.h"
#include "exti.h"
#include "delay.h"
#include "led.h"
#include "key.h"
#include "usart.h"
DMA_HandleTypeDef dma_handle; // DMA句柄
extern UART_HandleTypeDef UART1_Handler; // UART句柄
/**
* @brief 串口TX DMA初始化函数
* @note 这里的传输形式是固定的, 这点要根据不同的情况来修改
* 从存储器 -> 外设模式/8位数据宽度/存储器增量模式
* @param dmax_chy : DMA的通道, DMA1_Channel1 ~ DMA1_Channel7, DMA2_Channel1 ~ DMA2_Channel5
* @retval 无
*/
void uasrt1_tx_dma_init(DMA_Channel_TypeDef* DMAx_CHx)
{
if ((uint32_t)DMAx_CHx > (uint32_t)DMA1_Channel7) /* 大于DMA1_Channel7, 则为DMA2的通道了 */
{
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2时钟使能 */
}
else
{
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1时钟使能 */
}
__HAL_LINKDMA(&UART1_Handler, hdmatx, dma_handle); /* 将DMA与USART1联系起来(发送DMA) */
/* -------------------- TX DMA配置 -------------------- */
dma_handle.Instance = DMAx_CHx; /* USART1_TX使用的DMA通道为: DMA1_Channel4 */
dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; /* DIR = 1 , 存储器到外设模式 */
// 存储器相关配置
dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 存储器增量模式 */
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 存储器数据长度:8位 */
// 外设相关配置
dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 外设非增量模式 */
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; /* 外设数据长度:8位 */
// 传输模式和通道优先级配置
dma_handle.Init.Mode = DMA_NORMAL; /* 外设单次传输模式 */
dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 中等优先级 */
HAL_DMA_Init(&dma_handle);
}
dma.h 文件代码如下:
c
#ifndef _DMA_H
#define _DMA_H
#include "sys.h"
extern DMA_HandleTypeDef dma_handle; /* DMA句柄 */
void uasrt1_tx_dma_init(DMA_Channel_TypeDef* DMAx_CHx);
#endif
main.c 文件代码如下:
c
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "string.h"
#include "stdio.h"
#include "uart4.h"
#include "dma.h"
#include "string.h"
// ====================================================================
uint8_t key_value = 0; // 按键扫描返回值
/* DMA相关 */
const uint8_t TEXT_TO_SEND[] = {"DMA1 CH4 传输串口数据"}; // 要循环发送的字符串
#define SEND_BUF_SIZE (sizeof(TEXT_TO_SEND) + 2) * 20 // 发送数据长度, 等于"TEXT_TO_SEND长度+2"的20倍, 其中+2是\r\n.
uint8_t sendbuf[SEND_BUF_SIZE]; // 发送数据缓冲区(真正要发送的数据)
extern DMA_HandleTypeDef dma_handle; // DMA句柄
extern UART_HandleTypeDef UART1_Handler; // UART句柄
// ====================================================================
int main(void)
{
/* 局部变量 */
uint8_t key = 0;
uint16_t i, k;
uint16_t len;
uint8_t mask = 0;
float pro = 0; // 进度
/* 初始化函数 */
HAL_Init(); // 初始化HAL库
Stm32_Clock_Init(RCC_PLL_MUL9); // 设置时钟,72M
delay_init(72); // 初始化延时函数
LED_Init(); // 初始化LED
KEY_Init(); // 按键初始化
uart_init(115200); // 串口1初始化
usart2_init(115200); // 串口2初始化
uart4_init(115200); // 串口4初始化
uasrt1_tx_dma_init(DMA1_Channel4); // DMA初始化(内存到外设)
/* 将文本循环填充进sendbuf中 */
len = sizeof(TEXT_TO_SEND); // 这里不用除以sizeof(TEXT_TO_SEND[0]),因为单个元素就是一字节长度(uint8_t类型)
k = 0;
for(i = 0; i < SEND_BUF_SIZE; i++) // 填充ASCII字符集数据
{
if(k >= len) // 加入\r\n
{
if(mask)
{
sendbuf[i] = 0x0a;
k = 0;
}
else
{
sendbuf[i] = 0x0d;
mask++;
}
}
else // 复制TEXT_TO_SEND语句
{
mask = 0;
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");
HAL_UART_Transmit_DMA(&UART1_Handler, sendbuf, SEND_BUF_SIZE); // 实际应用中,传输数据期间,可以执行另外的任务
while (1)
{
if ( __HAL_DMA_GET_FLAG(&dma_handle, DMA_FLAG_TC4)) // 等待 DMA1_Channel4 传输完成
{
__HAL_DMA_CLEAR_FLAG(&dma_handle, DMA_FLAG_TC4);
HAL_UART_DMAStop(&UART1_Handler); // 传输完成以后关闭串口DMA
break;
}
// // 显示传输进度,但不能通过printf来看,可以用LCD或OLED,同时传输的数据很长才能观察到pro的变化
// pro = DMA1_Channel4->CNDTR; // 得到当前还剩余多少个数据
// len = SEND_BUF_SIZE; // 总长度
// pro = 1 - (pro / len); // 得到百分比
// pro *= 100; // 扩大100倍
}
printf("\r\n------ 传输完成 ------\r\n");
}
i++;
delay_ms(10);
if (i == 20)
{
LED0=!LED0; // LED0闪烁,提示系统正在运行
i = 0;
}
}
}
实验结果如下:

12)问题
DMA 的 HAL 库函数使用比较麻烦(内部机制比较复杂),尤其是当需要实时修改 CNDTR 的时候,很容易就卡死。
在使用 HAL 库编程的时候,尽量不要修改 CNDTR,然后尽量用纯 HAL 库模式(严格使用 HAL 库函数)。如果非要实时修改 CNDTR,那么你就要使用 HAL+寄存器 编程模式,具体看下一章 ADC 的代码。