用一块 ESP32-S3 做了个能"喊一嗓子就放塔"的 AI 塔防游戏,从零跑通的全过程

折腾了一周,把这块吃灰的 ESP32-S3 开发板做成了一个用嘴喊就能玩的塔防游戏:板子上跑游戏逻辑+本地语音唤醒,电脑上跑 FastAPI + 阿里云 DashScope 的 ASR/LLM/TTS,浏览器再开一个控制台看实时战况。

整个项目最终长这样:

  • 对着板子喊一句"小塔小塔"——本地 esp-sr 离线识别到唤醒词

  • 接着说"帮我放个塔"或者"现在还剩多少血"

  • 板子把这段录音通过 WebSocket 二进制流推到电脑上的 FastAPI

  • FastAPI 把 PCM 喂给阿里云 Paraformer 做 ASR,再交给 Qwen 做对话和 function calling

  • Qwen 决定要调哪个游戏工具(比如 place_tower),调完再用 CosyVoice 合成一句中文回复

  • TTS 的 PCM 反向流回板子,从 ES8388 喇叭里播出来

听起来很复杂,但其实分三块跑就行:ESP32 固件FastAPI 服务Web 控制台。下面把我从 0 跑通的步骤写下来,所有踩过的坑都顺手记一笔,照着做一遍,新板子大概 2 小时能跑起来。


一、你需要准备什么

硬件

  • 一块带 PSRAM、ES8388 编解码器、麦克风和喇叭的 ESP32-S3 开发板(我用的是某宝带 LCD 屏的"AI 学习板",型号不重要,只要带 ES8388 + 8MB PSRAM 即可)

  • 一根能传数据的 USB Type-C 数据线(别拿那种只能充电的,会踩坑

  • 一台能连同一个 Wi-Fi 的电脑(Windows / macOS / Linux 都行,文章用 Windows)

软件账号

  • ESP-IDF v6.0(v5.x 也能跑,但部分 API 不一样)

  • Python 3.10+

  • Git

  • 阿里云百炼平台账号,开通 DashScope 后到「API-Key 管理」拿一个 key(新用户有免费额度)

云端 AI 选哪家

我也试过 OpenAI 的 Whisper + GPT-4o-mini + tts-1,效果非常好但贵;最后选了阿里云百炼,原因很现实:便宜。Paraformer-realtime-v2 + Qwen-Turbo + CosyVoice 一个完整对话大概 1 分钱不到,新人免费额度足够你折腾一周。

项目代码里也写了 OpenAI / Echo(本地正弦波,纯调试用)的实现,有需要直接改 .env 里的 AI_PROVIDER 即可。


二、把代码下下来

git clone https://github.com/wangwenwenwenAda/ESP32S3.git E:\ESP32S3
cd E:\ESP32S3

工程目录关键的几个地方:

E:\ESP32S3
├─ main\                  ESP-IDF 固件源码
│   ├─ main.c             启动入口
│   ├─ audio.c/.h         I2S + ES8388 驱动
│   ├─ voice_local.c      esp-sr 本地唤醒词
│   ├─ voice_session.c    云端会话状态机(VAD、上传、TTS 回放)
│   ├─ net_client.c       WebSocket 客户端
│   ├─ game.c/.h          塔防游戏逻辑
│   └─ lcd_ui.c/.h        LCD 上的 HUD
├─ server\fastapi\        FastAPI 服务
│   ├─ app.py             入口
│   ├─ ai_session.py      会话管理(ASR -> LLM -> 工具 -> TTS)
│   ├─ ai_provider\       各家云 AI 适配
│   └─ game_tools.py      LLM 可以调的游戏工具
└─ web\                   纯静态前端控制台

三、先把服务端跑起来

我建议先把服务端跑通,这样后面烧固件就能直接试。

1. 装 Python 依赖

cd E:\ESP32S3\server\fastapi
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
pip install dashscope

dashscope 没写进 requirements.txt,因为是可选的(你可能用 OpenAI),手动装一下。

2. 配置 .env

copy .env.example .env
notepad .env

把里面的内容改成:

AI_PROVIDER=dashscope
DASHSCOPE_API_KEY=sk-你刚才在百炼控制台拿到的key
DASHSCOPE_ASR_MODEL=paraformer-realtime-v2
DASHSCOPE_CHAT_MODEL=qwen-turbo
DASHSCOPE_TTS_MODEL=cosyvoice-v1
DASHSCOPE_TTS_VOICE=longxiaochun

3. 启动服务

uvicorn app:app --host 0.0.0.0 --port 8000 --reload

--host 0.0.0.0 必须加,不然 ESP32 连不上你电脑。看到下面这行就 OK:

INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Application startup complete.

打开浏览器访问 http://127.0.0.1:8000/state,能看到一段 JSON 表示游戏状态,证明服务跑起来了。

4. 记住电脑的局域网 IP

ipconfig

找到你 Wi-Fi 网卡的 IPv4 地址,比如我的是 172.20.10.3。这个 IP 后面要写进 ESP32 固件配置。

小贴士:如果你用的是 iPhone 个人热点,电脑的 IP 经常变,而且 iPhone 热点偶尔会做 client isolation 让设备之间互相 ping 不通。强烈建议用家里普通路由器,省心很多。


四、装好 ESP-IDF 环境

如果你之前没玩过 ESP-IDF,从 Espressif 官网下载 IDF Tools Installer,选 v6.0 一路下一步。装完之后会在开始菜单里多出一个"ESP-IDF 6.0 PowerShell",点开就直接是配好环境的终端。

我自己习惯往 PowerShell profile 里加一个 idfenv 别名,每次想用直接敲 idfenv 就进 ESP-IDF 环境,省得到处找快捷方式。

进去之后先验证一下:

idf.py --version

输出 ESP-IDF v6.0.x 就对了。


五、配置并烧录 ESP32 固件

1. 设置目标芯片

cd E:\ESP32S3
idf.py set-target esp32s3

2. 改 menuconfig

idf.py menuconfig

进入 Tower Defense Network,按你的实际情况改:

Wi-Fi SSID                 你的 Wi-Fi 名字
Wi-Fi password             Wi-Fi 密码
Device WebSocket URL       ws://172.20.10.3:8000/ws/device
Device ID                  s3-node-01

注意 Device WebSocket URL 里的 IP 要换成你电脑的 IP(第三步 ipconfig 看到的那个)。

往下还有一个 Cloud AI Voice Chat 子菜单,里面是云端 AI 相关的开关:

Enable cloud AI voice chat   y
VAD threshold                800
VAD silence (ms)             1200
Session max (ms)             8000

VAD 阈值是判断"没人说话"的能量门槛,800 是我实测一个比较通用的值。麦克风附近环境噪音大可以适当调高,不然录音容易被噪音"卡住"不结束。

S 保存,Q 退出。

3. 编译 + 烧录

idf.py build
idf.py -p COM13 flash monitor

COM13 换成你板子的实际串口号(设备管理器里能看到)。

第一次编译时间比较长(5~10 分钟,要装 esp-sr 模型和一堆组件),后面增量编译会快很多。

烧完之后串口里应该能看到:

Compile time:     May 9 2026 11:09:20
[VS] init done (ai_enable=1, tts_buf=96KB)
NET: wifi connecting to YourWiFi
NET: got ip: 172.20.10.4
NET: websocket connecting to ws://172.20.10.3:8000/ws/device
NET: websocket connected
[VOICE] detect start

服务端那边也会打印:

INFO:     172.20.10.4:xxxxx - "WebSocket /ws/device" [accepted]
INFO:     connection open

到这里链路就通了。


六、开浏览器看战局

服务端、固件都跑起来之后,开第三个 PowerShell:

cd E:\ESP32S3
python -m http.server 5173 -d web

然后浏览器打开:

http://127.0.0.1:5173

页面顶上有波次、HP、金币、状态,下面是 8x8 的格子和一堆控制按钮。如果服务端 + 固件正常工作,HP / 波次会跟着 ESP32 上传的游戏状态实时更新。

手机想访问就把 127.0.0.1 换成电脑 IP(必须连同一个 Wi-Fi)。


七、跟"小塔"开口说话

让板子靠近你嘴 20cm 左右,清晰地说:

"小塔小塔"

串口里应该出来:

[VOICE] cmd_id=5 phrase_id=5 text=xiaotaxiaota prob=0.684
[VOICE] wake -> cloud session prob=0.684
[VS] IDLE -> WAKE -> REC
[VS] recording start (vad_th=800 silence=1200ms max=8000ms)

板子开始录音了,紧接着说一句话:

"帮我放个塔"

说完别动,等大约 1.2 秒静音,VAD 会自动判停:

NET: ws send: {"type":"session_end",...,"reason":"vad_silence"}
[VS] REC -> WAIT

服务端那边会看到:

asr submit: 41644 pcm bytes (~1.30s) model=paraformer-realtime-v2
asr final text='帮我放个塔'
chat reply: 好的,我在 5 号格放了一个塔。
tts done: 32 chunks (~1.02s pcm), pacing=30ms burst=4

几秒钟后喇叭会传出"小塔"的合成语音,前端控制台上会出现一个新的塔。

整个对话来回大约 2~4 秒,主要瓶颈在云端响应。


八、几个我踩过、你大概率也会踩的坑

坑 1:Error create websocket task,ESP32 一直重启

这是内部 RAM 不够的典型症状。esp-sr 的 MultiNet 模型 + WiFi 协议栈 + LVGL 把 internal RAM 吃光后,WebSocket 任务的 4KB stack 拿不到连续内存,xTaskCreate 失败、ESP_ERROR_CHECK 直接 abort 重启。

修复有两步:

  1. 打开 SPIRAM 优化menuconfigComponent config -> ESP PSRAM,勾上 Try to allocate memories of WiFi and LWIP in SPIRAM firstly;再调小 WiFi 动态/静态 RX buffer 数量

  2. 调整启动顺序main.c 里把 net_client_init() 挪到 voice_local_init() 之前。这样 WiFi 连上后就立刻 alloc WebSocket task stack,那时 esp-sr 还没加载,internal RAM 充足

坑 2:服务端日志显示 dashscope asr empty output

第一次接 DashScope 我至少踩了三个坑:

  • 用了 paraformer-v2 —— 这模型根本不存在,DashScope 同步 API 只支持 paraformer-realtime-v2

  • 设了 format='wav' —— SDK 内部直接把文件 raw bytes 当 PCM 流喂上去,wav header 的 44 字节会变成噪音破坏识别

  • 写了 callback=None 又调 .call(file) —— 同步路径下 callback 完全不被触发,但同步路径会自己把 sentence 收集到 result 里

正确写法:

rec = Recognition(
    model="paraformer-realtime-v2",
    format="pcm",          # 不是 wav
    sample_rate=16000,
    callback=None,
)
result = rec.call("/tmp/audio.pcm")  # 文件里是裸 PCM,没有 wav header
sentences = result.get_sentence()

坑 3:TTS 播放时声音断断续续

DashScope CosyVoice 流式生成的速度比实时播放快很多(基本一次性吐完整段),如果 ESP32 这边的接收 ringbuffer 太小,后面的 chunk 就直接被丢了。修复方案两条腿走路:

  • 服务端限流:每发一个 chunk 之间 await asyncio.sleep(0.03),让推送速率约等于实时速率

  • 设备端加大缓冲:把 xStreamBufferCreate 的容量从 32KB 加到 96KB,同时用 heap_caps_malloc(MALLOC_CAP_SPIRAM) 把这块 buffer 放到 PSRAM,省 internal RAM

坑 4:电脑能 ping 通 ESP32,但 ESP32 连不上 8000 端口

99% 是 Windows 防火墙或者 iPhone 热点 client isolation

防火墙的话开个入站规则:

New-NetFirewallRule -DisplayName "FastAPI 8000" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8000

iPhone 热点的话——换路由器。

坑 5:uvicorn --reload 会把 ESP32 的 WebSocket 弄死

--reload 检测到代码改动会 kill 所有旧连接(发 RST)。ESP32 端的 esp_websocket_client 收到 RST 后不会自动重连,会一直卡在 disconnected 状态。

我加了一个 watchdog:在 heartbeat 任务里检测连续 10 秒没连上就主动 esp_websocket_client_stop + esp_websocket_client_start 一次。代码在 net_client.cheartbeat_task 里。


九、然后呢

跑通之后能继续玩的方向很多:

  • 加更多 LLM 工具:在 game_tools.py 里多注册几个 schema,比如查询某个塔的等级、升级塔、切换难度

  • 换更大模型DASHSCOPE_CHAT_MODEL=qwen-plus,对话理解会明显变聪明,但延迟和价格也上去

  • 本地放歌:CosyVoice 是 TTS,不会真的唱歌,但可以挂个 SD 卡、把 16kHz/16bit/mono 的 raw PCM 文件直接喂给 audio_play_pcm_chunk,再让 LLM 调一个 play_music(name=...) 工具

  • 多设备协同:FastAPI 已经有 device_id 概念,理论上可以挂 N 块板子,让它们组队对战

  • 接入 Home Assistant:把游戏状态广播成 MQTT,或者反过来让 LLM 控制家里的灯


写在最后

这个项目完整代码在我的 GitHub(链接放评论里),README 里也写了所有命令的复制粘贴版。

整个过程里最爽的瞬间不是看 Qwen 输出 JSON,而是真的对着一块小破板子喊"小塔小塔",听到喇叭里冒出一句口语化的"好的,我帮你放上了"——硬件 + 网络 + 云端 AI 全栈打通的那种闭环感,比单纯写几个 demo 要满足太多。

如果你也在玩 ESP32-S3,希望这篇能帮你少走点弯路。有问题留言或私信都可以。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐