IAP
1.内部flash相关知识:
不同型号的 STM32,其 FLASH 容量也有所不同,分为小容量。中等容量,大容量产品,内部flash大于256k的的产品是大容量产品。
对于我的大容量STM32F103RCT6来说,其被划分为 128 页,每页 2K 字节。注意,小容量和中容量产品则每页只有 1K 字节。
1)读内部FLASH(标准库库函数)
读取 FLASH 指定地址的半字的函数固件库并没有给出来,这里我们自己写的一个函数:
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
return *(vu16*)faddr;
}
//只要是擦除后的区域,读取没有那么多的要求,实际上就是取到对应地址,然后取内容即可。
2)写内部FLASH(标准库库函数)
固件库提供了三个 FLASH 写函数:
32位写入 : FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data);
32 位字节写入,其内部源码实际上是写入的两次 16 位数据。
16位写入 :FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data);
我们用这个16位,STM32 闪存的编程每次必须写入 16 位,即写入半字(halfworld)。
8位写入 :FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data);
写入 8位实际也是占用的两个地址了即2字节,跟写入 16 位基本上没啥区别。
3)解锁和加锁内部FALSH(标准库库函数)
在对内部FLASH进行写入操作时必须解锁,本质时在FLASH_KEYR 寄存器写入特定的序列(KEY1 和 KEY2)(KEY1=0X45670123,KEY2=0XCDEF89AB,stm32规定的),标准库函数以及帮我们写好这个解锁和加锁函数了,内部自动写KEY1和KEY2。
解锁:void FLASH_Unlock(void);
加锁: void FLASH_Lock(void);
4)获取内部FLASH状态(标准库库函数)
FLASH_Status FLASH_GetStatus(void);
返回值代表其状态,类型如下:
typedef enum
{
FLASH_BUSY = 1, //忙
FLASH_ERROR_PG, //编程错误
FLASH_ERROR_WRP, //写保护错误
FLASH_COMPLETE, //操作完成
FLASH_TIMEOUT //操作超时
}FLASH_Status;
5)擦除内部FLASH函数 (标准库库函数)
擦除特定地址的页:FLASH_Status FLASH_ErasePage(uint32_t Page_Address);
返回一个FLASH_Status
类型的值来表示操作的结果
擦除所有的闪存页:FLASH_Status FLASH_EraseAllPages(void);
返回一个FLASH_Status
类型的值来表示操作的结果
擦除芯片的选项字节:FLASH_Status FLASH_EraseOptionBytes(void);
(一般用不到,因为选项字节通常用于配置芯片的特定功能,例如读写保护、电源管理设置、存储区域的访问权限等。FLASH_EraseOptionBytes 函数的作用就是擦除这些选项字节,以便重新配置芯片的特定设置。)
返回一个FLASH_Status
类型的值来表示操作的结果
2.启动方式:
STM32有三种启动方式:
这里的0和1代表相对应的引脚接低高电平,我们采用主闪存存储器FLASH启动,也时默认启动方式。
在映射地址中主存储器FLASH的起始地址是 0X08000000(参考官方手册中地址映射)。
3.主闪存(FLASH)的启动流程
当产生复位,并且离开复位状态后, CM3 内核做的第一件事就是读取下列两个 32 位整数的值:
- (1)从地址 0x0800 0000 处取出堆栈指针 MSP 的初始值(一般指向SRAM的映射地址),该值就是栈顶地址。
- (2)从地址 0x0800 0004 处取出复位向量(一般指向FLASH映射地址区域内,即复位处理程序的入口地址,从而引导系统启动,复位处理程序通常包括初始化硬件、设置堆栈指针、跳转到主程序入口等操作)。
- (3)在复位中断服务程序执行完之后,会跳转到我们的main 函数(死循环)。
- (4)main 函数执行过程中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,然后,根据中断源进入相应的中断服务程序在执行完中断服务程序以后,程序再次返回 main 函数执行。
4.内存分配(芯片flash共256kb)
名称 大小 映射flash地址范围 作用
BootLoader 16k 0X4000 0X0800 0000 - 0X8003FFF 引导程序,根据
APP1 120k 0x1E000 0X0800 4000 - 0X0820 1FFF 程序运行区域
APP2 120K 0X1E000 0X082 2000 - 0X0804 0000 备份升级区域
APP1_FLAG 4Byte 0X0802 1FFC - 0X0802 2000 app1标志位,更新完后清理
APP2_FALG 4Byte 0X0803 FFFB - 0X0804 0000 app2标志位,判断是否有app升级
5.具体升级流程
首先我们需要3个工程,都保存在FLASH内部,使用STLINK下载时,直接修改下载地址即可。
(1) BootLoader : 引导工程(分16kb的flash)有以下功能
1)编写读写内部flash的相关功能代码
2)判断APP2标志位是否为我们设置的更新标志,
・有的化将APP2所在的区域(120k)写入APP1所在的区域(120k)(一定要记得先清除APP1区域的内容,否则无法写入,在 FLASH_SR 寄存器的 PGERR 位将得到一个警告)。
・没有的话跳转到APP1去运行。
3)将栈顶指针MSP偏移到APP1区域(汇编)
4)利用函数指针跳转到APP1代码的首地址,即0X0800 4000
ps:STlink烧录到0X0800 0000 , 复位后最先运行此程序,如右下图所示,魔法棒里更改
BootLoader 的main.c
#include "stm32f10x.h"
#include "stdio.h"
#include "led.h"
#include "delay.h"
#include "lcd_init.h"
#include "lcd.h"
#include "usart.h"
#include "key.h"
#include "stmflash.h"
/*****************************************************************
FLASH大小 256k
BOOTLOADER 16K 0X0800 0000 - 0X0800 3FFF 0X4000
APP1 120K 0X0800 4000 - 0X0820 1FFF 0X1E000
APP2 120K 0X0802 2000 - 0X0804 0000 0X1E000
*******************************************************************/
#define FLASH_APP1_ADDR 0X08004000 //app1起始地址
#define FLASH_APP2_ADDR 0X08022000 //app2起始地址
#define APP1_FLAG_ADDR (FLASH_APP2_ADDR-4) //app1更新标志位
#define APP2_FLAG_ADDR (0X08040000-4) //是否有更新程序标志位
#define APP_SIZE (0X1E111/STM_SECTOR_SIZE) //多少扇区,大容量一个扇区2k
#define APP_ADDR_SIZE 0X1E111
//定义一个类型,该类型为一个指向无参无返回值的函数的指针。
typedef void (*iapfun)(void);
iapfun jump2app; //定义一个函数指针 jump2app(进行函数的地址跳转)
uint16_t readBuff[STM_SECTOR_SIZE/2] = {0};
/*****************************************************************
*函 数 名 称:MSR_MSP (汇编写的)
*函 数 功 能:设置栈顶地址,传入的参数地址会被认为是栈顶指针
*函 数 形 参:addr:栈顶地址
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
__asm void MSR_MSP(u32 addr)
{
/*
MSR :用于将数据写入到特殊寄存器中。
MSP :主栈指针。
r0 :在函数调用中,r0 被用作第一个参数的传递寄存器。当一个函数被调用时,
第一个参数的值会被放置在 r0 中,然后传递给被调用的函数。
*/
MSR MSP, r0 //从r0寄存器,即参数写入主栈指针
/*
BX:分支交换(Branch and Exchange)指令。它可以实现程序的跳转控制,同时还能够切换处理器
的工作状态。
r14:链接寄存器,在函数调用过程中,用于保存函数的返回地址。
*/
BX r14 //返回到调用这个函数的地方继续执行程序。
}
/*****************************************************************
*函 数 名 称:iap_load_app
*函 数 功 能:跳转到应用程序段
*函 数 形 参:appxaddr:用户代码起始地址.
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
iapfun jump2app; //设置一个函数指针,该指针指向一个无参无返回值的函数
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(u32 appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法,确保栈顶落在0x24000000-0x24FFFFFF 之间,刚好在stm32的RAM范围内
{
printf("栈顶地址是%x\r\n",*(vu32*)appxaddr);
//将jump2app赋值为用户代码段的地址大小
jump2app=(iapfun)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址),
//
MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
printf("即将退出bootloader,运行用户代码\r\n");
Delay_nms(3000);
jump2app(); //跳转到APP.
}
}
/*****************************************************************
*函 数 名 称:UserFlashAppRun
*函 数 功 能:运行Flash中app1区域内的代码
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void UserFlashAppRun(void)
{
printf("开始执行FLASH中APP1区的用户代码!!\r\n");
//判断里面有没有程序代码,有的话,从栈顶指针偏移4字节后是PC指针
if(((*(vu32*)(FLASH_APP1_ADDR+4))&0xFF000000)==0x08000000)//判断是否为0X08XXXXXX.
{
iap_load_app(FLASH_APP1_ADDR);//执行FLASH APP代码
}else
{
printf("APP程序加载失败!\r\n");
}
}
/*****************************************************************
*函 数 名 称:UpdateFun
*函 数 功 能:进行IAP升级
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void UpdateFun(void)
{
uint16_t app2FlagBuff[2] = {0xFFFF, 0xFFFF};
printf("开始升级\r\n");
//app对应的多少扇区,用app2全部覆盖app1
for(uint16_t i=0 ; i<APP_SIZE ; i++)
{
printf("正在更新,已更新 %d 字节,%d K\r\n",i*2046,i*2); //必须16位16位写入
//app2的地址+偏移的扇区的地址 ,读取缓存区 ,一扇区半字个数
FLASH_WaitForLastOperation(1000);
STMFLASH_Read(FLASH_APP2_ADDR+i*STM_SECTOR_SIZE, readBuff, STM_SECTOR_SIZE/2);
FLASH_WaitForLastOperation(1000);
//从这里开始卡住
//APP1的地址+偏移的扇区的地址 ,读取缓存区 ,一扇区半字个数
STMFLASH_Write(FLASH_APP1_ADDR+i*STM_SECTOR_SIZE, readBuff, STM_SECTOR_SIZE/2);
}
printf("固件库更新完成!\r\n");
//升级完后清除APP2和APP1标志位,(实际上只擦除APP2即可先擦除,在写入)
STMFLASH_Erase(APP2_FLAG_ADDR,4);
STMFLASH_Erase(APP1_FLAG_ADDR,4);
// //在app2的标志位地址写入2个半字即4个字节的标志位
// STMFLASH_Write(APP2_FLAG_ADDR, app2FlagBuff, 2);
// //同理在app1的标志位地址写入2个半字即4个字节的标志位
// STMFLASH_Write(APP1_FLAG_ADDR , app2FlagBuff, 2);
printf("即将运行更新后的代码!\r\n");
//运行app1区域内更新完成的代码
UserFlashAppRun();
}
int main(void)
{
//中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//2位(4)抢占,2位(4)相应。
LED_Init(); //初始化灯
LCD_Init(); //初始化lcd屏幕
USART_Config(); //串口初始化
printf("你好世界\r\n");
LCD_Fill(0,0,128,128,GRAY);//指定区域填充颜色
KEY_Config(); //按键初始化
EXTI_Config(); //按键中断初始化
// printf("即将进入睡眠!\r\n");
// Delay_nms(5000);
// printf("进入睡眠,并且关闭屏幕\r\n");
// LCD_BLK_Clr();
uint16_t app2FlagBuff[2] = {0};
while(1)
{
//读取更新标志位
STMFLASH_Read(APP2_FLAG_ADDR , app2FlagBuff , 2);
printf("更新标志位是%x,%x",app2FlagBuff[0],app2FlagBuff[1]);
if(app2FlagBuff[0] == 0XAAAA && app2FlagBuff[1] == 0XAAAA)
{
//说明有程序更新
printf("监测到程序更新,即将更新程序\r\n");
//擦除app1的代码,不擦就会卡住
STMFLASH_Erase(FLASH_APP1_ADDR,APP_ADDR_SIZE);
printf("已经擦除APP1区域数据!\r\n");
Delay_nms(3000);
UpdateFun();
}
else
{
printf("没有新的程序,执行原有app \r\n");
Delay_nms(3000);
UserFlashAppRun();
}
Delay_nms(3000);
printf("出错,即将退出bootloader\r\n");
//APP1里由东西就不会到这里
while(1)
{
printf("出错!\r\n");
}
}
}
BootLoader 的flash.c
#include "stmflash.h"
/*****************************************************************
*函 数 名 称:STMFLASH_ReadHalfWord
*函 数 功 能:读取指定地址的半字(16bit),没什么特殊的,直接取相应地址的内容即可
*函 数 形 参:faddr 要读取的地址
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
u16 STMFLASH_ReadHalfWord (u32 faddr)
{
//vu16 = volatile unsigned short
return *(vu16 *)faddr;
}
/*****************************************************************
*函 数 名 称:STMFLASH_Read
*函 数 功 能:从指定地址开始读出指定长度的数据
*函 数 形 参://ReadAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead)
{
u16 i;
for(i=0; i<NumToRead; i++)
{
pBuffer[i] = STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
ReadAddr += 2;//偏移2个字节.
}
}
/*****************************************************************
*函 数 名 称:STMFLASH_Write_NoCheck
*函 数 功 能:不检查的写入
*函 数 形 参://WriteAddr:起始地址
//pBuffer:数据指针
//NumToWrite:半字(16位)数
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_Write_NoCheck(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite)
{
u16 i;
for(i=0; i<NumToWrite; i++)
{
FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
WriteAddr += 2;//地址增加2.
}
}
/*****************************************************************
*函 数 名 称:
*函 数 功 能:从指定地址开始写入指定长度的数据
*函 数 形 参:WriteAddr 写入的起始地址,必须是2的整数倍,即16位,2字节
pBuffer 数据指针,即要写入的数据
NumToWrite 要写入的半字(16bit)的个数
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2];//最多是2K字节,即2048/2个半字
void STMFLASH_Write(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite)
{
u32 sec_addr; //扇区地址
u16 sec_offset; //扇区内偏移地址(16bit,2字节,半字半字的算)
u16 sec_remain; //扇区内剩余地址(16bit,2字节,半字半字的算)
u32 offset_addr; //去掉0X0800 0000后的地址
u16 i;
//起始地址不在芯片的FLASH范围内 < 0x08000000 或者 > 0x08000000+FLASH大小
if(WriteAddr < STM32_FLASH_BASE || (WriteAddr >= STM32_FLASH_BASE +1024 * STM32_FLASH_SIZE))
return;
FLASH_Unlock(); //解锁
offset_addr = WriteAddr - STM32_FLASH_BASE; //实际偏移的地址=要写入的地址 - flash起始地址
sec_addr = offset_addr / STM_SECTOR_SIZE; //扇区地址即第几个扇区 = 实际偏移的地址 / 扇区大小 2048(2k)
sec_offset = (offset_addr % STM_SECTOR_SIZE)/2; //扇区内偏移的地址 = 实际偏移地址 % 扇区大小/2 (2个字节位基本单位半字)
sec_remain = STM_SECTOR_SIZE/2 -sec_offset; //扇区内剩余地址 = 扇区大小/2 (2个字节位基本单位半字)
//若写入字节不大于本扇区剩余字节
if(NumToWrite <= sec_remain)
sec_remain = NumToWrite; //令剩余长度 = 写入的长度
while(1)
{
//读取整个扇区内容 //要操作的扇区地址 //临时存入数据的数组 //半字个数
STMFLASH_Read(sec_addr * STM_SECTOR_SIZE + STM32_FLASH_BASE , STMFLASH_BUF , STM_SECTOR_SIZE/2 );
for(i=0 ; i < sec_remain ; i++) //校验数据
{
if(STMFLASH_BUF[sec_offset+i] != 0xFFFF) //如果扇区内有一个半字里非初始化的0XFFFF,则擦除整个扇区
break; //因为擦除只能针对扇区或者整个芯片
}
if(i < sec_remain) //条件成立说明扇区内有不为0xffff的半字,擦除整个扇区
{
FLASH_ErasePage(sec_offset * STM_SECTOR_SIZE + STM32_FLASH_SIZE); //擦除整个扇区
//将要写入的数据复制页内剩余长度的大小进入缓存区
for(i=0 ; i<sec_remain ; i++)
STMFLASH_BUF[i+sec_offset] = pBuffer[i];
//直接写入 写入的地址 要写入的内容 写入的半字长度
STMFLASH_Write_NoCheck(sec_addr * STM_SECTOR_SIZE + STM32_FLASH_BASE , STMFLASH_BUF , STM_SECTOR_SIZE/2);
}
else STMFLASH_Write_NoCheck(WriteAddr , pBuffer , sec_remain);//已经擦除,直接写入扇区剩余区间.
if(NumToWrite==sec_remain)
break;//写入结束了
else //写未结束
{
sec_addr++; //扇区地址增1,即第几个扇区
sec_offset = 0; //偏移位置置0
pBuffer += sec_remain; //写的内容指针偏移
WriteAddr += sec_remain; //写地址偏移
NumToWrite -= sec_remain; //字节写入熟练 -= 写入扇区的剩余半字
if(NumToWrite > (STM_SECTOR_SIZE/2))
sec_remain = STM_SECTOR_SIZE/2; //下一个扇区还是写不完
else
sec_remain = NumToWrite; //下一个扇区可以写完
}
}
FLASH_Lock(); //上锁
}
/*****************************************************************
*函 数 名 称:STMFLASH_Erase
*函 数 功 能:STMFLASH擦除函数,擦除任意大小,
*函 数 形 参:startAddr :开始擦除的地址
eraseSize :擦除的大小
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
//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;
}
BootLoader 的flash.h
#ifndef __STM_FLASH_H
#define __STM_FLASH_H
#include "stm32f10x.h"
#define STM32_FLASH_SIZE 256 //本芯片内部flash容量有多少KB
#define STM_SECTOR_SIZE 2048 //大容量产品一个扇区2k
#define STM32_FLASH_BASE 0x08000000 //STM32 FLASH的起始地址
u16 STMFLASH_ReadHalfWord (u32 faddr);
void STMFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead);
void STMFLASH_Write_NoCheck(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite) ;
void STMFLASH_Write(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite);
uint8_t STMFLASH_Erase(u32 startAddr, u32 eraseSize);
#endif
(2) APP1 : 我们运行的区域(分120kb的flash)有以下功能
1)你要的功能,比如传感器什么的,即项目运行的程序,我以屏幕颜色来区分3个工程
2)偏移中断向量表起始地址(汇编)
3)擦除APP2区域代码,方便后续写入数据到APP2区域(因为不擦除就不能写)
4)通过串口接收APP2的.bin文件,并且写入APP2区域。
ps:STlink烧录到0X0800 4000 , 复位后BootLoader会跳转到这里,如右下图所示,魔法棒里更改
APP1 的 mian.c
#include "stm32f10x.h"
#include "stdio.h"
#include "led.h" //这是我的lcd初始化,我两个app的差别就在于lcd屏幕显示不同颜色背景
#include "delay.h"
#include "lcd_init.h"
#include "lcd.h"
#include "usart.h"
#include "key.h"
#include "stmflash.h" //
extern uint32_t cnt;
int main(void)
{
/*
SCB:是系统控制块(System Control Block)的缩写,在许多微控制器中,它包含了各种系统级的控制和状态寄存器。
VTOR:(Vector Table Offset Register)是向量表偏移寄存器。它用于指定中断向量表的起始地址。
FLASH_BASE : 代表 Flash 存储器起始地址.
*/
//将中断向量表的起始地址设置为 Flash 存储器的某个特定地址加上偏移量0x4000。
SCB->VTOR = FLASH_BASE | 0x4000;//中断向量表的地址偏移 16kb
//中断优先级分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//2位(4)抢占,2位(4)相应。
LED_Init(); //初始化灯
LCD_Init(); //初始化lcd屏幕
USART_Config(); //串口初始化
printf("你好世界\r\n");
LCD_Fill(0,0,128,128,RED); //指定区域填充颜色
//在写入新程序前必须擦除,不擦除不行
printf("新的APP正在执行\r\n");
if(STMFLASH_Erase(FLASH_APP2_ADDR, APP_MAX_SIZE) == 0)
{
printf("备份区域APP2擦除成功\r\n");
}
while(1)
{
RecvOverFun();
printf("APP1 is running!\r\n");
printf("已经写入%d个字节数据\r\n",cnt);
Delay_nms(100);
}
}
APP1 的 usart.c
1#include "usart.h"
#include "stdio.h"
#include "stmflash.h"
#include "delay.h"
//固定数据长度 0xAA 0x55 0x01 0x55 0xAA,
//如果不知道数据有多长?怎么判断接受完成?
uint8_t Rx_buff[2]={0};//保存串口的数据
uint32_t cnt=0;//接受数据计数
uint8_t Rx_flag = 0;//接受完成标志
uint32_t addr = FLASH_APP2_ADDR;
uint8_t recvTime = 0;
uint16_t app2FlagBuff[2] = {0XAAAA, 0XAAAA};
/*****************************************************************
*函 数 名 称:RecvOverFun
*函 数 功 能:判断是否将APP2的bin文件写入了内部FLASAH的APP2区域
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void RecvOverFun(void)
{
if(Rx_flag == 1) {
printf("APP数据接收完成,共:%d个字节,%lf KB数据\r\n", cnt,cnt/1024.0);
cnt = 0;
Rx_flag = 0;
recvTime = 0;
u16 test_buff[2] = {0};
addr = FLASH_APP2_ADDR;
//半字半字的写,在app2末尾地址写入0XAAAA AAAA以便bootloader可以识别出来有更任务
STMFLASH_WriteHalfWord(APP2_FLAG_ADDR, 0xAAAA);
STMFLASH_WriteHalfWord(APP2_FLAG_ADDR+2, 0XAAAA);
Delay_1us();Delay_1us();Delay_1us();Delay_1us();Delay_1us();
FLASH_WaitForLastOperation(1000);
STMFLASH_Read(APP2_FLAG_ADDR,test_buff,2);
printf("app2读取到:%x,%x",test_buff[0],test_buff[1]);
printf("核对数据无误后,请按下复位按键进行数据更新r\n");
}
}
//PA9/USART1_TX PA10/USART1_RX
/*****************************************************************
*函 数 名 称:USART_Config
*函 数 功 能:初始化串口1,我这里用的是USART1,PC9,10
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void USART_Config(void)
{
USART_InitTypeDef USART_InitStruct={0};
NVIC_InitTypeDef NVIC_InitStructure={0};
//开时钟的代码
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//配置GPIO
GPIO_InitTypeDef GPIO_InitStructure={0};
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//服用推挽输出模式
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;//初始化的引脚
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//引脚速度
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;//初始化的引脚
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//浮空输入模式
GPIO_Init(GPIOA,&GPIO_InitStructure);
//开USART1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//配置USART
USART_InitStruct.USART_BaudRate = 115200;//波特率
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//RTS和CTS管脚
USART_InitStruct.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;//收发模式
USART_InitStruct.USART_Parity = USART_Parity_No;//校验选择
USART_InitStruct.USART_StopBits = USART_StopBits_1;//停止位
USART_InitStruct.USART_WordLength = USART_WordLength_8b;//数据长度
USART_Init(USART1,&USART_InitStruct);
//配置串口中断
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);//接收中断
USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);//空闲中断
//配置NVIC
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//配置中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;//抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00;//响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能通道
NVIC_Init(&NVIC_InitStructure);
//使能串口1
USART_Cmd(USART1, ENABLE);
}
/*****************************************************************
*函 数 名 称:UART1_SendData
*函 数 功 能:串口1发送数据
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void UART1_SendData(uint8_t data)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
USART_SendData(USART1,data);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC) == RESET);
}
/*****************************************************************
*函 数 名 称:UART1_SendStr
*函 数 功 能:串口1发送数据
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void UART1_SendStr(uint8_t *pdata)
{
while(*pdata != '\0')//判断字符串结束
{
UART1_SendData(*pdata);//发送字符串
pdata++;//字符串偏移
}
}
/*****************************************************************
*函 数 名 称:fputc
*函 数 功 能:重写fputc,实现串口输出
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
int fputc(int c,FILE* p)
{
UART1_SendData(c);
return c;
}
/*****************************************************************
*函 数 名 称:USART1_IRQHandler
*函 数 功 能:串口1中断函数,串口接收APP2的更新数据,并且写入APP2区域内,空闲中断用来判断是否发送完毕
*函 数 形 参:无
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void USART1_IRQHandler(void)//(再写入字库时候先屏蔽)
{
uint8_t data=0;
u16 app_temp =0;
//接收中断
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
data = USART_ReceiveData(USART1);
Rx_buff[cnt%2] = data;
//将两个8bite拼接成1个16bit的数据
if((cnt%2) == 1)
{
app_temp = ((u16)Rx_buff[1]<<8) + (u16)Rx_buff[0];
//写入FLASH
STMFLASH_WriteHalfWord(addr, app_temp);
addr+=2;
recvTime = 1;//重新计时
}
cnt++;
// //将串口1收到的数据,通过串口3发出去
// USART3->DR = data;
}
//空闲中断
if(USART_GetITStatus(USART1,USART_IT_IDLE) == SET)
{
//清空闲中断,不需要具体读到什么东西
data = USART1->SR;
data = USART1->DR;
Rx_flag = 1;//接受完成,记得用完后置0
}
}
APP1 的 usart.h
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"
#define APP_MAX_SIZE 0x1E000 //第二个app所占flsh的大小,为120k
#define FLASH_APP1_ADDR 0x08004000 //第一个应用程序起始地址(存放在FLASH)
#define FLASH_APP2_ADDR 0x08022000 //第二个应用程序起始地址(存放在FLASH)
#define APP2_FLAG_ADDR (0x08040000-4) //是否有更新程序标志位
void USART_Config(void);
void UART1_SendData(uint8_t data);
void UART1_SendStr(uint8_t *pdata);
void RecvOverFun(void);
#endif
APP1 的 stmflash.c
#include "stmflash.h"
/*****************************************************************
*函 数 名 称:STMFLASH_ReadHalfWord
*函 数 功 能:读取指定地址的半字(16位数据)
*函 数 形 参:faddr:读地址(此地址必须为2的倍数!!)
*函 数 返 回:读取的对应地址的16位数据.
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
u16 STMFLASH_ReadHalfWord(u32 faddr)
{
return *(vu16*)faddr;
}
/*****************************************************************
*函 数 名 称:STMFLASH_Write_NoCheck
*函 数 功 能:不检查的写入
*函 数 形 参:WriteAddr:起始地址
pBuffer:数据指针
NumToWrite:半字(16位)数
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_Write_NoCheck(u32 WriteAddr, u16 *pBuffer, u16 NumToWrite)
{
u16 i;
for(i=0; i<NumToWrite; i++)
{
FLASH_ProgramHalfWord(WriteAddr,pBuffer[i]);
WriteAddr += 2;//地址增加2.
}
}
/*****************************************************************
*函 数 名 称:STMFLASH_Read
*函 数 功 能:从指定地址开始读出指定长度的数据
*函 数 形 参:ReadAddr:起始地址
pBuffer:数据指针
NumToWrite:半字(16位)数
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_Read(u32 ReadAddr, u16 *pBuffer, u16 NumToRead)
{
u16 i;
for(i=0; i<NumToRead; i++)
{
pBuffer[i] = STMFLASH_ReadHalfWord(ReadAddr);//读取2个字节.
ReadAddr += 2;//偏移2个字节.
}
}
/*****************************************************************
*函 数 名 称:STMFLASH_Erase
*函 数 功 能:擦除eraseSize大小的半字数据
*函 数 形 参:startAddr :擦除的起始地址必须是2K的整数倍
numToErase :要擦除的内存大小,必须是2K的整数倍,即必须要从扇区首地址开始
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
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;
}
/*****************************************************************
*函 数 名 称:STMFLASH_WriteHalfWord
*函 数 功 能:半字数据写入
*函 数 形 参:startAddr:写入的地址
startAddr :写入的数据 16bit
*函 数 返 回:无
*作 者:YYK
*修 改 日 期:xx/xx/xx
*******************************************************************/
void STMFLASH_WriteHalfWord(u32 startAddr, u16 data)
{
FLASH_Unlock(); //解锁
//printf("flash已经解锁,写入%x,到%x\r\n",data,startAddr);
FLASH_ProgramHalfWord(startAddr, data);
FLASH_Lock();//上锁
}
APP1 的 stmflash.h
#ifndef __STMFLASH_H__
#define __STMFLASH_H__
#include "stm32f10x.h"
#define STM32_FLASH_SIZE 512 //所选STM32的FLASH容量大小(单位为K)
#if STM32_FLASH_SIZE<256
#define STM_SECTOR_SIZE 1024 //字节
#else
#define STM_SECTOR_SIZE 2048
#endif
//FLASH起始地址
#define STM32_FLASH_BASE 0x08000000 //STM32 FLASH的起始地址
//FLASH解锁键值
u16 STMFLASH_ReadHalfWord(u32 faddr); //读出半字
void STMFLASH_WriteLenByte(u32 WriteAddr,u32 DataToWrite,u16 Len); //指定地址开始写入指定长度的数据
u32 STMFLASH_ReadLenByte(u32 ReadAddr,u16 Len); //指定地址开始读取指定长度数据
void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite); //从指定地址开始写入指定长度的数据
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead); //从指定地址开始读出指定长度的数据
uint8_t STMFLASH_Erase(u32 startAddr, u32 eraseSize);
//测试写入
void Test_Write(u32 WriteAddr,u16 WriteData);
void STMFLASH_WriteHalfWord(u32 startAddr, u16 data);
#endif
(2) APP2 : 我们更新的代码(分120kb的flash),需要生产.bin文件,在APP1运行时通过串口发送过去。
1)在APP1基础上修改或添加的功能,除此之外与APP1一致,保证后续能够继续升级。
我这里只把屏幕改了个颜色。
2)在APP2中需要以.axf为基础,生成bin文件需要下面的指令
D:\TOLL\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o .\Objects\STM32F1_Template.bin .\Objects\STM32F1_Template.axf
D:\TOLL\Keil_v5\ARM\ARMCC\bin\fromelf.exe 是咱们keil根目录下的一个程序,他可以根据.axf生成.bin文件。
.\Objects\STM32F1_Template.axf 是我们工程文件夹下project下object目录里的文件
把fromelf.exe路径和STM32F1_Template.axf换成你们自己的目录
添加到如下图所示位置
之后编译生成.bin文件即可,
如左图所示,生产了.bin文件。
6.结果展示
(1)下载进去BootLoader后的程序,找不到APP1,也没有APP2更新,屏幕颜色为BootLoader程序里屏幕的初始化的浅灰色。
(2)下载进去APP1到0X0800 4000后复位,BootLoader会找到APP1起始地址后4个地址后,即PC指向是否在FLASH内,不会再报错,APP1烧录进去后就不会报错了。
程序会从BootLoader跳转到APP1里运行,屏幕颜色为APP1程序里屏幕初始化为了红色
(3)生成APP2的.bin文件后通过串口发送给单片机,此时单片机正在执行APP1,会一直等待接收消息。
进入到APP1里面后即可发送APP2的.bin文件
(4)复位后BootLoader就开始进行数据的搬移,会先删除APP1区域(120kb)代码,然后将APP2(120kb)代码搬到APP1区域中。
屏幕颜色为APP2程序中屏幕初始化为黄色:
至此IAP结束,后面我会更新OTA的文章,IAP中需要注意两点
1)受限于内存大小原因BootLoader中我直接删除了APP1区域的代码,搬运APP2到APP1。这是极其不应该的,因为如果APP2区域的内容有损坏或者搬运过程中断电,那么程序就会丢失,单片机会变砖头。用户又没有keil软件,可烧录不了程序。稳妥起见应该预留一个APP3区域来备份当前可以运行的程序。
2) 本次操作受限于设备,所有FLASH均为内部FLASH,当然也可以用外部的FLASH,流程一样,只不过APP2的区域要在外部FLASH。