目录
[4、 FSMC](#4、 FSMC)
[5、DMA 2](#5、DMA 2)
本文作者旨在介绍如何使用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传输,手动操作速度很慢,所以未做判断处理。
三、运行与调试
测试过程中仍然发现与参考文章一样的情况,等待作者解决后重新发布出来。