52. IAP串口升级
一、IAP简介
IAP,即在应用编程,通俗地说法就是“程序升级”。产品阶段设计完成后,在脱离实验室的调试环境下,如果想对产品做功能升级或 BUG 修复会十分麻烦,如果硬件支持,在出厂时预留一套升级固件的流程,就可以很好解决这个问题,IAP 技术就是为此而生的。
IAP(In Application Programming)即在应用编程。在讲解 STM32 的启动模式时我们已经知道 STM32 可以通过设置 MSP 的方式从不同的地址启动:包括 Flash 地址、RAM 地址等,在默认方式下,我们的嵌入式程序是以连续二进制的方式烧录到 STM32 的可寻址 Flash 区域上的。如果我们用的 Flash 容量大到可以存储两个或多个的完整程序,在保证每个程序完整的情况下,上电后的程序通过修改 MSP 的方式,就可以保证一个单片机上有多个有功能差异的嵌入式软件,这就是我们要讲解的 IAP 的设计思路。
二、IAP原理
IAP 是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。通常实现 IAP 功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首先是第一个项目代码开始运行,它做如下操作:
- 检查是否需要对第二部分代码进行更新。
- 如果不需要更新则转到步骤 4。
- 执行更新操作。
- 跳转到第二部分代码执行。
第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码可以使用第一部分代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP 代码更新。
我们将第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP 程序,他们存放在 STM32F407 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就是 APP 程序。这样我们就是要实现 2 个程序:Bootloader 和 APP。
三、程序执行流程
STM32 的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行。STM32 的程序正常执行流程如下:
- 跳转到复位中断服务函数。
- 跳转到 main() 函数。
- 发生中断时,会强制跳转到中断向量表。
- 根据中断源,跳转到对应的中断服务函数。
- 指定中断服务函数后,回到 main() 函数原来的位置继续执行。
当加入 IAP 程序之后,程序运行流程如下所示:
- 跳转到复位中断服务函数。
- 跳转到 IAP 程序的 main() 函数。
- 执行 IAP 过程,跳转到 APP 中断向量表。
- 跳转到 APP 的 main() 函数。
- 发生中断时,会强制跳转到 IAP 的中断向量表(地址为 0x08000000 的中断向量表)。
- 根据中断向量表的偏移量,跳转到 APP 对应的中断服务函数。
- 执行中断服务函数后,回到 APP 的 main() 函数原来的位置继续执行。
通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求:
- 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始。
- 必须将新程序的中断向量表相应的移动,移动的偏移量为 x。
四、APP程序生成步骤
4.1、设置APP程序的起始地址和存储空间大小。
ST 公司规定,用户的代码存放在 Block0 块的 0x0800 0000 ~ 0x080F FFFF 总共 1MB 空间大小的存储范围内。
在 FLASH 文件中,我们也可以看到,默认情况下程序的起始地址(Start)一般为 0x0800 0000,大小(Size)为 0x100000,即从 0x0800 0000 开始的 1024K(1M) 空间为我们的程序存储区。
这里,我们设置起始地址(Start)为 0x0801 0000,即偏移量为 0x10000(64K 字节,即留给 BootLoader 的空间),因而,留给 APP 用的 FLASH 空间(Size)为 0x100000-0x10000=0xF0000(960KB)。设置好 Start 和 Size,就完成 APP 程序的起始地址设置。
/* Specify the memory areas */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K
CCMRAM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 960K
}
设置 APP 起始程序要注意:(1)、APP 要在 BootLoader 后面。(2)、内存不能出现重写。(3)、偏移量是 0x200 的倍数。
4.2、设置中断向量表偏移量
VTOR 寄存器存放的是中断向量表的起始地址。默认的情况它由 BOOT 的启动模式决定,对于 STM32F407 来说就是指向 0x0800 0000 这个位置,也就是从默认的启动位置加载中断向量等信息,不过 ST 允许重定向这个位置,这样就可以从 Flash 区域的任意位置启动我们的代码了。
/**
* @brief 设置中断向量表偏移地址
*
* @param baseAddress 基地址
* @param offset 偏移量
*/
void System_NVIC_SetVectorTable(uint32_t baseAddress, uint32_t offset)
{
// 设置NVIC的向量表偏移寄存器,VTOR低9位保留,即[8:0]保留
SCB->VTOR = baseAddress | (offset & (uint32_t)0xFFFFFE00);
}
4.3、编译生成bin文件
Bin 文件是经过压缩的可执行文件,去掉ELF格式的东西。是直接的内存映像的表示。在系统没有加载操作系统的时候可以执行。ELF(executable and link format)文件里面包含了符号表,汇编等。BIN 文件是将 elf 文件中的代码段,数据段,还有一些自定义的段抽取出来做成的一个内存的镜像。
int main(void)
{
System_NVIC_SetVectorTable(FLASH_BASE, 0x10000)
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
LED_Init();
while (1)
{
LED_Status(GPIOF, GPIO_PIN_9, LED_ON);
LED_Status(GPIOF, GPIO_PIN_10, LED_OFF);
HAL_Delay(1000);
LED_Status(GPIOF, GPIO_PIN_9, LED_OFF);
LED_Status(GPIOF, GPIO_PIN_10, LED_ON);
HAL_Delay(1000);
}
return 0;
}
五、Bootloader程序
5.1、串口程序
串口初始化函数:
UART_HandleTypeDef g_usart1_handle; // USART1句柄
uint8_t g_uart_rx_buffer[1]; // HAL库使用的串口接收数据缓冲区
/**
* @brief 串口初始化函数
*
* @param huart 串口句柄
* @param UARTx 串口寄存器基地址
* @param band 波特率
*/
void UART_Init(UART_HandleTypeDef *huart, USART_TypeDef *UARTx, uint32_t band)
{
huart->Instance = UARTx; // 寄存器基地址
huart->Init.BaudRate = band; // 波特率
huart->Init.WordLength = UART_WORDLENGTH_8B; // 数据位
huart->Init.StopBits = UART_STOPBITS_1; // 停止位
huart->Init.Parity = UART_PARITY_NONE; // 奇偶校验位
huart->Init.Mode = UART_MODE_TX_RX; // 收发模式
huart->Init.HwFlowCtl = UART_HWCONTROL_NONE; // 硬件流控制
huart->Init.OverSampling = UART_OVERSAMPLING_16; // 过采样
HAL_UART_Init(huart);
HAL_UART_Receive_IT(huart, (uint8_t *)g_uart_rx_buffer, 1); // 开启接收中断
}
串口底层初始化函数:
/**
* @brief 串口底层初始化函数
*
* @param huart 串口句柄
*/
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if (huart->Instance == USART1) // 初始化的串口是否是USART1
{
__HAL_RCC_USART1_CLK_ENABLE(); // 使能USART1时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能对应GPIO的时钟
// PA9 -> USART TXD
GPIO_InitStruct.Pin = GPIO_PIN_9; // USART1 TXD的引脚
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 推挽式复用
GPIO_InitStruct.Pull = GPIO_NOPULL; // 不使用上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 输出速度
GPIO_InitStruct.Alternate = GPIO_AF7_USART1; // 复用功能
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// PA10 -> USART RXD
GPIO_InitStruct.Pin = GPIO_PIN_10; // USART1 RXD的引脚
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_EnableIRQ(USART1_IRQn); // 使能USART1中断
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); // 设置中断优先级
}
}
USART1 中断服务函数:
/**
* @brief USART1中断服务函数
*
*/
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&g_usart1_handle); // 调用HAL库公共处理函数
HAL_UART_Receive_IT(&g_usart1_handle, (uint8_t *)g_uart_rx_buffer, 1); // 再次开启接收中断
}
USART 接收中断回调函数:
uint8_t g_usart1_rx_buffer[UART_RECEIVE_LENGTH] __attribute__((section(".app_ram"))); // 接收数据缓冲区
uint32_t g_usart1_rx_count = 0; // 接收状态标记
/**
* @brief USART接收中断回调函数
*
* @param huart 串口句柄
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
if (g_usart1_rx_count < UART_RECEIVE_LENGTH)
{
g_usart1_rx_buffer[g_usart1_rx_count++] = g_uart_rx_buffer[0];
}
HAL_UART_Receive_IT(huart, (uint8_t *)g_uart_rx_buffer, 1);
}
}
重定向 printf() 函数:
/**
* @brief 重写_write使用printf()函数
*
* @param fd 一个非负整数,代表要写入数据的文件或设备的标识
* @param ptr 一个指向字符数据的指针,即要写入的数据的起始位置
* @param length 一个整数,表示要写入的数据的字节数
* @return int 数据的字节数
*/
int _write(int fd, char *ptr, int length)
{
HAL_UART_Transmit(&g_usart1_handle, (uint8_t *)ptr, length, 0xFFFF); // g_usart1_handle是对应串口
return length;
}
在 FLASH 文件中分配一段内存用来存放串口接收的 bin 文件。
5.2、更新固件程序
将固件写入到 FLASH 中:
typedef void (*iap_fun)(void); // 定义一个函数类型的参数
uint32_t g_iap_buffer[512]; // 2K字节缓存
iap_fun jumpToApp;
/**
* @brief 将固件写入到FLASH中
*
* @param address 应用程序的起始地址
* @param data 执行应用程序bin文件的指针
* @param length 应用程序大小(字节数)
*/
void IAP_WriteAppBin(uint32_t address, uint8_t *data, uint32_t length)
{
uint32_t temp = 0;
uint8_t *current_data = data;
uint32_t k = 0;
uint32_t current_address = address;
for (uint32_t i = 0; i < length; i += 4)
{
// 拼接数据
temp = (uint32_t)current_data[3] << 24;
temp |= (uint32_t)current_data[2] << 16;
temp |= (uint32_t)current_data[1] << 8;
temp |= (uint32_t)current_data[0];
current_data += 4;
g_iap_buffer[k++] = temp;
if (k == 512)
{
k = 0;
FLASH_WriteData(current_address, g_iap_buffer, 512);
current_address += 2048; // 偏移2K字节
}
}
if (k)
{
FLASH_WriteData(current_address, g_iap_buffer, k); // 将最后剩余的字节写入
}
}
5.3、跳转到应用程序段
/**
* @brief 跳转到应用程序段(执行APP)
*
* @param address 应用程序的起始地址
*/
void IAP_LoadApp(uint32_t address)
{
printf("%#lX\r\n", (*(volatile uint32_t *)address));
// 栈顶检查没有通过,栈顶地址为: 0X20020000
// if (((*(volatile uint32_t *)address) & 0x2FFE0000) == 0x20000000) // 检查栈顶地址是否合法.可以放在内部SRAM共128KB(0x20000000)
{
jumpToApp = (iap_fun)*(volatile uint32_t *)(address + 4); // 用户代码区第二个字为程序开始地址(复位地址)
// 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
__set_MSP(address); // 设置栈顶地址
jumpToApp(); // 跳转到APP
}
}
5.4、main()函数
#define FLASH_APP1_ADDRESS 0x08010000 // 第一个应用程序起始地址(存放在内部FLASH)
int main(void)
{
uint32_t last_count = 0, app_length = 0;
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
UART_Init(&g_usart1_handle, USART1, 115200);
Key_Init();
printf("K1按键加载FLASH APP程序!\r\n");
printf("K3按键运行FLASH APP程序\r\n");
printf("%p\r\n", g_usart1_rx_buffer);
while (1)
{
if (g_usart1_rx_count)
{
if (last_count == g_usart1_rx_count) // 新周期内,没有收到任何数据,认为本次数据接收完成
{
app_length = g_usart1_rx_count;
last_count = 0;
g_usart1_rx_count = 0;
printf("用户程序接收完成!\r\n");
printf("代码长度:%ld Bytes\r\n", app_length);
}
else
{
last_count = g_usart1_rx_count;
}
}
switch (Key_Scan(0))
{
case KEY1_PRESS: // 更新固件到FLASH
if (app_length)
{
// APP程序的开始地址+4是中断向量表的复位中断服务函数的地址,这个地址从0x08000000开始,代表的是FLASH的地址
printf("%#lX\r\n", (*(volatile uint32_t *)(0x20010000 + 4)));
if (((*(volatile uint32_t *)(0x20010000 + 4)) & 0xFF000000) == 0x08000000)
{
printf("开始更新固件!\r\n");
IAP_WriteAppBin(FLASH_APP1_ADDRESS, g_usart1_rx_buffer, app_length);
printf("固件更新完成!\r\n");
}
}
break;
case KEY3_PRESS:
// 判断FLASH里面是否有APP,有的话执行
if (((*(volatile uint32_t *)(0x20010000 + 4)) & 0xFF000000) == 0x08000000)
{
printf("开始执行APP中的程序!\r\n");
IAP_LoadApp(FLASH_APP1_ADDRESS);
}
break;
default:
break;
}
HAL_Delay(100);
}
return 0;
}