用一块 ESP32-S3 做了个能“喊一嗓子就放塔“的 AI 塔防游戏,从零跑通的全过程折腾了一周,把这块吃灰的 ESP32-S3 开发板做成了一个用嘴喊就能玩的塔防游戏:板子上跑游戏逻辑+本地语
用一块 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 重启。
修复有两步:
-
打开 SPIRAM 优化:
menuconfig进Component config -> ESP PSRAM,勾上Try to allocate memories of WiFi and LWIP in SPIRAM firstly;再调小 WiFi 动态/静态 RX buffer 数量 -
调整启动顺序:
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.c 的 heartbeat_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,希望这篇能帮你少走点弯路。有问题留言或私信都可以。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)