1、概述
通过IAP原理一文我们大概知道了IAP的工作原理和工作流程。但是现在要通过串口来将这个功能实现,我们应该怎么做呢。总体上整个代码可以分为4个部分:串口功能初始化、串口不定长数据接收、Flash写入以及IAP跳转。接下来我将一一解释。
2、串口功能初始化
这一部分就是在给串口IAP造轮子,是功能实现的基础。在这里我通过Cubemx进行配置,将串口2配置成115200波特率,其它值默认,打开串口中断,设置中断优先级为(3,0),最后生成代码。
生成代码后添加串口重定向语句,随后进行简单的收发测试,确定串口初始化成功。重定向语句如下:
#include "stdio.h"
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart2 , (uint8_t *)&ch, 1 , 0x0f);
return ch;
}
3、串口接收不定长数据
在通过串口发送待升级的bin文件时,由于我们不知道、也没办法固定bin文件内容的长度,所以我们就要将串口的接收设置称接收不定长的长度。但这里就有问题了,我该怎么样才知道它是否接收完成了呢?这里我查了一些资料,它们都是通过判断串口是否连续一段时间没有接收到数据来判断是否接收完成。在使用串口助手进行文件发送时,它会固定的没发多少个字节就会延时一段时间,我们只需要让我们判断的间隔时间大于串口助手的延时就可以。
我是通过在滴答中断函数中添加一个不断自增的变量time,当接收到数据时就降将该变量清理,当该变量的值超过了比较的值就认为接收完成,此时就会开始处理接收的数据。如果一直接收不到数据,time的值就会一直自增。由于设定的比较值为1S,所以我让time的值在大于等于1500就置0.这样就可以避免变量的大小超过变量类型的长度进而出现莫名其妙的问题。
4、Flash写入
在IAP原理时我就说过, STM32H7系列单片机采用的是存储区(BANK)和扇区(SECTOR)的结构。STM32H7的FLASH分为两个独立的存储区(即BANK1和BANK2),每个存储区再由多个128K大小的扇区组成。由于STM32H7的FLASH最小1M,最大2M,所以每一个存储区就被分成了4或8个扇区,分别对应(SECTOR_0至SECTOR_7)。由于扇区是结构中最小的单位,这就决定了FLASH的最小单位就扇区(即128K大小),所以在设置内存地址时需要注意,不要在擦除时不小心擦除了需要使用的区域。
所以我封装了2个函数:
u32 STMFLASH_ReadWord(u32 faddr);
void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite);
STMFLASH_Read函数的作用就是从指定地址开始读取4个字节的内容,这个函数主要是在写入前被调用去判断要写入的区域是否被擦除,如果有内容就要重新擦除一遍,因为Flash如果未被擦除是不能写入的。
5、IAP跳转
这一部分就比较简单了,STM32有现成的函数可以直接使用。在被封装以后就剩一个函数:iap_load_app(u32 appxaddr);这个函数的作用就是跳转到指定的地址,从指定地址开始运行。
此函数我在最后的跳转前增加了一行内容:__disable_irq();这条语句的内容就是关闭当前程序的中断请求,让bootloader程序不会影响App程序的运行。
6、注意事项
如果在IAP跳转前添加了“__disable_irq();”,那在App程序开始前一定要添加“__enable_irq();”语句开打开中断请求。
bootloader程序一定要设置好Flash的起始地址和大小,防止空间被占用,进而导致flash被修改。
App程序在开始前一定要加上中断向量表偏移语句,同时记得修改程序的开始地址。
7、以下是我封装好的IAP和flash代码
#include "IAP_Flash.h"
/********************************Flash操作部分代码********************************************/
//读取指定地址的字(32位数据)
//faddr:读地址
//返回值:对应数据.
u32 STMFLASH_ReadWord(u32 faddr)
{
return *(__IO uint32_t *)faddr;
}
//获取某个地址所在的flash扇区,仅用于BANK1!!
//addr:flash地址
//返回值:0~11,即addr所在的扇区
uint16_t STMFLASH_GetFlashSector(u32 addr)
{
if(addr<ADDR_FLASH_SECTOR_1_BANK1)return FLASH_SECTOR_0;
else if(addr<ADDR_FLASH_SECTOR_2_BANK1)return FLASH_SECTOR_1;
else if(addr<ADDR_FLASH_SECTOR_3_BANK1)return FLASH_SECTOR_2;
else if(addr<ADDR_FLASH_SECTOR_4_BANK1)return FLASH_SECTOR_3;
else if(addr<ADDR_FLASH_SECTOR_5_BANK1)return FLASH_SECTOR_4;
else if(addr<ADDR_FLASH_SECTOR_6_BANK1)return FLASH_SECTOR_5;
else if(addr<ADDR_FLASH_SECTOR_7_BANK1)return FLASH_SECTOR_6;
return FLASH_SECTOR_7;
}
//从指定地址开始写入指定长度的数据
//特别注意:因为STM32H7的扇区实在太大,没办法本地保存扇区数据,所以本函数
// 写地址如果非0XFF,那么会先擦除整个扇区且不保存扇区数据.所以
// 写非0XFF的地址,将导致整个扇区数据丢失.建议写之前确保扇区里
// 没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写.
//该函数对OTP区域也有效!可以用来写OTP区!
//OTP区域地址范围:0X1FF0F000~0X1FF0F41F
//WriteAddr:起始地址(此地址必须为4的倍数!!)
//pBuffer:数据指针
//NumToWrite:字(32位)数(就是要写入的32位数据的个数.)
void STMFLASH_Write(u32 WriteAddr,u32 *pBuffer,u32 NumToWrite)
{
FLASH_EraseInitTypeDef FlashEraseInit;
HAL_StatusTypeDef FlashStatus=HAL_OK;
u32 SectorError=0;
u32 addrx=0;
u32 endaddr=0;
if(WriteAddr<STM32_FLASH_BASE||WriteAddr%4)return; //非法地址
HAL_FLASH_Unlock(); //解锁
addrx=WriteAddr; //写入的起始地址
endaddr=WriteAddr+NumToWrite*4; //写入的结束地址
if(addrx<0X1FF00000)
{
while(addrx<endaddr) //扫清一切障碍.(对非FFFFFFFF的地方,先擦除)
{
if(STMFLASH_ReadWord(addrx)!=0XFFFFFFFF)//有非0XFFFFFFFF的地方,要擦除这个扇区
{
FlashEraseInit.Banks=FLASH_BANK_1; //操作BANK1
FlashEraseInit.TypeErase=FLASH_TYPEERASE_SECTORS; //擦除类型,扇区擦除
FlashEraseInit.Sector=STMFLASH_GetFlashSector(addrx); //要擦除的扇区
FlashEraseInit.NbSectors=1; //一次只擦除一个扇区
FlashEraseInit.VoltageRange=FLASH_VOLTAGE_RANGE_3; //电压范围,VCC=2.7~3.6V之间!!
if(HAL_FLASHEx_Erase(&FlashEraseInit,&SectorError)!=HAL_OK)
{
break;//发生错误了
}
SCB_CleanInvalidateDCache(); //清除无效的D-Cache
}
else addrx+=4;
FLASH_WaitForLastOperation(FLASH_WAITETIME,FLASH_BANK_1); //等待上次操作完成
}
}
FlashStatus=FLASH_WaitForLastOperation(FLASH_WAITETIME,FLASH_BANK_1); //等待上次操作完成
if(FlashStatus==HAL_OK)
{
while(WriteAddr<endaddr)//写数据
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD,WriteAddr,(uint64_t)pBuffer)!=HAL_OK)//写入数据
{
break; //写入异常
}
WriteAddr+=32;
pBuffer+=8;
}
}
HAL_FLASH_Lock(); //上锁
}
//从指定地址开始读出指定长度的数据
//ReadAddr:起始地址
//pBuffer:数据指针
//NumToRead:字(32位)数
void STMFLASH_Read(u32 ReadAddr,u32 *pBuffer,u32 NumToRead)
{
u32 i;
for(i=0;i<NumToRead;i++)
{
pBuffer[i]=STMFLASH_ReadWord(ReadAddr);//读取4个字节.
ReadAddr+=4;//偏移4个字节.
}
}
/********************************IAP跳转部分代码********************************************/
iapfun jump2app;
u32 iapbuf[512]; //2K字节缓存
//appxaddr:应用程序的起始地址
//appbuf:应用程序CODE.
//appsize:应用程序大小(字节).
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{
u32 t;
u16 i=0;
u32 temp;
u32 fwaddr=appxaddr;//当前写入的地址
u8 *dfu=appbuf;
for(t=0;t<appsize;t+=4)
{
temp=(u32)dfu[3]<<24;
temp|=(u32)dfu[2]<<16;
temp|=(u32)dfu[1]<<8;
temp|=(u32)dfu[0];
dfu+=4;//偏移4个字节
iapbuf[i++]=temp;
if(i==512)
{
i=0;
STMFLASH_Write(fwaddr,iapbuf,512);
fwaddr+=2048;//偏移2048 512*4=2048
}
}
if(i)STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.
}
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FF00000)==0x24000000) //检查栈顶地址是否合法.
{
printf("跳转到APP\r\n");
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
__disable_irq();
jump2app(); //跳转到APP.
}
}
//设置栈顶地址
//addr:栈顶地址
__asm void MSR_MSP(u32 addr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}