小城故事
1. 简述
IAP(In-Application-Programming):应用编程,是应用在 Flash 程序存储器的一种编程模式,它可以在应用程序正常运行的情况下,通过调用特定的 IAP 程序对另外一段程序 Flash(User Flash) 空间进行读/写操作,甚至可以控制对某段、某页甚至某个字节的读/写操作。主要用于数据存储和固件升级。对于 IAP 应用,通常会有两个程序,第一个程序 Bootloader 程序不执行正常功能,只是通过某种方式(串口,usb,SD卡)接收第二个程序,并进行更新。第二个程序APP程序是执行的主体,用于实现应用功能。两部分项目代码都同时烧录在 User Flash 中。
执行流程如下:
![39d3b8fb82c20f4dcc0511305670153c.png](https://i-blog.csdnimg.cn/blog_migrate/f73799f41a20807c819879f3d1ee2a36.png)
第一部分代码(Bootloader 程序)必须通过其它手段,如 JTAG 或 ISP 烧入;
第二部分代码(APP 程序)可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP 代码更新。他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP 程序。
2 .STM32程序流程
2.1 STM32 正常的程序运行流程
下图为 STM32 正常的程序运行流程:
![883480d67423bcd0704fd9a0b2a9d7ac.png](https://i-blog.csdnimg.cn/blog_migrate/865c5ffc9f04df1c9127ff730b01451b.jpeg)
STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32 的内部硬件机制亦会自动将 PC 指针定位到中断向量表处,并根据中断源取出对应的中断向量执行中断服务程序。
如上图,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号 ① 所示;在复位中断服务程序执行完之后,会跳转到 main 函数,如图标号 ② 所示;而 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,如图标号 ③ 所示;然后,根据中断源进入相应的中断服务程序,如图标号 ④ 所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号 ⑤ 所示。
2.2 STM32 IAP程序运行流程
下图为 STM32 加入 IAP后的程序运行流程:
![c315241ad11074ef0b2ba085fee7802a.png](https://i-blog.csdnimg.cn/blog_migrate/148bd534a3d76505dd950c709ad6522e.jpeg)
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 相关起始地址的设置:
![38829468504594ee8f04aad2b6ac254e.png](https://i-blog.csdnimg.cn/blog_migrate/3c6d6acc029a8772f8761a48b41491e6.jpeg)
此处将 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 程序的内存。
![6629f809afaa727a76b8bed53e333984.png](https://i-blog.csdnimg.cn/blog_migrate/85c47c117fabbd390bcae37797de6b7b.png)
这里仅是示例,只要满足如下条件,可自行修改设置:
(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 程序的起始地址设置。
![613e54bdc0e644fd77f8522182748bf4.png](https://i-blog.csdnimg.cn/blog_migrate/aea5c22ba94ca2186d4b2a94f85a8c57.png)
这里的 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。
![54c83d25e46d1814c4313f9bfb4efcec.png](https://i-blog.csdnimg.cn/blog_migrate/3440bd119178000eb24749e39576c2d2.png)
至此,我们就成功配置了 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;t2)
{
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_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。
![e407fc6347eed0f9dcbf8b9de1aea7ff.png](https://i-blog.csdnimg.cn/blog_migrate/0af17df983dc0fd001b81431a7e4bc2c.png)
至此,编译通过之后,就可以得到 APP程序的 BIN 文件,通过串口助手打开 即可下载。
![7d937845ffcf93b8f157f11e0ce8c074.png](https://i-blog.csdnimg.cn/blog_migrate/0d6730966f4e2ef850e87db36f683031.jpeg)