From 4b7b1949d481eb58882afb64079eebf29228a5d5 Mon Sep 17 00:00:00 2001 From: Rdzleo Date: Thu, 14 May 2026 11:38:48 +0800 Subject: [PATCH] =?UTF-8?q?perf(rtc-only):=20Phase=206=20=E6=94=B6?= =?UTF-8?q?=E5=B0=BE=20-=20=E5=8D=A1=E9=A1=BF=E4=BC=98=E5=8C=96=20+=20Powe?= =?UTF-8?q?rSaveTimer=20=E5=AE=88=E5=8D=AB=20+=20=E5=BC=80=E6=9C=BA?= =?UTF-8?q?=E5=8A=A0=E9=80=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 代码改动: - AudioLoop 加 vTaskDelay(1),让出 Core 1 idle task 防 WiFi/RTC 饥饿 - BackgroundTask 优先级 2 → 5,提升 Opus 解码实时性 - LVGL 刷新 5ms → 16ms (60Hz),CPU 占用降 60% - GIF 定时器 20ms → 33ms (3 处),PSRAM 流量减半 - AI 字幕推送 100ms 节流,避免 LVGL 锁争抢 - EnterIdleHibernate 清空 audio_decode_queue_,防 standby_sound 残留误触发首帧 - PowerSaveTimer OnEnterSleepMode 加 device_state 守卫,拦截 dialog/connecting 期间关功放(修复欢迎语期间被静音 bug) - 取消开机 ADC 阻塞采样,开机播报响应从 6 秒缩到 < 3 秒 新增规划: - Phase 7 占位文档:电量保护 + PowerSaveTimer 重构 + 唤醒杂音根治 + RTC 抖动缓解 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../phases/phase_07_battery_psm/README.md | 97 +++++++++++++++++++ main/application.cc | 55 +++++++---- main/background_task.cc | 4 +- .../movecall_moji_esp32s3.cc | 9 ++ main/dzbj/ai_chat_ui.c | 18 +++- main/dzbj/bg_gif_demo.c | 2 +- main/dzbj/lcd.c | 2 +- 7 files changed, 161 insertions(+), 26 deletions(-) create mode 100644 .planning/milestones/digital_human_rtc/phases/phase_07_battery_psm/README.md diff --git a/.planning/milestones/digital_human_rtc/phases/phase_07_battery_psm/README.md b/.planning/milestones/digital_human_rtc/phases/phase_07_battery_psm/README.md new file mode 100644 index 0000000..4bf40eb --- /dev/null +++ b/.planning/milestones/digital_human_rtc/phases/phase_07_battery_psm/README.md @@ -0,0 +1,97 @@ +# Phase 7:电量保护 + 低功耗管理重构 + +## 背景 + +Phase 6 在调试唤醒杂音过程中,暴露出三个**历史代码的耦合问题**,它们彼此牵连影响 UX: + +1. **开机电量保护**([application.cc:614](../../../../main/application.cc#L614) 原 618-630) + - 同步采样 20 × 10 × 10ms = **6 秒阻塞**才能进入开机播报 + - 电量 ≤ 25% 直接 `SetOutputVolumeRuntime(0)` 静音,没有 UI 提示 + - 无屏 UI 阶段的遗留设计(防止低电压下功放产生噪声) + - **Phase 6 已临时禁用**,恢复开机响应速度 + +2. **PowerSaveTimer 在 dialog/connecting 状态错误关闭功放** + - PowerSaveCheck 状态机 `in_sleep_mode_` 翻转有边角 bug:WakeUp 重置 ticks 但 `in_sleep_mode_` 残留为 true 的路径 + - 历史症状:欢迎语期间 PowerSaveTimer 触发 OnEnterSleepMode → `codec->EnableOutput(false)` → 听不到欢迎语 + - **Phase 6 已加 device_state 守卫拦截**([movecall_moji_esp32s3.cc:259](../../../../main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc#L259)),但只是补丁,根因未除 + +3. **PowerSaveCheck callback 外的 esp_pm_configure 不受守卫保护** + - [power_save_timer.cc:65](../../../../main/boards/common/power_save_timer.cc) callback 后无条件下发 `light_sleep_enable=true` + - 即使守卫拦截关功放,I2C/I2S 总线仍可能因 Light Sleep 被掐 + - 历史症状:唤醒后 codec 通信失败 / I2S DMA 卡死 + +## 目标 + +把上述三块**重构成一个连贯系统**,而非局部打补丁: + +- 异步 + 增量电量监测,移除开机阻塞 +- 屏幕 UI 低电提示(图标 + 文案),替代粗暴静音 +- 分级低电策略(>25% 正常 / 15-25% 降音量 / <15% UI 警告 / <5% 强制 idle) +- PowerSaveTimer 状态机重写,根本性解决 `in_sleep_mode_` 边角 +- esp_pm_configure 调用统一收口到 callback 内部,受 device_state 守卫保护 + +## 范围(暂定,进入 Phase 7 时细化) + +### 7.1 异步电量监测 +- 后台 FreeRTOS task 定时(如 5s 一次)ADC 采样,更新 `battery_level_` 原子变量 +- `GetBatteryLevel()` 立即返回缓存值,开机首次返回 100% 或上次 NVS 持久化值 +- 开机播报不再被电池采样阻塞 + +### 7.2 屏幕低电 UI +- 顶部状态栏电量图标(已有 LVGL 框架支持) +- ≤15% 弹窗"电量不足,请充电",但**不静音**,让用户主动响应 +- ≤5% 才强制进入 idle,配合 Phase 6 hibernate 流程退出 RTC 房间 + +### 7.3 PowerSaveTimer 状态机重写 +- 用清晰的 4 态机:ACTIVE / DIMMING / SLEEPING / WAKING +- `WakeUp()` 同时清 `ticks_` 和 `in_sleep_mode_`,消除"已睡未标记"路径 +- `OnEnterSleepMode` 内部统一调用 `esp_pm_configure`,被 device_state 守卫保护 +- 与 Phase 6 hibernate 状态机协同(不重复进入 sleep) + +### 7.4 PA 启停时机 / 唤醒杂音根治 +- PowerSaveTimer/hibernate 都不应在 dialog 期间关 codec/PA +- 唤醒后 codec EnableOutput → 真实 PCM 到达约有 1 秒空窗,I2S 跑空 DMA → 杂音 +- 候选方案: + - 推迟 EnableOutput(true) 到 OnIncomingAudio 首帧(彻底消除空窗) + - GPIO PA 推迟到首帧 PCM 入队(事件驱动,不用 ramp) + - 用 codec 软静音但**不启用 DAC ramp**(避免之前 23s 爬升副作用),首帧瞬时解 +- 多方案对比并实测后再决定 + +### 7.5 RTC 抖动缓解(音质优化) +- **下行音频编码 G.711A → Opus**: + - 当前 G.711A = 64 kbps,对丢包无 FEC 保护 + - Opus 16 kbps 自带 FEC + DTX,抗丢包/带宽降 4 倍 + - 需要服务端配合切换编解码器 +- **Jitter buffer target 调整**:100ms → 200-300ms + - 用更多缓冲延迟换抗抖动能力 + - 实测当前 buffer_ms 经常被自适应拉到 240-440ms,目标 100ms 偏低 +- **Adaptive jitter buffer**:根据近 10s reor/expand_loss 动态调整 target +- 评估指标:reor 降到 < 200,expand_loss 降到 < 5/2 秒为达标 + +## 当前临时状态(进入 Phase 7 前) + +| 模块 | 临时方案 | 长期方案 | +|---|---|---| +| 开机电量保护 | application.cc 注释,直接用 NVS 音量 | Phase 7.1 + 7.2 | +| PowerSaveTimer 误关功放 | board.cc OnEnterSleepMode 加 device_state 守卫 | Phase 7.3 | +| 唤醒杂音 | 已知短板,~1s 杂音用户可接受 | Phase 7.4 | +| 下行音频抖动 | 接受 reor 700-1800 / expand_loss 20-130 的现状 | Phase 7.5 | +| hibernate 队列残留 | EnterIdleHibernate 清空 audio_decode_queue_ | 保留 | + +## 输入文档 + +- [Phase 6 PLAN.md](../phase_06_idle_hibernate/PLAN.md) - hibernate 流程 +- [Phase 6 HIBERNATE_REPORT.md](../phase_06_idle_hibernate/HIBERNATE_REPORT.md) - 实施记录 +- [音频卡顿_全局资源分析.md](../../../../docs/Rtc_AIavatar/音频卡顿_全局资源分析.md) +- 本次调试笔记(待补充):唤醒杂音 → soft ramp 副作用 → 回退教训 + +## 触发条件 + +进入 Phase 7 的前置条件: +- [ ] Phase 6 hibernate 稳定运行 ≥ 1 周无回归 +- [ ] 用户体验确认开机/休眠/唤醒流程顺畅 +- [ ] 决定是否同步实现电量 UI(依赖屏幕设计稿) + +## 状态 + +🟡 **占位中** - 等待 Phase 6 稳定后启动正式规划。 diff --git a/main/application.cc b/main/application.cc index f2508f5..a6ea689 100644 --- a/main/application.cc +++ b/main/application.cc @@ -611,23 +611,18 @@ void Application::Start() { uplink_resampler_.Configure(16000, 8000); codec->Start(); } - { - int battery_level = 0; - bool charging = false; - bool discharging = false; - if (board.GetBatteryLevel(battery_level, charging, discharging)) { - // 如果电池电量低于25%,则将输出音量设置为0(静音) - if (battery_level <= 25) { - codec->SetOutputVolumeRuntime(0); - } else { - Settings s("audio", false); - int vol = s.GetInt("output_volume", AudioCodec::default_output_volume()); - if (vol <= 0) { - vol = AudioCodec::default_output_volume(); - } - codec->SetOutputVolumeRuntime(vol);// 设置运行时输出音量 - } + // ⚠️ 开机电量保护逻辑临时禁用(Phase 7 重构) + // 原设计:开机同步采样 20×10×10ms ADC 数据 → 电量≤25% 时强制静音 + // 问题:阻塞 6 秒才能播放开机播报,且阈值粗暴无 UI 提示 + // 临时方案:跳过阻塞采样,直接读 NVS 音量设置,恢复开机响应速度 + // 长期方案:见 .planning/milestones/digital_human_rtc/phases/phase_07_battery_psm/ + { + Settings s("audio", false); + int vol = s.GetInt("output_volume", AudioCodec::default_output_volume()); + if (vol <= 0) { + vol = AudioCodec::default_output_volume(); } + codec->SetOutputVolumeRuntime(vol); } // // 在启动阶段创建并运行播放管道以统一输出(开机启动播放管道) @@ -2023,6 +2018,10 @@ void Application::AudioLoop() { if (codec->output_enabled()) { OnAudioOutput(); } + // 卡顿优化 1: 让出 Core 1 idle task(FreeRTOS 100Hz tick = 10ms) + // 避免 busy loop 占满 Core 1,防止 WiFi 中断/RTC 协议栈饥饿 + // OnAudioInput/Output 内部本身处理一个完整 PCM 帧(20ms),10ms 调度间隔够 + vTaskDelay(1); } } @@ -4383,26 +4382,42 @@ void Application::EnterIdleHibernate() { auto display = Board::GetInstance().GetDisplay(); + // [废弃方案] 静音填充曾尝试在此处用 codec->OutputData 填 200ms 静音覆盖 DMA 残留 + // 但实测会让 ES7210 codec 进入卡死状态(连续 10 次重启 ES7210 I2C Open fail) + // 移除该方案,杂音问题需要用其他方式解决(如降低唤醒后初始音量) + // 1. 真退出 RTC 房间(释放 License) // Protocol 基类的虚函数 LeaveRoom 默认回退到 CloseAudioChannel, // VolcRtcProtocol 覆写为 volc_rtc_stop + volc_rtc_destroy + // 注意:LeaveRoom 内部会触发 on_audio_channel_closed_ 回调 → codec EnableOutput(false) if (protocol_) { protocol_->LeaveRoom(); } - // 2. 字幕显示推迟到最后做(此时 LVGL 锁竞争最少)— 见步骤 9 + auto codec = Board::GetInstance().GetAudioCodec(); - // 3. 关闭 codec input/output 让状态机重置 + // 3. 字幕显示推迟到最后做(此时 LVGL 锁竞争最少)— 见步骤 9 + + // 4. 显式关闭 codec input/output 让状态机重置(回调可能已关 output,这里幂等 + 关 input) // 修复 bug:若不关闭,唤醒后 EnableInput(true) 会进入 "已 open" 异常路径 // → esp_codec_dev_set_in_channel_gain ES_ERROR_CHECK 失败 abort - // → ESP32-S3 软重启而不是恢复对话 - auto codec = Board::GetInstance().GetAudioCodec(); if (codec) { ESP_LOGI(TAG, "EnterIdleHibernate: 关闭 codec input/output 重置状态机"); codec->EnableInput(false); codec->EnableOutput(false); } + // 3.5. 清空音频解码队列:阻止 hibernate 之前残留的 standby_sound / AI 半句 PCM + // 在唤醒后的 OnAudioOutput 中被错误"首帧"识别,从而把软静音过早解开。 + { + std::lock_guard lock(mutex_); + if (!audio_decode_queue_.empty()) { + ESP_LOGI(TAG, "EnterIdleHibernate: 清空残留音频队列 size=%zu", + audio_decode_queue_.size()); + audio_decode_queue_.clear(); + } + } + // 4. 关闭录音管道(避免唤醒后重新打开时冲突) if (recorder_pipeline_) { recorder_pipeline_close(recorder_pipeline_); diff --git a/main/background_task.cc b/main/background_task.cc index 9886fc2..a993f45 100644 --- a/main/background_task.cc +++ b/main/background_task.cc @@ -6,10 +6,12 @@ #define TAG "BackgroundTask" BackgroundTask::BackgroundTask(uint32_t stack_size) { + // 卡顿优化 2: priority 2 → 5 + // 避免 AI Opus 解码被 main_loop(pri 4)延迟,提升音频实时性 xTaskCreate([](void* arg) { BackgroundTask* task = (BackgroundTask*)arg; task->BackgroundTaskLoop(); - }, "background_task", stack_size, this, 2, &background_task_handle_); + }, "background_task", stack_size, this, 5, &background_task_handle_); } BackgroundTask::~BackgroundTask() { diff --git a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc index f9ab905..de582ea 100644 --- a/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc +++ b/main/boards/movecall-moji-esp32s3/movecall_moji_esp32s3.cc @@ -257,6 +257,15 @@ public: // 创建 PowerSaveTimer(仅 AI 模式需要) power_save_timer_ = new PowerSaveTimer(240, 10, -1); power_save_timer_->OnEnterSleepMode([this]() { + // 门禁:CanEnterSleepMode 已要求 idle,但 PowerSaveTimer 状态机存在 + // "in_sleep_mode_ 未翻转 + WakeUp 后立即再次进入"的边角情况, + // 历史上曾在 dialog/connecting 期间关功放,导致欢迎语无声。 + auto& app = Application::GetInstance(); + auto state = app.GetDeviceState(); + if (state != kDeviceStateIdle) { + ESP_LOGW(TAG, "PowerSaveTimer 在非 idle 状态(%d)触发,忽略关功放", (int)state); + return; + } ESP_LOGI(TAG, "🔋 进入低功耗模式:CPU降频、Light Sleep启用、功放关闭"); auto codec = GetAudioCodec(); if (codec) { diff --git a/main/dzbj/ai_chat_ui.c b/main/dzbj/ai_chat_ui.c index 864bffb..6a33e80 100644 --- a/main/dzbj/ai_chat_ui.c +++ b/main/dzbj/ai_chat_ui.c @@ -2,6 +2,7 @@ #include "lvgl.h" #include "esp_lvgl_port.h" #include "esp_log.h" +#include "esp_timer.h" // 卡顿优化 5: 字幕节流用 esp_timer_get_time #include // ==================================================================== @@ -174,7 +175,7 @@ void ai_chat_screen_init(void) { // 降低 GIF 定时器频率(10ms→20ms),平衡动画流畅度与 CPU 占用 lv_gif_t *gifobj = (lv_gif_t *)gif_emotion; - lv_timer_set_period(gifobj->timer, 20); + lv_timer_set_period(gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms 减半 PSRAM 流量 // GIF 图标(表情上方居中,45x45) // 表情高89,顶边y=-44.5,icon高45,中心再上移几像素避免重叠 @@ -373,14 +374,14 @@ void ai_chat_set_emotion(const char* emotion) { lv_gif_set_src(gif_emotion, entry->emotion_gif); // set_src 内部会重建 10ms 定时器,重新设置为 50ms 降低 CPU 占用 lv_gif_t *gifobj = (lv_gif_t *)gif_emotion; - lv_timer_set_period(gifobj->timer, 20); + lv_timer_set_period(gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms 减半 PSRAM 流量 gif_animation_paused = false; // 处理叠加图标 if (entry->icon_gif) { lv_gif_set_src(gif_icon, entry->icon_gif); lv_gif_t *icon_gifobj = (lv_gif_t *)gif_icon; - lv_timer_set_period(icon_gifobj->timer, 20); + lv_timer_set_period(icon_gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms lv_obj_clear_flag(gif_icon, LV_OBJ_FLAG_HIDDEN); } else { // 隐藏图标时暂停其定时器,避免空跑浪费 CPU @@ -425,6 +426,17 @@ void ai_chat_set_chat_message(const char* role, const char* content) { return; } + // 卡顿优化 5: 100ms 最小更新间隔(防抖) + // AI 流式 TTS 字幕每秒 5-15 次推送,节流后最多每秒 10 次 + // 减少 PSRAM 写入流量 5-10 倍(chat_label 重绘) + // 例外:空内容(清空字幕)不节流,立即响应 + static int64_t last_update_us = 0; + int64_t now_us = esp_timer_get_time(); + if (content[0] != '\0' && (now_us - last_update_us) < 100000) { // 100ms + return; + } + last_update_us = now_us; + if (!lvgl_port_lock(500)) { // 200ms → 500ms(GIF 解码繁忙时给予更长等待) ESP_LOGW(TAG, "LVGL锁超时,跳过字幕更新"); return; diff --git a/main/dzbj/bg_gif_demo.c b/main/dzbj/bg_gif_demo.c index 815f9c4..949b13e 100644 --- a/main/dzbj/bg_gif_demo.c +++ b/main/dzbj/bg_gif_demo.c @@ -215,7 +215,7 @@ esp_err_t bg_gif_demo_switch_gif(const char *new_gif_path) { // (CLAUDE.md "lv_gif_set_src 会重建定时器" 经验) lv_gif_t *gifobj = (lv_gif_t *)g_gif_obj; if (gifobj->timer) { - lv_timer_set_period(gifobj->timer, 20); + lv_timer_set_period(gifobj->timer, 33); // 卡顿优化 3: 20ms→33ms 减半 PSRAM 流量 } lvgl_port_unlock(); diff --git a/main/dzbj/lcd.c b/main/dzbj/lcd.c index 938d6d7..c59fe12 100644 --- a/main/dzbj/lcd.c +++ b/main/dzbj/lcd.c @@ -330,7 +330,7 @@ void lvgl_lcd_init(){ .task_stack = 8192, .task_affinity = -1, .task_max_sleep_ms = 500, - .timer_period_ms = 5 + .timer_period_ms = 16 // 卡顿优化 4: 5ms→16ms (60Hz) 减少 LVGL CPU 占用 60% }; lvgl_port_init(&lvgl_cfg);