目录

  • 简介
  • 外部设备地址映射
    • NOR和PSRAM的地址映射
    • NAND/PC Card地址映射
    • SDRAM地址映射
  • NOR/PSRAM控制器
    • 接口描述
    • 控制时序
      • 模式1
      • 模式2
  • NAND Flash或PC Card控制器
    • 接口描述
    • 控制时序
  • SDRAM控制器
    • 接口描述
    • 控制时序
    • 突发读操作
    • 突发写操作
    • 读写FIFO
    • 跨边界读写操作
    • 低功耗模式
      • 自刷新模式
      • 掉电模式
  • 例程
    • SDRAM读写例程
      • 初始化GPIO
      • 初始化EXMC
      • 初始化SDRAM
      • SDRAM写
      • SDRAM读

简介

外部存储器控制器EXMC,用来访问各种片外存储器,通过配置寄存器,EXMC可以把AMBA
协议转换为专用的片外存储器通信协议,包括SRAM,ROM,NOR Flash,NAND Flash,PC Card和SDRAM

外部设备地址映射

EXMC的工作原理其实就是把外部储存器的地址映射到内部的特定地址上,用户想访问外部的存储器,我们只需要访问对应的内部地址,EXMC就会自动地帮用户寻址到外部存储器的对应地址。在GD32F4中,从0x60000000-0xDFFFFFFF之间的地址空间是留给EXMC做外部存储器地址映射的,这些地址空间又会被分成4个Bank,给不同种类的外部存储器做映射,如下图所示。

每个bank为256MB,bank0给NOR Flash和PSRAM做映射,bank1和bank2给NAND Flash做映射,bank3给PC Card做映射,剩下的地址给SDRAM做映射。

NOR和PSRAM的地址映射

NOR和PSRAM的bank0区域又会被分成4等份,称为Region,每个region有64MB,每份地址空间又会用作不同外部存储器的映射,如下图所示。

唯一的区别就是,只有region0支持SQPI-PSRAM,全部的region都支持NOR Flash和PSRAM。

NAND/PC Card地址映射

Bank1和Bank2用来访问NAND Flash,Bank3用来访问PC Card。每个Bank地址映射被分为多个存储空间,如下图所示。

对于NAND Flash,通用和属性空间又可以细划分为3个区域,如下图所示。

SDRAM地址映射

SDRAM的地址映射就比较简单,4个bank剩下的内存空间被分成2份内存空间——SDRAM device0和SDRAM device1,每个device就对应一个SDRAM存储器。
虽然说每个device最大寻址256MB,但这是在SDRAM是32bit数据宽度的前提下,不同的数据位宽最大的寻址空间也不同,可以参考下面这个表。

存储器数据宽度最大储存容量
8-bit64MB
16-bit128MB
32-bit256MB

NOR/PSRAM控制器

EXMC模块的NOR/PSRAM控制器控制Bank0,它可以支持NOR Flash、PSRAM、SRAM、ROM和CRAM外部存储器。

接口描述

NOR Flash接口信号描述:

PSRAM非复用接口信号描述:

SQPI-PSRAM接口信号描述:

控制时序

NOR/PSRAM控制器提供了丰富的控制时序,以满足不同的需求,如下表所示。

拓展模式为1指的是在该时序模式下,读时序和写时序可以设置不同的参数。
DSET为数据建立时间;ASET为地址建立时间;AHLD为地址保持时间;BUSLAT为总线延迟;DLAT为数据延迟;CKDIV为同步时钟分频比
如果参数前有“W”,说明这是写时序的参数

下面讲一下比较常用的模式1和2。

模式1

模式1用于SRAM、PSRAM和CRAM。
模式1读时序:

模式1写时序:

除开数据和写使能部分,其他的操作都是一样的,发送地址、使能芯片、选择字节、使能输出。
对于读操作,写使能一直为高,在数据建立时间内EXMC会接收到存储器发送的数据。
对于写操作,在地址建立时间后,写使能应拉低,并开始向存储器写入数据;数据写入完成后拉高写使能,并在1个HCLK周期后结束本次写操作。

模式2

模式2是用于NOR Flash。
模式2读时序:

模式2写时序:

模式2的时序和上面讲到的模式1时序区别也不大,主要在于地址有效线的控制。
在模式2中,每一次读写数据都需要将地址有效线拉高,表示此时地址无效。

NAND Flash或PC Card控制器

EXMC外设支持8位、16位的NAND FLASH以及16位PC卡;对于NAND FLASH,EXMC还提供ECC计算模块。

接口描述

8 位/16 位NAND接口信号描述:

16位PC Card接口信号描述:


控制时序

该控制器提供了以下可编程参数来控制存储器:

参数功能描述最小值(单位:HCLK)最大值(单位:HCLK)
存储器数据总线高阻时间(HIZ)启动写操作之后保持数据总线为高阻态的时间1255
存储器保持时间(HLD)在发送命令结束后保持地址的时钟(HCLK)周期数目,写操作时也是数据的保持时间1254
存储器等待时间(WAIT)发出命令的最短持续时间时钟(HCLK)周期数目2255
存储器建立时间(SET)发出命令之前建立地址的(HCLK)时钟周期数目1256

NAND Flash的通用空间操作时序如下图所示:

NAND Flash的简要操作步骤如下:

  1. 配置NAND Flash相关寄存器;
  2. 往通用空间写入NAND Flash读数据命令(在EXMC_NCE和EXMC_NWE有效时,EXMC_CLE(A[16])变为有效电平(高));
  3. 往通用空间写入读操作的起始地址(在EXMC_NCE和EXMC_NWE有效期间,EXMC_ALE(A[17])变为有效电平(高));
  4. 等待 NAND 就绪信号;
  5. 从通用空间的数据区逐字节的读出数据;
  6. 不写入新的命令和地址,可以自动读出 NAND 下一页数据;或转到3写入新的地址进
    行下一页的读取;或转到2写入新的命令和地址。

SDRAM控制器

该控制器支持8 位,16 位,32 位数据带宽;AHB字、半字、字节访问;写使能和字节选择输出;自动进行行和bank边界管理;多个bank的乒乓访问;具有16×35位深度的写数据FIFO;具有16×31位深度的写地址FIFO;6×32位深度的可缓存的读数据FIFO;6×14位深度的可缓存读地址FIFO;可调整的读数据采样时钟;自刷新模式。

接口描述

控制时序

SDRAM的控制相对来说复杂,总体的步骤可分为下面几步:

  1. 初始化SDRAM寄存器,包括设置控制参数、时序参数、预充电、自刷新模式、自刷新频率。

预充电:若SDRAM控制器在存取时需要进行行切换,那么首先需要将该行地址对应块的读写放大器去使能,使其进入空闲状态,为下一行的读写操作进行准备。这个过程叫做预充电,或者行去使能。

  1. 发送行使能命令。该命令将行地址所在的块使能,完整的行地址由2比特的块地址EXMC_A[15:14]和13比特的行地址EXMC_A[12:0]组成。
  2. 发送列地址并进行读写访问。为了连续访问,控制器通常会保存之前操作的行号。若下一次的读取位置是在相同的行号或是已经使能的其他行号,那么读操作会未中断的执行。

突发读操作

突发读操作是对一个未被使能的行突发读操作,在读之前发送了行使能指令。

由上图可以看到,当发送了列地址后,数据并没有被立即读取,而是延迟了一定的HCLK周期后再进行采样。
这是因为当HCLK采样数据不准的时候,用户可以使用根据HCLK微调的内部可调时钟。当使能这个时钟的时候,读取的数据在进入AHB总线之前会先存储在异步FIFO中。2-3个HCLK延迟会被添加到读命令过程中。

RCD:行到列延迟。它代表了行使能到SDRAM读写间的最小等待时间。

突发写操作

突发写操作是对一个未被使能的行突发写操作,在写之前发送了行使能指令。

读写FIFO

在进行读写操作时会充分地利用SDRAM控制器中的FIFO;FIFO有两组,一组给读操作使用,一组给写操作使用;每组中有2个FIFO,一个储存地址,一个储存数据。数据FIFO能够最多缓存6个32位的数据字,同时地址FIFO携带6个14位的地址标签
当在AHB总线上出现一个读命令时,读写命令预处理模块将首先检查这个地址是否和某个地址
标签匹配,如果匹配,则直接从FIFO中读取数据。否则,向存储器发一个新的读命令,FIFO会
被新的数据更新。如果FIFO满了,旧的数据会被丢失。
当一个写访问或者预充电命令出现时,读FIFO缓冲区中的数据就会被清除掉,用以填充新的数
据。

跨边界读写操作

跨边界读写操作就是一次读写中会操作不同的行或bank的读写操作。
它们的时序其实就是加上了行预充电的延迟而已,如下图所示。

跨边界读操作:

跨边界写操作:

低功耗模式

对于SDRAM来说,它的性质和我们平常用的电脑内存是差不多的,是一种易失性的存储器。为了保持SDRAM中的数据,我们需要不断地刷新SDRAM;因为SDRAM的储存单元是由电容所组成的,若长时间不对电容充电是会损失数据的。
低功耗模式就是设置SDRAM的刷新策略;EXMC对SDRAM提供了2种低功耗模式——自刷新模式和掉电模式

自刷新模式

在自刷新模式下,EXMC不提供时钟信号,刷新完全由SDRAM内部提供。
下面演示了如何进入和退出自刷新模式:

当发送ARef命令后,EXMC关闭时钟输入,关闭时钟使能,命令保持在NOP操作,SDRAM进入自刷新模式;退出该模式也是通过发送ARef命令,使能时钟来实现。

掉电模式

在掉电模式中,刷新则由SDRAM控制器提供。
下面演示了如何进入和退出掉电模式:

进入掉电模式,只需要关闭时钟即可;而退出该模式除了打开时钟外,还需要发送Active命令。

例程

SDRAM读写例程

在本例程中,我使用的是立创梁山派开发板,这个开发板搭载GD32F470ZGT6主控,自带一颗华邦(Winbond)型号为W9825G6KH-6L的SDRAM芯片,储存容量高达256MB

因为SDRAM的驱动动则上百行,下面就以每一步的要点进行讲解。

初始化GPIO

GPIO的初始化就不放代码了,大家应该都会写的。GOIO的初始化包括13根地址线(EXMC_A[0-12])、16根数据线(EXMC_D[0-15])、写数据标记线(EXMC_NBL[0-1])、时钟使能信号线(EXMC_CKE0)、Bank片选线(EXMC_BA[0-1])、时钟信号线(EXMC_CLK)、列地址选通线(EXMC_NCAS)、行地址选通线(EXMC_NRAS)、读使能线(EXMC_NOE)、写使能线(EXMC_NWE)
所有的GPIO口全部设置成复用推挽上拉模式,并且全部GPIO口都要将复用口映射到GPIO_AF_12中

SDRAM涉及的GPIO接口较多,建议在数据手册中查找对应的GPIO口;在初始化RCU的时候也要注意初始化SDRAM用到的所有GPIO时钟。

初始化EXMC

exmc_sdram_parameter_struct sdram_init_struct;exmc_sdram_timing_parameter_struct sdram_timing_init_struct;/* enable EXMC clock */rcu_periph_clock_enable(RCU_EXMC);/* config SDRAM timing registers *//* LMRD: 2 clock cycles */sdram_timing_init_struct.load_mode_register_delay = 2;/* XSRD: 9 / 120MHz = 75ns */sdram_timing_init_struct.exit_selfrefresh_delay = 9;/* RASD: 5 / 120MHz = 45ns */sdram_timing_init_struct.row_address_select_delay = 5;/* ARFD: 8 / 120MHz = 74ns */sdram_timing_init_struct.auto_refresh_delay = 8;/* WRD:min = 2 Clock cycles */sdram_timing_init_struct.write_recovery_delay = 3;/* RPD:3 / 120MHz = 27ns */sdram_timing_init_struct.row_precharge_delay = 3;/* RCD:3 / 120MHz = 27ns */sdram_timing_init_struct.row_to_column_delay = 3;/* config SDRAM control registers */sdram_init_struct.sdram_device = EXMC_SDRAM_DEVICEx;sdram_init_struct.column_address_width = EXMC_SDRAM_COW_ADDRESS_9;// 9bit of column addresssdram_init_struct.row_address_width = EXMC_SDRAM_ROW_ADDRESS_13;// 13bit of row addresssdram_init_struct.data_width = EXMC_SDRAM_DATABUS_WIDTH_16B;// 16bit of data widthsdram_init_struct.internal_bank_number = EXMC_SDRAM_4_INTER_BANK;// 4 internal banksdram_init_struct.cas_latency = EXMC_CAS_LATENCY_3_SDCLK;// 3 sdram clock of CAS latencysdram_init_struct.write_protection = DISABLE;sdram_init_struct.sdclock_config = EXMC_SDCLK_PERIODS_2_HCLK;// SDCLK = HCLK / 2 = 240MHz / 2 = 120MHzsdram_init_struct.burst_read_switch = ENABLE;sdram_init_struct.pipeline_read_delay = EXMC_PIPELINE_DELAY_2_HCLK;sdram_init_struct.timing = &sdram_timing_init_struct;exmc_sdram_init(&sdram_init_struct);

这一部分别用到两个结构体——exmc_sdram_parameter_struct和exmc_sdram_timing_parameter_struct,一个是用于初始化SDRAM的控制寄存器,一个是用于初始化SDRAM的时序寄存器。

这里先设置SDRAM的时序寄存器,结构体的成员名字其实都写得很清楚是哪一部分的时序,但为了照顾部分英语不好的同学,我简单翻译一下。
上面时序初始化结构体成员从上到下为模式寄存器加载延迟、自刷新模式退出延迟、行地址选通延迟、自刷新延迟、写恢复延迟、行预充电延迟、行到列延迟
这些时序该如何填写需要参考对应SDRAM芯片数据手册的说明,但有时候不同厂商对同一种时序的命名是不同的,所以如果实在找不到的话可以参考兆易创新官方的例程,或者把数值设得大一点,因为一般来说控制得慢一点是没有问题的。

接着设置SDRAM的控制寄存器,从上到下到下的设置为SDRAM设备片选,这个取决于你想把SDRAM挂载在哪里,一般选SDRAM_DEVICE0就行了;列地址宽度,选择9位;行地址宽度,选择13位;内部bank数目,4个;CAS延迟,2个或3个SDCLK周期都行,不同的选择,SDRAM的最高工作频率会不同,这里选择3个周期,对应最高工作频率为166MHz;

下面是我使用的这颗SDRAM的CAS延迟与频率参考表:

可以看到如果CAS延迟选3个SDCLK周期,那么SDRAM能最高工作在166MHz的速度下,如果选择2个SDCLK周期就只能最高工作在133MHz速度下。

写保护,不使能;时钟频率,选择HCLK二分频,这个频率十分重要,上面时序的计算就要用到这个频率,而且这个频率不能高于SDRAM的最高工作频率突发读切换,使能;管道读延迟,2个HCLK周期;时序,填入我们设置好的时序初始化结构体。

初始化SDRAM

单片机的外设初始化完成后,我们还要通过发送命令初始化SDRAM。这里要用到exmc_sdram_command_parameter_struct这个结构体。

exmc_sdram_command_parameter_struct sdram_command_init_struct;/* configure CKE high command */sdram_command_init_struct.command = EXMC_SDRAM_CLOCK_ENABLE;sdram_command_init_struct.bank_select = EXMC_SDRAM_DEVICEx_SELECT;sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_2_SDCLK;sdram_command_init_struct.mode_register_content = 0;/* wait until the SDRAM controller is ready */timeout = SDRAM_TIMEOUT;while((exmc_flag_get(EXMC_SDRAM_DEVICEx, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {timeout--;}/* send the command */exmc_sdram_command_config(&sdram_command_init_struct);delay_1ms(10);/* configure precharge all command */sdram_command_init_struct.command = EXMC_SDRAM_PRECHARGE_ALL;sdram_command_init_struct.bank_select = EXMC_SDRAM_DEVICEx_SELECT;sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_2_SDCLK;sdram_command_init_struct.mode_register_content = 0;/* wait until the SDRAM controller is ready */timeout = SDRAM_TIMEOUT;while((exmc_flag_get(EXMC_SDRAM_DEVICEx, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {timeout--;}/* send the command */exmc_sdram_command_config(&sdram_command_init_struct);/* configure Auto-Refresh command */sdram_command_init_struct.command = EXMC_SDRAM_AUTO_REFRESH;sdram_command_init_struct.bank_select = EXMC_SDRAM_DEVICEx_SELECT;sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_9_SDCLK;sdram_command_init_struct.mode_register_content = 0;/* wait until the SDRAM controller is ready */timeout = SDRAM_TIMEOUT;while((exmc_flag_get(EXMC_SDRAM_DEVICEx, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {timeout--;}/* send the command */exmc_sdram_command_config(&sdram_command_init_struct);/* configure load mode register command *//* program mode register */uint32_t command_content = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_1 |SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL |SDRAM_MODEREG_CAS_LATENCY_3 |SDRAM_MODEREG_WRITEBURST_READBURST;sdram_command_init_struct.command = EXMC_SDRAM_LOAD_MODE_REGISTER;sdram_command_init_struct.bank_select = EXMC_SDRAM_DEVICEx_SELECT;sdram_command_init_struct.auto_refresh_number = EXMC_SDRAM_AUTO_REFLESH_2_SDCLK;sdram_command_init_struct.mode_register_content = command_content;/* wait until the SDRAM controller is ready */timeout = SDRAM_TIMEOUT;while((exmc_flag_get(EXMC_SDRAM_DEVICEx, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {timeout--;}/* send the command */exmc_sdram_command_config(&sdram_command_init_struct);/* step 8 : set the auto-refresh rate counter--------------------------------*//* 64ms, 8192-cycle refresh, 64ms/8192=7.81us *//* SDCLK_Freq = SYS_Freq/2 *//* (7.81 us * SDCLK_Freq) - 20 */exmc_sdram_refresh_count_set(761);/* wait until the SDRAM controller is ready */timeout = SDRAM_TIMEOUT;while((exmc_flag_get(EXMC_SDRAM_DEVICEx, EXMC_SDRAM_FLAG_NREADY) != RESET) && (timeout > 0)) {timeout--;}

第一步,发送SDRAM时钟使能命令;第二步,预充电所有行;第三步,使能自刷新;第四步,设置模式加载寄存器,模式加载寄存器的内容每个SDRAM芯片都不尽相同,我这颗芯片的模式加载寄存器如下图。

第1-3位设置的是突发读写的长度,可以选择1位、2位、4位、8位和整页;第4位设置的是寻址模式,可以选择顺序模式和插入模式;第5-7位设置的是CAS延迟,可以选择2个或3个SDCLK周期;第10位设置的是单独写模式,可以设置突发读写和突发读单独写。

我的设置是突发读写长度为1位,寻址模式为顺序模式,CAS延迟和之前初始化结构体一样为3个SDCLK周期,单独写模式为突发读和写。

第五步,设置自刷新计数寄存器值,GD官方的例程中给出了这个值的计算公式,但是看了半天也不知道是怎么计算的,所有如果有大神看懂了,欢迎在评论区留言。

至此,SDRAM就完全初始化好了,可以开始读写数据了。

SDRAM写

void SDRAM_WriteBuffer(uint8_t *buf, uint32_t writeaddr, uint32_t num){/* while there is data to*/for(uint32_t i = 0; i < num; ++i){/* read a byte from the memory */*(uint8_t *)(SDRAM_DEVICE0_ADDR + writeaddr) = buf[i];/* increment the address */++writeaddr;}}

因为EXMC帮我们完成了两个存储器地址之间的映射和通行工作,所以我们读写SDRAM跟操作单片机内部的内存一样简单。
向SDRAM写一串数据,只需要将SDRAM设备的基地址加上地址偏移即可得到写入的地址,解指针后为其赋值即可将数据存入SDRAM。

SDRAM设备0的基地址为0xC0000000
SDRAM设备1的基地址为0xD0000000

SDRAM读

void SDRAM_ReadBuffer(uint8_t *buf, uint32_t readaddr, uint32_t num){/* while there is data to read */for(uint32_t i = 0; i < num; ++i){/* read a byte from the memory */buf[i] = *(uint8_t *)(SDRAM_DEVICE0_ADDR + readaddr);/* increment the address */++readaddr;}}

SDRAM的读跟写差不多,也是得到目标地址,解指针后将值赋给数组或变量,即可实现SDRAM的读。

下面SDRAM读写的串口输出结果