痞子衡嵌入式:从头开始认识i.MXRT启动头FDCB里的lookupTable


  大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是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(保留了FlexSPI->AHBRXBUF3CR0[BUFSZ]的默认配置,实际上这种情况等效于将四个RX Buffer全部置0),根据手册,这种配置意味着仅启用Buffer3作为唯一的RX Buffer(当Buffer3是唯一Buffer时,其BUFSZ设置是无效的,其真实大小永远等于总Buffer大小),并且Buffer3大小为1KB。那么我们现在知道了,在Prefetch开启的情况下,SEQ_CTL工作一次就会读取1KB数据。当然Prefetch功能是可以在应用程序里被关掉的,如果Prefetch不使能,SEQ_CTL工作一次获取的数据由AHB Burst Read策略决定(关于这个痞子衡会单独写一篇文章)。

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<<kFlexSpiMiscOffset_SkipAhbBufConfig)))
        {
            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痞子衡便介绍完毕了,掌声在哪里~~~

欢迎订阅

文章会同时发布到我的 博客园主页CSDN主页知乎主页微信公众号 平台上。

微信搜索"痞子衡嵌入式"或者扫描下面二维码,就可以在手机上第一时间看了哦。

posted @ 2021-04-09 22:28  痞子衡  阅读(2119)  评论(0编辑  收藏  举报