这两天忙里偷闲,自己编程打通了Bootloader/IAP的通道。仅仅是打通了一个通道,能实现上电进入bootloader,通过串口下载APP程序的功能。还有很多细枝末节的东西没有完善,但最主要的通道打开了,以后的升级完善就全看个人发挥啦。
先大致概括一下Bootloader和APP的程序功能
1.BootLoader: 开机后倒计时十秒,若无数据从串口导入,则跳入APP程序,若有数据传入,则将传入数据烧到APP起始地址,然后跳入APP运行(具体怎么跳可以参考我的另一篇文章https://blog.csdn.net/spiremoon/article/details/89460441);主要包括定时器中断、串口收发、Flash等操作,不算复杂。
2.APP: APP程序就我之前瞎编的一个小程序,主要用来监测IAP是否成功。
下面详细说一下BootLoader程序
int count = 0;
while (1)
{
if(Bootloader_Str.Code_size_rev == 0)
{
printf("Run APP %d seconds later\n",10-count);
HAL_Delay(1000);
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_7);
count++;
if(count >= 10)
jump2app(0x8008000);
}
if(Global_Flag.Rev_Completed)
{
Function_IAP();
Global_Flag.Rev_Completed = 0;
jump2app(0x8008000);
}
}
其中两个if语句中的判断条件来源于两个结构体,定义如下
struct BOOTLOADER_STR{
volatile uint32_t Code_size_rev;
volatile uint32_t Code_size_rev_befor;
uint8_t CODE_CACHE[CODE_MAX_SIZE];
};
struct GLOBAL_FLAG{
uint8_t Rev_Completed;
};
其中BOOTLOADER_STR中包含了串口接收size和上一次定时器检查接收长度时检测到的size_rev_before,还有串口数据接收缓存区CODE_CACHE[CODE_MAX_SIZE];
何为定时器检查接收长度呢?就是开启一个定时器中断,每次中断服务时检查Code_size_rev和Code_size_rev_befor是否相等,若相等则认为接收完了,则将GLOBAL_FLAG结构体中的 Rev_Completed位置1,在主循环里进行代码烧录的工作。大致意思就是,我设置定时器中断间隔为x毫秒,若x毫秒内接收到的数据size没变,那么我就认为完成了这一次的接收。这个方法比较粗暴且实用,但难免在有些情况下不太稳定,主要是我偷懒,否则完全可以写一个上位机,制定一套协议,从而使得数据传输更加稳定。
继续讲主循环,工作很简单,就是每隔1秒改变一下led灯的亮灭,同时打印一句“程序将在x秒后启动”,然后就是判断Rev_Completed标志位,若Rev_Completed为1,则表示数据接收完毕,接下来去进行烧录工作;再就是10秒之后未收到数据,则跳入APP运行。和有些电脑开机时的倒计时有点像,其实电脑的倒计时也算是个BootLoader。
再简单讲一下串口接收,就是在串口接收中断中,每来一字节数据,我就将它放入CODE_CACHE缓存区中,并且让Code_size_rev加一,有点像栈的数据结构。当定时器检测到规定时间内Code_size_rev不变了,则进入烧录程序。
其实解决了数据接收并保存到缓存区的问题,剩下的烧录程序就是简单的Flash操作了。可以看到主循环中检测到Global_Flag.Rev_Completed标志位置1时,会如下运行
if(Global_Flag.Rev_Completed)
{
Function_IAP();
Global_Flag.Rev_Completed = 0;
jump2app(0x8008000);
}
其中Function_IAP()就是烧录程序的过程,这个函数运行完后就清除标志位,并跳转,看一下Function_IAP()代码
void Function_IAP()
{
TIM3->CR1 &= ~(0x01);
HAL_TIM_Base_Stop_IT(&htim3);
printf("Get BIN -> Recevie Size = %d ->\n",Bootloader_Str.Code_size_rev);
Flash_Erase();
Flash_Program();
BOOTLOADER_STR_CLEAR();
printf("Jump to APP ->\n");
}
首先禁用定时器和定时器中断,防止烧录过程中再次进入Function_IAP();然后打印接收的数据长度,以作为参考看看收没收对,当然,我也是为了偷懒,并没有加入数据收发校验。之后便开始Flash擦除和烧录过程,代码如下
void Flash_Erase(void)
{
uint32_t sector_error;
FLASH_EraseInitTypeDef Flash_erase;
Flash_erase.TypeErase = FLASH_TYPEERASE_SECTORS;
Flash_erase.Sector = FLASH_SECTOR_2;
Flash_erase.NbSectors = 1;
Flash_erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
printf("Erasing. . . . . .\n");
HAL_FLASH_Unlock();
HAL_FLASHEx_Erase(&Flash_erase,NULL);
HAL_FLASH_Lock();
}
void Flash_Program(void)
{
uint32_t addr_base = 0x8008000;
HAL_Delay(1);
printf("Programing. . . . . .\n");
HAL_FLASH_Unlock();
for(int i =0;i
因为擦写时,我知道我程序将要烧到0x8008000地址,并且也知道程序只有11k左右,所以才直接将指定的sector擦除。真正应用时应该结合stm32f407参考手册,看看Flash的sector分布,结合接收数据大小进而有选择性的擦除一个或多个扇区。
烧写过程就是逐地址编程,这个没有啥好说的,就从起始地址开始一个个字节的烧就好了。之前在操作w25q系列Flash时,烧录过程的地址是自动增加的,这样就可以直接传一个首地址,再传一个烧录数据数组就好了,stm32估计也有这个功能,大概是我没深入研究过。不过这样在一个for循环里烧录一字节的方法也并不复杂。
等烧好程序号会有一个BOOTLOADER_STR_CLEAR(),就是将BOOTLOADER_STR结构体用一个memset清零。再之后,当然是调到APP程序中去啦。
开机后再倒计时如下
在倒计时结束时将准备好的bin文件发送出去(如何从keil获得bin文件,请参考https://blog.csdn.net/nx505j/article/details/80924902),现象如下图
实验结果完全和预期一样,我下载的程序就是开始打印一句"This means START",然后循环打印"In while"。再次重启后,10秒之内不发程序,运行现象如下,和预期一样。实验成功
整个实验过程仅仅是通过自己编程打通了BootLoader/IAP的一个通道。作为一个成熟的BootLoader/IAP,我认为还需要有以下升级优化的必要
1.在stm32 Flash中找个地址,作为更新标志位,每次启动时Bootloader若监测到需要更新,则进入等待接收数据并更新的程序;从而不用每次启动都要傻傻等个几秒;或者可以通过监测GPIO高低电平的方法,如需要更新时将某个Pin接地,Bootloader监测到Pin为低,则进入程序,显然,这种方法不适用于批量升级。
2.在APP程序中可以设置一个接收中断,当收到某个命令时,进入BootLoader;
3.向BootLoader传输数据时增加一个协议,定义头帧、长度、功能码、CRC校验等协议,从而保证数据传输的可靠性。但这样需要自己按协议写一个上位机,有时间的同学可以试试,我是懒得弄。。;
4.IAP方式不仅限于串口发送,可以是外部SD卡升级、USB升级、CAN升级、SPI升级…………………………茫茫多方法,总有一款适合你。