蓝牙模块OTA升级怎么弄?2026最全固件空中升级教程

发布时间:2026-03-27 · 阅读时长:6分钟

先说结论

OTA升级就是让蓝牙模块"无线更新",不用拆机不用接线,手机APP直接推送新固件。 原理是模块内置双分区——一个区正常运行,另一个区接收新固件,接收完成后跳转启动就完成了。核心要点:Flash必须分双区、升级包要做CRC校验、断电要能恢复。我们深圳市颖特新科技帮上百个项目配过OTA方案,遇到过各种坑,这篇把经验全部告诉你。


什么场景需要OTA?

很多新手以为OTA是"大厂才用的功能",其实错了。只要你的设备软件可能需要更新,就必须考虑OTA。 典型场景:

不做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方案 支持签名验证
资源紧张 差分升级 只传变化部分

相关文章


*以上内容由深圳市颖特新科技技术团队整理,专注蓝牙模块分销与技术方案支持。如有OTA升级相关问题,欢迎联系:0755-82591179,邮箱:ivy@yingtexin.net*

技术问题?查看更多 设计指南文章