1. IAP简介
IAP(In Application Programming)即在应用编程。STM32 可以通过设置 MSP 的方式从不同的地址启动: 包括 Flash 地址、 RAM 地址等, 在默认方式下,我们的嵌入式程序是以连续二进制的方式烧录到 STM32 的可寻址 Flash 区域上的。如果我们用的 Flash 容量大到可以存储两个或多个的完整程序, 在保证每个程序完整的情况下,上电后的程序通过修改 MSP 的方式, 就可以保证一个单片机上有多个有功能差异的嵌入式软件。
IAP 是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。 通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它做如下操作:
1)检查是否需要对第二部分代码进行更新
2)如果不需要更新则转到 4)
3)执行更新操作
4)跳转到第二部分代码执行
第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP代码更新。
我们将第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP 程序,他们存放在 STM32F407 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP 程序(注意,如果 FLASH 容量足够,是可以设计很多 APP 程序的,本章我们只讨论一个 APP 程序的情况)。这样我们就是要实现 2 个程序: Bootloader 和 APP。
STM32F407 的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行。
STM32F407 正常的程序运行流程如图:
STM32F407 的内部闪存(FLASH)地址起始于 0x0800 0000, 一般情况下,程序文件就从此地址开始写入。此外 STM32F407 是基于 Cortex-M4 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动, 而这张“中断向量表”的起始地址是 0x0800 0004,当中断来临, STM32F407 的内部硬件机制亦会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
STM32F407 在复位后,先从 0x08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生了中断),此时 STM32F407 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
当加入 IAP 程序之后,程序运行流程如图:
STM32F407 复位后,还是从 0x08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,在执行完 IAP 以后(即将新的 APP 代码写入STM32F407 的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0x08000004+N+M), 跳转至新写入程序的复位向量表, 取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数, 如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32F407 的 FLASH,在不同位置上,共有两个中断向量表。
在 main 函数执行过程中,如果 CPU 得到一个中断请求, PC 指针仍然会强制跳转到地址0x08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
1) 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
1.1 APP 程序起始地址设置方法
APP 程序使用以前例程即可,不需要对程序进行修改。默认的条件下,图中 IROM1 的起始地址(Start)一般为 0x08000000,大小(Size)为 0x100000,即从 0x08000000 开始的 1024K 空间为我们的程序存储区。
我们设置起始地址(Start)为 0x08010000,即偏移量为 0x10000(64K 字节,即留给 BootLoader 的空间),因而,留给 APP 用的 FLASH 空间( Size)为 0x80000- 0x10000=0x70000(448KB)。设置好 Start 和 Size,就完成 APP 程序的起始地址设置。 IRAM 是内存的地址, APP 可以独占这些内存,我们不需要修改。
注意:需要确保 APP 起始地址在 Bootloader 程序结束位置之后,并且偏移量为 0x200 的倍数即可。
1.2 中断向量表的偏移量设置方法
VTOR 寄存器存放的是中断向量表的起始地址。默认的情况它由 BOOT 的启动模式决定,对于 STM32F407 来说就是指向 0x0800 0000 这个位置,也就是从默认的启动位置加载中断向量等信息,不过 ST 允许重定向这个位置,这样就可以从 Flash 区域的任意位置启动我们的代码了。
我们可以通过调用 sys.c 里面的 sys_nvic_set_vector_table 函数实现
/**
* @brief 设置中断向量表偏移地址
* @param baseaddr : 基址
* @param offset : 偏移量
* @retval 无
*/
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset)
{
/* 设置 NVIC 的向量表偏移寄存器,VTOR 低 9 位保留,即[8:0]保留 */
SCB->VTOR = baseaddr | (offset & (uint32_t)0xFFFFFE00);
}
该函数用于设置中断向量偏移, baseaddr 为基地址(即 APP 程序首地址), Offset 为偏移量,需要根据自己的实际情况进行设置。比如 FLASH APP 设置中断向量表偏移量为 0x10000,调用情况如下:
/* 设置中断向量表偏移量为 0x10000 */
sys_nvic_set_vector_table(FLASH_BASE, 0x10000);
通过以上两个步骤的设置,我们就可以生成 APP 程序了,只要 APP 程序的 FLASH 和 SRAM大小不超过我们的设置即可。不过 MDK 默认生成的文件是.hex 文件,并不方便我们用作 IAP更新,我们希望生成的文件是.bin 文件,这样可以方便进行 IAP 升级(至于为什么,请大家自行百度 HEX 和 BIN 文件的区别!)。这里我们通过 MDK 自带的格式转换工具 fromelf.exe,如果安装在 C 盘的默认路径,它的位置是 D:\Keil_v5\ARM\ARMCC\bin\fromelf.exe,来实现.axf 文件到.bin 文件的转换。该工具在 MDK 的安装目录\ARM\ARMCC\bin 文件夹里面。
我们可以通过在 MDK 点击 Options for Target→User 选项卡,在 After Build/Rebuild一栏中,勾选 Run #1,我们推荐使用相对地址,在勾选的同一行后的输入框并写入命令行: fromelf --bin -o ..\..\Output\@L.bin ..\..\Output\%L,如图
通过这一步设置,我们就可以在 MDK 编译成功之后,调用 fromelf.exe, ..\..\Output\%L 表示当前编译的链接文件(..\是相对路径,表示上级目录,编译器默认从工程文件*.uvprojx 开始查找,根据我的工程文件 Output 的位置就能明白路径的含义),指令--bin –o ..\..\Output\@L.bin表示在 Output 目录下生成一个.bin 文件, @L 在 Keil 的下表示 Output 选项卡下的 Name of Executable 后面的字符串,即在 Output 文件夹下生成一个 atk_f407.bin 文件。在得到.bin 文件之后,我们只需要将这个 bin 文件传送给单片机,即可执行 IAP 升级。
1) 设置 APP 程序的起始地址和存储空间大小
对于在 FLASH 里面运行的 APP 程序,我们只需要设置 APP 程序的起始地址,和存储空间大小即可。需要确保 APP 程序的大小不超过我们的设置范围。
2) 设置中断向量表偏移量
此步,通过调用 sys_nvic_set_vector_table 函数,实现对中断向量表偏移量的设置。
3) 设置编译后运行 fromelf.exe,生成.bin 文件
通过在 User 选项卡,设置编译后调用 fromelf.exe,根据.axf 文件生成.bin 文件,用于 IAP更新。
以上 3 个步骤,就可以得到一个.bin 的 APP 程序,通过 Bootlader 程序即可实现更新。
1、 FLASH IAP Bootloader,起始地址为 0x08000000,设置为我们用于升级的跳转的程序,我们将用串口 1 来作数据接收程序,通过按键功能手动跳转到指定 APP。
2、 FLASH APP,仅使用 STM32 内部 FLASH,大小为 120KB。本程序使用:实验 15 RTC实验,作为 FLASH APP 程序(起始地址为 0x08010000)
iap.h
#ifndef __IAP_H
#define __IAP_H
#include "./SYSTEM/sys/sys.h"
typedef void (*iapfun)(void); /* 定义一个函数类型的参数 */
#define FLASH_APP1_ADDR 0x08010000 /* 第一个应用程序起始地址(存放在内部FLASH)
* 保留 0x08000000~0x0800FFFF 的空间为 Bootloader 使用(共64KB)
*/
void iap_load_app(uint32_t appxaddr); /* 跳转到APP程序执行 */
void iap_write_appbin(uint32_t appxaddr,uint8_t *appbuf,uint32_t applen); /* 在指定地址开始,写入bin */
#endif
iap.c
#include "./IAP/iap.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/STMFLASH/stmflash.h"
iapfun jump2app;
uint32_t g_iapbuf[512]; /* 2K字节缓存 */
/**
* @brief IAP写入APP BIN
* @param appxaddr : 应用程序的起始地址
* @param appbuf : 应用程序CODE
* @param appsize : 应用程序大小(字节)
* @retval 无
*/
void iap_write_appbin(uint32_t appxaddr, uint8_t *appbuf, uint32_t appsize)
{
uint32_t t;
uint16_t i = 0;
uint32_t temp;
uint32_t fwaddr = appxaddr; /* 当前写入的地址 */
uint8_t *dfu = appbuf;
for (t = 0; t < appsize; t += 4)
{
temp = (uint32_t)dfu[3] << 24;
temp |= (uint32_t)dfu[2] << 16;
temp |= (uint32_t)dfu[1] << 8;
temp |= (uint32_t)dfu[0];
dfu += 4; /* 偏移2个字节 */
g_iapbuf[i++] = temp;
if (i == 512)
{
i = 0;
stmflash_write(fwaddr, g_iapbuf, 512);
fwaddr += 2048; /* 偏移2048 16 = 2 * 8 所以要乘以2 */
}
}
if (i)
{
stmflash_write(fwaddr, g_iapbuf, i); /* 将最后的一些内容字节写进去 */
}
}
/**
* @brief 跳转到应用程序段(执行APP)
* @param appxaddr : 应用程序的起始地址
* @retval 无
*/
void iap_load_app(uint32_t appxaddr)
{
if (((*(volatile uint32_t *)appxaddr) & 0x2FFE0000) == 0x20000000) /* 检查栈顶地址是否合法.可以放在内部SRAM共64KB(0x20000000) */
{
/* 用户代码区第二个字为程序开始地址(复位地址) */
jump2app = (iapfun) * (volatile uint32_t *)(appxaddr + 4);
/* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
sys_msr_msp(*(volatile uint32_t *)appxaddr);
/* 跳转到APP */
jump2app();
}
}
main.c
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/STMFLASH/stmflash.h"
#include "./IAP/iap.h"
int main(void)
{
uint8_t t;
uint8_t key;
uint32_t lastcount = 0; /* 上一次串口接收数据值 */
uint32_t applenth = 0; /* 接收到的app代码长度 */
uint8_t clearflag = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "IAP TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
lcd_show_string(30, 110, 200, 16, 16, "KEY_UP: Copy APP2FLASH!", RED);
lcd_show_string(30, 130, 200, 16, 16, "KEY1: Run FLASH APP", RED);
lcd_show_string(30, 150, 200, 16, 16, "KEY0: Run SRAM APP", RED);
while (1)
{
if (g_usart_rx_cnt)
{
if (lastcount == g_usart_rx_cnt) /* 新周期内,没有收到任何数据,认为本次数据接收完成 */
{
applenth = g_usart_rx_cnt;
lastcount = 0;
g_usart_rx_cnt = 0;
printf("用户程序接收完成!\r\n");
printf("代码长度:%dBytes\r\n", applenth);
}
else lastcount = g_usart_rx_cnt;
}
t++;
delay_ms(100);
if (t == 3)
{
LED0_TOGGLE();
t = 0;
if (clearflag)
{
clearflag--;
if (clearflag == 0)
{
lcd_fill(30, 190, 240, 210 + 16, WHITE); /* 清除显示 */
}
}
}
key = key_scan(0);
if (key == WKUP_PRES) /* WKUP按下,更新固件到FLASH */
{
if (applenth)
{
printf("开始更新固件...\r\n");
lcd_show_string(30, 190, 200, 16, 16, "Copying APP2FLASH...", BLUE);
if (((*(volatile uint32_t *)(0X20001000 + 4)) & 0xFF000000) == 0x08000000) /* 判断是否为0X08XXXXXX */
{
iap_write_appbin(FLASH_APP1_ADDR, g_usart_rx_buf, applenth); /* 更新FLASH代码 */
lcd_show_string(30, 190, 200, 16, 16, "Copy APP Successed!!", BLUE);
printf("固件更新完成!\r\n");
}
else
{
lcd_show_string(30, 190, 200, 16, 16, "Illegal FLASH APP! ", BLUE);
printf("非FLASH应用程序!\r\n");
}
}
else
{
printf("没有可以更新的固件!\r\n");
lcd_show_string(30, 190, 200, 16, 16, "No APP!", BLUE);
}
clearflag = 7; /* 标志更新了显示,并且设置7*300ms后清除显示 */
}
if (key == KEY1_PRES) /* KEY1按键按下, 运行FLASH APP代码 */
{
printf("flash addr :%x \r\n",(*(volatile uint32_t *)(FLASH_APP1_ADDR + 4)) & 0xFF000000);
if (((*(volatile uint32_t *)(FLASH_APP1_ADDR + 4)) & 0xFF000000) == 0x08000000) /* 判断FLASH里面是否有APP,有的话执行 */
{
printf("开始执行FLASH用户代码!!\r\n\r\n");
delay_ms(10);
iap_load_app(FLASH_APP1_ADDR);/* 执行FLASH APP代码 */
}
else
{
printf("没有可以运行的固件!\r\n");
lcd_show_string(30, 190, 200, 16, 16, "No APP!", BLUE);
}
clearflag = 7; /* 标志更新了显示,并且设置7*300ms后清除显示 */
}
if (key == KEY0_PRES) /* KEY0按下 */
{
printf("开始执行SRAM用户代码!!\r\n\r\n");
delay_ms(10);
if (((*(volatile uint32_t *)(0x20001000 + 4)) & 0xFF000000) == 0x20000000) /* 判断是否为0X20XXXXXX */
{
iap_load_app(0x20001000); /* SRAM地址 */
}
else
{
printf("非SRAM应用程序,无法执行!\r\n");
lcd_show_string(30, 190, 200, 16, 16, "Illegal SRAM APP!", BLUE);
}
clearflag = 7; /* 标志更新了显示,并且设置7*300ms后清除显示 */
}
}
}
完整工程参考正点原子F407探索者HAL工程源码IAP实验。