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

15 KiB

BLE 重新配网内存保护提交说明

一、提交信息

提交:6495f0e1077adb8a9be15e6a3960430d9329c544
标题:fix(connectivity): harden BLE reprovisioning under memory pressure

二、问题原因

设备运行期间 Wi-Fi 断开且候选网络重连失败后,会自动切换到小程序 BLE 配网。 问题发生时,音频、本地语音命令、WebSocket、LCD 等运行时资源已经运行较长时间, 内部 SRAM 和 DMA 内存存在占用及碎片化。

BLE 控制器虽然配置为让 NimBLE Host 优先使用 PSRAM,但控制器底层仍需要 INTERNAL + DMA 属性的连续内存。故障日志显示:

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)

实际故障链路为:

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:

free_internal >= 49152
largest_dma  >= 12288

这些门槛在 Kconfig 中可调整。

3.3 调整 BLE 和配网任务启动顺序

新顺序为:

创建队列和事件组
    -> 检查内存
    -> 初始化 BLE/NimBLE 并开始广播
    -> 创建 4096 字节配网任务

BLE 优先获得所需的内部连续内存。若 BLE 已启动但任务创建失败,会停止 BLE 并回收 资源。

3.4 内存不足时执行轻量恢复启动

运行时释放资源后仍不满足门槛时,不再冒险调用 BLE,而是:

写入 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 秒打印:

BLE provisioning active; waiting for mini-program credentials

这样可以区分“正常等待小程序连接”和“设备线程卡死”。

四、具体代码

4.1 内存门槛配置

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

默认配置:

CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_FREE_INTERNAL_BYTES=49152
CONFIG_TQ_MINI_PROGRAM_PROVISIONING_MIN_LARGEST_DMA_BYTES=12288

4.2 RTC 一次性恢复标志

#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 记录标志并受控重启

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 启动前内存检查

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 前释放资源

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 优先于配网任务启动

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 配网等待心跳

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 恢复启动跳过高内存步骤

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 恢复配网成功后回到正常启动

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 已知无关日志

停止本地命令时可能出现:

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。