LV16-18-DMA-02-DMA实例

本文主要是STM32开发——DMA实例的一些相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
  • 通用
分类 网址说明
官方网站https://www.arm.com/ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.st.com/content/st_com/zh.htmlST官方网站,在这里我们可以找到STM32的相关文档
https://www.stmcu.com.cn/意法半导体ST中文官方网站,在这里我们可以找到STM32的相关中文参考文档
http://elm-chan.org/fsw/ff/00index_e.htmlFatFs文件系统官网
教程书籍《ARM Cortex-M3权威指南》ARM公司专家Joseph Yiu(姚文祥)的力作,中文翻译是NXP的宋岩
《ARM Cortex-M0权威指南》
《ARM Cortex-M3与Cortex-M4权威指南》
开发论坛http://47.111.11.73/forum.php开源电子网,正点原子的资料下载及问题讨论论坛
https://www.firebbs.cn/forum.php国内Kinetis开发板-野火/秉火(刘火良)主持的论坛,现也做STM32和i.MX RT
https://www.amobbs.com/index.php阿莫(莫进明)主持的论坛,号称国内最早最火的电子论坛,以交流Atmel AVR系列单片机起家,现已拓展到嵌入式全平台,其STM32系列帖子有70W+。
http://download.100ask.net/index.html韦东山嵌入式资料中心,有些STM32和linux的相关资料也可以来这里找。
博客参考http://www.openedv.com/开源网-原子哥个人博客
http://blog.chinaaet.com/jihceng0622博主是原Freescale现NXP的现场应用工程师
cortex-m-resources这其实并不算是一个博客,这是ARM公司专家Joseph Yiu收集整理的所有对开发者有用的官方Cortex-M资料链接(也包含极少数外部资源链接)
  • STM32
STM32STM32 HAL库开发实战指南——基于F103系列开发板野火STM32开发教程在线文档
STM32库开发实战指南——基于野火霸道开发板野火STM32开发教程在线文档
  • SD卡
SD Association提供了SD存储卡和SDIO卡系统规范
点击查看相关文件下载
STM32F103xx英文数据手册STM32F103xC/D/E系列的英文数据手册
STM32F103xx中文数据手册STM32F103xC/D/E系列的中文数据手册
STM32F10xxx英文参考手册(RM0008)STM32F10xxx系列的英文参考手册
STM32F10xxx中文参考手册(RM0008)STM32F10xxx系列的中文参考手册
Arm Cortex-M3 处理器技术参考手册-英文版Cortex-M3技术参考手册-英文版
STM32F10xxx Cortex-M3编程手册-英文版(PM0056)STM32F10xxx/20xxx/21xxx/L1xxxx系列Cortex-M3编程手册-英文版
SD卡相关资料——最新版本有关SD卡的一些资料可以从这里下载
SD卡相关资料——历史版本有关SD卡的一些历史版本资料可以从这里下载,比如后边看的SD卡2.0协议
SD 2.0 协议标准完整版这是一篇关于SD卡2.0协议的中文文档,还是比较有参考价值的,可以一看

这里的实例都是通过STM32CubeMX软件进行配置。

一、存储器到存储器

FLASH to SRAM,把内部FLASH的数据传输到内部的SRAM。在这里相当于将FLASH当做外设,将SRAM作为存储器,将数据从外设传输到存储器 。一般步骤如下:

(1)使能 DMA 时钟;

(2)配置 DMA 数据参数;

(3)使能 DMA,进行传输;

(4)等待传输完成,并对源数据和目标地址数据进行比较。

1. STM32CubeMX配置

image-20230505195655829

①、选择DMA外设;

②、点击【Add】添加一个DMA请求,注意我们只能选择已经开启的外设;

③、配置DMA模式,对于MEMTOMEM来说我们只能选择Normal模式;

④、地址增量配置,勾选表示地址递增,不勾选表示地址不变;

⑤、数据宽度,看我们要传输的数据宽度了,可以选择字节、半字、字,分别代表8位、16位、32位。

2. 变量定义

1
2
3
4
5
6
7
8
9
10
11
12
/* 定义aSRC_Const_Buffer数组作为DMA传输数据源 const关键字将aSRC_Const_Buffer数组变量定义为常量类型 */
const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80};
/* 定义DMA传输目标存储器 */
uint32_t aDST_Buffer[BUFFER_SIZE];

我们需要定义一个静态的源数据,存放在内部 FLASH,然后使用 DMA 传输把源数据拷贝到目标地址上(内部 SRAM),最后对比源数据和目标地址的数据,看看是否传输准确。

3. 代码分析

3.1 DMA初始化

我们按照上图配置完成后,会生成下边的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DMA_HandleTypeDef hdma_memtomem_dma1_channel1;

void MX_DMA_Init(void)
{

/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE();

/* Configure DMA request hdma_memtomem_dma1_channel1 on DMA1_Channel1 */
hdma_memtomem_dma1_channel1.Instance = DMA1_Channel1;
hdma_memtomem_dma1_channel1.Init.Direction = DMA_MEMORY_TO_MEMORY; // 存储器到存储器
hdma_memtomem_dma1_channel1.Init.PeriphInc = DMA_PINC_ENABLE; // 外设地址增量模式
hdma_memtomem_dma1_channel1.Init.MemInc = DMA_MINC_ENABLE; // 存储器地址增量模式
hdma_memtomem_dma1_channel1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; // 外设数据长度:32 位
hdma_memtomem_dma1_channel1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; // 存储器数据长度:32 位
hdma_memtomem_dma1_channel1.Init.Mode = DMA_NORMAL; // 外设普通模式
hdma_memtomem_dma1_channel1.Init.Priority = DMA_PRIORITY_MEDIUM; // 配置优先级
if (HAL_DMA_Init(&hdma_memtomem_dma1_channel1) != HAL_OK)
{
Error_Handler();
}

}

3.2 比较数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 判断指定长度的两个数据源是否完全相等,
* 如果完全相等返回1,只要其中一对数据不相等返回0
*/
uint8_t Buffercmp(const uint32_t* pBuffer, uint32_t* pBuffer1, uint16_t BufferLength)
{
while(BufferLength--) /* 数据长度递减 */
{

if(*pBuffer != *pBuffer1) /* 判断两个数据源是否对应相等 */
{
return 0; /* 对应数据源不相等马上退出函数,并返回0 */
}
pBuffer++; /* 递增两个数据源的地址指针 */
pBuffer1++;
}
/* 完成判断并且对应数据相等 */
return 1;
}

3.3 启动并等待DMA传输

1
2
3
4
5
6
void dma_mem2mem_start(void)
{
HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)aSRC_Const_Buffer, (uint32_t)aDST_Buffer,BUFFER_SIZE);
/* 等待DMA传输完成 */
while(__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1, DMA_FLAG_TC1) == DISABLE);
}

3.4 测试函数

1
2
3
4
5
6
7
void dma_mem2mem_test(void)
{
uint8_t TransferStatus = 0;
dma_mem2mem_start();
TransferStatus = Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE);
printf("TransferStatus=%d\r\n", TransferStatus);
}

二、 存储器到外设

SRAM to 串口,同时LED灯闪烁,演示DMA传数据不需要占用CPU。 一般步骤如下:

(1)配置 USART 通信功能;

(2)设置串口 DMA 工作参数;

(3)使能 DMA;

(4)DMA 传输同时 CPU 可以运行其他任务。

1. STM32CubeMX配置

这里只写DMA的配置,串口和LED的可以根据以前的笔记来。

image-20230505221303430

①、选择USART外设或者选择DMA外设,都可以进入DMA的配置界面;

②、点击【Add】添加一个DMA请求,注意我们只能选择已经开启的外设,我们这里选择USART1_TX,通道的话软件会主动帮我们选好;

③、配置DMA模式,对于MEMTOMEM来说我们只能选择Normal模式;

④、地址增量配置,勾选表示地址递增,不勾选表示地址不变,这里的外设地址不能勾选,因为串口的寄存器只有一个DR,根本没必要递增,软件中也直接无法勾选

⑤、数据宽度,看我们要传输的数据宽度了,可以选择字节、半字、字,分别代表8位、16位、32位,在这里我们需要选择8位,因为我们串口的数据寄存器就是8位有效的。

2. 变量定义

1
2
3
4
5
6
7
#define SENDBUFF_SIZE                     		1000//发送的数据量
uint8_t SendBuff[SENDBUFF_SIZE];
/*填充将要发送的数据*/
for(i=0;i<SENDBUFF_SIZE;i++)
{
SendBuff[i] = 'A';
}

3. 代码分析

3.1 DMA初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(uartHandle->Instance==USART1)
{
// 串口GPIO的配置,这里省略......
/* USART1 DMA Init */
/* USART1_TX Init */
hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; // 存储器到外设
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE; // 外设非增量模式
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; // 存储器增量模式
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; // 外设数据长度:8 位
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; // 存储器数据长度:8 位
hdma_usart1_tx.Init.Mode = DMA_NORMAL; //外设普通模式
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM; //中等优先级
if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
{
Error_Handler();
}

__HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx); // Associate the DMA handle

}
}

因为数据是从存储器到串口,所以设置存储器为源地址,串口的数据寄存器为目标地址,要发送的数据有很多且都先存储在存储器中,则存储器地址指针递增,串口数据寄存器只有一个,则外设地址地址不变,两边数据单位设置成一致,传输模式可选一次或者循环传输,只有一个 DMA请求,优先级随便设,最后调用 HAL_DMA_Init 函数把这些参数写到 DMA 的寄存器中,然后使能 DMA 开始传输。

为演示DMA持续运行而CPU还能处理其它事情,持续使用DMA发送数据,量非常大,长时间运行可能会导致电脑端串口调试助手会卡死,鼠标乱飞的情况,所以这里将DMA配置中的循环模式改为单次模式。

3.2 中断处理函数

这里在软件中配置的时候好像自动开启DMA中断,中断的优先级我们可以在NVIC中进行设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 初始化函数
void MX_DMA_Init(void)
{

/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE();

/* DMA interrupt init */
/* DMA1_Channel4_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);

}

// 中断回调函数
// DMA1_Channel4_IRQHandler()-->HAL_DMA_IRQHandler()-->有三个,注册实例如下
DMA_HandleTypeDef hdma_usart1_tx;
hdma_usart1_tx.XferHalfCpltCallback = ferHalfCpltCallbackFuncName;

中断回调函数是需要在初始化的时候进行注册的,没有注册的话就不会被调用啦,这里我们暂时不用,就没有注册了。

3.3 启动传输

1
2
3
4
/* USART1 向 DMA发出TX请求 */
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
// 例如
HAL_UART_Transmit_DMA(&UartHandle, (uint8_t *)SendBuff , SENDBUFF_SIZE);

HAL_UART_Transmit_DMA 函数用于启动 USART 的 DMA 传输,第一个参数为外设的初始化句柄而非dma初始化使用的句柄。。只需要指定源数据地址及长度,运行该函数后 USART 的 DMA 发送传输就开始了,根据配置它会通过 USART 循环发送数据。DMA 传输过程是不占用 CPU 资源的,可以一边传输一次运行其他任务。

3.4 测试函数

这个我们直接写到main函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define SENDBUFF_SIZE                     		60000//发送的数据量
uint8_t SendBuff[SENDBUFF_SIZE];/
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();

uint32_t i = 0;
for(i=0;i<SENDBUFF_SIZE;i++)
{
SendBuff[i] = 'A';
}

printf("This is my test!!!\r\n");
HAL_UART_Transmit_DMA(&huart1, (uint8_t *)SendBuff ,SENDBUFF_SIZE);
while (1)
{
LED1 = !LED1;
HAL_Delay(500);
}
}

4. 实验现象

这里描述一下现象,若是没有DMA的话,我们将上边的 60000 个 A 发送到串口需要占用大量的时间,此时我们需要等待传输完成才能看到LED1闪烁,但是有了DMA之后,只会在进入循环前开启DMA,然后LED1就能立刻进入闪烁状态,此时串口也可以看到有数据发送过来。

image-20230505225138835

可以看到下方有60020个数据,这是因为我还加了个printf打印了一句话,这就说明我们发往串口的数据全部被打印出来了,而且注意观察的话,会发现LED1同时在闪烁,并不受串口数据传输的影响。