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);
	}
}
  • 整个程序的实现到这里已经完成,可能我这里说的不是很清晰,有疑问或者写的不足的地方,可以在下发留言,有时间我也会为大家解答。
posted @ 2019-09-21 18:24  天堂的风声  阅读(12180)  评论(7编辑  收藏  举报