本文介紹如何在STM32上實現升級功能,程序包括:bootloader和APP(也叫IAP, In Application Programming),基於STM32F103RCT6型號的MCU作爲實驗平臺,以STM32CubeMX工具進行工程的建立及底層配置等工程,工程基於STM32 HAL庫開發。
一、整體框架
整體上,在flash上燒寫2個程序,bootloader和APP。
bootloader程序位於0x80000000處,即默認的程序啓動地址;
APP程序則位於bootloader程序的往後某地址,空間大小需自行定義。
STM32F103RCT6的flash大小爲256K。
1、flash分區概述
如我flash空間分配如下:
2、各分區說明
分區 | 地址 | 大小 | 作用 |
---|---|---|---|
bootloader | 0x8000 0000 | 32K | 校驗、引導APP、升級 |
param | 0x8000 8000 | 16K | 參數區,保存一些斷電不丟失參數 |
APP | 0x8000 C000 | 192K | 主應用程序 |
reserve | 0x8003 0000 | 16K | 預留區(可用作參數的備份,或其他) |
二、bootloader
bootloader的主要功能:校驗數據、啓動APP、升級APP。
bootloader的工作流程如下:
1、基礎功能初始化(時鐘、外設等);
主要進行一些BSP板級初始化:(僅供參考,因工程而異)
/******************************************************************/
/**
* BSP初始化函數\n
*
*/
/******************************************************************/
void BSP_Init(void)
{
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
/* Initialize GPIO */
MX_GPIO_Init();
/* Initialize usart */
MX_UART_Init();
/* Initialize timer */
Timer_ParamInit();
MX_TIM3_Init();
MX_TIM4_Init();
}
2、數據校驗(參數區信息、APP程序的檢驗)
此步驟爲後續啓動過程讀取一些基礎參數,以及校驗數據的準確性等。
讀取參數區的參數:如我將升級相關的參數寫在此分區(目前僅是啓動標誌,具體可自行定義),根據此標誌來判斷下一步該如何走。
/* flash參數區信息結構 */
struct param_info
{
UINT16 usStartFlag; // 啓動標誌 0x0A-跳至APP 0x0B-等待升級 0x0F-已強制啓動過
};
校驗數據:如我將校驗APP程序區的數據是否正常,採用CRC校驗。
/******************************************************************/
/**
* 檢驗flash參數區函數\n
*
*/
/******************************************************************/
int check_paramInfo(UINT32 unParamAddr, UINT32 unAppAddr, UINT32 unAppRunOffset)
{
struct param_info stParams = {0};
INT32 nRetAppHead = 0;
/* 檢查參數區-判斷啓動APP/升級? */
memset(&stParams, 0, sizeof(struct param_info));
cpuflash_read(unParamAddr, (UINT8 *)&stParams, sizeof(struct param_info));
if(stParams.usStartFlag == BOOT_FALG_NORMAL_RUNAPP) // 直接跳轉APP
{
/* APP校驗 */
nRetAppHead = check_AppInfo(unAppAddr, unAppRunOffset);
if(nRetAppHead == 0)
{
HAL_TIM_Base_Stop_IT(&htim3); // ??? 不關中斷跳轉不了-原因未明
loadAPP(unAppAddr +unAppRunOffset);
}
else
{
printf("%s: check_AppInfo failed, [may be crc error] !\n", __FUNCTION__);
}
}
/* 若參數/APP頭信息錯誤-嘗試強制啓動 */
if((stParams.usStartFlag!=BOOT_FALG_WAIT_UPGRADE && stParams.usStartFlag!=BOOT_FALG_FORCE_RUNAPP) || nRetAppHead!=0)
{
force_loadAPP(unParamAddr, unAppAddr, unAppRunOffset);
}
else
printf("%s: ------------ wait to upgrade ------------\n", __FUNCTION__);
return 0;
}
流程:先讀取參數,判斷下一步是直接啓動APP還是留在bootloader等待升級。若是啓動APP則校驗APP程序數據是否正常,若校驗失敗則可嘗試強制啓動一次(啓動失敗也沒關係,看門狗會自動復位)。
3、跳轉APP或升級APP。
如何跳轉至APP呢?跳轉函數:
/*****************************************************************/
/**
* 加載APP \n
*
*/
/******************************************************************/
void loadAPP(INT32U unLoadAddr)
{
void (*fnJump2APP)(void);
INT32U unJumpAddr;
if(((*(__IO INT32U *)unLoadAddr) & 0x2FFE0000) == 0x20000000) /* 檢查棧頂地址是否合法 */
{
printf("%s: ----------------------> run APP addr: 0x%x\r\n", __FUNCTION__, unLoadAddr);
/* 用戶代碼區第5~8字節爲程序開始地址(復位地址) */
unJumpAddr = *(__IO INT32U *)(unLoadAddr + 4);
fnJump2APP = (void (*)(void))unJumpAddr;
/* 初始化APP堆棧指針(用戶代碼區的前4個字節用於存放棧頂地址) */
__set_MSP(*(__IO INT32U *)unLoadAddr);
fnJump2APP();
}
else
{
printf("ERROR: %s: Stack top address is not valid! Can not run func!\r\n", __FUNCTION__);
while(1);
}
}
升級,即讀取到的參數標誌爲升級狀態,則留在bootloader等待接收升級包數據(APP程序數據),並將其寫入flash的APP分區。(具體見程序)
三、APP
APP的主要功能:除升級功能外的所有應用功能,及跳轉至bootloader準備升級。
中斷向量表重映射:由於APP程序的起始地址的變化,因此需要重映射,否則程序異常。
/* 宏定義 */
#define IS_NVIC_VECTTAB(VECTTAB) (((VECTTAB) == SRAM_BASE) || ((VECTTAB) == FLASH_BASE))
#define IS_NVIC_OFFSET(OFFSET) ((OFFSET) < 0x000FFFFF)
/****************************************************************
* Func :
* Desc : 中斷向量表重映射
* Input : flash中斷向量地址(一般爲0x08000000U)、偏移地址(flash程序燒寫地址相對NVIC_VectTab偏移)
* Output:
* Return:
*****************************************************************/
void NVIC_SetVectorTable(UINT32 NVIC_VectTab, UINT32 Offset)
{
/* Check the parameters */
assert_param(IS_NVIC_VECTTAB(NVIC_VectTab));
assert_param(IS_NVIC_OFFSET(Offset));
SCB->VTOR = NVIC_VectTab | (Offset & (UINT32)0x1FFFFF80);
}
跳轉功能可用bootloader的跳轉函數,也可直接重啓reboot,兩種方式都可達到目的---進入bootloader運行。
四、升級功能
升級,即是將新的程序數據替換舊的程序數據,因此,只需在程序數據所在區域擦除舊數據再寫上新數據即可。
程序數據位置CPU內部flash區,因此需要以flash的讀寫擦等函數操作爲基礎,如下:
/****************************************************************
* Func :
* Desc : 讀取CPU內部flash
* Input :
* Output:
* Return:
*****************************************************************/
INT32 cpuflash_read(UINT32 unStartAddr, UINT8 *pData, UINT16 usSize)
{
if(pData == NULL)
return -1;
memcpy(pData, (INT8U *)unStartAddr, usSize);
return 0;
}
/****************************************************************
* Func :
* Desc : 寫入CPU內部flash (要先erase才能寫)
* Input :
* Output:
* Return:
*****************************************************************/
INT32 cpuflash_write(UINT32 unStartAddr, UINT8 *pData, UINT16 usSize)
{
INT32 i = 0;
INT32 nRet = 0;
UINT16 usTemp1 = 0;
UINT16 usTemp2 = 0;
UINT16 usTempALL = 0;
if(usSize%2 != 0)
{
usSize += 1;
}
HAL_FLASH_Unlock(); // unlock
for(i=0; i<usSize/2; i++)
{
usTemp1 = *pData;
usTemp2 = *(pData+1);
usTempALL = ((usTemp1&0X00FF) | ((usTemp2<<8)&0XFF00));
//usTemp = ((*pData>>8)&0X00FF) | (*(pData+1)&0XFF00);
//usTemp = *(INT16U *)pData;/*這個會導致硬件崩潰*/
nRet = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, unStartAddr, usTempALL);
if(nRet != HAL_OK)
{
HAL_FLASH_Lock(); // lock
printf("ERROR: %s: program[%d %d] failed-code[%d]\n", __FUNCTION__, usTemp1, usTemp2, nRet);
return -1;
}
unStartAddr += 2;
pData += 2;
}
HAL_FLASH_Lock(); // lock
return 0;
}
/****************************************************************
* Func :
* Desc : 擦除CPU內部flash(整頁)
* Input :
* Output:
* Return:
*****************************************************************/
INT32 cpuflash_erase(UINT32 unStartAddr, UINT32 unEndAddr)
{
FLASH_EraseInitTypeDef stEraseInit;
UINT32 ucPageErr = 0;
UINT32 unTempAddr = 0;
INT32 nRet = 0;
HAL_FLASH_Unlock(); // unlock
for(unTempAddr=unStartAddr; unTempAddr<=unEndAddr; unTempAddr+=FLASH_PAGE_SIZE)
{
stEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
stEraseInit.PageAddress = unTempAddr;
stEraseInit.NbPages = 1;
nRet = HAL_FLASHEx_Erase(&stEraseInit, &ucPageErr);
if(nRet != HAL_OK)
{
HAL_FLASH_Lock();
return -1;
}
GPIO_feedDog();
}
HAL_FLASH_Lock(); // lock
return 0;
}
順便說下,STM32內部flash庫的保護問題,若不加保護,則內部程序可輕易被J-Flash等工具讀出。因此,常用的措施是:對內部flash添加讀寫保護機制。鎖定與解除函數如下:
/****************************************************************
* Func :
* Desc : 使能讀保護函數
* Input :
* Output:
* Return:
*****************************************************************/
void cpuflash_enableReadProtect(void)
{
FLASH_OBProgramInitTypeDef OBInit;
__HAL_FLASH_PREFETCH_BUFFER_DISABLE();
HAL_FLASHEx_OBGetConfig(&OBInit);
if(OBInit.RDPLevel == OB_RDP_LEVEL_0)
{
printf("%s: ------------ set ----------\n", __FUNCTION__);
OBInit.OptionType = OPTIONBYTE_RDP;
OBInit.RDPLevel = OB_RDP_LEVEL_1;
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
HAL_FLASHEx_OBProgram(&OBInit);
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
//HAL_FLASH_OB_Launch();
}
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
}
/****************************************************************
* Func :
* Desc : 失能讀保護函數
* Input :
* Output:
* Return:
*****************************************************************/
void cpuflash_disableReadProtect(void)
{
FLASH_OBProgramInitTypeDef OBInit;
__HAL_FLASH_PREFETCH_BUFFER_DISABLE();
HAL_FLASHEx_OBGetConfig(&OBInit);
if(OBInit.RDPLevel == OB_RDP_LEVEL_1)
{
printf("%s: ------------ set ----------\n", __FUNCTION__);
OBInit.OptionType = OPTIONBYTE_RDP;
OBInit.RDPLevel = OB_RDP_LEVEL_0;
HAL_FLASH_Unlock();
HAL_FLASH_OB_Unlock();
HAL_FLASHEx_OBProgram(&OBInit);
HAL_FLASH_OB_Lock();
HAL_FLASH_Lock();
//HAL_FLASH_OB_Launch();
}
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
}
以上爲flash操作的相關函數。
有了flash操作的函數,升級操作也就順理成章了,無非就是一包一包地接收數據,然後一包一包地寫入flash。
其中,通信是通過串口,自己定義協議傳送數據,將程序數據拆成N個包,一包一包地傳輸,例如設定3條協議,分別是:開始升級、發送升級數據、升級完成確認(自定義啦,暫不提供)。
處理一包升級數據的函數:
/****************************************************************
* Func :
* Desc : 初始化
* Input : usSeq-順序號 pData-數據 usDatalen-長度 pstAPPHead-APP頭信息(只第一包有用)
* Output:
* Return:
*****************************************************************/
int update_packDataHandle(UINT16 usSeq, UINT8 *pData, UINT16 usDatalen, struct APP_headinfo *pstAPPHead)
{
static INT16U usLastSeq = 0; // 上一次成功的序號
static INT32U unProgAddr = 0; // 燒寫地址
INT32 nRet = 0;
if(usSeq == usLastSeq) // 重複包
return 0;
if(usSeq == 1)
{
printf("%s: ----------------- the first packet -----------------\n", __FUNCTION__);
/* erase APP area */
nRet = cpuflash_erase(FLASH_PAGE_APP_START, FLASH_PAGE_APP_START +pstAPPHead->unAPPSize);
if(nRet != 0)
return -1;
unProgAddr = FLASH_PAGE_APP_START;
}
if(unProgAddr<FLASH_PAGE_APP_START || unProgAddr>=FLASH_PAGE_APP_END+FLASH_PAGE_SIZE)
return -1;
nRet = cpuflash_write(unProgAddr, pData, usDatalen);
if(nRet != 0)
return -1;
/* 設置燒寫地址偏移和升級包序號-0xFFFF表示最後一包 */
if(usLastSeq!=0xffff && usSeq!=0xffff)
{
unProgAddr += usDatalen;
}
usLastSeq = usSeq;
if (usLastSeq == 0xffff)
{
printf("%s: Recv the last packet data.\r\n", __FUNCTION__);
usLastSeq = 0;
}
return 0;
}
升級大概如此,不盡詳細,歡迎吐槽。
附件:示例工程鏈接
https://github.com/zengzhaorong/stm32_IAP-demo
。