|
第二十六章 HyperRAM实验
STM32H7R7L8H6H自带620K字节的SRAM,对一般应用来说,已经足够了,不过在一些对内存要求高的场合,STM32H7R7自带的这些内存就不够用了。比如使用LTDC驱动RGB 屏、跑算法或者跑GUI等,就可能不太够用,所以STM32H7R7开发板根据不同版本板载了两种不同容量规格的HyperRAM,分别为8MB和32MB,满足大内存使用的需求。 本章,我们将使用STM32H7R7来驱动开发板板载的HyperRAM,并通过XSPI的内存映射功能,将HyperRAM作为STM32H7R7内存来使用,并测试其容量。本章分为如下几个部分: 26.1 HyperRAM简介 26.2 硬件设计 26.3 程序设计 26.4 下载验证
26.1 HyperRAM简介本章我们将通过经过STM32H7R7的XSPI接口,来驱动开发板板载的HyperRAM芯片,本节我们将重点介绍HyperRAM相关知识点,关于XSPI的相关知识点,会在后续的章节中进行介绍。 26.1.1 HyperRAM简介HyperRAM是具备HyperBus接口的高速CMOS自刷新DRAM。其存储阵列的内部结构类似于DRAM,而外在行为则与SRAM相似,这意味着HyperRAM能够再外部主机未进行读取或写入操作时,从内部实现DRAM阵列的刷新操作。STM32H7R7的XSPI接口支持HyperBus协议,因此,我们可以外挂 HyperRAM,从而大大降低外扩内存的成本。以开发板板载的32MB容量的HyperRAM为例,其型号为W958D8NBY,其特点如下: l 接口:HyperBus l 供电电压:1.7V-2.0V l 最大时钟频率:250MHz l DDR模式下可达500MB/s l 支持单端时钟或差分时钟两种模式 l 具有片选信号 l 8位数据总线 l 硬件复位信号 l 读写数据选通信号 W958D8NBY的内部结构框图如图26.1.1.1所示: 图26.1.1.1 W958D8NBY内部结构框图 接下来,我们结合图26.1.1.1,对HyperRAM的几个重要知识点进行介绍。 (1) 信号线 HyperRAM使用HyperBus协议来传输,HyperBus是一种低信号计数、双数据速率(DDR)接口,可实现高速读写吞吐量。DDR协议在DQ输入/输出信号上每个时钟周期传输两个数据字节。HyperBus上的读写事务由内部HyperRAM阵列上的16位宽、一个时钟周期的数据传输。 命令、地址和数据信息通过8个DQ[7:0]信号传输。时钟(CK)用于从接收到的DQ信号上对信息进行捕获。 HyperRAM信号线如表26.1.1.1所示: 信号线 | 类型 | 说明 | | | | | | 时钟使能,禁止时钟时,HyperRAM会进入自刷新模式 | | | 在读写事务中,命令、地址和数据信息通过这些信号传递 | | | | | | | | | 用于提供CK/CK#,CS#,RESET#,DQ[7:0]和RWD内部电路和存储阵列的输入缓冲器 | | | | | | | | | | | | |
表26.1.1.1 HyperRAM信号线 (2) 存储空间 HyperRAM的存储单元是以阵列的形式排列的,阵列大小(密度)可以从行地址和列地址的系统地址位的总数来确定,这些地址由列表中的“行地址位计数”和“列地址位计数”字段表示。 W958D8NBY 的存储结构为: 256Mb设备内的行数有32768 行,每行有64个半页,每半页有8个字, 每列有512个字(1K字节)。半页地址(Half-Page Address)也称为上列地址,Word of HP Address称为下列地址。如下图26.1.1.2和图26.1.1.3所示: 图26.1.1.2 存储空间地址映射(16位字) 图26.1.1.3 存储空间地址映射(16位字) HyperRAM设备的容量可由上图26.1.1.3中行和列的地址总线位数来确定,上图26.1.1.3中,Row Address使用地址总线为A23~A9,共15个行地址位,而Column Address使用地址总线为A8~A0,共9个列地址位,行地址位和列地址位一共为24个地址位,那么HyperRAM设备的容量则为224=16M(16位字)=32MB。 (3) 寄存器空间 寄存器空间用于外部主机获取HyperRAM设备信息和配置HyperRAM设备,例如获取W958D8NBY的设备ID或配置W958D8NBY的差分时钟模式等。如下图26.1.1.4所示: 图26.1.1.4 寄存器空间地址映射 上图中寄存器的功能如下: Identification Register 0:读取ID寄存器0 Identification Register 1:读取ID寄存器1 Configuration Register 0 Read:读取配置寄存器0 Configuration Register 0 Write:写入配置寄存器0 Configuration Register 1 Read:读取配置寄存器1 Configuration Register 1 Write:写入配置寄存器1 了解了寄存器的功能之后,那图26.1.1.4中寄存器的地址怎么看呢?图26.1.1.4中的System Address表示的就是外部主机访问HyperRAM设备时,需要访问的地址,该地址是一个32位的地址,这么一来可以由图26.1.1.4总结出各个寄存器对应的32位系统地址,如下表26.1.1.2所示: 表26.1.1.2 HyperRAM寄存器32位系统地址 可以看到,读取配置寄存器0和写入配置寄存器0的32位系统地址都是0x00000800,因此,外部主机可以通过0x00000800对配置寄存器0进行读取和写入的操作,同理,外部主机可通过0x00000801对配置寄存器1进行读取和写入操作。 但是需要特别注意的是,如果在STM32H7R7上直接使用表26.1.1.2中的32位系统地址访问HyperRAM是无法得到想要的结果的,其原因在STM32H7R7的参考手册中有说明,如下图所示: 图26.1.1.5 STM32H7R7 XSPI访问HyperRAM注意事项 如图26.1.1.5所示,某些存储器会规定每个地址对应1个16位的值,但是STM32H7R7的XSPI规定每个地址对应1个8位的值,因此,在STM32H7R7使用XSPI访问HyperRAM的寄存器时,需要将对应寄存器的系统地址在软件上做乘以2的处理。 了解了寄存器的地址及其注意事项后,下面来看看各个寄存器的作用。 读取ID寄存器0和读取ID寄存器1,这两个寄存器用于读取HyperRAM的ID,如下图26.1.1.6和图26.1.1.7所示: 图26.1.1.6 读取ID寄存器0描述 图26.1.1.7 读取ID寄存器1描述 由上图26.1.1.6和图26.1.1.7可知,HyperRAM的ID0因该为0x0E86,HyperRAM的ID1应该为0x0001。 配置寄存器0可用于配置HyperRAM的深度睡眠模式、驱动能力、初始化时序、突发长度等,在本章实验中用不到配置寄存器0,因此这里不过多介绍。 配置寄存器1可用于配置HyperRAM的时钟模式、混合睡眠模式、阵列刷新模式、刷新间隔等参数,在本章实验中主要使用该寄存器配置使能HyperRAM的差分时钟模式,其余的内容本章不过多介绍。HyperRAM的差分时钟模式由配置寄存器1的bit6进行配置,当bit6被置1时,HyperRAM使用单端时钟模式,这也是HyperRAM复位后的默认模式,当bit6被清0时,HyperRAM使用差分始终模式,差分时钟模式有着更好的抗干扰能力,但需要多使用1个引脚和外部主机的支持。 26.1.2 XSPI接口简介HyperRAM的驱动使用的是STM32H7R7的XSPI接口,对于该接口的详细介绍,请读者跳转到后续的《XSPI实验》小节中进行查阅,本小节不重复介绍。 26.2 硬件设计 本章实验功能简介:开机后,显示提示信息,然后按下 KEY0 按键,即测试外部 SDRAM容量大小并显示在 LCD 上。按下 KEY1 按键,即显示预存在外部 SDRAM 的数据。DS0 指示程序运行状态。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) KEY0 和 KEY1 按键 3) 串口 4) TFTLCD 模块 5) W958D8NBY 这些我们都已经介绍过( W958D8NBY 与 STM32H7R7 的各 IO 对应关系,请参考光盘原理图),接下来我们开始软件设计。 26.3 程序设计 26.3.1 程序解析1. HyperRAM驱动代码 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。HyperRAM驱动源码包括两个文件:hyperram.c和hyperram.h。 hyperram.h头文件只有函数声明,就不解释了。下面我们直接解析hyperram.c的程序,该函数的实现代码如下: /** * @param 无 * @retval 初始化结果 * @arg0: 初始化成功 * @arg1: 初始化失败 */ uint8_t hyperram_init(void) { RCC_PeriphCLKInitTypeDef rcc_periph_clk_init_struct = {0}; GPIO_InitTypeDef gpio_init_struct = {0}; XSPIM_CfgTypeDef xspim_cfg_struct = {0}; XSPI_HyperbusCfgTypeDef xspi_hyperbus_cfg_struct = {0};
/* 配置XSPI时钟源 */ rcc_periph_clk_init_struct.PeriphClockSelection = RCC_PERIPHCLK_XSPI2; rcc_periph_clk_init_struct.Xspi2ClockSelection = RCC_XSPI2CLKSOURCE_PLL2S; if (HAL_RCCEx_PeriphCLKConfig(&rcc_periph_clk_init_struct) != HAL_OK) { return 1; }
/* 使能时钟 */ __HAL_RCC_XSPIM_CLK_ENABLE(); __HAL_RCC_XSPI2_CLK_ENABLE(); __HAL_RCC_GPION_CLK_ENABLE();
/* 初始化XSPIM引脚 */ gpio_init_struct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11; gpio_init_struct.Mode = GPIO_MODE_AF_PP; gpio_init_struct.Pull = GPIO_NOPULL; gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio_init_struct.Alternate = GPIO_AF9_XSPIM_P2; HAL_GPIO_Init(GPION, &gpio_init_struct);
/* 初始化复位引脚 */ gpio_init_struct.Pin = GPIO_PIN_12; gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; gpio_init_struct.Pull = GPIO_PULLUP; gpio_init_struct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPION, &gpio_init_struct);
/* 初始化XSPI */ g_xspi_handle.Instance = XSPI2; g_xspi_handle.Init.FifoThresholdByte = 4; g_xspi_handle.Init.MemoryMode = HAL_XSPI_SINGLE_MEM; g_xspi_handle.Init.MemoryType = HAL_XSPI_MEMTYPE_HYPERBUS; g_xspi_handle.Init.MemorySize = HAL_XSPI_SIZE_256MB; g_xspi_handle.Init.ChipSelectHighTimeCycle = 2; g_xspi_handle.Init.FreeRunningClock = HAL_XSPI_FREERUNCLK_DISABLE; g_xspi_handle.Init.ClockMode = HAL_XSPI_CLOCK_MODE_0; g_xspi_handle.Init.WrapSize = HAL_XSPI_WRAP_32_BYTES; g_xspi_handle.Init.ClockPrescaler = 2 - 1; g_xspi_handle.Init.SampleShifting = HAL_XSPI_SAMPLE_SHIFT_NONE; g_xspi_handle.Init.DelayHoldQuarterCycle = HAL_XSPI_DHQC_DISABLE; g_xspi_handle.Init.ChipSelectBoundary = HAL_XSPI_BONDARYOF_NONE; g_xspi_handle.Init.MaxTran = 0; g_xspi_handle.Init.Refresh = 0; g_xspi_handle.Init.MemorySelect = HAL_XSPI_CSSEL_NCS1; if (HAL_XSPI_Init(&g_xspi_handle) != HAL_OK) { return 1; }
/* 配置XSPIM */ xspim_cfg_struct.nCSOverride = HAL_XSPI_CSSEL_OVR_NCS1; xspim_cfg_struct.IOPort = HAL_XSPIM_IOPORT_2; if (HAL_XSPIM_Config(&g_xspi_handle, &xspim_cfg_struct, HAL_XSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) { return 1; }
/* 配置HyperBus参数 */ xspi_hyperbus_cfg_struct.RWRecoveryTimeCycle = 7; xspi_hyperbus_cfg_struct.AccessTimeCycle = 7; xspi_hyperbus_cfg_struct.WriteZeroLatency = HAL_XSPI_LATENCY_ON_WRITE; xspi_hyperbus_cfg_struct.LatencyMode = HAL_XSPI_FIXED_LATENCY; if (HAL_XSPI_HyperbusCfg(&g_xspi_handle, &xspi_hyperbus_cfg_struct, HAL_XSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK) { return 1; }
/* 硬件复位HyperRAM */ hyperram_hardware_reset();
/* 检查HyperRAM的ID */ if (hyperram_check_identification() != 0) { return 1; }
/* 配置HyperRAM差分时钟模式 */ if (hyperram_config_differential_clock() != 0) { return 1; }
/* 配置HyperRAM内存映射 */ if (hyperram_memory_mapped() != 0) { return 1; }
return 0; } 该部分是HyperRAM的初始化函数,可以看到,整个初始化流程主要分为四个步骤。分别为:初始化XSPI接口、复位HyperRAM、校验HyperRAM的ID、配置HyperRAM的差分时钟模式、配置XSPI的内存映射模式,其中关于XSPI的初始化和使用在后续的章节中会进行讲解,本章主要介绍HyperRAM的使用。 复位HyperRAM的函数如下所示: /** * @brief 硬件复位HyperRAM * @param 无 * @retval 无 */ static void hyperram_hardware_reset(void) { HAL_GPIO_WritePin(GPION, GPIO_PIN_12, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPION, GPIO_PIN_12, GPIO_PIN_SET); HAL_Delay(1); } 该函数比较简单,就是对HyperRAM进行引脚复位。 接下来是校验HyperRAM的设备ID,其函数如下所示: /** * @brief 检查HyperRAM的ID * @param 无 * @retval 检查结果 * @arg 0: 检查成功 * @arg 1: 检查失败 */ static uint8_t hyperram_check_identification(void) { uint16_t data;
if (hyperram_read_register(HYPERRAM_IDENTIFICATION_REGISTER_0, &data) != 0) { return 1; }
if (data != HYPERRAM_IDENTIFICATION_0) { return 1; }
if (hyperram_read_register(HYPERRAM_IDENTIFICATION_REGISTER_1, &data) != 0) { return 1; }
if (data != HYPERRAM_IDENTIFICATION_1) { return 1; }
return 0; } 前面的章节中也说到,读取ID寄存器0和读取ID寄存器1可以读取HyperRAM设备的ID,该函数就是读取这两个寄存器检查HyperRAM设备的ID的。 接下来是配置HyperRAM的差分时钟模式,我们的开发板是支持HyperRAM的差分时钟模式的,如果不想使用HyperRAM的差分时钟模式,也可以跳过此步骤,使用HyperRAM默认的单端时钟模式,配置HyperRAM差分时钟模式的函数如下所示: /** * @brief 配置HyperRAM差分时钟模式 * @param 无 * @retval 配置结果 * @arg 0: 配置成功 * @arg 1: 配置失败 */ static uint8_t hyperram_config_differential_clock(void) { uint16_t data;
if (hyperram_read_register(HYPERRAM_CONFIGURATION_REGISTER_1, &data) != 0) { return 1; }
data &= ~(1UL << 6); if (hyperram_write_register(HYPERRAM_CONFIGURATION_REGISTER_1, data) != 0) { return 1; }
return 0; } 前面的章节中也说到,配置寄存器1的bit6可用于配置HyperRAM设备的时钟模式,该函数就是通过读-修改-写的方式将HyperRAM配置寄存器1的bit6清0来使能HyperRAM设备的差分时钟模式的。 最后便是配置XSPI来开启内存映射模式,在开启内存映射模式后,STM32H7R7访问HyperRAM便可像访问内存SRAM一样通过寻址的方式进行访问,我们开发板上的HyperRAM连接到STM32H7R7的XSPI2接口上,XSPI2开启内存映射后的基地址为0x70000000,因此,只要访问0x70000000地址中的数据就相当于访问HyperRAM中的数据了。 2. main.c代码在main函数之前,我们添加了hyperram_test这个函数,代码如下: /** * @brief 测试HyperRAM容量 * @param x: LCD上显示提示信息的起始X坐标 * @param y: LCD上显示提示信息的起始Y坐标 * @retval 无 */ void hyperram_test(uint16_t x, uint16_t y) { uint32_t i; uint32_t temp = 0; uint32_t sval = 0;
lcd_show_string(x, y, 180, y + 16, 16, "HyperRAM Test: KB", RED);
/* 每间隔16KB写入1字数据 */ for (i = 0; i < HYPERRAM_SIZE; i += 0x4000UL) { *(volatile uint32_t *)(HYPERRAM_BASE_ADDR + i) = temp++; }
/* 每间隔16KB读取1字数据进行校验 */ for (i = 0; i < HYPERRAM_SIZE; i += 0x4000UL) { temp = *(volatile uint32_t *)(HYPERRAM_BASE_ADDR + i);
if ((temp != 0) && (temp <= sval)) { break; } else { sval = temp; }
lcd_show_num(x + 15 * 8, y, (temp + 1) * 16, 5, 16, BLUE); printf("HyperRAM Capacity: %dKB\r\n", (temp + 1) * 16); } } 该函数用于测试外部 HyperRAM的容量大小,并显示其容量。 main函数代码如下: int main(void) { uint8_t t = 0; uint8_t key; uint32_t i;
sys_mpu_config(); /* 配置MPU */ sys_cache_enable(); /* 使能Cache */ HAL_Init(); /* 初始化HAL库 */ sys_stm32_clock_init(300, 6, 2); /* 配置时钟,600MHz */ delay_init(600); /* 初始化延时 */ usart_init(115200); /* 初始化串口 */ led_init(); /* 初始化LED */ key_init(); /* 初始化按键 */ lcd_init(); /* 初始化LCD */ hyperram_init(); /* 初始化HyperRAM */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED); lcd_show_string(30, 70, 200, 16, 16, "HyperRAM TEST", RED); lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED); lcd_show_string(30, 110, 200, 16, 16, "KEY0: Test HyperRAM", RED); lcd_show_string(30, 130, 200, 16, 16, "KEY1: Test Data", RED);
/* 准备测试数据 */ for (i = 0; i < (sizeof(buffer) / sizeof(uint32_t)); i++) { buffer = i; }
while (1) { key = key_scan(0); if (key == KEY0_PRES) { /* 测试HyperRAM容量 */ hyperram_test(30, 170); } else if (key == KEY1_PRES) { /* 读取测试数据 */ for (i = 0; i < (sizeof(buffer) / sizeof(uint32_t)); i++) { lcd_show_num(30, 190, buffer, 7, 16, BLUE); printf("buffer[%d]: %d\r\n", i, buffer); } }
if (++t == 20) { t = 0; LED0_TOGGLE(); }
delay_ms(10); } } 此段代码,我们定义了一个超大数组 buffer,我们指定该数组定义在外部 HyperRAM 起始地址(__attribute__((section(".bss.ARM.__at_0x70000000")))),该数组用来测试外部 HyperRAM 数据的读写。注意该数组的定义方法,是我们推荐的使用外部 HyperRAM 的方法。 另外, hyperram_test 函数和 main 函数,我们都加入了 printf 输出结果,对于没有 MCU屏模块的朋友来说,可以打开串口调试助手,观看实验结果,软件部分就给大家介绍到这里。 26.4 下载验证在代码编译成功之后,我们通过下载代码到STM32开发板,得到如图26.4.1所示: 图26.4.1 程序运行效果图 此时,我们按下KEY0,就可以在LCD上看到内存测试的画面,同样,按下KEY1,就可以看到LCD显示存放在数组buffer里面的测试数据,如图26.4.2所示: 图26.4.2 外部HyperRAM测试界面 对于没有MCU屏模块的朋友,我们可以用串口来检查测试结果,如图26.4.3所示: 图26.4.3 串口观看测试结果
|