-
一、IAP的基本概念
IAP,全名为in applacation programming,即在应用编程。
也就是在应用程序中升级。好处就太多了,比如远程在线升级,不用人到现场拆开,用烧写器连接升级。
实现IAP技术的核心是一段预先烧写在单片机内部的IAP程序。这段程序主要负责与外部的上位机软件进行握手同步,然后将通过外设通信接口将来自于上位机软件的程序数据接收后写入单片机内部指定的闪存区域,然后再跳转执行新写入的程序,最终就达到了程序更新的目的。
原理和ISP有点类似。简单的讲:
ISP是ST官方写的一段bootloader代码,通过UART1烧录程序。
此时BOOT0=1;
IAP需要我们自己写一段bootloader代码,支持串口,网络,can等实现应用程序升级。这次就选择用串口实现IAP应用。
在STM32微控制器上实现IAP程序之前首先要回顾一下STM32的内部闪存组织架构和其启动过程。STM32的内部闪存地址起始于0x8000000,一般情况下,程序文件就从此地址开始写入。此外STM32是基于Cortex-M3内核的微控制器,其内部通过一张“中断向量表”来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成启动。而这张“中断向量表”的起始地址是0x8000004,当中断来临,STM32的内部硬件机制亦会自动将PC指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断服务程序。最后还需要知道关键的一点,通过修改STM32工程的链接脚本可以修改程序文件写入闪存的起始地址。
对图1解读如下:
1、 STM32复位后,会从地址为0x8000004处取出复位中断向量的地址,并跳转执行复位中断服务程序,如图1中标号○1所示。
2、 复位中断服务程序执行的最终结果是跳转至C程序的main函数,如图1中标号○2所示,而main函数应该是一个死循环,是一个永不返回的函数。
3、 在main函数执行的过程中,发生了一个中断请求,此时STM32的硬件机制会将PC指针强制指回中断向量表处,如图1中标号○3所示。
4、 根据中断源进入相应的中断服务程序,如图1中标号○5所示。
5、 中断服务程序执行完毕后,程序再度返回至main函数中执行,如图1中标号○6所示。
打开一个stm32程序的bin文件。
如图,前面4字节是栈顶地址:0x20000648
接着4字节是复位中断函数地址0x080001A1。执行完复位中断函数后,回到主函数。
若在STM32中加入了IAP程序,则情况会如图2所示。
对图2的解读如下:
1、 STM32复位后,从地址为0x8000004处取出复位中断向量的地址,并跳转执行复位中断服务程序,随后跳转至IAP程序的main函数,如图2中标号○1、○2所示。这个过程和图1相应部分是一致的。
2、 执行完IAP过程后(STM32内部多出了新写入的程序,图2中以灰色底纹方格表示,地址始于0x8000004+N+M)跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main函数,其过程如图2的标号○3所示。新程序的main函数应该也具有永不返回的特性。同时应该注意在STM32的内部存储空间在不同的位置上出现了2个中断向量表。
3、 在新程序main函数执行的过程中,一个中断请求来临,PC指针仍会回转至地址为0x8000004中断向量表处,而并不是新程序的中断向量表,如图2中标号○5所示。注意到这是由STM32的硬件机制决定的。
4、 根据中断源跳转至对应的中断服务,如图2中标号○6所示。注意此时是跳转至了新程序的中断服务程序中。
5、 中断服务执行完毕后,返回main函数。如图2中标号○8所示。
从上述两个过程的分析可以得知,对将使用IAP过程写入的程序要满足2个要求:
1、新程序必须从IAP程序之后的某个偏移量为x的地址开始;
2、必须将新程序的中断向量表相应的移动,移动的偏移量为x;
我们打开一个APP程序的bin文件
打开APP.bin文件,能够看到前面4字节是栈顶地址:0x20005460,接着4字节是复位中断函数入口地址:0x080081A1。
-
二、STM32F405RGT6的FLASH
-
FLASH分为ROM和RAM。
ROM大小是1024KB。分为12个扇区。扇区大小不完全一样。
RAM大小介绍说192(112+16+64)KB其中192K的内存包括64K的CCM(仅CPU可访问)、112K的SRAM1(主RAM)和16K的SRAM2(外设使用)。
CCM内存的基地址是0x10000000 用Keil看不到
SRAM1内存的基地址是0x20000000大小0x1C000(112K)
SRAM2内存的基地址是0x2001C000,大小是0x4000 (16K)
我们把ROM空间划分为两部分。分别存放Bootloader程序和APPlication程序。下文简称为boot和app。
由于IAP的功能需要在线升级程序,所以我们最核心的功能需求就是改写程序存储区中的内容。
MCU复位后,先从0x08000000地址开始运行boot程序;
如果需要更新app程序,则由boot程序获取app程序的烧写文件(二进制bin文件格式),将其写入到app对应的地址中;
如果需要运行app程序,则从boot程序中跳转到app对应的地址执行。
我们对ROM划分区域,分为三部分,前面两部分用于存放boot和app编译生成的文件。
预留多大空间合适呢?
一般boot预留16KB足够用。自己的boot编译生成的文件大小
Program Size: Code=8324 RO-data=528 RW-data=32 ZI-data=1576
code : 代码
RO-data :指的是程序中的指令和常量
RW-data :程序中已经初始化的变量
ZI-data :程序中未初始化的便利那个
烧录文件的大小=code + RO-data + RW-data=8324 +528 + 32=8884个字节
烧录文件大小指的是bin文件而非hex文件,bin的才是下载的二进制文件。
芯片需要的RAM大小=RW-data+ZI-data=32+1576=1608个字节
三、BOOT程序
1、产品功能
上电后,通过uart1接收升级指令,启动升级,通过uart1接收app编译生成的bin文件。接收完成后,运行app程序。
2、接收指令和判断指令
HAL_UART_Receive(&huart1,datatemp,256,1000);//接收指令
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
If(strstr((const char *)datatemp,erasure_CMD) !=NULL) //判断指令
函数strstr是用于在一个字符串A中,查找第一次出现的另外一个字符串B的位置,查找到了,返回B在A中的起始位置的指针;没有找到,就返回NULL。
temp=HAL_UART_Receive(&huart1,datatemp,256,30*1000);//接收bin文件的256个字节到缓冲区。
For(i=0;i<64;i++)
{
Data_32=*(uisigned int *)(&datatemp[i<<2]) //把256个字节 修改成64个32位数据读取
}
3、bin文件生成指令。
①$K\ARM\ARMCC\bin\fromelf.exe --bin --output=Bin\@L.bin !L 会在MDK目录下生成一个文件夹,里面是.bin格式文件
②fromelf.exe --bin -o "$L@L.bin" "#L 会在.hex同目录下生成一个.bin 格式文件
4、FLASH写入
FLASH写入的时候,需要先解锁,接着擦除,然后写入,最后给FLASH上锁。
所以函数名称是flash_eraser_write()
解锁
HAL_FLASH_Unlock( );
擦除
HAL_FLASHEx_Erase(&EraseInitStruct,&SectorError);
FLASH_EraseInitTypeDef EraseInitStruct;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS ;//擦除模式按照扇区
EraseInitStruct.Sector = FLASH_SECTOR_2;//APP占用的起始地址所在的扇区
EraseInitStruct.NbSectors=1; //APP占用的扇区数
EraseInitStruct.VoltageRange= FLASH_VOLTAGE_RANGE_3; //供电电压
写入
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,Address,DATA32[i++]);
上锁
HAL_FLASH_Lock( );
STM32F103的Flash擦除的最小单位是页,也就是每次一擦除,都会把擦除地址所在页的整页数据擦掉;
STM32F407的Flash擦除的最小单位是扇区,也就是每次一擦除,都会把擦除地址所在扇区的扇区数据擦掉;
5、栈顶地址判断
if (((*(volatile u32*)ApplicationAddress) & 0x2FFE0000 ) == 0x20000000)分析:
ApplicationAddress存放的是用户程序Flash的首地址,(*(volatile u32*)ApplicationAddress)
将 addr 强制转换为 volatile uint32_t 指针,然后取该指针所指向的地址的值,即得到了 addr地址的值。意思是取用户程序首地址里面的数据,这个数据就是用户代码的堆栈地址,堆栈地址指向RAM,而RAM的起始地址是0x20000000,因此上面的判断语句执行:判断用户代码的堆栈地址是否落在:0x20000000~0x2001ffff区间中,这个区间的大小为128K,笔者查阅STM32F4各型号的RAM大小,目前RAM最大的容量可以做到192K+4K,时钟频率为168MHZ。
6、APP跳转
JumpAddress = *(__IOuint32_t*) (APPLICATION_ADDRESS +4);// ②
JumpToApplication = (pFunction) JumpAddress;//③
/* Initialize user application's Stack Pointer */
__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);// ④
JumpToApplication(); // ⑤
②程序跳转地址的确认,前面已经说过0x08008004处的4个字节存放的是复位函数的入口地址,该句的意思为获得ApplicationAddress + 4地址处的数据,即为获得新的复位函数入口地址。
③令Jump_To_Application这个函数指针指向复位函数入口地址。
1.这句的意思是将上一句取得的中断函数地址转为函数指针。void (*pFunction)(void);是声明了一个函数指针。
2.此时,JumpToApplication指向了复位中断函数所在的地址。
④堆栈的初始化,调用__set_MSP重新设定栈顶代地址,把栈顶地址设置为用户代码指向的栈顶地址。
⑤跳转到新的复位函数。设置PC指针为复位地址。
-
7、stm32cubemx配置
配置RCC
配置SYS烧写
配置串口
-
8、Keil配置
选用微库,
配置IROM1和IROM2
-
9、代码
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "string.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
#define NVIC_VectTab_RAM_Start((uint32_t)0x20000000) //RAM起始地址
#define NVIC_VectTab_RAM_End((uint32_t)0x20020000) //RAM结束地址,大小为128K,根据自己的实际芯片大小修改
#define NVIC_VectTab_FLASH((uint32_t)0x08000000) //Flash起始地址
#define BOOT_SIZE 0x8000 //Boot大小,32KB,占用前面两个扇区(其实上只需要占用第一个扇区16KB即可)
#define ApplicationAddress (NVIC_VectTab_FLASH + BOOT_SIZE) //APP的起始地址
#define Application_SIZE 0x4000 //APP大小为16KB。占用第三个扇区
#define FLASH_USER_END_ADDR (ApplicationAddress+Application_SIZE) //APP的结束地址
#define UPDATE_CMD "update" //升级擦除指令
typedef void (*iapfun)(void);
void SystemClock_Config(void);
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
//uint32_t FirstSector =1,NbofSectors=1;
uint32_t SectorError =0 ;//擦除错误的扇区地址,擦除成功时候该值为0xffffffff
iapfun jump2app; //用函数指针指向复位函数入口地址
// unsigned int count2 = 0;
unsigned char datatemp[256] = {0};//用于存放串口接收缓冲区,存放256B数据。
unsigned char boot_flag = 0;//擦除完成标志
unsigned char time_out_flag = 0;//接收串口数据超时标志
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/* USER CODE END 0 */
/**
* @briefThe application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
unsigned char i;
printf("boot start\r\n");
printf("input \"update\"\t to erasure user flash, or wait 10s to start user app\r\n");//提示输出指令“update”,10s内没接收到指令,开始启动APP
for(i = 0; i<10; i++)
{
//上电后每秒阻塞接收升级指令,连续10秒未收到则跳转App,10秒内收到则接收App数据并写入
HAL_UART_Receive(&huart1, datatemp, 256, 1000);
if(strstr((const char *)datatemp, UPDATE_CMD) != NULL) //查询接收的数据是否包含指令。
{
// 擦除App区域
FLASH_EraseInitTypeDef EraseInitStruct;
EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS ;//擦除模式按照扇区
EraseInitStruct.Sector = FLASH_SECTOR_2;//APP占用的起始地址所在的扇区
EraseInitStruct.NbSectors=1; //APP占用的扇区数
EraseInitStruct.VoltageRange= FLASH_VOLTAGE_RANGE_3; //供电电压
HAL_FLASH_Unlock();//解锁
if(HAL_FLASHEx_Erase(&EraseInitStruct, &SectorError) != HAL_OK)//如果擦除不成功,上锁
{
HAL_FLASH_Lock();
printf("Erase fail at:0x%x\n\r",SectorError);//返回擦除失败的扇区位置
return 0;
}
printf("当SectorError=0x%X\t;表示擦除成功\r\n",SectorError);
boot_flag = 1;//擦除成功,开始升级
printf("Erase OK\n\r");
break;
}
}
if(boot_flag == 1)
{
HAL_StatusTypeDef temp;//状态标志符
unsigned int Address;//地址变量
unsigned int data_32;//按照字写入,也就是4B
unsigned char j = 0;//
printf("ready to receive bin, please send in 30s\n\r");//提示在30s内发送2进制APP文件
Address = ApplicationAddress; //指向预留存放APP的FLASH地址
temp = HAL_UART_Receive(&huart1, datatemp, 256, 30*1000); //在30s内通过串口1发送2进制APP文件
if(temp == HAL_TIMEOUT)
{
//阻塞30S,未收到App数据则退出
printf("time out, end wait to receive bin\n\r");
return 0;
}
else
if(temp == HAL_OK)
{
//收到则循环接收,每秒阻塞接收256字节,并写入Flash
while(1)
{
unsigned char i;
HAL_FLASH_Unlock();//解锁
for(i=0; i<64; i++)//把接收的256字节,按照每次写入4字节,写入64次
{
data_32 = *(unsigned int *)(&datatemp[i<<2]);//很巧妙,字节转换成字
if(Address < FLASH_USER_END_ADDR)//写入地址未超出预留区域
{
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, Address,data_32)==HAL_OK)//写入函数,按照字格式写入
{
Address = Address + 4;
}
else
{
HAL_FLASH_Lock();
printf("write fail at: 0x%x\n\r", Address);
return 0;
}
}
else
printf("写入的升级程序超出预留APP区域\r\n");
}
HAL_FLASH_Lock();
printf("write 256 btye OK: 0x%x\t%d\n\r",Address,j++);
//循环调用串口接收函数。最后一包数据不足256字节时会接收超时,防止app数据不完整,不足256字节数据接收完以后处理一次,下一次接收超时认为接收完成,跳转App
temp = HAL_UART_Receive(&huart1, datatemp, 256, 2*1000);
if(temp == HAL_TIMEOUT)
{
time_out_flag++;
if(time_out_flag == 2)
{
printf("End write OK\n\r");
goto START_APP;
}
}
}
}
}
else if(boot_flag == 0) //没有收到升级命令
{
START_APP:
printf("start user app\n\r");
HAL_Delay(10);
/*
判断App的栈顶指针是否合法(即是否有App)。
ApplicationAddress为App在flash中的地址,(*(volatile u32*)ApplicationAddress)的意思
取用户程序首地址里面的数据,这个数据就是用户代码的堆栈地址,堆栈地址指向RAM,而RAM的起始地址是0x20000000,
因此上面的判断语句执行:判断用户代码的堆栈地址是否落在:0x20000000~0x2001ffff区间中
这里的目的是判断App的栈顶指针是否在0x20000000到0x2001FFFF之间,在的话就认为有App,不在就没有
*/
printf("ApplicationAddress:%0x\r\n", (*(unsigned int *)ApplicationAddress));
if(((*(unsigned int *)ApplicationAddress)>= NVIC_VectTab_RAM_Start) &&
((*(unsigned int *)ApplicationAddress)<= NVIC_VectTab_RAM_End))
{
// // disable irq, if use this, must enable irq at app
// //__disable_irq();
//
__set_MSP(*(unsigned int *)ApplicationAddress); //调用__set_MSP重新设定栈顶代地址,把栈顶地址设置为APP代码指向的栈顶地址。
jump2app=(iapfun)*(unsigned int *)(ApplicationAddress+4);//获得新的复位函数入口地址,让jump2这个函数指针指向复位函数入口地址。
jump2app(); //跳转到新的复位函数
}
else
{
printf("no user app\n\r");
return 0;
}
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
四、APP程序
-
1、偏移地址
设置APP在FLASH中的起始地址和设置中断向量偏移地址
#define NVIC_VectTab_FLASH ((uint32_t)0x08000000) //Flash起始地址
#define BOOT_SIZE 0x8000 //Boot大小
SCB->VTOR = NVIC_VectTab_FLASH | BOOT_SIZE; /* 设置中断向量偏移地址 */
-
2、Keil配置
-
3、程序
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2024 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#define NVIC_VectTab_FLASH((uint32_t)0x08000000) //Flash起始地址
#define BOOT_SIZE 0x8000 //Boot大小
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/* USER CODE END 0 */
/**
* @briefThe application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
uint32_t Count = 0;
/* 设置中断向量偏移地址 */
SCB->VTOR = NVIC_VectTab_FLASH | BOOT_SIZE;
printf("\n\r app start\n\r");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_Delay(10);
if(Count%100 == 0)
{
printf("this is app!\t%d\n\r",Count/100);
}
Count++;
}
/* USER CODE END 3 */
}
五、运行结果
-
1、烧写boot程序
用sscom打开串口。输入指令,注意只要输入的内容包含update即可匹配
-
2、发送设置
-
3、发送bin文件
-
4、app烧录完成,并运行