天天看點

電子産品如何使用IAP方式更新程式

目錄

1、ICP、ISP和IAP的概念

2、IAP更新程式的原理

3、IAP更新程式的流程

4、IAR環境下IAP的實作

4.1、BootLoader程式設計

4.2、User Application程式設計

4.3、IAR位址配置及檔案輸出

5、拓展:解析HEX檔案

在項目開發過程中通常使用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、以太網等通信方式實作程式更新。

通常一塊MCU晶片的Code(代碼)區内隻有一個使用者程式,而IAP方案則是将代碼區劃分為兩部分,兩部分區域各存放一個程式,一個為BootLoader(引導加載程式),另一個為User Application(使用者應用程式)。

BootLoader在出廠時就固定下來了,在需要變更User Application時隻需要通過觸發BootLoader對User Application的擦除和重新寫入即可完成使用者應用的更換。

電子産品如何使用IAP方式更新程式

程式執行初始化後首先會進入BootLoader,在BootLoader裡面檢測條件是否被觸發(可通過按鍵是否被按下、序列槽是否接收到特定的資料、U盤是否插入等),如果有則進行對User Application進行擦除和重新寫入操作新程式,如果沒有則直接跳轉到BootLoader執行User Application。

假設裝置僅有User Application,以STM32F103ZET6為例,其啟動方式有三種:内置FLASH啟動、内置SRAM啟動、系統存儲器ROM啟動。通過BOOT0和BOOT1引腳的設定可以選擇從哪中方式啟動,這裡選擇内置的FLASH啟動,STM32F103ZET6 FLASH的位址為0x08000000—0x0807FFFF,共512KB。

通常STM32發生中斷的過程為以下五步:

1、發生中斷(中斷請求);

2、到中斷向量表查找中斷函數入口位址;

3、跳轉到中斷函數;

4、執行中斷函數;

5、中斷傳回。

也就是說,STM32的内置的Flash中有一個中斷向量表來存放各個中斷服務函數的入口位址,内置Flash的配置設定情況如下圖所示:

電子産品如何使用IAP方式更新程式
是以當隻有一個程式的情況下(僅有User Applicatio時),程式執行的走向如下所示:
電子産品如何使用IAP方式更新程式

解析上圖:

STM32F103ZET6有一個中斷向量表,這個中斷向量表存放在代碼開始部分的後4個位元組處(即0x08000004),代碼開始的4個位元組存放的是堆棧棧頂的位址,當發生中斷後程式通過查找該表得到相應的中斷服務程式入口位址,然後再跳到相應的中斷服務程式中執行。

裝置上電後從0x08000004處取出複位中斷向量的位址,然後跳轉到複位中斷程式的入口(标号①所示),執行結束後跳轉到main函數中(标号②所示)。在執行main函數的過程中發生中斷,則STM32強制将PC指針指回中斷向量表處(标号③所示),從中斷向量表中找到相應的中斷函數入口位址,跳轉到相應的中斷服務函數(标号④所示),執行完中斷函數後再傳回到main函數中來(标号⑤所示)。

下面要講正題了。

若将STM32F103ZET6在内置的Flash裡面添加User Application和BootLoader程式,則Flash配置設定情況大緻如下圖所示:

電子産品如何使用IAP方式更新程式
此時,User Application和BootLoader程式各有一個中斷向量表,假設BootLoader程式占用的空間為N+M位元組,則程式的走向應該如下圖所示:
電子産品如何使用IAP方式更新程式

裝置上電初始程式依然從0x08000004處取出複位中斷向量位址,執行複位中斷函數後跳轉到IAP的main(标号①所示),在IAP的main函數執行完成後(在BootLoader裡面檢測條件是否被觸發(可通過按鍵是否被按下、序列槽是否接收到特定的資料、U盤是否插入等),如果有則進行對User Application進行擦除和重新寫入操作新程式,如果沒有則直接跳轉到BootLoader執行User Application)強制跳轉到0x08000004+N+M處(标号②所示),最後跳轉到新的main函數中來(标号③所示),當發生中斷請求後,程式跳轉到新的中斷向量表中取出新的中斷函數入口位址,再跳轉到新的中斷服務函數中執行(标号④⑤所示),執行完中斷函數後再傳回到main函數中來(标号⑥所示)。

以IAR環境為例,簡單講述IAP的實作步驟。這裡MCU以華大HC32L130為例,因為使用的MCU不同,是以實作的細節也不一緻,但是基本上官方都會提供Demo例程。

本示例Flash配置設定情況為:BootLoader位址:0x00000000~0x00000DFF,User Application位址:0x00001000~0x0000FFFF。

第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更新中。。。
}      

在本示例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);        //清中斷狀态位
    }
}      

最後還需要簡答配置下IAR環境。

第1步:确定輸出的Linker配置位址,因為需要在這裡程式修改位址。

電子産品如何使用IAP方式更新程式

第2步:找到Linker配置檔案,修改BootLoader程式位址:0x00000000~0x00000DFF,User Application程式位址:0x00001000~0x0000FFFF。

電子産品如何使用IAP方式更新程式

第3步:找到User Application程式的配置檔案(字尾為.s的檔案),添加程式中斷向量偏移長度:0x00001000,和BootLoader程式配置檔案相比有兩處不同之處,如下所示:

電子産品如何使用IAP方式更新程式

第4步:将這兩個程式按照ICP方式(SWD、JTAG等)燒錄後,此後就可以使用IAP方式通過序列槽燒錄HEX檔案程式或者BIN檔案程式。輸出及燒錄HEX檔案程式或者BIN檔案程式方式如下圖所示:

電子産品如何使用IAP方式更新程式

HEX檔案可以通過UltraEdit、Notepad++、記事本等工具打開,用Notepad++打開之後會看到以下資料内容:

電子産品如何使用IAP方式更新程式

使用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檔案的結束部分如下圖所示:

電子産品如何使用IAP方式更新程式

最後一行的01表示檔案結束了,最後的FF表示校驗資料,由0x100-0x01=0xFF得來。

電子産品如何使用IAP方式更新程式