蓝牙 OTA 升级断连、升级失败底层链路问题定位

OTA(Over-The-Air)升级是 TWS 耳机的"高频事故现场"------升级到一半断连、升级后变砖、双耳只能升一只。这些问题 60% 不是 OTA 代码的 bug,而是底层链路在长时间大数据传输下暴露的问题。这篇拆解 OTA 失败的链路层根因和定位方法。

TL;DR

  • OTA 失败的三类现象:断连、超时、数据错误。每类根因不同。
  • 断连多因链路质量差导致 connection timeout 或 watchdog 超时。查 RSSI、连接参数、watchdog 配置。
  • 超时多因吞吐不足。查 ATT MTU、连接间隔、加密开销。
  • 数据错误多因 CRC 校验实现错误或 Flash 写入异常。查 CRC 算法和 Flash 操作。
  • OTA 过程中禁用其他业务(A2DP/HFP),避免链路负载过重。

目录

  1. [OTA 升级的链路特点](#OTA 升级的链路特点)
  2. 失败现象分类
  3. 断连问题定位
  4. 超时问题定位
  5. 数据错误问题定位
  6. [TWS 双耳 OTA 特殊问题](#TWS 双耳 OTA 特殊问题)
  7. 防砖设计

1. OTA 升级的链路特点

1.1 和正常业务的区别

OTA 升级和日常使用(音乐/通话)完全不同:

维度 日常业务 OTA 升级
持续时间 几分钟到几小时 5-30 分钟(全程高负载)
数据量 实时流,几百 kbps 几 MB 固件,全量传输
链路要求 偶尔丢包可接受 一个包都不能丢(要重传)
功耗 间歇 持续高功耗(发热)
失败影响 断了重连 可能变砖

OTA 是对蓝牙链路最严苛的考验------长时间、大数据、不能丢、还要保证 Flash 写入正确。

1.2 OTA 的数据流

#mermaid-svg-N6JMmvfwMF59ThsE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N6JMmvfwMF59ThsE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N6JMmvfwMF59ThsE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N6JMmvfwMF59ThsE .error-icon{fill:#552222;}#mermaid-svg-N6JMmvfwMF59ThsE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N6JMmvfwMF59ThsE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N6JMmvfwMF59ThsE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N6JMmvfwMF59ThsE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N6JMmvfwMF59ThsE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N6JMmvfwMF59ThsE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N6JMmvfwMF59ThsE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N6JMmvfwMF59ThsE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N6JMmvfwMF59ThsE .marker.cross{stroke:#333333;}#mermaid-svg-N6JMmvfwMF59ThsE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N6JMmvfwMF59ThsE p{margin:0;}#mermaid-svg-N6JMmvfwMF59ThsE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N6JMmvfwMF59ThsE .cluster-label text{fill:#333;}#mermaid-svg-N6JMmvfwMF59ThsE .cluster-label span{color:#333;}#mermaid-svg-N6JMmvfwMF59ThsE .cluster-label span p{background-color:transparent;}#mermaid-svg-N6JMmvfwMF59ThsE .label text,#mermaid-svg-N6JMmvfwMF59ThsE span{fill:#333;color:#333;}#mermaid-svg-N6JMmvfwMF59ThsE .node rect,#mermaid-svg-N6JMmvfwMF59ThsE .node circle,#mermaid-svg-N6JMmvfwMF59ThsE .node ellipse,#mermaid-svg-N6JMmvfwMF59ThsE .node polygon,#mermaid-svg-N6JMmvfwMF59ThsE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N6JMmvfwMF59ThsE .rough-node .label text,#mermaid-svg-N6JMmvfwMF59ThsE .node .label text,#mermaid-svg-N6JMmvfwMF59ThsE .image-shape .label,#mermaid-svg-N6JMmvfwMF59ThsE .icon-shape .label{text-anchor:middle;}#mermaid-svg-N6JMmvfwMF59ThsE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N6JMmvfwMF59ThsE .rough-node .label,#mermaid-svg-N6JMmvfwMF59ThsE .node .label,#mermaid-svg-N6JMmvfwMF59ThsE .image-shape .label,#mermaid-svg-N6JMmvfwMF59ThsE .icon-shape .label{text-align:center;}#mermaid-svg-N6JMmvfwMF59ThsE .node.clickable{cursor:pointer;}#mermaid-svg-N6JMmvfwMF59ThsE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N6JMmvfwMF59ThsE .arrowheadPath{fill:#333333;}#mermaid-svg-N6JMmvfwMF59ThsE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N6JMmvfwMF59ThsE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N6JMmvfwMF59ThsE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N6JMmvfwMF59ThsE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N6JMmvfwMF59ThsE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N6JMmvfwMF59ThsE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N6JMmvfwMF59ThsE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N6JMmvfwMF59ThsE .cluster text{fill:#333;}#mermaid-svg-N6JMmvfwMF59ThsE .cluster span{color:#333;}#mermaid-svg-N6JMmvfwMF59ThsE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N6JMmvfwMF59ThsE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N6JMmvfwMF59ThsE rect.text{fill:none;stroke-width:0;}#mermaid-svg-N6JMmvfwMF59ThsE .icon-shape,#mermaid-svg-N6JMmvfwMF59ThsE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N6JMmvfwMF59ThsE .icon-shape p,#mermaid-svg-N6JMmvfwMF59ThsE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N6JMmvfwMF59ThsE .icon-shape .label rect,#mermaid-svg-N6JMmvfwMF59ThsE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N6JMmvfwMF59ThsE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N6JMmvfwMF59ThsE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N6JMmvfwMF59ThsE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} BLE/BREDR
手机 APP
固件传输
RAM 缓冲
CRC 校验
Flash 写入
校验重启

链路层的问题主要发生在"固件传输"阶段------长时间大数据传输,链路质量波动会累积暴露。

1.3 OTA 协议栈

OTA 通常用以下协议之一:

协议 通道 特点
BLE GATT BLE ATT 主流,兼容性好
BLE L2CAP CoC BLE L2CAP 高吞吐
RFCOMM/SPP BREDR 老方案
自定义 HCI 透传 BREDR/BLE 厂商私有

BLE GATT 最常见,但吞吐最低。本文以 BLE GATT 为例。


2. 失败现象分类

2.1 三类现象

现象 表现 根因方向
断连 升级到一半连接断开 链路质量、watchdog、参数
超时 升级进度极慢,最终超时 吞吐不足、重传多
数据错误 升级完成但校验失败 CRC、Flash 写入

2.2 失败时间分布

  • 0-10%:建立连接阶段,配置问题
  • 10-50%:传输中段,链路质量问题
  • 50-90%:传输后段,Flash 写入慢导致超时
  • 90-100%:校验重启阶段,Flash 数据错误

中段失败最常见------链路质量问题在长时间传输下暴露。


3. 断连问题定位

3.1 断连的直接原因

BLE 断连由 HCI_Disconnection_Complete 事件报告,原因码:

Reason 含义 常见根因
0x08 Connection Timeout 链路质量差,长时间收不到包
0x13 Remote User Terminated 对端主动断开
0x16 Connection Terminated By Local Host 本端主动断开
0x22 Connection Timeout LSTO 超时
0x3B Instant Passed 参数更新失败

3.2 Connection Timeout (0x08)

最常见。BLE 的 Supervision Timeout 到期------在 timeout 时间内没收到任何包。

排查

bash 复制代码
# 查 Supervision Timeout 配置
hci_dump | grep -i "supervision"

# 典型配置
# ConnInterval = 20ms
# ConnLatency = 0
# SupervisionTimeout = 2s (2000ms)

如果 SupervisionTimeout 太短(如 500ms),链路短暂波动就会断。OTA 期间建议设到 4-6 秒:

c 复制代码
// OTA 期间增大 SupervisionTimeout
ble_gap_update_connection_params(conn_handle, {
    .min_interval = 15,   // 20ms
    .max_interval = 15,   // 20ms
    .latency = 0,
    .supervision_timeout = 600,  // 6 秒 (单位 10ms)
});

根因

  1. RSSI 低:距离远或遮挡,包丢失多
  2. 干扰:Wi-Fi 或其他蓝牙设备
  3. CPU 忙:Flash 写入时 CPU 占用高,来不及处理 BLE 协议栈
  4. Crystal 漂移:长时间工作后晶振频偏累积

3.3 Watchdog 超时

很多设备有看门狗,BLE 协议栈必须定期喂狗。OTA 期间如果 Flash 写入阻塞了协议栈,看门狗复位导致"断连"------实际是设备重启。

排查

  • 看设备日志,是否有 watchdog reset 记录
  • 测 Flash 写入耗时,是否超过 watchdog 超时

解决

c 复制代码
// Flash 写入时拆分喂狗
void flash_write_with_watchdog(uint32_t addr, uint8_t *data, size_t len) {
    const size_t chunk = 256;  // 每次写 256 字节
    for (size_t i = 0; i < len; i += chunk) {
        flash_write(addr + i, data + i, min(chunk, len - i));
        watchdog_feed();  // 每写一块喂狗
    }
}

3.4 Flash 写入阻塞

Flash 写入是原子操作,写入期间 CPU 不能取指执行(某些芯片)。如果一次写 4KB,可能阻塞 50-100ms------BLE 协议栈在这期间无法响应,可能断连。

解决

  1. 拆分写入:每次写 256 字节或更小,间隙处理 BLE 事件
  2. 双缓冲:一边收数据到 RAM,另一边写 Flash,交替进行
  3. 降低写入频率:攒够一定量再写,减少写入次数
c 复制代码
// 双缓冲示例
uint8_t buf[2][4096];
int active_buf = 0;
int buf_offset = 0;

void on_ota_data(uint8_t *data, size_t len) {
    memcpy(buf[active_buf] + buf_offset, data, len);
    buf_offset += len;

    if (buf_offset >= 4096) {
        // 触发 Flash 写入(非阻塞,用 DMA 或中断)
        flash_write_async(buf[active_buf], 4096);
        active_buf = 1 - active_buf;  // 切换缓冲
        buf_offset = 0;
    }
}

3.5 热问题

OTA 持续高功耗,设备发热。某些芯片高温下 RF 性能下降,导致链路质量变差。TWS 耳机体积小散热差,这个问题更明显。

排查

  • 测 OTA 期间设备温度
  • 对比常温和高温下的链路质量

解决

  • OTA 时降低发射功率(缩短距离配合)
  • 分段升级,每段间休息散热
  • 改善 PCB 散热设计

4. 超时问题定位

4.1 吞吐瓶颈

BLE OTA 的典型吞吐:

配置 理论吞吐 实际吞吐
BLE 4.0, MTU=23, Interval=100ms ~8 kbps 5-7 kbps
BLE 4.2, MTU=185, Interval=30ms ~150 kbps 80-120 kbps
BLE 5.0, 2M PHY, MTU=251, Interval=15ms ~500 kbps 200-300 kbps

升一个 2MB 固件:

  • BLE 4.0:约 40 分钟(太久)
  • BLE 4.2:约 3-5 分钟(可接受)
  • BLE 5.0:约 1-2 分钟(理想)

4.2 提升 MTU

MTU(Maximum Transmission Unit)决定每个包能装多少数据:

复制代码
ATT 包 = ATT Header (3B) + ATT Value (MTU - 3)

MTU=23 时,每个 ATT 包只能装 20 字节 OTA 数据------效率极低。

优化

c 复制代码
// OTA 开始时协商大 MTU
ble_att_request_mtu_exchange(conn_handle, 247);  // 请求 MTU 247

// 等待 MTU 交换完成
void on_mtu_exchanged(uint16_t mtu) {
    ota_mtu = mtu;
    // 每包能装 mtu - 3 字节 OTA 数据
}

4.3 连接间隔

ConnInterval 影响每秒能发多少包:

ConnInterval 每秒连接事件 每秒数据量(MTU=247)
100ms 10 2.4 KB/s
30ms 33 8 KB/s
15ms 66 16 KB/s
7.5ms 133 32 KB/s

OTA 期间用小 Interval 提升吞吐:

c 复制代码
// OTA 期间调小 ConnInterval
ble_gap_update_connection_params(conn_handle, {
    .min_interval = 6,   // 7.5ms (单位 1.25ms)
    .max_interval = 6,   // 7.5ms
    .latency = 0,
    .supervision_timeout = 600,
});

4.4 数据重传

包丢失导致重传,重传多吞吐下降。查重传率:

bash 复制代码
# 看重传统计
btmon | grep -i "retransmit"

重传率 > 10% 时吞吐明显下降。优化方向:

  1. 改善 RF 环境(减少干扰)
  2. 缩短距离
  3. 用 1M PHY 替代 2M PHY(2M 灵敏度差,弱信号下重传更多)

4.5 加密开销

OTA 通常走加密连接(LE Secure Connections)。加密的加解密有 CPU 开销,低端芯片可能成为瓶颈。

优化

  • 用硬件加密加速(多数芯片支持)
  • 如果安全要求不高,可以不加密(不推荐)

5. 数据错误问题定位

5.1 CRC 校验失败

OTA 传输完成后,对收到的固件做 CRC 校验。校验失败说明数据在传输或写入过程中出错。

排查

  1. 在传输层加 CRC(每包 CRC)
  2. 在 Flash 写入后读回校验
  3. 对比 RAM 中的数据和 Flash 中的数据
c 复制代码
// 分层校验
void on_ota_packet(uint8_t *data, size_t len, uint16_t crc) {
    // 第一层:包 CRC
    if (crc16(data, len) != crc) {
        send_nack();  // 要求重传
        return;
    }
    send_ack();
    buffer_data(data, len);
}

void on_ota_complete() {
    // 第二层:整包 CRC
    if (crc32(firmware_buf, firmware_size) != expected_crc32) {
        send_ota_error(ERR_CRC_MISMATCH);
        return;
    }

    // 第三层:Flash 读回校验
    for (int i = 0; i < firmware_size; i += 4096) {
        uint8_t readback[4096];
        flash_read(OTA_ADDR + i, readback, 4096);
        if (memcmp(readback, firmware_buf + i, 4096) != 0) {
            send_ota_error(ERR_FLASH_VERIFY);
            return;
        }
    }
}

5.2 Flash 写入错误

Flash 写入失败的常见原因:

原因 表现 解决
写入前未擦除 写入失败或数据错 先擦后写
跨页写入 部分写入成功 按页对齐写入
写入超时 看门狗复位 拆分写入
Flash 寿命到 写入失败 换 Flash
电压不足 写入异常 检查电源

排查

c 复制代码
// Flash 写入要检查返回值
flash_err_t err = flash_write(addr, data, len);
if (err != FLASH_OK) {
    LOG_E("Flash write failed: addr=0x%x, err=%d", addr, err);
    // 重试或上报错误
}

5.3 数据顺序错误

如果 OTA 协议支持乱序传输(类似 TCP 的选择性 ACK),接收端要正确重组。顺序错误会导致固件错乱。

排查

  • 给每个包加序号
  • 接收端检查序号连续性
  • 乱序时缓存等缺失包

6. TWS 双耳 OTA 特殊问题

6.1 双耳同步升级

TWS 有两只耳朵,固件要同步升级。两种方案:

方案1:分别升级

手机分别连主耳和从耳,各自升级。

  • 优点:简单
  • 缺点:慢,且可能出现版本不一致(一只升级成功,另一只失败)

方案2:主耳转发

手机只连主耳,主耳收固件后转发给从耳。

  • 优点:版本一致
  • 缺点:复杂,转发链路质量问题

6.2 主从转发 OTA 的链路问题

主耳同时维护两条链路:手机↔主耳(收固件)、主耳↔从耳(转发固件)。两条链路共享 RF,调度复杂。

问题:

  1. 链路切换:主耳在两条链路间切换,切换延迟影响吞吐
  2. 转发延迟:主耳收完一个包才能转发,串行传输慢
  3. 双链路质量:任一链路差都会拖慢整体

优化

  • 用流水线:主耳收到包 N 时转发包 N-1,并行处理
  • 动态调整两条链路的时间片比例
  • 双链路独立 CRC,避免一个链路的错误影响另一个

6.3 主从切换导致 OTA 失败

如果 OTA 过程中发生主从切换(如主耳电量低),正在进行的 OTA 会中断。

解决

  1. OTA 前检查电量,低电量禁止 OTA
  2. OTA 期间禁用主从切换
  3. 支持断点续传------切换后从断点继续

6.4 双耳版本不一致

升级后一只耳朵是新版本,另一只是旧版本,无法配对。

解决

  1. 双耳都升级完成后才重启
  2. 重启后校验双方版本一致才配对
  3. 不一致时回滚到旧版本

7. 防砖设计

7.1 双区升级

用两个固件区:A 区和 B 区。升级时写 B 区,启动时校验 B 区,OK 则切换到 B 区,失败则继续用 A 区。

复制代码
Flash 布局:
  Bootloader: 0x0000-0x3FFF
  A 区固件:  0x4000-0x3FFFF  (当前运行)
  B 区固件:  0x40000-0x7FFFF  (升级写入)
  配置区:    0x80000-0x80FFF

升级流程:
  1. 运行 A 区,接收固件写 B 区
  2. 写完校验 B 区 CRC
  3. 标记启动区为 B
  4. 重启 → Bootloader 校验 B 区 → OK 则跳转 B 区
  5. 失败则回退 A 区

7.2 Bootloader 校验

Bootloader 必须简单可靠,不能出错。它的职责:

  1. 选择启动区(读标志)
  2. 校验固件 CRC
  3. 校验失败回退另一区
  4. 跳转固件
c 复制代码
// Bootloader 伪代码
void bootloader_main() {
    int active = read_active_partition();

    if (verify_firmware(active)) {
        jump_to_firmware(active);
    } else {
        // 回退另一区
        int backup = 1 - active;
        if (verify_firmware(backup)) {
            write_active_partition(backup);
            jump_to_firmware(backup);
        } else {
            // 两区都坏,进恢复模式
            enter_recovery_mode();
        }
    }
}

7.3 恢复模式

如果两区都坏,要有恢复模式:

  • 通过 USB/UART 接口升级(有线)
  • 通过 BLE 强制升级(最小化 BLE 固件,只支持 OTA)

7.4 断点续传

支持断点续传,断连后重连从断点继续,不用从头开始:

c 复制代码
// 断点续传协议
void on_ota_start() {
    uint32_t offset = read_ota_progress();  // 读上次进度
    send_ota_resume_request(offset);  // 请求从 offset 继续
}

void on_ota_data(uint32_t offset, uint8_t *data, size_t len) {
    flash_write(OTA_ADDR + offset, data, len);
    write_ota_progress(offset + len);  // 保存进度
}

7.5 防掉电

升级过程中掉电是最常见的变砖原因。防护:

  1. 升级前检查电量:电量 < 30% 禁止升级
  2. 写 Flash 原子操作:写完一页才更新进度,避免半写状态
  3. 电容储能:检测到掉电时用电容储能完成当前 Flash 写入

相关推荐
attitude.x2 小时前
2026年大数据分析软件推荐:性能与安全对比
安全·数据挖掘·数据分析
振南的单片机世界2 小时前
3.5T静默分帧,CRC16校验:Modbus-RTU帧结构解析
arm开发·stm32·单片机·嵌入式硬件
LingzhiPi2 小时前
零知派ESP32--AS5600磁吸旋钮音量控制器
c++·单片机·嵌入式硬件
尼喃2 小时前
42V热拔插认证过压保护芯片:70V耐压+响应<1μs+可调OVP+SOT23-6
嵌入式硬件
0x3F(小茶)3 小时前
STM32 SPI的5种方向模式
c语言·stm32·单片机·嵌入式硬件
海带紫菜菠萝汤3 小时前
金融文档翻译安全合规:ISO 27001与数据保护实践
安全·金融
Sagittarius_A*4 小时前
【RCELABS】Level 17~18 —— PHP命令执行函数与环境变量注入
开发语言·安全·web安全·靶场·php·rce
BSD_CGQ4 小时前
FSR压力传感器信号调理电路设计要点
单片机·嵌入式硬件·压力传感器·源头工厂·薄膜压力传感器
爱就是恒久忍耐5 小时前
CanFestival移植到STM32 F103芯片(基于HAL库)
stm32·单片机·嵌入式硬件