开发工具: Keil 5
开发芯片: STM32F103RCT6
文档编写工具: Markdown
因为只有一个JTAG的下载器,所以在调试程序的时候想用IAP实现远程修改/升级程序.
1 IAP的基础知识
1.1 STM32的编程方式
- ISP:In System Programming (在系统中编程),通过芯片专用的串行编程接口对其内部的程序存储器进行擦写。
- IAP:In Application Programming( 在应用中编程),通过调用特定的bootloader程序,对程序存储器的指定段进行读/写操作,从而实现对目标板的程序的修改。
ISP即我们平时所用的JLINK之类的下载器通过专门的接口来下载程序,IAP是通过调用Bootloader来充当下载器的功能实现更新程序的作用。
1.2 Bootloader
一般程序下载方式:
STM32的内部内存(FLASH)地址起始于0x08000000,一般程序由此地址写入,0x08000004开始存放中断向量表。当中断开始时,STM32的内部硬件机制会将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。
- STM32复位之后,从0x08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序。
- 在复位中断服务程序执行完之后,跳转至main函数。main函数执行过程中,如果收到中断请求,STM32将PC指针重新拨回中断向量表处。
- 根据中断源进入相应的中断服务程序。
- 在执行完中断服务程序之后,程序再次返回main函数中执行。
IAP下程序的运行流程:
- STM32复位之后,从0x08000004地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到IAP的main函数;
- 在执行完IAP以后,跳转至新写入程序的复位向量表,取出新程序的复位中断向量表的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数,上图中②和③;
- 在main函数执行过程中,如果CPU得到一个中断请求,PC指针仍强制跳转到地址0x08000004中断向量表处,而不是新程序的中断向量表;
- 程序根据设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中;
- 在执行完中断服务程序后,程序返回main函数继续运行。
2 IAP的实现
2.1 Keil设置
keil中需要准备两个工程,一个是IAP,一个是APP。Bootloader通过ISP方式下载到Flash中,APP则是通过串口将编译生成的bin文件发送下载。
使用的STM32型号为STM32F103RCT6,参数如下表所示:
基本参数 |
|
名称 |
STM32F103RCT6 |
架构 |
ARM Cortex-M3 |
Flash容量 |
256KB |
RAM容量 |
48K |
两个工程将Flash分成两个区域,Bootloader存储的起始地址为0x08000000,分配大小这里设置为0x2000字节;用户APP信息存储从0x08002000处开始,分配空间大小为(0x08040000-0x08002000=0x803E000)。IAP工程文件中使用keil默认设置即可,APP工程中的设置如下图所示。
2.2 驱动程序
串口接收:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| u8 serial_Buffer[SERIAL_MAX_LENGTH] = {0};
u16 serial_Buffer_Length = 0;
u8 receiveMode = 0; u8 receiveExpectCount = 0;
static void SerialRecv(u8 ch) { if(receiveMode == 0) { if((serial_Buffer_Length&0x8000) == 0x8000) { serial_Buffer_Length |= 0x8000; } else if((serial_Buffer_Length&0x4000) == 0x4000) { if(ch == '\n')serial_Buffer_Length |= 0x8000; else { serial_Buffer_Length = 0; } } else { if((serial_Buffer_Length&0xff) < SERIAL_MAX_LENGTH) { if(ch == '\r')serial_Buffer_Length |= 0x4000; else { serial_Buffer[(serial_Buffer_Length&0xff)] = ch; serial_Buffer_Length++; } } else { serial_Buffer_Length = 0; } } } else { if(receiveExpectCount == 0) { receiveExpectCount = ch; } else { if((serial_Buffer_Length&0x8000) == 0x8000) { serial_Buffer_Length |= 0x8000; } else { serial_Buffer[(serial_Buffer_Length&0xff)] = ch; serial_Buffer_Length++; if((serial_Buffer_Length&0xff) == receiveExpectCount) { serial_Buffer_Length |= 0x8000; } } } } }
void USART1_IRQHandler(void) { u8 ch = 0; if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET) { ch = (u8)USART_ReceiveData(USART1); USART_ClearITPendingBit(USART1, USART_IT_RXNE);
SerialRecv(ch); } }
|
IAP程序
IAP程序包括iap_down(下载程序,片机接收来自于上位机的数据),iap_jump_app(IAP跳转到APP的跳转指令),iap_over(指示IAP完成,将系统缓冲区清空),iap_set_flag(检测到该标志时跳转到APP程序中),iap_clear_flag(清除APP标志,让IAP不再自动跳转到APP中),app_jump_iap(app跳转到iap的跳转指令)。
IAP_set_flag
1 2 3 4 5 6 7 8 9 10 11 12 13
| void iap_set_flag(void) { Test_Write(APP_CONFIG_ADDR,APP_CONFIG_SET_VALUE); printf("固化成功\r\n"); }
void Test_Write(u32 WriteAddr,u16 WriteData) { STMFLASH_Write(WriteAddr,&WriteData,1); }
|
在keil中我们设置0x08000000-0x08003000来存放iap代码,并将0x08001FFC作为存放app固化标志的地方,在宏定义中设置各个变量的地址:
1 2 3 4 5
| #define APP_CONFIG_ADDR 0X08001FFC
#define APP_CONFIG_SET_VALUE 0X5555
#define APP_CONFIG_CLEAR_VALUE 0XFFFF
|
iap_claer_flag
清除标志的方式与写入标志的方式同理,在 APP_CONFIG_ADDR 这个地址写入清零值。
1 2 3 4 5 6 7
| void iap_clear_flag(void) { Test_Write(APP_CONFIG_ADDR,APP_CONFIG_CLEAR_VALUE); printf("清除成功\r\n"); }
|
iap_jump_app
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| typedef void (*iapfun)(void); iapfun jump2app;
__asm void MSR_MSP(u32 addr) { MSR MSP, r0 BX r14 }
void iap_jump_app(void) { iap_load_app(FLASH_APP1_ADDR); }
void iap_load_app(u32 appxaddr) { if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) { printf("ok\r\n"); Delay_Ms(10); jump2app=(iapfun)*(vu32*)(appxaddr+4); MSR_MSP(*(vu32*)appxaddr); jump2app(); } else { printf("program in flash is error\r\n"); } }
|
程序解释:
检查栈顶地址
1
| if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000)
|
在实际的程序中,设置APP的起始地址为0x08003000,即appxaddr==0x08003000,而 *(vu32 *)appxaddr即取0x08003000-0x08003003这4个字节的值,因为APP中设置中断向量表放置在0x08003000开始的位置,中断向量表中第一个放的则是栈顶地址的值。通过判断栈顶地址值是否正确(是否在0x2000 0000 - 0x 2000 2000之间) 来判断是否应用程序已经下载了,因为应用程序的启动文件会初始化化栈空间,如果栈顶值对了,说明启动文件的初始化执行了应用程也已经下载了。
程序开始地址
1
| jump2app=(iapfun)*(vu32*)(appxaddr+4);
|
(appxaddr+4)即0x08003004,这个地址放的时中断向量表的第二项“复位地址”
1
| typedef void (*iapfun)(void)
|
一般,typedef int a,是给整型定义一个别名 a ;而 void (* iapfun)(void) 是声明一个函数指针,加上 typedef 之后 iapfun 只不过是类型 void(*)void 的一个别名。所以,此时的jump2app已经指向了复位函数所在的地址 Reset_Handler(中断向量表的第二项),跳转到main函数。下图为STM32启动文件 startup_stm32f10x_hd.s 中的代码解释:
ST公司都提供了现成的直接可用的启动文件,程序开发人员可以直接引用启动文件后直接进行C应用程序的开发。这样能大大减小开发人员从其它微控制器平台跳转至STM32平台,也降低了适应STM32微控制器的难度。相对于ARM上一代的主流ARM7/ARM9内核架构,新一代Cortex内核架构的启动方式有了比较大的变化。ARM7/ARM9内核的控制器在复位后,CPU会从存储空间的绝对地址0x000000取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为0x000000(PC = 0x000000)同时中断向量表的位置并不是固定的。而Cortex-M3内核则正好相反,有3种情况*:
1、 通过boot引脚设置可以将中断向量表定位于SRAM区,即起始地址为0x2000000,同时复位后PC指针位于0x2000000处;
2、 通过boot引脚设置可以将中断向量表定位于FLASH区,即起始地址为 0x8000000,同时复位后PC指针位于0x8000000处;
3、 通过boot引脚设置可以将中断向量表定位于内置Bootloader区,本文不对这种情况做论述;
而Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32*位空间取出复位中断入口向量,跳转执行复位中断服务程序。
STM32 IAP 在线升级详解——CSDN
关于Stm32的IAP详细和应用——CSDN
app_jump_iap
1 2 3 4 5
| void app_jump_iap() { SCB->VTOR = FLASH_BASE; NVIC_SystemReset(); }
|
其中,NVIC_SystemReset()这个函数在新版的STM32的官方固件库文件 core_cm3.h 中1720行处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
static __INLINE void NVIC_SystemReset(void) { SCB->AIRCR = ((0x5FA << SCB_AIRCR_VECTKEY_Pos) | (SCB->AIRCR & SCB_AIRCR_PRIGROUP_Msk) | SCB_AIRCR_SYSRESETREQ_Msk); __DSB(); while(1); }
|
如果使用的是正点原子的老教程,版本文件老旧 core_cm3.h文件可能没有更新,则没有这个函数
iap和app之间的跳转必须关闭所有中断 ,并且复位NVIC中断寄存器的值,因为跳转函数是用程序指针完成的,但跳转只是强制改变了PC指针的位置,NVIC寄存器的值还是保持着原来main的值,所以一旦发生中断就会指向跳转前的main函数的中断函数入口地址,程序会卡死导致 HardFault。所以最好的方法是使用上述的软件重启的思路,其余的处理方式还有:①跳转之前复位或者关闭所有打开的中断②跳转后在初始化时加入RCC_DeInit();NVIC_DeInit ();等让中断恢复默认值。
iap_down_s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| #define FLASH_APP1_ADDR 0x08002000
u16 iapbuf[1024] = {0}; u16 receiveDataCur = 0; u32 addrCur = FLASH_APP1_ADDR;
void iap_down_s(void) { u16 i = 0; u16 temp = 0; u16 receiveCount; printf("begin,wait data download\r\n"); receiveMode = 1; while(1) { if(serial_Buffer_Length & 0x8000) { receiveCount = (u8)(serial_Buffer_Length&0x00ff); if(receiveCount == 128) { for(i = 0; i < receiveCount; i+=2) { temp = (((u16)serial_Buffer[i+1])<<8) + ((u16)serial_Buffer[i]); iapbuf[receiveDataCur] = temp; receiveDataCur++; } receiveExpectCount = 0; serial_Buffer_Length = 0; printf("."); if(receiveDataCur == 1024) { STMFLASH_Write(addrCur,iapbuf,1024); addrCur += 2048; receiveDataCur = 0; } else { } } else { for(i = 0; i < receiveCount; i+=2) { temp = (((u16)serial_Buffer[i+1])<<8) + ((u16)serial_Buffer[i]); iapbuf[receiveDataCur] = temp; receiveDataCur++; } receiveExpectCount = 0; serial_Buffer_Length = 0; printf("."); STMFLASH_Write(addrCur,iapbuf,receiveDataCur); addrCur = FLASH_APP1_ADDR; receiveDataCur = 0; printf("download over\r\n"); receiveMode = 0; return; } } } }
|
代码的核心思想是上位机每次发送128个数据,128个8位数据通过位操作两两融合成16位数据,每个新数据占2个地址,写满2048个addr后写一次flash;当最后一包数据不是128时说明数据发送完成了,将最后的数据烧入flash之后把地址恢复到原来位置addrCur = FLASH_APP1_ADDR,退出下载模式 receiveMode从1置为0;可能会出现的情况在于最后一包的数据也是128个,此时iap_down_s的判断机制仍处于下载模式,针对这种情况定义一个新指令iap_over,上位机侦测到最后一包数据也是128个时补充发送该命令,下位机将缓存写入并退出。
iap_over_s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
void iap_over_s(void) { if(receiveDataCur != 0) { STMFLASH_Write(addrCur,iapbuf,receiveDataCur); printf("write addr %x,length %d",addrCur,receiveDataCur); addrCur = FLASH_APP1_ADDR; receiveDataCur = 0; receiveMode = 0; } printf("最后一包发送完成\r\n"); }
|
Flash擦写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
|
u16 STMFLASH_ReadHalfWord(u32 faddr) { return *(vu16*)faddr; }
#if STM32_FLASH_WREN
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; } }
#if STM32_FLASH_SIZE<256 #define STM_SECTOR_SIZE 1024 #else #define STM_SECTOR_SIZE 2048 #endif
u16 STMFLASH_BUF[STM_SECTOR_SIZE/2]; void STMFLASH_Write(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) { u32 secpos; u16 secoff; u16 secremain; u16 i; u32 offaddr; if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*STM32_FLASH_SIZE)))return; FLASH_Unlock(); offaddr=WriteAddr-STM32_FLASH_BASE; secpos=offaddr/STM_SECTOR_SIZE; secoff=(offaddr%STM_SECTOR_SIZE)/2; secremain=STM_SECTOR_SIZE/2-secoff; if(NumToWrite<=secremain)secremain=NumToWrite; while(1) { STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2); for(i=0;i<secremain;i++) { if(STMFLASH_BUF[secoff+i]!=0XFFFF)break; } if(i<secremain) { FLASH_ErasePage(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE); for(i=0;i<secremain;i++) { STMFLASH_BUF[i+secoff]=pBuffer[i]; } STMFLASH_Write_NoCheck(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF,STM_SECTOR_SIZE/2); }else STMFLASH_Write_NoCheck(WriteAddr,pBuffer,secremain); if(NumToWrite==secremain)break; else { secpos++; secoff=0; pBuffer+=secremain; WriteAddr+=secremain; NumToWrite-=secremain; if(NumToWrite>(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2; else secremain=NumToWrite; } }; FLASH_Lock(); } #endif
void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead) { u16 i; for(i=0;i<NumToRead;i++) { pBuffer[i]=STMFLASH_ReadHalfWord(ReadAddr); ReadAddr+=2; } }
|
用户回调函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void Help_Proc_Func(void) { printf("DzyLink shell v1.0\r\n"); printf("modify by Dingzy\r\n"); printf("2020/10/12 21:44\r\n"); }
void List_Proc_Func(void) { u8 i = 0; printf("command num is %d\r\n",COMMAND_NUM); for(i = 0; i < COMMAND_NUM; i++) { printf("%d : %s\r\n",i,commandStringList[i]); } printf("*****************************************************\r\n"); }
|
CommandScan扫描命令函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| void CommandScan(void) { u8 commandLength1; u8 commandLength2; u8 i = 0,j = 0; if((serial_Buffer_Length & 0x8000) == 0x8000) { if(Command_Is_Vailed()) { Command_Copy(); Command_Remove_Space_Head(); Command_Remove_Space_End(); Command_Remove_Space_Inner(); commandLength1 = Command_Find_Space_Postion(1); if(commandLength1 == 0)commandLength1 = commandStringLength; for(i = 0; i < COMMAND_NUM; i++) { commandLength2 = StringGetLength(commandStringList[i]); if(commandLength1 == commandLength2) { for(j = 0; j < commandLength1; j++) { if(commandStringBuffer[j] == commandStringList[i][j])continue; else break; } if(j == commandLength1) { Command_Proc_Func_Table[i](); return; } } else { continue; } } if(i == COMMAND_NUM) { printf("not find command\r\n"); } } else { printf("command can't all space\r\n"); serial_Buffer_Length = 0; } } }
Command_Proc_Func Command_Proc_Func_Table[] = { Help_Proc_Func, List_Proc_Func, iap_down_s, iap_jump_app, iap_over_s, iap_set_flag, iap_clear_flag };
|
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| int main(void) { NVIC_Group_Init(); Debug_Serial_Init(115200); Delay_Init(); Command_Init(100); while(1) { if(STMFLASH_ReadHalfWord(APP_CONFIG_ADDR) == 0x5555) { iap_jump_app_s(); } CommandScan(); } }
|
2.3 APP程序
app使用最简单的蜂鸣器实验(代码来自正点原子),硬件部分将蜂鸣器的I/O口连接至PB8,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include "sys.h" #include "delay.h" #include "led.h" #include "beep.h"
int main(void) { delay_init(); LED_Init(); BEEP_Init(); while(1) { LED0=0; BEEP=0; delay_ms(300); LED0=1; BEEP=1; delay_ms(300); } }
|
3 通信协议
挖坑待填…
4 Q&A
4.1 报错
..\..\Libraries\CMSIS\stm32f10x.h(298): error: #67: expected a "}"
ADC1_2_IRQn = 18, /*!< ADC1 and ADC2 global Interrupt */
..\..\Libraries\CMSIS\stm32f10x.h(472): warning: #12-D: parsing restarts here after previous syntax error} IRQn_Type;
..\..\User\main.c: 1 warning, 1 error
"..\..\User\main.c" - 1 Error(s), 1 Warning(s).
1 2 3 4 5 6
| **解决方法:**在C/C++选项卡里,把STM3210X_HD从**Prepocessor symbol define** 里面删掉。在老版本的官方STM32F10x.h文件里,是`...&&!defined(STM32F10X_HD) && ...` 原来是有括号的,不做标识符来处理,而新版的直接说明了出来.
- ``` ..\driver\debugSerial.c(14): error: #260-D: explicit type is missing ("int" assumed) _sys_exit(int x)
|
**问题原因:**_sys_exit(int x) 这个函数没有返回类型,产生这个的原因是因为用了C99的库,C99和C89的区别详见https://www.cnblogs.com/ys77/p/11541827.html
**解决方法:**添加 void 不报错,编译通过。
4.2 IAP跳转执行的问题
在栈顶地址验证通过之后,Flash进行了擦除-拷贝-跳转执行的操作,问题在于跳转执行之后,Bootloader又将其引导回了流程一开始的阶段,两次擦除和拷贝之后,栈顶地址发生了改变,程序无法运行,如图所示:
解决方法:跳转程序没有正常执行,三种问题可能会导致APP跳转失败。
设置了APP标志后,APP能够跳转到IAP中,但IAP马上又会跳转回APP,永远不能等待下载;
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void iap_set_flag_s(void) { Test_Write(APP_CONFIG_ADDR,APP_CONFIG_SET_VALUE); printf("ok\r\n"); }
void iap_clear_flag(void) { Test_Write(APP_CONFIG_ADDR,APP_CONFIG_CLEAR_VALUE); printf("ok\r\n"); }
|
先清除APP标志,然后再跳转到IAP程序中,标志就不会影响IAP的下载流程了。
APP的工程中,不仅是在[Target]中要设置Flash的起始地址和SIze,在Jlink的[Flash Download]中也需要设置芯片的起始地址和Size,如图所示:
中断向量表的设置。在IAP中不需要考虑中断向量表,IAP的默认程序就是从0x8000000位置开始的,但是APP代码的起始位置必须从IAP程序之后的地址开始,因此必须重新设置中断向量表。在system_stm32f10x.c中又一个system_init的函数,该函数的作用为启动时调用配置系统时钟,该函数的最后为:
1 2 3 4 5
| #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; #else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; #endif
|
其中VECT_TAB_OFFSET就是需要修改的偏移量,也就是APP程序的起始地址偏移,这个设置必须与IAP同步,我们设置为2000。该值的宏就需要修改,在128行的位置,将0x0修改为0x2000(与2中设置同步)。
1 2
| #define VECT_TAB_OFFSET 0x2000
|