使用stm32进行IAP升级
前言:
年前了解了IAP升级,并参考了许多博主的文章,结合自己的实际情况进行了尝试,中途虽然踩了许多坑但最终好在成功。现在想将之前的过程进行总结,方便后续自己的回顾。
正文:
一、基础概念
1.1、IAP是什么?
我的理解,IAP是不需要使用下载器(JLINK,STLINK)就能实现代码的更新的一种方式和stm32通过串口就可以实现代码烧录一样(唯一区别就是这种方式,厂家进行的准备),而IAP是用户根据自己的需要进行的准备。
1.2、整体思路
IAP主要分为两部分: Bootloader:判断是否需要升级,需要---更新代码---->跳转应用代码(app);
不需要------------->跳转应用代码(app);
APP:应用代码(app),执行其具体代码操作(用户经常使用单片机直接烧录的哪个代码,执行用户希望其执行控制的逻辑代码)。
1.3、flash擦写原理:
关于最底层直接与flash打交道的是stm32提供的库函数,这些函数就是最底层实际直接执行者,而后面我面我们自己编写的哪些新的读flash的读取函数,实际就是不断多次连续的调用这些库函数。接下来就来进行介绍这些基本的库函数:
(1)读函数:
关于读取的原理,其实就是利用指针的解引用来实现对指定地址的内容进行返回从而实现读取,而每次读取多少个字节,其实是与__IO uint32_t这个类型来决定的,例如要读取半字(2字节)就将其32改为16变为__IO uint16_t,依此类推__IO uint8_t就是读取1字节内容。
//读取flash
uint32_t MYFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t *)(Address));
}
uint16_t MYFLASH_ReadHalfWord(uint32_t Address)
{
return *((__IO uint16_t *)(Address));
}
uint8_t MYFLASH_ReadByte(uint32_t Address)
{
return *((__IO uint8_t *)(Address));
}
(2)擦除函数:
- 在对flash进行写操作之前,需要必须先对flash进行擦除为0xFF(11111111),对于其写操作本质其实就是将1变为0的过程,来实现对flash进行数据的写入。
- 对flash实现的擦除步骤:先解锁--->在擦除--->最后再上锁。
- flash由很多页组成的,可以理解为将一整块flash划分为很多页的结构组成,不同芯片上的flash页的数量不一样。
- 库函数提供2类函数对flash进行擦除:FLASH_EraseAllPages()实现一次性对整块flash的擦除;FLASH_ErasePage(Page_Address)实现一次性对整块flash中的单页进行擦除。
//擦除flash
void MYFLASH_EreaseAllChip(void) //对flash整块进行擦除
{
FLASH_Unlock(); //先解锁
FLASH_EraseAllPages(); //再擦除
FLASH_Lock(); //最后进行上锁
}
void MYFLASH_EreasePage(uint32_t Page_Address) //对falsh整块中的单页进行擦除
{
FLASH_Unlock();
FLASH_ErasePage(Page_Address);
FLASH_Lock();
}
(3)写函数:
- 库函数提供2类写flash的函数, FLASH_ProgramHalfWord(Address, *Data)对指定地址一次性写入半字大小(2个字节)的内容,FLASH_ProgramWord(Address,Data)对指定地址写入字大小(4字节)的内容。
- 写入步骤过程:先解锁--->再写入--->最后再上锁。
//写flash
void MYFLASH_ProgramHalfWord(uint32_t Address,uint16_t *Data) //一次性对指定位置写入半字大小(2字节)的内容
{
FLASH_Unlock();
FLASH_ProgramHalfWord(Address, *Data);
FLASH_Lock();
}
void MYFLASH_ProgramWord(uint32_t Address,uint32_t Data)//一次性对指定位置写入字大小(4字节)的内容
{
FLASH_Unlock();
FLASH_ProgramWord(Address,Data);
FLASH_Lock();
}
二、IAP实现过程:
2.1 读写函数的编写
- 明明库函数已经有提供对flash的读写函数,为什么还要自己编写对flash读写函数呢?
原因在于库函数提供的太基础了,一次性无论是读写还是擦除flash内容都比较少,而我们往往一次性需要对大量内容进行操作,因此我们要对函数进行合适的封装(本质:按需求多次连续调用库函数,实现对大量数据操作)。
(1)读函数:
- 功能:该函数可以一次性从指定的地址读取所需要的大小的数据内容,将其存入到指定的位置。
- 参数:参数1:Address---指定的地址 参数2:length---读取的数量;参数3:p---读取出来的数值指定存入的地方(此处是存入数组,不过是利用指针来实现的)
//读取多个指定的字节,采用数组做为函数参数的方法,其表示方法有3种
//形式参数是一个指针
void MYFLASH_MoreReadByte(uint32_t Address,uint16_t length,uint8_t *p)
{
uint16_t i = 0;
for(i = 0;i < length;i++){
p[i] = *((__IO uint8_t *)(Address + i));
}
}
(2)擦除函数:
- 功能:此函数擦除flash中指定位置的页数。
- 参数:参数1:Page_Address---指定擦除页的起始位置,参数2:pagenum---连续需要擦除的页数。
//擦除多页
void MYFLASH_EreaseMorePage(uint32_t Page_Address,uint16_t pagenum)
{
uint8_t i = 0;
FLASH_Unlock();
for(i = 0;i < pagenum;i++){
FLASH_ErasePage(Page_Address + (1024 * FALSH_PAGE_SIZE * i));
}
FLASH_Lock();
}
(3)写函数:
- 功能:连续将多字节的内容写入
- 参数:参数1:Address---指定写入的地址;参数2:Length---指定写入字节的数量
//可以连续写多个字节,这里是半字的写入也就是(16字节),每次长度都是对应半字。
void MYFLASH_ProgramHalfLength(uint32_t Address,uint8_t *Data,uint16_t Length)
{
uint16_t index = 0;
uint16_t buf = 0;
FLASH_Unlock();
for(index = 0;index < Length;index += 2)
{
buf = Data[index + 1] << 8 | Data[index];
FLASH_ProgramHalfWord(Address,buf);
Address = Address + 2;
}
FLASH_Lock();
}
//参考示例
void FLASH_WritePage(uint8_t Page_Num,uint8_t *pBuff)
{
uint16_t i;
uint16_t Buff;
uint32_t Addr;
//是否超出范围
if(Page_Num > STM32_SECTOR_NUM)
return;
//解锁
FLASH_Unlock();
//先计算页首地址
Addr = Page_Num * STM32_SECTOR_SIZE + FALSH_BASE;
for(i = 0;i < STM32_SECTOR_SIZE ;i += 2)
{
Buff = ((uint16_t)pBuff[i+1] << 8) | pBuff[i];
FLASH_ProgramHalfWord(Addr,Buff);
Addr += 2;
}
//上锁
FLASH_Lock();
}
2.2 bootloader程序的编写
2.2.1 逻辑思路:
-
判断是否需要升级---若需要就更新固件
-
跳转APP(在跳转之前需要关闭外设使能,中断释放资源)
-
进入循环中接收串口传输的固件数据(若没跳转成功,程序就会进入循环,跳转成功就不会进入while循环,而是从APP开始执行)
2.2.2 固件的接收存储:
关于固件的接收:会涉及串口通信,存储:会涉及flash的写入,下面我们按顺序进行拆分讲解。
接收:
采用DMA+空闲中断的方式进行,往往对于IAP升级来说,数据量一般都比较大,因此采用DMA的方式。而DMA只有传输过半或者传输完成才会发生中断,若最后一次转运的数据不足无法满足中断条件,那么就无法及时处理,因此就结合空闲中断的特性进行完成数据的接收。
//DMA配置
DMA_DeInit(DMA1_Channel5);
DMA_StructInit(&DMA_InitStruct);
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)rxbuffer;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_BufferSize = dma_buffersize;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; /*普通模式:字节数为0,不在继续进行,除非使能,重设计数,再开启使能,优点:可以确保数据被处理,不会被覆盖,当读取数据速度
慢于传送速度时会造成数据丢失,但却保证了每回读取数据正确性。*/
/*循环模式:到0自动重装在会一直进行转移,会一个循环的形式对数据更新,从最早的数据进行一位一位的覆盖,从而保证数据的完整性
及不丢失,尽可能的保证数据的覆盖都是针对于已经读取的*/
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_Init( DMA1_Channel5,&DMA_InitStruct );
//开启空闲中断
USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);
//NVIC配置
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd =ENABLE ;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);
存储:
代码大小数据大小往往不一定是写入flash页的整数倍,因此最后会留有不足1页的代码数据量,这个时候我们可以利用定时器延时来解决这问题。
while(1)
{
if((g_rxflag != 0) || (flag_end == 1)) //接收数据
{
if(((len > 20)&&(RX_END == 1)) || (flag_end == 1)){
if(len > 20){
num++;
count++;
TIM_SetCounter(TIM2,10);
NVIC_EnableIRQ(TIM2_IRQn);//开启TIM2中断
TIM_Cmd(TIM2,ENABLE);
Split_data(rxbuffer,Package_cache);
Merge_data(concatenated_data,Package_cache);
}
if((8 == num) || (flag_end == 1)){//若剩下的最后那点没有1K怎么办,还需想办法处理。若是可以使用定时器
num = 0;
// flag_end = 0;
offset = 0;
IAP_Wappbin(((1024 * seq )+ FLASH_DOWNLOAD_ADDRESS),concatenated_data,1024);//烧写固件
seq++;
memset(concatenated_data, 0, sizeof(concatenated_data));
memset(Package_cache, 0, sizeof(Package_cache));
if(flag_end == 1){
flag_end =0;
printf("Receiving firmware completed!\r\n");
printf("Start system reset...\r\n");
softwareReset(); //系统软件复位
}
}
send_ack(count);
RX_END = 0;
}
else send_string("Reception completed\r\n");
g_rxflag = 0;
DMA_SetCurrDataCounter(DMA1_Channel5,dma_buffersize);
DMA_Cmd(DMA1_Channel5,ENABLE);
}
}
数据传输过程:代码数据传输是利用上位机分包进行发送的(如串口助手),因为对于单片机来说要想一次性将整个代码固件进行接收,就需要至少对于大小的缓存RAM,而查看芯片手册,往往大多数的RAM都比较小,因此我们就要进行分包发送,单片机就需要将从每个分包的数据中提取出有效数据,然后进行拼包直到接收完整的代码为止。
#include "data_processing.h"
#define HEADER1 0xAB
#define HEADER2 0xCD
uint8_t Package_cache[PACKET_SIZE]={0};
uint8_t concatenated_data[SIZE];
uint8_t start_idx = 0;
uint16_t offset = 0;
//提取有效数据
void Split_data(uint8_t *packet,uint8_t arry[]){
uint8_t i;
for(i=0;i<128;i++){
if(packet[i] == HEADER1 && packet[i + 1] == HEADER2){
start_idx = i + 4;
memcpy(arry, &packet[start_idx], PACKET_SIZE);
break;
}
}
}
//有效数据拼包
void Merge_data(uint8_t *concatenated_data,uint8_t arry[]){
memcpy(&concatenated_data[offset], arry, PACKET_SIZE);
offset += PACKET_SIZE;
}
2.2.3 app代码的更新:
- app代码的更新,其将接收到的完整的固件,按照指定的位置写入flash中,往往在写入完成后,会在flash预留的标志位处,置完成标志。
- 代码更新完成后,就会进行软复位,从头再次开始判断是否需要升级(判断依据就是完成标志位)
- 更新代码,将下载区接收的完整代码,读出来,烧录到app代码区实现app代码的跟新
//下载接收区
void IAP_Wappbin(uint32_t Address,uint8_t *p,uint16_t Length){
//写入之前,先擦除
uint8_t buf[] = {0xAA,0xBB};
MYFLASH_EreaseMorePage(Address,DOWNLOAD_PAGE_NUM);
MYFLASH_ProgramHalfLength(Address,p,Length);
//标志位AABB写入
MYFLASH_EreaseMorePage(FLASH_FLAG_ADDRESS,FLAG_PAGE_NUM); //擦除对应扇区
MYFLASH_ProgramHalfLength(FLASH_FLAG_ADDRESS,buf,2); //写入标志符
}
//判断是否需要升级固件
if(MYFLASH_ReadHalfWord(FLASH_FLAG_ADDRESS) == 0xBBAA){
send_string("Start Updata APP... \r\n");
IAP_Copy_to_App(FLASH_APP_ADDRESS,5120);
}
//app区
void IAP_Copy_to_App(uint32_t Address,uint16_t Length){
//写入之前,先擦除
uint8_t rxbuffer[BUF_SIZE];
uint8_t buf[] = {0x00,0x00};
MYFLASH_MoreReadByte(FLASH_DOWNLOAD_ADDRESS,BUF_SIZE,rxbuffer);
MYFLASH_EreaseMorePage(Address,APP_PAGE_NUM);
MYFLASH_ProgramHalfLength(Address,rxbuffer,Length);
MYFLASH_EreaseMorePage(FLASH_FLAG_ADDRESS,FLAG_PAGE_NUM);
MYFLASH_ProgramHalfLength(FLASH_FLAG_ADDRESS,buf,2);
printf("Firmware update completed!\r\n");
}
2.2.4 跳转到app:
- 检测到app区有代码时,就会程序就会跳转app执行。
- 跳转之前需要外设关闭中断和使能,否则会影响跳转的过程,导致跳转app失败
void IAP_Load_App(uint32_t Addr)
{
// uint32_t appResetHandler;
// 检查栈顶地址是否合法
if(((*(vu32*)Addr) & 0x2FFE0000) == 0x20000000)
{
__disable_irq();
// SCB->VTOR = 0x08002000;
// appResetHandler = 0x08002000 + 4;
JumpApp = (IAP_Fun)*(vu32 *)(Addr + 4) ;
__set_MSP(*(vu32 *)Addr);
JumpApp();
// ((void (*)(void)) appResetHandler)();
}
}
2.3 app代码的编写:
2.3.1 偏移设置:

我app程序是从flash中0x800200开始的因此为了保证后续中断时能自动准确定位到app的向量表,因此需要在app初始化时设定偏移量,说人话就是:无论什么中断起始都是从头开始的(第一个中断向量表)但在flash中有俩个程序bootloader、app,bootloader在前,所以每次发生中断时系统默认定位是从bootloader中断向量表开始的,而我们进入app后就需要定位到app的中断向量表,因此我们就会设定偏移量,让每次查询向量表的操作移动个设定的偏移量的长度到真正所需要的位置来。
SCB->VTOR = FLASH_BASE | 0x2000 ;
2.3.2 再次更新:
关于IAP的讲解中,在APP程序中,需要考虑到再次更新固件这个场景,这就需要我们接收到更新固件的命令后,再次跳转回bootloader程序里面,进行相关的固件更新。
- 接收固件新指令
if(receive_instruction(rxbuffer) == 0xBBAA){ //接收到跳转到BOOTLOADER指令
uint8_t buf[] = {0xAB,0xCD};
MYFLASH_EreaseMorePage(FLASH_FLAG_ADDRESS,FLAG_PAGE_NUM);
MYFLASH_ProgramHalfLength(FLASH_FLAG_ADDRESS,buf,2);
softwareReset(); //软件复位,跳转到bootloader区
}
- 跳回bootloader
关于跳回,由于bootloader是处于flash最开始的区,因此我采用的就是软件复位来实现的
void softwareReset(void) {
__set_FAULTMASK(1); // 禁用所有中断,确保复位过程不被干扰
NVIC_SystemReset(); // 触发系统复位
2.4 串口助手传输bin文件:
关于通过上位机下发代码给单片机,我是使用正点原子的XCOM串口助手,需要注意的是,正点原子的串口助手是将代码文件进行分包下发的,每下发一包数据都需要下位机发送响应,当响应正确才会继续下发下一包,因此单片机在关于串口通信方面需要编写响应函数,经过我踩坑经验,响应的正确性取决于每包数据的顺序号是否对应的上(响应中是否一样)。
//返回应答信号
void send_ack(uint8_t num)
{
uint8_t buf[6] = {0};
uint16_t rxdata = 0;
uint8_t rxdata2 = 0;
send_bytedata(0xAB);
send_bytedata(0xCD);
send_bytedata(num);
send_bytedata(2);
send_bytedata(0xFF);
send_bytedata(0xFE);
buf[0] = 0xAB;
buf[1] = 0xCD;
buf[2] = num;
buf[3] = 2;
buf[4] = 0xFF;
buf[5] = 0xFE;
rxdata = CRC16_Modbus(buf,6);
send_bytedata(rxdata);
rxdata2 = rxdata >> 8;
send_bytedata(rxdata2);
}
2.5 关于串口通信的拓展:
2.5.1 中断接收:
2.5.2 空闲中断:
2.5.3 DMA:
2.5.4 组合使用:
总结:
浙公网安备 33010602011771号