细说STM32F407单片机以DMA方式读写外部SRAM的方法

目录

一、工程配置

1、时钟、DEBUG、GPIO、CodeGenerator

2、USART3

3、NVIC

[4、 FSMC](#4、 FSMC)

[5、DMA 2](#5、DMA 2)

(1)创建MemToMem类型DMA流

(2)开启DMA流的中断

二、软件设计

1、KEYLED

2、fsmc.h、fsmc.c、dma.h、dma.c

3、main.h

4、main.c

三、运行与调试


本文作者旨在介绍如何使用DMA方式读写外部SRAM。继续使用旺宝红龙开发板STM32F407ZGT6 KIT V1.0。

参考文章:细说STM32F407单片机以轮询方式读写外部SRAM的方法-CSDN博客 https://wenchm.blog.csdn.net/article/details/144959391

原理图,详见参考文章。

一、工程配置

外部SRAM还可以通过DMA方式访问,HAL驱动程序中提供了HAL_SRAM_Write_DMA()和HAL_SRAM_Read_DMA()两个函数用于外部SRAM的DMA方式读写数据。但是在FSMC的参数设置界面并没有DMA设置界面,外部SRAM的DMA配置方法与一般的外设不同。在FSMC组件的配置界面没有DMA设置页面,为此需要在CubeMX里单独创建一个DMA流,然后在程序里编写少量代码将创建的DMA流与Bank 1子区3对象关联。

工程仍然引用KEYLED文件夹中的文件,使用方法详见参考文章。其按键的功能定义:

cpp 复制代码
[S2]KeyUp   = Write directly.    LED1 ON
[S3]KeyDown = Write by DMA.      LED2 ON
[S4]KeyLeft = Read by DMA.       LED3 ON

1、时钟、DEBUG、GPIO、CodeGenerator

外部时钟,25MHz,设置到HCLK=168MHz,PCLK1=42MHz,PCLK2=84MHz,其它,都设置成168MHz。

DEBUG,选择serial wire,CodeGenerator的设置同参考文章1。

2、USART3

使用管脚PB10、PB11,不用USART6是因为IDE提示与FSMC有争执。

3、NVIC

开启DMA2全局中断,优先级=1,修改TIME BASE中断优先级=0。

4、 FSMC

设置与参考文章相同。

5、DMA 2

(1)创建MemToMem类型DMA流

在组件面板的System Core分组里有一个DMA组件,可以在这里管理已经为外设的DMA请求配置好的DMA流,也可以在这里直接创建DMA配置。DMA组件没有任何模式参数需要设置,界面如图所示:

访问外部SRAM的DMA传输方向是Memory To Memory(存储器到存储器),只有DMA2控制器支持这种类型的DMA传输,在Mem ToMem页面配置的DMA流会自动显示在DMA2页面。本示例创建的DMA配置,DMA请求只能选择为MEMTOMEM,选择一个流DMA2 Stream2(只能是DMA2控制器的DMA流),传输方向是Memory To Memory。

这个DMA流的属性参数设置需要注意以下事项:

  • DMA流的工作模式(Mode)只能设置为正常(Normal)模式,不能设置为循环模式。
  • DMA流会自动使用FIFO,且不能关闭。
  • 源存储器和目标存储器的数据宽度(Data Width)设置为Word,这是因为函数HAL_SRAM_Write_DMA()和HAL_SRAM_Read_DMA()只支持uint32_t类型的数据缓冲区。
  • 源存储器和目标存储器都应该开启地址自增功能。

CubeMX会为这样配置的一个DMA流生成初始化代码,也就是会定义DMA_HandleTypeDef类型的DMA流对象变量,并根据CubeMX里的设置生成赋值代码,用函数HAL_DMA_Init()进行DMA流的初始化,但是不会生成代码将DMA流对象与外设关联,也就是不会生成调用函数__HAL_LINKDMA()的代码,需要用户自己在程序中编写代码将DMA流对象与FSMC Bank 1子区3对象关联。这与前面介绍过的一些外设使用DMA的配置方法有差异。

(2)开启DMA流的中断

前面创建的DMA配置中用到DMA2 Stream2流,这个DMA流的中断并不会自动打开。不打开DMA流的中断,DMA传输完成中断事件的回调函数就不会被调用。所以,还需要在NVIC管理界面开启DMA2 Stream2的全局中断,将这个中断的抢占优先级设置为1,以防回调函数里直接或间接用到延时函数HAL Delay()。

二、软件设计

1、KEYLED

本例的项目中要使用KEYLED,其中的keyled.c和keyled.h的使用方法与参考文章相同。

2、fsmc.h、fsmc.c、dma.h、dma.c

由IDE自动生成,不需要修改。

3、main.h

cpp 复制代码
/* USER CODE BEGIN Private defines */
void SRAM_WriteDirect();
void SRAM_WriteDMA();
void SRAM_ReadDMA();
/* USER CODE END Private defines */

4、main.c

cpp 复制代码
/* USER CODE BEGIN Includes */
#include "keyled.h"
#include <stdio.h>
/* USER CODE END Includes */
cpp 复制代码
/* USER CODE BEGIN PD */
// SRAM的容量不同,该处的定义就不同,更改SRAM就得修改此处的定义
#define SRAM_ADDR_BEGIN	 0x68000000UL //Bank1子区3的SRAM起始地址
#define SRAM_ADDR_HALF	 0x6801FFFFUL //SRAM容量256K*16bit,中间地址128K字节
#define SRAM_ADDR_END	 0x6803FFFFUL //SRAM容量256K*16bit,结束地址512K字节
//#define SRAM_ADDR_HALF 0x68080000UL //SRAM容量512K*16bit,中间地址512K字节
//#define SRAM_ADDR_END	 0x680FFFFFUL //SRAM容量512K*16bit,结束地址1024K字节
/* USER CODE END PD */
cpp 复制代码
/* USER CODE BEGIN PV */
#define	 COUNT 5			//缓存区数据个数
uint32_t txBuffer[COUNT];	//DMA发送缓存区
uint32_t rxBuffer[COUNT];	//DMA接收缓存区

uint8_t	 DMA_Direction=1;	//DMA传输方向,1=write, 0=read
uint8_t	 DMA_Busy=0;		//DMA工作状态,1=busy, 0=idle

/* USER CODE END PV */
cpp 复制代码
/* USER CODE BEGIN 2 */
  __HAL_LINKDMA(&hsram3,hdma,hdma_memtomem_dma2_stream0);  //将DMA传输配置与外设hsram3关联

  printf("Demo19_2_FSMC_DMA: External SRAM\r\n");
  printf("Read/Write SRAM by DMA\r\n");

  //显示菜单
  printf("[S2]KeyUp   = Write directly.\r\n");
  printf("[S3]KeyDown = Write by DMA.\r\n");
  printf("[S4]KeyLeft = Read by DMA.\r\n\r\n");

  // MCU output low level LED light is on
  LED1_OFF();
  LED2_OFF();
  LED3_OFF();

  /* USER CODE END 2 */

在main()函数里完成外设初始化后,执行了下面一行语句:

cpp 复制代码
__HAL_LINKDMA(&hsram3,hdma,hdma_memtomem_dma2_stream0);

执行这行语句相当于执行了下面两行语句:

cpp 复制代码
(&hsram3)->hdma =&(hdma_memtomem_dma2_stream0);	//hsram3的hdma指向具体的DMA流对象
(hdma_memtomem_dma2_stream0).Parent=(&hsram3);	//DMA流对象的Parent指向具体外设hsram3

所以,其功能就是将DMA流对象hdma_memtomem_dma2_stream0与外设hsram3互相关联。就是将初始化DMA流对象的代码放到了函数MX_DMA_Init()里,没有自动生成调用__HAL_LINKDMA()的代码实现DMA流与外设的互相关联。所以在main()函数里,需要调用函数__HAL_LINKDMA()将外设hsram3与DMA流对象hdma_memtomem_dma2_stream0关联起来。

cpp 复制代码
 /* USER CODE BEGIN 3 */
	KEYS  curKey=ScanPressedKey(KEY_WAIT_ALWAYS);
	switch(curKey)
	{
		case KEY_UP:{
			SRAM_WriteDirect();
			LED1_ON();
			LED2_OFF();
			LED3_OFF();
			break;
		}

		case KEY_DOWN:{
			SRAM_WriteDMA();
			LED1_OFF();
			LED2_ON();
			LED3_OFF();
			break;
		}

		case KEY_LEFT:{
			SRAM_ReadDMA();
			LED1_OFF();
			LED2_OFF();
			LED3_ON();
			break;
		}

		default:{
			LED1_OFF();
			LED2_OFF();
			LED3_OFF();
		}
	}

	HAL_Delay(500);	//延时,消除按键抖动影��?
  }
  /* USER CODE END 3 */

文件main.c定义了几个全局变量用于DMA数据传输。

在外设初始化部分,MX_DMA_Init()用于DMA初始化,就是初始化CubeMX中定义的MemToMem类型的DMA流对象。MX_FSMC_Init()用于FSMC初始化,这个函数的代码与参考文章的示例的定义完全相同。

外设初始化完成后,要调用函数__HAL_LINKDMA()将DMA流对象hdma_memtomem_dma2_stream0与FSMC Bank 1子区3对象hsram3关联。(有时候,会用refactor方法将其更名为hdma_m2m_sram。但是重新生成式又会恢复到IDE自动生成的对象名。)

主程序里显示了一个菜单,while循环里通过检测按键对菜单做出响应,响应代码中用到3个自定义函数,USER CODE BEGIN 4数据对里介绍这几个函数的实现代码。

cpp 复制代码
/* USER CODE BEGIN 4 */
//直接写入
void SRAM_WriteDirect()
{
	//准备数组数据
	printf("Writing 32bit array directly...\r\n");
	uint32_t Value=600;
	uint32_t *pAddr_32b=(uint32_t *)(SRAM_ADDR_BEGIN+256);	//给指针赋值
	//准备数组,写数据
	for(uint8_t i=0; i<COUNT; i++)
	{
		txBuffer[i]=Value;
		HAL_SRAM_Write_32b(&hsram3, pAddr_32b, &Value, 1);
		printf("The data writed at ADD %p = %ld\r\n",pAddr_32b,Value);
		Value += 5;
		pAddr_32b ++;
	}

	if (HAL_SRAM_Write_32b(&hsram3, pAddr_32b, txBuffer, COUNT) == HAL_OK)
		printf("Array is written directly successfully.\r\n\r\n");
}

//DMA方式写入SRAM
void SRAM_WriteDMA()
{
	printf("Writing 32bit array by DMA...\r\n");
	uint32_t Value=800;
	uint32_t *pAddr_32b=(uint32_t *)(SRAM_ADDR_BEGIN+256);

	DMA_Direction=1;	//DMA传输方向,1=write, 0=read
	DMA_Busy=1;			//表示DMA正在传输状态,1=working, 0=idle
	for(uint8_t i=0; i<COUNT; i++)
	{
		txBuffer[i]=Value;
		Value += 6;
	}
	HAL_SRAM_Write_DMA(&hsram3, pAddr_32b, txBuffer, COUNT);
}

//DMA方式读取SRAM
void SRAM_ReadDMA()
{
	printf("Reading 32bit array by DMA...\r\n");
	DMA_Direction=0;	//DMA传输方向,1=write, 0=read
	DMA_Busy=1;			//表示DMA正在传输状态,1=working, 0=idle

	uint32_t *pAddr_32b=(uint32_t *)(SRAM_ADDR_BEGIN+256);
	HAL_SRAM_Read_DMA(&hsram3, pAddr_32b, rxBuffer, COUNT);	//以DMA方式读取SRAM
}

//DMA传输完成中断回调函数
void HAL_SRAM_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma)
{
	uint32_t *pAddr_32b=(uint32_t *)(SRAM_ADDR_BEGIN+256);
	if (DMA_Direction == 1)
	{	//DMA传输方向,1=write, 0=read
		for(uint8_t i=0; i<COUNT; i++){
			printf("The data writed at ADD %p = %ld\r\n",pAddr_32b,txBuffer[i]);
			pAddr_32b ++;
		}
		printf("Written by DMA complete.\r\n\r\n");
	}
	else if (DMA_Direction == 0)
	{
		for(uint8_t i=0; i<COUNT; i++){
			printf("The data read at ADD %p = %ld\r\n",pAddr_32b,rxBuffer[i]);
			pAddr_32b ++;
		}
		printf("Read by DMA complete.\r\n\r\n");
	}
	else
		return;
	DMA_Busy=0;			//表示DMA结束了传输,1=working, 0=idle
}

int __io_putchar(int ch)
{
	HAL_UART_Transmit(&huart3,(uint8_t*)&ch,1,0xFFFF);
	return ch;
}
/* USER CODE END 4 */

按下KeyUp键后,调用函数SRAM_WriteDirect(),其功能是调用函数HAL_SRAM_Write_32b()向外部SRAM写入一个数组的数据,主要是为了测试DMA方式读出的数据是否正确。hsram3关联的DMA流是MemToMem类型的,使用函数HAL_SRAM_Write_DMA()以DMA方式写入数据,或使用函数HAL_SRAM_Read_DMA()以DMA方式读取数据时,都使用这个DMA流,回调函数都是HAL_SRAM_DMA_XferCpltCallback()。所以,定义了两个全局变量表示DMA传输方向和DMA工作状态。

  • 全局变量DMA_Direction表示DMA传输方向:DMA_Direction为1时,表示数据写入;为0时,表示数据读出。
  • 全局变量DMA_Busy表示是否正在进行DMA传输:DMA_Busy为1时,表示正在进行DMA传输;为0时,表示空闲。

按下KeyDown键时,调用函数SRAM_WriteDMA(),其功能是调用HAL_SRAM_Write_DMA()以DMA方式向外部SRAM写入一个数组的数据。在开启DMA传输之前,将全局变量DMA_Direction设置为1,表示写入操作,将DMA_Busy设置为1。

按下KeyLeftt键时,调用函数SRAM_ReadDMA(),其功能是调用函数HAL_SRAM_Read_DMA(),以DMA方式从外部SRAM读取一批数据。在开启DMA传输之前,将全局变量DMA_Direction设置为0,表示读取操作,DMA_Busy设置为1。

函数HAL SRAM_Write_DMA()和HAL_SRAM_Read_DMA()启动的DMA传输完成后,会触发DMA流的传输完成事件中断,会调用相同的一个回调函数HAL_SRAM_DMA_XferCpltCallback(),所以需要在这个回调函数区分DMA传输方向。通过全局变量DMA_Direction可以判断DMA传输方向,从而做出相应的响应。回调函数处理完成后,将全局变量DMA_Busy的值设置为0,表示DMA传输完成。

在函数SRAM_WriteDMA()和SRAM_ReadDMA()中启动DMA传输之前,理论上还应该判断变量DMA_Busy的值。如果DMA_Busy为1,表示有未完成的DMA传输,需要等待DMA_Busy变为0之后再启动一次DMA传输。本示例中使用按键启动DMA传输,手动操作速度很慢,所以未做判断处理。

三、运行与调试

测试过程中仍然发现与参考文章一样的情况,等待作者解决后重新发布出来。

相关推荐
厉昱辰18 分钟前
51单片机入门基础
单片机·嵌入式硬件·51单片机
JaneZJW18 分钟前
江科大STM32入门——UART通信笔记总结
笔记·stm32·单片机·嵌入式
就叫飞六吧37 分钟前
51 单片机和 STM32 引脚命名对照表与解析
c++·stm32·单片机·嵌入式硬件·51单片机
2301_8059629344 分钟前
NRF24L01模块STM32-调试心得:报错 1E
stm32·单片机·嵌入式硬件
YunB西风英1 小时前
(STM32笔记)十二、DMA的基础知识与用法 第三部分
笔记·stm32·单片机·嵌入式硬件·dma·嵌入式
疯狂飙车的蜗牛1 小时前
工作生活的感悟
嵌入式硬件·程序人生·职场和发展·感悟
lucy153027510792 小时前
刷式直流电机驱动芯片,适用于打印机、电器、工业设备以及其他小型机器中——GC8870
人工智能·stm32·单片机·嵌入式硬件·机器人
皮皮黄-机电工程师2 小时前
单片机控制步进电机 A4988 Proteus仿真
单片机·proteus·步进电机·a4988
誓约酱2 小时前
Linux下字符设备驱动编写(RK3568)
linux·运维·服务器·c语言·c++·嵌入式硬件·物联网
一只搬砖的猹2 小时前
项目实战——使用python脚本完成指定OTA或者其他功能的自动化断电上电测试
linux·单片机·嵌入式硬件·python自动化·rtos·嵌入式软件·ota