Files
lucky-cat/docs/ble-reprovisioning-memory-protection.zh-CN.md
T

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`。