1. 项目概述
在嵌入式开发领域,尤其是针对那些需要长期部署在野外、工厂或用户家中的设备,固件升级一直是个让人头疼的问题。想象一下,一个智能电表或者工业传感器,出厂后发现了一个软件bug,或者需要增加新功能,难道要把成千上万的设备全部拆下来,用昂贵的编程器重新烧录一遍吗?这显然不现实。因此, 在应用编程 技术应运而生,它让微控制器自己具备了“在线手术”的能力,能在不中断核心业务逻辑(或短暂中断后恢复)的情况下,通过软件指令更新自身的程序存储器。
今天要深入探讨的,是NXP(恩智浦)经典的8位微控制器 P89LPC952/954 的IAP实现机制。这颗芯片虽然现在看来有些年头,但其IAP的设计思想非常经典,涵盖了授权、保护、错误处理等完整的安全与可靠性考量,对于理解现代MCU的固件更新原理仍有很高的参考价值。很多新手在初次接触IAP时,往往只关注“如何调用那个神秘的函数”,却忽略了背后的安全门锁和异常处理机制,结果在实际产品中埋下了系统崩溃甚至被恶意篡改的隐患。这篇文章,我将结合手册内容和实际调试中踩过的坑,为你拆解P89LPC952/954 IAP的每一个关键细节,从原理到实操,从配置到排错,让你不仅能“跑通”代码,更能“吃透”设计,写出稳定可靠的固件更新程序。
2. IAP核心机制与硬件基础解析
2.1 什么是IAP?与ISP、ICP的区别
在深入P89LPC952/954的具体实现前,我们必须先厘清几个容易混淆的概念: IAP 、 ISP 和 ICP 。它们都是对微控制器内部Flash存储器进行编程的方法,但应用场景和实现方式截然不同。
- ICP :在电路编程。这是最“底层”和“强力”的模式。通常需要连接专用的编程器(如J-Link、ULINK)到芯片的SWD/JTAG接口。在这种模式下,编程器完全接管芯片的控制权,可以无视芯片内部任何软件设置的保护机制,对Flash进行擦写。它主要用于产品量产时的初次烧录,或者当芯片被锁死、IAP/ISP功能失效时的最后救援手段。
- ISP :在系统编程。芯片通过内置的Bootloader程序,在系统复位后、用户程序运行前的一个特殊阶段,通过某个通信接口(如UART)接收新的固件数据并写入Flash。P89LPC952/954的ISP功能通常通过特定的引脚电平组合(如拉低P1.5)在复位时触发。ISP依赖于芯片出厂时预置的、不可更改的Bootloader代码。
-
IAP
:在应用编程。这是本文的重点。IAP功能是由
用户应用程序自身
调用的一段固化的底层例程(在P89LPC952/954中,入口地址是
0xFF03)来实现的。这意味着,你的主程序正在运行的过程中,可以主动地、有选择地擦除和编程Flash的某些部分。IAP的核心价值在于“在线”和“动态”,它为远程无线升级、参数存储、自举程序更新等高级功能提供了可能。
简单来说, ICP是“外科医生从外部动刀”,ISP是“芯片启动时自助升级”,而IAP是“芯片在运行时给自己做手术” 。P89LPC952/954的IAP功能非常灵活,但与之相伴的是一套复杂的安全和保护机制,这也是其设计的精妙之处。
2.2 P89LPC952/954的Flash存储器架构与IAP入口
P89LPC952/954的Flash存储器并非一个可以随意读写的“黑盒”。它被组织成多个
扇区
,每个扇区是擦除的最小单位。编程则可以按页(Page)或字节进行(取决于具体命令)。IAP的所有操作,都是通过一个统一的软件接口
PGM_MTP
来完成的。
这个接口的入口地址固定在
0xFF03
。在C语言中,你可以通过函数指针来调用它。手册中给出的Keil C编译器示例非常经典:
short (*pgm_mtp) (void) = 0xFF00; /* 注意:实际调用地址是0xFF03,但函数指针指向0xFF00 */
这里有一个
极易出错
的细节:为什么函数指针指向
0xFF00
,而不是
0xFF03
?这是因为对于8051架构,C语言函数指针指向的是函数代码的起始地址。而
0xFF03
是
PGM_MTP
例程的入口点,但该例程的前几个字节可能包含一些跳转或初始化指令。编译器生成的调用代码(如
LCALL
指令)需要知道函数的确切开始地址。在P89LPC952/954的上下文中,将指针设为
0xFF00
,然后通过
(*pgm_mtp)()
调用,编译器会生成一个对
0xFF00
的调用指令,而芯片硬件或固化代码会正确处理这个调用,最终跳转到
0xFF03
执行真正的IAP例程。
这是一个与编译器、芯片硬件紧密相关的约定,不同编译器或不同系列的芯片可能不同,务必参考具体的数据手册和编译器手册。
IAP的功能选择,不是通过传递参数给这个函数,而是通过预先设置好微控制器的特定寄存器(主要是累加器ACC和R3-R7寄存器)来实现的。这很像一个通过寄存器传递参数的“软件中断”。调用
pgm_mtp()
后,CPU会暂停执行你的应用程序,跳转到内部的固化代码执行擦写操作,完成后返回,并设置相应的状态(通常通过进位标志C和R7寄存器)。
3. IAP操作的三重安全锁:授权、写使能与配置保护
IAP功能强大,但也危险。一次错误的写操作可能导致程序跑飞,系统崩溃。因此,P89LPC952/954设计了多层保护机制,像三道安全锁,防止意外或恶意的Flash修改。
3.1 第一道锁:IAP授权密钥
这是最直接的一道锁。任何试图
写入或擦除
代码存储器的IAP调用(例如编程用户代码页、擦除扇区等),在调用
PGM_MTP
之前,
必须
先设置一个授权密钥。
具体操作是向RAM地址
0xFF
写入固定值
0x96
。
#include <ABSACC.H> /* 启用绝对地址访问 */
#define key DBYTE[0xFF] /* 强制key变量位于地址0xFF */
short (*pgm_mtp) (void) = 0xFF00;
...
key = 0x96; /* 设置授权密钥 */
result = pgm_mtp(); /* 执行IAP函数调用 */
关键细节与避坑指南:
-
每次调用都需授权
:手册明确说明,IAP例程在处理完一次调用后,会自动清除这个密钥。这意味着,如果你的代码需要连续进行多次写/擦除操作(比如擦除一个扇区后写入多页数据),
必须在每次调用
pgm_mtp()前,都重新执行key = 0x96;。忘记这一步是导致IAP调用失败(触发芯片复位)的常见原因。 -
地址的绝对性
:
0xFF这个地址是芯片硬件规定的,不能更改。使用DBYTE[0xFF]或类似的方式是为了确保编译器将变量精确地定位到这个地址。如果只是定义一个普通变量并赋值,编译器可能会将其分配到其他地址,导致授权失败。 - 复位惩罚 :如果调用了需要密钥的IAP函数但密钥无效或未设置,微控制器不会简单地返回一个错误码,而是会 直接执行一个硬件复位 。这是一个非常严厉的保护措施,旨在立即终止可能非法的操作。在调试时,如果你的程序在调用IAP后莫名复位,首先就要检查密钥设置是否正确、是否在每次调用前都设置了。
3.2 第二道锁:硬件写使能标志
即使通过了密钥认证,芯片还有第二道硬件开关:
写使能标志
。这是一个内部的标志位,其状态由
BOOTSTAT.7
(AWE位)和用户命令共同控制。
-
AWE位的作用 :
-
如果
AWE = 0,则内部的写使能(WE)标志被 强制置位 ,写操作总是被允许。这降低了安全性,但简化了操作。 -
如果
AWE = 1,则WE标志可以由用户通过软件命令来控制,实现了更灵活的权限管理。
-
如果
-
如何控制WE标志 :
-
置位WE(允许写)
:向
FMCON寄存器写入0x08(Set Write Enable命令),然后立即向FMDATA寄存器写入密钥0x96。FMCON = 0x08; FMDATA = 0x96; -
清除WE(禁止写)
:向
FMCON寄存器写入0x0B(Clear Write Enable命令),然后立即向FMDATA写入0x96,或者直接触发一个芯片复位。FMCON = 0x0B; FMDATA = 0x96;
-
置位WE(允许写)
:向
实操心得:
在典型的IAP应用中,我们通常会将
AWE
位编程为1,以启用软件写使能控制。那么,在每次执行IAP写/擦除操作前,流程就变成了:
-
设置授权密钥 (
key = 0x96)。 -
发送Set Write Enable命令 (
FMCON=0x08; FMDATA=0x96;)。 - 设置IAP调用参数(ACC, R3-R7)。
-
调用
pgm_mtp()。 - (可选)发送Clear Write Enable命令以立即禁用写操作,增强安全性。
手册也提到,芯片的ISP功能会在调用IAP例程前自动设置WE标志,并在写操作后自动清除它。但 在纯IAP模式下,这个责任完全交给了用户程序 。忘记设置WE是另一个常见的IAP操作无声失败的原因(操作不报错,但Flash内容没有变化)。
3.3 第三道锁:配置字节写保护
Flash中有一部分特殊的区域,叫做
配置字节
,包括
UCFG1
(用户配置字节1)、
BOOTVEC
(引导向量)和
BOOTSTAT
(引导状态)本身。这些字节决定了芯片的基础行为,如时钟源、看门狗、复位引脚模式等,一旦被意外修改,可能导致芯片无法启动。
因此,芯片提供了专门的写保护位
CWP
(位于
BOOTSTAT.6
):
-
CWP = 1:禁止对配置字节进行写操作。 -
CWP = 0:允许对配置字节进行写操作。
这个保护同时作用于ISP和IAP模式。更复杂的是,还存在一个
DCCP
位(
BOOTSTAT.7
),当它被置1时,会在ISP/IAP模式下
禁用
清除配置保护(CCP)命令。这意味着,如果
DCCP=1
且
CWP=1
,那么在ISP/IAP模式下你将无法再修改配置字节,只能通过ICP或并行编程器来解锁。这是一个“破釜沉舟”式的保护,用于产品发布后彻底锁定关键配置。
配置保护操作流程示例(假设需要修改UCFG1):
-
检查
CWP位。如果为1,需先清除。 -
通过IAP“Misc. Write”功能(ACC=0x02),向寄存器地址
0x10(对应CCP命令)写入任意数据(配合密钥),即可清除CWP位。 注意 :此操作可能受DCCP位限制。 -
CWP清除后,再次使用“Misc. Write”功能,向寄存器地址0x00(对应UCFG1)写入新的配置值。 -
如果需要,可以再次使用“Misc. Write”功能向地址
0x03(对应BOOTSTAT)写入数据,将CWP位置1,重新锁住配置。
4. IAP函数调用详解与参数设置
理解了安全机制,我们来看武器库本身——IAP提供的各种函数调用。所有调用都通过设置ACC寄存器来选择功能,其他参数通过R3-R7传递。下表是核心函数的摘要:
| IAP功能 (ACC值) | 描述 | 是否需要密钥 | 关键输入参数 | 返回参数 |
|---|---|---|---|---|
| 0x00: 编程用户代码页 | 将RAM缓冲区中的数据写入Flash的一页。 | 是 | R3: 字节数; R4,R5: 页地址(高,低); R7: RAM数据缓冲区指针; F1: 0=使用IDATA | R7: 状态; C: 错误标志 |
| 0x01: 读取版本ID | 读取IAP固件的版本号。 | 否 | 无 | R7: 版本ID |
| 0x02: 杂项写入 | 写配置字节、安全字节或执行CCP命令。 | 是 | R5: 要写入的数据; R7: 寄存器地址(见手册表) | R7: 状态; C: 错误标志 |
| 0x03: 杂项读取 | 读配置字节、安全字节或设备ID。 | 否 | R7: 寄存器地址 | R7: 数据/状态; C: 错误标志 |
| 0x04: 擦除扇区/页 | 擦除指定的一个扇区或一页。 | 是 | R4,R5: 地址(高,低); R7: 0=擦页,1=擦扇区 | R7: 状态; C: 错误标志 |
| 0x05: 读取扇区CRC | 计算并返回指定扇区的32位CRC值。 | 否 | R7: 扇区地址 | R4-R7: CRC值; C: 错误标志 |
| 0x06: 读取全局CRC | 计算并返回整个用户代码区的32位CRC值。 | 否 | 无 | R4-R7: CRC值; C: 错误标志 |
| 0x07: 读取用户代码 | 从指定地址读取一个字节的用户代码。 | 否 | R4,R5: 地址(高,低) | R7: 读取的数据 |
4.1 实战:编程用户代码页(ACC=0x00)
这是最常用的功能,用于将新的固件数据写入Flash。假设我们要将
idata
区中
buffer
数组的64字节数据,写入到Flash地址
0x1000
开始的页中。
#include <absacc.h> /* 用于绝对地址访问 */
#include <reg52.h> /* 包含特殊功能寄存器定义 */
#define IAP_KEY_ADDR 0xFF
#define IAP_ENTRY_POINT 0xFF00
/* 声明IAP函数指针 */
short (*iap_entry)(void) = IAP_ENTRY_POINT;
/* 在idata区定义数据缓冲区 */
unsigned char idata buffer[64];
/* 设置授权密钥的宏 */
#define SET_IAP_KEY() (DBYTE[IAP_KEY_ADDR] = 0x96)
/* 设置写使能标志的函数 */
void flash_write_enable(void) {
FMCON = 0x08; /* Set Write Enable 命令 */
FMDATA = 0x96; /* 命令密钥 */
/* 可选:添加短暂延时或检查 */
}
/* 编程一页Flash的函数 */
unsigned char program_flash_page(unsigned int page_addr, unsigned char bytes_to_program, unsigned char *data_ptr) {
unsigned char status;
/* 1. 设置授权密钥 */
SET_IAP_KEY();
/* 2. 设置写使能标志 (如果AWE=1) */
flash_write_enable();
/* 3. 设置IAP调用参数 */
ACC = 0x00; /* 功能:编程用户代码页 */
R3 = bytes_to_program; /* 要编程的字节数 */
R4 = (page_addr >> 8) & 0xFF; /* 页地址高字节 */
R5 = page_addr & 0xFF; /* 页地址低字节 */
R7 = (unsigned char)data_ptr; /* 数据缓冲区在IDATA中的地址 */
F1 = 0; /* 使用IDATA区 */
/* 4. 调用IAP例程 */
status = (*iap_entry)();
/* 5. 检查操作结果 */
if (status & 0x80) { /* 通过检查R7的最高位或进位标志C来判断错误,具体需参考手册 */
/* 处理错误 */
return status; /* 返回错误状态 */
}
return 0; /* 成功 */
}
void main_example(void) {
unsigned char result;
/* ... 填充buffer数据 ... */
/* 编程Flash地址0x1000处的一页(64字节) */
result = program_flash_page(0x1000, 64, buffer);
if (result != 0) {
/* 编程失败处理 */
}
}
关键点解析:
-
地址对齐
:虽然手册没说必须页对齐,但Flash编程通常有页大小限制(例如64字节/页)。
page_addr最好是页大小的整数倍。 -
缓冲区位置
:
R7传递的是缓冲区在RAM中的地址。示例中使用了idata(内部RAM,地址0x00-0xFF),因为F1=0指定了使用IDATA。如果你使用xdata(外部RAM),则需要设置F1=1,并且R7的含义可能不同(可能指向一个地址指针),这需要仔细查阅编译器关于指针到寄存器传递的约定。 -
参数设置顺序
:务必在调用
iap_entry()之前 设置好所有寄存器(ACC, R3, R4, R5, R7, F1)。编译器优化可能会打乱代码顺序,对于这种底层硬件操作,建议将参数设置和函数调用放在一个#pragma禁用优化的块中,或者使用volatile关键字确保顺序。 -
错误检查
:调用后必须检查状态。状态信息在
R7寄存器中,同时进位标志C也会被设置。你需要根据R7的值查询表103来确定具体错误类型。
4.2 实战:擦除扇区(ACC=0x04)
擦除是编程的前提。Flash只能从1(已擦除)写成0,或从0写成0,不能从0写成1。因此,在写入新数据前,必须将目标区域擦除为全1(0xFF)。
unsigned char erase_flash_sector(unsigned int sector_addr) {
unsigned char status;
SET_IAP_KEY(); /* 擦除也需要密钥 */
flash_write_enable(); /* 擦除也需要写使能 */
ACC = 0x04; /* 功能:擦除 */
R4 = (sector_addr >> 8) & 0xFF; /* 扇区地址高字节 */
R5 = sector_addr & 0xFF; /* 扇区地址低字节 */
R7 = 0x01; /* 0x01 = 擦除扇区,0x00 = 擦除页 */
status = (*iap_entry)();
/* ... 错误检查 ... */
return status;
}
重要提示 :擦除操作是以 扇区 为最小单位的。你需要查阅P89LPC952/954的内存映射图,确定每个扇区的起始地址和大小。错误的地址可能导致擦除不该擦的区域,破坏程序。
5. IAP错误处理与系统稳定性保障
IAP操作是在运行中的程序里对存储程序的Flash进行修改,这是一个非常“脆弱”的过程。完善错误处理是保证系统鲁棒性的关键。P89LPC952/954的IAP例程提供了详细的错误状态反馈(见表103)。
5.1 错误状态位详解
调用IAP函数后,如果进位标志
C
被置1,说明发生了错误。此时
R7
寄存器中保存的错误状态字各位含义如下:
| 位 | 标志 | 描述与原因分析 |
|---|---|---|
| 0 | OI (操作中断) | 最常遇到 的错误。在编程/擦除/CRC计算周期中发生了中断。IAP例程会中止当前操作以响应中断,导致操作未完成。 |
| 1 | SV (安全违规) |
试图编程或擦除一个被安全位保护的扇区。例如,该扇区的
SPEDISx
或
EDISx
位被设置。
|
| 2 | HVE (高电压错误) | Flash编程所需的高压生成电路出现故障。这属于硬件问题,可能芯片损坏或电源不稳。 |
| 3 | VE (验证错误) |
编程完成后,读取刚写入的地址,发现数据与预期不符。
注意
:尝试编程受
MOVCDISx
保护的扇区时,即使编程成功,也会在验证阶段触发此错误。
|
5.2 中断处理策略
OI错误是IAP编程中最需要妥善处理的问题 。因为Flash在编程/擦除期间,CPU不能从中读取指令,所以一旦发生中断,IAP操作必须中止。
解决方案有以下几种:
-
全局关闭中断(最简单粗暴) :
EA = 0; /* 8051中关闭所有中断 */ status = (*iap_entry)(); EA = 1; /* 恢复中断 */优点 :实现简单,绝对不会有OI错误。 缺点 :在擦写大扇区或编程多页数据时(可能需要几毫秒到几十毫秒),系统无法响应任何中断(包括定时器、串口等),可能导致通信超时、看门狗复位等问题。 不推荐用于对实时性有要求的系统。
-
精心设计IAP任务上下文 :
- 将IAP操作(如固件更新)放在一个独立的、优先级最低的任务中。
- 在进入IAP关键操作(单次擦/写)前,临时提升任务优先级或关闭相关中断,操作完成后立即恢复。
- 确保单次IAP函数调用的执行时间短于最短的中断间隔。例如,编程一页(64字节)的时间是确定的,如果这个时间小于系统定时器中断的周期,那么可以在关闭定时器中断的情况下安全完成单页编程。
-
允许中断并重试(最稳健) : 这是最复杂的策略,但能兼顾实时性和可靠性。思路是:
- 允许中断发生。
-
每次IAP调用后,检查
C标志和OI位。 -
如果发生
OI错误,则 重复执行上一次操作 。因为中断导致操作被中止,Flash可能处于不确定状态,必须重做。
#define MAX_RETRY 3 unsigned char iap_program_with_retry(...) { unsigned char status, retry = 0; do { SET_IAP_KEY(); flash_write_enable(); /* ... 设置参数 ... */ status = (*iap_entry)(); retry++; } while ((status & 0x01) && (retry < MAX_RETRY)); /* 检查OI位并重试 */ if (retry >= MAX_RETRY) { /* 重试多次仍失败,转入严重错误处理 */ } return status; }注意 :重试时,必须 重新设置授权密钥和写使能 ,因为前一次调用后它们已被清除。
5.3 安全位冲突处理
如果遇到
SV
错误,说明你的操作触发了安全保护。你需要检查目标扇区的安全字节(
SECx
)。
-
MOVCDISx:禁止MOVC指令读取该扇区。 这会影响IAP编程后的验证阶段 ,导致VE错误。如果你的应用不需要读取该扇区的代码,可以忽略这个VE错误。 -
SPEDISx:禁止在ISP/IAP模式下对该扇区进行编程或页擦除。但 扇区擦除和全局擦除(商用编程器)仍被允许 。如果你需要在IAP中更新这个扇区,必须在更新流程开始前,通过商用编程器或ICP模式将其擦除并解除此保护。 -
EDISx:禁止在ISP/IAP模式下擦除该扇区。 只有全局擦除(商用编程器)可以擦除它 。这是最高级别的保护,一旦设置,该扇区在IAP模式下就变成“只读”了。
产品规划建议
:在Flash内存布局规划时,就将需要IAP更新的区域(如Bootloader、应用程序A/B区)放在独立的安全扇区中,并将这些扇区的
SPEDISx
和
EDISx
位保持为0(未编程)。将永远不变的底层库、加密密钥等放在设置了
EDISx
的扇区,防止被意外或恶意修改。
6. 完整IAP固件更新流程设计与实操
理论说了这么多,我们来看一个完整的、用于远程升级的IAP流程设计。假设我们有一个典型的Bootloader+Application的双区结构。
6.1 内存布局规划
-
扇区0
:Bootloader程序。负责检查更新标志、与上位机通信、接收新固件、调用IAP写入应用程序区。这个扇区通常较小且稳定,可以设置较强的保护(如
EDISx)。 - 扇区1~N :应用程序区(Application)。这是需要更新的主体。
- Flash末尾某个扇区 :参数区。存储更新标志、新固件CRC、版本号等。
6.2 Bootloader中的IAP更新流程
- 初始化 :Bootloader启动后,检查参数区的更新标志位。
- 通信准备 :如果标志位有效,初始化通信模块(如UART),通知上位机准备接收数据。
- 接收与校验 :循环接收固件数据包,存入RAM缓冲区,并计算CRC。
-
擦除目标区
:在写入新固件前,先擦除应用程序区的所有相关扇区。
for (addr = APP_START_ADDR; addr < APP_END_ADDR; addr += SECTOR_SIZE) { do { status = erase_flash_sector(addr); } while (status & 0x01); /* 如果因中断失败则重试擦除 */ if (status & 0xFE) { /* 其他错误(非OI)*/ /* 报告错误并中止 */ send_error_to_host(SECURITY_OR_HV_ERROR); break; } } -
编程Flash
:将RAM缓冲区中的数据按页编程到Flash。
for (offset = 0; offset < total_firmware_size; offset += PAGE_SIZE) { /* 从通信接口填充buffer */ fill_buffer_from_uart(buffer, PAGE_SIZE); page_addr = APP_START_ADDR + offset; do { status = program_flash_page(page_addr, PAGE_SIZE, buffer); } while (status & 0x01); /* 重试因中断失败的操作 */ if (status & 0x02) { /* SV错误 */ /* 安全违规,目标扇区可能被保护,更新无法继续 */ send_error_to_host(SECURITY_VIOLATION); return; } if (status & 0x04) { /* HVE错误 */ /* 硬件故障,风险高 */ enter_safe_mode(); return; } /* VE错误可能由MOVCDIS引起,如果确认该扇区允许MOVC,则需视为失败 */ } - 最终验证 :编程完成后,计算整个应用程序区的CRC,与接收到的CRC校验和比对。
- 更新标志 :验证通过后,清除参数区的更新标志,写入新版本号。
- 跳转应用程序 :软件复位或直接设置PC指针,跳转到应用程序起始地址执行。
6.3 应用程序中的IAP调用注意事项
如果应用程序本身也需要使用IAP(例如,存储运行参数到某个未用的Flash扇区),需要格外小心:
- 中断上下文 :避免在中断服务程序中调用IAP函数。IAP操作耗时且可能被中断打断,在ISR中处理重试逻辑会非常复杂。
- 代码位置 : 绝对不要试图擦写当前正在执行代码所在的扇区 。这会导致立即崩溃。如果需要更新自身,必须采用双区切换(A/B区)的方式,或者将IAP相关代码全部放在RAM中执行(实现复杂)。
- 栈空间 :确保有足够的栈空间。IAP例程内部可能会使用一些栈。
- 看门狗 :如果开启了看门狗,在长时间的IAP操作(如擦除大扇区)前,可能需要临时喂狗或禁用看门狗,防止超时复位。但要注意禁用看门狗的安全性。
7. 常见问题排查与调试技巧
在实际开发中,IAP相关的问题往往比较隐蔽。这里分享一些排查思路和调试技巧。
7.1 问题速查表
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 调用IAP后芯片复位 |
1. 授权密钥未设置或设置错误。
2. 写使能未开启(AWE=1时)。 3. 目标地址非法(如写入了Bootloader保护区)。 |
1. 检查
0xFF
地址在调用前是否被写入
0x96
。
2. 检查
BOOTSTAT.7
(AWE)和
FMCON/FMDATA
操作序列。
3. 核对内存映射,确认目标地址可写。 |
| IAP调用后程序跑飞 |
1. 寄存器参数设置错误,导致IAP例程访问了非法内存。
2. 中断在IAP操作期间发生,导致状态混乱。 3. 栈溢出。 |
1. 单步调试,检查ACC、R3-R7、F1的值是否正确。
2. 在IAP调用前后关闭中断测试。 3. 检查栈指针SP,确保未覆盖重要数据。 |
| Flash内容未改变 |
1. 写使能未成功设置(AWE=1时)。
2. 目标扇区被安全位保护(
SPEDISx=1
)。
3. 编程/擦除操作因OI错误而中止,且未重试。 4. 数据缓冲区地址(R7)或F1位设置错误。 |
1. 确认
FMCON=0x08; FMDATA=0x96;
执行成功。
2. 读取目标扇区的安全字节(
SECx
)确认。
3. 检查IAP返回状态,确认是否发生了OI错误。 4. 确认使用的是
idata
还是
xdata
,以及指针传递是否正确。
|
| 能擦除但不能编程 |
1. 扇区未正确擦除(必须为全0xFF才能编程)。
2. 编程的数据本身有0位(Flash只能写0,不能写1)。 3. 页地址或字节数参数错误。 |
1. 擦除后,先读取目标地址,确认是否为0xFF。
2. 检查要写入的数据缓冲区内容。 3. 确认页地址对齐,字节数不超过页大小。 |
| CRC计算错误 |
1. 计算CRC的扇区包含受
MOVCDISx
保护的区域。
2. 在CRC计算过程中发生中断(OI错误)。 |
1. 检查相关扇区的
MOVCDISx
位。
2. 计算CRC时关闭中断。 |
7.2 调试技巧
-
软件仿真
:在Keil uVision等IDE中,可以利用软件仿真器来调试IAP流程。你可以观察和修改
0xFF地址的内容、FMCON/FMDATA寄存器,以及ACC、R3-R7等。虽然无法真实模拟Flash擦写,但可以验证你的参数设置和调用流程是否正确。 - 硬件调试器 :使用J-Link等调试器配合支持Flash断点功能的IDE。你可以在IAP调用前后设置断点,观察内存和寄存器的变化。 注意 :在单步执行IAP调用时,由于CPU会跳转到内部固化代码,调试器可能会失去控制,需要手动暂停或设置断点在IAP调用返回后的地址。
- 串口日志 :在关键步骤(设置密钥、设置写使能、调用IAP前、调用IAP后)通过串口打印状态信息到PC。这是最实用的调试手段。例如,打印出ACC、目标地址、返回的R7值等。
- LED或IO口指示 :用几个GPIO引脚连接LED。在程序不同阶段点亮不同的LED,可以直观判断程序执行到哪一步卡住了。比如,进入IAP函数前点亮LED1,成功返回后点亮LED2,出错后闪烁LED3。
- 读取验证 :编程完成后,立刻使用IAP的“Read User Code”功能(ACC=0x07)读回刚写入的数据,与原始数据对比。这是验证编程操作是否成功的最直接方法。
7.3 一个真实的“坑”:编译器优化与寄存器设置
这是我早期遇到的一个棘手问题。我的IAP函数看起来完全正确,但就是无法成功编程。最后发现是编译器优化导致的。我的代码类似这样:
void set_iap_params(unsigned char func, unsigned int addr, ...) {
ACC = func;
R4 = addr >> 8;
R5 = addr & 0xFF;
// ... 设置其他参数
}
...
set_iap_params(0x00, page_addr, ...);
(*iap_entry)();
问题在于,编译器认为
ACC
、
R4
、
R5
是普通变量,
set_iap_params
函数调用和
iap_entry
调用之间没有依赖关系,可能会进行重排序优化,导致
(*iap_entry)()
执行时,寄存器还未被正确设置。
解决方案 :
-
将参数设置和函数调用放在同一个函数内,并声明该函数为
#pragma NOAREGS(禁止使用绝对寄存器)或使用volatile关键字。 -
更稳妥的方法是使用内联汇编来确保操作的原子性和顺序:
当然,这需要编译器支持内联汇编,并且要处理好与C代码的接口。#pragma ASM MOV ACC, #0x00 ; 功能号 MOV R3, #64 ; 字节数 ... ; 设置其他寄存器 LCALL 0FF03h ; 调用IAP入口 #pragma ENDASM
P89LPC952/954的IAP功能是一把双刃剑,它赋予了产品强大的现场更新能力,但也对开发者的细心和严谨提出了更高要求。理解其三重保护机制(密钥、写使能、配置保护),熟练掌握各个功能调用的参数设置,并建立完善的错误处理和重试逻辑,是成功应用IAP的关键。希望这篇结合了手册原理和实战经验的解析,能帮助你在下一次嵌入式固件升级项目中,更加得心应手。

4318


被折叠的 条评论
为什么被折叠?



