最近 B 站上赛博小狗和小智 AI 都火得不行,复刻一个的想法自然就冒了出来,最好还能把两者揉到一起。研究一圈后发现,作者们的开源方案有些地方跟自己的习惯不太对付,于是决定动手改造——然后,就一头扎进了踩坑之旅。
缘由
赛博小狗这个开源项目,作者用的是现成的语音识别和文本转语音模块,那两个模拟模块又贵又死板,而且主控还是 STM32。要想结合小智 AI,开发起来难度不小。
转念一想,乐鑫 ESP32 本身就已集成语音唤醒、语音识别和文本转语音能力,完全可以用一颗 MCU 把外部模块的功能替换掉。成本大幅降低,而且小智 AI 本身也是基于 ESP32 开发的,两者融合更顺畅。
再仔细看了看小智 AI 开源硬件端的项目,发现它是基于 ESP-IDF 开发的——这就有点头疼了。虽说官方的 ESP-IDF 比 Arduino 框架更强大、配置更灵活,但从开发便捷性来说,Arduino 框架明显更易上手。
为了把这些内容改成自己顺手的模样,踩坑之旅正式开启。
开发环境
虽然玩 ESP32 时一直用 Arduino 框架,但从未碰过 Arduino IDE 那玩意儿。用的是 Visual Studio Code + PlatformIO 插件。Arduino IDE 的编辑器太古老了,代码提示、自动补全、符号跳转这些基本功能一个都没有,用起来实在憋屈。
Visual Studio Code 在这方面就强太多了,再加上最近 AI 编程工具(比如 Codium 或 Cursor)的流行,写代码效率翻了好几倍。而且 PlatformIO 还能管理多个 MCU 平台的 SDK 和编译,一个 App 搞定多种固件开发,省心省力。
这次搭建 ESP32 + ESP-SR + ESP-TTS 开发环境,也基于 VSCode + PlatformIO 完成。真心推荐电子 DIY 和 Arduino 爱好者试试这套组合。
ESP-SR 库找不到?
第一步,先测试官方语音识别 ESP-SR 里的示例。从 arduino-esp32 仓库里找到 ESP_SR 的示例代码,原样复制过来,一编译就直接报错——头文件 ESP_I2S.h 和 ESP_SR.h 找不到。

翻到 platformio 的 framework-arduinoespressif32 目录里一看,还真没有……
折腾了好一会儿才搞明白:PlatformIO 用的 arduino-esp32 仓库,是另一个项目打包的,打包时直接就把 ESP-SR 相关的库删掉了。
又花了点时间,找到一个 pioarduino/platform-espressif32 仓库。这个仓库紧跟官方上游,用最新的 ESP-IDF 打包 Arduino 框架。只要在 platformio.ini 里把 platform 字段设成他们的仓库就行。

保存,等 PlatformIO 下载完依赖,终于能编译通过了。
I2S 初始化失败?
拿上最新的固件编译、烧录,打开串口监视器——怎么一直在重启?暂停日志一看,全卡在下面这个错误上:
看起来是因为我用的 ESP32-S3-N16R8 带 PSRAM 的版本,初始化 I2S 时,中断函数被放到了 PSRAM 里,而不是 SRAM 里。
可测试代码基本没改过,按理说分配到 PSRAM 需要额外指定,示例代码里根本没做这种事。
继续费劲地找,还真找到了几天前有人提的 issue:I2S failed to set up tx callback。原来是官方 SDK 打包时参数设置错了——好么……
评论里提到会在 3.2.0-RC2 中修复,那就等着新版本的 arduino-esp32 吧。
幸好用了 16M Flash,分区才够用
等了一周,新版本 arduino-esp32 终于出来了,固件也能在 ESP32 上正常跑起来了。但运行时还是报错,说找不到唤醒词。
搜了一下才知道,模型得单独上传,需要在 Flash 上单独创建一个分区。某教程里推荐用 esp_sr_16.csv 这个预置的分区配置:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x300000, app1, app, ota_1, 0x310000, 0x300000, spiffs, data, spiffs, 0x610000, 0x700000, model, data, spiffs, 0xD10000, 0x2E0000, coredump, data, coredump,0xFF0000, 0x10000,
这个分区配置创建了一个标签为 model、大小 2944KB 的分区,用来存放 ESP-SR 需要的模型(语音唤醒和命令识别两种)。
用这个分区表之后,配置唤醒模型倒是可以了,但后面的 TTS 模型又需要额外分区,而且这个分区大小也不太够。最后还是改了一下分区表,用自定义配置:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x300000, app1, app, ota_1, 0x310000, 0x300000, spiffs, data, spiffs, 0x610000, 0x100000, model, data, spiffs, 0x710000, 0x5E0000, voice_data,data,fat, 0xCF0000, 0x300000, coredump, data, coredump,0xFF0000, 0x10000,
额外的两个分区配置:
model:存放语音唤醒模型,大小 6016KB
voice_data:存放 TTS 模型,大小 3072KB
为了用上 MultiNet6,直接支持用拼音配置中文指令,model 分区调到了 6MB 多,不然 WakeNet 和 MultiNet 模型根本放不下。
唤醒模型该怎么配置啊?
好,终于能跑起来了。一运行,继续提示缺少提示词——明白了,分区创建了,但模型数据还没上传。
在 esp-sr 项目里找到 WakeNet 和 MultiNet 的模型数据,项目主页上列出了目前已有的唤醒词。
这里选了 小鸭小鸭 作为唤醒词。
另外还需要 MultiNet 的指令识别模型,为了支持中文,选了 mn6_cn,同样在 esp-sr 项目的 model 目录里能找到。
把它们放在一个目录里,用自带的 Python 脚本生成目标分区数据:
python3 pack_model.py -m mytarget -o srmodels.bin
再用 esp-tool 上传到 model 分区:
esptool.py --baud 2000000 --before default_reset --after hard_reset write_flash 0x710000 data/srmodels.bin
搞定。现在终于能用“小鸭小鸭”唤醒 ESP32,再用“打开空调”来操作了。
TTS 也要上传模型哦
弄完 ESP-SR 的语音唤醒和命令识别,接着折腾 TTS。
根据 esp-skainet 里的示例,TTS 的用法也不复杂,参考 https://github.com/espressif/esp-skainet/blob/master/examples/chinese_tts/main/main.c 中的代码就行。
不过得吐槽一句:官方这些示例、文档、模型文件真是东一块西一块,找起来费老劲了……
TTS 的模型藏在 esp-sr 项目里,路径是 esp-tts/esp_tts_chinese/esp_tts_voice_data_xiaole.dat。
几种音色的区别在 samples 目录里有示例试听。
这里用了 xiaoxin_small,用以下命令刷入 Flash:
esptool.py --baud 2000000 --before default_reset --after hard_reset write_flash 0xCF0000 ./data/esp_tts_voice_data_xiaoxin_small.dat
再用官方示例代码测试就行。
这语音咋声音不对呢
测试环境用的硬件是 MAX98357 I2S 音频放大器模块。参照官方示例初始化 I2S 并播放 TTS 音频时,声音始终不对劲——调了采样率和位宽都对不上。最后发现 ESP-TTS 输出的数据是单声道,把输出模式从立体声改成单声道就解决了。
用的是 ESP_I2S 库,在 begin 时把第 4 个参数从 I2S_SLOT_MODE_STEREO 改成 I2S_SLOT_MODE_MONO,一切正常。
好了终于跑通了
断断续续折腾了几个星期,终于在 ESP32-S3 的 Arduino 框架下把 ESP-SR 语音识别、语音指令、文本转语音这些功能全跑通了。接下来再挤点时间,就可以继续去折腾赛博小狗和小智 AI 了
