1. 项目概述深入理解ISP与IAP的价值在嵌入式开发这条路上摸爬滚打十几年我处理过无数种微控制器也经历过各种固件更新的“痛苦”。早期给产品升级要么得把芯片从板子上吹下来用编程器烧录再焊回去费时费力还容易损坏要么就得预留一个庞大的JTAG接口既占PCB面积又增加成本。直到后来系统内编程ISP和在应用编程IAP技术成熟起来才真正把我们从这些繁琐的物理操作中解放出来。今天我想以一款非常经典且具有代表性的芯片——Philips现NXP的P89LPC932A1为例把ISP和IAP这两项技术的里里外外、实操细节和踩过的坑给大家掰开揉碎了讲清楚。简单来说ISP和IAP的核心目的就是让你能在不把单片机从电路板上取下来的情况下更新它的程序。这听起来简单但对产品生命周期管理而言是革命性的。想象一下一个安装在偏远地区的工业传感器或者已经销售到用户手中的智能家电当需要修复一个软件BUG或者增加新功能时如果支持ISP或IAP你只需要通过串口、网络等通信渠道把新的固件文件发送过去设备自己就能完成更新无需返厂也无需专业人员上门。这极大地降低了维护成本延长了产品有效生命周期也是实现物联网设备OTA空中下载升级的基础。P89LPC932A1作为一款增强型的80C51微控制器其ISP和IAP功能设计得非常典型和完整。ISP依赖于芯片出厂时固化在Boot ROM中的一段引导程序通过串口协议与上位机通信完成对整个Flash存储器的擦写。而IAP则更强大它是一组可供用户应用程序调用的底层函数允许你在程序运行过程中对特定的Flash扇区进行读、写、擦除操作甚至能操作配置字节和安全位。这意味着你不仅能用它更新程序还能把一部分Flash当作非易失性数据存储器来用比如存储校准参数、运行日志、用户配置等这对于资源紧张的8位单片机来说是个非常宝贵的能力。2. ISP与IAP的核心原理与差异解析2.1 系统内编程ISP的工作机制ISP的本质是芯片在出厂时就在一段不可擦除的ROMBoot ROM里预先烧录好了一个“引导加载程序”Bootloader。这个程序不归用户管你擦不掉也改不了。当芯片满足特定条件通常是复位时某个引脚处于特定电平对于P89LPC932A1是PSEN引脚为低电平时CPU就不会从常规的应用程序入口地址0000H启动而是跳转到Boot ROM中的这段引导程序开始执行。这段引导程序干的事情很专一初始化一个串口通常是UART然后等待上位机发送过来的命令和数据。它遵循一套特定的通信协议对于P89LPC932A1就是基于Intel HEX格式的指令集。上位机软件比如Flash烧录工具通过串口发送形如:100000000102030405060708090A0B0C0D0E0FCC这样的记录其中包含了操作类型是编程、擦除还是读取、目标地址、数据以及校验和。Bootloader程序解析这些记录执行对应的Flash操作并将结果成功或失败反馈给上位机。ISP的整个过程中用户原有的应用程序是不运行的芯片完全由Bootloader接管。因此ISP通常用于产品的初次烧录、量产烧录或者当应用程序完全损坏“变砖”后的救援性烧录。它的优势在于不依赖用户代码只要硬件连接正确总能进入这个模式非常可靠。P89LPC932A1的ISP只需要连接VDD、VSS、TxD、RxD和RST五个引脚到一个简单的电平转换和接口电路即可对PCB空间占用极小。2.2 在应用编程IAP的工作机制IAP则是在另一个层面上运作。它不再是芯片自带的独立引导程序而是一组以“软件中断”或“函数调用”形式提供给用户程序的编程接口。在P89LPC932A1中这个入口地址是固定的0xFF03。当你的应用程序需要操作Flash时比如保存一个参数它可以通过设置好一系列寄存器ACC, R3, R4, R5, R7等然后调用这个地址CPU就会暂时挂起你的应用程序跳转到内部固件执行Flash擦写操作完成后再返回你的程序继续执行。这就好比你的应用程序用户态需要执行一个特权操作内核态通过一个系统调用来完成。IAP让你在程序运行时动态地管理Flash。例如你可以设计一个功能设备通过串口接收一段新的子程序代码将其暂存在RAM中然后通过IAP函数将其写入Flash的某个空闲扇区。之后你的主程序可以通过函数指针跳转到这个新写入的代码区域执行从而实现功能的动态扩展或补丁更新。IAP和ISP最关键的区别在于“主动性”和“上下文”。ISP是被动的由外部工具发起并控制整个过程IAP是主动的由芯片内部正在运行的程序发起是应用程序功能的一部分。IAP必须在应用程序正常运行且功能完好的前提下才能使用如果程序跑飞了或者Flash里根本没有程序IAP也就无从谈起。2.3 P89LPC932A1的ISP/IAP硬件与引脚需求无论是ISP还是IAP其物理基础都是芯片内部的Flash存储器和相关编程电路。P89LPC932A1的Flash分为多个扇区Sector和页Page这是擦除操作的最小单位。编程写入则可以按字节进行但写入前必须先擦除而擦除会使整个扇区或页的所有位变为10xFF。对于ISP硬件连接极其简单VDD/VSS电源和地必须稳定。TxD/RxD串口收发线需要连接到上位机的串口通常需经过RS-232电平转换或USB转串口芯片。RST复位引脚。在P89LPC932A1中进入ISP模式通常需要一个特定的复位序列。常见做法是先拉低RST然后拉低PSEN引脚再释放RST最后释放PSEN。此时芯片会从Boot ROM启动等待串口命令。具体时序需要参考数据手册不同的工具可能有细微差别。注意ISP通信的波特率是自适应的。引导程序会通过检测接收到的第一个字符大写字母‘U’的位时间来反推波特率。因此只要上位机发送的‘U’字符格式正确从1200到115200甚至更高的波特率都能自动适配这大大简化了连接无需精确匹配时钟频率。对于IAP则无需特殊硬件连接因为它完全是软件行为。但需要注意的是执行IAP擦写操作时芯片内核会暂停进入编程空闲状态此时无法响应中断。如果中断发生会打断编程过程并导致错误。因此在调用IAP函数前通常需要关闭全局中断EA0操作完成后再开启。3. ISP协议详解与上位机通信实战3.1 Intel HEX记录格式深度解析P89LPC932A1的ISP通信完全基于Intel HEX格式这是一种用ASCII文本表示二进制数据的通用格式。理解它的每一个字段是编写或调试自定义ISP下载工具的基础。一条完整的记录如下:NNAAAARRDDDDDDDDDDDDDDDDCCCRLF起始符 (:)每条记录以冒号开始。字节计数 (NN)用两个十六进制ASCII字符表示本记录中数据字节的数量。例如10表示有16个字节的数据。P89LPC932A1限制最大为0x4064字节。地址 (AAAA)用四个十六进制ASCII字符表示本记录中第一个数据字节要写入的Flash内存地址偏移量。地址是16位的。记录类型 (RR)两个字符定义记录的功能。00数据记录。表示后面的DD...是要写入的程序代码或数据。01文件结束记录。表示HEX文件结束没有数据。其他02-08在ISP语境下这些是命令记录用于触发擦除、读取ID、设置波特率等特殊操作。数据 (DD...)实际的数据字节每个字节由两个十六进制ASCII字符表示。数量等于字节计数NN。校验和 (CC)两个字符。计算方法是从字节计数开始到数据结束所有字节的二进制值求和然后取和的二进制补码即用0x100减去这个和再取低字节。接收方会重新计算校验和如果匹配则回复一个点字符.如果不匹配则回复X。例如记录:100000000102030405060708090A0B0C0D0E0FCC解析如下10: 有16个数据字节。0000: 起始地址为0x0000。00: 这是一个数据记录。0102030405060708090A0B0C0D0E0F: 16个字节的数据。CC: 校验和。计算0x100x000x000x000x01...0x0F 0x134取低字节0x34其补码为0x100 - 0x34 0xCC正确。3.2 ISP命令集实操指南除了00写数据和01结束P89LPC932A1的ISP定义了一系列命令记录其格式为:02xxxxCCSSDDCC或类似变体其中SS是子功能码。下面结合实例说明几个最常用的命令擦除扇区/页 (Record Type 04) 命令格式:03xxxx04SSAAAACCSS00擦除页01擦除扇区。AAAA要擦除的页或扇区地址。示例擦除地址0x0000开始的扇区:03000004010000F8实操要点擦除是耗时操作毫秒级。发送命令后必须等待足够的时间数据手册会给出典型值如20ms再发送下一条命令或进行验证。匆忙发送下一条指令会导致通信超时或失败。读取器件ID (Record Type 03, Subfunction 0x11/0x12) 命令格式:01xxxx03SSCCSS0x11读取制造商ID0x12读取设备ID。示例读取设备ID:0100000312CC实操要点这是验证ISP连接是否成功、芯片型号是否正确的第一步。成功执行后Bootloader会返回一个包含ID数据的特殊数据记录。上位机需要解析这个返回记录来获取ID。直接加载波特率 (Record Type 07) 命令格式:02xxxx07HHLLCCHH和LL是定时器重装值的高低位用于直接设定串口波特率发生器跳过初始的‘U’字符自适应阶段。实操心得这个命令通常用在需要固定高速波特率的场合。在发送‘U’字符建立初始低速连接后可以立即发送此命令切换到更高的、更稳定的波特率以加速后续大量数据的传输。复位MCU (Record Type 08) 命令格式:00xxxx08CC示例:00000008F8注意事项发送此命令后芯片会立即复位并跳出ISP模式从用户程序区或由BOOTVEC定义的地址开始执行。上位机发送此命令后应等待一段时间并做好串口断开连接的准备。3.3 构建一个简单的ISP下载流程一个完整的ISP编程流程可以概括为以下几步我通常用Python或C#写个小工具来实现硬件连接与上电确保VDD、GND、TxD、RxD、RST正确连接。按前述时序操作RST和PSEN引脚使芯片进入ISP模式。打开串口发送引导字符以任意波特率通常从9600开始尝试打开串口发送一个大写字母‘U’。如果连接正确芯片会回显这个‘U’。这一步建立了通信的波特率基准。可选切换波特率发送07命令切换到更高的目标波特率如115200并重新以新波特率打开串口。擦除目标区域根据需要发送04命令擦除整个芯片或特定扇区。务必在命令间加入延时。发送程序数据将编译生成的Intel HEX文件按行读取发送所有00类型的数据记录。每发送一条等待接收一个.成功或X失败需重发。这里要做好流量控制和错误重传机制。校验可选但推荐发送03命令子功能码05或06读取扇区或全局的CRC校验值与本地计算的CRC进行比对确保数据写入无误。结束与复位发送01类型记录:00000001FF表示文件结束。然后发送08命令复位MCU让其运行新程序。关闭串口。踩坑记录早期我用ISP下载时经常遇到数据传一半就卡住的情况。后来发现是两个问题一是串口读写缓冲区没处理好上位机发送太快芯片Bootloader处理不过来导致数据丢失二是擦除后等待时间不足。解决办法是在发送每条命令后同步读取串口返回值在发送下一条数据记录前增加一个几毫秒的短延时在擦除命令后增加一个50-100ms的长延时。稳定性大幅提升。4. IAP函数调用与应用程序设计4.1 IAP函数调用接口详解IAP功能通过调用固定地址0xFF03的子程序来使用。在调用前你需要根据想执行的操作设置好8051的几个核心工作寄存器。这很像操作系统的系统调用。P89LPC932A1提供了丰富的IAP功能其调用参数和返回值在数据手册的Table 99中有明确定义。这里我们重点分析几个最常用的函数调用统一入口// 在C语言中可以这样定义函数指针 typedef short (*IAP_FUNC)(void); IAP_FUNC iap_entry (IAP_FUNC)0xFF03; // 或者直接用汇编调用 CALL 0FF03H关键函数解析编程用户代码页 (ACC0x00)功能将RAM中的数据写入Flash的一页。输入参数ACC 0x00R3要编程的字节数1-64。R4, R5目标页地址的高位和低位。注意地址必须对齐到页边界对于P89LPC932A1页大小通常是64字节。R7指向源数据缓冲区在RAM中地址的指针。F1缓冲区位置选择。0表示使用IDATA内部RAM1表示使用XDATA外部RAM如果支持。返回R7为状态码CY进位标志置位表示出错。C语言调用示例Keil C51#include absacc.h #define IAP_KEY DBYTE[0xFF] // 授权密钥地址 #define IAP_ENTRY ((void (*)(void))0xFF03) void program_flash_page(unsigned int page_addr, unsigned char xdata *data_buf) { IAP_KEY 0x96; // 第一步设置授权密钥 ACC 0x00; // 功能号编程页 R3 64; // 编程64字节一整页 R4 (unsigned char)(page_addr 8); R5 (unsigned char)(page_addr 0xFF); R7 (unsigned char)data_buf; // 假设数据在IDATA区 F1 0; IAP_ENTRY(); // 调用IAP // 调用后检查CY标志位判断是否成功 }注意事项调用前必须在RAM地址0xFF处写入授权密钥0x96且每次调用都需要重新写入因为IAP函数执行后会清除该密钥。擦除扇区/页 (ACC0x04)功能擦除一个扇区或一页。输入参数ACC 0x04R70x00擦除页0x01擦除扇区。R4, R5扇区/页地址。这是破坏性操作擦除后该区域所有字节变为0xFF。务必确保你擦除的是正确的、不再需要的或准备重新编程的区域。读取用户代码 (ACC0x07)功能从Flash中读取一个字节。常用于数据校验或运行时读取存储在Flash中的常量、配置信息。输入参数ACC0x07,R4,R5为地址。返回R7为读出的数据。4.2 IAP实战设计一个简单的数据存储模块假设我们要用IAP在Flash的最后一个扇区例如地址0x1C00开始存储一些系统参数如设备序列号、校准值等。第一步规划Flash布局我们必须避开程序代码占用的区域。通常在链接脚本.l51文件中指定程序的结束地址确保为数据存储区留出空间。例如CODE(0x0000-0x17FF) // 主程序区 DATA_FLASH(0x1C00-0x1FFF) // 数据存储区最后一个扇区第二步编写数据存储函数#define DATA_FLASH_BASE 0x1C00 #define IAP_KEY_LOC 0xFF #define IAP_ENTRY ((void (code *)(void))0xFF03) bit iap_program_page(unsigned int addr, unsigned char *buf) { bit success 0; // 1. 关闭中断防止打断编程过程 EA 0; // 2. 设置授权密钥 *(unsigned char idata *)IAP_KEY_LOC 0x96; // 3. 设置寄存器参数使用内联汇编或直接赋值给特殊寄存器 _asm { MOV ACC, #00h ; 功能号编程页 MOV R3, #64 ; 编程64字节 MOV R4, #high(DATA_FLASH_BASE) ; 地址高字节 MOV R5, #low(DATA_FLASH_BASE) ; 地址低字节 MOV R7, buf ; 数据缓冲区指针假设在IDATA MOV F1, #0 ; 使用IDATA LCALL IAP_ENTRY ; 调用IAP MOV success, C ; 将进位标志错误标志保存到success变量 } // 4. 重新开启中断 EA 1; return !success; // 如果CY0无错误返回1成功 } bit iap_erase_sector(unsigned int addr) { bit success 0; EA 0; *(unsigned char idata *)IAP_KEY_LOC 0x96; _asm { MOV ACC, #04h ; 功能号擦除 MOV R7, #01h ; 01擦除扇区 MOV R4, #high(addr) MOV R5, #low(addr) LCALL IAP_ENTRY MOV success, C } EA 1; return !success; }第三步设计存储逻辑Flash不能像RAM那样直接覆盖写入。写入前必须先擦除整扇区/页擦除而擦除次数有限通常10万次。因此我们需要一个简单的“磨损均衡”或“标志位”管理机制。typedef struct { unsigned char valid_flag; // 例如 0xA5 表示数据有效 unsigned long serial_num; float calibration_factor; // ... 其他参数 } SystemParams_t; void save_params(SystemParams_t *params) { unsigned char buffer[64]; // 一页的大小 SystemParams_t *p_buf (SystemParams_t*)buffer; // 1. 将参数拷贝到缓冲区 *p_buf *params; p_buf-valid_flag 0xA5; // 2. 擦除目标扇区谨慎操作 if (!iap_erase_sector(DATA_FLASH_BASE)) { // 处理擦除失败 return; } // 3. 等待擦除完成可加入延时或状态查询 delay_ms(50); // 4. 编程数据 if (!iap_program_page(DATA_FLASH_BASE, buffer)) { // 处理编程失败 } } bit load_params(SystemParams_t *params) { unsigned char code *p (unsigned char code *)DATA_FLASH_BASE; SystemParams_t code *stored_params (SystemParams_t code *)p; // 简单检查标志位 if (stored_params-valid_flag 0xA5) { *params *stored_params; // 从Flash拷贝到RAM return 1; } return 0; // 数据无效 }核心经验永远不要在中断服务程序ISR中调用IAP函数因为IAP执行期间CPU会挂起如果此时发生中断会强制中止Flash操作导致数据损坏或程序崩溃。务必在调用IAP前关闭总中断EA0操作完成后再打开。此外Flash编程电压较高操作期间功耗会增大在电池供电设备中需注意。5. 安全机制、配置字节与常见问题排查5.1 安全位与写保护机制解析P89LPC932A1提供了多层次的安全保护防止代码被非法读取或篡改这对于保护知识产权至关重要。扇区安全字节SECx每个Flash扇区都有三个安全位MOVCDISx, SPEDISx, EDISx它们共同决定了该扇区的保护级别。MOVCDISx置1后禁止MOVC指令读取该扇区。即使有人通过调试接口读取Flash读到的也是无效数据。注意这也会影响IAP的“读取用户代码”功能对该扇区的读取。SPEDISx置1后禁止在ISP/IAP模式下对该扇区进行编程或擦除。但通过商用编程器并行模式的“全局擦除”命令仍可擦除。EDISx置1后禁止在ISP/IAP模式下擦除该扇区。只有商用编程器的“全局擦除”才能擦除它。组合效果如表105所示EDISx1是最高级别的保护连ISP/IAP擦除都禁止了。SPEDISx1可以防止误编程。MOVCDISx1主要用于防读取。硬件写使能WE标志这是一个额外的安全锁。当BOOTSTAT寄存器中的AWP位为1时WE标志由用户通过FMCON和FMDATA寄存器控制写入0x08/0x96置位写入0x0B/0x96清除。任何Flash写操作包括ISP和IAP前WE标志必须为1。ISP程序会自动设置它但如果你在用户程序中使用IAP必须在每次调用前手动置位WE标志。配置字节写保护CWPBOOTSTAT.6位。置1后将禁止对用户配置字节UCFG1, BOOTVEC, BOOTSTAT进行写操作防止关键配置被意外修改。只能通过Clear Configuration Protection (CCP)命令在ISP/IAP模式下如果DCCP位为0或商用编程器来清除。禁止CCP命令位DCCPBOOTSTAT.7位。这是“锁中锁”。一旦置位将永久禁用ISP和IAP模式下的CCP命令。这意味着如果CWP位被置1且DCCP也被置1那么你将无法再通过ISP/IAP修改任何配置字节只能求助于商用并行编程器。此操作不可逆务必谨慎安全策略建议产品开发调试阶段保持所有安全位为0未编程状态。量产时根据需求设置如果防止复制是首要任务编程MOVCDISx位。如果防止现场被恶意升级编程SPEDISx或EDISx位。如果希望完全锁定配置防止终端用户误操作编程CWP和DCCP位。编程DCCP前必须百分百确认代码和配置已最终定型。5.2 用户配置字节UCFG1与启动向量这两个配置在芯片上电复位时被读取决定了MCU的初始行为。UCFG1这是一个至关重要的字节位于Flash特定地址。它配置了看门狗使能WDTE是否启用看门狗复位。复位引脚使能RPEP1.5是作为复位引脚还是普通IO。掉电检测使能BOE是否启用掉电复位功能。振荡器选择FOSC[2:0]选择芯片的时钟源是外部晶振、内部RC还是外部时钟输入。这个配置错了芯片可能根本无法启动。配置方法通常由编程器在烧录HEX文件时一并烧写也可以通过ISP命令02子功能00来修改。BOOTVEC 和 BSB它们共同决定了复位后的启动地址。BSB0从常规地址0x0000启动。BSB1从BOOTVEC指定的地址启动。例如BOOTVEC0x1E则从0x1E00启动。应用场景可以实现双程序区备份。主程序在0x0000备份程序或Bootloader在0x1E00。通过一个标志位在RAM或EEPROM中可以在复位时动态决定跳转到哪里实现安全升级或故障恢复。5.3 IAP错误状态与问题排查实录调用IAP函数后必须检查状态。状态信息通过R7寄存器和进位标志CY返回。Table 98详细列出了错误位。OI (Operation Interrupted)操作被中断。这是最常见的IAP错误。原因就是在Flash操作期间发生了中断。解决方案在调用IAP前必须执行CLR EA或EA0;关闭总中断。SV (Security Violation)安全违规。试图编程或擦除一个被安全位保护的扇区。检查目标扇区的SPEDISx和EDISx位。HVE (High Voltage Error)高压错误。内部编程电压生成电路故障。通常与电源电压不稳或芯片本身有关。VE (Verify Error)验证错误。编程后读取的数据与写入的不符。可能原因1) 目标地址受MOVCDISx保护验证读取失败2) Flash单元已损坏。一个完整的、健壮的IAP调用应包含以下步骤bit safe_iap_call(unsigned char func_code, ...) { bit result 0; unsigned char ie_temp IE; // 保存中断使能寄存器 EA 0; // 关闭所有中断 IAP_KEY_LOC 0x96; // 设置密钥 // 根据func_code设置ACC, R3-R7, F1等寄存器... // ... (此处省略参数设置细节) iap_entry(); // 调用IAP // 检查结果 _asm { MOV result, C // 将进位标志存入result } if (result 0) { // CY0无错误 // 可以进一步检查R7中的详细状态位 if ((R7 0x01) ! 0) { // OI错误提示“操作被中断” } else if ((R7 0x02) ! 0) { // SV错误提示“安全保护冲突” } // ... 检查其他错误位 } IE ie_temp; // 恢复中断设置 return (result 0); // 返回成功与否 }5.4 典型问题排查清单在实际项目中ISP/IAP出问题无非围绕“连不上”、“写不进”、“读不出”、“跑不起来”几点。下面是我总结的排查清单问题现象可能原因排查步骤ISP无法连接无回显‘U’1. 硬件连接错误Tx/Rx接反、电平不匹配2. 复位序列时序不对3. 芯片已损坏4. 波特率不匹配首次‘U’字符1. 用万用表或示波器检查VDD、RST、PSEN引脚电平。2. 确认串口线是交叉的MCU的TxD接PC的RxD。3. 尝试降低波特率如4800发送‘U’。4. 检查芯片的VDD是否在允许范围内如2.4V-3.6V。ISP连接成功但擦除/编程失败1. 电源噪声大编程电压不稳2. Flash保护位如EDISx已设置3. 写使能WE标志未置位4. 配置字节保护CWP生效1. 在MCU的VDD和GND之间并联一个10uF电解电容和一个0.1uF瓷片电容。2. 尝试读取安全字节状态ISP命令03子功能08-0F。3. 检查BOOTSTAT寄存器的AWP和CWP位。IAP函数调用后程序跑飞1. 中断未关闭导致OI错误并可能破坏现场2. 堆栈设置不当IAP调用破坏堆栈3. IAP函数调用后未正确返回1.确保在IAP调用前后正确开关中断。2. 确保有足够的堆栈空间8051堆栈向上生长注意不要覆盖重要数据。3. 使用调试器单步跟踪观察调用IAP前后PC和SP指针的变化。通过IAP存储的数据重启后丢失1. 擦除后未等待足够时间就编程2. 编程的数据地址未对齐到页边界3. 数据存储区被链接器分配给了代码1. 在擦除和编程操作之间增加至少20ms延时。2. 确保编程地址是64字节页大小的整数倍。3. 检查链接器映射文件.M51确认你使用的数据存储区地址不在程序CODE范围内。启用安全位后无法再次编程1. 设置了EDISx且未使用商用编程器2. 设置了DCCP禁用了ISP/IAP的CCP命令1. 如果只设置了SPEDISx可用商用编程器“全局擦除”后恢复。2. 如果设置了DCCP则ISP/IAP模式永久无法修改配置必须使用并行编程器。这是最终保护设计时需极度谨慎。最后分享一个我早期犯过的错误我在产品中使用了IAP来记录运行日志。一开始工作正常但设备运行几个月后日志区突然无法写入。排查后发现是Flash的擦写寿命到了标称10万次但实际在频繁写入下可能更早出现坏块。教训是对于需要频繁更新的数据一定要设计磨损均衡算法或者干脆使用专门的EEPROMP89LPC932A1内部有512字节EEPROM或外置Flash/FRAM。把Flash当EEPROM用一定要心中有“数”。