9.0 KiB
云端抽签触发系统事件任务栈溢出:问题与修复说明
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 或异常会话抢占。