基于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分区定义适用于 STM32F103C8T664KB 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小灯不断快速闪烁可以使用串口助手看到交替打印ab在完成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

相关新闻

25M参数大模型CPU从零训练:完整技术路线与踩坑指南

25M参数大模型CPU从零训练:完整技术路线与踩坑指南

直接说结论:这个项目值得做,而且很适合当作你彻底搞懂“大模型是怎么炼成的”这条完整链路的第一站。25M参数,也就是约2500万参数的LLM,听起来不大,却是一个五脏俱全的自回归语言模型:有自己的词表、自己的…

2026/10/11 4:36:17 阅读更多 →
多线程编程的真实代价:从上下文切换到锁竞争

多线程编程的真实代价:从上下文切换到锁竞争

1. 真贵在何处:多线程不是白给的午餐上个月我接了个任务,某跨平台系统的消息处理模块,单线程跑得倒是规矩,就是延迟偶尔波动,老板让我提高吞吐。我当时的第一反应也简单:上多线程,八条线程并行处…

2026/10/11 4:35:16 阅读更多 →
GESP C++二级“画画”题详解:二维数组模拟与边界处理

GESP C++二级“画画”题详解:二维数组模拟与边界处理

GESP 2026年3月认证的C二级,第三部分编程题第二题叫“画画”。我拿到这道题的时候第一反应是:这不就是给二维数组填字符嘛。但等到自己真上手写,或者是在考场上盯着题目的时候,还是有不少人会丢分——不是不会写,而是细…

2026/10/11 4:35:16 阅读更多 →

最新新闻

gRPC从入门到实战:跨语言微服务通信与Python并发避坑指南

gRPC从入门到实战:跨语言微服务通信与Python并发避坑指南

我真的是太爱学习了——这种念头冒出来的时候,通常不是因为刚学会了一个妙招,而是因为刚被一个不算难的框架折腾到凌晨一点。最近这个对象就是 gRPC。很长一段时间里,我提到 gRPC 都是能躲就躲:要写 .proto、要装 protoc、跨语言还…

2026/10/12 8:44:35 阅读更多 →
从人耳到算法:MFCC梅尔倒谱系数原理与实战调参指南

从人耳到算法:MFCC梅尔倒谱系数原理与实战调参指南

1. 从人耳到算法:为什么需要梅尔倒谱系数做语音相关项目的人,迟早会撞上MFCC这个词。不管你是做语音识别、声纹比对、情感计算,还是只是想做个简单的关键词唤醒,翻开源码或者论文,MFCC几乎无处不在。但很多人第一次看到…

2026/10/12 8:44:35 阅读更多 →
2025制造业AI落地实战:视觉质检、预测性维护与排程优化

2025制造业AI落地实战:视觉质检、预测性维护与排程优化

1. 制造业AI落地的真实图景:从概念狂欢到车间实战2025年过半,我跑了十几个城市的制造企业,从年产值几十亿的汽车零部件龙头到只有百来号人的精密加工厂,聊下来最大的感受是:制造业对AI的态度已经彻底变了。前两年大家还…

2026/10/12 8:44:35 阅读更多 →
3个爆火Claude Code工具深度解析:TaoToken统一Key下如何选对赛道

3个爆火Claude Code工具深度解析:TaoToken统一Key下如何选对赛道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 8:44:35 阅读更多 →
一站式接入Kimi K3:OpenAI协议兼容的零改造迁移方案

一站式接入Kimi K3:OpenAI协议兼容的零改造迁移方案

1. 项目概述:为什么“一站式接入Kimi K3”突然成了刚需?最近两周,我陆续收到七八位不同背景的朋友咨询——有做教育类SaaS产品的技术负责人,有独立开发AI助教工具的自由开发者,还有某高校实验室带学生做对话系统课题的…

2026/10/12 8:44:35 阅读更多 →
OpenAI DevDay 2026:dots、Spaces与GPT-6.1 Sol的工程化落地指南

OpenAI DevDay 2026:dots、Spaces与GPT-6.1 Sol的工程化落地指南

1. 这不是发布会速记,而是一份开发者视角的“技术落地可行性评估报告”OpenAI DevDay 2026 这个标题本身已经释放出强烈的信号:它不再是一场面向大众的炫技秀,而是一次明确指向工程实践边界的深度技术校准。我全程参与了线上直播与会后48小时…

2026/10/12 8:43:30 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →