STM32CubeMX学习笔记21--- FLASH应用(Bootloader+APP)
在16章我们学习了FLASH的读写操作,FLASH内的数据是断电可保存的,而且stm32的运行程序也是存放在内部FLASH当中,那么可以编写一段程序A,将另一段程序B写入FLASH中并引导单片机运行程序B。在专业的角度上程序A称为引导/启动程序(Bootloader),简称Boot。程序B叫应用程序,简称APP。
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程序。
五、参考文献
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)