尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ESP32+WT3000TX离线语音通知盒子:从硬件接线到代码实战

ESP32+WT3000TX离线语音通知盒子:从硬件接线到代码实战 大概两个月前我在桌面上摆了一块ESP32开发板和一个小喇叭折腾出了一套能自动播报“快递到门口”“电饭煲煮好了”“服务器挂了”的语音通知盒子。核心方案就是标题里写的ESP32负责联网取数据WT3000TX负责离线TTS播报。这套东西做起来不难但踩坑点不少尤其是WT3000TX的串口协议和编码转换网上资料又碎又散。今天我把整个方案的选型思路、硬件接线、代码实现、场景化玩法还有我踩过的坑一次性整理出来给想自己动手做智能语音通知的朋友做参考。这套方案适合三类人第一类是正在玩ESP32、想给它加个“嘴巴”的嵌入式爱好者第二类是想给家里的智能家居加语音提示、但又不想依赖云端的动手派第三类是需要在工位或实验室搞一个低成本状态播报器的开发者。不需要很深的功底会基本的Arduino编程就能跟着做出来。1. 方案选型为什么是ESP32搭配WT3000TX1.1 WiFi主控的选择逻辑一开始我考虑过好几条路线用ESP8266省成本用树莓派跑完整TTS服务或者直接选一块带WiFi、蓝牙、双核的ESP32。最终定下来用ESP32最关键的原因是它有余量。ESP8266虽然便宜但RAM只有160KB可用跑TLS加密的HTTPS请求、解析JSON、再管理串口缓冲内存经常捉襟见肘指纹登录接口稍微复杂一点就崩。ESP32就从容很多520KB SRAM双核240MHz后续想加传感器、加OLED屏、加BLE蓝牙控制都不会卡死。而且ESP32的WiFi稳定性比8266好不少掉线重连机制写好了能几个月不用管。树莓派那种跑完整Linux的方案我也考虑过但直接否了。一个是成本一张树莓派够买十几套ESP32另一个是启动速度和可靠性树莓派要等系统启动断电还可能损坏SD卡ESP32上电三秒就能跑完初始化开始干活。对于一个“播报通知”这种单一任务杀鸡不用牛刀。1.2 TTS方案的三个选择维度TTSText To Speech文本转语音这部分市面上可选方案主要分三类纯在线API、ESP32本地软件TTS、专用TTS芯片。我在项目里抛开了前两者直接锁定了WT3000TX原因是三条硬指标第一是离线可用。在线API虽然音色自然但依赖网络状况还要注册账号、处理鉴权家里宽带断一次语音通知就哑了这违背了我做“可靠通知器”的初衷。WT3000TX是芯片级的离线TTS语音合成在芯片内部完成不占ESP32的资源断网也能播报本地状态。第二是零内存压力。如果让ESP32跑本地TTS算法要么用极简的蜂鸣器“哔哔”方案要么引入体积巨大的语音库还要处理I2S音频输出、DMA缓冲区管理代码复杂度直线上升。WT3000TX把文本通过串口发过去就完事了芯片自己负责文字到语音的合成输出音频信号直接驱动功放ESP32只当个“传话筒”。第三是中文支持与性价比。WT3000TX支持中英文混合播报GB2312编码直接能读不用做拼音到音频的映射。模块价格大概十几块钱到二十几块钱比加一个云服务的一年订阅费便宜太多。1.3 整体架构设计这套系统的数据流很清晰信息源天气API / MQTT服务器 / GPIO按钮 / 传感器 ↓ 网络请求或本地事件 ESP32拉取文本 → 解析出播报语句 → 组帧 ↓ UART串口9600波特率 GB2312编码 WT3000TX接收文本指令 → 离线语音合成 ↓ 音频输出DAC输出 → 功放 → 喇叭 用户耳朵听到语音通知ESP32在这个架构里是“大脑”负责所有逻辑判断和网络交互WT3000TX是“嘴”只管把文字变成声音。两者通过串口通信协议异常简单。整台设备功耗不高实测WiFi常开状态下整机电流大概80~120mAUSB口供电就够了。2. 硬件准备与接线实战2.1 元器件清单先列一份我实际用到的物料清单都是常见模块电商平台随便买元件型号/规格参考价作用主控ESP32 DevKitC V430pin15~25元WiFi联网、逻辑控制TTS芯片模块WT3000TX模块含喇叭接口新用户10几元起离线语音合成功放PAM8403迷你功放板 或 模块自带功放5~10元驱动喇叭发声喇叭8Ω 2W~3W直径4cm~8cm3~8元音频输出按键轻触开关若干1元手动触发播报电源手机充电头MicroUSB线 或 5V/2A电源模块已有整机供电可选0.96寸OLEDI2C8~12元显示IP和状态如果买的WT3000TX模块是带功放输出的完整版有喇叭接线柱那种PAM8403可以省掉。我买的是集成模块直接接喇叭就行省了功放电路。新手建议也买这种集成好的别自己搭功放调试起来麻烦。2.2 接线表与关键注意点接线特别简单核心就三根线TTS模块的RXD接ESP32的TXDTTS的TXD接ESP32的RXD再共地。注意一定是交叉接否则两边都在发数据、都在收数据串口对不上收到的全是乱码。ESP32 DevKitC的默认串口引脚是TX0 GPIO1接WT3000TX的RXDRX0 GPIO3接WT3000TX的TXD两者GND共地WT3000TX的VCC接3.3V或5V具体看模块说明书我这块是5V供电。如果是3.3V逻辑的模块可以直接和ESP32的3.3V通信模块标注5V的话串口电平多数兼容3.3V但我建议用万用表确认一下防止烧TTS模块的串口引脚。喇叭接模块的SPK和SPK-没有正负之分但注意别短路。模块音量调节一般有个电位器先转到中间位置再上电测试。2.3 供电设计的一个隐蔽坑这是我第一次点亮时踩的坑用电脑USB口供电一切正常换成充电头供电播报声音就变调或者中途卡住。查了半天才发现劣质充电头输出纹波太大TTS芯片在做语音合成时对电源比较敏感电压一抖声音就失真。解决方法是加一个1000μF电解电容并在电源入口处或者在ESP32的5V和GND之间并几个100nF陶瓷电容。另外一个更稳妥的办法是直接买个明纬或者普通的5V/2A开关电源别省这点钱电源是整个系统稳定性的底层基础。3. 核心代码实现与串口协议解析3.1 环境准备代码用的Arduino框架先在Arduino IDE里装好ESP32开发板支持包开发板管理器地址用官方espressif的JSON网上搜一下大把教程。板卡选择ESP32 Dev Module上传速度建议115200。串口监视器波特率设成115200方便看调试日志。需要用到的库就两个WiFi.hESP32自带不需要额外装WiFiManager.h开源库用于配网也可以不用我后面会讲两种方式ArduinoJson.h解析接口返回的JSON数据我用的是版本6注意和相关代码的兼容性3.2 Wi-Fi连接的两种配网方式在讲正式的语音播报前先解决联网这个基础问题。方式一硬编码账号密码快速验证用适合在家调试直接把WiFi名称和密码写死在代码里#include WiFi.h const char* ssid YourWiFiSSID; const char* password YourWiFiPassword; void setup() { Serial.begin(115200); // 调试串口 Serial2.begin(9600); // 串口2用于和WT3000TX通信稍后会用到 WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println(\nWiFi connected, IP address: ); Serial.println(WiFi.localIP()); } void loop() { // 业务逻辑放这里 }方式二SmartConfig一键配网体验更好推荐手机下载“ESP8266 SmartConfig”类App或者用微信小程序搜索“ESP Touch”长按按键让ESP32进入配网模式手机App把WiFi账号密码广播过去。代码用SmartConfig库实现#include WiFi.h void smartConfigSetup() { WiFi.mode(WIFI_STA); WiFi.beginSmartConfig(); Serial.println(Waiting for SmartConfig...); while (!WiFi.smartConfigDone()) { delay(500); Serial.print(.); } Serial.println(SmartConfig received, connecting...); while (WiFi.status() ! WL_CONNECTED) { delay(500); } Serial.println(Connected, IP: WiFi.localIP().toString()); }第一次配网之后ESP32会自动保存WiFi信息到Flash里断电重启也会自动重连不用每次重新配。这个改动极大提升了使用体验做产品原型或者送给朋友用都更体面。3.3 向WT3000TX发送TTS指令协议细节与代码封装WT3000TX的通信协议是典型的串口帧格式帧头 数据长度 命令字 数据内容 校验具体每一字节的取值以你手里芯片的《数据手册》为准。下面是一个通用模板我以常见的WT3000系列帧结构为例实际参数请对照芯片手册// 发送文本到WT3000TX播报 // text为要播报的中文文本需为GB2312编码 void sendTTS(const char* text, uint8_t textLen) { // 组帧帧头 长度 命令(0x01表示文本播报) 文本数据 // 长度为数据域长度 2命令字 校验位 // 这里仅示意具体对应关系以芯片手册为准 uint8_t frame[128]; // 根据文本长度调整 frame[0] 0xFD; // 帧头 frame[1] (uint16_t)(textLen 3) 8; // 长度高字节 frame[2] (uint16_t)(textLen 3) 0xFF; // 长度低字节 frame[3] 0x01; // 命令字0x01文本播报 memcpy(frame[4], text, textLen); // 文本数据 // 异或校验部分芯片是累加和以手册为准 uint8_t checksum 0; for (uint8_t i 0; i textLen 4; i) { checksum ^ frame[i]; } frame[textLen 4] checksum; Serial2.write(frame, textLen 5); }这里有一个最大的坑编码转换。WT3000TX需要的是GB2312编码的中文字节流而我们编写的Arduino代码源文件默认是UTF-8。如果直接把字符串“你好”发过去芯片会认为这是两个乱码字符。解决方案有两个方案一最简单在写代码时用VSCode的“更改文件编码”功能把.ino文件编码改为GB2312但这种方式会导致WiFi名称、日志等中文字符串也变成GB2312混合编码很容易乱套我不推荐。方案二推荐用文本编码转换库。Arduino有个开源库GBK_Encoding支持UTF-8到GBK的转换用法如下#include GBK_Encoding.h String utf8Text 快递已到门口; String gbkText Utf8ToGbk(utf8Text); sendTTS(gbkText.c_str(), gbkText.length());没有现成库也可以用查表法实现但代码量比较大。我建议直接在GitHub搜“ESP32 GBK UTF8”下载转换库塞进libraries文件夹就行这一行转换能省你三小时排查乱码的青春。3.4 一个完整的播报函数封装把上面的内容整合一下我的播报函数长这样#include WiFi.h #include GBK_Encoding.h HardwareSerial TTS(2); // 用串口2接TTS模块 void ttsInit() { TTS.begin(9600, SERIAL_8N1, 16, 17); // RXGPIO16, TXGPIO17 } void ttsSpeak(String utf8Text) { String gbkText Utf8ToGbk(utf8Text); sendTTS(gbkText.c_str(), gbkText.length()); Serial.println([TTS] utf8Text); }这里我选择用UART2GPIO16/17而不是默认的UART0原因是UART0同时也是USB调试串口如果给TTS用了Serial.print()打印日志就会和语音数据混在一起排查问题非常痛苦。分离之后调试信息走UART0语音数据走UART2互不干扰。4. 场景化应用把语音通知变得真正有用代码框架打好了接下来就是往里面装“灵魂”——让WiFi拿到的数据变成嘴里说出来的话。这里分享四个我实际做过的场景代码思路都可以复用。4.1 场景一天气预警定时播报每天早上一睁眼先让系统从天气API拉取温度、天气状况和降水概率然后合成一句话播报“早上好今天多云气温22到28度降水概率百分之三十记得带伞。”用免费的OpenWeatherMap或者心知天气APIESP32发起HTTP请求拿到JSON后解析#include HTTPClient.h #include ArduinoJson.h String fetchWeather() { HTTPClient http; http.begin(https://api.seniverse.com/v3/weather/now?keyYOUR_KEYlocationjiangsulanguagezh-Hans); int httpCode http.GET(); if (httpCode 0) { String payload http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, payload); const char* temperature doc[results][0][now][temperature]; const char* weather doc[results][0][now][text]; String report 早上好当前温度 String(temperature) 度 String(weather) 。; http.end(); return report; } http.end(); return 天气服务暂时不可用; }用HTTPS连接需要初始化TLS指纹和证书HTTPClient库默认能处理大部分场景但遇到握手失败就多试几个版本的内置证书更新包。这种习惯性问题网络上有很多现成方案这里不展开细说。4.2 场景二GPIO触发即时播报这个场景最简单也最实用门口放一个微波雷达模块或人体红外传感器检测到有人靠近就播报“欢迎光临”门上放一个门磁传感器门打开就播报“大门已开启”。代码核心就是中断或轮询GPIO状态#define BUTTON_PIN 0 // 按键或者传感器信号引脚 void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); ttsInit(); // WiFi连接等等 } void loop() { if (digitalRead(BUTTON_PIN) LOW) { ttsSpeak(门口有人按门铃请查收。); delay(3000); // 防止连续触发 } }注意点传感器输出如果是高电平脉冲需要用digitalRead延时消抖如果追求可靠性可以在GPIO口加一个上拉电阻和100nF滤波电容防止干扰信号误触发。我实测裸奔状态在阳台门附近偶尔会被风刮一下误报加了消抖之后就稳了。4.3 场景三MQTT消息订阅播报这是整套方案最有生产力的一种玩法家里NAS、跑批服务器、甚至树莓派上的脚本执行完关键任务后通过MQTT发一条消息ESP32收到后播报“任务已完成”或“构建失败”。MQTT客户端用PubSubClient库#include PubSubClient.h WiFiClient espClient; PubSubClient mqtt(espClient); void mqttCallback(char* topic, byte* payload, unsigned int length) { String msg ; for (int i 0; i length; i) { msg (char)payload[i]; } ttsSpeak(收到新消息 msg); } void setup() { mqtt.setServer(192.168.1.100, 1883); mqtt.setCallback(mqttCallback); } void loop() { if (!mqtt.connected()) { mqtt.connect(esp32_tts); mqtt.subscribe(home/notification); } mqtt.loop(); }设置好重连逻辑后这台设备就可以像“语音小助理”一样接收来自任何设备的通知消息。我把电脑上的rsync备份脚本、家里的门铃、甚至路由器断线告警都接进了这个Topic。体验非常奇妙——你坐在沙发上系统在你身后报一句“备份同步完成”然后你甚至不用回头。4.4 音量调节与多音色切换WT3000TX本身支持音量调节一般通过指令字控制比如带音量参数的播报指令。我封装了这样一个函数void ttsSetVolume(uint8_t volume) { // 组帧设置音量指令具体帧内容参考芯片手册 uint8_t cmd[] {0xFD, 0x00, 0x02, 0x22, volume}; TTS.write(cmd, sizeof(cmd)); }灵感可以把音量拉大到最大值在客厅播报晚上用手机通过MQTT发一条“音量低”把音量调小不影响家人睡觉。多音色切换同理男声、女声、童声在芯片内部有不同音色ID指令换一字节就行。具体音频参数表在数据手册最后几页照抄即可。5. 常见问题排查与避坑实录5.1 问题速查表现象可能原因解决方法串口输出乱码波特率不匹配 或 未共地确认两边都设9600波特率GND必须连一起播报出来是乱音文本不是GB2312编码确认做了UTF-8到GB2312的转换播报声音断续电源纹波大 或 模块供电不足换质量好的电源加1000μF电容降低音量到80%以下播报一次后卡死帧长度计算错误 或 库栈溢出检查帧长度字节确认文本长度不超过模块最大支持字数连不上WiFiSSID/密码错误 或 路由器5G频段改用2.4G频段确认账号密码没写错在线API拉取失败证书问题 或 接口限流用HTTP临时调试仅限测试或者检查API限额5.2 深坑帧长度字节的“大端陷阱”WT3000TX这类芯片的帧长度字段普遍是大端序高位在前我第一次写组帧代码习惯性地用了小端序结果芯片一个字都不说也不回ACK。卡了一晚上最后拿逻辑分析仪抓波形才发现问题。写框架的时候千万别想当然一定要对着手册扒每一字节。如果发现芯片完全没反应先循环发送一个固定文本用示波器/逻辑分析仪看串口波形是否正常发出。90%的“芯片不响应”问题都能在这种排查里找到答案。5.3 深坑串口二选一别占用了系统调试口有朋友写的代码死活不播报日志也看不见。后来发现他把TTS接在GPIO1UART0的TX上导致USB转串口芯片也被占用整个串口链路就乱了。ESP32 DevKitC的USB串口芯片只接到UART0所以只要把TTS换到UART2就能解决。我的固定接法GPIO16 TTS_RXGPIO17 TTS_TX与UART0完全隔离调试体验直线上升。5.4 深坑中文文本长度别超出限制WT3000TX这种入门级TTS芯片单次播报的文本长度通常限制在几百字节以内具体看手册如果发长句子芯片可能截断或者不播报。我的做法是把长文案拆成多个短句每条单独发送中间隔200ms延时避免芯片内部缓冲区溢出void ttsSpeakLong(String text) { // 粗略按30个字拆一次具体长度参考芯片限制 while (text.length() 60) { String sub text.substring(0, 60); ttsSpeak(sub); text text.substring(60); delay(200); } if (text.length() 0) { ttsSpeak(text); } }这个“拆句播报”的心得让我原先一个很长的日报播报顺利实现不然你辛苦拼接完的整段话芯片只念一半挺尴尬的。5.5 合规与安全提醒最后多啰嗦一句这套方案里ESP32是连接你自己家或你自己管理的网络设备。配网时密码要对但不要试图去连接或破解不属于你的网络和设备这既是法律红线和基本道德也是网络安全管理的基本常识。做智能家居产品的时候保障自己网络密码的强度开启路由器的访客隔离IoT设备网段隔离都是非常必要的安全习惯。咱们玩开源硬件是为了提升生活质量不是给自己惹麻烦。附我的一点调试心法这套系统跑通之后我的生活确实变化不小门铃响了人在书房语音报一句“有人来了”不用跑过去看服务器半夜报警它在客厅喊“生产环境异常”比手机推送靠谱得多因为语音具有天然的强存在感。最后再分享一个小技巧在loop()里加一个简单的优先级队列门铃通知的优先级永远高于天气播报这样当两个事件同时出现时语音不会被低优先级的内容打断。我就是这样一点点把它变成一台真正“懂事”的语音通知设备。DIY的魅力就在于此——你不只是在做一个喇叭而是在给自己的生活安排一个可靠的信使。
返回列表