258 lines
9.0 KiB
Markdown
258 lines
9.0 KiB
Markdown
# 云端抽签触发系统事件任务栈溢出:问题与修复说明
|
||
|
||
## 1. 对应提交
|
||
|
||
```text
|
||
d386de2 fix(multimodal): prepare session for cloud fortune draw
|
||
```
|
||
|
||
本文说明首次抽签能够自动创建多模态会话后,按下 SW2 导致 `cp_system_event` 任务栈溢出的问题,以及最终采用的异步 worker 方案。
|
||
|
||
## 2. 问题现象
|
||
|
||
修复“首次抽签缺少会话”后,设备按下 SW2 可以进入 `Start` 发送,但随即触发 FreeRTOS 栈保护:
|
||
|
||
```text
|
||
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.
|
||
```
|
||
|
||
回溯显示:
|
||
|
||
```text
|
||
panic_abort
|
||
esp_system_abort
|
||
vApplicationStackOverflowHook
|
||
vTaskSwitchContext
|
||
```
|
||
|
||
栈已经被破坏,因此回溯无法继续还原完整的业务调用链,但任务名明确指向 `cp_system_event`。
|
||
|
||
## 3. 问题原因
|
||
|
||
### 3.1 系统事件任务栈较小
|
||
|
||
系统事件任务配置为 4096 字节:
|
||
|
||
```c
|
||
#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
|
||
|
||
首版会话准备逻辑在登记请求后立即调用推进函数:
|
||
|
||
```c
|
||
ESP_LOGI(TAG, "Cloud chouqian request queued");
|
||
control_plane_try_cloud_chouqian_request();
|
||
return ESP_OK;
|
||
```
|
||
|
||
实际调用链变成:
|
||
|
||
```text
|
||
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:
|
||
|
||
```c
|
||
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` 只执行:
|
||
|
||
```text
|
||
写入少量受锁保护的状态
|
||
-> 打印 queued 日志
|
||
-> 发送 FreeRTOS task notification
|
||
-> 返回事件循环
|
||
```
|
||
|
||
### 4.2 使用 FreeRTOS task notification 唤醒 watchdog
|
||
|
||
```c
|
||
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 每秒固定延时:
|
||
|
||
```c
|
||
vTaskDelay(tick_interval);
|
||
```
|
||
|
||
修改后使用带超时的通知等待:
|
||
|
||
```c
|
||
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:
|
||
|
||
```c
|
||
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 字节:
|
||
|
||
```c
|
||
#define CONTROL_PLANE_WATCHDOG_TASK_STACK_SIZE 8192
|
||
```
|
||
|
||
增加的 2048 字节用于覆盖多模态协议 JSON 构造、日志格式化和 WebSocket 发送调用链。系统事件任务仍保持原来的 4096 字节,不为网络工作长期扩容。
|
||
|
||
## 5. 修复后的执行上下文
|
||
|
||
```text
|
||
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 或异常会话抢占。
|
||
|