一、IAP简介
IAP(In Application Programming)即在应用编程, IAP 是用户自己的程序在运行过程中对
User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产
品中的固件程序进行更新升级。
通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两部分代码,第一部分程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、 USART)接收程序或数据,执行对第二部分代码的更新;第二部分代码才是真正的功能代码。
两部分代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一部分代码开始运行,它作如下操作:
- 检查是否需要对第二部分代码进行更新;
- 如果不需要更新则转到第二部分代码执行;
- 执行更新操作;
- 跳转到第二部分代码执行;
第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP代码更新。
将第一部分项目代码称之为 Bootloader 程序,第二部分代码称之为 APP 程序,他们存放在STM32F4 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是APP 程序(注意,如果 FLASH 容量足够,是可以设计很多 APP 程序的,同时还可以分两个bank区)。这样我们就是要实现 2 个程序: Bootloader 和 APP。
STM32F4 的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行。
二、正常的程序运行流程
内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。
程序启动后,将首先从"中断向量表"取出复位中断向量执行复位中断程序完成启动,而这张"中断向量表"的起始地址是 0x08000004,当中断来临, 内部硬件机制亦会自动将 PC 指针定位到"中断向量表"处,并根据中断源取出对应的中断向量执行中断服务程序。
复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断), PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。
三、IAP 程序的运行流程
程序还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数
在执行完 IAP 以后(即将新的 APP 代码写入 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,但在不同位置上,共有两个中断向量表。
**通俗讲:**其实就是在正常运行的程序(APP程序)之前加上一个程序(bootloader程序), bootloader程序运行完之后再运行APP程序,两个是独立的个体,都需要有自己的中断。所以bootloader程序运行完之后就需将中断向量表位置移到APP程序的位置,不然APP程序发生中断响应回去执行bootloader程序的中断程序。
IAP 程序必须满足两个要求:
- 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
- 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
四、运用实例
bootloader程序开机的时候先显示提示信息,然后等待串口输入接收 APP 程序(无校验,一次性接收),在串口接收到 APP 程序之后,即可执行 IAP。
按下 KEY1 按键,将串口接收到的 APP 程序存放到 STM32F103 的 FLASH,之后再按 KEY2 既可以跳转执行这个 FLASH APP 程序。
Bootloader 程序:
main.c
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_key.h"
#include "bsp_TiMbase.h"
#include "bsp_usart.h"
/* 用于获取接收数据的计数 */
uint32_t USART_RX_CNT;
/* 串口接收的数据保存在ram的0X20001000地址中 */
uint8_t USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));
/* 接收数据函数处理 */
void Receive_App_Data(void)
{
static uint16_t oldcount = 0;
static uint16_t applenth = 0;
static uint16_t com_delay = 0;
if (com_delay < 1000)
{
com_delay++;
}
else
{
if (USART_RX_CNT)
{
if (oldcount == USART_RX_CNT)
{
applenth = USART_RX_CNT;
printf("Receiving APP data succeeded.\r\n");
printf("Code len: %dBytes\r\n",applenth);
oldcount = 0;
USART_RX_CNT = 0;
}
else
{
oldcount = USART_RX_CNT;
}
}
}
/* 扫描Key1, 将接收的app数据写进内部flash中 */
if(Key_Scan(KEY1_GPIO_PORT, KEY1_GPIO_PIN) == KEY_ON)
{
if(applenth)
{
printf("Start updating firmware...\r\n");
/* 判断APP程序的复位地址是否在范围内 */
if(((*(volatile uint32_t *)(0x20001000 + 4)) & 0xFF000000) == 0x08000000)
{
Iap_Write_Appbin(FLASH_APP1_ADDR, USART_RX_BUF, applenth);
printf("Firmware update complete!\r\n");
}
else
{
printf("Non-flash applications!\r\n");
}
}
else
{
printf("There is no firmware to update!\r\n");
}
}
}
int main(void)
{
USART_Config();
LED_GPIO_Config();
Key_GPIO_Config();
BASIC_TIM_Init();
printf("IAP Test !\n");
while(1)
{
if( Key_Scan(KEY2_GPIO_PORT, KEY2_GPIO_PIN) == KEY_ON)
{
printf("Start APP code!\r\n");
if(((*(volatile uint32_t *)(FLASH_APP1_ADDR + 4)) & 0xFF000000)==0x08000000)
{
printf("success\n");
/* 跳转到App程序中,最好放在主循环里,不要放在定时器里 */
Iap_Load_App(FLASH_APP1_ADDR);
}
else
{
printf("Non-flash applications cannot be executed!\r\n");
}
}
}
}
主要运行跳转到app的函数。
iap.c
#include "iap.h"
uint16_t iapbuf[1024]; //2K字节缓存
iapfun jump_to_app;
/* 以每2k字节进行写入 */
void Iap_Write_Appbin(uint32_t appxaddr, uint8_t *appbuf, uint32_t appsize)
{
uint16_t t;
uint16_t i=0;
uint16_t temp;
uint32_t fwaddr = appxaddr;//当前写入的地址
uint8_t *dfu = appbuf;
for(t = 0; t < appsize; t += 2)
{
temp = (u16)dfu[1] << 8;
temp += (u16)dfu[0];
dfu += 2;//偏移2个字节
iapbuf[i++] = temp;
if(i == 1024)
{
i = 0;
Flash_Write(fwaddr, iapbuf, 1024);
fwaddr += 2048;//偏移2048 16=2*8.所以要乘以2.
}
}
if (i)
Flash_Write(fwaddr, iapbuf, i);//将最后的一些内容字节写进去.
}
void Iap_Load_App(uint32_t appxaddr)
{
/* 检查栈顶地址是否合法 */
if(((*(volatile uint32_t *)appxaddr) & 0x2FFE0000) == 0x20000000)
{
/* 关中断 */
__disable_irq();
/* 关闭外设时钟 */
RCC->APB1ENR = 0;
RCC->APB2ENR = 0;
SysTick->CTRL = 0;
/* 设置主堆栈指针 */
__set_MSP(*(volatile uint32_t *)appxaddr);
/* APP程序的复位地址 */
jump_to_app = (iapfun) * (volatile uint32_t *)(appxaddr + 4);
jump_to_app();
}
}
/*********************************************END OF FILE**********************/
主要是将app程序写入flash,和跳转到app操作。
注意:一定要关中断和关闭外设(由于本程序用了TIM,所以关了中断,没用也可以不关,但最好是关)
串口中断-:
// 串口中断服务函数
void DEBUG_USART_IRQHandler(void)
{
uint8_t ucTemp;
if(USART_GetITStatus(DEBUG_USARTx, USART_IT_RXNE)!=RESET)
{
ucTemp = USART_ReceiveData(DEBUG_USARTx);
USART_SendData(DEBUG_USARTx,ucTemp);
if(USART_RX_CNT < USART_REC_LEN)
{
USART_RX_BUF[USART_RX_CNT] = ucTemp;
USART_RX_CNT++;
}
}
}
主要是接收app程序。
定时器中断-bsp_TiMbase.c:
void BASIC_TIM_IRQHandler (void)
{
if (TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET )
{
LED_Test();
Receive_App_Data();
TIM_ClearITPendingBit(BASIC_TIM , TIM_FLAG_Update);
}
}
运行LED闪烁程序和接收app数据并写入到flash里。
bootloader程序大小的确定:
双击工程得到.map文件看到该程序占用10.99kb大小。
使用11kb进行计算得出:2c00,但我使用了5000,为了方便扩展,当然你可以设置该大小。
APP 程序:
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_TiMbase.h"
volatile uint32_t time = 0; // ms 计时变量
int main(void)
{
SCB->VTOR = 0x08005000;
__enable_irq();
LED_GPIO_Config();
BASIC_TIM_Init();
while(1)
{
if ( time == 1000 ) /* 1000 * 1 ms = 1s 时间到 */
{
time = 0;
LED1_TOGGLE;
}
}
}
主要使1s LED闪烁一次
/* 设置中断向量表的偏移量 */
SCB->VTOR = 0x08005000;
/* 使能中断 */
__enable_irq();
这两个必须要加上,否则无法跳转。
用XCOM进行app文件的发送:
按下KEY1:
按下KEY2: