同一套小智源码,换块 ESP32 开发板为何还要重新适配?
TL;DR
- 场景:ESP32-S3 跑通小智后,换另一块同芯片板子,串口能起但屏幕不亮、麦克风无效输入。
- 结论:
Board::GetInstance()不是运行时自动选驱动;构造期已由构建配置选好的板级.cc决定。同名接口(GetAudioCodec/GetDisplay)下隐藏的硬件路径不同:bread-compact-wifi默认走NoAudioCodecSimplex,BOX-3 走BoxAudioCodec+ ES8311/ES7210;同一芯片不会自动对齐走线、采样率、控制总线。换板先核对本次构建配置和板文件,再追接口实现。workbench-audio-r1仅交换数据脚的设计可以只改新板config.h中两个宏,但BOARD_DIR、工厂声明、Kconfig.projbuild的依赖条件、config.json名称、OLED 选中态都要闭合为独立身份。 - 产出:板型/工厂/板级实例三处源码关系图 + 两块板 audio codec 配置对照 +
workbench-audio-r1设计差异落到 5 个修改位置 + 6 项"设计已定 / 验证待做"分栏记录。
文章正文
你在一块 ESP32-S3 开发板上跑通了小智,换到另一块同芯片的板子,串口能启动,屏幕却不亮,麦克风也没有可用输入。这时,先改语音服务参数往往找错了位置。芯片能运行程序,只证明了一部分条件;程序究竟初始化哪组引脚、哪种音频设备和哪块屏幕,要看构建时选进来的板级实现。
这里的故障场景是用于解释排查顺序的假设,不是本文的设备测试记录。小智提供了一套可复用的应用源码,硬件差异集中在 Board 和相关驱动里。它带来的便利,是让上层用共同接口调用设备;它没有把每种板卡的固件变成可以相互替换的文件。
本文核验 78/xiaozhi-esp32 固定提交 6240b777aaa2bc0cad43a4ce25b30de23f36ad00,日期为 2026-09-12。以 bread-compact-wifi 和 espressif/esp32-s3-box-3 两个目录为例,只说明源码中的构建与设备契约。没有编译、刷机、声学或外置 DSP 实测,也不代表其他提交和全部开发板。
程序拿到的 Board,已经由构建过程选好了
先看一个常见误会:Board::GetInstance() 看起来像运行时的统一入口,会不会启动后识别当前硬件,自动挑选合适的驱动?这份实现并没有在这里扫描板型。
main/CMakeLists.txt 根据 CONFIG_BOARD_TYPE_* 配置设置 BOARD_DIR。选择 bread-compact-wifi 时,目录是 bread-compact-wifi;选择 BOX-3 时,目录是 espressif/esp32-s3-box-3。随后用这个目录收集板级 .cc 和 .c 文件并追加到构建源文件中。上层公共代码可以复用,具体板级文件已经在构建阶段分流。
再看 board.h 的两处官方摘录,分别对应单例入口与工厂宏,二者不是一段连续代码:
static Board* instance = static_cast<Board*>(create_board());
#define DECLARE_BOARD(BOARD_CLASS_NAME) \
void* create_board() { \
return new BOARD_CLASS_NAME(); \
}
bread-compact-wifi 在板文件末尾声明 DECLARE_BOARD(CompactWifiBoard),BOX-3 声明 DECLARE_BOARD(EspBox3Board)。GetInstance() 取得的是该固件里工厂创建的对象;共同接口后面的具体构造函数,已经由所选板级源文件决定。
因此,先核对本次构建的配置和板文件,再追 GetAudioCodec()。不要从"这份仓库支持 BOX-3"直接跳到"我手里的任意小智固件都包含 BOX-3 的初始化"。这里也不能反向断言所有底层驱动都被彻底裁掉:构建脚本还包含公共音频代码,本节证明的是板级实例的选择方式。
还有一个容易混淆的名字。BOX-3 的源码目录是 espressif/esp32-s3-box-3,其 config.json 中的 type 和构建项 name 则为 esp-box-3。CMake 会读取配置中的 type 作为 BOARD_TYPE,缺失时才从目录生成。排查固件来源时,应保存目录、配置符号和发布名称的对应关系,不能凭字符串长得不完全一样就判定刷错。
同一个 GetAudioCodec,下面接的是两条不同的硬件路径
AudioCodec 在这里是音频输入输出的 C++ 接口抽象,不等于一定存在一颗同名硬件芯片。对比两个具体板文件,就能看到这层抽象隐藏了什么。
bread-compact-wifi 的 config.h 默认定义 AUDIO_I2S_METHOD_SIMPLEX,GetAudioCodec() 因而创建 NoAudioCodecSimplex,把麦克风和扬声器各自的时钟、数据引脚传进去。取消这个宏会走 NoAudioCodecDuplex 分支,但这是源码配置变化,需要重新构建,不是设备启动后自动检测出的模式。
这里的 Simplex 容易让人联想到"只能听,或者只能说"。该类实际上分别建立发送和接收 I2S 通道,输入和输出可以使用不同的引脚与采样率。名称描述的是此驱动组织 I2S 的方式,不能据此给整个语音产品下"必然半双工对话"的结论。是否边播边听,还要看上层状态和音频处理。
BOX-3 的 GetAudioCodec() 则创建 BoxAudioCodec,传入 I2C 控制总线、I2S 引脚、功放控制脚,以及 ES8311、ES7210 地址和参考输入开关。在该 codec 实现里,ES8311 用于输出,ES7210 用于输入。I2S 负责搬运音频样本,I2C 控制接口负责配置芯片;只接对音频数据线,还不足以替代控制与功放初始化。
| 当前源码配置 | bread-compact-wifi 默认 Simplex 分支 | ESP32-S3-BOX-3 |
|---|---|---|
| codec 实例 | NoAudioCodecSimplex | BoxAudioCodec |
| 配置输入 / 输出采样率 | 16000 / 24000 Hz | 24000 / 24000 Hz |
| 具体引脚例子 | 麦克风 DIN=6;扬声器 DOUT=7 | DIN=16;DOUT=15;MCLK=2 |
| 板文件传入的额外控制 | 分开的麦克风与扬声器 I2S 时钟脚 | I2C 总线、芯片地址、PA 控制脚、参考输入标记 |
表格摘自两个目录的 config.h 和 GetAudioCodec(),是这两个配置的对照,不是通用接线表。相同的 ESP32-S3 不会让 GPIO6 与 GPIO16 自动连到同一颗麦克风,也不会让设备忽略实际走线。
上层调用能统一,不代表采样率和通道语义天然统一。移植时至少要把实际板子的采样格式、时钟、通道与供电控制对上,再判断语音数据为什么为空、失真或播放异常。本文没有把这些可能症状归因到某个已经发生的具体故障。
屏幕能否工作,也不能只看有没有 GetDisplay
板级构造函数不仅负责音频。bread-compact-wifi 初始化显示用的 I2C 总线,再创建 OLED 显示路径,尺寸由 SSD1306 或 SH1106 相关配置决定。BOX-3 则初始化 I2C、SPI 和 ILI9341 显示路径,配置尺寸为 320×240,并在构造阶段恢复背光亮度。
所以,黑屏时只改上层 UI 文本,无法补上错误的总线、面板驱动或背光控制。甚至"函数返回了一个 Display 对象"都未必证明存在实体显示器:公共 Board::GetDisplay() 的默认实现返回 NoDisplay 对象;默认 GetBacklight() 返回空指针。无屏降级和存在实体屏幕,是接口允许的两种情况。
这对扩展功能的判断很有用。移植完成后,应查目标板是否重写了对应接口、初始化到了哪个对象,再查上层是否使用该能力。一个统一的设备 API,并不承诺每块板都有屏幕、可调背光或摄像头。本文的两块板都有具体显示实现,默认 NoDisplay 是公共接口的边界证据,不是说这两块板没有屏幕。
接入外置 DSP 时,先说明送进来的样本代表什么
如果新板使用外置 DSP,也就是在 ESP32 之外进行音频处理的芯片或模块,问题会再向下移动一层:它交给 ESP32 的,究竟是原始多麦克风采样,还是已经处理后的单通道语音?是否还带有播放参考信号?不能因为最终都是 PCM 数字音频,就认为可以无条件接入现有处理链。
这里可以从现有源码看到一个具体约束。BoxAudioCodec 按 input_reference 设置输入通道数:开启参考输入时为两个,否则为一个。AfeAudioEngine 根据 codec 的 input_channels() 和 input_reference() 组织输入格式,用 M 标记麦克风通道、R 标记参考通道,再交给音频前端。AEC 是回声消除,播放参考用于帮助区分扬声器回声和用户声音;两个通道不等于两路都可以当作用户麦克风。
官方代码中的关键组织逻辑如下。这是忠实摘录,省略了前后的初始化代码:
int ref_num = codec_->input_reference() ? 1 : 0;
std::string input_format;
for (int i = 0; i < codec_->input_channels() - ref_num; ++i) {
input_format.push_back('M');
}
for (int i = 0; i < ref_num; ++i) {
input_format.push_back('R');
}
该引擎开启设备 AEC 时还会检查 codec 是否声明播放参考通道。声明影响的是处理路径如何解释输入,并不能凭空产生正确的参考样本,更不能证明回声已经被消除。
据此,外置 DSP 适配需要先记录它输出的通道数、交织顺序、采样率、样本位宽,以及是否已执行回声消除、降噪等处理。再决定 ESP32 侧 codec 如何描述这些输入、哪些处理继续保留。这是由接口约束推出的工程建议,不是对某款外置 DSP 已兼容的测试结论。两个主对比板的 codec 实例也不在本文被称作已验证的外置 DSP 接入方案。
如果模块只输出一路已处理语音,不能把"DSP 支持 AEC"直接换成 input_reference=true。这个标记应对应送入引擎的实际参考通道。若模块还输出单独参考,则要核实通道顺序与时序,不能拿一个布尔开关替代样本验证。
走完一次移植:只交换两根音频数据线,应该改到哪里
把前面的差异落到一块具体的设计上。假设要做一块名为 workbench-audio-r1 的自定义板,基于 bread-compact-wifi 默认的 Simplex 路径,麦克风、扬声器模块、独立输入输出时钟、OLED 及其他外围保持相同。唯一的布线变化是:麦克风数据输入从 GPIO6 改到 GPIO7,扬声器数据输出从 GPIO7 改到 GPIO6。
这是本文构造的设计推演,没有对应实物和原理图附件。这组数字只用于展示如何沿既有源码作出决定,不是接线建议。实际项目必须从目标板原理图确认连线、管脚可用性及其他功能占用,从模块资料确认接口格式、电平与供电条件;"模块和格式相同"在此处是明确的假设,不能当作已测结果。
先把设计差异转换成有限的代码改动
在这组约束下,音频数据仍由原来的 Simplex 收发路径处理,不需要因为两根数据线互换就引入 BOX-3 的 I2C codec。新板 config.h 中,输入与输出的数据脚应分别改为:
// 本文自定义板设计示例,非官方现成配置,也未编译或刷机
#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_7
#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_6
保留 AUDIO_I2S_METHOD_SIMPLEX,输入 16000 Hz、输出 24000 Hz,以及原有的各组时钟配置。依据是什么?CompactWifiBoard::GetAudioCodec() 已把这些宏作为参数传给 NoAudioCodecSimplex;后者把 mic_din 放入接收配置,把 spk_dout 放入发送配置。因此,当前差异可以由新板配置表达,不必改变公共 NoAudioCodec::Read() 或 Write()。
但"音频实现只需改两项配置"不等于"直接覆盖原板文件后就能发布"。为了使自定义板有独立身份,示例采用新建板目录的路线。各处的职责可以明确对应:
| 修改位置 | 本例拟采用的内容 | 为什么需要它 |
|---|---|---|
main/boards/workbench-audio-r1/config.h | 复制基准配置后,只交换上述两个数据脚;保留其余已约定配置 | 用配置表达走线差异,避免改动原板 |
新目录中的板级 .cc | 从基准实现派生出 WorkbenchAudioR1Board,保留本例所需初始化与 codec 选择,使用 DECLARE_BOARD(WorkbenchAudioR1Board) | 工厂必须创建本例板级类 |
main/Kconfig.projbuild | 增加示例板型 BOARD_TYPE_WORKBENCH_AUDIO_R1,目标约束按本例 ESP32-S3 设置 | 让构建配置能选择新板 |
main/CMakeLists.txt | 在板型选择分支中,把新配置符号映射到 BOARD_DIR "workbench-audio-r1" | 让构建实际收集新目录源文件 |
新板 config.json | type 和 builds[].name 使用 workbench-audio-r1,target 为 esp32s3;其他硬件覆盖项按真实板填写 | 区分上报身份与构建名称,不冒充原板标准固件 |
这些是本例需要完成的修改位置与设计内容,并非已经落地的可编译补丁。还有一个不能漏掉的依赖:基准 config.h 要求选中受支持的 OLED 配置,否则会触发 #error;固定版本 Kconfig 中的 OLED 选择又受板型条件约束。新增板型仍保留该 OLED 时,需要把新板加入相应选择条件,并明确选取实际屏幕型号。仅新增 BOARD_DIR,却没让 OLED 选项对新板生效,可能连预期配置都无法形成。构建身份的闭环,必须走到它依赖的外围配置。
什么时候这条"保留 codec,只改板配置"的路线不再适用?如果原理图显示音频硬件已经换成需要控制总线与功放使能的 ES8311/ES7210 方案,就超出了本例假设。前面的 BOX-3 实现说明,这时需要对应的 I2C 初始化、地址、PA 与 codec 对象;不能仅把 GPIO 号码抄到 NoAudioCodec 的构造参数中。若模块输出格式发生变化,同样要重新核对驱动的数据解释,而不能保留旧 Read 实现后只改注释。
启动对象正确之后,用什么观察决定下一步
完成上述设计,还不能把实测栏填成通过。实际执行时,先从本次构建配置和源文件选择确认新 Board,再保留固件摘要与启动记录。即使看到 Simplex channels created,也只说明代码执行到了通道创建后的日志位置:同一份源码中,输入通道的启用另由 EnableInput(true) 完成。创建日志不等于输入已经启用,更不等于麦克风的有效声音已到达应用。
接下来,应在 codec 读取边界取证,而不是先看云端识别出了什么。固定版本 NoAudioCodec::Read() 先请求 I2S 读取 32 位容器,读取调用不返回 ESP_OK 时直接返回 0;成功后按实际字节数算出样本数,将各值右移 12 位并限幅,再写入 int16_t 输出。这提供了三个可以分开观察的位置:底层读取结果与字节数、转换前的 32 位数据、转换后的 16 位样本。
下面的分支是待执行的诊断方案,没有虚构日志或录音值。
- 假如 Read 持续返回 0: 先记录底层返回码、请求量和有效读入量,确认是否调用了输入启用。由于这个函数把非成功读取都折叠为 0,单凭 0 不能判断是接错 GPIO、超时还是别的读取错误。此时继续核对接收配置是否确实用了 GPIO7、时钟和输入启用路径,再结合实际总线观察定位。不要从"没有样本"直接跳到"模型不识别这个麦克风"。
- 假如有读入字节,却只有零值、近似恒定值或与已知声音无关的数据: 只能排除"完全没有返回字节",还不能宣布采集正常。检查模块实际输出落在哪个时隙、源数据如何对齐,以及读取配置是否匹配。本例使用的 Simplex 构造函数设置左时隙、32 位数据宽度;如果真实模块不符合原先的格式假设,应先修正设计依据,而不是把任意数据都送往语音识别。
- 假如转换前的数据会随已知声音变化,转换后却严重限幅或幅度异常: 已有证据把问题进一步指向数据解释与转换。可以在同一段输入上比较右移、限幅前后的值,再核对模块有效位位置和幅度范围。不能靠随意改右移位数让波形"看起来有声";必须先确定真实样本格式。这个分支才有理由离开"数据线完全不通"的假设,但仍不证明信号来自正确麦克风或满足声学指标。
音频线上的 32 位容器和 codec 交给应用的 16 位 PCM,在这里是两个观察点。记录格式时如果只写"16 位音频",读者就容易拿应用样本格式去套总线配置。对本例,源码可以提前填写这两个格式及转换位置;实际录音的有效数据、幅度和失真仍需采集来确认。
扬声器一侧也独立走一次验证:确认新配置把发送数据脚设为 GPIO6,再用已知输出分别检查软件写入和实际播放。写入返回量只证明到相应软件接口,不能代替扬声器是否正确发声的观察。OLED 保持基准器件与引脚的前提下,先确认屏幕配置确实被选中;若新固件仍黑屏,再沿显示初始化取证,不把音频数据线交换当作黑屏的当然原因。
将本例的设计栏填完,把实测栏留给真实设备
经过上述推导,本例已经可以交付一份有结论的设计记录,而不必等到实验后才开始填表:
| 项目 | 本例已确定的设计 / 源码依据 | 必须补上的实测证据 |
|---|---|---|
| 适配对象 | workbench-audio-r1,假设 ESP32-S3;固定源码提交同本文;原型基于 bread-compact-wifi,保留同款外围,仅交换两根数据线 | 实际板版本、原理图、模块资料、Flash 等配置、最终固件摘要:待验 |
| 构建与工厂 | 新板配置符号→新 BOARD_DIR→WorkbenchAudioR1Board 工厂;OLED 选择条件也需加入新板;这是拟实施的修改路径 | 构建配置、编译结果、板级启动记录:待验 |
| 音频引脚与对象 | MIC_DIN 6→7,SPK_DOUT 7→6;保留 NoAudioCodecSimplex、分离时钟、输入 16 kHz / 输出 24 kHz | 实際连线、通道启用与读取结果:待验 |
| 样本解释 | 所用构造分支配置左时隙、32 位数据;Read 右移 12 位并限幅到 16 位样本;沿用理由是本例假设模块格式未变 | 实际有效位、已知声音下转换前后数据及录音质量:待验 |
| 显示与外部处理 | OLED 型号与配置需要按实际基准选择;本例未增加外置 DSP,也不声明新增参考通道 | 画面与输入输出实效:待验 |
| 当前结论 | 配置修改范围与诊断分支已由固定源码推演完成;若硬件假设不成立,回到 codec / 初始化方案重新选择 | 未编译、未刷机、未完成硬件适配验收 |
这份记录已经回答"为什么本例可以保留 codec,具体要改哪里,以及什么观察会推翻当前判断"。它没有把待验项伪装成完成项。实际板与设计假设一致并通过对应验证后,才有依据报告该板适配完成。
官方《自定义开发板指南》还提醒:不要直接覆盖原板 IO 配置后仍沿用其身份,应创建新板型或用不同构建名称和配置区分,避免未来升级用标准固件覆盖自定义配置。本例选择独立目录、类型与构建名称,正是为了让维护对象继续对应这块板。这里采用的是官方指南的维护要求,没有审计实际云端 OTA 分发。
参考资料
所有链接固定到本文核验提交;正文中的 GPIO、采样率和默认分支以链接配置为准。
- 构建时板型选择、板目录源码收集
- 板型配置选择
- Board 工厂与可选接口
- 默认 NoDisplay 实现
- bread-compact-wifi 实例
- bread-compact-wifi 引脚与采样率
- BOX-3 实例
- BOX-3 引脚与采样率
- BOX-3 发布名称
- NoAudioCodec 的 I2S 通道实现
- BoxAudioCodec 芯片与参考输入
- AFE 输入通道解释与 AEC 条件
- 官方自定义开发板指南
资料复核于 2026-09-12。官方链接支撑板型选择、工厂声明、audio codec 配置、显示初始化、外置 DSP 接入约束的方向;workbench-audio-r1 5 处修改位置、3 类诊断分支、6 项设计/验证分栏记录均为作者示例与推演,未编译、刷机或声学实测。
配图保持方法示意与官方源码摘录的区别;图上"2026-09-12 核验"标签保留。设计推演的具体数字与边界仍需在目标板与真实硬件上验证。
错误速查卡
| 症状 | 根因 | 定位 | 修复 |
|---|---|---|---|
Board::GetInstance() 启动后自动识别板型 | 板级 .cc 已在构建期被选入 | 看 CMakeLists.txt 的 BOARD_DIR 与板文件 DECLARE_BOARD | 先核对本次构建配置与板文件,再追接口实现 |
GetDisplay() 返回对象就证明有实体屏 | 默认实现可返回 NoDisplay、GetBacklight() 返回空指针 | 看公共 board.cc 默认实现 | 看目标板是否重写接口、初始化到哪个对象 |
| 换同芯片板只改 GPIO 号 | 采样率、控制总线、OLED 配置都可能不同 | 比对两块板 config.h 与 GetAudioCodec() | 完整对照板级实现与引脚,而非只看芯片型号 |
BOX-3 目录 = esp-box-3 = BOX-3 三者一致 | 目录、配置 type 与 builds[].name 不一定相等 | 核 config.json 与 CMake BOARD_TYPE 来源 | 保存目录、配置符号、发布名称的对应关系 |
NoAudioCodecSimplex 必然半双工 | 名称描述驱动组织 I2S 方式 | 看驱动实现与上层状态机 | 不据此给整体下"半双工"结论 |
| 音频数据线接对就够 | BOX-3 还需要 I2C 总线、PA、ES8311/ES7210 初始化 | 看 BoxAudioCodec 构造参数 | 同时核对控制总线与 PA |
| 未选中 OLED 型号只警告 | bread-compact-wifi/config.h L48-50 是 #error | 看 #error "OLED display type is not selected" | 新板加入 OLED 依赖条件并选中实际型号 |
input_reference=true 等价"DSP 支持 AEC" | 该标记只对应送入引擎的实际参考通道 | 看 AfeAudioEngine 输入格式组织逻辑 | 先记录 DSP 输出的通道、交织、采样、位宽与处理 |
新增 BOARD_DIR 即可编译 | 还需要 Kconfig 板型符号、工厂声明、OLED 依赖 | 看 Kconfig.projbuild 与 config.json | 五处修改全部闭合为独立身份 |
| 按目录名自动生成 build name | CMake 读 config.json 的 type,缺失才从目录生成 | 看 CMakeLists.txt 对 BOARD_TYPE 的设置 | 显式设置 type 与 builds[].name |
| 覆盖原板 IO 后仍用原板身份发布 | 官方指南要求独立类型与构建名称 | 看 docs/custom-board_zh.md | 新建独立目录、类型与构建名称 |
| 右移 12 位是通用常数 | 来自本实现,其他驱动不一定 | 看 no_audio_codec.cc L250-254 | 按目标驱动实现核对移位与限幅 |
| Read 返回 0 = 没有输入 | 非 ESP_OK 被折叠为 0 | 看 Read() 内 != ESP_OK 分支 | 同时记录底层返回码与字节数 |
| 0 返回 = 总线无数据 | 也可能是 GPIO 错配、超时、未启用输入 | 沿接收配置、时钟、输入启用路径分别查 | 在总线容器与应用样本两侧分别观察 |
| 凭"没有样本"跳到"模型不识别麦克风" | 把软件侧症状归因到模型 | 看模型识别前的音频链路 | 先完成 codec 边界取证 |
| 扬声器能写入 = 扬声器在发声 | 写入返回量只证明到软件接口 | 看 Write() 返回与实际播放 | 用已知输出分别检查软件写入和实际播放 |
作者:武子康的个人博客 发布日期:2026-09-14(资料复核 2026-09-12)