1 IAP介绍 IAP(In Application Programming)即在应用编程, IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信接口对产品中的固件程序进行更新升级。通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、 USART,蓝牙)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作: - 检查是否需要对第二部分代码进行更新;
- 如果不需要更新则转到第二部分代码执行;
- 如果需要更新则执行更新操作;
- 跳转到第二部分代码执行;
第一部分代码的烧入必须通过其它手段,如 JTAG 或 ISP;第二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP代码更新。 我们将第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP(功能代码) 程序,他们存放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP 程序。这样我们就是要实现 3 个程序:Bootloader 和 APP1和APP2。 我们先来看看 STM32 正常的程序运行流程: STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32 的内部硬件机制会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。 STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程中,如果收到中断请求,此时 STM32 强制将 PC 指针指回中断向量表处,如图标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。 当加入 IAP 程序之后,程序运行流程如图: 加入 IAP 之后程序运行流程图中,STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示,在执行完 IAP 以后(即将新的 APP 代码写入 STM32的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。 在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。 通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求: - 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始;
- 必须将新程序的中断向量表相应的移动,移动的偏移量为 x;
2 内存分区 我们写的代码最终都会被编译成二进制文件并保存在Flash中,那么我们就可以进一步对我们程序进行分区。STM32的闪存模块由:主存储器、信息块和闪存存储器接口寄存器等 3 部分组成。 1)主存储器,该部分用来存放代码和数据常数(如 const 类型的数据)。对于大容量产品,其被划分为 256 页,每页 2K 字节。注意,小容量和中容量产品则每页只有 1K 字节。从上图可以看出主存储器的起始地址就是0X08000000, B0、B1 都接 GND 的时候,就是从 0X08000000开始运行代码的。 2)信息块,该部分分为 2 个小部分,其中启动程序代码,是用来存储 ST 自带的启动程序,用于串口下载代码,当 B0 接 V3.3,B1 接 GND 的时候,运行的就是这部分代码。 3)闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制机构。对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电压由内部产生。 那么我们分区就是主要对FLASH的主存储器进行分区,STM32F103ZE共512K的Flash大小,我们将它分成三个区,BootLoader区、App1区、App2区(备份区)具体划分如下表: BootLoader区存放启动代码 App1区存放应用代码 App2区存放暂存的升级代码 0x08000000---0x08007FFFF 3 整体设计流程图先执行BootLoader程序, 先去检查APP2区有没有程序,如果有程序就将App2区(备份区)的程序拷贝到App1区, 然后再跳转去执行App1的程序。 然后执行App1程序, 因为BootLoader和App1这两个程序的向量表不一样, 所以跳转到App1之后第一步是先去更改程序的向量表,然后再去执行其他的应用程序。 在应用程序里面会加入程序升级的部分,这部分主要工作是拿到升级程序,然后将他们放到App2区(备份区),以便下次启动的时候通过BootLoader更新App1的程序。流程图如下图所示: 4 Boot Loader的代码编写 在BootLoader程序中,我们主要实现对APP2区域的中的标志检查,读取APP2内部的程序,写入APP1地址程序,代码执行跳转程序。 具体的代码执行过程,我们参考示例代码来掌握,整个执行流程如下图所示: 5 APP1代码编写 在整个APP代码的编写上,我们首先修改向量表, 因为本程序是由BootLoader跳转过来的, 不修改向量表后面会出现问题;需要在APP的基本功能上加入串口接收数据并保存到APP2(备份区)的功能代码。 具体的代码执行过程,我们参考示例代码来掌握,整个执行流程如下图所示: 在APP程序的编译时,我们需要修改地址属性,具体如下图所示:
6 APP2代码编写 APP2的代码,理论上来说和APP1的代码除了基本功能不同外,在代码上级上的功能一样。只是APP2的代码,我们可以直接将编译后的二进制文件使用串口等方式发送(也可以无线远程发送,SD卡等方式存储)到芯片内部,并存储在APP2的地址区域内。事实上接收和存储的过程是运行的APP1的程序,然后将接收到的数据存放到APP2的地址区域内。 APP2的编译地址和APP1保持一致,但是我们发送的文件并不是hex文件,而是编译后的bin文件,那如何生成bin文件呢?需要修改配置属性: D:\Keil_v5\Arm\ARMCC\bin\fromelf.exe --bin -o .\Objects\weather.bin .\Objects\weather.axf E:\keil\ARM\ARMCC\bin\fromelf.exe--bin -o .\Objects\test.bin .\Objects\test.axf 其中:D:\Keil_v5\Arm\ARMCC\bin\fromelf.exe 是一个keil自带的生成bin文件的工具绝对路径。 通过这一步设置,我们就可以在 MDK 编译成功之后,调用 fromelf.exe(注意,我的 MDK 是安装在 D盘文件夹下,如果你是安装在其他目录,请根据你自己的目录修改fromelf.exe 的路径),根据当前工程的 weather.axf(如果是其他的名字,请记住修改,这个文件存放在 Objects 目录下面,格式为 xxx.axf),生成一个 RTC.bin 的文件。并存放在 axf 文件相同的目录下,即工程的 Objects 文件夹里面。在得到.bin 文件之后,我们只需要将这个 bin 文件传送给单片机,即可执行 IAP 升级。(我们也可以将bin文件无线发送,存放在SD卡内,存放在外部FLASH内等等方式进行代码升级,其中无线发送的形式叫OTA)。 我们把APP2生成的bin文件,通过串口,发送到APP1的运行设备上,就会自动的保存APP2的代码数据到对应的Flash地址下,那么按下复位按键后(也可以软件复位),再次运行bootloader代码,就会加载APP2的数据到APP1的地址下,并运行新的程序。 stm32内部flash操作相关函数//读取指定地址的半字(16位数据)//faddr:读地址(此地址必须为2的倍数!!)//返回值:对应数据.u16 STMFLASH_ReadHalfWord(u32 faddr){ return *(vu16*)faddr; }//不检查的写入//WriteAddr:起始地址//pBuffer:数据指针//NumToWrite:半字(16位)数 void STMFLASH_Write_NoCheck(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite) { u16 i; for(i=0; i<NumToWrite; i++) { FLASH_ProgramHalfWord(WriteAddr,pBuffer); WriteAddr += 2;//地址增加2. } } //从指定地址开始读出指定长度的数据//ReadAddr:起始地址//pBuffer:数据指针//NumToWrite:半字(16位)数void STMFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead) { u16 i; for(i=0; i<NumToRead; i++) { pBuffer = STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节. ReadAddr += 2;//偏移2个字节. }}//startAddr地址必须是2K的整数倍//numToErase 要擦除的内存大小,必须是2K的整数倍uint8_t STMFLASH_Erase(u32 startAddr, u32 eraseSize) { uint16_t i=0; uint16_t len = eraseSize/STM_SECTOR_SIZE; if(startAddr<STM32_FLASH_BASE||(startAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE))) return -1;//非法地址 if((startAddr % STM_SECTOR_SIZE)) return -2;//非法地址 FLASH_Unlock();//解锁 for(i=0; i<len; i++) { FLASH_ErasePage(startAddr+i*STM_SECTOR_SIZE);//擦除这个扇区 } FLASH_Lock();//上锁 return 0;}//半字数据写入void STMFLASH_WriteHalfWord(u32 startAddr, u16 data){ FLASH_Unlock(); //解锁 FLASH_ProgramHalfWord(startAddr, data); FLASH_Lock();//上锁}
这段代码是针对STM32单片机的FLASH操作的函数实现。主要包括读取指定地址的半字数据、不检查的写入数据、从指定地址开始读出指定长度的数据、擦除指定地址范围内的FLASH数据以及半字数据的写入。 具体分析如下: - STMFLASH_ReadHalfWord函数用于读取指定地址的半字(16位数据),通过将地址转换为指针,然后读取指针对应的数据。
- STMFLASH_Write_NoCheck函数用于不进行检查的写入数据,通过循环将数据逐个写入指定地址,并递增地址。
- STMFLASH_Read函数用于从指定地址开始读出指定长度的数据,通过循环将每个地址对应的半字数据读取到指定的数据缓冲区中。
- STMFLASH_Erase函数用于擦除指定地址范围内的FLASH数据,先判断地址的合法性,然后对每个扇区进行擦除操作。
- STMFLASH_WriteHalfWord函数用于将半字数据写入指定地址,先解锁FLASH,然后进行半字数据的写入,最后上锁FLASH。
这段代码主要实现了对STM32单片机的FLASH进行读取、写入和擦除操作的功能。可以通过调用这些函数来实现对FLASH的数据存储和擦除操作。
|