工程:E:\ESP\StackChanSample(ESP-IDF 工程,ESP32-S3)
目标:把 StackChan Avatar + 小智(xiaozhi-esp32)整合跑通,修复编译/链接/运行时问题,并把对话气泡接到 Avatar 上显示中文。


1. 背景与工程结构

这个仓库本质是“主工程 + 引入 xiaozhi-esp32 的 main 目录代码 + StackChan 自己的 hal/stackchan/assets”等混合构建。

关键目录:

  • main/:本工程自己的组件(包含 hal/stackchan/assets/main.cpp 等)
  • xiaozhi-esp32/main/:小智的核心实现(显示、音频、MCP、资源系统等)
  • managed_components/:组件依赖(如 esp_videoesp_codec_dev 等)

2. 问题时间线(按出现顺序)

2.1 链接错误:GetHAL() / Hal::init() undefined reference

现象

  • 链接阶段报 undefined reference,典型原因是:实现文件存在,但没有被编译进当前组件/目标。

定位

  • GetHAL() 定义在 main/hal/hal.cpp,声明在 main/hal/hal.h
  • 重点检查 main/CMakeLists.txt 是否把 main/hal/*.cpp 收进 idf_component_register(SRCS ...)

修复思路

  • 修正 file(GLOB_RECURSE ...) 的源文件收集方式,确保 hal/*.c|cc|cpp 被编译进 main 组件

2.2 编译错误:StackChanAvatarDisplay 是抽象类,无法 new

现象

  • new StackChanAvatarDisplay(...) 报“抽象类无法实例化”。

根因

  • StackChanAvatarDisplay 继承 LvglDisplay
  • LvglDisplay 有纯虚:Lock() / Unlock()
    不实现就会变抽象类

修复

  • main/hal/board/stackchan_display.h/.cc 实现:
    • Lock()lvgl_port_lock(timeout)
    • Unlock()lvgl_port_unlock()

2.3 编译错误:StackChan 抽象类(缺纯虚函数实现)

现象

  • static stackchan::StackChan stackchan; 报抽象类。

根因

  • Modifiable 接口里有 hasAvatar() 等纯虚
  • StackChan 没实现就会抽象

修复

  • main/stackchan/stackchan.h 补齐 hasAvatar()(以及后续逐步恢复 attachAvatar/avatar()/update()/modifier pool 等能力)

2.4 编译错误:DefaultAvatar 未声明(命名空间/包含)

现象

  • DefaultAvatar 找不到。

根因

  • 类型实际在 main/stackchan/avatar/skins/default/default.h
  • 命名空间:stackchan::avatar::DefaultAvatar

修复

  • #include <.../default.h>
  • 或使用 using namespace stackchan::avatar;
  • 或全限定名 stackchan::avatar::DefaultAvatar

2.5 链接错误:_binary_camera_shutter_ogg_start/end 未定义

现象

  • 链接时报:
    • _binary_camera_shutter_ogg_start
    • _binary_camera_shutter_ogg_end

根因

  • main/assets/assets.h 里用 asm("_binary_camera_shutter_ogg_start") 声明了嵌入资源符号
  • main/assets/sfx/camera_shutter.ogg 没有加入 idf_component_register(EMBED_FILES ...)
  • 所以不会生成对应的 *.ogg.S.obj

修复

  • main/CMakeLists.txt 增加:
    • file(GLOB STACKCHAN_SFX_SOUNDS ${CMAKE_CURRENT_SOURCE_DIR}/assets/sfx/*.ogg)
    • EMBED_FILES ... ${STACKCHAN_SFX_SOUNDS}

2.6 运行时崩溃:Guru Meditation LoadStoreAlignment(MCP ParseCapabilities)

现象

  • 崩溃点:McpServer::ParseCapabilities() 调用 camera->SetExplainUrl(...)
  • EXCVADDR 是一个“诡异的非对齐地址”

根因(非常典型)

  • 板级类里 camera_ 指针没初始化(垃圾值)
  • GetCamera() 返回了随机地址 → “看起来非空” → 立刻调用虚函数/成员 → 对齐异常崩溃

修复

  • 在板级实现 main/hal/board/stackchansample.cc
    • camera_ 等指针成员默认初始化为 nullptr
    • 若要启用相机,再显式调用 InitializeCamera()camera_ 赋值

2.7 音频日志:i2s_channel_disable(): the channel has not been enabled yet

现象

  • 串口反复出现 I2S “disable 未 enable 通道”的报错。

关键理解

  • Audio 通常同时用两条总线:
    • I2C:控制 codec 寄存器(音量/增益/模式)
    • I2S:真正的 PCM 音频数据通道(播放/录音数据流)
  • 这条报错多发生在“内部为了重配/关闭先 disable 一下”,但通道当时未处于 enable 状态,属于非致命的噪声日志。

处理方式(消噪)

  • esp_codec_dev 的 I2S 适配层里(managed_components/espressif__esp_codec_dev/platform/audio_codec_data_i2s.c
    让 disable 变成幂等:如果当前方向本来就没 enable,就直接返回 OK,不再调用底层 i2s_channel_disable()

2.8 对话气泡中文是“方块/乱码”

现象

  • 气泡里中文显示成方块(缺字形)。

根因

  • DefaultAvatar::init(..., font = &lv_font_montserrat_16) 默认用的是 Montserrat(基本无中文字库)
  • 气泡 Label 在创建时是 setTextFont(font) 固定死的,不会自动跟随主题字体

修复

  • 正确做法:实现并接入 Display::SetTheme(Theme*) 的重载,在主题切换/资源刷新时,把 LvglThemetext_font() 同步到 StackChan Avatar 的气泡 Label(setSpeechTextFont()),这样才能彻底消除方块字并保证后续主题变更也正常生效。
  • 仅在创建 Avatar 时手动传入某个内置字体(例如 BUILTIN_TEXT_FONT)只能作为“启动阶段兜底”,字符覆盖不一定完整,无法保证对话内容里所有汉字/符号都不变方块。

2.9 链接错误:StackChanAvatarDisplay::SetStatus/SetEmotion/SetTheme/SetPreviewImage undefined reference

现象

  • 链接阶段 vtable 报未定义引用:
    • SetStatus
    • SetEmotion
    • SetTheme
    • SetPreviewImage

根因

  • 头文件里声明了 override,但 .cc 没有对应实现 → vtable 需要符号,链接失败。

修复

  • main/hal/board/stackchan_display.cc 补齐这些成员函数实现:
    • SetStatus():复用 LvglDisplay::SetStatus
    • SetEmotion():字符串 → stackchan::avatar::Emotion 映射 → avatar.setEmotion(...)
    • SetTheme():保存主题 + 把主题字体下发给气泡(setSpeechTextFont(...)
    • SetPreviewImage():显示/隐藏预览图,配合定时器自动隐藏

3. 关键机制解释(把“为什么”讲清楚)

3.1 为什么 I2C + I2S 会同时存在?

  • I2C:控制通道(写寄存器、改音量、开关输入/输出)
  • I2S:数据通道(真正搬运音频采样)
  • 所以“音频 codec 用 I2C”并不等于“不会出现 I2S API”,I2S API 反而是播放/录音的核心。

3.2 SetTheme() 到底在哪被调用?

两条主路径:

  1. MCP 工具调用
  • xiaozhi-esp32/main/mcp_server.cc 注册 self.screen.set_theme
  • 工具被调用时执行 display->SetTheme(theme)
  1. 资产/皮肤应用后刷新
  • xiaozhi-esp32/main/assets.cc 应用皮肤后会 display->SetTheme(current_theme) 刷新显示

这也是为什么你写了 SetTheme() 后,气泡字体能跟随主题变正常:因为这两条路径会触发你同步更新气泡字体。


4. 最终效果(我们达成了什么)

  • 编译/链接层面:修复多个 undefined reference(HAL、资源嵌入、vtable 虚函数)
  • 运行时稳定性:修复 camera 指针未初始化导致的对齐崩溃
  • UI 体验:对话气泡中文不再显示方块;主题切换时气泡字体能同步更新
  • 日志质量:消除 I2S disable 的误报噪声(避免串口被刷屏)

5. 经验总结(适合写在博客结尾)

  • ESP-IDF 链接错误优先查“源文件有没有进组件”,而不是怀疑函数没写
  • C++ vtable 链接错误几乎都来自:声明了 virtual/override 但漏实现
  • 板级指针成员必须初始化nullptr),否则“看起来能跑”但必炸(而且炸得很诡异)
  • 字体方块 = 字库缺 glyph,LVGL 不会“自动造字”,需要显式选对字体
  • 音频 I2C≠音频数据链路,数据流是 I2S;控制才是 I2C

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐