大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是i.MXRT启动头FDCB里的lookupTable。
一个MCU内部通常有很多外设模块,这些外设模块是各MCU厂商做差异化产品的本质,也是各厂商核心竞争力所在(这里特指那些生产ARM Cortex-M内核MCU的厂商)。在做MCU开发时有时候并不需要了解全部的外设,因为有些外设在项目里不一定会用到,但是要想把恩智浦i.MXRT系列MCU玩起来,有一个外设是必须要有所了解的,它就是FlexSPI,这个外设负责与外部串行NOR Flash连接,实现外部NOR Flash里的应用程序指令与数据的读取,而串行NOR Flash正是i.MXRT首选的启动设备。
那么在FlexSPI外设模块里究竟是什么机制实现了Flash中应用程序指令与数据的读取功能呢?痞子衡从i.MXRT启动头FDCB里的lookupTable设定开始说起:
一、为何i.MXRT能从外部Flash XIP启动?
关于在串行NOR Flash XIP执行原理,痞子衡其实在之前一篇文章 《在串行NOR Flash XIP调试原理》 的第二小节 i.MXRT FlexSPI外设特性 介绍过,是FlexSPI这个外设实现了从串行Flash任意地址取指令的功能,这是先决条件。
有了从Flash任意地址取指的先决条件基础,在i.MXRT芯片上电后,BootROM便只需要将FlexSPI外设配置到指定工作状态(这里详见 《深入i.MXRT1050系列ROM中串行NOR Flash启动初始化流程》 一文,尤其是文中最后一节提到的第二次FlexSPI初始化,本文讨论的内容其实属于第二次初始化后的状态),FlexSPI外设配置信息完全来自于启动头FDCB(一共512bytes),FlexSPI配置完成后,BootROM再把CPU控制权交给应用程序,这就完成了启动任务。
下面的 qspiflash_config 便是i.MXRT SDK包里使用的一个典型的适用符合JEDEC SFDP标准且容量为8MB的QSPI NOR Flash的FDCB头。这个启动头将FlexSPI配置成了四线模式,100MHz时钟频率,Quad I/O Fast Read时序模式(注意这个头里lookupTable设定写法其实并不标准,没有显式地写出模式序列和停止序列,后面痞子衡会细说):
当PC开始指向FlexSPI映射空间(0x60000000 - 0x607FFFFF)去执行用户程序时,FlexSPI便在背后一直默默为CPU送上指定的指令数据,如下图绿色箭头流向所示。指令数据从外部Flash中通过IO_CTL且按照SEQ_CTL指定的时序送入RX_FIFO,再到AHB_RX_BUF,最后经过AHB_CTL送到系统AHB总线上,以被CPU无障碍获取。整个过程中最重要的自动化环节其实是黄色框内的SEQ_CTL,是这个SEQ_CTL在时刻驱动着FlexSPI发送符合Flash要求的读访问时序。
二、FlexSPI外设的SEQ_CTL是如何工作的?
经过上一节的分析,我们知道了是FlexSPI中的SEQ_CTL组件实现了核心的Flash访问时序控制,那么SEQ_CTL我们该怎么控制它?别急,这时候该LUT登场,LUT是Look Up Table的简称,它其实是FlexSPI内部的一块存储区(即FlexSPI->LUTx寄存器),它的组织结构如下,LUT由多个Sequence组成(比如i.MXRT1050上是16个),每个Sequence由最多8个instruction组成,每个instruction大小为16bits,分为opcode(序列编号) + num_pads(管脚模式) + operand(序列参数值)三部分。
每个instruction,你可以理解为一个Flash访问传输子序列(比如命令序列、地址序列、模式序列,dummy序列,读/写数据序列,停止序列等),在FlexSPI外设模块里面预先实现了很多个基础instruction,instruction中的opcode即是那些预实现的序列编号。opcode全部编号如下:
命令序列:
CMD_SDR - 0x01, CMD_DDR - 0x21
地址序列:
RADDR_SDR - 0x02, RADDR_DDR - 0x22, CADDR_SDR - 0x03, CADDR_DDR - 0x23
模式序列:
MODE1_SDR - 0x04, MODE1_DDR - 0x24, MODE2_SDR - 0x05, MODE2_DDR - 0x25
MODE4_SDR - 0x06, MODE4_DDR - 0x26, MODE8_SDR - 0x07, MODE8_DDR - 0x27
写数据序列:
WRITE_SDR - 0x08, WRITE_DDR - 0x28
读数据序列:
READ_SDR - 0x09, READ_DDR - 0x29
LEARN序列:
LEARN_SDR - 0x0A, LEARN_DDR - 0x2A
数据长度设置序列(适用FPGA):
DATSZ_SDR - 0x0B, DATSZ_DDR - 0x2B
空指令序列::
DUMMY_SDR - 0x0C, DUMMY_DDR - 0x2C, DUMMY_RWDS_SDR - 0x0D, DUMMY_RWDS_DDR - 0x2D
JMP序列:
JMP_ON_CS - 0x1F
停止序列:
STOP - 0x00
有了这些基础instruction,我们便可以自由组合它们(最多8个),得到我们想要的完整传输Sequence。比如最常见的Quad I/O Read SDR传输时序便由CMD_SDR + RADDR_SDR + MODE8_SDR + DUMMY_SDR + READ_SDR + STOP六个子序列组成,如下表所示:
- Note: 关于READ_SDR的参数值设置(即读取数据长度)需要特别说明一下,这个参数仅对IP CMD方式的访问时序有效;而对于AHB CMD方式的访问时序,这个参数值设定是无效的,实际读取数据长度是由AHB RX Buffer策略灵活决定的。
从引脚信号上来看,完整Quad I/O Read SDR传输时序如下图所示。注意有一处要特别说明,从FlexSPI外设本身而言,MODE8_SDR序列和DUMMY_SDR序列是互相独立的,但在不少Flash芯片上,MODE8_SDR所占的2个时钟周期也被算在了总Dummy时钟周期数里。
LUT中最多可以存储16个Sequence,对于XIP执行而言,只需要一个读访问时序(比如最常用的Quad I/O Read SDR传输时序)即可。如果是IAP,那么还需要添加擦除时序,写访问时序,写使能时序,读状态寄存器时序等。这些预先存放在LUT中的Sequence被用户按需触发以实现各种不同类型的Flash访问,这就是SEQ_CTL工作机制。
三、FDCB中的lookupTable是如何配置进FlexSPI->LUT的?
从FlexSPI外设模块设计上而言,LUT里16个Sequence地位是相同的,对于XIP执行,必要的读访问时序可以放在LUT中的任何一个Sequence位置,只需要在FlexSPI->FLSHxCR2寄存器(x可取A1/A2/B1/B2,具体根据Flash引脚连接来定)中的ARDSEQID位指明读访问时序在LUT中的位置(index)即可。
但是毕竟应用程序是由BootROM引导的,BootROM有自己的一套配置FlexSPI规则,它定死了CMD_LUT_SEQ_IDX_READ位置,即读访问时序必须是FlexSPI->LUT[]中第一个Sequence,因为FlexSPI->FLSHxCR2[ARDSEQID]被BootROM配置成了0。所以我们在准备FDCB时,lookupTable中第一个Sequence必须放置读访问时序。
再来看BootROM中的FlexSPI初始化函数,在外设模块基本初始化 flexspi_init() 完成后,然后 flexspi_update_lut() 被调用去更新了一次LUT就直接结束了。这次的LUT更新其实仅仅是将FDCB里的lookupTable[0] - lookupTable[3](第一条Sequence) 填到 FlexSPI->LUT[0] - FlexSPI->LUT[3]里。至于为何有时候你会看到FDCB里lookupTable中不止一条Sequence,这个痞子衡后面另有文章再聊。
status_t flexspi_nor_flash_init(uint32_t instance, flexspi_nor_config_t *config)
{
status_t status = kStatus_InvalidArgument;
status = flexspi_init(instance, (flexspi_mem_config_t *)config);
if (status != kStatus_Success)
{
break;
}
// Configure Lookup table for Read
// 将config->memConfig.lookupTable里的第一个sequence放到FlexSPI->LUT[0] - FlexSPI->LUT[3]里
flexspi_update_lut(instance, 0, config->memConfig.lookupTable, 1);
return status;
}
四、设定FDCB中lookupTable的一个实例
我们以i.MXRT官方EVK上配套的典型Flash型号IS25WP064AJBLE来实战,下图是该Flash的Fast Read Quad I/O Sequence,这个时序图中命令序列、地址序列、Dummy序列的参数值是明确的,但模式序列、读数据序列参数值并不明确,我们给它明确一下,模式序列中mode bits我们设为0x00(其实只要不是0xAx均可),即 non-continuous read mode;读数据序列中data out byte其实不可设(上面讲过AHB访问下是由RX Buffer策略自动控制的),随便写个非0值即可。
基于上面的真实Flash读数据传输时序图,我们在FDCB中lookupTable里的对应设定应如下:
#define CMD_LUT_SEQ_IDX_READ 0
#define FLEXSPI_LUT_SEQ(cmd0, pad0, op0, cmd1, pad1, op1)
(FLEXSPI_LUT_OPERAND0(op0) | FLEXSPI_LUT_NUM_PADS0(pad0) | FLEXSPI_LUT_OPCODE0(cmd0) |
FLEXSPI_LUT_OPERAND1(op1) | FLEXSPI_LUT_NUM_PADS1(pad1) | FLEXSPI_LUT_OPCODE1(cmd1))
#define FLEXSPI_1PAD 0
#define FLEXSPI_2PAD 1
#define FLEXSPI_4PAD 2
#define FLEXSPI_8PAD 3
const flexspi_nor_config_t qspiflash_config = {
.memConfig =
{
.lookupTable =
{
// Quad I/O Fast Read LUTs
// 第1个instruction是CMD_SDR,参数值为0xEB,即Quad I/O Fast Read命令
// 第2个instruction是RADDR_SDR,参数值为0x18,即24bits地址(三字节)
[4*CMD_LUT_SEQ_IDX_READ + 0] = FLEXSPI_LUT_SEQ(CMD_SDR, FLEXSPI_1PAD, 0xEB, RADDR_SDR, FLEXSPI_4PAD, 0x18),
// 第3个instruction是MODE8_SDR,参数值为0x00。注意对于IS25WP064AJBLE它同时也算2个Dummy时钟周期!!!
// 第4个instruction是DUMMY_SDR,参数值为0x04,加上上面一共6个时钟周期
[4*CMD_LUT_SEQ_IDX_READ + 1] = FLEXSPI_LUT_SEQ(MODE8_SDR, FLEXSPI_4PAD, 0x00, DUMMY_SDR, FLEXSPI_4PAD, 0x04),
// 第5个instruction是READ_SDR,参数值为0x04,设定并不生效,随便写个非0值都行
// 第6个instruction是STOP
[4*CMD_LUT_SEQ_IDX_READ + 2] = FLEXSPI_LUT_SEQ(READ_SDR, FLEXSPI_4PAD, 0x04, STOP, FLEXSPI_1PAD, 0x00),
[4*CMD_LUT_SEQ_IDX_READ + 3] = 0,
},
},
};
五、对FlexSPI映射区域进行AHB读访问一定会启动SEQ_CTL工作吗?
当我们放好了正确的FDCB,BootROM正常配置完FlexSPI,并启动了应用程序后,CPU便开始按部就班从FlexSPI映射区域直接AHB访问去获取应用程序指令,是不是每一次的CPU访问都会让SEQ_CTL组件按LUT里的设定发送一次读访问时序呢?其实并不是!
我们知道i.MXRT系列会有L1 Cache,如果Flash某地址里的指令内容缓存在L1 Cache里,那么当前CPU访问该Flash地址处的指令并不需要从Flash里重新再获取一次,CPU直接从cache里便可以得到指令,此时SEQ_CTL不会工作。
即便L1 Cache里没有缓存到CPU所要指令,如果FlexSPI本身的Cacheable和Prefetch功能打开的话,AHB RX/TX Buffer里可能也会缓存CPU所要指令。如果所需指令确实缓存在AHB Buffer里,SEQ_CTL仍然不会工作。
仅当CPU所要指令是全新的,完全没有缓存,SEQ_CTL才会真正开始工作,按LUT设定去发送读数据访问时序给Flash。
六、AHB读访问下SEQ_CTL工作一次到底获取多长的数据?
前面讲了,我们在lookupTable里无法有效设置读数据序列中data out byte,因为AHB访问下的一次读取的长度是由RX Buffer策略控制的。在i.MXRT1050中AHB RX Buffer总大小为1KB,分为四个:AHB RX Buffer0 - AHB RX Buffer3,每个Buffer的大小都是可配的。具体配置在如下FlexSPI->AHBRXBUFxCR0寄存器里:
BootROM使用了如下 flexspi_config_ahb_buffers() 函数配置了AHB Buffer,即开启了FlexSPI的Prefetch功能,并且将四个FlexSPI->AHBRXBUFxCR0[BUFSZ]全部设为了0,根据手册,这种配置意味着仅启用Buffer3作为唯一的RX Buffer,并且Buffer3大小为1KB。那么我们现在知道了,在Prefetch开启的情况下,SEQ_CTL工作一次就会读取1KB数据。当然Prefetch功能是可以在应用程序里被关掉的,如果Prefetch不使能,SEQ_CTL工作一次仅获取最小数据单元(8bytes)。
status_t flexspi_config_ahb_buffers(FLEXSPI_Type *base, flexspi_mem_config_t *config)
{
uint32_t temp;
uint32_t index;
status_t status = kStatus_InvalidArgument;
do
{
if ((base == NULL) || (config == NULL))
{
break;
}
if (config->deviceType == kFlexSpiDeviceType_SerialNOR)
{
// Configure AHBCR
temp = base->AHBCR & (~FLEXSPI_AHBCR_APAREN_MASK);
// Remove alignment limitation when Flash device works under DDR mode.
temp |= FLEXSPI_AHBCR_READADDROPT_MASK;
#if FLEXSPI_FEATURE_HAS_PARALLEL_MODE
if (flexspi_is_parallel_mode(config))
{
temp |= FLEXSPI_AHBCR_APAREN_MASK;
}
#endif // FLEXSPI_FEATURE_HAS_PARALLEL_MODE
base->AHBCR = temp;
}
// Enable prefetch feature
base->AHBCR |= FLEXSPI_AHBCR_PREFETCHEN_MASK;
// Skip AHB buffer configuration if corresponding bit is set
if ((config->controllerMiscOption & (1< {
status = kStatus_Success;
break;
}
// Configure AHB RX buffer
for (index = 0; index < FLEXSPI_AHBRXBUFCR0_COUNT - 1; index++)
{
base->AHBRXBUFCR0[index] &=
~(FLEXSPI_AHBRXBUFCR0_BUFSZ_MASK | FLEXSPI_AHBRXBUFCR0_MSTRID_MASK | FLEXSPI_AHBRXBUFCR0_PRIORITY_MASK);
}
status = kStatus_Success;
} while (0);
return status;
}
至此,i.MXRT启动头FDCB里的lookupTable痞子衡便介绍完毕了,掌声在哪里~~~