STM32 串口IAP在線升級


  • IAP即在線應用編程,平時我們寫好的程序都是通過下載器去下載的,但是對於組裝好的產品在想更新底層硬件代碼是很麻煩的事情,如果在公司情況還沒那么糟糕,要是發出去的產品出現bug,你不可能要用戶給你下載程序的。IAP這種技術,我們就可以像軟件一樣,可以實現遠程更新了。
  • 我們需要做的就是,寫單片機FLASH的讀寫接口,程序可以通過上位機進行下發,然后單片機程序調用FLASH寫函數,把下發的代碼寫到對於FLASH進行覆蓋,即實現更新。當然這只是一個大概思路,具體實現還是要注意很多細節的東西。網上也有好多關於這方面的教程,但是能用到項目中的卻很少,我寫這邊文章就是想和大家分享我在實際項目中應用。
  • 剛好項目中用到了在線升級功能,趁着還有設計思路,就以我實際開發過程來寫吧,這里對新人來說也可以當作一篇教程來學習。

一、FLASH讀寫接口的實現

  • 這里大家可以參考原子哥的FLASH模擬EEPROM實驗來寫。因為我們做的是程序更新,數據流很大,需要做一些優化,以加快寫入速度。
  • 首先我們來了解一下STM32F1的FLASH,如下圖,我們要看的只有主存儲區,可以看到單片機內部FLASH是按2K一頁來區分的,而且對其讀寫是有如下幾點要求:
  1. 每次寫入必須為2個字節。
  2. 寫入地址為2的倍數。
  3. 寫入之前必須是被擦除的(即其值為0xFFFF),也可以理解為,寫入數據只能把位寫0,不能置1。
  4. 寫入速度≤24MHz。
  5. 擦除方式:頁擦除和整片擦除(這個要注意,如果你是做數據保存,就必須先把這一頁的數據讀取到緩存中,然后修改緩存里的值,再整頁寫入)。

  • FLASH寫入過程如下:
  1. 解鎖
  2. 讀頁數據到緩存
  3. 頁擦除
  4. 修改緩存數據
  5. 把緩存數據頁寫入
  6. 上鎖
  • 首先我們得有一些基本的讀寫函數,寫函數官方庫已經為我們提供,我們要寫的就是讀函數,代碼如下:
//讀1個字節
uint8_t FLASH_ReadByte(uint32_t Addr)
{
	return *(vu8 *)Addr;
}
//讀2個字節
uint16_t FLASH_ReadHalfWord(uint32_t Addr)
{
	return *(vu16 *)Addr;
}
//讀N個字節
void FLASH_ReadNByte(uint32_t Addr,uint8_t *pBuff,uint32_t Len)
{
	uint32_t i;
	
	for(i = 0;i < Len;i++)
	{
		pBuff[i] = FLASH_ReadByte(Addr);
		Addr += 1;
	}
}
  • 然后就是在基本函數的基礎上面擴展我們需要的函數,因為升級過程中,我們需要保存一些標志,需要用到讀某一頁的函數。
#define STM32_SECTOR_SIZE	2048	//頁大小
#define STM32_SECTOR_NUM	255		//頁數

//STM32 FLASH的起始地址
#define STM32_FLASH_BASE 0x08000000

void FLASH_ReadPage(uint8_t Page_Num,uint8_t *pBuff)
{
	uint16_t i;
	uint32_t Buff;
	uint32_t Addr;
	
	//是否超出范圍
	if(Page_Num > STM32_SECTOR_NUM)
		return;
	//先計算頁首地址
	Addr = Page_Num * STM32_SECTOR_SIZE + STM32_FLASH_BASE;
	
	for(i = 0;i < STM32_SECTOR_SIZE;i += 4)
	{
		Buff = FLASH_ReadWord(Addr);
		
		pBuff[i]   = Buff;
		pBuff[i+1] = Buff >> 8;
		pBuff[i+2] = Buff >> 16;
		pBuff[i+3] = Buff >> 24;
		
		Addr += 4;
	}
}
  • 需要讀頁就需要寫頁,再來寫一個寫頁函數,由於一次只能寫2字節,所有我們調用的是官方庫函數FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)。
void FLASH_WritePage(uint8_t Page_Num,uint8_t *pBuff)
{
	uint16_t i;
	uint16_t Buff;
	uint32_t Addr;
	
	//是否超出范圍
	if(Page_Num > STM32_SECTOR_NUM)
		return;
	//解鎖
	FLASH_Unlock();
	//先計算頁首地址
	Addr = Page_Num * STM32_SECTOR_SIZE + STM32_FLASH_BASE;
	
	for(i = 0;i < STM32_SECTOR_SIZE ;i += 2)
	{
		Buff = ((uint16_t)pBuff[i+1] << 8) | pBuff[i];
		FLASH_ProgramHalfWord(Addr,Buff);
		Addr += 2;
	}
	//上鎖
	FLASH_Lock();
}
  • 然后我們還要寫兩個重要的函數,他們都是寫N字節函數,區別是一個要先把頁數據讀到緩存中,再寫入,這個函數用來保存一些標志等等,另一個函數我們不負責扇區數據擦除保存等處理,我們只管往某個地址寫入數據,這個函數用來做升級用,這樣速度會快一些。下來就來實現這兩個函數。
void FLASH_WriteNData(uint32_t Addr,uint8_t *pBuff,uint32_t Len)
{
	uint32_t Offset;
	uint8_t  Page_Num;
	uint16_t Page_Offset;
	uint16_t Free_Space;
	uint16_t i;
	
	if((Addr < STM32_FLASH_BASE) || (Addr > STM32_FLASH_END))
		return;
	
	Offset = Addr - STM32_FLASH_BASE;//偏移地址
	Page_Num = Offset / STM32_SECTOR_SIZE;//得到地址所在頁
	Page_Offset = Offset % STM32_SECTOR_SIZE;//在頁內的偏移地址
	Free_Space = STM32_SECTOR_SIZE -  Page_Offset;//頁區剩余空間
	//要寫入的數據是否大於剩余空間
	if(Len <= Free_Space)
		Free_Space = Len;
	
	FLASH_Unlock();//解鎖
	
	while(1)
	{
		FLASH_ReadPage(Page_Num,STM32_FLASH_BUFF);//先把數據讀到緩存中
		FLASH_ErasePage(Page_Num * STM32_SECTOR_SIZE + STM32_FLASH_BASE);//頁擦除
		//修改緩存數據
		for(i = 0;i < Free_Space;i++)
		{
			STM32_FLASH_BUFF[i+Page_Offset] = pBuff[i];
		}
		FLASH_WritePage(Page_Num,STM32_FLASH_BUFF);//把緩存數據寫入
		//判斷是否超出當前頁,超出進入下一頁
		if(Len == Free_Space)
			break;
		else
		{
			Page_Num++;//下一頁
			Page_Offset = 0;
			pBuff += Free_Space;
			
			Len -= Free_Space;
			if(Len > STM32_SECTOR_SIZE)
				Free_Space = STM32_SECTOR_SIZE;
			else
				Free_Space = Len;
		}
	}
	FLASH_Lock();
}
void FLASH_WriteNByte(uint32_t Addr,uint8_t *pBuff,uint32_t Len)
{
	uint16_t i;
	uint16_t temp = 0;
	
	if((Addr < STM32_FLASH_BASE) || (Addr > STM32_FLASH_END))
		return;
	
	FLASH_Unlock();//解鎖
	
	for(i = 0;i < Len;i += 2)
	{
		temp = pBuff[i];
		temp |= (uint16_t)pBuff[i+1] << 8;
		
		FLASH_ProgramHalfWord(Addr,temp);
		Addr += 2;
		if(Addr > STM32_FLASH_END)
		{
			FLASH_Lock();
			return;
		}
	}
	FLASH_Lock();
}
  • 因為我們程序可能會占用多頁,所以我們需要寫一個擦除指定頁的函數,代碼如下。
void Flash_EraseSector(uint8_t Start_Page,uint8_t End_Page)
{
	uint8_t i;
	uint8_t num = 0;
	
	if(Start_Page > End_Page)
		return;
	
	FLASH_Unlock();//解鎖
	
	num = End_Page - Start_Page;//擦除頁數
	
	for(i = 0;i <= num;i++)
	{
		FLASH_ErasePage((Start_Page + i) * STM32_SECTOR_SIZE + STM32_FLASH_BASE);//頁擦除
	}
	
	FLASH_Lock();
}
  • 我們寫了幾個接口,我們要測試一下是否好用,開發就是要穩扎穩打,保證每個功能穩定。測試嘛,給它們搭一個小舞台,讓它們上去表演一下,哈哈。我們要的就是往某頁寫入數據,再讀出來,看看是否相同,注意你程序的大小不要把當前運行的代碼覆蓋咯。下面是我的測試代碼:
void Test_Flash_WR(uint8_t Page_Num)
{
	uint16_t i = 0;
	uint8_t j = 0;
	
	//是否超出范圍
	if(Page_Num > STM32_SECTOR_NUM)
		return;
	
	for(i = 0;i < STM32_SECTOR_SIZE;i++)
	{
		buff[i] = j++;
	}
	//頁擦除
//	Flash_EraseSector(Page_Num,Page_Num);
	//寫入
//	FLASH_WritePage(Page_Num,buff);
	//寫入
//	FLASH_WriteNByte(Page_Num * STM32_SECTOR_SIZE + STM32_FLASH_BASE,buff,STM32_SECTOR_SIZE);
	//寫入
	FLASH_WriteNData(Page_Num * STM32_SECTOR_SIZE + STM32_FLASH_BASE + 4,buff,10);
	//清零
	memset(buff,0,STM32_SECTOR_SIZE);
	//讀出
	FLASH_ReadPage(Page_Num,buff);
	
	for(i = 0;i < STM32_SECTOR_SIZE;i++)
	{
		printf("%02X ",buff[i]);
	}
	printf("\r\n");
}
  • 以上只是接口的功能實現,大概了解每個函數是如何實現的,以及它的功能即可,下面才是設計思路。

二、分區規划

  • 寫完FLASH接口函數,下來就是進行對我們的FLASH進行分區了,這樣才知道我們的數據到底應該寫到哪里。下面是我自己使用的分區方式。
  • 首先是Bootloader分區,放置我們的引導程序,主要負責判斷標志來決定是跳轉執行app程序,還是進行固件更新。
  • 其次是APP分區,這里存放的是我們的主程序。
  • 下來是Download分區,負責存儲我們下發的更新代碼,這樣做是保證代碼完整再進行更新,保證更新成功率。實際項目也不可能開辟大內存給更新用,一般都是緩存到FLASH中。
  • 最后是Flag分區,存放一些標志性數據。
分區 大小 扇區 備注
Bootloader 12K 0 - 5 引導程序
APP 100K 6 - 55 存儲App
Download 100K 56 - 105 下載緩存
Flag 2K 255 升級標志

三、Bootloader程序實現

  • 說一下Bootloader程序設計思路吧,單片機上電進入Bootloader程序,先判斷升級標志是否需要升級固件,需要就把Download分區拷貝到app分區,然后清空升級標志;下來判斷APP分區中斷向量表是否正確,正確說明有app可以跑,直接跳轉到app運行;如果沒有在bootloader里循環等待接收app固件。下面是我程序的整體框架:
#define FLASH_APP_ADDR                STM32_SECTOR6_ADDR
#define FLASH_DOWNLOAD_ADDR     STM32_SECTOR56_ADDR
#define FLASH_APP_FLAG                 STM32_SECTOR255_ADDR
#define FLASH_UPDATA_FLAG           FLASH_APP_FLAG + 2

int main(void)
{
	SystemInit();
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	Delay_Init();
	
	LED_Init();
	KEY_Init();
	USART1_Init(115200);
	//判斷是否需要升級固件
	if(FLASH_ReadHalfWord(FLASH_UPDATA_FLAG) == 0xAA55)
	{
		printf("Updata App...\r\n");
		
		IAP_Copy_App();//拷貝到app分區
		
		printf("Updata App Succeed...\r\n");
	}
	//判斷是否有APP程序
	//中斷向量表判斷
	if(((*(vu32*)(FLASH_APP_ADDR + 4))&0xFF000000) == 0x08000000)
	{
		printf("Run App...\r\n\r\n");
		
		Delay_ms(10);
		IAP_Load_App(FLASH_APP_ADDR);//轉到app
	}
	
	printf("No App\r\n");
	
	TIM3_Init(1000,72);//定時0.001s
	
	while(1)
	{
		Task_Process();
        
        if(USART1_RX_CNT > 0)
		{
			IAP_WriteBin(FLASH_DOWNLOAD_ADDR,USART1_RxBuff,USART1_RX_CNT);
			USART1_RX_CNT = 0;
		}
	}
}
  • 這里我們需要實現的函數有分區拷貝函數IAP_Copy_App()和跳轉函數IAP_Load_App(),代碼如下:
typedef void (*IAP_Fun)(void);
IAP_Fun JumpApp;

uint8_t STM32_FLASH_BUFF[STM32_SECTOR_SIZE] = {0};

void IAP_Copy_App(void)
{
	uint8_t i;
	uint8_t buf[2] = {0x00,0x00};
	//擦除App扇區
	Flash_EraseSector(6,55);
	
	for(i = 0;i < 50;i++)
	{
		FLASH_ReadPage(56 + i,STM32_FLASH_BUFF);
		FLASH_WritePage(6 + i,STM32_FLASH_BUFF);
		LED3 = !LED3;
	}
	
	FLASH_WriteNData(FLASH_UPDATA_FLAG,buf,2);
}

void IAP_Load_App(uint32_t Addr)
{
	//檢查棧頂地址是否合法
	if(((*(vu32*)Addr) & 0x2FFE0000) == 0x20000000)
	{
		__disable_irq();
		JumpApp = (IAP_Fun)*(vu32 *)(Addr + 4);
		MSR_MSP(*(vu32 *)Addr);
		JumpApp();
	}
}
  • 然后我們還要寫一個關於下載程序的函數IAP_WriteBin(),一般我們數據會通過串口或網口下發過來,下發的數據要保存到下載分區,所以需要一個寫數據到下載分區的函數。
void IAP_WriteBin(uint32_t Addr,uint8_t *pBuff,uint32_t Len)
{
	uint8_t buf[2] = {0x55,0xAA};
	//擦除DOWNLOAD扇區
	Flash_EraseSector(56,105);
    //更新標記
	FLASH_WriteNData(FLASH_UPDATA_FLAG,buf,2);
	//寫入程序
	FLASH_WriteNByte(Addr,pBuff,Len);
	//復位單片機
	NVIC_SystemReset();
}
  • 最后我們再捋一下思路,如果我們運行再Bootloader程序的循環里,那么我們先判斷是否接收到了串口下發的程序,如果有下發過來的程序,我們就往DOWNLOAD分區里面寫入數據,然后寫標志位說明我們需要更新固件,寫入數據完成,我們復位單片機,重新啟動,檢測到升級標志有更新,那么就調用IAP_Copy_App把DOWNLOAD分區拷貝到App分區,然后檢測到程序合法,就跳轉到App程序。
  • 我這里用的原子哥的XCOM發送bin文件的,是一次發完,所以我的數據也是一次寫完,實際工程中,我們會分包處理,比如100K數據一般分2K,50次下發,協議里標記是哪一包數據,單片機做地址累加寫入即可。因為我也不會寫上位機,所有沒辦法給大家寫那么詳細了,反正思路差不多,相信大家也很聰明的,多思考才能進步。

四、App程序實現

  • App里面除了實現本身的功能以外,我們還要做接收程序更新准備,串口接收到數據寫入DOWNLOAD分區,寫更新標志,復位單片機。
  • 這里要注意幾點:
  1. 調用NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x03000);設置中斷向量表偏移地址,因為App分區起始地址是0x08003000,所有偏移地址為0x03000。
  2. 設置App程序起始地址為0x08003000,如下圖。
  3. 調用__enable_irq();打開中斷總開關,因為Bootloader里面關閉了中斷總開關
int main(void)
{
	SystemInit();
	NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x03000);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	Delay_Init();
	
	LED_Init();
	KEY_Init();
	USART1_Init(115200);
	TIM3_Init(1000,72);//定時0.001s
	
	__enable_irq();
	
	while(1)
	{
		Task_Process();
		
		if(USART1_RX_CNT > 0)
		{
			IAP_WriteBin(FLASH_DOWNLOAD_ADDR,USART1_RxBuff,USART1_RX_CNT);
			USART1_RX_CNT = 0;
		}
	}
}

五、串口接收函數實現

  • 串口接收部分我使用的是STM32串口DMA接收功能,采用空閑中斷的方式,這樣一次接收完成所有數據產生一次中斷。具體的實現大家可以查一下網上的一下文章,后面有空我也會寫一篇關於串口DMA接收的文章。
#define USART_DATA_MAX	50 * 1024

uint8_t USART1_RxBuff[USART_DATA_MAX];
uint16_t USART1_RX_CNT;

void USART1_IRQHandler(void)
{
	uint8_t Res;

	if(USART_GetITStatus(USART1,USART_IT_IDLE) != RESET)
	{
		//清除中斷標志
		Res = USART1->SR;
		Res = USART1->DR;

		DMA_Cmd(DMA1_Channel5,DISABLE);
		USART1_RX_CNT = USART_DATA_MAX - DMA_GetCurrDataCounter(DMA1_Channel5);//接收長度
		
		printf("Rcv %d Byte!\r\n",USART1_RX_CNT);
		//重新接收
		DMA_SetCurrDataCounter(DMA1_Channel5,USART_DATA_MAX);
		DMA_Cmd(DMA1_Channel5,ENABLE);
	}
}
  • 整個程序的實現到這里已經完成,可能我這里說的不是很清晰,有疑問或者寫的不足的地方,可以在下發留言,有時間我也會為大家解答。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM