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 升级的链路特点
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)
});
根因:
- RSSI 低:距离远或遮挡,包丢失多
- 干扰:Wi-Fi 或其他蓝牙设备
- CPU 忙:Flash 写入时 CPU 占用高,来不及处理 BLE 协议栈
- 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 协议栈在这期间无法响应,可能断连。
解决:
- 拆分写入:每次写 256 字节或更小,间隙处理 BLE 事件
- 双缓冲:一边收数据到 RAM,另一边写 Flash,交替进行
- 降低写入频率:攒够一定量再写,减少写入次数
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% 时吞吐明显下降。优化方向:
- 改善 RF 环境(减少干扰)
- 缩短距离
- 用 1M PHY 替代 2M PHY(2M 灵敏度差,弱信号下重传更多)
4.5 加密开销
OTA 通常走加密连接(LE Secure Connections)。加密的加解密有 CPU 开销,低端芯片可能成为瓶颈。
优化:
- 用硬件加密加速(多数芯片支持)
- 如果安全要求不高,可以不加密(不推荐)
5. 数据错误问题定位
5.1 CRC 校验失败
OTA 传输完成后,对收到的固件做 CRC 校验。校验失败说明数据在传输或写入过程中出错。
排查:
- 在传输层加 CRC(每包 CRC)
- 在 Flash 写入后读回校验
- 对比 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,调度复杂。
问题:
- 链路切换:主耳在两条链路间切换,切换延迟影响吞吐
- 转发延迟:主耳收完一个包才能转发,串行传输慢
- 双链路质量:任一链路差都会拖慢整体
优化:
- 用流水线:主耳收到包 N 时转发包 N-1,并行处理
- 动态调整两条链路的时间片比例
- 双链路独立 CRC,避免一个链路的错误影响另一个
6.3 主从切换导致 OTA 失败
如果 OTA 过程中发生主从切换(如主耳电量低),正在进行的 OTA 会中断。
解决:
- OTA 前检查电量,低电量禁止 OTA
- OTA 期间禁用主从切换
- 支持断点续传------切换后从断点继续
6.4 双耳版本不一致
升级后一只耳朵是新版本,另一只是旧版本,无法配对。
解决:
- 双耳都升级完成后才重启
- 重启后校验双方版本一致才配对
- 不一致时回滚到旧版本
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 必须简单可靠,不能出错。它的职责:
- 选择启动区(读标志)
- 校验固件 CRC
- 校验失败回退另一区
- 跳转固件
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 防掉电
升级过程中掉电是最常见的变砖原因。防护:
- 升级前检查电量:电量 < 30% 禁止升级
- 写 Flash 原子操作:写完一页才更新进度,避免半写状态
- 电容储能:检测到掉电时用电容储能完成当前 Flash 写入