跳到主要内容

第 10 章 构建系统与辅助资源

前 9 章覆盖的是 main/ 下跑在 ESP32 上的运行时代码。本章是收尾——把分散在仓库根目录的 5 类辅助资源讲清楚:CMake 与 Kconfig(怎么从源码变成 .bin)、partitions/(Flash 怎么布局)、scripts/(Python 工具链)、docs/(设计文档与接线图)、idf_component.yml(依赖管理)。


10.1 构建系统总览

                  ┌─────────────────────────────────┐
│ 开发者执行 idf.py build │
└────────────┬────────────────────┘

┌────────────────┴────────────────┐
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ CMakeLists.txt │ │ sdkconfig │
│ (项目根 + main/) │ ←──→ │ Kconfig 生成 │
└──────┬──────────────┘ └──────────────────────┘

│ 触发

┌─────────────────────────────────────────────┐
│ esp-idf CMake 子系统(cmake/build.cmake) │
│ │
│ 1. 解析 idf_component.yml 拉组件 │
│ 2. 编译 main 组件 + 所有依赖组件 │
│ 3. 跑 build_default_assets.py 生成 assets │
│ 4. 链接 ELF │
│ 5. 用 partitions/*.csv 生成 partition-table │
│ 6. esptool merge-bin 合 .bin │
└────────────┬─────────────────────────────────┘


┌─────────────────────────────────────────────┐
│ build/ │
│ ├── xiaozhi.elf │
│ ├── xiaozhi.bin (固件) │
│ ├── partition-table/partition-table.bin │
│ ├── bootloader/bootloader.bin │
│ ├── srmodels/srmodels.bin (assets 候选) │
│ ├── generated_assets.bin (assets 实际) │
│ └── merged-binary.bin (一键烧录用) │
└─────────────────────────────────────────────┘

涉及的入口文件:

文件角色
CMakeLists.txt(根)声明项目名 + PROJECT_VER + 包含 $ENV{IDF_PATH}/tools/cmake/project.cmake
main/CMakeLists.txt真正的"项目入口":列源文件 + 根据 BOARD_TYPE 添加板子专属代码 + 注册 partition 烧录命令
main/Kconfig.projbuild板子选项 / 语言选项 / 字体选项 / OTA URL 等可视化配置
main/idf_component.yml列依赖的第三方组件(espressif/esp_lvgl_port、espressif__esp-sr、xiaozhi-fonts 等)和版本
partitions/v{1,2}/*.csvFlash 分区表
sdkconfig.defaults.*不同芯片型号的默认 Kconfig 值

10.2 main/CMakeLists.txt —— 板子选择的大门

837 行(去掉 Kconfig 部分),核心结构:

10.2.1 第 1 段:通用源文件列表(第 1-46 行)

set(SOURCES "audio/audio_codec.cc"
"audio/audio_service.cc"
"audio/codecs/no_audio_codec.cc"
"audio/codecs/box_audio_codec.cc"
...
"audio/processors/audio_debugger.cc"
"led/single_led.cc"
"led/circular_strip.cc"
"led/gpio_led.cc"
"display/display.cc"
"display/lcd_display.cc"
...
"protocols/protocol.cc"
"protocols/mqtt_protocol.cc"
"protocols/websocket_protocol.cc"
"mcp_server.cc"
"system_info.cc"
"application.cc"
"ota.cc"
"settings.cc"
"device_state_machine.cc"
"assets.cc"
"main.cc"
)

set(INCLUDE_DIRS "." "display" "display/lvgl_display" "audio" "protocols")

# 板级共享文件(button.cc、backlight.cc、power_save_timer.cc 等)
file(GLOB BOARD_COMMON_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/boards/common/*.cc)
list(APPEND SOURCES ${BOARD_COMMON_SOURCES})
list(APPEND INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/boards/common)

所有 110 个板子都共享上面这套file(GLOB ...) 自动把 boards/common/ 下所有 .cc 全打进去——加新的 common 工具时只要丢进去,不用改 CMakeLists。

10.2.2 第 2 段:BOARD_TYPE 派发(第 67-660 行)

110 个 if-elseif-endif 分支,每个分支:

elseif(CONFIG_BOARD_TYPE_ESP_BOX_3)
set(BOARD_TYPE "esp-box-3")
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)

设置 4 个变量:

变量用途
BOARD_TYPE拼接路径 main/boards/<BOARD_TYPE>/<BOARD_TYPE>_board.cc
BUILTIN_TEXT_FONT编译进固件的中文字体(不依赖 assets 分区也能显示)
BUILTIN_ICON_FONT编译进固件的 Font Awesome 图标字体
DEFAULT_EMOJI_COLLECTION默认 emoji 集合(彩屏可用 twemoji_64,OLED 用 none

字体之所以可选是因为:

  • 大屏板用 20pt 字(可读性优先)+ Twemoji 64×64 彩色 emoji;
  • 小屏板用 14pt 字(128×64 OLED 屏太小)+ 文字 emoji 代替;
  • 这俩字体直接 .a 链进固件,assets.bin 即使损坏也能显示界面——容错设计。

随后:

list(APPEND SOURCES "boards/${BOARD_TYPE}/${BOARD_TYPE}_board.cc")
list(APPEND INCLUDE_DIRS "boards/${BOARD_TYPE}")

把对应板子的 <name>_board.cc 加入编译,把板子目录加入 include path(config.h 能被找到)。

10.2.3 第 3 段:动态查找组件路径(第 760-769 行)

find_component_by_pattern("espressif__esp-sr" ESP_SR_COMPONENT ESP_SR_COMPONENT_PATH)
if(ESP_SR_COMPONENT_PATH)
set(ESP_SR_MODEL_PATH "${ESP_SR_COMPONENT_PATH}/model")
endif()

find_component_by_pattern("xiaozhi-fonts" XIAOZHI_FONTS_COMPONENT XIAOZHI_FONTS_COMPONENT_PATH)
if(XIAOZHI_FONTS_COMPONENT_PATH)
set(XIAOZHI_FONTS_PATH "${XIAOZHI_FONTS_COMPONENT_PATH}")
endif()

ESP-IDF 组件被下载到 managed_components/ 目录,但名字带 hash 和版本号(如 espressif__esp-sr_2.1.5),具体路径事先不知道。find_component_by_pattern 在第 50-59 行定义,遍历 BUILD_COMPONENTS 找匹配。

得到组件路径后,提取 model/ 子目录(语音模型)和字体目录给后面 build_default_assets.py 用。

10.2.4 第 4 段:构建 assets.bin(第 810-857 行)

function(build_default_assets_bin)
set(GENERATED_ASSETS_BIN "${CMAKE_BINARY_DIR}/generated_assets.bin")

set(BUILD_ARGS
"--sdkconfig" "${SDKCONFIG}"
"--output" "${GENERATED_ASSETS_BIN}"
)
if(BUILTIN_TEXT_FONT) list(APPEND BUILD_ARGS "--builtin_text_font" "${BUILTIN_TEXT_FONT}") endif()
if(DEFAULT_EMOJI_COLLECTION) list(APPEND BUILD_ARGS "--emoji_collection" "${DEFAULT_EMOJI_COLLECTION}") endif()
if(DEFAULT_ASSETS_EXTRA_FILES) list(APPEND BUILD_ARGS "--extra_files" "${DEFAULT_ASSETS_EXTRA_FILES}") endif()
list(APPEND BUILD_ARGS "--esp_sr_model_path" "${ESP_SR_MODEL_PATH}")
list(APPEND BUILD_ARGS "--xiaozhi_fonts_path" "${XIAOZHI_FONTS_PATH}")

add_custom_command(
OUTPUT ${GENERATED_ASSETS_BIN}
COMMAND python ${PROJECT_DIR}/scripts/build_default_assets.py ${BUILD_ARGS}
DEPENDS ${SDKCONFIG} ${PROJECT_DIR}/scripts/build_default_assets.py
COMMENT "Building default assets.bin based on configuration"
VERBATIM
)
add_custom_target(generated_default_assets ALL DEPENDS ${GENERATED_ASSETS_BIN})
endfunction()

CMake 调用 Python 脚本(build_default_assets.py)把语音模型 + 字体 + emoji 打包成 generated_assets.bin,烧到 assets 分区。这是把"运行时静态数据"和"代码"分开的关键——固件更新换 xiaozhi.bin,资源更新换 assets.bin

add_custom_commandDEPENDS sdkconfig 表示:sdkconfig 改了(比如换字体)会重新跑脚本,否则缓存。

10.2.5 第 5 段:分区表条件烧录(第 917-936 行)

partition_table_get_partition_info(size "--partition-name assets" "size")
partition_table_get_partition_info(offset "--partition-name assets" "offset")
if ("${size}" AND "${offset}")
# v2 分区表有 assets 分区
if(CONFIG_FLASH_DEFAULT_ASSETS)
build_default_assets_bin()
esptool_py_flash_to_partition(flash "assets" "${GENERATED_ASSETS_LOCAL_FILE}")
elseif(CONFIG_FLASH_CUSTOM_ASSETS)
# 用户提供自己的 assets.bin(本地路径或 URL)
get_assets_local_file("${CONFIG_CUSTOM_ASSETS_FILE}" ASSETS_LOCAL_FILE)
esptool_py_flash_to_partition(flash "assets" "${ASSETS_LOCAL_FILE}")
elseif(CONFIG_FLASH_NONE_ASSETS)
message(STATUS "Assets flashing disabled")
endif()
else()
# v1 分区表没 assets 分区
message(STATUS "Assets partition not found, using v1 partition table")
endif()

3 个 Kconfig 分支:

  1. FLASH_DEFAULT_ASSETS:跑 Python 脚本自动生成;
  2. FLASH_CUSTOM_ASSETS:用户填 CUSTOM_ASSETS_FILE 路径或 URL,直接烧入;
  3. FLASH_NONE_ASSETS:不烧(用户自己 idf.py flash 别的)。

v1 分区表(老板子)没有 assets 分区,自动跳过——向后兼容。

10.2.6 特殊板子:在线下载 emoji(第 771-806 行)

if(CONFIG_BOARD_TYPE_ESP_HI)
set(URL "https://github.com/espressif2022/image_player/raw/main/test_apps/test_8bit")
set(FILES_TO_DOWNLOAD "Anger_enter.aaf" "Anger_loop.aaf" ...)
foreach(FILENAME IN LISTS FILES_TO_DOWNLOAD)
if(EXISTS ${LOCAL_FILE})
message(STATUS "File ${FILENAME} already exists, skipping")
else()
file(DOWNLOAD ${REMOTE_FILE} ${LOCAL_FILE} STATUS DOWNLOAD_STATUS)
...
endif()
endforeach()
endif()

esp-hi 这块板子的 EmoteEngine 资源(.aaf 动画文件,27 个)太大不放仓库里,构建时按需从 GitHub 拉。CMake file(DOWNLOAD) 是构建期下载,跟 OTA 完全无关。


10.3 Kconfig.projbuild —— 编译时配置入口

menu "Xiaozhi Assistant"
├── OTA_URL (字符串,默认 https://api.tenclass.net/xiaozhi/ota/)
├── FLASH_DEFAULT_ASSETS / FLASH_CUSTOM_ASSETS / FLASH_NONE_ASSETS
├── CUSTOM_ASSETS_FILE (字符串,自定义 assets 路径)
├── DEFAULT_LANGUAGE (40+ 语言可选)
├── BOARD_TYPE (110+ 板子可选)
├── 板子相关子选项(OLED 类型、4G 模块型号、麦克风类型等)
├── USE_HOTSPOT_WIFI_PROVISIONING (bool, 默认 y)
├── USE_ESP_BLUFI_WIFI_PROVISIONING (bool, 默认 n)
├── USE_ACOUSTIC_WIFI_PROVISIONING (bool, 默认 n)
├── ENABLE_AUDIO_DEBUGGER (bool, 默认 n)
├── AUDIO_DEBUG_SERVER_IP (字符串)
├── WAKE_WORD_TYPE (none / afe / esp_sr / custom)
└── ...

menuconfig 把这些做成 ncurses 菜单,开发者勾选 → 生成 sdkconfig 文件 → CMake 读 sdkconfig → 决定编译哪些代码。

10.3.1 Kconfig 设计模式

choice
prompt "Board Type"
default BOARD_TYPE_BREAD_COMPACT_WIFI

config BOARD_TYPE_BREAD_COMPACT_WIFI
bool "面包板 (WiFi)"
depends on IDF_TARGET_ESP32S3
config BOARD_TYPE_BREAD_COMPACT_ML307
bool "面包板 (ML307 4G)"
depends on IDF_TARGET_ESP32S3
...
endchoice
  • choice/endchoice 块表示互斥单选
  • depends on IDF_TARGET_ESP32S3 表示只有 ESP32-S3 目标芯片才能选
  • 选中的会被 sdkconfig 设成 CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI=y,其它的 =n
  • CMakeLists 用 if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI) 取这个 flag。

10.3.2 条件子菜单

config OLED_SSD1306_128X32
bool "SSD1306 128x32"
depends on BOARD_TYPE_BREAD_COMPACT_WIFI || BOARD_TYPE_BREAD_COMPACT_ML307

只有选了面包板才会出现 OLED 子选项——避免菜单噪音。

10.3.3 唤醒词类型

choice
prompt "Wake Word Type"
default WAKE_WORD_AFE
config WAKE_WORD_NONE
bool "No Wake Word"
config WAKE_WORD_AFE
bool "AFE Wake Word (recommended)"
config WAKE_WORD_ESP
bool "ESP Wake Word (single-channel)"
config WAKE_WORD_CUSTOM
bool "Custom Wake Word (Sherpa-ONNX)"
endchoice

第 4 章讲过,4 种唤醒词类型在编译期挑一个——避免不必要的代码进入固件。


10.4 partitions/ —— Flash 分区表

ESP32 的 Flash 是按分区表布局的,分区表本身也是个固定地址(0x8000)的小数据块。

10.4.1 v1 vs v2

v1(老版本,无 assets 分区)

# Name,   Type, SubType, Offset,  Size, Flags
nvs, data, nvs, 0x9000, 0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x3f0000,
ota_1, app, ota_1, , 0x3f0000,

5 个分区:nvs(16KB) / otadata(8KB) / phy_init(4KB) / ota_0(约 4MB) / ota_1(约 4MB)。资源数据要么编译进固件,要么用 SPIFFS 文件系统挂在 ota 分区某处——但小智项目从 v1 升 v2 后改成专门的 assets 分区。

v2(新版本,含 assets 分区)——partitions/v2/16m.csv

nvs,      data, nvs,     0x9000,    0x4000,
otadata, data, ota, 0xd000, 0x2000,
phy_init, data, phy, 0xf000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x3f0000,
ota_1, app, ota_1, , 0x3f0000,
assets, data, spiffs, 0x800000, 8M

新增 8MB 的 assets 分区(SubType 标 spiffs 是为了让分区表工具识别成数据分区,实际上代码用 raw flash 访问,不挂 SPIFFS)。第 7 章讲过 Assets::Apply()esp_partition_mmap 直接把分区映射到 CPU 地址空间,零拷贝访问。

10.4.2 不同容量的分区表

partitions/v2/ 下:

文件适用OTA 分区大小Assets 大小
4m.csv4MB Flash(如 esp-hi)factory only, ~2.5MB1.5MB
8m.csv8MB Flash~3MB × 21MB
16m.csv16MB Flash(主流)~4MB × 28MB
16m_c3.csvESP32-C3 16MB优化布局8MB
32m.csv32MB Flash(高端)~12MB × 28MB

4MB Flash 的 4m.csv 没有 OTA 双分区——空间太紧,只用 factory 分区,不能 OTA 升级,必须 USB 重新烧录。

注意 , , 中间留空:CMake 工具会自动填上一个分区的 offset+size,所以 ota_1 的 offset 自动是 0x410000(0x20000+0x3f0000)。

10.4.3 切换分区表的方法

sdkconfig 里:

CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/16m.csv"

由板子的 sdkconfig.defaults 默认设置或 menuconfig 手动改。


10.5 idf_component.yml —— 第三方组件依赖

dependencies:
espressif/cjson: "*"
espressif/esp-sr: "==2.1.5"
espressif/esp_lvgl_port: "==2.6.1"
espressif/esp_codec_dev: "^1.0.0"
espressif/button: "==4.1.4"
espressif/esp_lcd_panel_io_additions: "==1.0.0"
espressif/esp_audio_codec: "==2.3.0"
78__xiaozhi-fonts: "==0.4.0"
...

idf.py reconfigure 时 ESP-IDF Component Manager 会:

  1. 解析 yml;
  2. https://components.espressif.com 下载对应版本;
  3. 解压到 managed_components/<owner>__<name>_<version>/
  4. 把它们加入构建(每个组件自己有 CMakeLists.txt)。
组件角色
cjson第 6 章 MCP JSON 解析、第 5 章协议 JSON
esp-sr第 4 章 AFE / WakeNet / 语音模型
esp_lvgl_port第 8 章 LCD/OLED 的 LVGL 集成
esp_codec_dev音频 codec 抽象(ES8311 等)
button第 9 章 Button 类的底层(iot_button
esp_audio_codecOpus 编解码器(第 4 章用到的 opus_encoder 等)
xiaozhi-fonts自家维护的字体集合(普惠 / Font Awesome)

版本固定(==:保证不同人编译出来一致;^ 表示允许补丁版本变化。


10.6 scripts/ —— 配套 Python 工具链

13 个 Python 脚本/目录,按用途分 5 类。

10.6.1 构建辅助

build_default_assets.py(883 行) —— 上面 10.2.4 提到的"打包 assets.bin"脚本。流程:

  1. 解析 sdkconfig 找选了哪些 emoji / 字体 / 语言;
  2. pack_models() 把 esp-sr 的 wn9_nihaoxiaozhi.bin 等模型按特定格式打包成 srmodels.bin
  3. 拷贝字体文件(font_puhui_basic_14_1.bin 等)到临时目录;
  4. gen_lang.py 生成 lang_config.h(语言资源 C 头文件);
  5. srmodels.bin + 字体 + emoji + index.jsonspiffs_assets_gen.py 打成单一二进制;
  6. 输出 generated_assets.bin

关键技巧pack_models() 内手写二进制格式(不用 SPIFFS),是因为 SPIFFS 有元数据开销,对只读资源直接平铺 name + size + offset + data 更省。第 7 章 Assets::InitializePartition() 解析的就是这个格式。

gen_lang.py(187 行) —— 把 assets/locales/<lang>/language.json 转成 lang_config.h

namespace Lang {
constexpr const char* CODE = "zh-CN";
namespace Strings {
constexpr std::string_view VOLUME = "音量: ";
constexpr std::string_view MAX_VOLUME = "音量已最大";
...
}
namespace Sounds {
constexpr std::string_view OGG_WIFICONFIG = "/wificonfig.ogg";
...
}
}

constexpr std::string_view

  • 编译期常量,零运行时开销;
  • const char* 多个长度信息,避免 strlen;
  • 字符串直接放 Flash(.rodata),不占 RAM。

fallback 机制:当前语言缺某个 key 时用 en-US 的——保证不会显示空字符串。

versions.py —— 处理 PROJECT_VER 的 bump 逻辑(patch / minor / major)。

10.6.2 发布与打包

release.py(300+ 行) —— 批量编译所有板子变体 + 打 zip 发布包。流程:

  1. main/boards/<board>/config.json 列出 builds(一个板子可能有多个变体,如不同语言、不同麦克风方向);
  2. 对每个变体:
    • 写 sdkconfig append 块(CONFIG_BOARD_TYPE_xxx=y + 变体专属 sdkconfig);
    • idf.py set-target esp32s3idf.py -DBOARD_NAME=... build
    • idf.py merge-bin 合成 merged-binary.bin
    • 压缩成 releases/v1.5.0_<variant>.zip
  3. 跳过已存在的 zip(增量构建)。

关键代码——10.2 节提到的 _AUTO_SELECT_RULES

_AUTO_SELECT_RULES = {
"CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING": [
"CONFIG_BT_ENABLED=y",
"CONFIG_BT_BLUEDROID_ENABLED=y",
"CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y",
"CONFIG_BT_BLE_50_FEATURES_SUPPORTED=n",
"CONFIG_BT_BLE_BLUFI_ENABLE=y",
"CONFIG_MBEDTLS_DHM_C=y",
],
}

:Kconfig 的 select 关键字只在 menuconfig 交互式选择时自动应用依赖。release.py 是非交互式的——直接 append CONFIG_USE_ESP_BLUFI=y 不会自动开启它依赖的蓝牙栈。所以脚本里手动写一份依赖映射,保证 BluFi 配网编译时蓝牙栈也开。

download_github_runs.py —— 从 GitHub Actions 拉构建产物,方便 CI 协作。

10.6.3 资源转换

p3_tools/(音频 P3 格式工具)

  • convert_audio_to_p3.py:MP3/WAV → 自定义 P3 格式(Opus + 头);
  • convert_p3_to_audio.py:反向解码;
  • play_p3.py:本地播放 P3 文件验证;
  • batch_convert_gui.py:拖拽式 GUI 批量转换。

P3 格式(10.6.3 用到的):

每帧:[1B type=0, 1B reserved=0, 2B opus_len BE] [Opus payload...]

type=0 是普通音频帧,预留 type 字段可以扩展(控制命令、静音帧、关键帧等)。第 5 章 BinaryProtocol2/BinaryProtocol3 的雏形就是这个。

编码细节 —— convert_audio_to_p3.py

  1. librosa.load 加载任意格式音频;
  2. 立体声 → 单声道(librosa.to_mono);
  3. 可选 LUFS 响度归一化(pyloudnorm)—— 让所有提示音音量一致;
  4. 重采样到 16kHz(librosa.resample);
  5. opuslib.Encoder 编码 60ms 帧;
  6. 按 P3 格式写文件。

LUFS(Loudness Units Full Scale):广电级响度标准,比简单的 RMS 准。-16 LUFS 是流媒体常见值(YouTube -14 / Spotify -14 / Apple Music -16)。

ogg_converter/xiaozhi_ogg_converter.py —— MP3 → OGG(用于第 4 章 PlaySound() 播放的提示音)。OGG 容器内 Opus 编码,跟设备解码栈一致。

mp3_to_ogg.sh —— 一行 ffmpeg shell 包装。

Image_Converter/ —— PNG/JPG → C 字节数组(编进固件)。

10.6.4 测试与调试

audio_debug_server.py —— UDP 调试服务端(第 4 章 AudioDebugger 用到):

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.bind(('0.0.0.0', 8000))
wav_file = wave.open(f"{samplerate}_{channels}.wav", "wb")
wav_file.setsampwidth(2)
wav_file.setframerate(samplerate)

while True:
message, address = server_socket.recvfrom(8000)
wav_file.writeframes(message) # PCM 直接写 WAV

设备开了 ENABLE_AUDIO_DEBUGGER 后,每个 audio frame 通过 UDP 发到 PC 8000 端口,PC 直接存成 WAV——查 AFE 处理效果、回声消除好不好、噪声大不大。

acoustic_check/ —— 声学验证工具。

sonic_wifi_config.html —— 声波配网网页端。打开 HTML 输 SSID + 密码 → 网页用 Web Audio API 合成 AFSK 调制波形通过手机喇叭播放 → ESP32 麦克风听到 → 解调拿到凭证。零依赖配网神器。

10.6.5 资源打包

spiffs_assets/

  • build.py:单板的 assets 打包;
  • build_all.py:批量;
  • pack_model.py:esp-sr 语音模型打包逻辑;
  • spiffs_assets_gen.py:主入口,被 build_default_assets.py import。

10.7 docs/ —— 设计文档与接线图

docs/
├── blufi.md BluFi 蓝牙配网协议说明
├── custom-board.md 怎么添加新板子(步骤教程)
├── mcp-based-graph.jpg MCP 整体架构图(架构图)
├── mcp-protocol.md MCP 协议详解(设计文档)
├── mcp-usage.md MCP 工具使用指南
├── mqtt-udp.md MQTT+UDP 协议详解(第 5 章基础)
├── websocket.md WebSocket 协议详解(第 5 章基础)
├── v0/ 老版本板子的接线图(jpg)
└── v1/ 新版本板子的接线图 + 渲染图

v0/ 9 张图,v1/ 18 张——大部分板子的实物接线图,方便用户自己焊。

mcp-protocol.mdmcp-usage.md 是第 6 章实现的"协议说明书"——理解协议先读 doc 再读代码会容易很多。

websocket.md / mqtt-udp.md 是第 5 章协议的对端文档(服务器侧也要照着实现)。

custom-board.md 是第 9 章 9.11 的官方教程,比本章详细。


10.8 sdkconfig.defaults 系列

项目根有多个 sdkconfig.defaults 文件:

sdkconfig.defaults                # 通用默认
sdkconfig.defaults.esp32 # 普通 ESP32 默认(4MB 分区表等)
sdkconfig.defaults.esp32c3 # C3 默认
sdkconfig.defaults.esp32c5 # C5 默认
sdkconfig.defaults.esp32c6 # C6 默认
sdkconfig.defaults.esp32p4 # P4 默认(PSRAM 大、双核高频)
sdkconfig.defaults.esp32s3 # S3 默认(最常用,PSRAM Octal)

ESP-IDF 启动构建时按当前 IDF_TARGET 自动合并对应文件到初始 sdkconfig:

idf.py set-target esp32s3
# 此时项目根的 sdkconfig 会从 sdkconfig.defaults + sdkconfig.defaults.esp32s3 合并生成

典型内容(s3):

CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_FREERTOS_UNICORE=n
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_FREERTOS_HZ=1000
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/v2/16m.csv"
CONFIG_LWIP_MAX_SOCKETS=10
...

这些都是性能/功能基线——menuconfig 选板子时可以再覆盖(如某些小内存板子改 8m 分区表)。


10.9 完整构建流程串起来

# Step 1: 选目标芯片(写 sdkconfig 默认值)
idf.py set-target esp32s3

# Step 2: 配置(菜单选板子、语言、唤醒词、配网方式等)
idf.py menuconfig
# 选了 BOARD_TYPE_ESP_BOX_3 + LANGUAGE_ZH_CN + WAKE_WORD_AFE

# Step 3: 构建
idf.py build
# 内部依次:
# 1. 检查 idf_component.yml,下载缺失组件到 managed_components/
# 2. CMake configure:读 sdkconfig,根据 BOARD_TYPE 派发,准备源文件列表
# 3. CMake generate:生成 ninja/make 文件
# 4. ninja 增量编译所有源文件
# 5. 链接 ELF(xiaozhi.elf)
# 6. esptool elf2image 生成 xiaozhi.bin
# 7. 生成 partition-table.bin(按 16m.csv)
# 8. 跑 build_default_assets.py 生成 generated_assets.bin
# 9. 用 esptool merge-bin 把上面 4 个合成 merged-binary.bin

# Step 4: 烧录(首次完整刷)
idf.py flash
# 烧入:bootloader / partition-table / xiaozhi.bin / generated_assets.bin

# Step 5: 监视串口
idf.py monitor
# 按 Ctrl+] 退出

# 一条命令搞定后三步:
idf.py build flash monitor

后续小修改(改代码)→ idf.py build flash monitor,CMake 增量编译,几十秒就好。


10.10 本章用到的关键技术

技术应用
CMake if-elseif 派发110 板子选 1
file(GLOB ...)boards/common 自动包含
add_custom_command + DEPENDSsdkconfig 变 → 重跑 assets 打包
file(DOWNLOAD) 构建期下载esp-hi 板拉 .aaf 资源
find_component_by_patternmanaged_components 路径不固定
esptool_py_flash_to_partition自定义分区烧录命令
partition_table_get_partition_info检测分区是否存在
Kconfig choice/endchoice互斥单选
Kconfig depends on条件可见性
sdkconfig.defaults.<target>按芯片自动合并默认配置
idf_component.yml + Component Manager第三方组件版本固定
constexpr std::string_viewLang::Strings 零开销字符串
手写紧凑二进制资源格式assets.bin 比 SPIFFS 省空间
LUFS 响度归一化提示音音量一致
AFSK 声波编解调(Web Audio + 麦克风)零依赖配网
_AUTO_SELECT_RULES 手动依赖映射绕过 Kconfig select 在非交互场景失效的问题

10.11 看完本章你应该掌握的

  • idf.py buildmerged-binary.bin 的完整流程
  • main/CMakeLists.txt 里 110 个板子怎么用 if-elseif 派发
  • Kconfig.projbuild 怎么生成 menuconfig 菜单
  • v1 vs v2 分区表的区别(assets 分区)
  • 不同 Flash 容量(4M/8M/16M/32M)的分区布局差异
  • build_default_assets.py 怎么把语音模型+字体+emoji+语言资源打成一个 .bin
  • release.py 怎么批量编译所有板子变体并自动处理依赖
  • gen_lang.py 怎么把 JSON 翻译表转成 constexpr C 头
  • audio_debug_server.py 怎么收 ESP32 UDP 调试数据并存 WAV
  • p3_tools/ 转换音频到 Opus P3 格式的关键步骤
  • idf_component.yml 怎么管理第三方组件版本
  • sdkconfig.defaults.<target> 的合并机制

终章 全文回顾

至此 10 章覆盖完整个项目:

章节主题关键文件
1项目总览 + 启动流程 + 全文件分级表
2主调度器:Applicationmain.cc / application.cc/h
3设备状态机device_state_machine.cc/h / device_state.h
4音频子系统:AFE / 唤醒词 / Opusaudio/*
5网络协议:WebSocket / MQTT+UDPprotocols/*
6MCP 工具调用协议mcp_server.cc/h
7系统服务:OTA / Assets / Settingsota.cc/h, assets.cc/h, settings.cc/h, system_info.cc/h
8显示与指示display/, led/
9板级抽象boards/*
10构建系统 + 工具链CMakeLists.txt, Kconfig, partitions/, scripts/

核心架构再回顾

                          ┌──────────────┐
│ 服务器侧 │
└──────┬───────┘
│ WSS / MQTTS + UDP-AES-CTR

┌─────────────────────────────────────────────────────────────┐
│ ESP32 设备 │
│ │
│ ┌──────────────┐ ┌─────────────────┐ │
│ │ AudioService │ │ Application │ <─ Event Group │
│ │ │ │ (Main Loop) │ │
│ │ - I²S Input │────│ │ │
│ │ - AFE │ │ ┌────────────┐ │ │
│ │ - WakeWord │ │ │DeviceState │ │ │
│ │ - Opus Enc/ │ │ │ Machine │ │ │
│ │ Dec │ │ └────────────┘ │ │
│ └──────┬───────┘ │ │ │
│ │ │ ┌────────────┐ │ │
│ ▼ │ │ Protocol │ │ │
│ ┌──────────────┐ │ │ (WS / MQTT)│ │ │
│ │ AudioCodec │ │ └────────────┘ │ │
│ │ (硬件 codec) │ │ │ │
│ └──────────────┘ │ ┌────────────┐ │ │
│ │ │ MCP Server │ │ │
│ ┌──────────────┐ │ └────────────┘ │ │
│ │ Display │ │ │ │
│ └──────────────┘ │ ┌────────────┐ │ │
│ ┌──────────────┐ │ │ Ota / │ │ │
│ │ Led │ │ │ Assets │ │ │
│ └──────────────┘ │ └────────────┘ │ │
│ ┌──────────────┐ │ ┌────────────┐ │ │
│ │ Camera │ │ │ Board │ │ │
│ │ (可选) │ │ │ (硬件抽象) │ │ │
│ └──────────────┘ │ └────────────┘ │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘

项目的几个核心设计思想

  1. 状态机驱动:所有用户交互都从 DeviceState 转换中得到响应。
  2. 事件组解耦:FreeRTOS EventGroup 让任务间通信无锁、可超时。
  3. 抽象层透明:NetworkInterface / Protocol / AudioCodec / Display / Led 5 大抽象让上层完全不感知硬件/网络差异。
  4. 资源外部化:assets 分区让语音模型 / 字体 / emoji / 语言资源跟代码分离,可独立 OTA。
  5. MCP 工具开放:通过 JSON-RPC 让 LLM 直接调设备能力,不用写专有 API。
  6. 离线唤醒 + 流式 ASR:唤醒词本地跑省功耗,识别上云保准确率。
  7. 双协议并存:WebSocket 用户网络好用;MQTT+UDP 移动网络/IoT 场景更稳。
  8. 配网三选一:Hotspot / BLE / 声波,照顾各种用户的网络环境。
  9. A/B OTA + Anti-bricking:ota_0 / ota_1 双分区,刷坏自动回滚。
  10. 编译期板子选择:110 板共享代码,CMake if-elseif 派发零运行时开销。

读到这里,您应该可以:

  • 独立拿一块 ESP32-S3 + 屏幕 + I²S 麦克风跑起这个项目;
  • 添加自己的 MCP 工具(控制空调、查询天气、播报新闻……);
  • 用同一服务器对接的话,把这套设备端代码移植到非 ESP32 平台也只是替换硬件抽象层;
  • 自己添加新板子(按 9.11 步骤);
  • 看懂任意 commit 改动跟哪一章相关。

全文完。