网站首页 > 技术文章 正文
嵌入式设备软件开发里必备的一项绝技就是给你的产品软件里设计好OTA在线升级功能,方便你后续开发中能非常便利的进行软件维护、升级迭代,因此把OTA功能设计好是我们做嵌入式开发的一项必备技能。
OTA(Over-The-Air )在线升级功能通常是通过Bootloader程序和固件分区管理来实现的。以下是其实现的核心步骤及关键技术:
1. 硬件基础
《存储器分区》
单片机的Flash存储器需划分为 Bootloader区 和 应用区(甚至多分区备份)。
- Bootloader区:存放引导程序,负责固件更新和跳转,需确保不被覆盖。
- 应用区:存放主程序,可被擦写更新。
- 备份区(可选):存储临时固件或旧版本,用于容错回滚。
《通信接口》
通过串口(UART)、SPI、I2C、以太网、Wi-Fi、蓝牙等接收新固件数据。
2. 核心流程
步骤1:触发升级
- 外部指令触发(如服务器推送、本地按键)。
- 主程序检测到升级请求后,跳转至Bootloader。
步骤2:数据传输
- Bootloader通过通信接口接收新固件数据,并存储到 临时存储区(如Flash备份区或外部EEPROM)。
- 数据需分块传输,并校验完整性(CRC、MD5、SHA等)。
步骤3:固件验证
- 校验固件合法性(如数字签名、版本号)。
- 防止恶意固件注入(需加密或安全启动机制)。
步骤4:擦除与写入
- 擦除目标应用区的Flash扇区。
- 将新固件从临时区写入应用区(需按Flash页操作)。
步骤5:跳转执行
- 复位或软重启后,Bootloader检查新固件有效性。
- 若验证通过,跳转至新固件入口地址;失败则回滚或报警。
3. 关键技术
(1) Bootloader设计
- 最小化代码:占用少量Flash,仅实现固件接收、校验、擦写和跳转。
- 通信协议:定义数据包格式(如帧头、长度、校验、结束符)。
- 中断处理:升级时需暂停或重定向中断向量表。
(2) Flash操作
- 扇区管理:按Flash物理扇区擦除(如STM32的Sector Erase)。
- IAP(In-Application Programming):允许程序运行时修改Flash,需关闭全局中断。
(3) 容错机制
- 双备份(A/B分区):
保留旧版本固件,新固件异常时自动回退(需额外Flash空间)。
- 看门狗(Watchdog):防止升级过程卡死。
- 断电保护:写入前校验电压,或使用非易失性存储暂存状态。
(4) 安全机制
- 加密传输:AES等加密新固件,防止窃取。
- 签名验证:RSA/ECC验证固件来源合法性。
- 防回滚攻击:版本号强制递增,避免降级漏洞。
4. 典型实现方案
方案1:基础串口升级(UART、RS232、RS485、RS422)
基本流程:
1. 主程序收到升级指令,通过串口接收HEX/BIN文件。
2. 将数据缓存至RAM或外部Flash。
3. 跳转至Bootloader,擦除应用区并写入新固件。
4. 重启运行新程序。
方案2:无线OTA(如Wi-Fi)
基本流程:
1. 设备连接服务器,下载加密固件至外部Flash。
2. 主程序校验签名后,触发Bootloader更新。
3. 采用双分区切换(如ESP32的OTA机制)。-
5. 开发工具与库
- STM32:通过IAP库实现,或使用CubeProgrammer工具链。
- ESP32:原生支持双OTA分区和HTTPS固件下载。
- 开源Bootloader:
- OpenBLT(适用于ARM Cortex-M)。
- MCUBoot(支持Zephyr/Mynewt等RTOS)。
---
6. 开发注意事项
- 资源限制:Bootloader需精简,避免占用过多Flash/RAM。
- 时序控制:Flash擦写耗时较长,需合理设计超时机制。
- 兼容性:固件需适配硬件版本(如不同型号引脚变更)。
- 测试验证:需模拟断电、数据错误等异常场景。
7.实战案例:STM32的IAP流程
1. 主程序接收到升级指令后,通过UART/USB接收新固件至外部Flash。
2. 调用IAP函数跳转到Bootloader(位于0x08000000)。
3. Bootloader擦除主程序区(如0x08008000开始的扇区)。
4. 从外部Flash复制新固件到主程序区。
5. 校验CRC,若通过则跳转到0x08008000执行新程序。
下面是代码框架:
// bootloader.c
#include "stm32f4xx.h"
#include "crc.h" // CRC校验库
#include "flash.h" // Flash操作库
#define APP_ADDR 0x08008000 // 应用程序起始地址(需在Linker Script中配置)
#define OTA_BUFFER 0x08040000 // OTA临时存储区地址
// 应用程序有效性检查
int is_app_valid(uint32_t app_addr) {
// 检查栈顶指针是否在RAM范围内
uint32_t stack_ptr = *(volatile uint32_t*)app_addr;
if (stack_ptr < SRAM_BASE || stack_ptr > (SRAM_BASE + SRAM_SIZE)) {
return 0;
}
// 检查复位向量是否合法(假设合法地址 >= 0x08000000)
uint32_t reset_handler = *(volatile uint32_t*)(app_addr + 4);
if (reset_handler < FLASH_BASE) {
return 0;
}
return 1;
}
// 跳转到应用程序
void jump_to_app(uint32_t app_addr) {
typedef void (*app_entry)(void);
app_entry start_app = (app_entry)(*(volatile uint32_t*)(app_addr + 4));
// 关闭所有中断
__disable_irq();
// 重置向量表偏移(STM32专用)
SCB->VTOR = app_addr;
// 设置栈指针并跳转
__set_MSP(*(volatile uint32_t*)app_addr);
start_app();
}
// OTA固件更新流程
void ota_update(void) {
// 1. 从外部Flash/通信接口读取新固件到OTA_BUFFER
// ...
// 2. 验证固件(示例:CRC32校验)
uint32_t received_crc = read_crc_from_packet(); // 假设从数据包获取CRC
uint32_t calculated_crc = crc32((void*)OTA_BUFFER, firmware_size);
if (received_crc != calculated_crc) {
// 校验失败,丢弃固件
return;
}
// 3. 擦除应用程序区并写入新固件
flash_erase(APP_ADDR, firmware_size);
flash_write(APP_ADDR, (uint8_t*)OTA_BUFFER, firmware_size);
// 4. 验证写入是否成功
if (memcmp((void*)APP_ADDR, (void*)OTA_BUFFER, firmware_size) != 0) {
// 写入失败,触发回滚
return;
}
// 5. 更新成功,重启设备
NVIC_SystemReset();
}
int main(void) {
// 初始化硬件(UART、Flash、CRC等)
init_hardware();
// 检查是否需要OTA(如通过GPIO/UART指令)
if (check_ota_request()) {
ota_update();
}
// 检查应用程序是否有效
if (is_app_valid(APP_ADDR)) {
jump_to_app(APP_ADDR);
} else {
// 进入固件下载模式
enter_dfu_mode();
}
// 若所有操作失败,进入死循环
while(1);
}
通过合理设计Bootloader、存储分区和安全机制,单片机可实现在线升级功能,显著提升设备的可维护性和灵活性。
欢迎朋友们关注、分享转发,请注明文章来源,尊重原创,拒绝抄袭!
猜你喜欢
- 2025-08-05 42张图,带你真正搞懂redis数据类型的底层
- 2025-08-05 深度解密epoll 如何工作的?
- 2025-08-05 5分钟看懂的WebAssembly入门指南
- 2025-08-05 万字详文:Golang 汇编入门知识总结,看这一篇就够了
- 2025-08-05 Sliero VAD:高精度、轻量级的语音活动检测模型
- 2025-08-05 Go 语言 + aardio 快速开发图形化桌面软件,简单生成独立 EXE
- 2025-08-05 STM32+A3P125 图形控制器方案,多参数监护仪数据采集模块深度解析
- 2025-08-05 Go要点新解(二)map小解
- 2025-08-05 linux网络编程epoll模型
- 2025-08-05 盘点10个让你直呼“卧槽”的Go语言小技巧
- 08-06中等生如何学好初二数学函数篇
- 08-06C#构造函数
- 08-06初中数学:一次函数学习要点和方法
- 08-06仓颉编程语言基础-数据类型—结构类型
- 08-06C++实现委托机制
- 08-06初中VS高中三角函数:从"固定镜头"到"360°全景",数学视野升级
- 08-06一文讲透PLC中Static和Temp变量的区别
- 08-06类三剑客:一招修改所有对象!类方法与静态方法的核心区别!
- 1524℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 659℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 528℃MySQL service启动脚本浅析(r12笔记第59天)
- 494℃启用MySQL查询缓存(mysql8.0查询缓存)
- 493℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 481℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 463℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 461℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- windowsscripthost (69)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (70)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)