本文参考了STC的官方例程,根据官方例程来进行讲解IAP的详细操作
起因
STC8H8K64U 这类单片机做产品,出厂后难免要修 BUG、加功能,设备装在现场不可能每次都拆机接 ISP 线烧录,所以需要串口 IAP(在线升级)。《STC8系列单片机IAP更新方法》一篇是自己规划地址、手写汇编搬运;这一篇走官方路线:利用 STC8H 预留的用户 ISP 区,把升级引导代码放进官方指定的位置,用官方例程的串口协议完成升级。
需求
- 只用串口就能升级用户程序,不拆机、不接烧录器
- 升级代码本身不能被升级动作破坏(保住引导区)
- 升级过程有校验,数据错了不能把坏固件写进去
技术实现原理
为什么要有“用户 ISP 区”
8051 内核从 Flash 取指执行,擦写正在运行的程序区必然跑飞,所以“升级代码”与“被升级代码”必须分区存放。STC8H8K64U 在 Flash 高位固定划出用户 ISP 区(0xFA00 起的 512 字节),官方例程把最小引导放在这里;同时用 MAX_SIZE=118 个扇区(118×512≈60KB,即 0xEC00 以下)封顶擦写范围,0xEC00 之后放 ISP 相关函数的区域永远不会被升级动作碰到——双重保险。
升级是怎么被触发的(7F 唤醒序列)
用户程序平时正常运行,串口中断里每收到一个字节就喂给 Isp_Check。上位机想升级时连续发 16 个 0x7F:只要中间夹了别的字节,计数器清零重来——这既是“唤醒密码”,也天然抗波特率匹配阶段的乱码误触发。计数到 16 就关中断、重置寄存器组和栈指针,跳进 0xFA00 的 ISP 代码,回发 5A 69 表示就绪。
升级协议与可靠性设计
之后的对话是命令式的:5A 69 命令头 + 6 字节命令块(cmd/addr/len + 校验和)。校验和设计成“所有字节累加应为 0”,判断 sum != 0 一步到位;擦除按 512 字节扇区循环触发,编程按 64 字节一批,写完立刻读回比对(CBYTE[j] 按地址直读程序存储器),坏一批马上发现。每个耗时循环里都喂狗(WDT_CONTR = 0x17)——擦写耗时远超看门狗周期,不喂狗会在升级中途被复位。IAP_TPS = 23 是与 22.1184MHz 主频配套的 IAP 等待时间参数,换主频要跟着改。
一、内部FLASH的规划
STC不同型号的单片机,内部FLASH的规划大不相同,详细的使用规格书都有介绍,以下我只截图规格书当中我需要的部分进行讲解
1、首先是规格书当中STC8H8K64单片机的内部使用空间划分

FLASH 空间中,从地址 0000H 开始的连续 62.5K 字节的空间为用户程序区。当满足特定的下载条 件时,需要用户将 PC 跳转到用户 ISP 程序区,此时可对用户程序区进行擦除和改写,以达到更新用户 程序的目的。(只有0.5K的ISP空间所以实际使用代码是参考了官方例程的)
二、程序的基本框架
实际使用的程序框架如图所示:

三、复位程序
以下是官方代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#include "reg51.h"
typedef unsigned char BYTE;
typedef unsigned int WORD;
typedef void (* FARPROC)(BYTE *);
#define FOSC 22118400L //系统时钟频率
#define BAUD (65536 - FOSC/4/115200) //定义串口波特率
/* 定义串口相关SFR */
sfr AUXR = 0x8E; //波特率发生器控制寄存器
sfr BRT = 0x9C; //波特率发生器定时器
sfr T2H = 0xd6;
sfr T2L = 0xd7;
void (*Isp_Check)(BYTE *) = 0xfa00; //定义指针函数
BYTE cnt7f; //Isp_Check内部使用的变量
void uart() interrupt 4 using 1 //串口中断服务程序
{
if (TI) TI = 0; //发送完成中断
if (RI) //接收完成中断
{
Isp_Check(&cnt7f); //ISP检测
RI = 0; //清接收完成标志
}
}
void main()
{
SCON = (SCON & 0x3f) | 0x50;
T2L = BAUD;
T2H = BAUD>>8;
AUXR |= 0x15;
ES = 1; //使能串口中断
EA = 1; //打开全局中断开关
while (1)
{
P0++;
}
}
|
官方代码复位的主要用到以下三段代码用来复位
1
2
3
|
typedef void (* FARPROC)(BYTE *);
void (*Isp_Check)(BYTE *) = 0xFA00; //定义指针函数
|
这是重定义了一下指针函数使其指向地址0XFA00,也就是用户ISP区,实际使用的时候只需要满足条件后调用
即可进入用户ISP区,这里操作较为简单不做其他介绍。注意这个钩子是每收到一个字节就调用一次,开销很小;真正的唤醒检测逻辑全部在 ISP 区的代码里完成,用户程序只需要提供这个入口。
PS:规格书上也标注了调用 IAP_CONTR=0x60 即可软件复位,然后通过ISP的方式进行升级,这种也是可以的,就和重新上电烧程序的方式一致,这里不多做介绍,感兴趣的可以去官网看看有例程介绍。补充一点原理:IAP_CONTR 的软件复位有 0x20/0x60 两个常用值,区别在复位后从哪启动——0x20(例程 Isp_SoftReset 用的)复位后回用户程序区,0x60 复位后先进系统 ISP 监控区,等同断电冷启进入烧录模式,上位机直接当新芯片烧。
四、IAP升级代码
以下是官方例程,可以根据自己使用来修改协议(笔者是自己更改了串口、协议来进行升级,这个看个人需求)
absacc.h文件
内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
#ifndef __ABSACC_H__
#define __ABSACC_H__
#define CBYTE ((unsigned char volatile code *) 0)
#define DBYTE ((unsigned char volatile data *) 0)
#if !defined (__CX2__)
#define PBYTE ((unsigned char volatile pdata *) 0)
#endif
#define XBYTE ((unsigned char volatile xdata *) 0)
#define CWORD ((unsigned int volatile code *) 0)
#define DWORD ((unsigned int volatile data *) 0)
#if !defined (__CX2__)
#define PWORD ((unsigned int volatile pdata *) 0)
#endif
#define XWORD ((unsigned int volatile xdata *) 0)
#if defined (__CX51__) || defined (__CX2__)
#define FVAR(object, addr) (*((object volatile far *) (addr)))
#define FARRAY(object, base) ((object volatile far *) (base))
#define FCVAR(object, addr) (*((object const far *) (addr)))
#define FCARRAY(object, base) ((object const far *) (base))
#else
#define FVAR(object, addr) (*((object volatile far *) ((addr)+0x10000L)))
#define FCVAR(object, addr) (*((object const far *) ((addr)+0x810000L)))
#define FARRAY(object, base) ((object volatile far *) ((base)+0x10000L))
#define FCARRAY(object, base) ((object const far *) ((base)+0x810000L))
#endif
#if defined (__CX2__)
#define HBYTE ((unsigned char volatile huge *) 0)
#define HWORD ((unsigned int volatile huge *) 0)
#define HVAR(object, addr) (*((object volatile huge *) (addr)))
#define HARRAY(object, base) ((object volatile huge *) (base))
#endif
#define CVAR(object, addr) (*((object volatile code *) (addr)))
#define CARRAY(object, base) ((object volatile code *) (base))
#define DVAR(object, addr) (*((object volatile data *) (addr)))
#define DARRAY(object, base) ((object volatile data *) (base))
#define XVAR(object, addr) (*((object volatile xdata *) (addr)))
#define XARRAY(object, base) ((object volatile xdata *) (base))
#endif
|
IAP主程序(这段代码编译后整体定位到 Flash 高位,见第五节)
阅读顺序建议:串口中断 uart → Isp_Check(7F 检测 + 命令主循环)→ case 0 擦除 / case 1 编程。absacc.h 里的 CBYTE 宏用来按地址直接读程序存储器,写完回读校验就靠它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
#include "reg51.h"
#include "absacc.h"
/*定义常数*/
#define FOSC 22118400L //系统时钟频率
#define BAUD (65536 - (FOSC/4/115200)) //定义串口波特率
#define MAX_SIZE 118 //用户程序最大的可用扇区数
#define ENABLE_IAP 0x80 //使能IAP功能
/* 定义串口相关SFR */
sfr AUXR = 0x8E; //波特率发生器控制寄存器
sfr WDT_CONTR = 0xC1; //看门狗控制寄存器
sfr IAP_DATA = 0xC2; //IAP数据寄存器
sfr IAP_ADDRH = 0xC3; //IAP高地址寄存器
sfr IAP_ADDRL = 0xC4; //IAP低地址寄存器
sfr IAP_CMD = 0xC5; //IAP命令寄存器
sfr IAP_TRIG = 0xC6; //IAP命令触发寄存器
sfr IAP_CONTR = 0xC7; //IAP控制寄存器
sfr IAP_TPS = 0xF5; //IAP等待时间控制寄存器
sfr T2H = 0xd6;
sfr T2L = 0xd7;
//?PR?_ISP_CHECK?ISP(0xEE00),?PR?ISP_SOFTRESET?ISP(0xEF38),?PR?_ISP_SENDUART?ISP(0xEF3C),?PR?ISP_RECVUART?ISP(0xEF49),?PR?_ISP_RECVBLOCK?ISP(0xEF5B),?C?LIB_CODE(0xEF72)
typedef unsigned char BYTE;
typedef unsigned int WORD;
typedef struct
{
BYTE cmd;
WORD addr;
WORD len;
BYTE chk;
} CBW;
void Isp_Check(BYTE *p);
BYTE Isp_RecvUart();
void Isp_RecvBlock(BYTE *p, BYTE n);
void Isp_SendUart(BYTE dat);
void Isp_SoftReset();
BYTE cnt7f; //Isp_Check内部使用的变量,接收7F的计数器,当连续接收到16次7F后进入ISP下载模式
CBW cbw; //串口命令块
BYTE sum; //校验和变量
BYTE buf[64]; //数据缓冲区
/*------------------------------------------------
串口中断服务程序
------------------------------------------------*/
void uart() interrupt 4
{
if (TI) TI = 0; //发送完成中断
if (RI) //接收完成中断
{
Isp_Check(&cnt7f); //ISP检测
RI = 0; //清接收完成标志
}
}
void main()
{
SCON = (SCON & 0x3f) | 0x50;
T2L = BAUD;
T2H = BAUD>>8;
AUXR |= 0x15;
ES = 1; //使能串口中断
EA = 1; //打开全局中断开关
while (1);
}
/*------------------------------------------------
串口ISP命令序列检测模块
------------------------------------------------*/
void Isp_Check(BYTE *p)
{
BYTE i;
WORD j;
if (SBUF != 0x7f) //检测串口数据是否为7F
{
*p = 0; //若不是7F,则清7F计数值
}
else
{
(*p)++; //若是7F,则7F计数值+1
if (*p >= 16) //判断7F是否已连续接收到16次
{ //若>=16次,则进入ISP下载模式
IE = 0; //关闭所有中断
PSW = 0; //ISP模块使用第0组寄存器
SP = 0x5f; //重置ISP模块的堆栈指针
RI = 0; //清除串口接收标志
TI = 1; //置串口发送标志
Isp_SendUart(0x5a); //返回5A 69到PC,表示ISP模块已准备就绪
Isp_SendUart(0x69);
while (1) //ISP下载模式,主循环
{
sum = 0; //清校验和值
if ((Isp_RecvUart() == 0x5a) && //判断命令头是否为5A,69
(Isp_RecvUart() == 0x69))
{
Isp_RecvBlock((BYTE *)&cbw, 6); //接收6字节的命令序列
if (sum != 0) //判断命令序列是否正确
{
Isp_SoftReset(); //若命令出错,则程序复位
}
switch (cbw.cmd)
{
case 0: //0号命令为擦除命令
IAP_ADDRL = 0; //从第0扇区开始擦除
IAP_ADDRH = 0;
IAP_CONTR = ENABLE_IAP; //使能IAP功能
IAP_TPS = 23;
IAP_CMD = 3; //擦除命令
if (cbw.len > MAX_SIZE) //判断擦除扇区数是否超出范围
{
cbw.len = MAX_SIZE;
}
while (cbw.len--) //判断是否擦除完成
{
WDT_CONTR = 0x17; //清看门狗
IAP_TRIG = 0x5a; //触发ISP命令
IAP_TRIG = 0xa5;
IAP_ADDRH += 2; //目标地址+512
}
Isp_SendUart(0); //正确返回
break;
case 1: //1号命令为编程命令
sum = 0; //清除校验和值
Isp_RecvBlock(buf, 64); //接收64字节的编程数据
Isp_RecvUart(); //接收校验和
if (sum != 0) //判断数据是否正确
{
Isp_SoftReset(); //若数据出错,则程序复位
}
IAP_CONTR = ENABLE_IAP; //使能IAP功能
IAP_TPS = 23;
IAP_CMD = 2; //编程命令
j = cbw.addr; //编程目标地址
for (i=0; i<64; i++) //编程64字节数据
{
WDT_CONTR = 0x17; //清看门狗
IAP_DATA = buf[i]; //将当前数据送IAP数据寄存器
IAP_ADDRL = j; //目标地址送IAP地址寄存器
IAP_ADDRH = j >> 8;
IAP_TRIG = 0x5a; //触发ISP命令
IAP_TRIG = 0xa5;
j++; //目标地址+1
}
j = cbw.addr; //校验目标地址
for (i=0; i<64; i++) //校验64字节数据
{
WDT_CONTR = 0x17; //清看门狗
if (buf[i] != CBYTE[j]) //源数据与目标数据进行比较
break; //不相等,则编程出错
j++; //校验下一个字节
}
Isp_SendUart(!(i == 64)); //校验成功返回0; 否则返回1
break;
default:
Isp_SoftReset(); //接收到非法命令时,复位系统
break;
}
}
}
}
}
}
/*------------------------------------------------
接收1字节串口数据
出口参数: ACC (接收到的数据)
------------------------------------------------*/
BYTE Isp_RecvUart()
{
BYTE dat;
while (!RI) //等待接收完成
{
WDT_CONTR = 0x17; //清看门狗
}
dat = SBUF; //读取串口数据
RI = 0; //清除标志
sum += dat; //计算校验和
return dat; //返回接收到的串口数据
}
/*------------------------------------------------
接收一块串口数据
入口参数: R0 (数据缓冲区地址)
R7 (缓冲区长度)
------------------------------------------------*/
void Isp_RecvBlock(BYTE *p, BYTE n)
{
while (n--) //检测长度
{
*p = Isp_RecvUart(); //接收1字节,并保存到缓冲区
p++; //缓冲区地址+1
}
}
/*------------------------------------------------
发送1字节串口数据
入口参数: ACC (待发送的数据)
------------------------------------------------*/
void Isp_SendUart(BYTE dat)
{
while (!TI) //等待前一个数据发送完成
{
WDT_CONTR = 0x17; //清看门狗
}
TI = 0; //清除标志
SBUF = dat; //发送当前数据
}
/*------------------------------------------------
软件复位
------------------------------------------------*/
void Isp_SoftReset()
{
IAP_CONTR = 0x20; //用户程序区复位
}
|
五、KEIL中定位IAP升级代码的方法
这是最后一步,这里函数定位结束后IAP就可以使用了,这里参考了官方文档,具体定位方法如下:
先说原理:Keil C51 把每个函数编译成独立的 CODE 段,段名格式是 ?PR?函数名?模块名(函数名前带下划线表示有参数传递);BL51 链接器的 Locate 页可以把指定段“钉”到绝对地址。所谓函数定位不是特殊魔法,就是告诉链接器“这几段不许自动摆放,按我给的地址放”。
下面以演示程序进行说明
演示程序中有ReadIAP、ProgramIAP和EraseIAP三个函数
最终目的是将这三个函数都定位到0x8000之后
第一步:新建一个项目“Demo”,并将源文件“Demo.C”添加到项目中

第二步:直接编译,并打开编译后生成的“Demo.M51”文件

从M51文件的“CODE MEMORY”信息中,可以看到3个函数的链接名称、链接地址和函数长度
ReadIAP的链接名称为“?PR?_READIAP?DEMO”,链接地址为“0003H”,长度为16H字节
ProgramIAP的链接名称为“?PR?_PROGRAMIAP?DEMO”,链接地址为“0019H”,长度为16H字节
EraseIAP的链接名称为“?PR?_ERASEIAP?DEMO”,链接地址为“0044H”,长度为14H字节
第三步:根据M51中函数的长度信息计算出各个函数重定位的地址,
1
2
3
4
5
|
ReadIAP的重定位的地址为0x8000
ProgramIAP的重定位的地址为0x8016
EraseIAP的重定位的地址为0x802C
|
第四步:打开项目选项中的“BL51 Locate”属性页


在上图的“Code”域中输入下列语句
1
|
“?PR?_READIAP?DEMO(0x8000), ?PR?_PROGRAMIAP?DEMO(0x8016), ?PR?_ERASEIAP?DEMO(0x802C)”
|
地址的计算方法就是“上一个函数的地址 + 它的长度”:0x8000 + 0x16 = 0x8016,0x8016 + 0x16 = 0x802C。代码一改动函数长度就变,定位地址要重新按 M51 算一遍。
踩坑记录与注意事项
- MAX_SIZE 是安全边界:擦除扇区数被 MAX_SIZE 封顶,扩大它之前先确认引导代码所在的区域不会被扫进去,否则升级动作会把自己的引导擦掉,变砖;
- IAP_TPS 与主频绑定:例程按 22.1184MHz 配 23,换主频按手册公式重算,配错读写不稳;
- 16 个 0x7F 必须连续:上位机发唤醒序列时中间不能夹别的字节,程序里任何杂散发送都会让计数清零,现象是“怎么发都进不了升级模式”;
- 升级循环里的喂狗别删:擦写耗时超过看门狗周期,删掉 WDT_CONTR = 0x17 会在升级中途被复位,前功尽弃;
- 函数改一行,定位重一遍:函数长度变了 M51 里的地址就得重算,段地址相互覆盖会链接报错甚至跑飞;定位多个函数时按“地址+长度”逐个排;
- Code 域里的标点用半角:段名列表的引号、逗号、括号全用半角,混进全角标点会静默无效;
- 校验和约定:协议把校验设计成“整块累加为 0”,改协议时保持这个约定,接收端一个 if 就能判断,不需要额外传期望值。