写在前面:三周之前,我突然想写一个远程升级的程序。那个时候我只是大概知道IAP的意思是在应用编程,但怎么编,我还一无所知。我给自己定下一个个阶段目标,从最基础的代码一点点写起,解决一个又一个的问题。三个周之后,我用自己设计的方法实验了50多次,无一例升级失败。
三个周来,遇到了很多的不解、困惑,甚至是想放弃,但我现在想说的是:很多未知的困难会挡在我们面前,我们会感觉毫无头绪甚至觉得毫无出路忍不住要放弃,但多坚持一下,那些困难不但能烟消云散还能带给我们进步。
本设计是基于LPC2114和Keil MDK(V4.10),但所有支持IAP的处理器都可借鉴本方案,重要的是思想,而不是用什么。
在应用编程(IAP)技术为系统在线升级和远程升级提供了良好的解决方案,也为数据存储和现场固件的升级都带来了极大的灵活性。通常可利用芯片的串行口接到计算机的RS232口、通过现有的Internet或、无线网络或者其他通信方式很方便地实现在线以及远程升级和维护。
本文以NXP的LPC2114 ARM微处理器为平台,以Keil MDK为开发工具,阐述IAP的原理、Flash的划分、分散加载机制、中断重映射以及在线升级的实现方案及其优化。本方案使用多种校验技术,最大限度的保障传输数据的正确性;使用bootloader机制,即使因意外事件(断电,编程Flash失败等)造成升级失败后,程序也能返回到升级前的状态。
1.1 扇区描述
LPC2114共有128KB片内Flash,共分为16个扇区,分别为0扇区~15扇区,每个扇区为8KB存储空间。其中第15扇区出厂时被固化为Boot Block区,控制复位后的初始化操作,并提供实现Flash 编程的方法。所以用户可用的Flash空间只有120KB。IAP程序固化于Boot Block中,IAP操作是以扇区为单位,并占用片内RAM的高32字节。下表列出LPC2114器件所包含的扇区数和存储器地址.
表1.1 LPC2114 Flash 器件中的扇区
1.2 Flash的扇区划分
本设计将Flash划分为四个区,扇区0存放跳转程序和升级引导程序(Bootloader)。分站上电后执行跳转程序,跳转到用户程序处。用户程序运行过程中,如果接收到升级指令,会从用户程序跳转到引导程序区(Bootloader),接收新程序数据包,完成Flash编程并跳转到新程序区执行程序。扇区1~扇区7为程序存储低区;扇区8~扇区13为程序存储高区;扇区14存放当前程序运行区域标志,如果当前程序运行在高区,该标志区的最低四个字节为0x00010000,如果当前程序运行在低区,该标志区的最低四个字节为0x00008000。
2.1 IAP的原理
IAP函数是固化在微处理器内部flash上的一些函数代码,最终的用户程序可以直接通过调用这些函数来对内部flash进行擦除和编程操作。LPC2114微处理器的内部flash有一个块称为Boot Block,位于flash的顶端,可供调用的IAP函数就位于该块中。上电后Boot Block被映射到内部地址空间的顶端,同样IAP函数人口地址也被映射到地址0x7ffffff0处。用户可通过跳转到该地址来调用相应的lAP函数。
2.2 IAP 命令
对于在应用编程来说,应当通过寄存器r0 中的字指针指向存储器(RAM)包含的命令代码和参数来调用IAP 程序。IAP 命令的结果返回到寄存器r1 所指向的返回表。用户可通过传递寄存器r0 和r1 中的相同指针重用命令表来得到结果。参数表应当大到足够保存所有的结果以防结果的数目大于参数的数目。参数传递见图2-1。参数和结果的数目根据IAP命令而有所不同。参数的最大数目为5,由“将RAM 内容复制到Flash”命令传递。结果的最大数目为2,由“扇区查空”命令返回。命令处理程序在接收到一个未定义的命令时发送状态代码INVALID_COMMAND。IAP 程序是thumb 代码,位于地址0x7FFFFFF0。
图2-1 IAP的参数传递
表2-1描述了IAP的命令。
表2-1 IAP 命令汇总
IAP命令 |
命令代码 |
描述 |
准备编程扇区 |
50 |
该命令必须在执行“将 RAM 内容复制到Flash”或“擦除扇区”命令之前执行。这两个命令的成功执行会导致相关的扇区再次被保护。该命令不能用于boot 扇区。要准备单个扇区,可将起始和结束扇区号设置为相同值。 |
将RAM内容复制到Flash |
51 |
该命令用于编程 Flash 存储器。受影响的扇区应当先通过调用“准备写操作的扇区”命令准备。当成功执行复制命令后,扇区将自动受到保护。该命令不能写boot 扇区。 |
擦除扇区 |
52 |
该命令用于擦除片内 Flash 存储器的一个或多个扇区。boot 扇区不能由该命令擦除。要擦除单个扇区可将起始和结束扇区号设定为相同值。 |
扇区查空 |
53 |
该命令用于对片内 Flash 存储器的一个或多个扇区进行查空。要查空单个扇区可将起始和结束扇区号设定为相同值。 |
读器件ID |
54 |
该命令用于读取器件的 ID 号。 |
读Boot版本 |
55 |
该命令用于读取 boot 代码版本号。 |
IAP比较 |
56 |
该命令用来比较两个地址单元的存储器内容。当源或目标地址包含从地址0 开始的前64字节中的任意一个时,比较的结果不一定正确。前64字节重新映射到Flash boot扇区。 |
2.3 IAP 编程函数接口
IAP 功能可用下面的C 代码来调用。
定义 IAP 程序的入口地址。由于IAP 地址的第0 位是1,因此,当程序计数器转移到该地址时会引起Thumb 指令集的变化。
#define IAP_LOCATION 0x7ffffff1
定义数据结构或指针,将IAP 命令表和结果表传递给IAP 函数
unsigned long command[5];
unsigned long result[2];
定义函数类型指针,函数包含2 个参数,无返回值。注意:IAP 将函数结果和R1 中的表格基址一同返回。
typedef void (*IAP) (unsigned int [ ] , unsigned int [ ]);
IAP iap_entry;
设置函数指针
iap_entry=(IAP) IAP_LOCATION;
使用下面的语句来调用IAP。
iap_entry (command , result);
Flash 存储器在写或擦除操作过程中不可被访问。执行Flash 写/擦除操作的IAP 命令使用片内RAM 顶端的32 个字节空间。如果应用程序中允许IAP 编程,那么用户程序不应使用该空间。
由于在升级程序软件设计中,分散加载机制、中断向量的重映射、软中断等的实现还与所使用的编译器紧密相关,因此,本文结合Keil MDK(V4.10)编译工具,来详细阐述升级程序的实现过程。
3.1 总体思路
分站上电后,首先运行位于Flash 0x000~0x3FF中的跳转程序。跳转程序会读取位于14扇区的当前程序运行标志,如果该扇区的最低四个字节为0x00010000,表示当前程序运行在高区,跳转程序会跳转到Flash的0x00010000处执行用户程序;如果该标志区的最低四个字节为0x00008000,表示当前程序运行在低区,跳转程序会跳转到Flash的0x00002000处执行用户程序。用户程序正常执行后,会按照设计进行正常的程序采集、数据处理传送。当接收到升级命令后,用户程序会跳转到Flash的0x00000400处的Bootloader处进行升级的一些操作。当升级成功后,Bootloader程序更新当前程序运行区标志,程序跳转到新程序处运行,如果升级不成功,返回升级前的程序。
流程图如下所示:
3.2 跳转程序的设计
跳转程序是分站上电后最先运行的程序,根据当前程序运行区标志,跳转到相应的用户程序区执行。本段程序占用Flash的最低1K字节空间,与Bootloader同在第0扇区。
跳转程序的启动代码仅初始化堆栈,不使用PLL和存储加速功能。下面描述了跳转程序的主要启动代码。
; Enter User Mode and set its Stack Pointer
MSR CPSR_c, #Mode_USR
MOV SP, R0
SUB SL, SP, #USR_Stack_Size
; Enter the C code
IMPORT __main
LDR R0, =__main
BX R0
当跳转程序确定要跳转到高区用户程序或者低区用户程序后,使用函数指针跳转到0x00010000处(高区用户函数入口地址)或0x00002000处(低区用户函数入口地址)。
定义函数指针:
void (*UserProgram)() ;
指定入口地址:
UserProgram = (void (*)()) (0x00010000);
UserProgram = (void (*)()) (0x00002000);
实现跳转:
(*UserProgram)() ;
要将用户代码精确定位到Flash的0x00010000处(高区用户函数入口地址)或0x00002000处(低区用户函数入口地址),需要使用编译器的分散加载机制,将在Bootloader中详细描述实现过程。
另外,跳转程序还在烧录代码的同时初始化当前程序运行区标志,即对Flash的0x0001C000地址处写入0x00008000,表示当前用户程序在低区。主要使用了编译器的__at关键字:精确定位变量。需要注意的是,使用该关键字必须包含头文件absacc.h。
const uint32 x __at(0x0001C000)=0x00008000; //初始化用户程序标志区,默认运行低区
3.3 升级程序Bootloader的设计
升级程序的好坏,在很大程度上取决于Bootloader设计的好坏。
一个优秀的IAP升级Bootloader,必须做好升级中出现故障等异常的处理。保证系统不会崩溃,即使升级失败,也能返回升级前的程序。
本设计的Bootload位于Flash的0x400开始的扇区0存储区内,使用分散加载机制,将程序的入口地址定位到0x00000400处。当用户程序接收到升级指令后,就会使用函数指针跳转到这个入口处。
3.3.1 使用IAP
图3-1 描述了使用IAP编程Flash所必须的步骤。
3.3.1.1 定义系统参数
在使用IAP前,需要定义一些系统参数,比如系统时钟、IAP中断入口、输入输出缓存。
#define IAP_CLK 11059200UL
#define IAP_LOCATION 0x7FFFFFF1
typedef void(*IAP)(uint32 [],uint32 []); //定义函数类型指针
IAP iap_entry=(IAP)IAP_LOCATION; //设置函数指针
unsigned long command[5] = {0,0,0,0,0};
unsigned long result[2]= {0,0};
3.3.1.2 选择扇区
在任何擦除和编程Flash之前,必须选中扇区,可以选中一个或多个。
/******************************************************************
* 名称:SelSector()
* 功能:IAP操作扇区选择,命令代码50。
* 入口参数:sec1 起始扇区
* sec2 终止扇区
* 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,BUSY,INVALID_SECTOR
*********************************************************************/
void SelSector(uint8 sec1, uint8 sec2)
{
paramin[0] = IAP_SELSECTOR; // 设置命令字
paramin[1] = sec1; // 设置参数
paramin[2] = sec2;
iap_entry(paramin, paramout); // 调用IAP服务程序
}
3.3.1.3 擦除扇区
在编程Flash前必须执行擦除操作,如果某个扇区已经擦除,就不需要再次擦除。可以一次擦除一个或多个扇区。
/******************************************************************
* 名称:EraseSector()
* 功能:扇区擦除,命令代码52。
* 入口参数:sec1 起始扇区
* sec2 终止扇区
* 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,BUSY,INVALID_SECTOR ************************************************************************/
void EraseSector(uint8 sec1, uint8 sec2)
{
paramin[0] = IAP_ERASESECTOR; // 设置命令字
paramin[1] = sec1; // 设置参数
paramin[2] = sec2;
paramin[3] = Fosc/1000; // 当不使用PLL功能时,Fcclk=Fosc
iap_entry(paramin, paramout); // 调用IAP服务程序
}
3.3.1.4 编程扇区
通过这个过程,数据可以从RAM中编程到片内Flash中。
注:
/*********************************************************************
* 名称:RamToFlash()
* 功能:复制RAM的数据到FLASH,命令代码51。
* 入口参数:dst 目标地址,即FLASH起始地址。以512字节为分界
* src 源地址,即RAM地址。地址必须字对齐
* no 复制字节个数,为512/1024/4096/8192
* 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR,
SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未选择扇区
********************************************************************/
void RamToFlash(uint32 dst, uint32 src, uint32 no)
{
paramin[0] = IAP_RAMTOFLASH; // 设置命令字
paramin[1] = dst; // 设置参数
paramin[2] = src;
paramin[3] = no;
paramin[4] = Fosc/1000; // 当不使用PLL功能时,Fcclk=Fosc
iap_entry(paramin, paramout); // 调用IAP服务程序
}
3.3.1.5 比较数据
通过这个函数,可以检查写入Flash中的数据和RAM中的是否相同。
注意源地址、目标地址和字节数必须是4的倍数。可使用Keil MDK提供的关键字__align(n) 来指定n字节对齐。
/********************************************************************
* 名称:Compare()
* 功能:校验数据,命令代码56。
* 入口参数:dst 目标地址,即RAM/FLASH起始地址。地址必须字对齐
* src 源地址,即FLASH/RAM地址。地址必须字对齐
* no 复制字节个数,必须能被4整除
* 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,COMPARE_ERROR,ADDR_ERROR
******************************************************************/
void Compare(uint32 dst, uint32 src, uint32 no)
{
paramin[0] = IAP_COMPARE; // 设置命令字
paramin[1] = dst; // 设置参数
paramin[2] = src;
paramin[3] = no;
iap_entry(paramin, paramout); // 调用IAP服务程序
}
3.3.2 IAP编程期间的中断管理
LPC2114片上Flash在擦除/编程期间绝不可被中断打断。但Bootloader中定时和串口接收又使用了中断,因此必须在擦除/编程之前禁止总中断,待操作完成后再使能总中断。Bootloader运行在用户模式下,不具有禁止/使能中断的权力,所以在本设计中使用软中断禁止/使能总中断。Keil MDK提供了关键字__svc来触发软中断。
软中断函数声明:
__svc(0x00) void EnableIrq(void); //使能中断,软中断0
__svc(0x01) void DisableIrq(void); //禁止中断,软中断1
软中断函数代码:
/*
*********************************************************************
* 功 能:禁止中断
* 描 述:利用软中断实现在用户模式下调用函数关中断
*********************************************************************/
void DisableIrqFunc(void)
{
int temp;
__asm
{
MRS temp,SPSR
ORR temp,temp,#0x80
MSR SPSR_c,temp
}
}
/*
********************************************************************
* 功 能:使能中断
* 描 述:利用软中断实现在用户模式下调用函数开中断
********************************************************************
*/
void EnableIrqFunc(void)
{
int temp;
__asm
{
MRS temp,SPSR
BIC temp,temp,#0x80
MSR SPSR_c,temp
}
}
更改启动代码,挂接软中断入口:
;软中断入口
EXPORT SWI_Handler
extern EnableIrq1
extern DisableIrq1
SWI_Handler
STMFD SP!, {R0,R12,LR} ;入栈
LDR R0, [LR,#-4] ;取软中断指令,软中断号就包含其中
BIC R0,R0,#0xFF000000
CMP R0,#0 ;判断是否软中断0
BLEQ EnableIrqFunc
BLNE DisableIrqFunc
LDMFD SP!,{R0,R12,PC}^
在程序中,如果想禁止中断,只需使用DisableIrq();若是能中断,只需使用EnableIrq()。
3.3.3 使用分散加载机制精确定位入口地址
应用程序接收到升级指令后,会跳转到0x00000400处执行Bootloader升级程序。因此Bootloader程序的入口地址必须精确定位到0x00000400处。这可以使用Keil MDK提供的分散加载机制来完成。
分散加载代码见代码3-8.
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
LR_IROM1 0x00000400 0x00001C00 { ; load region size_region
ER_IROM1 0x00000400 0x00001C00 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x40000040 0x00003FA0 { ; RW data
.ANY (+RW +ZI)
}
}
这段代码显示出Bootloader程序从0x00000400处开始执行,最多占用0x1C00字节的Flash空间。另外,该程序的RAM从0x40000040开始,长度为0x3FA0个字节。这样RAM的低64字节保留给中断向量映射使用,高32字节保留给IAP编程使用。
3.3.4 中断向量的重映射
Bootloader的起始地址位于0x00000400,中断向量也从这一地址开始存储。但默认情况下ARM发生异常时,会跳转到0x00000000处的64字节中断向量区域执行相应操作,所以为了使Bootloader能相应中断,必须将位于0x00000400开始的64字节中断向量表重映射到RAM的低区。LPC2114使用向寄存器MEMMAP写入0x02来完成这一过程。
代码3-9 描述了中断向量重映射的过程。
; Copy Exception Vectors to Internal RAM
ADR R8, Vectors ; 源地址
LDR R9, =RAM_BASE ; 目标地址,这里是0x40000000
LDMIA R8!, {R0-R7} ; 装载向量表
STMIA R9!, {R0-R7} ; 存储向量表
LDMIA R8!, {R0-R7} ; 装载处理程序地址
STMIA R9!, {R0-R7} ; 存储处理程序地址
; Memory Mapping (when Interrupt Vectors are in RAM)
MEMMAP EQU 0xE01FC040 ; Memory Mapping Control
IF :DEF:REMAP
LDR R0, =MEMMAP
IF :DEF:EXTMEM_MODE
MOV R1, #3
ELIF :DEF:RAM_MODE
MOV R1, #2
ELSE
MOV R1, #1
ENDIF
STR R1, [R0]
ENDIF
由于Keil MDK提供的启动代码中使用条件编译指令,所以,要想正确的执行中断向量重映射,还需要在Keil MDK编译器工程设置Options for target“你的工程目标名”下的Asm标签中找到Define编辑框,在编辑框中键入“REMAP RAM_MODE”。如图3-2所示
图3-2
注意:在擦除/编程Flash的时候还应该禁止PLL、存储器加速模块。
3.4 用户程序的设计
用户程序运行在高区(扇区8~13)或者低区(扇区1~7),用于实现数据的采集、处理和上传等等,用户程序除本身功能的要求外,还需要注意:
4.1 Intel的hex格式
Intel hex文件是记录文本行的ASCII文本文件,在Intel HEX文件中,每一行是一个HEX记录,由十六进制数组成的机器码或者数据常量。一个数据记录以一个回车和一个换行结束。
一个Intel HEX文件可以包含任意多的十六进制记录,每条记录有五个域,下面是一个记录的格式.
: LL AAAA TT [DD...] CC
每一组字母是独立的一域,每一个字母是一个十六进制数字,每一域至少由两个十六进制数字组成,下面是字节的描述.
一个Intel HEX文件必须有一个文件结束记录,这个记录的类型域必须是01,
一个EOF记录总是这样:
:00000001FF
4.2 对上位机软件的要求
为验证升级程序的稳定性,对分站进行重上电、复位、远程升级等一些列实验,实验记录及如下。
5.1 测试程序跳转功能.
程序在上电或复位之后,应顺利跳转到用户程序。
5.2 测试Bootloader(一)
上位机发送升级命令但不发送升级数据包,程序应能进入Bootloader并发送当前程序所在的区域(高区或者低区代号),10S后程序应跳转到用户程序。
5.3 测试Bootloader(二)
上位机发送升级命令,发送升级数据包,但发送到一半时停止发送。程序在10S后应能跳转到用户程序区。
5.4 测试Bootloader(三)
上位机发送升级命令,发送升级数据包,但发送中途给分站断电,重新上电后,应还能执行原来的程序。
5.5 测试Bootloader(四)
上位机发送升级命令,发送完成升级数据包。程序应能接收升级数据包并编程Flash,完成用户程序的更新,更新用户程序后,跳转到新的用户程序。
本次升级方案虽然是以LPC2114为基础的,但任何具有IAP功能的单片机、ARM都可使用本设计方案。
设计的重点在于如何保证升级的安全性,分站采取了一些列校验、超时处理以及看门狗等措施,一是保障升级数据包的正确传送,二是即使升级失败也能退回原升级程序。上位机的校验措施需相关部门配合。从实验数据来看,进行了几十次的远程升级,未有一例失败,安全性能可以得到保证。
8.1 后记1
分散加载文件,软中断,中断向量表重映射,变量对齐,精确定位变量等等这些东西的详细讲解在我的参考资料上都能找的到,发现问题并能解决它,是件很美妙的事情,所以我没打算也没时间详细写这些东西的用法。
需要说的是,我在设计的时候走了一个弯路,现在想想还觉得挺可笑。我以为上面讲的东西要在一个工程里面实现才好,这样才能生成一个.hex可烧录文件,可以一次性的将用户程序、Bootloader程序烧写进处理器,我想弯了。正确的做法是建三个工程:Bootloader、用户低区程序、用户高区程序。如果你懂了.hex文件的格式,就完全可以将Bootloader和用户低区程序(或者Bootloader和用户高区程序)这三个工程生成的.hex文件合成一个。灵活多变的处理问题,这是我最大的收获。
8.2 后记2
很多同学看完后都希望得到源码,这种心情我是理解的,最初的时候我也希望有一套别人的源码的,毕竟这样可以进行的快点.所以我将一个远程升级的例子放在下面的链接里,大家想看看的就去下载吧,这个不是我产品中用到的,因为我的代码毕竟是含有公司的一些信息.如果我最近有时间,会把其中的英文文档翻译一下的.
链接:http://download.csdn.net/detail/zhzht19861011/3618966
8.3 后记3
补充一个隐含的Bug,从Bootloader跳转到应用程序前,应该使处理器进入特权模式,否则用户程序可能出现堆栈错误。----2012.12.10
8.4 后记4
重新编排了文章格式。本来这篇文章是在Word中书写的,排版十分清晰,但是从Word复制csdn博客,很多word格式csdn并不支持,因此版面很是凌乱,这一凌乱一下就持续了四年多。
以前写博客是为了给自己看,每当有个心得,记下来以便以后查阅。后来发现自己写的很多文章会有比较高的访问量,很多网友会留言说“xx文章对我帮助很大”,这让我意识到原来我可以帮助很多人。既然有人看,那么对我而言,有两个方面需要特别考虑:一是要准确,二是要清晰。准确源于平时积累,书写成文字必须多考证;清晰不仅要求文章内容逻辑上要清晰,而且文章排版也要清晰。这是我重新排版的原因。
因为各种原因,我的文章可能不总是准确,如果读者发现错误,请正面留言告诉我。CSDN博客有个功能, 如果你觉得这篇文章很好,可以在文章最后点“顶”,反之你觉得这篇文章误导了你,你可以点“踩”。这篇文章就被人点了“踩”,但并没有反馈为什么点踩。如果你不告诉我点踩的原因,比如里面有错误,那么不仅我没有进步,别人也可能会继续看到错误的东西。所以,如果你发现了任何错误或者不满,正面告诉我,对我以及以后看文章的网友都是件好事。