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