大家好,我是痞子衡,是正经搞技术的痞子。今天痞子衡给大家介绍的是 i.MXRT 系列 ROM 中的 FlexSPI 驱动 API 实现 IAP。
痞子衡的技术交流群里经常有群友提问: i.MXRT 中的 FlexSPI 驱动 API 到底怎么用啊?这个问题已经出现过好几次了,本来痞子衡不打算专门为这个写文章的,因为这部分内容在芯片手册 System Boot 章节里的最后一节 ROM APIs 里其实介绍得非常详细了,但是既然还是有不少朋友在问这个,看起来手册里的内容藏得有点深,这么好的东西被埋没太可惜了,那么今天痞子衡就跟大家再认真聊一聊。
一、ROM API 简介
1.1、API 产生背景
i.MXRT 系列都是 Flashless(没有内置 NVM)的芯片,所以 BootROM 必不可少。BootROM 是个很特殊的东西,本质上它是一个完整的 C 代码写成的系统级 App,这个系统级 App 专门用于从外部存储器中加载用户级 App 执行。简单地说,BootROM 就是 PC 机里的 BIOS。
BootROM 代码是存放在专门的 ROM 区域的(前面讲 i.MXRT 系列没有内置 NVM,其实不够准确,其实是有内部 ROM 空间的,只不过这个 ROM 区域用户无法下载程序使用,因此等效于没有 NVM),ROM 顾名思义 Readonly,所以 BootROM 代码只能随着芯片一起 Tapeout,代码无法更改(其实也有 ROM patch 机制,以后再介绍)。
ROM 空间其实挺大的,从 64KB 到 512KB 不等,因芯片启动功能复杂程度而异。下图是 i.MXRT1050 系列的 BootROM 所占空间,ROM 起始地址是 0x200000(起始地址在 i.MXRT 上都一样),ROM 大小为 96KB(这是标准启动功能所要的代码长度。在 i.MXRT1010 上是 64KB - 精简启动功能,在 i.MXRT1170 上是 256KB - 复杂启动功能)。
BootROM 代码其实并没有占满全部 ROM 空间,总有些剩余空间(因为工艺原因,ROM 空间都是 8/16KB 倍数),这部分空间浪费了着实可惜。如果我们能把 SDK 里的一些常用模块驱动(比如 WDOG)顺便放进去供用户调用,既充分利用 ROM 空间,也为用户节省 Flash 空间,岂不是一举两得。此外,BootROM 功能代码中也有一些现成模块驱动(比如各种启动设备存储器驱动接口)可以一并导出,这便是 API 由来。
1.2、API 设计实现
有了 API 想法,现在就是设计实现了。其实 i.MXRT ROM API 设计并不是重头开始的,在这个 MCU 系列被主推之前,Kinetis 系列也曾当红过,Kinetis 中也内置了 ROM,并且提供了 ROM API,痞子衡之前为此写过一篇文章 《飞思卡尔 Kinetis 系列 MCU 启动那些事(11)- KBOOT 特性(ROM API)》。i.MXRT ROM API 设计思路完全复用了 Kinetis ROM API 的设计。
API 说到底就是一个个功能函数的结合,我们知道工程代码都是由链接器自动分配的,因此每个函数实际链接地址是无法预期的(在链接文件里给每个函数分配固定地址链接这种方法不在考虑范畴,当函数数量众多时,这种方法太麻烦),业界上一个比较通用的做法是定义成员是函数指针的结构体,i.MXRT ROM API 就是采用的业界通用方式,下面 bootloader_api_entry_t 便是 i.MXRT1060 中 API 原型,g_bootloaderTree 就是实例:
typedef struct
{
const uint32_t version;
const char *copyright;
void (*runBootloader)(void *arg);
const hab_rvt_t *habDriver;
//!< FlexSPI NOR Flash API
const flexspi_nor_driver_interface_t *flexSpiNorDriver;
const nand_ecc_driver_interface_t *nandEccDriver;
const clock_driver_interface_t *clockDriver;
const rtwdog_driver_interface_t *rtwdogDriver;
const wdog_driver_interface_t *wdogDriver;
const stdlib_driver_interface_t *stdlibDriver;
} bootloader_api_entry_t;
// Bootloader API Tree
const bootloader_api_entry_t g_bootloaderTree = {
.copyright = "Copyright 2018 NXP",
.version = MAKE_VERSION(1, 0, 0),
.runBootloader = run_bootloader,
.habDriver = &hab_rvt,
.flexSpiNorDriver = &g_flexspiNorDriverInterface,
.nandEccDriver = &g_nandEccDriverInterface,
.clockDriver = &g_clockDriverInterface,
.rtwdogDriver = &g_rtwdogDriverInterface,
.wdogDriver = &g_wdogDriverInterface,
.stdlibDriver = &g_stdlibDriverInterface,
};
从上面代码我们可以看出,bootloader_api_entry_t 成员好像并不是函数指针,是的,为了分组方便,bootloader_api_entry_t 成员还是一个个结构体,它的这些结构体成员(比如 flexspi_nor_driver_interface_t)才是真正包含一个个函数指针的结构体。API 从功能来分一共提供了 7 类:HAB、FlexSPI NOR、NAND ECC、Clock、RT-WDOG、WDOG、stdlib。
设计到这里,我们通过 g_bootloaderTree 结构体常量就可以调用所有的 API 函数了,最后剩下的问题就是如何在 ROM 里找一个确定的地方保存随机链接的 g_bootloaderTree 地址(只要 4 字节即可)。是的,还是 Kinetis ROM API 用的那个巧妙的方法,下面是 BootROM 工程的 startup 文件(Keil 版),BootROM 将 g_bootloaderTree 的地址放到了中断向量表第 8 个向量的位置处(该向量为 ARM Cortex-M 未定义的系统向量),因此 0x20001c 处开始的 4bytes 便固定是 g_bootloaderTree 地址。
PRESERVE8
THUMB
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
IMPORT |Image$$ARM_LIB_STACK$$ZI$$Limit|
IMPORT g_bootloaderTree
__Vectors DCD |Image$$ARM_LIB_STACK$$ZI$$Limit|
DCD Reset_Handler
DCD DefaultISR
DCD HardFault_Handler
DCD DefaultISR
DCD DefaultISR
DCD DefaultISR
DCD g_bootloaderTree
DCD 0
DCD 0
DCD 0
DCD SVC_Handler
DCD DefaultISR
DCD 0
DCD DefaultISR
DCD DefaultISR
;; ...
1.3、API 调用方法
了解了前面介绍的 ROM API 产生背景与设计实现,它的调用方法就非常简单了,以 WDOG API 调用为例,只需要如下简单 3 句代码:
// 找到 API 根结构体
#define g_bootloaderTree (*(bootloader_api_entry_t **)0x0020001c)
// 定义 WDOG 模块配置变量
wdog_config_t config;
// 调用 API 中 WDOG_Init()
g_bootloaderTree->wdogDriver->WDOG_Init(WDOG1, config);
1.4、支持 API 的 i.MXRT 型号
截止目前,i.MXRT1xxx 系列一共出了 7 款型号,但并不是每个型号都开放了 ROM API,最早诞生的三款型号(105x、1021、1015)就并没有开放 API(不是没有 API,而是没有严格测试),其余型号都支持 API。
RT 芯片型号 | 是否支持 ROM API |
---|---|
i.MXRT117x | 支持 |
i.MXRT1064 | 支持 |
i.MXRT106x | 支持 |
i.MXRT105x | 未开放 |
i.MXRT1021 | 未开放 |
i.MXRT1015 | 未开放 |
i.MXRT1011 | 支持 |
二、API 之 FlexSPI 驱动
前面铺垫了太多 ROM API 设计细节,到这里才算进入正题,本文其实主要是要跟大家聊如何利用 API 里的 FlexSPI NOR 驱动实现 IAP。痞子衡在前面铺垫那么多的原因其实主要是想告诉大家,API 里的每个驱动都是经过完善测试的,尤其是这个 FlexSPI NOR 驱动,更是经过了千锤百炼,无论是易用性、运行稳定性还是 Flash 型号的支持度上都是首屈一指的。
对于 JESD216 标准下的串行 SPI 接口 Flash 驱动,大家知道更多的可能是 RT-Thread 技术总监朱天龙大神的开源 SFUD 项目,但痞子衡告诉你,i.MXRT ROM API 里的这个串行 Flash 驱动也毫不逊色(持续维护与优化了近 6 年,历经多款 MCU 的 ROM,是真正的产品级),只是不如开源项目那么知名,不过它的源代码也是开源在 SDK 里的(\SDK\middleware\mcu-boot\src\drivers\flexspi_nor),BSD-3-Clause 许可证。
2.1 FlexSPI 驱动原型
flexspi_nor_driver_interface_t 便是 FlexSPI NOR 驱动的原型,寻常的读写擦功能自然不在话下,除此以外,API 里面还有一个非常厉害的 xfer()函数,这个函数可以用来实现其他定制化的 Flash 操作函数,有兴趣的朋友可以进一步去研究。
typedef struct
{
uint32_t version;
status_t (*init)(uint32_t instance, flexspi_nor_config_t *config);
status_t (*program)(uint32_t instance, flexspi_nor_config_t *config, uint32_t dst_addr, const uint32_t *src);
status_t (*erase_all)(uint32_t instance, flexspi_nor_config_t *config);
status_t (*erase)(uint32_t instance, flexspi_nor_config_t *config, uint32_t start, uint32_t lengthInBytes);
status_t (*read)(uint32_t instance, flexspi_nor_config_t *config, uint32_t *dst, uint32_t addr, uint32_t lengthInBytes);
void (*clear_cache)(uint32_t instance);
status_t (*xfer)(uint32_t instance, flexspi_xfer_t *xfer);
status_t (*update_lut)(uint32_t instance, uint32_t seqIndex, const uint32_t *lutBase, uint32_t seqNumber);
status_t (*get_config)(uint32_t instance, flexspi_nor_config_t *config, serial_nor_config_option_t *option);
} flexspi_nor_driver_interface_t;
2.2 FlexSPI 驱动使用示例
FlexSPI 驱动使用基本三步走,先调用 get_config()获取完整 FlexSPI 模块配置,然后调用 init()函数去初始化 FlexSPI 以及访问 Flash 获取 SFDP 表信息,最后就是调用 Flash 操作函数(比如 erase())。
// 找到 API 根结构体
#define g_bootloaderTree (*(bootloader_api_entry_t **)0x0020001c)
// 定义 FlexSPI, Flash 配置变量
flexspi_nor_config_t config;
serial_nor_config_option_t option;
option.option0.U = 0xC0000008; // QuadSPI NOR, Frequency: 133MHz
uint32_t instance = 0;
// 调用 API 中 get_config()函数
g_bootloaderTree->flexSpiNorDriver->get_config(instance, &config, &option);
// 调用 API 中 init()函数
g_bootloaderTree->flexSpiNorDriver->init(instance, &config);
// 调用 API 中 erase()函数
g_bootloaderTree->flexSpiNorDriver->erase(instance, &config, 0x40000, 0x1000);
2.3 FlexSPI 驱动特点
因为 FlexSPI NOR 驱动 API 来自于 BootROM,因此其在使用上有一些小小的限制,也算是其特点吧。FlexSPI 驱动 API 里并没有提供 Flash 连接的 Pinmux 配置,其 Pinmux 配置已经写死在 init()函数中,就是 ROM 支持启动的 FlexSPI PORTA 上的那些 pin(片选是 SS0)。
在上面的使用示例代码中,你会看到 option.option0.U = 0xC0000008 代码,这算是 FlexSPI 驱动最大的特点了,这是一个简化的 option 配置 word(其原型可在芯片手册里找到),通过这个简化的 option,用户可以轻松配置来访问不同厂商的 Flash,下面是常用的 Flash 模式配置值。
• QuadSPI NOR - Quad SDR Read: option0 = 0xc0000008 (133MHz)
• QuadSPI NOR - Quad DDR Read: option0 = 0xc0100003 (60MHz)
• HyperFLASH 1V8: option0 = 0xc0233009 (166MHz)
• HyperFLASH 3V0: option0 = 0xc0333006 (100MHz)
• MXIC OPI DDR (OPI DDR enabled by default): option=0xc0433008(133MHz)
• Micron Octal DDR: option0=0xc0600006 (100MHz)
• Micron OPI DDR: option0=0xc0603008 (133MHz), SPI->OPI DDR
• Micron OPI DDR (DDR read enabled by default): option0 = 0xc0633008 (133MHz)
• Adesto OPI DDR: option0=0xc0803008(133MHz)
2.4 FlexSPI 驱动用作 IAP
IAP 其实就是在 App 中实现 Flash 擦写,单纯从技术上来说并不是一个很难的东西。但 i.MXRT 上很多时候 App 代码本身也在同一片 Flash 里执行(也叫 XIP),而市面上很多 Flash 都是不支持 RWW(Read-While-Write)的,这就导致一个问题,当你调用 Flash 操作函数去擦写 Flash 时,CPU 又需要继续去 Flash 获取指令,违反了 RWW,因此你只能把 Flash 相关操作函数全部放在 RAM 中去执行(这涉及分散加载了,对于初级嵌入式用户来说稍微有点难)。
现在我们有了 ROM API,FlexSPI 驱动代码体全部都在 ROM 空间里,并不占用 Flash 空间,因此不存在 RWW 问题,真是天然为 IAP 而生,再也不用再管什么分散加载这么麻烦的事了。
三、FlexSPI API 业界应用
最后再介绍一下 i.MXRT FlexSPI API 在业界的应用,这个 API 其实并不小众,目前已被主流 IDE 和调试工具用作 i.MXRT Flash 下载算法。
3.1 用于 IAR 下载算法
如果你的 IAR 版本够新,能够支持 i.MXRT1060 等型号,随便打开一个 i.MXRT1060 SDK 工程,在工程 Option 里找到 Debugger,然后进入 Flashloader 配置,你会看到页面里有 Extra parameters 一栏,在下面的解释里有这个参数的示例,它就是前面 2.3 节里介绍的 option0。有了这种方式设计的 Flash 下载算法,你再也不用手动更新下载算法文件去支持不同的 Flash 了,改参数就行了。
3.2 用于 J-Link 下载算法
目前最新的 Jlink 驱动里的下载算法也是基于 ROM API 的,痞子衡有一个开源项目,收集了 i.MXRT 所有型号的下载算法源代码工程,其中 jlink 算法是最全的,其他 IDE 算法还在陆续完善中。
https://github.com/JayHeng/imxrt-tool-flash-algo
至此,i.MXRT 系列 ROM 中的 FlexSPI 驱动 API 实现 IAP 痞子衡便介绍完毕了,掌声在哪里~~~