STM32具備升級功能的bootloader及APP/IAP的實現

本文介紹如何在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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章