云计算百科
云计算领域专业知识百科平台

基于stm32 的OTA在线升级学习笔记(1)

OTA升级指的是。。。

现在网络上很多教程都把原理讲的很明白了,但是我自己学的时候没有找到很详细的步骤从0开始一步一步完成双区OTA升级的实验过程,因此我把我的学习过程记录下面供和我一样的小白参考。

原理我这里就不细说了,相信来了解这部分内容的大家应该有一定的理解。

这里我写我的实现过程。bootloader程序我参考的下面这篇博客

STM32网络在线升级(OTA)完整实现指南_mob64ca1403528a的技术博客_51CTO博客

他是基于hal库来编写的,我是使用的标准库,因此缺少一些结构体和函数的定义,但是现在ai可以很方便的帮你补全这部分的定义。

#include <string.h>
#include "usart1.h"
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"

// Flash分区定义(适用于 STM32F103C8T6,64KB Flash)
#define BOOTLOADER_BASE 0x08000000
#define APPLICATION_BASE 0x08003000 // APP A 起始
#define OTA_BUFFER_BASE 0x0800C000 // APP B / 缓冲区
#define CONFIG_BASE 0x0800F800 // 参数区,页对齐

#define BOOTLOADER_SIZE (12 * 1024) // 12KB,给 Bootloader 留足余量
#define APPLICATION_SIZE (36 * 1024) // 从 0x08003000 到 0x0800C000 = 36KB
#define OTA_BUFFER_SIZE (36 * 1024) // 从 0x0800C000 到 0x0800F800 = 36KB
#define FLASH_PAGE_SIZE 1024 // F103C8T6 的页大小 1KB

// 升级标志结构
typedef struct {
uint32_t magic; // 魔术字 0xDEADBEEF
uint32_t update_flag; // 升级标志 1:需要升级
uint32_t firmware_size; // 固件大小
uint32_t crc32; // CRC32校验值
} ota_config_t;

typedef enum {
HAL_OK = 0x00,
HAL_ERROR = 0x01,
HAL_BUSY = 0x02,
HAL_TIMEOUT = 0x03
} StatusTypeDef;

// 函数声明
static void jump_to_application(uint32_t address);
static int check_application_valid(uint32_t address);
StatusTypeDef flash_erase(uint32_t start_addr, uint32_t size);
StatusTypeDef flash_write(uint32_t dest_addr, uint8_t *src, uint32_t size);
uint32_t calculate_crc32(uint8_t *data, uint32_t size);
int perform_firmware_update(void);

// CRC32计算
static uint32_t calculate_crc32(uint8_t *data, uint32_t size)
{
uint32_t i;
uint32_t *p = (uint32_t *)data;

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_CRC, ENABLE); // 使能 CRC 时钟
CRC_ResetDR(); // 复位 CRC 数据寄存器到 0xFFFFFFFF

for (i = 0; i < (size / 4); i++) {
CRC->DR = p[i];
}

return CRC->DR;
}

// 检查应用程序有效性
static int check_application_valid(uint32_t address) {
uint32_t sp = *(__IO uint32_t*)address;

// 检查栈指针是否在RAM范围内
if ((sp & 0x2FFE0000) == 0x20000000) {
return 1;
}
return 0;
}

void HAL_DeInit(void)
{
/* 复位 APB1 总线上的外设 (USART2/3/4/5, TIM2-7, SPI2, I2C1/2 等) */
RCC_APB1PeriphResetCmd(RCC_APB1Periph_ALL, ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_ALL, DISABLE);

/* 复位 APB2 总线上的外设 (USART1, SPI1, TIM1, GPIOA-E 等) */
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ALL, ENABLE);
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ALL, DISABLE);

}

// 跳转到应用程序
static void jump_to_application(uint32_t address) {
typedef void (*pFunction)(void);
pFunction jump_to_app;
uint32_t jump_address;

// 获取复位向量
jump_address = *(__IO uint32_t*)(address + 4);
jump_to_app = (pFunction)jump_address;

// 设置主栈指针
__set_MSP(*(__IO uint32_t*)address);

// 关闭所有中断
__disable_irq();

// 复位所有外设
HAL_DeInit();

// 设置向量表
SCB->VTOR = address;

// 跳转到应用程序
jump_to_app();
}

// Flash擦除
static StatusTypeDef flash_erase(uint32_t start_addr, uint32_t size)
{
uint32_t page_addr = start_addr;
uint32_t end_addr = start_addr + size;
FLASH_Status status;

FLASH_Unlock(); // 解锁 Flash 控制寄存器[citation:7]

// 按页循环擦除
while (page_addr < end_addr) {
status = FLASH_ErasePage(page_addr);
if (status != FLASH_COMPLETE) {
FLASH_Lock();
return HAL_ERROR;
}
page_addr += FLASH_PAGE_SIZE;
}

FLASH_Lock();
return HAL_OK;
}

// Flash写入
static StatusTypeDef flash_write(uint32_t dest_addr, uint8_t *src, uint32_t size)
{
uint32_t i;
uint32_t word;
FLASH_Status status;

FLASH_Unlock();

for (i = 0; i < size; i += 4) {
// 拼装一个 32 位字
word = (uint32_t)src[i]
| ((uint32_t)src[i + 1] << 8)
| ((uint32_t)src[i + 2] << 16)
| ((uint32_t)src[i + 3] << 24);

status = FLASH_ProgramWord(dest_addr + i, word);
if (status != FLASH_COMPLETE) {
FLASH_Lock();
return HAL_ERROR;
}
}

FLASH_Lock();
return HAL_OK;
}

// 执行固件更新
static int perform_firmware_update(void) {
ota_config_t *config = (ota_config_t*)CONFIG_BASE;
uint8_t *ota_data = (uint8_t*)OTA_BUFFER_BASE;
uint32_t calculated_crc;

// 检查魔术字
if (config->magic != 0xDEADBEEF) {
return -1;
}

// 检查更新标志
if (config->update_flag != 1) {
return -1;
}

// 验证CRC
calculated_crc = calculate_crc32(ota_data, config->firmware_size);
if (calculated_crc != config->crc32) {
return -2; // CRC错误
}

// 擦除应用程序区域
if (flash_erase(APPLICATION_BASE, config->firmware_size) != HAL_OK) {
return -3;
}

// 写入新固件
if (flash_write(APPLICATION_BASE, ota_data, config->firmware_size) != HAL_OK) {
return -4;
}

// 清除更新标志
ota_config_t clear_config = {0};
flash_erase(CONFIG_BASE, sizeof(ota_config_t));
flash_write(CONFIG_BASE, (uint8_t*)&clear_config, sizeof(ota_config_t));

return 0;
}

// Bootloader主函数
int main(void) {

SystemInit();
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
USART1_Config();
delay_init(); // 初始化延时
LED_GPIO_Config(); //led、蜂鸣器初始化

// ★ 初始化串口(内部会开中断)
//uart_init(115200);
// ★ 配置DMA(内部会开中断)
//DMA_Config(DMA1_Channel5,(u32)&USART1->DR,(u32)CercleBuffer, CercleBufferSize);//DMA1通道5,外设为串口1,存储器为SendBuff,长度SEND_BUF_SIZE.

// 检查是否需要升级
ota_config_t *config = (ota_config_t*)CONFIG_BASE;
if (config->magic == 0xDEADBEEF && config->update_flag == 1) {
// LED快闪表示正在升级
for (int i = 0; i < 10; i++) {
//HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
//HAL_Delay(100);
LED1(i);
delay_ms(100);
}

// 执行升级
if (perform_firmware_update() == 0) {
// 升级成功,LED常亮1秒
//HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
//HAL_Delay(1000);
LED1(1);
delay_ms(100);
} else {
// 升级失败,LED慢闪
for (int i = 0; i < 5; i++) {
//HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
//HAL_Delay(500);
LED1(i);
delay_ms(100);
}
}
}

// 检查应用程序是否有效
if (check_application_valid(APPLICATION_BASE)) {
// 跳转到应用程序
jump_to_application(APPLICATION_BASE);
}

// 如果无法跳转,LED持续慢闪
while (1) {
//HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
//HAL_Delay(1000);
GPIO_SetBits(GPIOC, GPIO_Pin_13 );
delay_ms(100);
USART_SendData(USART1,'a');
GPIO_ResetBits(GPIOC, GPIO_Pin_13 );
delay_ms(100);
USART_SendData(USART1,'b');

}
}

由于他这篇博客目标是实现单区的OTA升级,因此在他的bootloader中没有写固件的接收部分,他把接收部分放到了A区中进行。我这里主要是跟他复现了一边,实际来说,烧录bootloader和正常烧录程序没有什么区别,唯一需要注意的是,这部分函数需要写的尽可能精简,因为所有的程序共享一块flash,这里占的空间多,主要的运行程序剩下的空间就少。

在烧录时需要注意如下图步骤6所示的位置它代表的是允许烧录算法访问的范围。当你的bootloader程序体积超出这个范围是,就会出现烧录失败的情况,这是为了避免出现越区导致烧坏的情况。所以如果对自己的程序有把握,这里也可以之间填0x08000000和 0x10000避免反复修改

在keil中,编译后的程序体积可以在build output中查看

上面的bootloader程序烧录成功现象为:

板上PC13小灯不断快速闪烁,可以使用串口助手看到交替打印'a''b'

在完成bootloader程序有个基本概念后,继续写APP1分区的程序,这里作为基本实验只看有没有成功运行,因此以简单的程序为代表。

需要注意的是SCB->VTOR = 0x08003000;这一句是必要的,它的作用是修改向量表,不改写中断向量表,APP 里的中断会全部跑到 Bootloader 的向量表里去,结果是"主循环能跑,一进中断就死"。这里的地址的APP1的起始地址。

#include <string.h>
#include "usart1.h"
#include "stm32f10x.h"
#include "led.h"
#include "delay.h"

int main(void)
{
SystemInit();
SCB->VTOR = 0x08003000; // 和 APPLICATION_BASE 一致,放在 SystemInit 之后
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
USART1_Config();
delay_init();
LED_GPIO_Config();

char *str1 = "jump success!\\r\\n";
int len = strlen(str1);

while (1) {
GPIO_SetBits(GPIOC, GPIO_Pin_13);
delay_ms(500);
for (int i = 0; i < len; i++) {
USART_SendData(USART1, str1[i]);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
delay_ms(500);
}
}

由于板中已经存在了bootloader,所以很好理解的,APP1不能从原理的初始地址开始写,否则会把bootloader程序覆盖,因此在烧录前需要修改代码的起始位置,同时和烧录bootloader一样,也要将擦除方式修改为Erase Sectors。

完成这两个步骤就可以烧写了,

上面的APP1程序烧录成功现象为:

板上PC13小灯不断缓慢闪烁,可以使用串口助手看到打印jump success!

代表程序成功从bootloader跳到了APP1

赞(0)
未经允许不得转载:网硕互联帮助中心 » 基于stm32 的OTA在线升级学习笔记(1)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!