第一篇
在项目开发中,至关重要的是保证产品运行的可靠,如果遇到异常,能否恢复很重要,而不是像砖头一样,程序死在某个地方。固件升级的原理就是重写向量表,在引导区更新app区的flash,然后跳转app区。实际开发中就会有以下问题:
1.如果MCU复位,比如POR,PDR,WDT等复位,都会使sp指针指向复位地址。那么MCU从引导区执行,如果APP区程序有效,应该如何控制程序跳转到APP区。
2.如果
APP区或者引导区接受新固件,在更新
APP区
flash时,如果此时
MCU发生掉电,当再次上电后,
MCU该如何执行。或许有人说,我们有外部的
EEP或者外部的
FLASH,会使用状态和标志去记录当时
MCU操作
flash的状态,当然这些状态和标志有校验,并且存储到外部
EEP或
FLASH。上电后我们会判断校验,然后读出来作为依据。在理想情况下,这样做非常完美,但是
MCU在运行中,什么情况都可能发生。比如
电源掉的很快,那么算出来的校验有什么意义,还怎么保证写到
EEP或
FLASH的可靠性,特别是有外部
FLASH,几
ma的
电流
MCU瞬时根本扛不住。即使是
EEP,就算将引导区配置成最低功耗,这种意外也是不可避免的,此时的标志和状态只是徒劳。那么会造成一种
MCU假死状态,滞留在引导区,然后死循环。如果要解除,只能通过
仿真器进入仿真模式,更改变量值去解除。而这样的后果就违背了升级的初衷和产品的可靠。
3.对于新固件的更新,是接收全部数据再更新还是接收部分数据更新FLASH,这个具体依据自己使用的硬件资源,不过重点还是在于第二点的处理。
4.如果升级过程中,传输数据或读取数据突然中断,或者新的固件验证失败,那么这些操作该如何恢复,而不至于MCU假死。
自己实践中的处理,总结了如下几条:
1.首先我们要明白MCU复位后是要从复位执行,并且MCU中断后,会跳转到实际中断向量地址,也就是向量区重写。在应用区如果有中断发生,MCU会跳转到中断原始地址,通过跳转指令执行位于应用区实际的中断处理函数。例如我使用的是MSP430的FR6972,它的FRAM分配是0x4400-0x13FFF,它的向量区地址在0xFF80-0xFFFF。假如分成两个区,引导区0xF000-0xFFFF,APP区0x7C00-0xEFFF。现在程序执行在0x7C00-0xEFFF的应用区,此时MCU响应了一个中断,假设这个中断函数的入口地址是0xEFF2,按照常理,MCU也应该执行这个地址的内容,实际上,MCU会跳转到这个中断的原始中断向量地址0xFFF2,因为0xEF80-0xEFFF只是我们虚拟的中断向量地址,0xFF80-0xFFFF才是真正的中断向量区。这也是为什么要在引导区重写中断向量,如
#pragma vector=WDT_VECTOR
__interrupt void WDT_ISR(void) //0xFFF2
{
asm(" br &0xEFF2;");
}
执行中断,栈会保存sp等寄存器的内容,执行完后会恢复,继续执行APP区程序。
2.不管是引导区和APP区,MCU的寄存器地址都是固定的,ram的地址也是一样的,但是FLASH是各自独立的,不能重叠。特别注意的是,在引导区和APP区处理全局变量或静态变量时,一定要初始化,或者依据校验从存储器恢复,因为跳转(非中断跳转)会导致这些变量是乱的。
3.要明白编译出的文件格式,知道数据要写到MCU中FLASH的地址。例如MSP430编译出的文件:
@F000
01 02 03 04 05 06 07
@F008
31 40 00 24 8C 00 08 1C 3E 40 17 02 3F 4000 00
B0 13 B4 FE 8C 00 00 1C 8D 00 00 F0 3E 4007 00
……
@FFC6
38 F0 3E F0 44 F0 4A F0 50 F0 56 F0 5C F062 F0
68 F0 6E F0 74 F0 7A F0 80 F0 86 F0 8C F092 F0
98 F0 9E F0 A4 F0 AA F0 B0 F0
@FFF2
B6 F0 BC F0 C2 F0 C8 F0 CE F0 D4 F0 08 F0
q
@后的内容是代码段的地址,是说明段数据要写入的地址,这些地址不需要写入到FLASH中。地址的分配与link文件分配有关。
-Z(CONST)DATA16_C,DATA16_ID,TLS16_ID,DIFUNCT,CHECKSUM=F000-FF7F
-Z(CONST)DATA20_C,DATA20_ID,CODE_ID=F000-FF7F
-Z(CODE)CSTART,ISR_CODE,CODE16=F000-FF7F
-P(CODE)CODE=F000-FF7F
-Z(CONST)SIGNATURE=FF80-FF8F
-Z(CONST)JTAGSIGNATURE=FF80-FF83
-Z(CONST)BSLSIGNATURE=FF84-FF87
-Z(CONST)IPESIGNATURE=FF88-FF8F
-Z(CODE)INTVEC=FF90-FFFF
-Z(CODE)RESET=FFFE-FFFF
像-z,-p这些都是编译指令,(data)(const)(code)都是说明修饰,DATA16_C,DATA16_ID等都是数据段类型描述。
q就是结束标志。
例如stm8l编译出的我文件格式:
:108000008200FBA0820166548200FE8D82016655CB //起始地址是0x8000
:108010008200FEA78200FEA8820126B18200F1C77D //起始地址是0x8010
:108020008200FEA98200FEAA820113AA8200F38ABE //起始地址是0x8020
:108030008200F5C68200F6EB8200FEAB8200EFD82C //起始地址是0x8030
:108040008200F5F982014AE38200FEAC8200FEADB7 //起始地址是0x8040
:108050008200FEAE820136958200FEAF8201164399 //起始地址是0x8050
:108060008200FEB082012E8B8200FEB18200F24BB4 //起始地址是0x8060
:108070008200FEB28200FEB38200FEB48200FEB532 //起始地址是0x8070
……
:108610008D011DBB3D002608BE042602BE0626E9CC
:0F862000BE042602BE06260435020000B60087FF
:10862F00AE013CBF00905FAE01648D00FC0E725F27
:10863F00016435820165725F016635020167350895
……
我们可以很容易百度hex文件格式说明,Hex文件是可以烧录到MCU中,被MCU执行的一种文件格式。整个文件以行为单位,每行以冒号开头,内容全部为16进制码。例如”:1000080080318B1E0828092820280B1D0C280D2854”。
第一个字节0x10表示本行数据的长度,“80318B1E0828092820280B1D0C280D28”。
第二,三个字节0x00,0x08表示本行数据的起始地址。
第四个字节0x00表示数据类型,数据类型说明:
'00' Data Rrecord:用来记录数据,HEX文件的大部分记录都是数据记录
'01' End of File Record: 用来标识文件结束,放在文件的最后,标识HEX文件的结尾
'02' Extended Segment Address Record: 用来标识扩展段地址的记录
'03' Start Segment Address Record:开始段地址记录
'04' Extended Linear Address Record: 用来标识扩展线性地址的记录
'05' Start Linear Address Record:开始线性地址记录
理解了这些文件的内容,我们就知道了向量区需要些的内容,这点很重要。同时我们可以根据自己的通信协议进行扩展,重新转换这些内容,传输到MCU中进行固件升级。
4.原始的中断向量最好与引导区在一个区域,在引导区执行,最好关闭中断响应,通过查询的标志位的方式来处理。引导区的作用就是实现APP区的FLASH更新,中断的跳转。如果将原始向量区分配到APP区,会导致需要很大的外部存储空间接受新的固件,而且程序的设计也会头重脚轻,不建议使用。
5.如何保证固件更新的成功率,和解决擦写FLASH出现的异常,最好的操作就是先擦写APP区的向量区内容,更新完APP区FLASH,最后写复位向量内容。这样可以省去很多的判断流程,即使中途更新失败,或者掉电,MCU上电后也可以继续更新固件,并且出错率很低。像MSP430,烧写FLASH或者仿真下载,如果烧写时选择默认Memmory op
tion,那么FLASH默认的值是0xFF。当知道分配的APP区的起始地址后,就判断复位向量值是否为0xFF,如果是则说明APP区没有内容,则不跳转,接受或更新固件。如果不是,一般情况下APP区是有内容的,则执行跳转。
6.在实现固件升级时,最好测试指针类型长度,因为选择的数据模式不同,访问不到0x10000以上的地址。比如MSP430的,如果选择mid,所有的指针类型长度占2byte,这样能访问的最大地址是0xFFFF,只有选择large,才可以访问0x10000以上的地址,但是相应的代码面会增加,因为函数列表的内容就扩大了一倍多,还有其他指针操作。Stm8l区分的就是near和far,就052r8分好几种大小的flash,可以在stm8l15x.h中更改宏。还有就是对内存的操作,需要注意对齐补齐,特别是32位机,像cortex-0内核。比如我经常使用
#define M8(adr) (*((FAR uint8_t* )(adr)))
#define M16(adr) (*((FAR uint16_t* )(adr)))
#define M32(adr) (*((FAR uint32_t* )(adr)))
FAR只是一种修饰。可以省略,也可以使用volitale。
7.合并引导区和APP区文件,然后通过烧程器烧录完整文件。关键就是只保留一个结束标志。
第二篇
以下我会通过实例代码来说明,如果有不足,请大家提出建议。
这是stm8L引导区的main()函数。
void main()
{
//禁止中断使能
disableInterrupts();
Clk_Config();
IO_Config();
LCD_Config();
//解锁FLASH操作
FLASH->PUKR = FLASH_RASS_KEY1;
FLASH->PUKR = FLASH_RASS_KEY2;
//从存储区读出数据
FLASH_ByteRead(start_EEPROM1ADDR,(uint8_t*)&ImagePara,sizeof(Image));
//使用CRC校验,返回0没错误
if(TestAdjust((uint8_t *)&ImagePara,sizeof(Image))==0)
jumpflasg=1;
//0x01初始化状态,判断口状态,因为我使用的CPU卡
if((jumpflasg==1)&&(ImagePara.CurState==0x01)&&((GPIOC->IDR&0x02)==0))
{
//擦除APP区向量
FLASHEraseBlock(APPST_BLOCK_NUM,FLASH_MemType_Program);
//擦除整个APP区
FlashErase(APPST_BLOCK_NUM,0xEF80);
}
//如果向量地址有效,跳转引导区
if(ResetVectorValid()==1)
{
asm("LDW X, SP");
asm("LD A, $FF");
asm("LD XL, A");
asm("LDW SP, X");
asm("JPF $9000");
}
//显示boot
{
LCD->RAM[3]=0x40;
LCD->RAM[4]=0x10;
LCD->RAM[7]=0xFC;
LCD->RAM[8]=0x01;
LCD->RAM[10]=0xC0;
LCD->RAM[11]=0x3F;
}
while(1)
{
//禁止中断
disableInterrupts();
//实时判校验
if(TestAdjust((uint8_t *)&ImagePara,sizeof(Image)))
FLASH_ByteRead(start_EEPROM1ADDR,(uint8_t*)&ImagePara,sizeof(Image));
//如果有卡标志
if((GPIOC->IDR&0x02)==0)
{
//如果读卡没错误
if(carderr!=1)
{
Delayms(10);
//读卡第一个块内容,包含我的块个数和校验和
carderr=Read_ImageInfo();
if(carderr!=1)
{
//更新flash,按块操作
while((++ImagePara.Block_dyn)<=ImagePara.Block_static)
{
//读CPU卡内容,更新flash
if(Read_Image(ImagePara.Block_dyn)==FALSE)
{
carderr=1;
break;
}
}
if(carderr!=1)
{
//最后写入向量区
if(Get_Vector()==FALSE)
{
carderr=1;
}
}
if(carderr!=1)
{
//验证块的状态
if(Verify_Image()==FALSE)
{
//校验失败
ImagePara.CurState=0x02;
}
else
{
//校验成功
ImagePara.CurState=0x03;
}
//算校验,更新数据
adjust_write((uint8_t*)&ImagePara,start_EEPROM1ADDR,sizeof(Image));
}
}
}
}
else
{
if(ImagePara.CurState==0x02)
{
memset((uint8_t *)&ImagePara,0,sizeof(Image));
ImagePara.CurState=0x01;
adjust_write((uint8_t *)&ImagePara,start_EEPROM1ADDR,sizeof(Image));
}
carderr=0;
}
if(carderr==1)
LCD->RAM[12]=0x40;
else
LCD->RAM[12]=0x00;
if(ImagePara.CurState==0x03)
{
asm("LDW X, SP");
asm("LD A, $FF");
asm("LD XL, A");
asm("LDW SP, X");
asm("JPF $9000");
}
}
}
这是MSP430引导区main()函数
void main()
{
WDTCTL = WDTPW | WDTHOLD;
Init_cmu();
Init_uart_Mbus();
Init_uart_HW();
Test_Image();
if(TestAdjust((uint8*)&ImagPara,sizeof(Image))==0)
jumpflag=1;
if((jumpflag==1)&&(ImagPara.curstate==0x04))
Flash_Erase(APP_VECSTARTADDR,APP_VECLEN);
if(ResetVectorValid()==1)
asm("mov &0xEFFE,PC;");
while(1)
{
__disable_interrupt();
Test_Image();
CommPara_Mbus.chbuff=&Buff_MBUS[0];
CommPara_HW.chbuff=&Buff_HW[0];
if(UCA1IFG & UCRXIFG)
{
UCA1IFG &= ~UCRXIFG;
StreamHandler_putChar((CommData*)&CommPara_Mbus,UCA1RXBUF);
}
if(UCA0IFG & UCRXIFG)
{
UCA0IFG &= ~UCRXIFG;
StreamHandler_putChar((CommData*)&CommPara_HW,UCA0RXBUF);
}
ApplayerHandler();
if(ImagPara.curstate==0x04)
{
Flash_Erase(APP_VECSTARTADDR,APP_VECLEN);
Flash_Erase(APP_CODESTARTADDR,APPSIZE);
EEP_TO_FRAM();
if(Flash_check()==0)
{
ImagPara.curstate=0x05;
}
else
{
ImagPara.curstate=0x06;
}
adjust_write((uint8*)&ImagPara,IMAGE_ADDR,sizeof(Image));
}
if(ImagPara.curstate==0x06)
{
asm("mov &0xEFFE,PC;");
}
}
}
这是一个比较早的版本,cortex-0内核的,
int main(void)
{
Delay_FreeDog();
Poweroff_IO();
gpio config();
init_all();
system init();
while(1)
{
FreeDog();
if(Flash_Updata_sta()==TRUE)
{
DLMS_ImageTypeImageparas,ImageparasA,ImageparasB;
uint8_tflag=0xff;
timecout=SecondCounMax;
while(!DiaoDianSTATE()==0)
{
FreeDog();
` __disable_irq();
Uart_revANDsend(HT_UART4,RS485_1CHL);
Uart_revANDsend(HT_UART0,HONGW_CHL);
Uart_revANDsend(HT_UART2,MODULE_CHL);
DLMS_dataHandler(RS485_1CHL);
DLMS_dataHandler(HONGW_CHL);
DLMS_dataHandler(MODULE_CHL);
if((((ImageparasA.TransferStatus==ImageActivationInitiated)&&(!(flag&EBIT1)))||((ImageparasB.TransferStatus==ImageActivationInitiated)&&(!(flag&EBIT2))))&&(!(flag&EBIT0)))
{
if((memcmp((uint8_t*)&ImageparasA.ImageIdentifier[Versionoffset],(uint8_t*)&Imageparas.ImageIdentifier[Versionoffset],0x04)>0x00)&&(ImageparasA.TransferStatus==ImageActivationInitiated)&&(!(flag&EBIT1)))
{
ImagedataExchange((DLMS_ImageType*)&Imageparas,(DLMS_ImageType*)&ImageparasA,(DLMS_ImageType*)&ImageparasB,Code_Flash_ADDR);
}
}
timecout++;
if(timecout>SecondCounMax)
{
timecout=0;
if(ImageParsDataTest((DLMS_ImageType*)&Imageparas,ImageFlash_ADDR)==TRUE)
flag&=(uint8_t)fbit0;
else
flag&=EBIT0;
if(ImageParsDataTest((DLMS_ImageType*)&ImageparasA,ImagePara_ADDR)==TRUE)
flag&=(uint8_t)fbit1;
else
flag&=EBIT1;
if(ImageParsDataTest((DLMS_ImageType*)&ImageparasB,ImageBank_ADDR)==TRUE)
flag&=(uint8_t)fbit2;
else
flag&=EBIT2;
CommunicationTimeout(RS485_1CHL);
CommunicationTimeout(HONGW_CHL);
CommunicationTimeout(MODULE_CHL);
if((ImageParsDataTest(&Imageparas,ImageFlash_ADDR)==TRUE)&&(Imageparas.TransferStatus==ImageActivationInitiated))
{
resetcount++;
if(resetcount>ResetCountMax)
{
ImageParas.TransferStatus=ImageActivationSuccessful; adjust_write((uint8_t*)&ImageParas,ImageFlash_ADDR,sizeof(DLMS_ImageType));
entryrstprogram();
}
}
}
}
Poweroff_IO();
while(!DiaoDianSTATE()==1)
{
FreeDog();
}
}
else
entryrstprogram();
}
}
很明显,使用了大量的标志,并且判断复杂,在操作flash时,就会出现掉电状态错误,而导致程序滞留在引导区,很难恢复。即使用户要求版本高低,比如高版本不能回到低版本,也不建议这样判断使用。而且占用了大量的空间,在一级优化模式下居然都有12KB。而使用先擦除向量区最后写入向量区,做可靠的APP区不会超过4KB,并且成功率很高。