前言:
为了方便查看博客,特意申请了一个公众号,附上二维码,有兴趣的朋友可以关注,和我一起讨论学习,一起享受技术,一起成长。
1. 简述
IAP(In-Application-Programming):应用编程,是应用在 Flash 程序存储器的一种编程模式,它可以在应用程序正常运行的情况下,通过调用特定的 IAP 程序对另外一段程序 Flash(User Flash) 空间进行读/写操作,甚至可以控制对某段、某页甚至某个字节的读/写操作。主要用于数据存储和固件升级。对于 IAP 应用,通常会有两个程序,第一个程序 Bootloader 程序不执行正常功能,只是通过某种方式(串口,usb,SD卡)接收第二个程序,并进行更新。第二个程序APP程序是执行的主体,用于实现应用功能。两部分项目代码都同时烧录在 User Flash 中。
执行流程如下:
第一部分代码(Bootloader 程序)必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码(APP 程序)可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP 代码更新。他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP 程序。
2 .STM32程序流程
2.1 STM32 正常的程序运行流程
下图为 STM32 正常的程序运行流程:
STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32 的内部硬件机制亦会自动将 PC 指针定位到中断向量表处,并根据中断源取出对应的中断向量执行中断服务程序。
如上图,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号 ① 所示;在复位中断服务程序执行完之后,会跳转到 main 函数,如图标号 ② 所示;而 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,如图标号 ③ 所示;然后,根据中断源进入相应的中断服务程序,如图标号 ④ 所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号 ⑤ 所示。
2.2 STM32 IAP程序运行流程
下图为 STM32 加入 IAP后的程序运行流程:
STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号 ① 所示,此部分同正常执行的程序一样;在执行完 IAP 以后(即将新的 APP 代码写入 STM32 的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号 ② 和 ③ 所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。
在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址 0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号 ④ 所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号 ⑤ 所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号 ⑥ 所示。
通过以上两个过程的分析,IAP 程序必须满足两个要求:
(1)新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
(2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
3 .IAP 更新固件地址设置方法
3.1 SRAM 中运行程序地址设置
如下图所示,我们可以查到 STM32 相关起始地址的设置:
此处将 IROM1 的起始地址(Start)定义为: 0X20001000,大小为 0XA000(40K 字节),即从地址 0X20000000 偏移 0X1000 开始, 之后的 40K 字节,用于存放 APP 代码。因为整个STM32F103RCT6 的 SRAM 大小为 48K 字节, 且偏移了 4K(0X1000), 所以 IRAM1(SRAM)的起始地址变为 0X2000B000(0X1000+0XA000),大小只有 0X1000(4K 字节)。这样,整个 STM32F103RCT6 的 SRAM 分配情况为:最开始的 4K 给 Bootloader 程序使用,随后的 40K 存放 APP 程序,最后 4K,用作 APP 程序的内存。
这里仅是示例,只要满足如下条件,可自行修改设置:
(1)保证偏移量为 0X200 的倍数(这里为 0X1000);
(2) IROM1 的容量最大为 41KB(因为 IAP 代码里面接收数组最大是 41K 字节);
(3) IROM1 的地址区域和 IRAM1 的地址区域不能重叠;
(4) IROM1 大小 + IRAM1 大小,不要超过 44KB(48K-4K)。
3.2 FLASH 中运行程序地址设置
默认的条件下, IROM1 的起始地址(Start)一般为 0X08000000,大小(Size)为 0X40000,即从 0X08000000 开始的 256K 空间为我们的程序存储区。如下图,设置起始地址(Start)为0X08010000,即偏移量为 0X10000(64K 字节),因而,留给 APP 用的 FLASH 空间(Size)只有0X80000-0X10000=0X30000(192K 字节)大小了。设置好 Start 和 Szie,就完成 APP 程序的起始地址设置。
这里的 64K 字节不是固定的,可以根据 Bootloader 程序大小进行不同设置, 理论上只需要确保 APP 起始地址在 Bootloader 之后,并且偏移量为 0X200 的倍数即可。 比如本章的 Bootloader 程序为 30K 左右,设置为 64K,还留有 20K 左右的余量供后续在 IAP 里面新增其他功能之用。
3.3 中断向量表的偏移量设置方法
系统启动的时候,会首先调用 systemInit 函数初始化时钟系统,同时 systemInit 还完成了中断向量表的设置,可以打开 systemInit 函数,看看函数体的结尾处的几行代码:
void SystemInit (void)
{
/* Reset the RCC clock configuration to the default reset state(for debug purpose) */
/* Set HSION bit */
RCC->CR |= (uint32_t)0x00000001;
/* Reset SW, HPRE, PPRE1, PPRE2, ADCPRE and MCO bits */
#ifndef STM32F10X_CL
RCC->CFGR &= (uint32_t)0xF8FF0000;
#else
RCC->CFGR &= (uint32_t)0xF0FF0000;
#endif /* STM32F10X_CL */
/* Reset HSEON, CSSON and PLLON bits */
RCC->CR &= (uint32_t)0xFEF6FFFF;
/* Reset HSEBYP bit */
RCC->CR &= (uint32_t)0xFFFBFFFF;
/* Reset PLLSRC, PLLXTPRE, PLLMUL and USBPRE/OTGFSPRE bits */
RCC->CFGR &= (uint32_t)0xFF80FFFF;
#ifdef STM32F10X_CL
/* Reset PLL2ON and PLL3ON bits */
RCC->CR &= (uint32_t)0xEBFFFFFF;
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x00FF0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#elif defined (STM32F10X_LD_VL) || defined (STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
/* Reset CFGR2 register */
RCC->CFGR2 = 0x00000000;
#else
/* Disable all interrupts and clear pending bits */
RCC->CIR = 0x009F0000;
#endif /* STM32F10X_CL */
#if defined (STM32F10X_HD) || (defined STM32F10X_XL) || (defined STM32F10X_HD_VL)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif
/* Configure the System clock frequency, HCLK, PCLK2 and PCLK1 prescalers */
/* Configure the Flash Latency cycles and enable prefetch buffer */
SetSysClock();
#ifdef VECT_TAB_SRAM
SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#else
SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH. */
#endif
}
VTOR 寄 存 器 存 放 的 是 中 断 向 量 表 的 起 始 地 址 。 默 认 的 情 况VECT_TAB_SRAM 是没有定义,所以执行 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET;
对于 FLASH APP,设置为 FLASH_BASE + 偏移量 0x10000,所以可以在 FLASH APP 的 main 函数最开头处添加如下代码实现中断向量表的起始地址的重设:
SCB->VTOR = FLASH_BASE | 0x10000;
以上是 FLASH APP 的情况。
当使用 SRAM APP 的时候,设置起始地址为:
SCB->VTOR = SRAM_BASE | 0x1000;
这样, 就完成了中断向量表偏移量的设置。只要固件的程序大小不超过定义的大小,就可以通过 IAP 顺利更新代码。
注:
(1)IAP 更新程序烧写的是 bin 格式的文件,而不是 hex文件,区别请参见STM32学习笔记一一HEX文件和BIN文件格式
(2)bin 文件生成:通过 MDK 自带的格式转换工具 fromelf.exe,来实现 .axf 文件到 .bin 文件的转换。该工具在 MDK 的安装目录 \ARM\ARMCC\bin(与KEIL的版本有关)文件夹里面。fromelf.exe 转换工具的语法格式为:fromelf [options] input_file。
至此,我们就成功配置了 IAP 烧写程序的步骤了。
4 .软件实现
软件分为两部分:Bootloader + app程序,Bootloader 主要是配置串口接收数据,设置程序开始更新的地址,即 IAP 跳转地址处。app 程序就是具体的固件功能实现。
4.1 串口IAP 配置
typedef void (*iapfun)(void); //定义一个函数类型的参数.
#define FLASH_APP1_ADDR 0x08010000 //第一个应用程序起始地址(存放在FLASH)
//保留0X08000000~0X0800FFFF的空间为Bootloader使用(64KB)
void iap_load_app(u32 appxaddr); //跳转到APP程序执行
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 applen); //在指定地址开始,写入bin
#endif
/
iapfun jump2app;
u16 iapbuf[1024];
//appxaddr:应用程序的起始地址
//appbuf:应用程序CODE.
//appsize:应用程序大小(字节).
void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize)
{
u16 t;
u16 i=0;
u16 temp;
u32 fwaddr=appxaddr;//当前写入的地址
u8 *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;
STMFLASH_Write(fwaddr,iapbuf,1024);
fwaddr += 2048;//偏移2048 16=2*8.所以要乘以2.
}
}
if(i)
STMFLASH_Write(fwaddr,iapbuf,i);//将最后的一些内容字节写进去.
}
/跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app = (iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump2app(); //跳转到APP.
}
}
串口接收配置:
#define USART_REC_LEN 41*1024 //定义最大接收字节数 41K
//u8 USART_RX_BUF[USART_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.
u8 USART_RX_BUF[USART_REC_LEN] __attribute__ ((at(0X20001000)));//接收缓冲,最大USART_REC_LEN个字节,起始地址为0X20001000.
//接收状态
//bit15, 接收完成标志
//bit14, 接收到0x0d
//bit13~0, 接收到的有效字节数目
u16 USART_RX_STA=0; //接收状态标记
u16 USART_RX_CNT=0; //接收的字节数
#if EN_USART1_RX //如果使能了接收
void USART1_IRQHandler(void) //串口1中断服务程序
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
if(USART_RX_CNT<USART_REC_LEN)
{
USART_RX_BUF[USART_RX_CNT]=Res;
USART_RX_CNT++;
}
}
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
这样串口就可以接收数据了。我们在设计一个程序把 APP 程序的数据通过串口接收进行更新,就可以实现 IAP 的功能了。
这里举一个flash & sram运行的程序更新:
int main(void)
{
u8 t;
u8 key;
u16 oldcount=0; //老的串口接收数据值
u16 applenth=0; //接收到的app代码长度
u8 clearflag=0;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
delay_init(); //延时函数初始化
uart_init(256000); //串口初始化为256000
LED_Init(); //初始化与LED连接的硬件接口
KEY_Init(); //按键初始化
while(1)
{
if(USART_RX_CNT)
{
if(oldcount==USART_RX_CNT)//新周期内,没有收到任何数据,认为本次数据接收完成.
{
applenth=USART_RX_CNT;
oldcount=0;
USART_RX_CNT=0;
printf("用户程序接收完成!\r\n");
printf("代码长度:%dBytes\r\n",applenth);
}
else
oldcount=USART_RX_CNT;
}
t++;
delay_ms(10);
if(t==200)
{
LED0=!LED0;
t=0;
}
key=KEY_Scan(0);
if(key==WKUP_PRES) //WK_UP按键按下
{
if(applenth)
{
printf("开始更新固件...\r\n");
printf("Copying APP2FLASH...\r\n");
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_write_appbin(FLASH_APP1_ADDR,USART_RX_BUF,applenth);//更新FLASH代码
printf("Copy APP Successed!!");
printf("固件更新完成!\r\n");
}else
{
printf("Illegal FLASH APP! \r\n");
printf("非FLASH应用程序!\r\n");
}
}else
{
printf("没有可以更新的固件!\r\n");
printf("No APP!\r\n");
}
clearflag=7;//标志更新了显示,并且设置7*300ms后清除显示
}
if(key==KEY1_PRES)
{
printf("开始执行FLASH用户代码!!\r\n");
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}else
{
printf("非FLASH应用程序,无法执行!\r\n");
printf("Illegal FLASH APP!\r\n");
}
}
if(key==KEY0_PRES)
{
printf("开始执行SRAM用户代码!!\r\n");
if(((*(vu32*)(0X20001000+4))&0xFF000000)==0x20000000)//判断是否为0X20XXXXXX.
{
iap_load_app(0X20001000);//SRAM地址
}else
{
printf("非SRAM应用程序,无法执行!\r\n");
printf("Illegal SRAM APP!\r\n");
}
}
}
}
注:FLASH 需写入数据,SRAM 是直接运行的。对于在 STM32 的片上 FLASH 写入数据,请参看 :STM32学习笔记一一FLASH 模拟 EEPROM
对于 app 程序,我们只需在原先的程序上在 main 函数的入口处加上地址跳转和 IROM1、IRAM1 的起始地址和大小,就可以作为一个可以通过 IAP 更新的程序:
eg:flash上运行的APP。
至此,编译通过之后,就可以得到 APP程序的 BIN 文件,通过串口助手打开 即可下载。
参考:
2.原子库函数手册