本篇详细的记录了如何使用STM32CubeMX配置 STM32F767IGT6 的硬件FMC外设与 SDRAM 通信(W9825G6KH)。
1. 准备工作硬件准备
-
开发板
首先需要准备一个开发板,这里我准备的是STM32F767IGT6的核心板。 -
SDRAM
核心板板载一片SDRAM,型号为W9825G6KH
,大小为 32 MB。
软件准备
需要准备一份 W9825G6KH-6 的数据手册。
2. STM32 FMC外设概述2.1. 什么是FMC
FMC全称Flexible Memory Controller,灵活的内存控制器,顾名思义,其主要作用是:负责向外部扩展的存储类设备提供控制信号。
FMC内存控制器支持的存储设备有:
- Nor Flash、SRAM、PSRAM
- Nand Flash
- SDRAM
- 网卡DM9000(类存储设备)
此外,FMC外设还可以通过配置与LCD控制器连接,它提供Intel 8080并口模式和Motorola 6080并口模式,并且可以灵活的配置为指定的LCD接口类型。
2.2. FMC外设的功能框图
2.3. 外部设备的地址映射(重点)
从FMC的角度来看,外部的存储设备被分为几个固定大小的Bank,每个bank 256 MB。
整个FMC外设映射地址的划分如图:
2.3.1. Bank1
Bank1的地址空间为:0x6000 0000 - 0x6FFF FFFF
,支持外接Nor Flash、PSRAM、SRAM等设备,还可以外接DM9000等类存储设备。
整个Bank1的地址空间被划分为四个子bank,每个子bank的大小为64MB,刚好对应FMC外设的地址总线(FMC_A[0:25])有26条(2^26=64MB)。
FMC还有两条内部总线ADDR[27:26],用这两路控制片选信号,如下表:
2.3.2. Bank3
只能外接Nand Flash设备。
2.3.3. SDRAM Bank
只能外接SDRAM设备。
3. 使用STM32CubeMX生成工程选择芯片型号
打开STM32CubeMX,打开MCU选择器:
搜索并选中芯片STM32F767IGT6
:
配置时钟源
- 如果选择使用外部高速时钟(HSE),则需要在System Core中配置RCC;
- 如果使用默认内部时钟(HSI),这一步可以略过;
这里我都使用外部时钟:
配置串口
开发板板载了一个CH340换串口,连接到USART1,但是引脚不是默认引脚,需要手动修改。
接下来开始配置USART1
并修改引脚:
配置FMC外设
在配置FMC外设的时候,需要了解所用SDRAM的一些参数,如果你还对SDRAM的内部结构不熟悉,请阅读这篇博客:SDRAM学习笔记(eg. W9825G6KH)。
FMC配置
开发板上SDRAM(W9825G6KH)的原理图如下:
通过原理图可以看出:
- 数据总线位宽使用了16bit:FMC D0 - FMC D15;
- 地址总线位宽使用了13bit:FMC A0 - FMC A12;
- BANK选择信号线有两条:FMC BA0 和 FMC BA1;
- 时钟使能信号使用FMC SDCKE0,片选信号使能使用FMC SDME0,可以看出使用SDRAM区域1;
- 其它通用信号线:FMC SDNWE、FMC SDNCAS、FMC SDNRAS、FMC SDCLK;
- 数据掩码信号线使用 FMC NBL0 和 FMC NBL1,分别控制输出高8位还是低8位;
根据这些信息,在STM32CubeMX中先配置SDRAM1的基本设置:
SDRAM基本参数配置
这部分信息从 SDRAM(W9825G6KH) 的数据手册中即可看到。
① 速度等级:当CL=3时最高速度为166Mhz。因为STM32F767的HCLK=216Mhz,所以需要进行二分频,使SDRAM的时钟频率为108Mhz。
② 行地址宽度和列地址宽度:有A0-A12 总共13条行地址线,有A0-A8总共9条列地址线。
最后配置如下:
SDRAM时序参数配置
通过之前的设置,SDRAM的时钟频率为108Mhz,一个时钟周期就是9.26 ns,所以下面参数的单位都是9.26ns。
① LoadToActiveDelay:TMRD 定义加载模式寄存器的命令与激活命令或刷新命令之间的延迟,最小值为2个clk。
② ExitSelfRefreshDelay:从退出自刷新到行有效的时间延迟,最小72ns,所以参数设置为8。
③ SelfRefreshTime:自刷新周期,最小是42ns,所以设置为6。
④ RowCycleDelay(tRC):刷新命令和激活命令之间的延迟,最小值为60,所以设置为7。
⑤ WriteRecoveryTime:写命令和预充电命令之间的延迟,在CL=3的情况下,最小是2个clk。
⑥ RPDelay(tRP):预充电命令与其它命令之间的延迟,最小15ns,所以此项设置为2。
⑦ RCDDelay(tRCD):激活命令与读/写命令之间的延迟,最小15ns,所以设置为2。
配置情况如下:
配置时钟树
STM32G070RB的最高主频到216M,使HCLK = 216Mhz
即可:
生成工程设置
代码生成设置
最后设置生成独立的初始化文件:
生成代码
点击GENERATE CODE
即可生成MDK-V5工程:
4.1. 编写SDRAM初始化代码
新建SDRAM驱动文件sdram_fmc_drv.h
:
/**
*@file sdram_fmc_drv.h
*@brief 使用 FMC 操作 SDRAM
*@author mculover666
*@date 2020-08-27
*@note 此驱动测试 W9825G6KH SDRAM芯片通过
*/
#ifndef _SDRAM_FMC_DRV_H_
#define _SDRAM_FMC_DRV_H_
#include "fmc.h"
#define SDRAM_MODEREG_BURST_LENGTH_1 ((uint16_t)0x0000)
#define SDRAM_MODEREG_BURST_LENGTH_2 ((uint16_t)0x0001)
#define SDRAM_MODEREG_BURST_LENGTH_4 ((uint16_t)0x0002)
#define SDRAM_MODEREG_BURST_LENGTH_8 ((uint16_t)0x0004)
#define SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL ((uint16_t)0x0000)
#define SDRAM_MODEREG_BURST_TYPE_INTERLEAVED ((uint16_t)0x0008)
#define SDRAM_MODEREG_CAS_LATENCY_2 ((uint16_t)0x0020)
#define SDRAM_MODEREG_CAS_LATENCY_3 ((uint16_t)0x0030)
#define SDRAM_MODEREG_OPERATING_MODE_STANDARD ((uint16_t)0x0000)
#define SDRAM_MODEREG_WRITEBURST_MODE_PROGRAMMED ((uint16_t)0x0000)
#define SDRAM_MODEREG_WRITEBURST_MODE_SINGLE ((uint16_t)0x0200)
void SDRAM_Init(void);
#endif /* _SDRAM_FMC_DRV_H_ */
然后在c文件中封装一个向SDRAM发送命令的函数:
static int SDRAM_SendCommand(uint32_t CommandMode, uint32_t Bank, uint32_t RefreshNum, uint32_t RegVal)
{
uint32_t CommandTarget;
FMC_SDRAM_CommandTypeDef Command;
if (Bank == 1) {
CommandTarget = FMC_SDRAM_CMD_TARGET_BANK1;
} else if (Bank == 2) {
CommandTarget = FMC_SDRAM_CMD_TARGET_BANK2;
}
Command.CommandMode = CommandMode;
Command.CommandTarget = CommandTarget;
Command.AutoRefreshNumber = RefreshNum;
Command.ModeRegisterDefinition = RegVal;
if (HAL_SDRAM_SendCommand(&hsdram1, &Command, 0x1000) != HAL_OK) {
return -1;
}
return 0;
}
最后实现SDRAM初始化的函数:
void SDRAM_Init(void)
{
uint32_t temp;
/* 1. 时钟使能命令 */
SDRAM_SendCommand(FMC_SDRAM_CMD_CLK_ENABLE, 1, 1, 0);
/* 2. 延时,至少100us */
HAL_Delay(1);
/* 3. SDRAM全部预充电命令 */
SDRAM_SendCommand(FMC_SDRAM_CMD_PALL, 1, 1, 0);
/* 4. 自动刷新命令 */
SDRAM_SendCommand(FMC_SDRAM_CMD_AUTOREFRESH_MODE, 1, 8, 0);
/* 5. 配置SDRAM模式寄存器 */
temp = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_1 | //设置突发长度:1
SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL | //设置突发类型:连续
SDRAM_MODEREG_CAS_LATENCY_3 | //设置CL值:3
SDRAM_MODEREG_OPERATING_MODE_STANDARD | //设置操作模式:标准
SDRAM_MODEREG_WRITEBURST_MODE_SINGLE; //设置突发写模式:单点访问
SDRAM_SendCommand(FMC_SDRAM_CMD_LOAD_MODE, 1, 1, temp);
/* 6. 设置自刷新频率 */
/*
SDRAM refresh period / Number of rows)*SDRAM时钟速度 – 20
= 64000(64 ms) / 4096 *108MHz - 20
= 1667.5 取值1668
*/
HAL_SDRAM_ProgramRefreshRate(&hsdram1, 1668);
}
4.2. 编写SDRAM读写测试代码
接下来在main.c中添加SDRAM测试代码。
此测试代码来自安富莱电子。
① 引入SDRAM驱动头文件:
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
#include "sdram_fmc_drv.h"
/* USER CODE END Includes */
② 宏定义SDRAM的映射地址以及SDRAM的大小:
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#define EXT_SDRAM_ADDR ((uint32_t)0xC0000000)
#define EXT_SDRAM_SIZE (32 * 1024 * 1024)
uint32_t bsp_TestExtSDRAM(void);
/* USER CODE END 0 */
③ 编写测试函数:
/* USER CODE BEGIN 4 */
/*
*********************************************************************************************************
* 函 数 名: bsp_TestExtSDRAM
* 功能说明: 扫描测试外部SDRAM的全部单元。
* 形 参: 无
* 返 回 值: 0 表示测试通过; 大于0表示错误单元的个数。
*********************************************************************************************************
*/
uint32_t bsp_TestExtSDRAM(void)
{
uint32_t i;
uint32_t *pSRAM;
uint8_t *pBytes;
uint32_t err;
const uint8_t ByteBuf[4] = {0x55, 0xA5, 0x5A, 0xAA};
/* 写SDRAM */
pSRAM = (uint32_t *)EXT_SDRAM_ADDR;
for (i = 0; i < EXT_SDRAM_SIZE / 4; i++)
{
*pSRAM++ = i;
}
/* 读SDRAM */
err = 0;
pSRAM = (uint32_t *)EXT_SDRAM_ADDR;
for (i = 0; i < EXT_SDRAM_SIZE / 4; i++)
{
if (*pSRAM++ != i)
{
err++;
}
}
if (err > 0)
{
return (4 * err);
}
/* 对SDRAM 的数据求反并写入 */
pSRAM = (uint32_t *)EXT_SDRAM_ADDR;
for (i = 0; i < EXT_SDRAM_SIZE / 4; i++)
{
*pSRAM = ~*pSRAM;
pSRAM++;
}
/* 再次比较SDRAM的数据 */
err = 0;
pSRAM = (uint32_t *)EXT_SDRAM_ADDR;
for (i = 0; i < EXT_SDRAM_SIZE / 4; i++)
{
if (*pSRAM++ != (~i))
{
err++;
}
}
if (err > 0)
{
return (4 * err);
}
/* 测试按字节方式访问, 目的是验证 FSMC_NBL0 、 FSMC_NBL1 口线 */
pBytes = (uint8_t *)EXT_SDRAM_ADDR;
for (i = 0; i < sizeof(ByteBuf); i++)
{
*pBytes++ = ByteBuf[i];
}
/* 比较SDRAM的数据 */
err = 0;
pBytes = (uint8_t *)EXT_SDRAM_ADDR;
for (i = 0; i < sizeof(ByteBuf); i++)
{
if (*pBytes++ != ByteBuf[i])
{
err++;
}
}
if (err > 0)
{
return err;
}
return 0;
}
/* USER CODE END 4 */
④ 在main函数中调用:
/* USER CODE BEGIN 2 */
printf("STM32F767 SDRAM Test By Mculover666\r\n");
SDRAM_Init();
printf("SDRAM W9825G6KH Init success\r\n");
if (bsp_TestExtSDRAM() == 0) {
printf("SDRAM Test success\r\n");
} else {
printf("SDRAM Test fail\r\n");
}
/* USER CODE END 2 */
4.3. 实验结果
编译,下载到开发板中,在串口助手中查看实验结果:
第4节中的测试方法是使用指针访问SDRAM空间,未免过于麻烦。在实际使用中,可以直接定义一个非常大的数组,将整个数组都存储到SDRAM上,然后动态的使用SDRAM内存空间。
要注意使用这种方法定义变量时,必须在函数外把它定义成全局变量,才可以存储到指定地址上。
测试过程如下:
① 定义全局变量并指定绝对地址:
/* 绝对定位方式访问 SDRAM,这种方式必须定义成全局变量 */
uint8_t testValue __attribute__((at(EXT_SDRAM_ADDR)));
② 在main函数中赋值,然后打印:
/* 操作在SDRAM的变量 */
testValue = 0x5a;
printf("testValue is %#x\r\n", testValue);
编译,下载,查看实验结果: