大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家分享的是深入 i.MXRT1050 系列 ROM 中串行 NOR Flash 启动初始化流程。
从外部串行 NOR Flash 启动问题是 i.MXRT 系列开发最高频的话题,无论是开发调试 XIP 应用程序阶段还是最终产品量产阶段都绕不开 NOR Flash 选型以及为它设计一个匹配的 FDCB 配置块。如果不了解 FDCB 是什么,先去看痞子衡之前的文章 《Bootable image 格式与加载》。
实际开发过程中,影响串行 NOR Flash 正常下载 / 启动的因素有很多,痞子衡已经写过三篇:《16MB 以上使用不当因素》、《SFDP 因素》、《QE bit 因素》,列举了三个不同因素,当然这都是出了问题,具体调试分析才定位出来的,显然还有很多未知因素等待陆续被发掘。
如果总是被动去解决问题,那问题是解不完的。不如我们主动出击,摸清 i.MXRT 启动串行 NOR Flash 设备到底是怎样的初始化流程,搞清这个流程,将来定位启动问题才能游刃有余,话不多说,开始今天的主题。
- 备注:本文主角是 i.MXRT1050,但内容也同样适用 i.MXRT1020/1015,对于 i.MXRT1010 也算适用但有两处微小差别(冗余 App 启动支持,Flash 上电等待时间处理)。
一、整体初始化流程
我们知道外部串行 NOR Flash 是接到 i.MXRT 的 FlexSPI 外设引脚上,有时串行 NOR Flash 启动也叫 FlexSPI NOR 启动。关于 FlexSPI NOR 启动流程,i.MXRT1050 参考手册 System Boot 章节有如下流图,蓝框之外的流程属于常规 i.MXRT 启动 XIP App 流程,是个通用流程。蓝框之内才是具体 FlexSPI 初始化步骤,这个步骤概括得比较精炼。
为了让大家对 FlexSPI NOR 设备启动初始化流程有个更具体的概念,痞子衡重新画了一张更详细的流程图,图中灰底框里描述得是 FlexSPI 初始化流程,痞子衡将其分解成了六步,我们有必要深入这六步初始化流程。
二、分解初始化流程
2.1 复位 Flash 芯片(可选)
第一步是尝试复位 Flash 芯片,这步是可选的,在 fuse_0x6e0[7]里配置,默认是不使能的。复位 Flash 目的是为了让 Flash 处于一个确定的初始状态,方便 i.MXRT BootROM 去配置访问。为什么要强调 Flash 的初始状态,因为很多时候 i.MXRT 未必是冷启动(上电启动),也有可能是软复位启动(比如调用 NVIC_SystemReset),这时候外部 Flash 已经被软复位前执行过的 BootROM 甚至用户 App 配置过,因此 Flash 的状态可能不是上电初始状态(一般来说板级设计里 Flash 的 RESET#引脚要么悬空,要么连接 i.MXRT 的 POR#引脚),这可能会影响软复位后 BootROM 去再次配置启动这块不定态的 Flash。
fuse 0x6e0[7] - FLEXSPI_RESET_PIN_EN
正常的 Flash 都提供了 RESET#引脚来实现跟上电复位一样的功能,对于普通 8-pin 的 QSPI Flash,这个 RESET#引脚往往是跟信号线 IO3 复用的(仅在 QE bit 没使能情况下有效),而对于 16-pin 的 QSPI Flash 或者 HyperFlash,其 RESET#引脚都是独立的。
BootROM 就是借助了 Flash 的 RESET#引脚来实现的复位操作,实现代码比较简单,i.MXRT1050 BootROM 直接指定了 GPIO1[9]当做复位信号线,板级设计里需要你将 GPIO1[9]连到 Flash 的 RESET#引脚,然后 BootROM 就是简单地拉低 GPIO1[9]即可。RESET#信号都是低电平有效,BootROM 直接拉低这个信号持续 250us,这个低电平持续时间对于复位来说是够够的,很多 Flash 数据手册里其实仅要求几 us 即可。
- 备注:对于 BootROM 的 Flash 复位功能来说,主要适用有独立 RESET#引脚的 Flash。
#define RESET_PAD_IDX kIOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B0_09
#define RESET_PIN_MUX IOMUXC_SW_MUX_CTL_PAD_MUX_MODE(5)
#define RESET_PIN_GPIO GPIO1
#define RESET_PIN_INDEX 9
if ((OCOTP->MISC_CONF1 & 0x80) >> 7)
{
// Set pinmux as GPIO
IOMUXC->SW_MUX_CTL_PAD[RESET_PAD_IDX] = RESET_PIN_MUX;
// Set GPIO to output mode
RESET_PIN_GPIO->GDIR |= (1U<
// High
RESET_PIN_GPIO->DR_SET = (1U< sw_delay_us(250);
// Low
RESET_PIN_GPIO->DR_CLR = (1U< sw_delay_us(250);
// High
RESET_PIN_GPIO->DR_SET = (1U< sw_delay_us(500);
}
2.2 准备初始 FDCB 配置块
第二步是准备一个初始的 FDCB 配置块(即 flexspi_nor_config_t,大小为 512 字节),这个初始 FDCB 配置块将被用来做 FlexSPI 外设的第一次初始化,目的是为了能够保证 FlexSPI 初始化之后 CPU 能够使用 AHB 方式正常读取 Flash(访问性能不要求最高,但求稳定访问)。这个初始 FDCB 并不是一个完全定死的配置块,部分值也是根据 fuse 来配置的,一共有三处 fuse 位置,其中最重要的是 FLASH_TYPE:
fuse 0x440[20] - QSPI_2ND_BOOTPIN_ENABLE,决定是否启动第二组 FlexSPI pinmux
fuse 0x450[10:8] - FLASH_TYPE,决定当前连接的 Flash 类型
fuse 0x470[30:24] - DELAY_CELL_NUM,设置 Flash 读访问时序数据线有效时间
初始 FDCB 配置块中仅给 memConfig 设了值,这个 memConfig 才是用于配置 FlexSPI 外设本身。如下部分赋值是固定的 FDCB 设置,不受 fuse 影响,从这个固定配置你可以看到,BootROM 假定了所有外接 Flash 都是 128MB,且访问时钟(SCK)速度能支持 30MHz,不要对这个假定感到焦虑,它只是用于 FlexSPI 第一次初始化,目的只求能正常访问 Flash 前 4KB 即可:
flexspi_nor_config_t config;
memset(config, 0, sizeof(config));
// 公共的 FDCB 配置
config.memConfig.tag = FLEXSPI_CFG_BLK_TAG;
config.memConfig.version = FLEXSPI_CFG_BLK_VERSION;
config.memConfig.deviceType = kFlexSpiDeviceType_SerialNOR;
config.memConfig.sflashA1Size = 128UL*1024*1024;
config.memConfig.serialClkFreq = kFlexSpiSerialClk_30MHz;
config.memConfig.dataHoldTime = 3;
config.memConfig.dataSetupTime = 3;
config.memConfig.timeoutInMs = 1000;
然后便是从 fuse 里获取 flashType,根据具体 flashType 来对初始 FDCB 配置块做进一步动态赋值,这进一步赋值才用于区分不同 Flash 种类(Pad 数量、DQS 信号属性、最重要的 lookupTable 等)。
// 从 fuse 里获取 flash 类型
uint32_t flashType;
if ((OCOTP->CFG3 & 0x100000) >> 20)
{
flashType = 7;
}
else
{
flashType = (OCOTP->CFG4 & 0x700) >> 8;
}
上图中最重要的 FDCB 赋值是 config.memConfig.lookupTable,它是 FlexSPI 外设需要的核心配置,有了这个配置,CPU 便可以直接从 AHB 总线读取 Flash 的内容,因为 FlexSPI 会自动解析 AHB 总线读请求然后翻译成具体 FlexSPI 读时序,底层读时序需要的命令、地址字节数、DUMMY 周期都在 lookupTable 里。BootROM 预存了如下 6 大类 Flash 的 lookupTable:
// Dedicated 3Byte Address Read(0x03), 24bit address
static const uint32_t s_dedicated3bRead[4] = {
FLEXSPI_LUT_SEQ(CMD_SDR, FLEXSPI_1PAD, 0x03, RADDR_SDR, FLEXSPI_1PAD, 0x18),
FLEXSPI_LUT_SEQ(READ_SDR, FLEXSPI_1PAD, 0x04, STOP, FLEXSPI_1PAD, 0),
0,
0
};
// Dedicated 4Byte Address Read(0x13), 32 bit address
static const uint32_t s_dedicated4bRead[4] = {
FLEXSPI_LUT_SEQ(CMD_SDR, FLEXSPI_1PAD, 0x13, RADDR_SDR, FLEXSPI_1PAD, 0x20),
FLEXSPI_LUT_SEQ(READ_SDR, FLEXSPI_1PAD, 0x04, STOP, FLEXSPI_1PAD, 0),
0,
0
};
// HyperFlash Read
static const uint32_t s_hyperflashRead[4] = {
FLEXSPI_LUT_SEQ(CMD_DDR, FLEXSPI_8PAD, 0xA0, RADDR_DDR, FLEXSPI_8PAD, 0x18),
FLEXSPI_LUT_SEQ(CADDR_DDR, FLEXSPI_8PAD, 0x10, DUMMY_RWDS_DDR, FLEXSPI_8PAD, 0x0c),
FLEXSPI_LUT_SEQ(READ_DDR, FLEXSPI_8PAD, 0x04, STOP, FLEXSPI_8PAD, 0),
0
};
// MXIC Octal DDR read
static const uint32_t s_mxicOctDdrRead[4] = {
FLEXSPI_LUT_SEQ(CMD_DDR, FLEXSPI_8PAD, 0xEE, CMD_DDR, FLEXSPI_8PAD, 0x11),
FLEXSPI_LUT_SEQ(RADDR_DDR, FLEXSPI_8PAD, 0x20, DUMMY_DDR, FLEXSPI_8PAD, 0xc),
FLEXSPI_LUT_SEQ(READ_DDR, FLEXSPI_8PAD, 0x04, STOP, FLEXSPI_8PAD, 0),
0
};
// Micron Octal DDR read
static const uint32_t s_micronOctDdrRead[4] = {
FLEXSPI_LUT_SEQ(CMD_SDR, FLEXSPI_8PAD, 0xFD, RADDR_DDR, FLEXSPI_8PAD, 0x20),
FLEXSPI_LUT_SEQ(DUMMY_DDR, FLEXSPI_8PAD, 0x8, READ_DDR, FLEXSPI_8PAD, 0x04),
0,
0
};
// Adesto Octal DDR read
static const uint32_t s_adestoOctDdrRead[4] = {
FLEXSPI_LUT_SEQ(CMD_SDR, FLEXSPI_8PAD, 0x0B, RADDR_DDR, FLEXSPI_8PAD, 0x20),
FLEXSPI_LUT_SEQ(DUMMY_DDR, FLEXSPI_8PAD, 0x8, READ_DDR, FLEXSPI_8PAD, 0x04),
0,
0
};
2.3 第一次 FlexSPI 初始化
第三步就是利用上述配置完成的初始 FDCB 块对 FlexSPI 外设进行第一次初始化,就是下面代码,这个流程跟官方 SDK 里的 flexspi_nor_flash_init()大同小异,这里不予具体展开。如果在这里初始化就返回失败(这里一般不会失败,因为仅仅是 FlexSPI 外设自身初始化,并不涉及操作外部 Flash 芯片的动作),BootROM 则直接退出 FlexSPI NOR 设备启动,转入 SDP 下载。
#define FLEXSPI_INSTANCE 0
uint32_t instance = FLEXSPI_INSTANCE;
status_t status = flexspi_init(instance, (flexspi_mem_config_t *)(&config));
if (status != kStatus_Success)
{
return status;
}
flexspi_update_lut(instance, 0, &config.memConfig.lookupTable, 1);
2.4 若干善后工作
上述第一次 FlexSPI 初始化一般都会成功的,但这并不代表 fuse 里的 flashType 等配置跟板子上 Flash 型号是匹配的,也就是说初始 FDCB 配置块此时还没有被充分验证其是否适用板载 Flash 型号。
FlexSPI 第一次初始化结束后,为了保证后续能正常 AHB 访问,BootROM 里做了一些善后工作,主要是两件事:
- 做一些访问前的延时:根绝 fuse 0x450[3:2] - HOLD TIME 来调用 microseconds_delay()做延时,以使 FlexSPI 外设完全准备好。做一次无效 AHB 访问:类似这样的代码 volatile uint32_t dummy = *(uint32_t *)0x60000000;,无效 AHB 读可以使 Flash 退出 continuous read 模式
2.5 获取用户 FDCB 配置块
善后工作结束之后,此时 CPU 应该可以通过 AHB 正常访问 Flash 了,这个阶段我们只需要从 Flash 的偏移 0 地址处读取用户 FDCB,验证用户 FDCB 是否存在,这里才是对前面初始 FDCB 配置块以及第一次 FlexSPI 外设初始化的真正考验。
验证用户 FDCB 是否存在就是简单读取 FDCB 的前四个字节(tag),验证这个 tag 是否合法。如果第一次验证 tag 不成功(有可能是 FlexSPI 配置不正确,也有可能是用户 FDCB 不存在),会尝试做一次三字节地址切换到四字节地址的 LUT 更新(仅适用 QSPI Flash),然后做第二次 tag 读取验证,如果此时还是验证失败(大概率是不存在用户 FDCB 了),BootROM 则直接退出 FlexSPI NOR 设备启动,转入 SDP 下载。
#define FlexSPI_AMBA_BASE (0x60000000U)
#define FLASH_BASE FlexSPI_AMBA_BASE
// 使用三字节地址的 LUT 对 Flash 进行初次 AHB 访问
flexspi_clear_cache(FLEXSPI_INSTANCE);
flexspi_nor_config_t *pConfig = (flexspi_nor_config_t *)FLASH_BASE;
if (pConfig->memConfig.tag != FLEXSPI_CFG_BLK_TAG)
{
// 因为拿不到用户 FDCB 的 tag,尝试切换使用四字节地址的 LUT
if (flashType == 0)
{
flexspi_update_lut(FLEXSPI_INSTANCE, 0, s_basic4bRead, 1);
}
flexspi_clear_cache(FLEXSPI_INSTANCE);
pConfig = (flexspi_nor_config_t *)FLASH_BASE;
}
// 对 Flash 进行第二次 AHB 访问,再次确认能否拿到用户 FDCB 的 tag
if (pConfig->memConfig.tag != FLEXSPI_CFG_BLK_TAG)
{
return kStatus_Fail;
}
上面代码里有 flexspi_clear_cache()操作,这个其实就是利用 FLEXSPI0->MCR0[SWRESET]做一个外设级别的软复位,另外代码里还涉及到一个四字节地址 QSPI Flash 的 LUT 表,即如下所示:
// Basic read with 32bit address
static const uint32_t s_basic4bRead[4] = {
FLEXSPI_LUT_SEQ(CMD_SDR, FLEXSPI_1PAD, 0x03, RADDR_SDR, FLEXSPI_1PAD, 0x20),
FLEXSPI_LUT_SEQ(READ_SDR, FLEXSPI_1PAD, 0x04, STOP, FLEXSPI_1PAD, 0),
0,
0
};
2.6 第二次 FlexSPI 初始化
到了这里,基本代表第一次 FlexSPI 初始化是正确且可用的,并且能够拿到有效的用户 FDCB 配置块。这时候就是利用用户 FDCB 配置块对 FlexSPI 外设做第二次初始化,初始化代码流程跟第一次初始化是一模一样的。
这个第二次初始化是非常有必要的,因为它反映了用户的真实需求,用户 FDCB 配置块里会准确描述板载 Flash 的全面特性(访问速度,真实存储空间大小,特殊定制 LUT 等等),这些信息必须由用户来提供。
需要注意的是,第二次 FlexSPI 初始化返回成功并不代表用户 FDCB 配置块一定就是正确的,还是那句话,这仅仅是对 FlexSPI 外设自身的初始化。后续常规 App 解析流程里才是对这个用户 FDCB 配置块的真正考验。
至此,深入 i.MXRT1050 系列 ROM 中串行 NOR Flash 启动初始化流程痞子衡便介绍完毕了,掌声在哪里~~~