固件代码导览:目录结构、状态机与开发规则
小智固件是一个 ESP-IDF 的 C/C++ 工程,同时支持多种芯片、开发板、屏幕、音频器件和网络通道。一次构建只会选择一个开发板实现。本页按上游 v2.5.2 的 AGENTS.md、main/audio/README.md 和源码整理,帮你在动手改代码前先找对位置。
一、源码地图
| 位置 | 职责 |
|---|---|
main/main.cc | 初始化 NVS,创建 Application 并进入 Run() 主循环 |
main/application.* | 主事件循环、协议生命周期、高层行为(唤醒、对话、升级、通知) |
main/device_state_machine.*、device_state.h | 合法的运行状态及状态转换 |
main/boards/common/ | 板级接口与可复用的硬件/网络辅助类 |
main/boards/<厂商>/<板>/ | 每个板子的引脚、初始化和构建变体(config.h、config.json) |
main/audio/ | 编解码器、音频任务、输入引擎、唤醒词与队列 |
main/protocols/ | 与传输无关的 Protocol 接口,以及 WebSocket、MQTT/UDP 两种实现 |
main/display/、main/led/ | LCD、OLED、表情显示与 LED 的通用实现 |
main/mcp_server.* | 设备端 MCP 工具及分发 |
main/ota.*、main/assets.*、main/settings.* | OTA 版本检查与激活、固件升级、assets 分区、NVS 设置 |
main/Kconfig.projbuild、main/CMakeLists.txt | 板型与功能配置;源码、板卡、语言、字体和资源的选择 |
scripts/build.py | 官方推荐的板型/变体构建入口 |
二、运行流程
main.cc 初始化 NVS 后调用 Application::GetInstance().Initialize() 与 Run(),Run() 是永不返回的主事件循环。运行状态由 DeviceStateMachine 维护,DeviceState 共有以下取值:
| 状态 | 含义 |
|---|---|
kDeviceStateStarting | 启动中 |
kDeviceStateWifiConfiguring | 配网模式,见首次启动 |
kDeviceStateIdle | 空闲,等待唤醒或按键 |
kDeviceStateConnecting | 正在建立服务器连接 |
kDeviceStateListening | 录音上行 |
kDeviceStateSpeaking | 播放回复 |
kDeviceStateNotifying | 通知提示 |
kDeviceStateUpgrading | 固件或资源升级中 |
kDeviceStateActivating | 激活流程 |
kDeviceStateAudioTesting | 音频测试 |
kDeviceStateFatalError | 致命错误 |
Application 对外提供 ToggleChatState()、StartListening()、StopListening()、WakeWordInvoke()、Alert()、Schedule()、SendMcpMessage() 等入口,板级代码(如按键回调)通过它们触发行为。
三、板级抽象
Board(main/boards/common/board.h)是核心代码依赖的接口,主要纯虚方法有 GetBoardType()、GetAudioCodec()、GetNetwork()、StartNetwork()、GetNetworkStateIcon()、SetPowerSaveLevel()、GetBoardJson()、GetDeviceStatusJson();背光、LED、显示、摄像头和电池则是可选能力。
boards/common/ 里提供了不同网络形态的基类,例如 WifiBoard、Ml307Board、DualNetworkBoard、EthernetBoard、RndisBoard、Nt26Board,以及按键、电源管理、休眠定时器等辅助类。每个具体板子继承其中一个基类,并用 DECLARE_BOARD(类名) 导出,构建中必须恰好有一个。
新增开发板的完整流程见自定义开发板指南。
四、音频路径
音频由 AudioService 管理:编解码器 I/O、Opus 编解码和网络队列与芯片相关的语音处理分离。
- 上行:麦克风 →
AudioCodec→AudioInputTask→ 一个AudioEngine(S3 / P4 / S31 为AfeAudioEngine,其余芯片为LiteAudioEngine)→ 16 kHz 单声道 PCM →OpusCodecTask编码 → 发送队列 → 网络。 - 下行:网络 → 解码队列 →
OpusCodecTask解码 → 播放队列 →AudioOutputTask→ 编解码器 → 喇叭。 - Opus 帧长 60 ms;队列使用编译期固定容量的存储。
引擎与芯片的对应关系见芯片支持与存储规格。
五、协议层
Protocol 抽象了开启/关闭音频通道、发送音频、发送唤醒词、开始/停止监听、中断播放和 MCP 消息等动作,具体传输由 WebsocketProtocol、MqttProtocol 实现。协议细节见 WebSocket 通信协议、MQTT + UDP 混合协议和 MCP 协议。
六、开发规则
以下规则摘自上游 AGENTS.md,改代码前请先对照:
- 保持改动聚焦,不要夹带无关格式化;只对改动过的 C/C++ 文件按仓库
.clang-format格式化,参见代码风格指南。 - 一次构建必须恰好导出一个
DECLARE_BOARD(...)。 - 不要为了适配别的硬件去改已有开发板的引脚;请新增一个名称唯一的板子或发布变体,因为板型标识会影响 OTA 兼容。
- 核心代码只依赖
Board接口,不依赖具体板类或某个板的config.h。 - 摄像头、背光、显示、LED、电池等都按可选能力处理。
- 运行状态只通过
Application::SetDeviceState()和状态机修改。 - 回调可能不在主任务中运行:用
Application::Schedule()或事件位切回主任务再修改应用状态。 - 不要阻塞主事件循环和音频任务;音频路径避免无界队列与反复的大块分配。
- 共享的消息语义放在
Protocol中;改动协议契约时要同时验证 WebSocket 和 MQTT/UDP。 - 校验网络输入,保持
cJSON的所有权约定;NVS 键属于持久化 API,修改需要迁移。 - 目标相关的功能用 Kconfig 或组件规则约束,不要假定每个芯片都有 PSRAM 或 S3/P4 的资源。
- 不要手工修改生成物或第三方输出:
build/、releases/、managed_components/、components/、sdkconfig*、main/assets/lang_config.h等。
七、构建与验证
构建前先加载 ESP-IDF 环境(source /path/to/esp-idf/export.sh,用 idf.py --version 确认),再使用上游推荐的命令:
# Discover exact board and variant names
python3 scripts/build.py --list-boards
# Canonical variant build
python3 scripts/build.py <board-directory> --name <variant-name>
# Host-side build tests
python3 -m unittest discover -s scripts/tests -vscripts/build.py 会改写本地 sdkconfig 和构建状态,切换板子后不要假定旧的构建目录还对应上一个目标。
上游对验证的要求是:
- 只改板级代码:构建受影响的变体,并在实体硬件上做冒烟测试。
- 改核心、通用板级、音频、协议、显示、依赖、Kconfig 或 CMake:运行主机侧测试,并构建有代表性的芯片与网络路径。
- 改音频:验证采集、播放、唤醒/VAD、打断、重连以及适用的 AEC 模式。
- 改 UI 或 assets:验证无屏/OLED/LVGL 各路径与分区大小。
- 构建成功不等于硬件验证,汇报时要写明测了什么、哪些还需要实体硬件。
相关文档
- ESP-IDF 开发环境搭建:IDF v6.0.1 及以上
- 自定义开发板指南:新增开发板的完整步骤
- 进阶扩展点:网络形态、唤醒词、MCP、通知与资源定制
- 问题排查:编译、烧录与运行时问题
对照来源:78/xiaozhi-esp32 v2.5.2 的 AGENTS.md、main/audio/README.md、main/application.h、main/device_state.h、main/boards/common/board.h、main/protocols/protocol.h。