写点什么

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

发布于: 2021 年 03 月 10 日
电子产品如何使用IAP方式升级程序

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 烧录功能程序。


/** ******************************************************************************* ** \brief  IAP 主函数 ** ** \param  None ** ** \retval int32_t Return value, if needed ** ******************************************************************************/int32_t main(void){    IAP_UpdateCheck();    IAP_Init();    IAP_Main();}
复制代码


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


/** ******************************************************************************* ** \brief  检查BootPara标记区数据值,判断是否需要升级APP程序. ** ** \param  None ** ** \retval None ** ******************************************************************************/void IAP_UpdateCheck(void){    uint32_t u32AppFlag;        u32AppFlag = *(__IO uint32_t *)BOOT_PARA_ADDRESS; //读出BootLoader para区标记值    if (APP_FLAG != u32AppFlag)                       //如果标记值不等于APP_FLAG,表示不需要升级APP程序    {        IAP_JumpToApp(APP_ADDRESS);                   //则直接跳转至APP    }    }
复制代码


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


/** ******************************************************************************* ** \brief  IAP 初始化 ** ** \param  [in] None ** ** \retval None ** ******************************************************************************/void IAP_Init(void){    PreiModule_Init();    Modem_RamInit();} /** ******************************************************************************* ** \brief CPU外围模块初始化 ** ** \param [in] None ** ** \retval None ** ******************************************************************************/void PreiModule_Init(void){    HC32_SetSystemClockToRCH22_12MHz();    HC32_InitUart();    HC32_InitCRC();    HC32_InitTIM();    HC32_InitFlash(FLASH_CONFIG_FREQ_22_12MHZ);} /** ******************************************************************************* ** \brief modem文件中相关变量参数初始化 ** ** \param [out] None ** \param [in]  None ** ** \retval None ** ******************************************************************************/void Modem_RamInit(void){        uint32_t i;        enFrameRecvStatus = FRAME_RECV_IDLE_STATUS;                         //帧状态初始化为空闲状态        for (i=0; i<FRAME_MAX_SIZE; i++)    {        u8FrameData[i] = 0;                                             //帧数据缓存初始化为零    }        u32FrameDataIndex = 0;                                              //帧缓存数组索引值初始化为零}
复制代码


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


/** ******************************************************************************* ** \brief  IAP APP程序升级主函数. ** ** \param  None ** ** \retval None ** ******************************************************************************/void IAP_Main(void){    en_result_t enRet;     while (1)    {        enRet = Modem_Process();                       //APP程序更新处理                if (Ok == enRet)        {            IAP_ResetConfig();                         //复位所有外设模块            if (Error == IAP_JumpToApp(APP_ADDRESS))   //如果跳转失败            {                while(1);            }        }    }} /** ******************************************************************************* ** \brief 上位机数据帧解析及处理 ** ** \param [in] None              ** ** \retval Ok                          APP程序升级完成,并接受到跳转至APP命令 ** \retval OperationInProgress         数据处理中 ** \retval Error                       通讯错误 ** ******************************************************************************/en_result_t Modem_Process(void){    uint8_t  u8Cmd, u8FlashAddrValid, u8Cnt, u8Ret;    uint16_t u16DataLength, u16PageNum, u16Ret;    uint32_t u32FlashAddr, u32FlashLength, u32Temp;        if (enFrameRecvStatus == FRAME_RECV_PROC_STATUS)                //有数据帧待处理, enFrameRecvStatus值在串口中断中调整    {        u8Cmd = u8FrameData[PACKET_CMD_INDEX];                      //获取帧指令码        if (PACKET_CMD_TYPE_DATA == u8FrameData[PACKET_TYPE_INDEX]) //如果是数据指令        {            u8FlashAddrValid = 0u;                        u32FlashAddr = u8FrameData[PACKET_ADDRESS_INDEX] +      //读取地址值                           (u8FrameData[PACKET_ADDRESS_INDEX + 1] << 8)  +                           (u8FrameData[PACKET_ADDRESS_INDEX + 2] << 16) +                           (u8FrameData[PACKET_ADDRESS_INDEX + 3] << 24);            if ((u32FlashAddr >= (FLASH_BASE + BOOT_SIZE)) && (u32FlashAddr < (FLASH_BASE + FLASH_SIZE)))  //如果地址值在有效范围内            {                u8FlashAddrValid = 1u;                              //标记地址有效            }        }                switch (u8Cmd)                                              //根据指令码跳转执行        {            case  PACKET_CMD_HANDSHAKE    :                         //握手帧 指令码                u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;   //返回状态为:正确                Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);   //发送应答帧给上位机                break;            case  PACKET_CMD_ERASE_FLASH  :                         //擦除flash 指令码                if ((u32FlashAddr % FLASH_SECTOR_SIZE) != 0)        //如果擦除地址不是页首地址                {                    u8FlashAddrValid = 0u;                          //标记地址无效                }                 if (1u == u8FlashAddrValid)                         //如果地址有效                {                    u32Temp = u8FrameData[PACKET_DATA_INDEX] +      //获取待擦除flash尺寸                              (u8FrameData[PACKET_DATA_INDEX + 1] << 8)  +                              (u8FrameData[PACKET_DATA_INDEX + 2] << 16) +                              (u8FrameData[PACKET_DATA_INDEX + 3] << 24);                    u16PageNum = FLASH_PageNumber(u32Temp);          //计算需擦除多少页                    for (u8Cnt=0; u8Cnt<u16PageNum; u8Cnt++)         //根据需要擦除指定数量的扇区                    {                        u8Ret = Flash_EraseSector(u32FlashAddr + (u8Cnt * FLASH_SECTOR_SIZE));                        if (Ok != u8Ret)                             //如果擦除失败,反馈上位机错误代码                        {                            u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ERROR;                            break;                        }                    }                    if (Ok == u8Ret)                                 //如果全部擦除成功,反馈上位机成功                    {                        u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;                    }else                                            //如果擦除失败,反馈上位机错误超时标志                    {                        u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_TIMEOUT;                    }                }                else                                                 //地址无效,反馈上位机地址错误                {                    u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR;                }                Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);             //发送应答帧到上位机                break;            case  PACKET_CMD_APP_DOWNLOAD :                          //数据下载 指令码                if (1u == u8FlashAddrValid)                          //如果地址有效                {                    u16DataLength = u8FrameData[FRAME_LENGTH_INDEX] + (u8FrameData[FRAME_LENGTH_INDEX + 1] << 8)                                     - PACKET_INSTRUCT_SEGMENT_SIZE; //获取数据包中的数据长度(不包含指令码指令类型等等)                    if (u16DataLength > PACKET_DATA_SEGMENT_SIZE)    //如果数据长度大于最大长度                    {                        u16DataLength = PACKET_DATA_SEGMENT_SIZE;    //设置数据最大值                    }                    u8Ret = Flash_WriteBytes(u32FlashAddr, (uint8_t *)&u8FrameData[PACKET_DATA_INDEX], u16DataLength); //把所有数据写入flash                    if (Ok != u8Ret)                                 //如果写数据失败                           {                        u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ERROR;                //反馈上位机错误 标志                    }                    else                                             //如果写数据成功                    {                        u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;                   //反馈上位机成功 标志                    }                }                else                                                 //如果地址无效                {                    u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR;               //反馈上位机地址错误                }                Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);             //发送应答帧到上位机                break;            case  PACKET_CMD_CRC_FLASH    :                          //查询flash校验值 指令码                if (1u == u8FlashAddrValid)                          //如果地址有效                {                    u32FlashLength = u8FrameData[PACKET_DATA_INDEX] +                                                     (u8FrameData[PACKET_DATA_INDEX + 1] << 8)  +                                    (u8FrameData[PACKET_DATA_INDEX + 2] << 16) +                                    (u8FrameData[PACKET_DATA_INDEX + 3] << 24);             //获取待校验flash大小                    if ((u32FlashLength + u32FlashAddr) > (FLASH_BASE + FLASH_SIZE))        //如果flash长度超出有效范围                    {                        u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_FLASH_SIZE_ERROR;     //反馈上位机flash尺寸错误                    }else                    {                        u16Ret = Cal_CRC16(((unsigned char *)u32FlashAddr), u32FlashLength);//读取flash指定区域的值并计算crc值                        u8FrameData[PACKET_FLASH_CRC_INDEX] = (uint8_t)u16Ret;              //把crc值存储到应答帧                        u8FrameData[PACKET_FLASH_CRC_INDEX+1] = (uint8_t)(u16Ret>>8);                        u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;                   //反馈上位机成功 标志                    }                }                else                                                                        //如果地址无效                {                    u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR;               //反馈上位机地址错误                }                Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE+2);           //发送应答帧到上位机                break;            case  PACKET_CMD_JUMP_TO_APP  :                          //跳转至APP 指令码                Flash_EraseSector(BOOT_PARA_ADDRESS);                //擦除BOOT parameter 扇区                u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;    //反馈上位机成功                Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);             //发送应答帧到上位机                return Ok;                                           //APP更新完成,返回OK,接下来执行跳转函数,跳转至APP            case  PACKET_CMD_APP_UPLOAD   :                          //数据上传                if (1u == u8FlashAddrValid)                          //如果地址有效                {                    u32Temp = u8FrameData[PACKET_DATA_INDEX] +                              (u8FrameData[PACKET_DATA_INDEX + 1] << 8)  +                              (u8FrameData[PACKET_DATA_INDEX + 2] << 16) +                              (u8FrameData[PACKET_DATA_INDEX + 3] << 24);                   //读取上传数据长度                    if (u32Temp > PACKET_DATA_SEGMENT_SIZE)                                 //如果数据长度大于最大值                    {                        u32Temp = PACKET_DATA_SEGMENT_SIZE;                                 //设置数据长度为最大值                    }                    Flash_ReadBytes(u32FlashAddr, (uint8_t *)&u8FrameData[PACKET_DATA_INDEX], u32Temp); //读flash数据                    u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;                       //反馈上位机成功 标志                    Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE + u32Temp);//发送应答帧到上位机                }                else                                                  //如果地址无效                {                    u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_ADDR_ERROR;               //反馈上位机地址错误 标志                    Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);         //发送应答帧到上位机                }                break;            case  PACKET_CMD_START_UPDATE :                           //启动APP更新(此指令正常在APP程序中调用)                u8FrameData[PACKET_RESULT_INDEX] = PACKET_ACK_OK;     //反馈上位机成功 标志                Modem_SendFrame(&u8FrameData[0], PACKET_INSTRUCT_SEGMENT_SIZE);             //发送应答帧到上位机                break;        }        enFrameRecvStatus = FRAME_RECV_IDLE_STATUS;                   //帧数据处理完成,帧接收状态恢复到空闲状态    }        return OperationInProgress;                                       //返回,APP更新中。。。}
复制代码


4.2、User Application 程序设计


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


//UART0中断函数void Uart0_IRQHandler(void){    if(Uart_GetStatus(M0P_UART0, UartRC))         //UART0数据接收    {        Uart_ClrStatus(M0P_UART0, UartRC);        //清中断状态位        u8RxData[u8RxCnt] = Uart_ReceiveData(M0P_UART0);   //接收数据字节        u8RxCnt++;                 if(u8RxCnt>=18)        {            u8RxCnt = 0;            if ((u8RxData[0]==0x6D)&&(u8RxData[1]==0xAC)&&(u8RxData[6]==0x26)&&(u8RxData[16]==0xA6)&&(u8RxData[17]==0xDA)) //是APP更新帧            {                for(uint32_t i=0;i<18;i++)                {                    Uart_SendDataPoll(M0P_UART0,u8TxData[i]); //查询方式发送数据                }                //boot para区域写标记值,通知BootLoader要更新程序了                Flash_SectorErase(0xF00);                Flash_WriteWord(0xF00, 0x12345678);                		        NVIC_SystemReset();  //软件复位MCU            }                            }    }        if(Uart_GetStatus(M0P_UART0, UartTC))         //UART0数据发送    {        Uart_ClrStatus(M0P_UART0, UartTC);        //清中断状态位    } }
复制代码


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方案的实现

发布于: 2021 年 03 月 10 日阅读数: 7
用户头像

【研究方向】物联网、嵌入式、AI、Python 2018.02.09 加入

【公众号】美男子玩编程

评论

发布
暂无评论
电子产品如何使用IAP方式升级程序