我们平常常用的ISP下载,实际上是通过单片机专用的串行编程接口对单片机内部的Flash存储器进行编程(通过厂商固化好的程序),需要外部电路辅助实现,而IAP则是通过将Flash映射为两个区域,一部分为用户引导程序区BOOT,一部分为用户程序,利用引导程序实现在程序内编程(IAP)。 而485也好,串口也好,只是硬件传输上的协议,本质方法都是一样的。
整体思路就是Flash映射为两部分,首先运行引导程序,发送指令和程序包给stm32,再让stm32将sram储存的内容写入Flash的第二部分,再让主进程跳转至Flash的第二部分即可。
第一步首先我们需要将Flash划分为两个区域,一部分为BOOT,一部分为用户程序。这就需要我们知道BOOT程序的大概大小。
通过keil5编译程序我们可以得到
linking... Program Size: Code=7252 RO-data=336 RW-data=60 ZI-data=48068 FromELF: creating hex file... "..\OBJ\USART.axf" - 0 Error(s), 0 Warning(s). Build Time Elapsed: 00:00:01
keil5代码分为两部分:
1. code,即程序代码部分
2. inline data. 即 literal pools(文字常量池), and short strings(短字符串)等
另外 RO Data: read-only data,只读的数据
RW Data: read write data,可读写的数据
ZI Data: zero initialized data,零初始化的可读写变量,存放未初始化的全局变量及初始化为0的变量
RO size: Code + RO_data,表示程序占用Flash空间的大小。 RW size: RW_data + ZI_data,表示运行时占用RAM的大小。
可以计算出我们的上面程序所占Flash大小为(7252+336)/1024=7K。
我采用的是stm32f103mini板,可以从keil5魔术棒Target栏看到
主要参数如下
CPU:STM32F103RCT6,LQFP64,FLASH:256KB,SRAM:48KB; flash起始地址为0x08000000,大小为0x4000(16进制)—>262144字节(10进制)—>256KB RAM起始地址为0x20000000,大小为0xC000(16进制)—>49125字节(10进制)—>48KB
从Flash起始到我们的用户代码区要预留充足的空间给引导程序。这里我预留了0x5000=20K,不够可以在加,但已经绰绰有余了。即我们的用户程序起始地址为0x08005000。
1.栈区stack:由编译器自动分配释放,存放函数的参数值,局部变量的值。
2.堆区heap:由程序员分配和释放,若程序员不释放,程序结束时由OS回收。
3.全局区(静态区 static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量、未初始化的静态变量在相邻的另一块区域。
4.文字常量区:常量字符串就是放在这里的。
5.程序代码区 : 存放函数体的二进制代码。
两个比较关键的指针
PC:Program Counter,是通用寄存器,但是有特殊用途,用来指向当前运行指令的下一条指令。
SP:Stack Pointer,堆栈指针,也是通用寄存器,用于入栈和出栈操作。
stm32矢量表 上图
转载-STM32片上FLASH内存映射、页面大小、寄存器映射 - kanite - 博客园
MSP主栈指针,reset复位向量入口地址,外设中断向量表IRQ0-More IRQs
stm32上电启动有3种,从flash种启动,从sram种启动,从系统存储器种启动。STM32上电复位以后,代码区都是从0x00000000开始的,三种启动模式只是将各自存储空间的地址映射到0x00000000中。stm32以为自己执行的是0x00000000,但实际却不是,只是被骗了。
第一步--- 芯片自动从0地址读取32位整数,设置MSP主堆栈指针 即设置SP=initial main sp。
第二部---芯片自动从4地址读取32位整数,并跳转到该地址执行 即PC->复位程序。
IAPFlash的写入采用的是正点原子的代码。
//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个字节 IAP_buff[i++]=temp; if(i==1024) { i=0; STMFLASH_Write(fwaddr,IAP_buff,1024); fwaddr+=2048;//偏移2048 16=2*8.所以要乘以2. } } if(i)STMFLASH_Write(fwaddr,IAP_buff,i);//将最后的一些内容字节写进去. }
代码具体是将我们要写入的u8类型数组每两个拼成一个u16数据,再把这些u16装到个BUFF里一共1024个(2K字节),满了便整体写入flash里,最后再把剩余的也写入。
定义我们串口接收数据的BUFF,sram中我们接收数据的地址,可以使用__attribute__
u8 Appreceive_buff[Max_lenth] __attribute__ ((at(0x20001000)));
需要修改代码储存地址和大小,以及中断向量表的映射。
中断向量表的映射即0x08000000开始的代码的表换成0x08005000代码的表
SCB->VTOR = FLASH_BASE | 0x05000; /* Vector Table Relocation in Internal FLASH. */
程序要运行起来,必须要经过四个步骤:预处理、编译、汇编和链接。
我们需要将我们的用户代码转化为bin文件。bin文件是最纯粹的二进制机器代码,也就是可执行文件。
keil5中魔术棒--user--After Build/Rebuild勾选--选择路径即keil中ARM/ARMCC/bin/fromelf.exe--添加" --bin -o ..\OBJ\XXX.bin ..\OBJ\XXX.axf" 即可生成bin文件,其中XXX为生成代码名称。
之后我们便可以通过串口助手发送bin文件,波特率设大些,否则很慢。
另外加入通讯协议,在运行引导程序时,必须发送“ok”返回“ok”才能发送程序,发送完成后发送“++”实现升级和跳转。
//usart1_state 2开始接收,5开始升级,6升级完成,7开始跳转 void USART1_IRQHandler(void) //串口1中断服务程序 { u8 Res; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断 { Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据 if(usart1_state==2) { if(usart1_cnt<Max_lenth)Appreceive_buff[usart1_cnt++]=Res; else printf("Save_Err\r\n"); } //接收到"ok"即可开始发送程序包 不能加发送新行! if((usart1_state==1)&&(Res==0x6B))//字母'k' { usart1_state=2; printf("ok\r\n"); } else if((usart1_state==1)&&(Res!=0x6B)) { printf("Err\r\n"); usart1_state=0; } if((usart1_state==0)&&(Res==0x6F))usart1_state=1; //字母'0' else if((usart1_state==0)&&(Res!=0x6F))printf("Err\r\n"); //接收完成后 发送"++"即可开始升级 if((usart1_state==4)&&(Res==0x2B))usart1_state=5; //'+' else if((usart1_state==4)&&(Res!=0x2B)) { usart1_state=3; printf("Err\r\n"); } if((usart1_state==3)&&(Res==0x2B))usart1_state=4; //'+' else if((usart1_state==3)&&(Res!=0x2B))printf("Error\r\n"); } }
iap_write_appbin(FLASH_APP_LOAD_ADDR,Appreceive_buff,Iap_lenth);//更新FLASH代码 IAP_function Jump_fuction; typedef void (*IAP_function)(void); void iap_load_app(u32 appxaddr) { if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法. { Jump_fuction=(IAP_function)*(vu32*)(appxaddr+4); //用户代码区第二个字为程序开始地址(复位地址) MSR_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) Jump_fuction(); //跳转到APP. } }
升级和跳转前都要检查内容是否合法。一是判断起始地址第一个int作为MSP是不是在0x20范围,二是判断第二个int作为PC是不是在0x08范围,即
if(((*(vu32*)(FLASH_APP_LOAD_ADDR+4))&0xFF000000)==0x08000000)
!!!跳转需要注意:需要先将复位入口地址取出来,再设置MSP,再执行复位。不能先设置MSP,再取地址跳转,这样修改MSP是无效的。
这样就不对 MSR_MSP(*(vu32*)0x08005000); ((IAP_function)*(vu32*)(0x08005000+4))();
我们会发现用户程序升级后复位首先还是会运行引导程序,所以我们需要跳转之前向flash再写一个标志位,这个标志位代表程序是否直接运行升级程序。加入控制引脚,根据引脚电平判断是否更改标志位。这样上电后便会由boot转到用户程序。
if((STMFLASH_ReadHalfWord(FLASH_JUMP_FLAG_ADDR)==Flag_Buff[0]))usart1_state=7;//读取状态标志 两种进入方式 1修改flash 2拉低PA8引脚电平 if(!GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_8)) { usart1_state=0; STMFLASH_Write((u32)(FLASH_JUMP_FLAG_ADDR),Flag_Buff+1,1);//拉低复位后强制修改flash标志位 } #define FLASH_JUMP_FLAG_ADDR 0x08004FE0 //0x5000-32 跳转标志地址 u16 Flag_Buff[2]={4,8}; //跳转标志位 在跳转之前 STMFLASH_Write((u32)(FLASH_JUMP_FLAG_ADDR),Flag_Buff,1); //向Flash里写一个标志位
向用户程序中添加flash的标志位的更改程序和软件复位程序,以达到利用通讯便可以跳转到引导程序且不跳转回来。
void USART1_IRQHandler(void) //串口1中断服务程序 { u8 Res; if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾) { Res =USART_ReceiveData(USART1); //读取接收到的数据 USART_RX_BUF[usart_cnt++]=Res; 返回引导程序 "~~" if((state==1)&&(Res==0x7E)) { printf("ok\r\n"); state=2; } else if((state==1)&&(Res!=0x7E)) { state=0;printf("ERROR\r\n"); } if((state==0)&&(Res==0x7E))state=1; } } void check_state(u8 *sta) { if(*sta==2) { STMFLASH_Write((u32)(FLASH_JUMP_FLAG_ADDR),Flag_Buff+1,1); //向Flash里写一个标志位 iap_load_app(0x08000000); *sta=0; } }
通讯协议接收到“~~”开始跳转state=2;在主程序内添加check_state(&state);即可。
八.实验效果
https://www.bilibili.com/video/BV1Yh411p7nM?share_source=copy_webhttps://www.bilibili.com/video/BV1Yh411p7nM?share_source=copy_webhttps://www.bilibili.com/video/BV1Yh411p7nM?share_source=copy_web