电子产品如何使用IAP方式升级程序

目录

1、ICP、ISP和IAP的概念

2、IAP升级程序的原理

3、IAP升级程序的流程

4、IAR环境下IAP的实现

4.1、BootLoader程序设计

4.2、User Application程序设计

4.3、IAR地址配置及文件输出

5、拓展:解析HEX文件


1、ICP、ISP和IAP的概念

在项目开发过程中通常使用SWD、JTAG等工具进行程序烧录和仿真,若产品节点较少还是比较方便,但是当设备节点量产时,就需要使用IAP的方式进行程序烧录。

简单说明几个概念ICP、ISP和IAP。

ICP In-circuit programmer

ICP:在电路编程,MCU内部不需要有程序,上电就能够对程序存储区域进行编程,例如平时使用JTAG、SWD等方式。

ISP In-system programer

ISP:在系统编程,通过MCU专用的串行编程接口进行编程,MCU需要具有运行的外部条件,例如有晶振等。

例如STM32通过设置BOOT引脚设置对应启动模式,然后通过串口等对内部Flash进行升级,可以说这种方式就是厂家在芯片内部固化了一个BootLoader程序。

IAP In-application programer 

IAP:在应用编程,开发者设计BootLoader程序,通过串口、CAN、以太网等通信方式实现程序升级。

2、IAP升级程序的原理

通常一块MCU芯片的Code(代码)区内只有一个用户程序,而IAP方案则是将代码划分为两部分,两部分区域各存放一个程序,一个为BootLoader(引导加载程序)另一个为User Application(用户应用程序)

BootLoader出厂时就固定下来了,在需要变更User Application时只需要通过触发BootLoader对User Application的擦除和重新写入即可完成用户应用的更换。

程序执行初始化后首先会进入BootLoader,在BootLoader里面检测条件是否被触发(可通过按键是否被按下、串口是否接收到特定的数据、U盘是否插入等),如果有则进行对User Application进行擦除和重新写入操作新程序,如果没有则直接跳转到BootLoader执行User Application

3、IAP升级程序的流程

假设设备仅有User Application,以STM32F103ZET6为例,其启动方式有三种:内置FLASH启动、内置SRAM启动、系统存储器ROM启动。通过BOOT0和BOOT1引脚的设置可以选择从哪中方式启动,这里选择内置的FLASH启动,STM32F103ZET6 FLASH的地址为0x08000000—0x0807FFFF,共512KB。

通常STM32发生中断的过程为以下五步:

1、发生中断(中断请求);

2、到中断向量表查找中断函数入口地址;

3、跳转到中断函数;

4、执行中断函数;

5、中断返回。

也就是说,STM32的内置的Flash中有一个中断向量表来存放各个中断服务函数的入口地址,内置Flash的分配情况如下图所示:

所以当只有一个程序的情况下(仅有User Applicatio时),程序执行的走向如下所示:

解析上图:

STM32F103ZET6有一个中断向量表,这个中断向量表存放在代码开始部分的后4个字节处(即0x08000004),代码开始的4个字节存放的是堆栈栈顶的地址,当发生中断后程序通过查找该表得到相应的中断服务程序入口地址,然后再跳到相应的中断服务程序中执行。

设备上电后从0x08000004处取出复位中断向量的地址,然后跳转到复位中断程序的入口(标号①所示),执行结束后跳转到main函数中(标号②所示)。在执行main函数的过程中发生中断,则STM32强制将PC指针指回中断向量表处(标号③所示),从中断向量表中找到相应的中断函数入口地址,跳转到相应的中断服务函数(标号④所示),执行完中断函数后再返回到main函数中来(标号⑤所示)。

下面要讲正题了。

若将STM32F103ZET6在内置的Flash里面添加User Application和BootLoader程序,则Flash分配情况大致如下图所示:

此时,User Application和BootLoader程序各有一个中断向量表,假设BootLoader程序占用的空间为N+M字节,则程序的走向应该如下图所示:

解析上图:

设备上电初始程序依然从0x08000004处取出复位中断向量地址,执行复位中断函数后跳转到IAP的main(标号①所示),在IAP的main函数执行完成后(在BootLoader里面检测条件是否被触发(可通过按键是否被按下、串口是否接收到特定的数据、U盘是否插入等),如果有则进行对User Application进行擦除和重新写入操作新程序,如果没有则直接跳转到BootLoader执行User Application)强制跳转到0x08000004+N+M处(标号②所示),最后跳转到新的main函数中来(标号③所示),当发生中断请求后,程序跳转到新的中断向量表中取出新的中断函数入口地址,再跳转到新的中断服务函数中执行(标号④⑤所示),执行完中断函数后再返回到main函数中来(标号⑥所示)。

4、IAR环境下IAP的实现

以IAR环境为例,简单讲述IAP的实现步骤。这里MCU以华大HC32L130为例,因为使用的MCU不同,所以实现的细节也不一致,但是基本上官方都会提供Demo例程。

本示例Flash分配情况为:BootLoader地址:0x00000000~0x00000DFF,User Application地址:0x00001000~0x0000FFFF。

4.1、BootLoader程序设计

第1步:设计总体架构,包含三个功能函数:检测BootLoader标志程序、IAP配置程序和IAP烧录功能程序。

  1. /**
  2. *******************************************************************************
  3. ** \brief IAP 主函数
  4. **
  5. ** \param None
  6. **
  7. ** \retval int32_t Return value, if needed
  8. **
  9. ******************************************************************************/
  10. int32_t main(void)
  11. {
  12. IAP_UpdateCheck();
  13. IAP_Init();
  14. IAP_Main();
  15. }

第2步:检查BootPara标记区数据值,判断是否需要升级APP程序,若需要升级则才会执行IAP_Init()和IAP_Main()函数,否则会直接跳转到User Application程序。

  1. /**
  2. *******************************************************************************
  3. ** \brief 检查BootPara标记区数据值,判断是否需要升级APP程序.
  4. **
  5. ** \param None
  6. **
  7. ** \retval None
  8. **
  9. ******************************************************************************/
  10. void IAP_UpdateCheck(void)
  11. {
  12. uint32_t u32AppFlag;
  13. u32AppFlag = *(__IO uint32_t *)BOOT_PARA_ADDRESS; //读出BootLoader para区标记值
  14. if (APP_FLAG != u32AppFlag) //如果标记值不等于APP_FLAG,表示不需要升级APP程序
  15. {
  16. IAP_JumpToApp(APP_ADDRESS); //则直接跳转至APP
  17. }
  18. }

第3步:IAP_Init()函数的实现,主要包括外围模块初始化和IAP通信协议标志初始化。

  1. /**
  2. *******************************************************************************
  3. ** \brief IAP 初始化
  4. **
  5. ** \param [in] None
  6. **
  7. ** \retval None
  8. **
  9. ******************************************************************************/
  10. void IAP_Init(void)
  11. {
  12. PreiModule_Init();
  13. Modem_RamInit();
  14. }
  15. /**
  16. *******************************************************************************
  17. ** \brief CPU外围模块初始化
  18. **
  19. ** \param [in] None
  20. **
  21. ** \retval None
  22. **
  23. ******************************************************************************/
  24. void PreiModule_Init(void)
  25. {
  26. HC32_SetSystemClockToRCH22_12MHz();
  27. HC32_InitUart();
  28. HC32_InitCRC();
  29. HC32_InitTIM();
  30. HC32_InitFlash(FLASH_CONFIG_FREQ_22_12MHZ);
  31. }
  32. /**
  33. *******************************************************************************
  34. ** \brief modem文件中相关变量参数初始化
  35. **
  36. ** \param [out] None
  37. ** \param [in] None
  38. **
  39. ** \retval None
  40. **
  41. ******************************************************************************/
  42. void Modem_RamInit(void)
  43. {
  44. uint32_t i;
  45. enFrameRecvStatus = FRAME_RECV_IDLE_STATUS; //帧状态初始化为空闲状态
  46. for (i=0; i<FRAME_MAX_SIZE; i++)
  47. {
  48. u8FrameData[i] = 0; //帧数据缓存初始化为零
  49. }
  50. u32FrameDataIndex = 0; //帧缓存数组索引值初始化为零
  51. }

第4步:IAP_Main()函数的实现,主要包含对User Application程序更新处理。

  1. /**
  2. *******************************************************************************
  3. ** \brief IAP APP程序升级主函数.
  4. **
  5. ** \param None
  6. **
  7. ** \retval None
  8. **
  9. ******************************************************************************/
  10. void IAP_Main(void)
  11. {
  12. en_result_t enRet;
  13. while (1)
  14. {
  15. enRet = Modem_Process(); //APP程序更新处理
  16. if (Ok == enRet)
  17. {
  18. IAP_ResetConfig(); //复位所有外设模块
  19. if (Error == IAP_JumpToApp(APP_ADDRESS)) //如果跳转失败
  20. {
  21. while(1);
  22. }
  23. }
  24. }
  25. }
  26. /**
  27. *******************************************************************************
  28. ** \brief 上位机数据帧解析及处理
  29. **
  30. ** \param [in] None
  31. **
  32. ** \retval Ok APP程序升级完成,并接受到跳转至APP命令
  33. ** \retval OperationInProgress 数据处理中
  34. ** \retval Error 通讯错误
  35. **
  36. ******************************************************************************/
  37. en_result_t Modem_Process(void)
  38. {
  39. uint8_t u8Cmd, u8FlashAddrValid, u8Cnt, u8Ret;
  40. uint16_t u16DataLength, u16PageNum, u16Ret;
  41. uint32_t u32FlashAddr, u32FlashLength, u32Temp;
  42. if (enFrameRecvStatus == FRAME_RECV_PROC_STATUS)
  43. //有数据帧待处理, enFrameRecvStatus值在串口中断中调整
  44. {
  45. u8Cmd = u8FrameData[PACKET_CMD_INDEX]; //获取帧指令码

  46. if (PACKET_CMD_TYPE_DATA == u8FrameData[PACKET_TYPE_INDEX]) //如果是数据指令

  47. {
  48. u8FlashAddrValid = 0u;
  49. u32FlashAddr = u8FrameData[PACKET_ADDRESS_INDEX] + //读取地址值
  50. (u8FrameData[PACKET_ADDRESS_INDEX + 1] << 8) +
  51. (u8FrameData[PACKET_ADDRESS_INDEX + 2] << 16) +
  52. (u8FrameData[PACKET_ADDRESS_INDEX + 3] << 24);
  53. if ((u32FlashAddr >= (FLASH_BASE + BOOT_SIZE)) && (u32FlashAddr
  54. < (FLASH_BASE + FLASH_SIZE))) //如果地址值在有效范围内
  55. {
  56. u8FlashAddrValid = 1u; //标记地址有效
  57. }
  58. }
  59. switch (u8Cmd)
  60. //根据指令码跳转执行
  61. {
  62. case PACKET_CMD_HANDSHAKE : //握手帧 指令码

  63. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //返回状态为:正确

  64. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);
  65. //发送应答帧给上位机
  66. break;
  67. case PACKET_CMD_ERASE_FLASH : //擦除flash 指令码

  68. if ((u32FlashAddr % FLASH_SECTOR_SIZE) != 0)
  69. //如果擦除地址不是页首地址
  70. {
  71. u8FlashAddrValid = 0u; //标记地址无效

  72. }
  73. if (1u == u8FlashAddrValid) //如果地址有效

  74. {
  75. u32Temp = u8FrameData[PACKET_DATA_INDEX] +
  76. //获取待擦除flash尺寸
  77. (u8FrameData[PACKET_DATA_INDEX + 1] << 8) +
  78. (u8FrameData[PACKET_DATA_INDEX + 2] << 16) +
  79. (u8FrameData[PACKET_DATA_INDEX + 3] << 24);
  80. u16PageNum = FLASH_PageNumber(u32Temp); //计算需擦除多少页

  81. for (u8Cnt=0; u8Cnt<u16PageNum; u8Cnt++)
  82. //根据需要擦除指定数量的扇区
  83. {
  84. u8Ret = Flash_EraseSector(u32FlashAddr +
  85. (u8Cnt * FLASH_SECTOR_SIZE));
  86. if (Ok != u8Ret)
  87. //如果擦除失败,反馈上位机错误代码
  88. {
  89. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ERROR;
  90. break;
  91. }
  92. }
  93. if (Ok == u8Ret)
  94. //如果全部擦除成功,反馈上位机成功
  95. {
  96. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;
  97. }else
  98. //如果擦除失败,反馈上位机错误超时标志
  99. {
  100. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_TIMEOUT;
  101. }
  102. }
  103. else
  104. //地址无效,反馈上位机地址错误
  105. {
  106. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR;
  107. }
  108. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);
  109. //发送应答帧到上位机
  110. break;
  111. case PACKET_CMD_APP_DOWNLOAD : //数据下载 指令码
  112. if (1u == u8FlashAddrValid) //如果地址有效
  113. {
  114. u16DataLength = u8FrameData[FRAME_LENGTH_INDEX] +
  115. (u8FrameData[FRAME_LENGTH_INDEX + 1] << 8)
  116. - PACKET_INSTRUCT_SEGMENT_SIZE;
  117. //获取数据包中的数据长度(不包含指令码指令类型等等)
  118. if (u16DataLength > PACKET_DATA_SEGMENT_SIZE)
  119. //如果数据长度大于最大长度
  120. {
  121. u16DataLength = PACKET_DATA_SEGMENT_SIZE; //设置数据最大值
  122. }
  123. u8Ret = Flash_WriteBytes(u32FlashAddr, (uint8_t *)&u8FrameData
  124. [PACKET_DATA_INDEX], u16DataLength); //把所有数据写入flash
  125. if (Ok != u8Ret)
  126. //如果写数据失败
  127. {
  128. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ERROR; //反馈上位机错误 标志
  129. }
  130. else //如果写数据成功

  131. {
  132. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功 标志
  133. }
  134. }
  135. else //如果地址无效

  136. {
  137. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR; //反馈上位机地址错误
  138. }
  139. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机
  140. break;
  141. case PACKET_CMD_CRC_FLASH :
  142. //查询flash校验值 指令码
  143. if (1u == u8FlashAddrValid) //如果地址有效
  144. {
  145. u32FlashLength = u8FrameData[PACKET_DATA_INDEX] +
  146. (u8FrameData[PACKET_DATA_INDEX + 1] << 8) +
  147. (u8FrameData[PACKET_DATA_INDEX + 2] << 16) +
  148. (u8FrameData[PACKET_DATA_INDEX + 3] << 24); //获取待校验flash大小
  149. if ((u32FlashLength + u32FlashAddr) > (FLASH_BASE + FLASH_SIZE)) //如果flash长度超出有效范围
  150. {
  151. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_FLASH_SIZE_ERROR;
  152. //反馈上位机flash尺寸错误
  153. }else
  154. {
  155. u16Ret = Cal_CRC16(((unsigned char *)u32FlashAddr), u32FlashLen
  156. gth);//读取flash指定区域的值并计算crc值
  157. u8FrameData[PACKET_FLASH_CRC_INDEX] = (uint8_t)u16Ret; //把crc值存储到应答帧
  158. u8FrameData[PACKET_FLASH_CRC_INDEX+1] = (uint8_t)(u16Ret>>8);
  159. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功 标志
  160. }
  161. }
  162. else //如果地址无效
  163. {
  164. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR; //反馈上位机地址错误
  165. }
  166. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE+2); //发送应答帧到上位机
  167. break;
  168. case PACKET_CMD_JUMP_TO_APP :
  169. //跳转至APP 指令码
  170. Flash_EraseSector(BOOT_PARA_ADDRESS);
  171. //擦除BOOT parameter 扇区
  172. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功
  173. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机
  174. return Ok;
  175. //APP更新完成,返回OK,接下来执行跳转函数,跳转至APP
  176. case PACKET_CMD_APP_UPLOAD :
  177. //数据上传
  178. if (1u == u8FlashAddrValid) //如果地址有效
  179. {
  180. u32Temp = u8FrameData[PACKET_DATA_INDEX] +
  181. (u8FrameData[PACKET_DATA_INDEX + 1] << 8) +
  182. (u8FrameData[PACKET_DATA_INDEX + 2] << 16) +
  183. (u8FrameData[PACKET_DATA_INDEX + 3] << 24); //读取上传数据长度
  184. if (u32Temp > PACKET_DATA_SEGMENT_SIZE) //如果数据长度大于最大值
  185. {
  186. u32Temp = PACKET_DATA_SEGMENT_SIZE; //设置数据长度为最大值
  187. }
  188. Flash_ReadBytes(u32FlashAddr, (uint8_t *)&u8FrameData[PACKET_DATA
  189. _INDEX], u32Temp); //读flash数据
  190. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK; //反馈上位机成功 标志
  191. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE +
  192. u32Temp);//发送应答帧到上位机
  193. }
  194. else
  195. //如果地址无效
  196. {
  197. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR; //反馈上位机地址错误 标志
  198. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE); //发送应答帧到上位机
  199. }
  200. break;
  201. case PACKET_CMD_START_UPDATE :
  202. //启动APP更新(此指令正常在APP程序中调用)
  203. u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;
  204. //反馈上位机成功 标志
  205. Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);
  206. //发送应答帧到上位机
  207. break;
  208. }
  209. enFrameRecvStatus = FRAME_RECV_IDLE_STATUS;
  210. //帧数据处理完成,帧接收状态恢复到空闲状态
  211. }
  212. return OperationInProgress;
  213. //返回,APP更新中。。。
  214. }

4.2、User Application程序设计

在本示例User Application中,触发BootLoader更新程序的标志在串口接收中实现。

  1. //UART0中断函数
  2. void Uart0_IRQHandler(void)
  3. {
  4. if(Uart_GetStatus(M0P_UART0, UartRC)) //UART0数据接收
  5. {
  6. Uart_ClrStatus(M0P_UART0, UartRC); //清中断状态位
  7. u8RxData[u8RxCnt] = Uart_ReceiveData(M0P_UART0); //接收数据字节
  8. u8RxCnt++;
  9. if(u8RxCnt>=18)
  10. {
  11. u8RxCnt = 0;
  12. if ((u8RxData[0]==0x6D)&&(u8RxData[1]==0xAC)&&(u8RxData[6]==0x26)&&
  13. (u8RxData[16]==0xA6)&&(u8RxData[17]==0xDA)) //是APP更新帧
  14. {
  15. for(uint32_t i=0;i<18;i++)
  16. {
  17. Uart_SendDataPoll(M0P_UART0,u8TxData[i]); //查询方式发送数据
  18. }
  19. //boot para区域写标记值,通知BootLoader要更新程序了
  20. Flash_SectorErase(0xF00);
  21. Flash_WriteWord(0xF00, 0x12345678);
  22. NVIC_SystemReset(); //软件复位MCU
  23. }
  24. }
  25. }
  26. if(Uart_GetStatus(M0P_UART0, UartTC)) //UART0数据发送
  27. {
  28. Uart_ClrStatus(M0P_UART0, UartTC); //清中断状态位
  29. }
  30. }

4.3、IAR地址配置及文件输出

最后还需要简答配置下IAR环境。

第1步:确定输出的Linker配置地址,因为需要在这里程序修改地址。

第2步:找到Linker配置文件,修改BootLoader程序地址:0x00000000~0x00000DFF,User Application程序地址:0x00001000~0x0000FFFF。

第3步:找到User Application程序的配置文件(后缀为.s的文件),添加程序中断向量偏移长度:0x00001000,和BootLoader程序配置文件相比有两处不同之处,如下所示:

第4步:将这两个程序按照ICP方式(SWD、JTAG等)烧录后,此后就可以使用IAP方式通过串口烧录HEX文件程序或者BIN文件程序。输出及烧录HEX文件程序或者BIN文件程序方式如下图所示:

5、拓展:解析HEX文件

HEX文件可以通过UltraEdit、Notepad++、记事本等工具打开,用Notepad++打开之后会看到以下数据内容:





















使用Notepad++打开后会不同含义的数据其颜色不同。每行数据都会有一个冒号开始,后面的数据由:数据长度、地址、标识符、有效数据、校验数据等构成。以上图的第一行为例,进行解析:

第1个字节10,表示该行具有0x10个数据,即16个字节的数据;

第2、3个字节3E00,表示该行的起始地址为0x3E00;

第4个字节00,表示该行记录的是数据;

第5-20个字节,表示的是有效数据;

第21个字节EB,表示前面数据的校验数据,校验方法:0x100-前面字节累加和

其中,第4个字节具有5种类型:00-05,含义如下:

字段含义
00表示后面记录的是数据
01表示文件结束
02表示扩展段地址
03表示开始段地址
04表示扩展线性地址
05表示开始线性地址

单片机的hex文件以00居多,都用来表示数据。hex文件的结束部分如下图所示:










最后一行的01表示文件结束了,最后的FF表示校验数据,由0x100-0x01=0xFF得来。


资源下载:IAR环境下STM32+IAP方案的实现

关注公众号,发送关键字:Java车牌识别,获取项目源码。