1、前言

         在16章我们学习了FLASH的读写操作,FLASH内的数据是断电可保存的,而且stm32的运行程序也是存放在内部FLASH当中,那么可以编写一段程序A,将另一段程序B写入FLASH中并引导单片机运行程序B。

        在专业的角度上程序A称为引导/启动程序(Bootloader),简称Boot。程序B叫应用程序,简称APP。

二、Bootloader

1、目标功能:

1、启动APP:通过更改stm32的执行地址,跳转至APP的启动地址,以实现启动APP功能

2、升级APP:通过串口通讯,将串口接收到的APP程序写入FLASH中,实现升级APP功能。(注:APP需为bin文件!)

 我们可以将用户flash区域分为两个区,一个是Bootloader程序区(0x0800 0000 - 0x0800 5000 )大小为8K Bytes,剩下的为APP程序区(0x0800 5000 - 0x0808 0000)大小为504K Byte。

Bootloader的工作流程:

2. 硬件设计

LED2指示灯用来提示系统运行状态,串口1获取APP数据,并写入FLASH中

  • LED2指示灯
  • USART1
  • STM32F1内部FLASH

3、STM32CubeMX设置

  • RCC设置外接HSE,时钟设置为72M
  • PE5设置为GPIO推挽输出模式、上拉、高速、默认输出电平为高电平
  • USART1选择为异步通讯方式,波特率设置为115200Bits/s,传输数据长度为8Bit,无奇偶校验,1位停止位

可参考STM32CubeMX学习笔记16--- STM32内部FLASH-CSDN博客

4、程序编程

创建Bootloader.h文件,这里定义了FLASH_SAVE_ADDR 0x08005000   //为APP 起始地址,需要更改的直接在这里修改即可。

#ifndef __Bootloader_H__
#define __Bootloader_H__

#include "main.h"  
#include "string.h"
#include "usart.h"
#include "tim.h"

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

#define FLASH_SAVE_ADDR 0x08005000   //APP 起始地址

#define countdown  20   //倒计时20s


//extern uint32_t flashaddress; //中间地址

//extern	uint8_t temp;

//extern uint8_t Rx_Order[200];//接收命令数组
//extern uint8_t Rx_Order_len;

//extern uint8_t Rx_APP[2][2048]; //接收APP数据
//extern uint16_t Rx_APP_len;//APP长度
//extern uint8_t Rx_APP_array;//APP组


//extern uint16_t Tx_Flash_len;//APP长度
//extern uint8_t Tx_Flash_array;//APP组
//extern uint8_t Tx_flag;  //传输标记
//	
//extern uint8_t Jump_flag;  //跳转标志,1时跳转
//extern uint8_t Upgrade_flag;  //升级标志,0:未接收到升级命令 1:接收到升级命令
//extern uint8_t Rx_flag;  //接收完一帧数据标志

//extern uint32_t rx_sum;
	
void overtime_check(void);

void BootInit(void);
void Bootloader(void);
void System_jumpApp(uint32_t APP_ADDR);	
uint8_t BootFlash(uint32_t WriteAddr,uint16_t *pBuffer,uint16_t NumToWrite);

extern void FLASH_PageErase(uint32_t PageAddress);

#endif


 编写Bootloader.c文件,为了方便使用,我将串口中断和定时器中断都放在了Bootloader.c文件中。有需要的朋友只需要复制这两个文件即可,注意这里有一个升级命令,需要通过串口发送这个升级命令后才可以升级APP。

#include "Bootloader.h"

uint8_t upcmd[]="#sic,up APP"; //升级命令,当接收到这条命令时进行升级

uint32_t flashaddress = FLASH_SAVE_ADDR;  //中间地址

uint8_t temp; 

uint8_t Rx_Order[200];//接收命令数组
uint8_t Rx_Order_len=0;

uint8_t Rx_APP[2][2048]; //接收APP数据
uint16_t Rx_APP_len=0;//APP长度
uint8_t Rx_APP_array=0;//APP组


uint16_t Tx_Flash_len=0;//APP长度
uint8_t Tx_Flash_array=0;//APP组
uint8_t Tx_flag=0;  //传输标记

uint8_t Jump_flag=0;  //跳转标志,1时跳转
uint8_t Upgrade_flag=0;  //升级标志,0:未接收到升级命令 1:接收到升级命令
uint8_t Rx_flag=0;  //接收完一帧数据标志
 
uint32_t rx_time=0; //实时时间

uint32_t rx_sum=0;  //计算总写入字节数量


//Boot初始化,主要开启定时器中断和串口中断
void BootInit(void)
{
	HAL_TIM_Base_Start_IT(&htim3);        //启动定时器中断模式计数
	 HAL_UART_Receive_IT(&huart1,&temp,1);//启动串口中断
}


/*
Flash写入函数
WriteAddr:写入地址
pBuffer:写入数据
NumToWrite:写入长度
*/
uint8_t BootFlash(uint32_t WriteAddr,uint16_t *pBuffer,uint16_t NumToWrite)
{
		if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*512)))return 0;//非法地址
		HAL_FLASH_Unlock();					//解锁
		
		uint16_t i;
		uint16_t num=0;
		if(NumToWrite%2==0) num=	NumToWrite/2;
			else num=	(NumToWrite+1)/2;
		for(i=0;i<num;i++)
		{
			HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,WriteAddr,pBuffer[i]);
			WriteAddr+=2;		//地址增加2.
		} 
			HAL_FLASH_Lock();		//上锁
			return 1;
}


/*
APP跳转函数
APP_ADDR:跳转地址
*/
void System_jumpApp(uint32_t APP_ADDR)
{
 if(((*(volatile uint32_t*)APP_ADDR)&0x2FFE0000)==0x20000000)//是否有数据写入
  {
	void (*SysJumpapp)(void); //声明一个函数指针
	__disable_irq();//关闭所有中断
	HAL_SuspendTick();//关闭定时器
	HAL_RCC_DeInit();//设置默认时钟HSI
	for(uint32_t i=0;i<8;i++)//清除所有中断位
	{
	 NVIC->ICER[i]=0xFFFFFFFF;
	 NVIC->ICPR[i]=0xFFFFFFFF;
	}
	SCB->VTOR=FLASH_BASE|(APP_ADDR-STM32_FLASH_BASE);//0x5000;//重定向中断向量表
	
	HAL_ResumeTick();//开启滴答定时器
	__enable_irq();//开启全局中断
	SysJumpapp = (void (*)(void)) (*((uint32_t *) (APP_ADDR + 4)));//中断复位地址
	__set_MSP(*(uint32_t *)APP_ADDR);//设置函数主堆栈指针
	__set_CONTROL(0);//如果使用了RTOS工程,需要用到这条语句,设置为特权级模式,使用MSP指针
	SysJumpapp();//跳转
	}
	
}


//boot检测函数 ,检测是否需要升级,
void Bootloader(void)
  {
	static	uint32_t sum=1,len=0;
		overtime_check();//超时检测
		if(Rx_flag)
		{
			
			Rx_flag=0;
		if(strncmp((const char *)Rx_Order,(const char *)upcmd,strlen((const char *)upcmd))==0) //接收到升级命令
			{		
				HAL_TIM_Base_Stop_IT(&htim3);//关闭定时器
				printf("正在擦除flash..\r\n");
			  uint8_t i=0;
			  HAL_FLASH_Unlock();					//解锁
				while(1)
				{
					if((FLASH_SAVE_ADDR+i*2048>=(STM32_FLASH_BASE+1024*512)))break;//非法地址
					FLASH_PageErase(FLASH_SAVE_ADDR+i*2048);	//擦除这个扇区
					FLASH_WaitForLastOperation(500);          //等待上次操作完成
						CLEAR_BIT(FLASH->CR, FLASH_CR_PER);				//清除CR寄存器的PER位,此操作应该在FLASH_PageErase()中完成!
					i++;
				}
				HAL_FLASH_Lock();		//上锁
				Upgrade_flag=1;//设置升级命令标志为				
				printf("请发送APP文件..\r\n");
			}
			else printf("%s",Rx_Order);
			memset(Rx_Order,0,200);//清除接收到的数据,防止影响下一次数据接收
		}
		
		if(Tx_flag) //往Flash中写入数据
		{
			HAL_Delay(5);
			Tx_flag=0;
			if(Tx_Flash_len!=0)
			{
				if(BootFlash(flashaddress,(uint16_t *)Rx_APP[Tx_Flash_array],Tx_Flash_len))
				{
  				printf("写入数据:%d*%d\r\n",sum++,Tx_Flash_len);
					memset(Rx_APP[Tx_Flash_array],0,1024*2);//清除数据
					flashaddress+=Tx_Flash_len;
					if(Tx_Flash_len<2048)Jump_flag=1,len=Tx_Flash_len; //跳转标志
					Tx_Flash_len=0;
				}
				else 
				{
					printf("写入失败\r\n");
					memset(Rx_APP[Tx_Flash_array],0,1024*2);//清除数据
				}
			}
		}
		if(Jump_flag)
		{
			Jump_flag=0;
			printf("共写入数据:%d*2048+%d=%d\r\n",rx_sum,len,rx_sum*2048+len);
			printf("跳转至APP\r\n\r\n\r\n\r\n");
			System_jumpApp(FLASH_SAVE_ADDR);//FLASH_SAVE_ADDRSTM32_FLASH_BASE
			
		}
  }

///*以下为中断回调函数
//串口接收中断,用于接收串口函数
uint32_t rx_re=0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
	
  if(huart->Instance == USART1){
		
		rx_time = HAL_GetTick();//获取实时时间
		
		if(Upgrade_flag==0)
		{
			Rx_Order[Rx_Order_len++]=temp;
			if(Rx_Order[Rx_Order_len-1]==0x0A&&Rx_Order[Rx_Order_len-2]==0x0D)
			{
				Rx_Order_len=0;
				Rx_flag=1;
			}
		}
		else
		{		
			Rx_APP[Rx_APP_array][Rx_APP_len++]=temp;
			rx_re++;			
			if(Rx_APP_len==1024*2) //接收到1024个数据
			{			
				Tx_Flash_array=Rx_APP_array;
				Tx_Flash_len=Rx_APP_len;
				Rx_APP_len=0;
				
				if(Rx_APP_array==0)Rx_APP_array=1; //更换数组
					else Rx_APP_array=0;
				Tx_flag=1;//标志可传输
				rx_sum++;
				Upgrade_flag=3;
			}
		}
			HAL_UART_Receive_IT(&huart1,&temp,1); //再次开启接收中断
  }
}


//超时检测,串口超过1s未接收到新数据默认已接收完成

void overtime_check(void)
{
	if(HAL_GetTick()!=rx_time)
		if((HAL_GetTick()-rx_time)>500 && Upgrade_flag==3) //超时500ms,标志传输结束
	{
				Tx_Flash_array=Rx_APP_array;
				Tx_Flash_len=Rx_APP_len;
				Rx_APP_len=0;
				
				if(Rx_APP_array==0)Rx_APP_array=1; //更换数组
					else Rx_APP_array=0;
				
				Tx_flag=1;//标志可传输
		rx_time=HAL_GetTick();		
	}
	
}

//定时器中断,20S倒计时,归0跳转至APP
int time=countdown*2;//计算倒计时
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
    if(htim == &htim3){
			
			time --;
			if(time==0) Jump_flag=1;
			if(time%2)printf("倒计时:%d",time/2);
			
        HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_5);   //LED1状态每500s翻转一次
	    	
    }
}

在main函数调用一下两个函数即可。

#include "Bootloader.h"
int main(void)
{
	printf("STM32 Bootloader Test...\r\n");
	BootInit(); //boot初始化,打开定时器中断和串口中断
  while (1)
  {
		Bootloader(); //调用boot检测函数,检测是否需要升级及升级处理

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

 关于APP的起始地址计算可以在程序编译后查看程序的大小来进行定义,如以上程序编译后的大小为:9014,换算成16进制为:0x2336,即该程序占用了FLASH中的0x08000000 - 0x08002336,按照精密计算应该再预留20%的内存,我这里为了以后升级预留了更多的内存,因此将APP的起始地址设定为0x08005000。

5、下载验证

编译无误后下载到板子上,串口发送非升级命令会返回接收到的数据,当发送升级命令后进入升级程序,此时往串口发送的数据都会存在flash中。

可以看到串口发送的数据都存在flash中。可以测试flash中扇区与扇区之间的数据是否连接正常来验证程序是否有误。

 6、程序讲解

(1)、串口接收这里我定义了个二维数组用于接收APP数据。每维数据2048个元素,对应stm32的一个扇区的大小。当串口接收到2048个数据时即第一层数组存满数据,即刻换成第二层数组来接收数据,同时第一层数据往flash中写入数据,当第二层数组存满又换回第一层数组来接收数据,如此往复即可接收任意长的APP长度,当文件末尾数据不足2048时会有个500ms的超时判断,即500ms内串口没接收到新数据时则判断为已经接收完成,即可跳转到APP程序执行。

uint8_t Rx_APP[2][2048]; //接收APP数据

(2)、在往flash中写入数据时踩了一个坑,就是flash写入数据的流程是解锁->擦除->写入->上锁,但是在擦除的过程中会导致串口中断不触发,就导致了数据接收遗漏了。因此我将擦除步骤给提前了,在接收到升级命令后就擦除全系flash,等待全部擦除后再进行接收APP数据。

/*
Flash写入函数
WriteAddr:写入地址
pBuffer:写入数据
NumToWrite:写入长度
*/
uint8_t BootFlash(uint32_t WriteAddr,uint16_t *pBuffer,uint16_t NumToWrite)
{
		if(WriteAddr<STM32_FLASH_BASE||(WriteAddr>=(STM32_FLASH_BASE+1024*512)))return 0;//非法地址
		HAL_FLASH_Unlock();					//解锁
		
		uint16_t i;
		uint16_t num=0;
		if(NumToWrite%2==0) num=	NumToWrite/2;
			else num=	(NumToWrite+1)/2;
		for(i=0;i<num;i++)
		{
			HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD,WriteAddr,pBuffer[i]);
			WriteAddr+=2;		//地址增加2.
		} 
			HAL_FLASH_Lock();		//上锁
			return 1;
}

(3)、跳转函数void System_jumpApp(uint32_t APP_ADDR),请不要在中断中调用,否则会影响中断向量的偏移,导致程序无法正常运行。

三、APP程序

由于boot程序已经对中断向量进行了偏移,所以APP程序只需要更改编译地址即可,如下图将IROM1改成0x8005000(如果你的APP程序内存比较大,还需要更改ROM和RAM的SIZE大小,以防止爆内存)

如果boot没有偏移中断向量,需要在main函数中进行中断向量偏移

SCB->VTOR=FLASH_BASE|(0x80005000-0x8000000);//重定向中断向量表

这里的程序采用的是定时器实验的程序,有需要的请看STM32CubeMX学习笔记5-定时器中断-CSDN博客

这里的APP程序需要生成BIN文件,可以在keil中添加以下命令,名字可以自由更换

 fromelf.exe --bin -o "STM32F103VET_Bootloader.bin" "#L"

编译后生成bin文件! 

四、程序验证

将单片机复位后,利用串口助手发送升级命令“#sic,up APP\r\n”,注意需要加回车换行!当回复“请发送APP文件时,即可将APP的bin文件发送到单片机了

可以看到bin文件大小为7804,发送完毕后会返回写入的总数量,数量一致即成功写入了,然后程序跳转至APP程序。 

五、参考文献 

[笔记]STM32基于HAL编写Bootloader+App程序结构_hal库开发boot和app-CSDN博客

STM32具备升级功能的bootloader及APP/IAP的实现_cubemx 做boot和app-CSDN博客

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐