STM32串⼝IAP在线升级
IAP即在线应⽤编程,平时我们写好的程序都是通过下载器去下载的,但是对于组装好的产品在想更新底层硬件代码是很⿇烦的事情,如果在公司情况还没那么糟糕,要是发出去的产品出现bug,你不可能要⽤户给你下载程序的。IAP这种技术,我们就可以像软件⼀样,可以实现远程更新了。
我们需要做的就是,写单⽚机FLASH的读写接⼝,程序可以通过上位机进⾏下发,然后单⽚机程序调⽤FLASH写函数,把下发的代码写到对于FLASH进⾏覆盖,即实现更新。当然这只是⼀个⼤概思路,具体实现还是要注意很多细节的东西。⽹上也有好多关于这⽅⾯的教程,但是能⽤到项⽬中的却很少,我写这边⽂章就是想和⼤家分享我在实际项⽬中应⽤。
刚好项⽬中⽤到了在线升级功能,趁着还有设计思路,就以我实际开发过程来写吧,这⾥对新⼈来说也可以当作⼀篇教程来学习。
⼀、FLASH读写接⼝的实现
这⾥⼤家可以参考原⼦哥的FLASH模拟EEPROM实验来写。因为我们做的是程序更新,数据流很⼤,需要做⼀些优化,以加快写⼊速度。
⾸先我们来了解⼀下STM32F1的FLASH,如下图,我们要看的只有主存储区,可以看到单⽚机内部FLA
SH是按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分区,存放⼀些标志性数据。
分区⼤⼩扇区备注
Bootloader12K0 - 5引导程序
APP100K  6 - 55存储App
Download100K56 - 105下载缓存
Flag2K255升级标志
三、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("\r\n");
IAP_Copy_App();//拷贝到app分区
printf("Updata \r\n");
}
//判断是否有APP程序
//中断向量表判断
if(((*(vu32*)(FLASH_APP_ADDR + 4))&0xFF000000) == 0x08000000)
{
printf("\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();
}

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。