427 lines
15 KiB
Markdown
427 lines
15 KiB
Markdown
# BLE 重新配网内存保护提交说明
|
|
|
|
## 一、提交信息
|
|
|
|
```text
|
|
提交:6495f0e1077adb8a9be15e6a3960430d9329c544
|
|
标题:fix(connectivity): harden BLE reprovisioning under memory pressure
|
|
```
|
|
|
|
## 二、问题原因
|
|
|
|
设备运行期间 Wi-Fi 断开且候选网络重连失败后,会自动切换到小程序 BLE 配网。
|
|
问题发生时,音频、本地语音命令、WebSocket、LCD 等运行时资源已经运行较长时间,
|
|
内部 SRAM 和 DMA 内存存在占用及碎片化。
|
|
|
|
BLE 控制器虽然配置为让 NimBLE Host 优先使用 PSRAM,但控制器底层仍需要
|
|
`INTERNAL + DMA` 属性的连续内存。故障日志显示:
|
|
|
|
```text
|
|
Entering mini-program BLE reprovisioning: reason=wifi_reconnect_failed
|
|
E BLE_INIT: Malloc failed
|
|
BLE assert emi.c 164, param 00000000 00001000
|
|
Guru Meditation Error: Core 0 panic'ed (Interrupt wdt timeout on CPU0)
|
|
```
|
|
|
|
实际故障链路为:
|
|
|
|
```text
|
|
Wi-Fi 重连失败
|
|
-> 运行态直接初始化 BLE
|
|
-> BLE 控制器内部连续 DMA 内存申请失败
|
|
-> BLE ROM 断言
|
|
-> CPU0 卡在断言路径
|
|
-> 中断看门狗超时
|
|
-> Panic 后软件重启
|
|
```
|
|
|
|
此外,原流程先创建 4096 字节配网任务,再初始化 BLE。该任务会提前占用一块内部
|
|
内存,进一步降低 BLE 控制器初始化时的连续内存余量。
|
|
|
|
在首次修复后,BLE 已能正常启动并广播,但设备会静默等待小程序连接。如果同时停止
|
|
LCD 和其他业务任务,用户会误认为设备已经卡死。因此最终提交还补充了配网二维码和
|
|
周期性等待日志。
|
|
|
|
## 三、解决方式
|
|
|
|
### 3.1 进入 BLE 前收敛运行时资源
|
|
|
|
运行时重新配网开始前执行以下操作:
|
|
|
|
1. 打开运行时停止门,阻止新业务继续启动。
|
|
2. 清除 PTT 活动状态。
|
|
3. 有界停止本地唤醒及其录音路径。
|
|
4. 停止多模态客户端和 WebSocket。
|
|
5. 停止下行播放并完整释放音频管线。
|
|
6. 将 LCD 切换到配网二维码界面。
|
|
7. 停止 Wi-Fi STA。
|
|
|
|
### 3.2 BLE 初始化前增加内存门槛
|
|
|
|
读取以下三项数据:
|
|
|
|
- 内部内存总空闲量。
|
|
- 内部内存最大连续块。
|
|
- DMA 内存最大连续块。
|
|
|
|
只有满足以下默认门槛才允许启动 BLE:
|
|
|
|
```text
|
|
free_internal >= 49152
|
|
largest_dma >= 12288
|
|
```
|
|
|
|
这些门槛在 Kconfig 中可调整。
|
|
|
|
### 3.3 调整 BLE 和配网任务启动顺序
|
|
|
|
新顺序为:
|
|
|
|
```text
|
|
创建队列和事件组
|
|
-> 检查内存
|
|
-> 初始化 BLE/NimBLE 并开始广播
|
|
-> 创建 4096 字节配网任务
|
|
```
|
|
|
|
BLE 优先获得所需的内部连续内存。若 BLE 已启动但任务创建失败,会停止 BLE 并回收
|
|
资源。
|
|
|
|
### 3.4 内存不足时执行轻量恢复启动
|
|
|
|
运行时释放资源后仍不满足门槛时,不再冒险调用 BLE,而是:
|
|
|
|
```text
|
|
写入 RTC no-init 一次性标志
|
|
-> 等待 100 ms
|
|
-> 软件重启
|
|
-> 下一次启动消费标志
|
|
-> 跳过高内存启动项
|
|
-> 直接进入 BLE 配网
|
|
```
|
|
|
|
RTC 标志不写 Flash,不产生 NVS 擦写损耗。标志由固定 Magic 和按位取反值共同校验,
|
|
降低随机 RTC 数据被误识别的可能性。
|
|
|
|
恢复启动阶段保留电池安全服务,但延后启动音、LCD 渲染任务、Factory CLI 和转盘
|
|
校准。BLE 初始化成功后再启动 LCD 显示配网界面。
|
|
|
|
### 3.5 防止恢复启动循环
|
|
|
|
RTC 标志只消费一次。若轻量恢复启动中的 BLE 仍启动失败,会清除恢复模式并继续正常
|
|
启动,不会持续自动重启。
|
|
|
|
### 3.6 增加可见等待状态
|
|
|
|
运行时切换 BLE 时保留 LCD,并显示配网二维码。配网任务每 10 秒打印:
|
|
|
|
```text
|
|
BLE provisioning active; waiting for mini-program credentials
|
|
```
|
|
|
|
这样可以区分“正常等待小程序连接”和“设备线程卡死”。
|
|
|
|
## 四、具体代码
|
|
|
|
### 4.1 内存门槛配置
|
|
|
|
```text
|
|
config TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES
|
|
int "Minimum free internal memory before BLE provisioning"
|
|
depends on TQ_MINI_PROGRAM_PROVISIONING_ENABLE
|
|
default 49152
|
|
range 16384 131072
|
|
|
|
config TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES
|
|
int "Minimum largest DMA block before BLE provisioning"
|
|
depends on TQ_MINI_PROGRAM_PROVISIONING_ENABLE
|
|
default 12288
|
|
range 4096 65536
|
|
```
|
|
|
|
默认配置:
|
|
|
|
```text
|
|
CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES=49152
|
|
CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES=12288
|
|
```
|
|
|
|
### 4.2 RTC 一次性恢复标志
|
|
|
|
```c
|
|
#define CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC 0x5451424cU
|
|
|
|
static bool s_provisioning_recovery_boot;
|
|
RTC_NOINIT_ATTR static uint32_t s_provisioning_recovery_marker;
|
|
RTC_NOINIT_ATTR static uint32_t s_provisioning_recovery_marker_inverse;
|
|
|
|
bool control_plane_take_mini_program_recovery_boot_request(void)
|
|
{
|
|
bool requested =
|
|
s_provisioning_recovery_marker ==
|
|
CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC &&
|
|
s_provisioning_recovery_marker_inverse ==
|
|
~CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC;
|
|
s_provisioning_recovery_marker = 0U;
|
|
s_provisioning_recovery_marker_inverse = 0U;
|
|
s_provisioning_recovery_boot = requested;
|
|
if (requested) {
|
|
ESP_LOGW(TAG, "Consumed one-shot low-memory BLE recovery boot request");
|
|
}
|
|
return requested;
|
|
}
|
|
```
|
|
|
|
### 4.3 记录标志并受控重启
|
|
|
|
```c
|
|
static void control_plane_request_provisioning_recovery_restart(
|
|
const domain_ota_memory_headroom_t *headroom)
|
|
{
|
|
s_provisioning_recovery_marker =
|
|
CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC;
|
|
s_provisioning_recovery_marker_inverse =
|
|
~CONTROL_PLANE_PROVISIONING_RECOVERY_MAGIC;
|
|
ESP_LOGW(TAG,
|
|
"Restarting into low-memory BLE recovery boot: "
|
|
"free_internal=%u largest_internal=%u largest_dma=%u",
|
|
headroom != NULL ? (unsigned)headroom->free_internal : 0U,
|
|
headroom != NULL ? (unsigned)headroom->largest_internal : 0U,
|
|
headroom != NULL ? (unsigned)headroom->largest_dma : 0U);
|
|
vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_RESTART_DELAY_MS));
|
|
domain_runtime_restart();
|
|
}
|
|
```
|
|
|
|
### 4.4 BLE 启动前内存检查
|
|
|
|
```c
|
|
static esp_err_t control_plane_check_provisioning_memory_headroom(void)
|
|
{
|
|
domain_ota_memory_headroom_t headroom = {0};
|
|
esp_err_t err = domain_runtime_ota_get_memory_headroom(&headroom);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Read BLE provisioning memory headroom failed: %s",
|
|
esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
ESP_LOGI(TAG,
|
|
"BLE provisioning memory headroom: "
|
|
"free_internal=%u largest_internal=%u largest_dma=%u",
|
|
(unsigned)headroom.free_internal,
|
|
(unsigned)headroom.largest_internal,
|
|
(unsigned)headroom.largest_dma);
|
|
|
|
if (headroom.free_internal >=
|
|
CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES &&
|
|
headroom.largest_dma >=
|
|
CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
ESP_LOGE(TAG,
|
|
"BLE provisioning memory headroom insufficient: "
|
|
"need free_internal>=%u largest_dma>=%u",
|
|
(unsigned)CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES,
|
|
(unsigned)CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES);
|
|
if (!s_provisioning_recovery_boot) {
|
|
control_plane_request_provisioning_recovery_restart(&headroom);
|
|
}
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
```
|
|
|
|
### 4.5 进入 BLE 前释放资源
|
|
|
|
```c
|
|
esp_err_t control_plane_enter_mini_program_reprovisioning(const char *reason)
|
|
{
|
|
if (control_plane_is_mini_program_provisioning_running()) {
|
|
return ESP_OK;
|
|
}
|
|
|
|
ESP_LOGW(TAG, "Entering mini-program BLE reprovisioning: reason=%s",
|
|
reason != NULL ? reason : "unspecified");
|
|
control_plane_wifi_set_keep_awake(true);
|
|
control_plane_set_runtime_stop_requested(true);
|
|
control_plane_set_ptt_active(false);
|
|
(void)control_plane_local_wake_stop(
|
|
CONTROL_PLANE_PROVISIONING_RUNTIME_QUIESCE_MS);
|
|
(void)control_plane_stop_multimodal_client();
|
|
(void)domain_runtime_multimodal_audio_stop_downstream_playback();
|
|
domain_runtime_multimodal_audio_deinit();
|
|
control_plane_show_provisioning_ui();
|
|
|
|
esp_err_t err = domain_runtime_wifi_portal_stop_sta();
|
|
if (err == ESP_OK) {
|
|
err = control_plane_start_mini_program_provisioning();
|
|
}
|
|
if (err != ESP_OK) {
|
|
control_plane_wifi_set_keep_awake(false);
|
|
if (!s_provisioning_recovery_boot) {
|
|
control_plane_request_provisioning_recovery_restart(NULL);
|
|
}
|
|
}
|
|
return err;
|
|
}
|
|
```
|
|
|
|
### 4.6 BLE 优先于配网任务启动
|
|
|
|
```c
|
|
esp_err_t err = control_plane_check_provisioning_memory_headroom();
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
|
|
err = domain_runtime_mini_program_provisioning_start(
|
|
control_plane_provisioning_event_callback,
|
|
NULL);
|
|
if (err != ESP_OK) {
|
|
return err;
|
|
}
|
|
|
|
BaseType_t created = xTaskCreate(control_plane_provisioning_worker,
|
|
"mini_prog_wifi",
|
|
CONTROL_PLANE_PROVISIONING_TASK_STACK_BYTES,
|
|
NULL,
|
|
CONTROL_PLANE_PROVISIONING_TASK_PRIORITY,
|
|
&s_provisioning_task);
|
|
if (created != pdPASS) {
|
|
s_provisioning_task = NULL;
|
|
ESP_LOGE(TAG,
|
|
"Create mini-program provisioning worker failed after BLE start");
|
|
(void)domain_runtime_mini_program_provisioning_stop(
|
|
CONTROL_PLANE_PROVISIONING_TRANSPORT_STOP_MS);
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
```
|
|
|
|
### 4.7 配网等待心跳
|
|
|
|
```c
|
|
if (xQueueReceive(s_provisioning_queue,
|
|
&event,
|
|
pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_POLL_MS)) != pdTRUE) {
|
|
TickType_t now_tick = xTaskGetTickCount();
|
|
if ((now_tick - last_wait_log_tick) >= wait_log_ticks) {
|
|
ESP_LOGI(TAG,
|
|
"BLE provisioning active; waiting for mini-program credentials");
|
|
last_wait_log_tick = now_tick;
|
|
}
|
|
continue;
|
|
}
|
|
```
|
|
|
|
### 4.8 恢复启动跳过高内存步骤
|
|
|
|
```c
|
|
bool provisioning_recovery_boot =
|
|
control_plane_take_mini_program_recovery_boot_request();
|
|
|
|
if (provisioning_recovery_boot) {
|
|
app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_AUDIO_INIT,
|
|
"audio_init",
|
|
"ble_recovery_boot");
|
|
app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_BOOT_SOUND,
|
|
"boot_sound",
|
|
"ble_recovery_boot");
|
|
app_composition_log_stage_skipped(APP_COMPOSITION_BOOT_STAGE_AUDIO_READY,
|
|
"audio_ready",
|
|
"ble_recovery_boot");
|
|
} else {
|
|
esp_err_t boot_sound_err =
|
|
control_plane_play_boot_sound(CONFIG_TQ_BOOT_SOUND_TIMEOUT_MS);
|
|
if (boot_sound_err != ESP_OK) {
|
|
ESP_LOGW(TAG, "Boot sound unavailable, continue boot: %s",
|
|
esp_err_to_name(boot_sound_err));
|
|
}
|
|
}
|
|
```
|
|
|
|
Factory CLI、LCD 渲染任务和转盘校准也使用同一个恢复启动条件延后。其中电池服务
|
|
不跳过,确保低电量保护在恢复配网期间仍然有效。
|
|
|
|
### 4.9 恢复配网成功后回到正常启动
|
|
|
|
```c
|
|
if (provisioning_succeeded && stop_err == ESP_OK &&
|
|
s_provisioning_recovery_boot) {
|
|
ESP_LOGI(TAG,
|
|
"BLE recovery provisioning completed; restarting into normal runtime");
|
|
s_provisioning_recovery_boot = false;
|
|
vTaskDelay(pdMS_TO_TICKS(CONTROL_PLANE_PROVISIONING_RESTART_DELAY_MS));
|
|
domain_runtime_restart();
|
|
}
|
|
```
|
|
|
|
## 五、解决成果
|
|
|
|
1. 实测运行时 Wi-Fi 重连失败后,内部内存从约 35 KB 恢复到约 66 KB。
|
|
2. DMA 最大连续块从约 10.5 KB 提升到约 18 KB。
|
|
3. BLE 控制器成功初始化,不再出现 `BLE_INIT: Malloc failed`。
|
|
4. 不再进入 BLE ROM assert 和 CPU0 中断看门狗复位路径。
|
|
5. BLE 能正常以 `TalkingQ-LuckyCat-*` 名称广播并等待小程序连接。
|
|
6. 配网等待期间显示二维码,并每 10 秒输出健康心跳,不再表现为静默卡死。
|
|
7. 内存仍不足时不会调用危险的 BLE 初始化,而是受控重启进入轻量模式。
|
|
8. 恢复标志不写 Flash,避免额外存储磨损。
|
|
9. 已使用 ESP-IDF 5.5.4 完整构建并成功链接。
|
|
10. 最终固件大小为 `0x2190f0`,最小应用分区仍剩余约 40%。
|
|
|
|
## 六、后续影响与注意事项
|
|
|
|
### 6.1 运行时进入配网后业务会暂停
|
|
|
|
进入 BLE 配网时会停止本地唤醒、多模态会话和音频管线,并关闭运行时业务门。这是
|
|
为了释放连续内部内存,也是预期行为。配网成功后恢复启动会重启设备,随后恢复完整
|
|
业务。
|
|
|
|
### 6.2 低内存情况下会多一次软件重启
|
|
|
|
如果运行时资源释放后仍未达到门槛,用户会看到一次受控软件重启。这比 BLE ROM
|
|
断言导致的异常重启更可控,并且下一次启动会直接进入配网模式。
|
|
|
|
### 6.3 Factory CLI 在轻量恢复启动中暂不可用
|
|
|
|
为了保留 BLE 内存余量,轻量恢复启动会延后 Factory CLI 任务。正常启动和普通运行
|
|
期间不受影响。配网完成并重启后 Factory CLI 会恢复。
|
|
|
|
### 6.4 LCD 行为
|
|
|
|
普通运行时切入 BLE 会保留 LCD 并显示二维码。轻量恢复启动会先初始化 BLE,再启动
|
|
LCD 渲染,避免 LCD 任务提前消耗 BLE 所需内存。
|
|
|
|
### 6.5 RTC 标志的生命周期
|
|
|
|
标志只保证跨软件重启保留,不是持久化配置。完全断电后标志可能丢失,这是可接受的,
|
|
因为完全断电本身已经重新整理内存,下一次启动通常不需要轻量恢复路径。
|
|
|
|
### 6.6 内存门槛需要硬件压力测试
|
|
|
|
48 KB 和 12 KB 是依据故障日志和当前 BLE 配置设定的保守值。若后续增加 BLE 服务、
|
|
连接数或控制器功能,应重新测量初始化峰值并调整门槛。
|
|
|
|
### 6.7 已知无关日志
|
|
|
|
停止本地命令时可能出现:
|
|
|
|
```text
|
|
MN_COMMAND: The mn commands is not initialized
|
|
```
|
|
|
|
该日志来自 MultiNet 命令表的冗余清理,不会阻塞 BLE,也不是本次卡死或复位原因。
|
|
可以在后续独立提交中优化,避免与本次连接恢复修复混合。
|
|
|
|
### 6.8 建议验证
|
|
|
|
- 无保存 Wi-Fi 时正常开机进入 BLE 配网。
|
|
- 设备联网后关闭路由器,等待候选遍历失败并进入 BLE。
|
|
- 使用小程序扫描广播、连接设备、读取设备信息并写入 Wi-Fi。
|
|
- 配网成功后确认设备重启并正常连接新 Wi-Fi。
|
|
- 在音频播放、本地唤醒运行和低内部内存条件下重复测试。
|
|
- 连续执行至少 20 次 Wi-Fi/BLE 模式切换。
|
|
- 确认不再出现 `BLE_INIT: Malloc failed`、ROM assert、Guru Meditation 或
|
|
`Interrupt wdt timeout on CPU0`。
|
|
|