LPC系列MCU是NXP于2003年推出的一款非常具有代表性的产品,至今已有近20年的历史。按照时间线演变,主要分为三代:
- Veteran:基于ARM7/9内核的LPC2000/3000系列- Backbone:基于Cortex-M0/0+/3/4内核的LPC800/1100/1200/1300/1500/1700/1800/4000/4300/54000 - New尖端:基于Cortex-M33内核的LPC5500系列。
其中核心产品之一就是今天皮子衡要重点介绍的经典MCU。从最初的LPC1800到至今仍有新型号的LPC800,仍然很受开发者欢迎。皮子衡今天要讨论的是内部Flash驱动程序,一个在嵌入式软件开发者中既不流行也不流行的话题:
注:本文内容主要以LPC845型号为例。它可能并不完全适用于其他经典的LPC 模型。详细内容需要查看相应的手册。
1. MCU内部Flash的基本概念
皮子恒首先解释了为什么内部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中的所有位从0恢复为0。 1. Flash写入是以PUnit为最小单位(可能是1/2/4/8字节),一次最多可以写入一页数据(这里指的是一次完整命令执行的等待过程)。擦除和写入操作不会立即完成,需要等待Memory media update完成(读取Flash控制器相应的状态位寄存器)。
LPC845内部Flash总共64KB,分为64个Sector,每个Sector大小为1KB。每个Sector包含16个Page,每个Page大小为64Bytes。支持按扇区/页擦除。 IAP仅支持Page写入(但控制器底层最小写入单位为4bytes),不支持RWW功能。
64KB N/A N/A 1KB 64Bytes 4Bytes闪存闪存组=闪存块闪存扇区闪存页=闪存PUnit=闪存字节| | | | | RWW 单位擦除单位擦除单位最大写入单位最小写入单位
关于Flash的擦写操作,还有一个重要的概念叫做Read-While-Write(简称RWW)。因为默认的代码是在Flash中执行的,如果此时我们仍然进行Flash的擦除和写入操作,那么同一个Flash将会处于另一种状态。擦除和写入过程还必须响应来自AHB总线的读命令请求。大多数Flash 不支持此功能。因此,常见的操作是将触发Flash擦除和写入命令的代码重定向并将Flash状态读取到RAM中执行。 LPC 上的不同Flash IAP 驱动程序设计旨在解决此RWW 限制。
2. 通用Flash驱动设计
在讲LPC Flash IAP功能驱动之前,我们先看一下通用MCU上的Flash驱动设计,以NXP Kinetis MK60DN512Z系列为例。其Flash外设为FTFL(详细信息请参见参考手册中的第28章闪存模块(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闪存闪存存储区=闪存块闪存扇区=闪存页闪存PUnit=闪存字节| | | | | RWW 单位擦除单位擦除单位最大写入单位最小写入单位
在官方驱动SDK_2_2_0_TWR-K60D100MdevicesMK60D10driversfsl_flash.c中,我们重点关注以下五个基本功能。这些函数直接操作FTFL外设寄存器来完成相应的Flash擦除和写入功能。其中,flash_command_sequence()的内部函数设计是核心。每个API基本上都会调用它。关于解决RWW限制有一个黑科技设计。皮子衡稍后会写一篇文章来介绍。
//通用初始化函数,主要是软件级初始化status_tFLASH_Init(flash_config_t*config); //专门设计的命令触发执行函数,解决RWW限制status_tFLASH_PrepareExecuteInRamFunctions(flash_config_t*config); staticstatus_tflash_command_sequence(flash_config_t*config)//擦除功能,长度不限(需要按Sector对齐),关键参数是减少误擦除的风险status_tFLASH_Erase(flash_config_t*config,uint32_tstart,uint32_tlengthInBytes,uint32_tkey);//Write函数,长度不限(仅最小写入单元对齐限制),函数自动组合Page和PUnit写入命令进行处理status_tFLASH_Program(flash_config_t*config,uint32_tstart,uint32_t*src,uint32_tlengthInBytes);
3. LPC Flash IAP驱动设计原理
最后我们来到了本文的核心——LPC Flash IAP驱动。根据我们一般的经验,首先是翻翻LPC845的用户手册,找到Flash外设。遗憾的是,用户手册中没有对Flash外设进行详细介绍。相反,有第5: 章LPC84x ISP 和IAP。由于整个LPC系列都包含BootROM(映射地址为0x0F00_0000 -0x0F00_3FFF),并且BootROM代码中包含Flash擦除驱动,所以官方建议用户直接调用ROM中的Flash驱动API来完成操作,而不是直接提供以传统方式进行说明。操作Flash外设寄存器的SDK源码。
BootROM 提供的不仅仅是闪存IAP API。所有API 都可以在引导过程章节中找到,如下所示。这里我们可以看到Flash IAP函数的统一入口地址为0x0F001FF1,在SDK中的LPC845_features.h文件中有如下特殊宏:
/*@briefPointertoROMIAPentryfunctions*/#defineFSL_FEATURE_SYSCON_IAP_ENTRY_LOCATION(0x0F001FF1)
有了IAP入口地址,调用就简单了。芯片用户手册中直接给出了参考C代码。可以看到API设计将所有13个支持的函数聚集在一起,并重用了输入参数列表command_param和输出。结果列表status_result。皮子恒之前写过一篇文章《第二代Kinetis上的Flash IAP设计》。 API接口设计更符合现代嵌入式软件开发者的习惯,而LPC Flash IAP接口设计则是在2008年推出的,在当时看来是超前的。
unsignedintcommand_param[5];unsignedintstatus_result[5];typedefvoid(*IAP)(unsignedint[],unsignedint[]);#defineIAP_LOCATION*(volatileunsignedint*)(0x0F001FF1)IAPiap_entry=(IAP)IAP_LOCATION;iap_entry(command_param,status_result) ;
4. LPC Flash IAP驱动快速上手
最后看一下官方驱动SDK_2_13_0_LPCXpresso845MAXdevicesLPC845driversfsl_iap.c。这相当于Flash IAP的二次封装。我们重点关注以下6个基本功能。其中iap_entry()最终调用的是ROM中的代码,直接在ROM区域执行,不会与Flash访问冲突。自然就不存在RWW限制问题了。
擦除函数IAP_ErasePage()/IAP_EraseSector()没什么好说的。写函数IAP_CopyRamToFlash()的名字有点绕,不符合一般习惯。需要特别注意的是,写入长度numOfBytes必须是Page的倍数,不能超过1。 Sector大小(但经测量可以跨两个Sector一次写入多个Page数据,所以这只是软件代码人为指定的,并非Flash控制器的限制)。
最后,还有一点需要注意的是,擦除和写入操作都是所谓的两步过程,这意味着需要先调用IAP_PrepareSectorForWrite()函数。这样的设计其实是为了降低程序跑掉时误擦写的风险。
//通用初始化函数,主要配置Flash访问时间voidIAP_ConfigAccessFlashTime(uint32_taccessTime); //进入ROMIAP入口函数staticinlinevoidiap_entry(uint32_t*cmd_param,uint32_t*status_result);//擦写前准备函数status_tIAP_PrepareSectorForWrite(uint32_tstartSector,uint) 32_tendSector );//擦除函数,以Page/Sector为单位status_tIAP_ErasePage(uint32) _tstartPage,uint32_tendPage ,uint32_tsystemCoreClock);status_tIAP_EraseSector(uint32_tstartSector,uint32_tendSector,uint32_tsystemCoreClock);//写入函数,最大长度限制为1个Sectorstatus _tIAP_CopyRamToFlash(uint32_tdstAddr,uint32_t*srcAddr ,uint32_tnumOfByte s,uint32_tsystemCoreClock);
审稿人:刘庆