今天给大家介绍的是恩智浦经典LPC系列MCU内部Flash IAP驱动。
LPC 系列 MCU 是恩智浦公司于 2003 年开始推出的非常具有代表性的产品,距今已经有近 20 年的生命。按时间线演进来说,其主要分为三代:
- 元老:基于 ARM7/9 内核的 LPC2000/3000 系列 - 中坚:基于 Cortex-M0/0+/3/4 内核的 LPC800/1100/1200/1300/1500/1700/1800/4000/4300/54000 - 新锐:基于 Cortex-M33 内核的 LPC5500 系列。
其中坚产品即是今天要重点聊的经典 MCU,从其第一颗 LPC1800 到至今仍有新型号出来的 LPC800,仍然深受广大开发者喜爱。今天想讨论的是内部 Flash 驱动这个对嵌入式软件开发者来说既冷门又不冷门的话题。
一、关于MCU内部Flash的基本概念
- Note:本文内容主要以 LPC845 这个型号为例,未必完全适用其它经典 LPC 型号,具体需要查看相应手册。
首先,解释一下为什么内部 Flash 驱动这个话题既冷门又不冷门。
说它冷门是因为大部分嵌入式软件开发工程师写的应用代码里很少包含 Flash 操作功能(除非应用需要 OTA 升级或者断电保存参数),因此对 Flash 模块的关注度不如其它外设模块;说它不冷门则是在 IDE 中调试或者编程器做量产又离不开 Flash 操作,所以避不可免地关注 Flash 擦写算法、性能、寿命、效率等。
话说回来,Flash 外设一般由两部分组成:Flash 控制器 + Flash Memory 介质,其 Memory 介质部分从原理上属于并行 NOR Flash,MCU 上电 Flash 外设总是使能的,可以通过 AHB 总线直接读取其映射空间内任意 Flash 地址处的数据/指令,所以其最主要的作用就是存储可执行代码。
如果应用程序需要做 OTA 升级,则需要借助 Flash 控制器完成擦除和写入操作。这里就有一些概念性的东西出现了,比如 Flash 擦除正常是按 Block/Sector 为单元(不排除有些支持按 Page 擦除),并且擦除操作是将 Block/Sector 里全部 bit 从 0 恢复为 1。
而 Flash 写入则是按 PUnit 为最小单元的(可能是 1/2/4/8 bytes),一次性最多写入一个 Page 的数据(这里指一次完整命令执行等待过程)。擦除和写入操作都不是立刻就完成的,需要等待 Memory 介质更新完成(读 Flash 控制器相应状态位寄存器)。
LPC845 内部 Flash 一共 64KB,划分为 64 个 Sector,每个 Sector 大小为 1KB。每个 Sector 包含 16 个 Page,每个 Page 大小为 64Bytes。支持按 Sector/Page 擦除,IAP 仅支持按 Page 写入(但是控制器底层最小写入单元是 4bytes),不支持 RWW 特性。
64KB N/A N/A 1KB 64Bytes 4Bytes Flash Memory > Flash Bank >= Flash Block > Flash Sector > Flash Page >= Flash PUnit >= Flash Byte | | | | | RWW单元 擦除单元 擦除单元 最大写入单元 最小写入单元
关于 Flash 擦写操作,还有一个重要概念叫 Read-While-Write(简称 RWW),因为默认代码是执行在 Flash 里,如果我们这个时候还做 Flash 擦写操作,就会让同一个 Flash 处于又做擦写处理同时也要响应 AHB 总线来的读指令请求,大部分 Flash 是无法支持这个特性的,因此常见的操作是将触发 Flash 擦写命令以及读 Flash 状态的代码重定向到 RAM 里去执行。而 LPC 上不一样的 Flash IAP 驱动设计,正是为了解决这个 RWW 限制的。
二、一般Flash驱动设计在讲 LPC Flash IAP 特色驱动之前,我们先来看看一般 MCU 上 Flash 驱动设计。
以恩智浦 Kinetis MK60DN512Z 系列为例,它的 Flash 外设是 FTFL (详见参考手册里 Chapter 28 Flash Memory Module (FTFL) 章节),Flash 大小为 512KB,分为两个 256KB Block (这里就相当于Bank),支持 RWW 特性(以 Block 为单元)。每个 Block 包含 128 个 Sector,每个 Sector 大小为 2KB。它其实没有明确的 Page 概念(但最大写入单元是专用 4KB FLEXRAM 的一半,可以理解为 Page 大小就是 2KB),支持的最小写入单元是 4bytes。
512KB 256KB 256KB 2KB 2KB 4Bytes Flash Memory > Flash Bank >= Flash Block > Flash Sector >= Flash Page > Flash PUnit >= Flash Byte | | | | | RWW单元 擦除单元 擦除单元 最大写入单元 最小写入单元
在官方驱动 \SDK_2_2_0_TWR-K60D100M\devices\MK60D10\drivers\fsl_flash.c 里我们重点关注如下 5 个基本函数,这些函数都是直接操作 FTFL 外设寄存器来完成相应 Flash 擦写功能的。其中 flash_command_sequence() 内部函数设计是核心,每一个 API 基本都会调用它,这里面有一个关于解决 RWW 限制的黑科技设计,后面会写文章专门介绍。
// 一般初始化函数,主要是软件层面初始化 status_t FLASH_Init(flash_config_t *config); // 为了解决 RWW 限制而特殊设计的命令触发执行函数 status_t FLASH_PrepareExecuteInRamFunctions(flash_config_t *config); static status_t flash_command_sequence(flash_config_t *config) // 擦除函数,长度不限(需要按 Sector 对齐),key 参数是为了降低误擦除风险 status_t FLASH_Erase(flash_config_t *config, uint32_t start, uint32_t lengthInBytes, uint32_t key); // 写入函数,长度不限(仅最小写入单元对齐限制),函数内部自动结合 Page 和 PUnit 写入命令做处理 status_t FLASH_Program(flash_config_t *config, uint32_t start, uint32_t *src, uint32_t lengthInBytes);三、LPC Flash IAP驱动设计原理
终于来到本文核心——LPC Flash IAP 驱动了。按照我们一般经验,首先是翻看 LPC845 用户手册寻找 Flash 外设,但是很遗憾,用户手册里并没有 Flash 外设详细介绍,取而代之的是 Chapter 5: LPC84x ISP and IAP 章节。因为 LPC 全系列都包含 BootROM(映射地址为 0x0F00_0000 - 0x0F00_3FFF),而 BootROM 代码里包含了 Flash 擦写驱动,因此官方直接推荐用户调用 ROM 里的 Flash 驱动 API 来完成操作,而不是按照传统方式提供直接操作 Flash 外设寄存器的 SDK 源码。
BootROM 提供的 API 不止 Flash IAP 一个,可以在 Boot Process 章节里如下图里找到全部 API。这里我们可以看到 Flash IAP 函数的统一入口地址是 0x0F001FF1,这在 SDK 里 LPC845_features.h 文件里有如下专门宏:
/* @brief Pointer to ROM IAP entry functions */ #define FSL_FEATURE_SYSCON_IAP_ENTRY_LOCATION (0x0F001FF1)
有了 IAP 入口地址,调用起来就简单了,芯片用户手册里直接给了参考 C 代码,可以看到 API 设计上将全部支持的 13 个函数集中在一起了,复用了输入参数列表 command_param 和输出结果列表 status_result。
大家可以参考文章 《二代 Kinetis 上的 Flash IAP 设计》,那个 API 接口设计更偏向现代嵌入式软件开发者的习惯,而 LPC Flash IAP 接口设计是 2008 年推出来的,那时候看是超前时代。
unsigned int command_param[5]; unsigned int status_result[5]; typedef void (*IAP)(unsigned int [],unsigned int[]); #define IAP_LOCATION *(volatile unsigned int *)(0x0F001FF1) IAP iap_entry=(IAP) IAP_LOCATION; iap_entry (command_param,status_result);四、LPC Flash IAP驱动快速上手
最后,我们再来看一下官方驱动 \SDK_2_13_0_LPCXpresso845MAX\devices\LPC845\drivers\fsl_iap.c ,这相当于将 Flash IAP 做了二次封装,我们重点关注如下 6 个基本函数。其中 iap_entry() 最终调用的是 ROM 中代码,直接执行在 ROM 区域,不会和 Flash 访问冲突,天然没有 RWW 限制问题。
擦除函数 IAP_ErasePage()/IAP_EraseSector() 没什么好说的,就是这个写入函数 IAP_CopyRamToFlash() 命名有点绕,不符合一般习惯,然后需要特别注意的是写入长度 numOfBytes 必须是 Page 倍数,且不能超过一个 Sector 大小(但是实测可以横跨两个 Sector 一次性写入多个 Page 数据,所以这仅仅是软件代码人为规定,不是 Flash 控制器限制)。
最后还有一个注意点就是,擦写操作都是所谓的 two step process,就是需要先调用一下 IAP_PrepareSectorForWrite() 函数才行,这个设计其实是为了降低程序跑飞出现误擦写的风险。
// 一般初始化函数,主要是配置 Flash 访问时间 void IAP_ConfigAccessFlashTime(uint32_t accessTime); // 进入 ROM IAP 的入口函数 static inline void iap_entry(uint32_t *cmd_param, uint32_t *status_result); // 擦除和写入前准备函数 status_t IAP_PrepareSectorForWrite(uint32_t startSector, uint32_t endSector); // 擦除函数,按 Page/Sector 为单位 status_t IAP_ErasePage(uint32_t startPage, uint32_t endPage, uint32_t systemCoreClock); status_t IAP_EraseSector(uint32_t startSector, uint32_t endSector, uint32_t systemCoreClock); // 写入函数,长度最大限定为一个 Sector status_t IAP_CopyRamToFlash(uint32_t dstAddr, uint32_t *srcAddr, uint32_t numOfBytes, uint32_t systemCoreClock);
至此,恩智浦经典LPC系列MCU内部Flash IAP驱动入门便介绍完毕了。