一、分区
首先要对flash进行分区,模拟固件升级的场景。一般来说分为bootloader区、A区、B区,我又加了一个更新标志位的区。
我使用的是stm32f103c8t6,flash是64k,sram是20k。查询数据手册可以看到flash和sram的起始地址分别为:0x08000000、0x0x20000000。(该图flash为128k,我的是64k)
明确起始地址后,还要知道容量是怎么转换为十六进制的。如flash大小是64k个字节,如何转换成十六进制呢?单片机的地址是按字节编址的,也就是说flash大小为64*1024=65536字节,十进制的65536转换成十六进制就是0x10000,因此,flash的大小就是0x10000,即从0x08000000-0x0800FFFF。
二、bootloader区
这个区主要就是用来进行上电检测的,读取更新标志区的标志位,来判断程序从A区还是B区启动。我将这个区大小设置为19k,随便设置的,可自行调整。转换成十六进制是0x4C00。需要注意的是每个区的sram均使用全部容量即可,即20k。下图是bootloader程序的keil对应设置。
除此之外还要注意,keil的下载设置,要设置为部分擦除,防止下载程序时影响其他分区,如下图。
下面为bootloader区的核心代码,主要就是初始化串口1、跳转程序。
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "flash.h"
#define A 1U
#define B 0U
/*
bootloader地址: 0x8000000 - 0x8004BFF
更新标志位地址: 0x8004C00 - 0x8004FFF
A区地址: 0x8005000 - 0x800A7FF
B区地址: 0x800A800 - 0x800FFFF
*/
void SystemClock_Config(void);
typedef __IO uint32_t vu32;
//声明指针函数
void (*jump2app)();
//跳转到应用程序段
//appxaddr:用户代码起始地址.
void iap_load_app(uint32_t appxaddr)
{
if(((*(vu32*)appxaddr)&0x2FFE0000)==0x20000000) //检查栈顶地址是否合法.
{
jump2app=(void(*)())*(vu32*)(appxaddr+4); //用户代码区第二个字为复位中断地址
__set_MSP(*(vu32*)appxaddr); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
__disable_irq(); //关闭中断,防止跳转过程中产生中断
jump2app(); //跳转到APP.
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
//输出提示信息
printf("This is bootloader\r\n");
HAL_Delay(500);
//跳转
if (flag_flash_read() == A)
{
printf("Gonna to jump to A!\r\n");
HAL_Delay(500);
iap_load_app(0x8005000);
}
else if (flag_flash_read() == B)
{
printf("Gonna to jump to B!\r\n");
HAL_Delay(500);
iap_load_app(0x800A800);
}
else
{
printf("jump error\r\n");
}
}
三、更新标志区
这个区主要就是用来各个区直接通信的,只存放了一个uint8_t的标志位。我将这个区大小设置为1k,随便设置的,可自行调整。这样bootloader+更新标志区一共是20k,占用0x5000大小的flash。
四、A区
这个区主要就是用来存放运行的代码区。我将这个区大小设置为22k,随便设置的,可自行调整。也就是占用0x5800大小的flash。即从0x8005000到0x800A7FF。下图是A区程序的keil对应设置。
下面为A区的核心代码,主要就是初始化串口1、使用串口1的接收中断来模拟更新检测。
#include "main.h"
#include "usart.h"
#include "gpio.h"
#include "flash.h"
#define A 1U
#define B 0U
extern uint8_t etr_uart1_flag; //串口1中定义的,初始为0,接收中断中修改为1
/*
bootloader地址: 0x8000000 - 0x8004BFF
更新标志位地址: 0x8004C00 - 0x8004FFF
A区地址: 0x8005000 - 0x800A7FF
B区地址: 0x800A800 - 0x800FFFF
*/
void SystemClock_Config(void);
//第一个参数是flash基地址0x08000000,第二个是偏移量
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset)
{
SCB->VTOR = NVIC_VectTab | Offset;
}
int main(void)
{
__enable_irq(); //开启中断
HAL_Init();
SystemClock_Config();
NVIC_SetVectorTable(0x08000000, 0x5000); //设置中断偏移
MX_GPIO_Init();
MX_USART1_UART_Init();
//输出提示信息
printf("Here is the app_a region\r\n");
//模拟固件更新
//实际场景中,下面代码应该是固件接收中断标志更改后,才执行
//这里采用外部中断,b5引脚接按键,来模拟固件接收中断
while (1)
{
if (etr_uart1_flag) //一旦检测到更新,就开始更新固件
{
printf("检查到新版本,下载至B区中...\r\n");
HAL_Delay(5000);
printf("固件下载完成,校验中...\r\n");
HAL_Delay(5000);
printf("固件校验完成,准备安装...\r\n");
flag_flash_write(B);
HAL_Delay(2000);
printf("固件安装完成,geng请重启...\r\n");
etr_uart1_flag = 0;
}
}
}
下面是代码中的串口1的配置代码。
#include "usart.h"
uint8_t etr_uart1_flag = 0;
UART_HandleTypeDef huart1;
void MX_USART1_UART_Init(void)
{
huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
GPIO_InitTypeDef GPIO_InitStruct;
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
//tx
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
//rx
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_INPUT;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(USART1_IRQn, 10, 1);
HAL_NVIC_EnableIRQ(USART1_IRQn);
}
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
return (ch);
}
/* USER CODE END 1 */
int fgetc(FILE *f)
{
int ch;
HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 1000);
return (ch);
}
void USART1_IRQHandler(void)
{
uint8_t ch = 0;
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
{
ch = (uint16_t) READ_REG(huart1.Instance->DR);
if (ch == 'b') //输入b即可在A区运行时,出发更新操作
etr_uart1_flag = 1;
}
}
下面是flah的读写函数,因为就用了一页来写,stm32f103c8中flash的64k分别划分为64页,每页1k。
#include "flash.h"
#define FLAG_FLASH_ADDR 0x8004C00 //更新标志区的起始地址
void flag_flash_write(uint8_t data)
{
FLASH_EraseInitTypeDef Flash_EraseInitStruct;
uint32_t pageError = 0;
//解锁
HAL_FLASH_Unlock();
Flash_EraseInitStruct.PageAddress = FLAG_FLASH_ADDR;
Flash_EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
Flash_EraseInitStruct.NbPages = 1;
Flash_EraseInitStruct.Banks = FLASH_BANK_1;
//擦除第20个页,也就是我们存放标志位的页
if (HAL_FLASHEx_Erase(&Flash_EraseInitStruct, &pageError) != HAL_OK)
{
printf("erase error\n");
}
FLASH_WaitForLastOperation(50000);
//写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, FLAG_FLASH_ADDR, data);
//锁定
HAL_FLASH_Lock();
}
uint8_t flag_flash_read(void)
{
return *(uint32_t*)FLAG_FLASH_ADDR;
}
五、B区
这个区主要就是用来存放另一个运行的代码区。我将这个区大小也设置为22k,随便设置的,可自行调整。也就是占用0x5800大小的flash。即从0x800A800到0x800FFFF。下图是B区程序的keil对应设置。
至于B区代码,和A区基本一致,唯一要改的就是,B区的主函数中,更新程序的部分这句flag_flash_write(B);改为:flag_flash_write(A);
六、运行结果展示
注意!!!为了防止第一次程序打印jump error
注意!!!为了防止第一次程序打印jump error
注意!!!为了防止第一次程序打印jump error
因为第一次上电时,更新标志区的值可能不是0也不是1,这个时候就会bootloader就会打印jump error。我们可以先把更新标志区写个A进去。具体做法是,在bootloader的main函数中调用flag_flash_writer(A),然后把这个程序烧录进去,上电就会把更新标志区初始化为1,即默认跳转A区。之后,再注释或删掉flag_flash_writer(A),然后再烧录一遍bootloader程序。之后就可以正常运行了。
下载好对应程序后,连接串口助手,复位单片机,输出如下信息:
可以看到程序默认从A区启动,此时输入'b',程序即模拟检测到固件更新,待提示如下信息后,复位单片机,即重启后便跳转至b区启动。
此时接着再输入'a',程序即模拟再次检测到固件更新,待提示如下信息后,复位单片机,即重启后又跳转至a区启动。循环往复,模拟固件更新。
至此,整个工作完成,也算是对自己近几天学习的记录与总结。
其中代码的细节完全靠自己的想法设计的,所以可能存在很多漏洞,也希望大家多多指点