Files
lucky-cat/docs/knowledge vault/bugfix-cloud-chouqian-system-event-stack-overflow.zh-CN.md
T

9.0 KiB
Raw Blame History

云端抽签触发系统事件任务栈溢出:问题与修复说明

1. 对应提交

d386de2 fix(multimodal): prepare session for cloud fortune draw

本文说明首次抽签能够自动创建多模态会话后,按下 SW2 导致 cp_system_event 任务栈溢出的问题,以及最终采用的异步 worker 方案。

2. 问题现象

修复“首次抽签缺少会话”后,设备按下 SW2 可以进入 Start 发送,但随即触发 FreeRTOS 栈保护:

control_plane_mm_rt: Cloud chouqian request queued
cp_mm_session: Start sent (...)
***ERROR*** A stack overflow in task cp_system_event has been detected.

回溯显示:

panic_abort
esp_system_abort
vApplicationStackOverflowHook
vTaskSwitchContext

栈已经被破坏,因此回溯无法继续还原完整的业务调用链,但任务名明确指向 cp_system_event。

3. 问题原因

3.1 系统事件任务栈较小

系统事件任务配置为 4096 字节:

#ifndef CONFIG_TQ_SYSTEM_EVENT_TASK_STACK
#define CONFIG_TQ_SYSTEM_EVENT_TASK_STACK 4096
#endif

xTaskCreatePinnedToCore(control_plane_system_event_task,
                        "cp_system_event",
                        CONFIG_TQ_SYSTEM_EVENT_TASK_STACK,
                        NULL,
                        4,
                        &s_system_event_task,
                        CONTROL_PLANE_SYSTEM_EVENT_TASK_CORE);

该任务原本只负责读取事件队列、识别按键类型并分发轻量控制命令,不适合承担协议报文构造和同步网络发送。

3.2 首版修复在按键调用栈内执行了 Start

首版会话准备逻辑在登记请求后立即调用推进函数:

ESP_LOGI(TAG, "Cloud chouqian request queued");
control_plane_try_cloud_chouqian_request();
return ESP_OK;

实际调用链变成:

cp_system_event
    -> control_plane_dispatch_key_event
    -> control_plane_request_cloud_chouqian
    -> control_plane_try_cloud_chouqian_request
    -> control_plane_multimodal_send_start
    -> 创建 cJSON 协议对象
    -> 序列化 Start JSON
    -> WebSocket 文本发送与日志格式化

Start 包含音频、下行、voice、client info、workspace 和 app 等参数。虽然 JSON 节点和序列化文本主要使用 heap,但多层函数调用、局部结构体、格式化和网络发送仍会消耗任务栈,最终超过 cp_system_event 的 4096 字节限制。

3.3 单纯扩大系统事件栈不是合适的职责划分

直接扩大 cp_system_event 栈可以缓解当前崩溃,但仍有以下问题:

  • 所有按键和系统事件都被迫为少数网络操作长期预留大栈;
  • WebSocket 发送可能等待锁或网络超时,阻塞其他系统事件;
  • 以后 Start 协议字段增加后仍可能再次逼近栈上限;
  • 事件分发任务承担了不属于它的会话编排职责。

因此最终方案是迁移执行上下文,而不是只增加系统事件任务的栈。

4. 解决方式

4.1 按键任务只登记状态并通知 worker

最终实现不再从 control_plane_request_cloud_chouqian() 直接执行 Start 或 Prompt:

esp_err_t control_plane_request_cloud_chouqian(void)
{
    uint32_t requested_ms = control_plane_now_ms();

    portENTER_CRITICAL(&s_multimodal_runtime_lock);
    if (s_cloud_chouqian_pending) {
        portEXIT_CRITICAL(&s_multimodal_runtime_lock);
        return ESP_ERR_INVALID_STATE;
    }
    s_cloud_chouqian_pending = true;
    s_cloud_chouqian_action_in_progress = false;
    s_cloud_chouqian_prompt_sent = false;
    s_cloud_chouqian_requested_ms = requested_ms;
    s_model_command_dispatched = false;
    portEXIT_CRITICAL(&s_multimodal_runtime_lock);

    ESP_LOGI(TAG, "Cloud chouqian request queued");
    control_plane_wake_multimodal_watchdog();
    return ESP_OK;
}

此时 cp_system_event 只执行:

写入少量受锁保护的状态
    -> 打印 queued 日志
    -> 发送 FreeRTOS task notification
    -> 返回事件循环

4.2 使用 FreeRTOS task notification 唤醒 watchdog

static void control_plane_wake_multimodal_watchdog(void)
{
    TaskHandle_t task =
            get_multimodal_task_handle(&s_multimodal_watchdog_task);
    if (task != NULL) {
        xTaskNotifyGive(task);
    }
}

任务通知不携带大对象,也不需要新建队列。抽签请求的真实状态保存在受 s_multimodal_runtime_lock 保护的静态状态中,通知只表示“需要立即检查一次”。

4.3 watchdog 同时支持通知唤醒和周期检查

原 watchdog 每秒固定延时:

vTaskDelay(tick_interval);

修改后使用带超时的通知等待:

while (!get_multimodal_stop_flag(&s_multimodal_watchdog_stop_requested)) {
    (void)ulTaskNotifyTake(pdTRUE, tick_interval);

    control_plane_multimodal_runtime_state_t state = {0};
    if (control_plane_multimodal_get_runtime_state(&state) != ESP_OK) {
        idle_transport_started_ms = 0U;
        continue;
    }

    uint32_t now_ms = control_plane_now_ms();
    control_plane_try_cloud_chouqian_request();
    control_plane_try_server_success_prompt(&state, now_ms);
    /* 原有 heartbeat、stall 和 idle transport 检查继续执行。 */
}

其语义为:

  • 收到抽签或会话事件通知时立即运行;
  • 没有通知时最多等待 1 秒,继续执行原有周期 watchdog 工作;
  • 通知在 worker 忙碌时会被记录,下一轮循环不会丢失唤醒;
  • pending 状态是业务事实来源,不依赖通知次数表达请求数量。

4.4 会话回调也不直接发送网络请求

TASK_STARTED 和 SERVER_LISTENING 可能发生在 WebSocket 消息处理上下文。最终实现让它们同样只唤醒 watchdog:

case CONTROL_PLANE_MULTIMODAL_EVENT_TASK_STARTED:
    control_plane_wake_multimodal_watchdog();
    break;

case CONTROL_PLANE_MULTIMODAL_EVENT_SERVER_LISTENING:
    control_plane_wake_multimodal_watchdog();
    break;

这样 Start 和 RequestToRespond 始终由同一个 control-plane worker 执行,不会把同步 WebSocket 发送压力转移到另一个事件回调任务。

4.5 增加 watchdog 栈余量

watchdog 现在负责构造和发送 Start、RequestToRespond,因此栈从 6144 字节增加到 8192 字节:

#define CONTROL_PLANE_WATCHDOG_TASK_STACK_SIZE 8192

增加的 2048 字节用于覆盖多模态协议 JSON 构造、日志格式化和 WebSocket 发送调用链。系统事件任务仍保持原来的 4096 字节,不为网络工作长期扩容。

5. 修复后的执行上下文

cp_system_event(4096 字节)
    -> 识别 SW2 SHORT_PRESS
    -> 设置抽签 pending
    -> xTaskNotifyGive(mm_watchdog)
    -> 立即返回

mm_watchdog(8192 字节)
    -> 被通知唤醒
    -> 查询多模态会话状态
    -> 必要时构造并发送 Start
    -> 等待服务端 Listening 通知
    -> 构造并发送 RequestToRespond

系统事件任务与网络发送调用栈已经完全分离。

6. 解决成果

  • cp_system_event 不再执行 Start JSON 构造和 WebSocket 发送;
  • SW2 按键分发能够快速返回,不会阻塞其他系统事件;
  • task-started 和 Listening 回调不再直接执行抽签网络请求;
  • watchdog 通过任务通知即时响应,不引入固定 1 秒操作延迟;
  • watchdog 栈增加到 8192 字节,为新增职责提供明确余量;
  • 原有 heartbeat、下行 stall 恢复和空闲 transport 管理仍按 1 秒周期运行;
  • ESP-IDF v5.5.4 完整构建通过;
  • 生成的应用固件大小为 0x20f450 字节,最小应用分区剩余约 41%。

7. 后续影响与边界

7.1 内存影响

watchdog 栈增加 2048 字节,通常来自 FreeRTOS 可用内部内存。这是本修复最明确的常驻内存成本。相比扩大通用系统事件任务栈,额外内存被分配给实际执行网络协议工作的 owner task,职责更匹配。

7.2 watchdog 唤醒频率

每次抽签排队、task started 和 Listening 事件都会通知 watchdog。通知只增加一次轻量调度,不创建新任务,不分配业务消息内存,也不会改变原有任务优先级。

7.3 原有周期任务行为

ulTaskNotifyTake(pdTRUE, tick_interval) 在无通知时仍以 1 秒超时返回,所以 heartbeat、无响应保护、stall 恢复和 idle transport 关闭逻辑的最大检查间隔没有变长。有通知时这些检查可能提前执行,这是安全的。

7.4 请求可靠性

task notification 只负责唤醒,不负责保存业务数据。即使多次通知合并,s_cloud_chouqian_pending 仍保持为 true,watchdog 的周期检查也会继续推进请求,因此不会因为通知合并而丢失抽签状态。

7.5 仍需关注的验证项

  • 冷启动后首次按 SW2,确认不再出现 cp_system_event 栈溢出;
  • 通过运行时 stack high-water mark 检查 mm_watchdog 的实际余量;
  • 弱网和 WebSocket 发送超时场景下,确认系统事件队列仍能持续处理其他按键;
  • 高频 SW2 和 PTT 交错操作下,确认不会发生重复 Prompt 或异常会话抢占。