先说结论
OTA升级就是让蓝牙模块"无线更新",不用拆机不用接线,手机APP直接推送新固件。 原理是模块内置双分区——一个区正常运行,另一个区接收新固件,接收完成后跳转启动就完成了。核心要点:Flash必须分双区、升级包要做CRC校验、断电要能恢复。我们深圳市颖特新科技帮上百个项目配过OTA方案,遇到过各种坑,这篇把经验全部告诉你。
什么场景需要OTA?
很多新手以为OTA是"大厂才用的功能",其实错了。只要你的设备软件可能需要更新,就必须考虑OTA。 典型场景:
- 智能锁出厂后发现有个bug,远程修复不用让用户寄回来
- 传感器固件需要定期更新算法
- 产品卖出去后要加新功能
- BLE数传模块协议改了,需要适配
不做OTA的后果:产品出问题只能召回,售后成本翻倍,客户流失。我见过最夸张的是一个智能门锁项目,因为不能OTA,召回了3000把锁,亏了50万。
OTA升级原理图解
┌──────────────────────────────────────────────────────┐
│ OTA升级完整流程 │
│ │
│ ┌─────────┐ 蓝牙传输 ┌─────────────┐ │
│ │ 手机APP │ ◄──────────────────► │ 蓝牙模块 │ │
│ │ │ 1.选择固件包 │ │ │
│ │ 2.推送 │ 3.发送数据 │ 4.写入Flash │ │
│ │ 命令 │ │ (备分区) │ │
│ └────┬────┘ └──────┬──────┘ │
│ │ │ │
│ │ 5.跳转备分区启动 │ │
│ │ ◄────────────────────────────────┘ │
│ │ 6.重启运行 │
│ ▼ │
│ ┌─────────┐ │
│ │ 新固件运行│ ← 成功!旧固件变备用分区 │
│ └─────────┘ │
└──────────────────────────────────────────────────────┘
技术本质:模块Flash分成两个固件区(双分区),运行区正在执行,备分区接收新包。接收完成后修改启动标志,系统复位后自动跳到新固件。
Flash分区设计(实战)
这是OTA最核心的部分,分区设计错了,后面全是坑。
典型分区表(以nRF52832为例,512KB Flash)
┌────────────────────────────────────────────────────────┐
│ Flash 512KB 分区布局 │
├────────────┬─────────────────┬─────────────────┬──────┤
│ Bootloader │ 固件区A │ 固件区B │ 配置区│
│ 32KB │ 200KB │ 200KB │ 48KB │
├────────────┼─────────────────┼─────────────────┼──────┤
│ 0x000000 │ 0x0008000 │ 0x003D000 │ │
│ 启动引导 │ (当前运行) │ (备分区/接收) │ 参数 │
└────────────┴─────────────────┴─────────────────┴──────┘
分区代码定义
// Flash地址定义(STM32F103示例,128KB Flash)
#define FLASH_BASE 0x08000000
#define BOOTLOADER_ADDR 0x08000000
#define BOOTLOADER_SIZE 0x4000 // 16KB
#define APP_A_ADDR 0x08004000 // 主固件区
#define APP_A_SIZE 0x1C000 // 112KB
#define APP_B_ADDR 0x08020000 // 备固件区
#define APP_B_SIZE 0x1C000 // 112KB
#define CONFIG_ADDR 0x0803C000 // 配置区
#define CONFIG_SIZE 0x4000 // 16KB
// 启动标志定义
typedef enum {
BOOT_APP_A = 0xA,
BOOT_APP_B = 0xB
} boot_target_t;
⚠️ 新手常见错误
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 不分区直接覆盖 | 升级失败设备变砖 | 必须双分区 |
| 分区太小 | 大固件放不下 | 按固件体积×1.2预留 |
| 不保存启动标志 | 不知道从哪个区启动 | Flash最后1页存标志 |
| 不擦除就写入 | 写入失败 | 先擦除再写入 |
升级传输协议设计
数据包格式
┌────────┬────────┬────────┬──────────────┬────────┬────────┐
│ Header │ Seq Num│ Length │ Payload │ CRC │ Tail │
│ 1字节 │ 2字节 │ 2字节 │ N字节 │ 2字节 │ 1字节 │
├────────┼────────┼────────┼──────────────┼────────┼────────┤
│ 0xAA │0~65535 │ 固件长度│ 固件数据 │CRC16 │ 0x55 │
└────────┴────────┴────────┴──────────────┴────────┴────────┘
传输控制逻辑
// OTA状态机
typedef enum {
OTA_STATE_IDLE = 0, // 空闲
OTA_STATE_START, // 收到开始命令
OTA_STATE_RECEIVING, // 接收中
OTA_STATE_VERIFY, // 校验中
OTA_STATE_REBOOT, // 准备重启
OTA_STATE_COMPLETE, // 完成
OTA_STATE_ERROR // 错误
} ota_state_t;
// 接收数据包
void ota_on_data_received(uint8_t *data, uint16_t len) {
ota_packet_t *pkt = (ota_packet_t *)data;
switch(ota.state) {
case OTA_STATE_START:
if(pkt->header == 0xAA && pkt->seq == 0) {
// 开始接收
ota.total_size = pkt->length;
ota.current_addr = APP_B_ADDR;
ota.current_crc = 0;
flash_erase(APP_B_ADDR, APP_B_SIZE);
ota.state = OTA_STATE_RECEIVING;
send_ack(0);
}
break;
case OTA_STATE_RECEIVING:
if(crc16_check(pkt->data, pkt->len, pkt->crc)) {
// CRC正确,写入Flash
flash_write(ota.current_addr, pkt->data, pkt->len);
ota.current_crc = crc16_update(ota.current_crc, pkt->data, pkt->len);
ota.current_addr += pkt->len;
send_ack(pkt->seq);
// 进度通知(用于UI显示)
notify_progress(ota.current_addr, ota.total_size);
} else {
// CRC错误,请求重传
send_nack(pkt->seq, ERR_CRC);
}
break;
}
}
传输参数建议
| 参数 | 推荐值 | 说明 |
|---|---|---|
| MTU大小 | 244字节 | BLE4.2建议值 |
| 单包有效数据 | 240字节 | 留4字节协议头 |
| 包间隔 | 10ms | 等待Flash写入 |
| 总超时 | 5分钟 | 大固件需要更长 |
| 传输速率 | ~20KB/s | BLE实际速率 |
Bootloader怎么写(核心代码)
Bootloader是OTA的最后一道防线,写不好就变砖。
标准Bootloader流程
// Bootloader主函数
int main(void) {
// 1. 初始化
system_init();
// 2. 检查是否需要OTA跳转
boot_target_t target = get_boot_target();
if(target == BOOT_APP_A) {
// 主固件区完整?跳转
if(verify_app_integrity(APP_A_ADDR)) {
jump_to_app(APP_A_ADDR);
}
} else if(target == BOOT_APP_B) {
// 备固件区完整?跳转
if(verify_app_integrity(APP_B_ADDR)) {
jump_to_app(APP_B_ADDR);
}
}
// 3. 都没法启动,进入Bootloader模式(等待升级)
enter_ota_mode();
}
// 跳转函数(关键!)
void jump_to_app(uint32_t app_addr) {
// 关闭所有中断
__disable_irq();
// 关闭外设
close_all_peripherals();
// 设置栈指针(向量表第一个字)
uint32_t stack_top = *(volatile uint32_t *)app_addr;
__set_MSP(stack_top);
// 获取复位向量(向量表第二个字)
uint32_t reset_handler = *(volatile uint32_t *)(app_addr + 4);
// 跳转
((void (*)(void))reset_handler)();
}
// 固件完整性校验
bool verify_app_integrity(uint32_t addr) {
// 检查栈顶地址是否合理
uint32_t stack_top = *(volatile uint32_t *)addr;
if(stack_top < 0x20000000 || stack_top > 0x20040000) {
return false;
}
// 检查固件CRC
uint32_t stored_crc = *(volatile uint32_t *)(addr + APP_SIZE - 4);
uint32_t calc_crc = crc32_calc(addr, APP_SIZE - 4);
return stored_crc == calc_crc;
}
安全机制(必须做)
1. 固件签名验证
// 使用AES-128校验固件合法性
bool verify_firmware(uint8_t *firmware, uint32_t size, uint8_t *signature) {
// 计算固件哈希
uint8_t hash[16];
SHA256(firmware, size, hash);
// 用公钥验证签名
return RSA_verify(hash, signature, public_key);
}
2. 防回滚机制
// 版本检查,防止降级
bool check_version(uint32_t new_version) {
uint32_t current = get_current_firmware_version();
// 默认不允许降级(危险!)
if(new_version < current) {
return false;
}
// 特殊情况下允许降级(需要额外验证)
if(new_version < current && is_emergency_recovery) {
return true;
}
return true;
}
3. 升级中断保护
// 保存断点信息到Flash
void save_ota_checkpoint(uint16_t last_seq, uint32_t addr, uint32_t crc) {
checkpoint_t cp;
cp.seq = last_seq;
cp.addr = addr;
cp.crc = crc;
cp.timestamp = get_tick();
flash_write(CONFIG_ADDR, &cp, sizeof(cp));
}
// 恢复断点
bool resume_ota(void) {
checkpoint_t cp;
flash_read(CONFIG_ADDR, &cp, sizeof(cp));
// 检查断点是否过期(超过10分钟)
if(get_tick() - cp.timestamp > 600000) {
return false; // 过期,重新开始
}
// 从断点继续
start_ota_from_seq(cp.seq);
return true;
}
实际案例:nRF52832 OTA方案
硬件要求
| 项目 | 要求 |
|---|---|
| Flash | ≥512KB |
| RAM | ≥64KB |
| 协议栈 | SoftDevice S132 v6+ |
推荐方案
| 方案 | 说明 | 适用场景 |
|---|---|---|
| Nordic DFU | 原厂方案,稳定 | 追求稳定 |
| 自定义OTA | 灵活可控 | 需要定制 |
| 双系统 | Bootloader+APP分离 | 复杂产品 |
深圳市颖特新科技现货供应
| 型号 | Flash | RAM | OTA支持 | 批量价(1K+) |
|---|---|---|---|---|
| nRF52832-QFAA | 512KB | 64KB | ✅ | 12-15元 |
| nRF52840-QIAA | 1MB | 256KB | ✅ | 20-25元 |
| nRF52833-QIAA | 512KB | 128KB | ✅ | 15-18元 |
Q&A常见问题
Q1:升级过程中断电了怎么办?
采用双分区方案,当前运行的固件不受影响。下次上电检测到备分区不完整,自动从主固件启动。已经写入的数据会被擦除,需要重新升级。
Q2:固件包要多大?传输要多久?
以200KB固件为例,BLE实际速率约20KB/s,传输需要10秒左右,加上校验和重启时间,总计约30秒。
Q3:模块Flash不够大怎么办?
方案一:换更大Flash的芯片;方案二:压缩固件(开启编译器优化);方案三:差分升级(只传变化的部分)。
Q4:怎么知道升级成功了?
升级完成后模块会自动重启,重启后读取版本号对比,或者在APP端显示"升级成功"提示。
Q5:可以多人同时升级吗?
不建议。一个模块同时只能有一个升级连接,其他连接会被拒绝,避免数据冲突。
Q6:BLE4.0模块能做OTA吗?
可以,但体验差。BLE4.0 MTU只有23字节,传输极慢。建议升级到BLE4.2以上,MTU可达512字节。
选型建议总结
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 追求稳定 | Nordic DFU | 原厂方案,久经验证 |
| 成本敏感 | 自定义OTA | 减少Flash浪费 |
| 需要加密 | 安全OTA方案 | 支持签名验证 |
| 资源紧张 | 差分升级 | 只传变化部分 |
相关文章
- [蓝牙模块选型指南:2026年主流型号对比](/articles/06-蓝牙模块选型指南)
- [BLE协议栈详解:从物理层到应用层](/articles/20-蓝牙协议栈基础)
- [nRF52832与ESP32深度对比](/articles/04-nRF52832蓝牙模块深度对比)
- [蓝牙模块EMC设计指南](/articles/26-蓝牙模块EMC设计)
- [BLE透传实战:串口与蓝牙数据透传](/articles/21-BLE透传实战)
*以上内容由深圳市颖特新科技技术团队整理,专注蓝牙模块分销与技术方案支持。如有OTA升级相关问题,欢迎联系:0755-82591179,邮箱:ivy@yingtexin.net*