目录
- 简介
- 外部设备地址映射
- 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-bit | 64MB |
16-bit | 128MB |
32-bit | 256MB |
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) | 启动写操作之后保持数据总线为高阻态的时间 | 1 | 255 |
存储器保持时间(HLD) | 在发送命令结束后保持地址的时钟(HCLK)周期数目,写操作时也是数据的保持时间 | 1 | 254 |
存储器等待时间(WAIT) | 发出命令的最短持续时间时钟(HCLK)周期数目 | 2 | 255 |
存储器建立时间(SET) | 发出命令之前建立地址的(HCLK)时钟周期数目 | 1 | 256 |
NAND Flash的通用空间操作时序如下图所示:
NAND Flash的简要操作步骤如下:
- 配置NAND Flash相关寄存器;
- 往通用空间写入NAND Flash读数据命令(在EXMC_NCE和EXMC_NWE有效时,EXMC_CLE(A[16])变为有效电平(高));
- 往通用空间写入读操作的起始地址(在EXMC_NCE和EXMC_NWE有效期间,EXMC_ALE(A[17])变为有效电平(高));
- 等待 NAND 就绪信号;
- 从通用空间的数据区逐字节的读出数据;
- 不写入新的命令和地址,可以自动读出 NAND 下一页数据;或转到3写入新的地址进
行下一页的读取;或转到2写入新的命令和地址。
SDRAM控制器
该控制器支持8 位,16 位,32 位数据带宽;AHB字、半字、字节访问;写使能和字节选择输出;自动进行行和bank边界管理;多个bank的乒乓访问;具有16x35位深度的写数据FIFO;具有16x31位深度的写地址FIFO;6x32位深度的可缓存的读数据FIFO;6x14位深度的可缓存读地址FIFO;可调整的读数据采样时钟;自刷新模式。
接口描述
控制时序
SDRAM的控制相对来说复杂,总体的步骤可分为下面几步:
- 初始化SDRAM寄存器,包括设置控制参数、时序参数、预充电、自刷新模式、自刷新频率。
预充电:若SDRAM控制器在存取时需要进行行切换,那么首先需要将该行地址对应块的读写放大器去使能,使其进入空闲状态,为下一行的读写操作进行准备。这个过程叫做预充电,或者行去使能。
- 发送行使能命令。该命令将行地址所在的块使能,完整的行地址由2比特的块地址EXMC_A[15:14]和13比特的行地址EXMC_A[12:0]组成。
- 发送列地址并进行读写访问。为了连续访问,控制器通常会保存之前操作的行号。若下一次的读取位置是在相同的行号或是已经使能的其他行号,那么读操作会未中断的执行。
突发读操作
突发读操作是对一个未被使能的行突发读操作,在读之前发送了行使能指令。
由上图可以看到,当发送了列地址后,数据并没有被立即读取,而是延迟了一定的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 address
sdram_init_struct.row_address_width = EXMC_SDRAM_ROW_ADDRESS_13; // 13bit of row address
sdram_init_struct.data_width = EXMC_SDRAM_DATABUS_WIDTH_16B; // 16bit of data width
sdram_init_struct.internal_bank_number = EXMC_SDRAM_4_INTER_BANK; // 4 internal bank
sdram_init_struct.cas_latency = EXMC_CAS_LATENCY_3_SDCLK; // 3 sdram clock of CAS latency
sdram_init_struct.write_protection = DISABLE;
sdram_init_struct.sdclock_config = EXMC_SDCLK_PERIODS_2_HCLK; // SDCLK = HCLK / 2 = 240MHz / 2 = 120MHz
sdram_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读写的串口输出结果