前言
大家好这里是小白,因为实在受不了一些网上代码和某些开发板例程的荼毒,决定开个blog吐槽一下学习和工作中踩过的一些雷和遇到的一些坑。博主目前主要是做硬件和嵌入式设计的,主要使用的STM32F1XX和STM32F4XX并配合Inter的FPGA做一些程控和信号处理,具体行业就不多说了,有兴趣可以点进我的头像看一哈,或者私信我交流一些设计心得。
IAP简介及原理
IAP是在线应用编程(In Application Programming)的简称,简单来说IAP的目的就是实现在线刷程序。在刚开始学习STM32的时候,我们只能通过J-LINK或者ST-LINK实现程序烧写,随着学习的深入,我们也可能会接触到使用ST官方的BOOTLOADER GUI进行程序下载,但是上述的这几种方式在烧写程序时要么需要使用特殊的下载工具,要么需要跳片改变STM32 boot0脚的状态并使用专用的软件,并不适合生产批量烧写,因此就有了IAP这种下载方式。
IAP的原理在STM32资料手册和很多博主都有介绍,有需要的朋友可以参考下面两个资料:
STM32+IAP实现原理: https://blog.csdn.net/wzy15965343032/article/details/88545225.
正点原子STM32探索者开发板IAP: https://www.bilibili.com/video/BV1Rx411R75t?p=91.
理论上IAP可以实现包括USART,CAN,SPI,IIC等任意一种通讯方式实现在线编程。
废话不多说直接说正题,由于工作需要,最近我也在编写基于USART的IAP实现方案,参考了网上一些资料后总觉得不太理想,因此分享一下我在编写IAP时遇到的一些问题以及解决方案。
IAP设计过程
目前我在网上找到两个相关的IAP设计例程,都是基于ymodem协议的,一个就是正点原子的IAP串口实现方案,另一个不知道算不算官方的例程,看函数介绍写的是MCD Application Team。
第一个例程我就不放了,大家可去正点原子官网下载。第二个例程采用的是超级终端控制的,但是需要上位机添加bin文件(这里我没有找到相应的上位机,理论上可以使用老司机常用的那个SSCOM42,只不过字符输入需要手动点击发送),我会在文章末尾放上下载链接,大家可根据需求自行下载。
IAP设计过程本质上就是编写两段(或多段)代码,一段是IAP代码,另一段是实际的APP应用程序代码,编写完成后将IAP烧写入STM32FLASH的最前端,再将APP应用程序编译后生成bin文件。然后将IAP程序烧录至FLASH中,随后根据需要将要使用的应用程序的bin文件通过USART(通讯)的方式发送给单片机,并存入单片机的FLASH。注意这里的应用程序存放要偏移一定的地址,偏移大小是设置的IAP程序的大小,否则会擦除掉IAP程序导致跳转失败或者IAP仅能使用一次。
比如我使用的是stm32f407单片机,该单片机一共12个SECTOR共1MB的FLASH存储空间,我一般是这样分配的,SECTOR 0用于存放IAP代码,SECTOR 1-SECTOR 4用于存放APP应用程序代码(如果不够可以往后扩展),SECTOR5-SECTOR 10用于存放需掉电保存的系统参数,最后SECTOR11存放一些关键的系统标志(这个后面会提到)。
APP应用程序我这里使用原子的UCOSII的移植例程,通过点亮两个LED灯检查IAP程序是否烧录进去,APP应用程序需要设置FLASH地址,并且需要设置APP应用程序的中断向量偏移。
最后再选择MDK自带的fromelf应用程序(在KEIL的安装路径下面,点击后面的小文件夹添加),生成bin文件就可以使用上位机更新程序了。
这里注意 --bin - o…是手动添加的,OBJ后面的文件名是你的项目output文件名。如我的项目output名称是UCOS-1,所以我这里应该填:
D:\Keil5\ARM\ARMCC\bin\fromelf.exe --bin -o …\OBJ\UCOS-1.bin …\OBJ\UCOS-1.axf
以上工作完成就可以使用上位机更新APP应用程序了,首先将IAP程序烧录进单片机,打开串口助手,将你的电路板与电脑连接,选择打开文件,将编译好的bin文件添加,点击发送文件,APP程序就写入了相应的FLASH中。
完整的IAP功能
以上就是IAP编程的完整过程,也是原子开发板例程中具有的功能,但是正当我想测试的时候发现一个问题,原子的例程是通过按键实现跳转的,而我设计的电路板,没有按键!!
然后我又看了看原子例程代码,想了想我的需求,发现这离我想要的IAP代码功能还差很多,于是我决定掏出我的陈年老代码重构一下改成IAP代码。
相比于原子的开发历程,我需要我的IAP程序具有以下几个功能:
1.在IAP程序中,通过特定串口指令进入IAP模式,然后再添加和发送APP应用程序的bin文件,防止发送的数据,也被写入FLASH。
2.可以实现APP应用程序跳转回IAP程序,实现程序的重新烧录,这一点也是通过特定的串口指令来实现的。
3.上电判断是否已有APP应用程序,如果已有程序,自动跳转APP程序运行。
4.由APP应用程序跳转回IAP后,IAP程序判断本次运行是重新上电还是由APP程序跳转而来,如果是由APP跳转而来,则用户需要重新烧录程序,先将已有的应用程序自动擦除,等待串口指令进入IAP模式。
具备了以上功能,IAP程序才算完整。
功能的实现与遇到的问题
以上功能并不难实现,有兴趣的朋友可以试着自己做一下,我这里来简单说一下我的做法。
1.首先针对第一点,设置一个串口指令标志位,只有收到了特定的指令(此处我设置的是十六进制A5 5A 31 42 0D),标志位置位,才可以开始接收bin文件,否则无论收到什么都显示指令错误。
if(USART_RX_CNT)
{
if(RxDataCount == USART_RX_CNT) //串口没有再收到新数据
{
RxDataLength = USART_RX_CNT;
if(RxCmdFlag == 0 && RxDataLength == 5) //接收IAP指令
{
if(USART_RX_BUF[0] == 0xA5 && USART_RX_BUF[1] == 0x5A && USART_RX_BUF[2] == 0x31
&& USART_RX_BUF[3] == 0x42 && USART_RX_BUF[4] == 0x0D)
{
RxCmdFlag = 1; //接收到更新APP代码指令,标志位置位
RxDataLength = 0; //清空指令长度,防止影响后面计算APP代码大小
printf("Ready to recieve app code,please add a binary file!\r\n"); //准备好接收bin文件,等待用户添加
}
else
{
CodeUpdateFlag = 0;
AppCodeLength = 0;
printf("Command Error!\r\n"); //未接收到IAP更新指令,其他任何串口发送数据都认为指令错误
}
}
else if(RxCmdFlag == 1 && RxDataLength > 10)//接收IAP程序
{
CodeUpdateFlag = 1; //代码更新标志位置位,用于应用程序代码接收完成后写FLASH
RxCmdFlag = 0;
AppCodeLength = USART_RX_CNT;
printf("App code recieve complete!\r\n");
printf("Code size:%dBytes\r\n",AppCodeLength);
}
else
{
RxDataLength = 0;
printf("Not correct file or command!\r\n"); //如果代码大小不足10Bytes,认为没有正确添加bin文件
}
RxDataCount = 0;
USART_RX_CNT = 0;
}
else
{
RxDataCount = USART_RX_CNT;
}
}
2.这个部分需要重点注意一下,我在测试跳转回IAP的时候,原本是在APP应用程序其中一个LED_TASK中做了个计数,计数10次之后自动跳转回IAP程序,到这里是没有问题的,但当我再次发送bin文件时就发生了问题。
void led0_task(void *pdata)
{
u8 count = 0;
while(1)
{
LED0=0;
delay_ms(80);
LED0=1;
delay_ms(920);
if(count > 10)
{
count = 0;
printf("跳转回IAP程序!\r\n");
delay_ms(10);
iap_load_app(STM32_FLASH_BASE);
}
else
{
count ++;
}
}
}
这里使用与IAP代码里使用的iap_load_app()函数是可以跳转回IAP代码的,但是当我再次发送应用程序bin文件时,并没有显示跳转APP程序,于是我挂了仿真器对IAP代码进行仿真,发现在我再次发送bin文件时,系统发生了错误进入了死循环。
void HardFault_Handler(void)
{
/* Go to infinite loop when Hard Fault exception occurs */
while (1)
{
}
}
这个问题我试了好久,也没有发现明显的逻辑错误,就是找不到原因,于是我去网上查IAP的资料,发现大多数都是IAP功能的介绍,没有什么具体示例。经过长时间查找,我终于看到了一篇文章,原来是我在跳转回IAP程序之后没有复位RCC时钟和NVIC中断,导致时钟或中断发生错乱。所以在跳转回IAP程序后,配置时钟之前要添加RCC_DeInit()函数,在配置中断前要添加NVIC_DeInit (),先将时钟和中断关闭之后再重新配置,当然这里也可以使用stm32库函数里面的系统复位函数来让程序重新从头开始运行,我这里使用的就是这种方法(别问,问就是偷懒)。
if(Jump_IAP_FLAG)
{
Jump_IAP_FLAG = 0;
delay_ms(10);
if(((*(vu32*)(STM32_FLASH_BASE + 4)) & 0xFF000000) == 0x08000000) //判断代码合法性
{
STMFLASH_Write(ADDR_CodeWriteFlag,IAPFlagBuf,2); //写IAP跳转标志位
delay_ms(100);
printf("Start Running IAP code!\r\n");
NVIC_SystemReset(); //注意此处跳转不是使用iap_load_app,而是将系统复位
}
else
{
printf("IAP code is illegal!\r\n");
}
}
3.解决了上述两个问题,第三点和第四点就好实现了,正如前文所说,我在SECTOR11中的连续两个地址存放了两个标志量。
#define ADDR_CodeWriteFlag ((u32)0x080E1000) //IAP程序更新标志位存放地址,如果是0x55,说明已经有应用程序
#define ADDR_JumpToIAPFlag ((u32)0x080E1001) //IAP程序跳转标志位,如果是0x55,说明是从APP跳转回IAP程序
在将bin文件写入FLASH之后跳转APP应用程序之前,连续从ADDR_CodeWriteFlag地址连续写入两个标志量0x55和0x00,跳转之后如果系统复位或者重新上电,在运行IAP程序时会首先读取ADDR_CodeWriteFlag地址是否等于0x55,如果等于则直接跳转APP程序运行,如果不相等,等待用户烧录程序再继续运行。
JumpToAPPFlag = FLASH_ReadWord(ADDR_CodeWriteFlag); //读取APP写入标志位,判断是否已有程序
if(JumpToAPPFlag != 0x55) //判断是否已有APP程序,如果已有,跳转至APP程序运行
{}
else
{
printf("There is an app code!\r\n");
printf("Start running app code!\r\n");
delay_ms(10);
IAP_Load_AppCode(FLASH_APPCODE_ADDR); //执行FLASH APP代码
}
同样由APP跳转回IAP之前,我会重新在上述地址连续写入0x00和0x55,这样跳转回IAP之后既不会再跳转回APP,也可以判定ADDR_JumpToIAPFlag位的值,如果该位等于0x55,说明是从APP程序跳转回来的,因此会先擦除存放APP代码的扇区,这里我是用的是SECTOR1-SECTOR4。
if(JumpToIAPFlag == 0x55) //判断是否从APP程序跳转回来,如果是擦除已有APP程序
{
STMFLASH_Erase();
JumpToIAPFlag = 0x00; //清跳转标志位,防止不停擦除FLASH
}
以上就是我IAP编写的历程和一些心得,希望对大家有帮助吧,下面放上源码,有需要的朋友自取,STM32F1系的单片机也可以使用,但是要修改一下各系统函数的初始化配置(F1和F4在配置上还是有少许区别的,这个有需求的话我也可以给大家开个贴交流一下)。
附录:程序源码
CSDN: https://download.csdn.net/download/suzuzhizhen/15611524?spm=1001.2014.3001.5503.
GitHub:https://github.com/baiyiqing-LC/stm32f4xx_usart_IAP