- IAP即在線應用編程,平時我們寫好的程序都是通過下載器去下載的,但是對於組裝好的產品在想更新底層硬件代碼是很麻煩的事情,如果在公司情況還沒那么糟糕,要是發出去的產品出現bug,你不可能要用戶給你下載程序的。IAP這種技術,我們就可以像軟件一樣,可以實現遠程更新了。
- 我們需要做的就是,寫單片機FLASH的讀寫接口,程序可以通過上位機進行下發,然后單片機程序調用FLASH寫函數,把下發的代碼寫到對於FLASH進行覆蓋,即實現更新。當然這只是一個大概思路,具體實現還是要注意很多細節的東西。網上也有好多關於這方面的教程,但是能用到項目中的卻很少,我寫這邊文章就是想和大家分享我在實際項目中應用。
- 剛好項目中用到了在線升級功能,趁着還有設計思路,就以我實際開發過程來寫吧,這里對新人來說也可以當作一篇教程來學習。
一、FLASH讀寫接口的實現
- 這里大家可以參考原子哥的FLASH模擬EEPROM實驗來寫。因為我們做的是程序更新,數據流很大,需要做一些優化,以加快寫入速度。
- 首先我們來了解一下STM32F1的FLASH,如下圖,我們要看的只有主存儲區,可以看到單片機內部FLASH是按2K一頁來區分的,而且對其讀寫是有如下幾點要求:
- 每次寫入必須為2個字節。
- 寫入地址為2的倍數。
- 寫入之前必須是被擦除的(即其值為0xFFFF),也可以理解為,寫入數據只能把位寫0,不能置1。
- 寫入速度≤24MHz。
- 擦除方式:頁擦除和整片擦除(這個要注意,如果你是做數據保存,就必須先把這一頁的數據讀取到緩存中,然后修改緩存里的值,再整頁寫入)。

- 解鎖
- 讀頁數據到緩存
- 頁擦除
- 修改緩存數據
- 把緩存數據頁寫入
- 上鎖
- 首先我們得有一些基本的讀寫函數,寫函數官方庫已經為我們提供,我們要寫的就是讀函數,代碼如下:
//讀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分區,寫更新標志,復位單片機。
- 這里要注意幾點:
- 調用NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x03000);設置中斷向量表偏移地址,因為App分區起始地址是0x08003000,所有偏移地址為0x03000。
- 設置App程序起始地址為0x08003000,如下圖。

- 調用__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);
}
}
- 整個程序的實現到這里已經完成,可能我這里說的不是很清晰,有疑問或者寫的不足的地方,可以在下發留言,有時間我也會為大家解答。