ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32+WT3000TX离线TTS:从0搭建WiFi语音通知设备

ESP32+WT3000TX离线TTS:从0搭建WiFi语音通知设备 去年做菜时手机放在客厅厨房里听不到来电等看到未接来电已经是半小时后。那之后我就琢磨做一个带WiFi的语音通知设备ESP32负责联网获取信息TTS文字转语音的事交给WT3000TX芯片接个小喇叭家里哪个角落都能听到。整条链路跑通以后我把它做成了一个通用方案不光能播报来电提醒还能报天气、报时间、报传感器状态、报服务器告警。这篇博文把完整思路、硬件连接、软件代码和排错记录整理出来给想自己动手做一版的朋友当个参考。这套方案的核心关键词就四个WiFi、TTS、ESP32、WT3000TX。ESP32是主控负责连网、拉取数据、处理逻辑WT3000TX是离线语音合成芯片负责把中文文字变成可以播放的语音。两者通过串口通信硬件上非常简单但实际做下来有几个地方特别容易翻车比如电平匹配、中文编码、断线重连、播报卡死文章里都会说到。1. 为什么是ESP32配WT3000TX而不是其他方案1.1 主控芯片的选型逻辑刚开始我也纠结过用ESP8266还是ESP32。如果只是简单报个时、报个温湿度ESP8266其实也够用它便宜而且同样有WiFi。但真正对比下来我最终还是选了ESP32原因很实际串口资源。ESP8266只有一个硬件串口调试口和功能口经常打架。ESP32有3个UART一个接WT3000TX芯片一个接调试一个还能留作扩展这一条就把体验拉开一大截。双核。ESP32是双核处理器可以把网络任务和播报任务放在不同核心上跑互不干扰。ESP8266是单核播报过程中一旦网络回调卡一下声音就可能断。外设丰富。ESP32自带I2S、ADC、DAC、触摸引脚、BLE后期想加按键、加传感器、加蓝牙配置都很方便扩展空间大。我的结论是如果只是做一个“能响的玩具”ESP8266也没问题但如果你打算把它做成长期在线的通知设备直接上ESP32省得后面换主控重新写代码。1.2 语音方案为什么不选“ESP32直接发声”很多人第一反应是“ESP32不是有DAC吗直接接个小喇叭不就行了”。理论上是能出声但涉及到的麻烦事比想象中多。第一种做法是在ESP32上跑TTS算法把文字合成成音频数据然后通过I2S或DAC播放。问题是ESP32的Flash普遍只有4MB到16MB中文语音库体积大需要抠内存合成过程占CPU长时间跑会影响WiFi稳定性而且软件TTS的音色普遍偏机械听久了难受。第二种做法是提前把MP3音频文件放进SD卡或Flash用I2S功放模块播放。这个方案的音质可以做得很好但致命缺点是“不能动态播报”——你没法在运行时把一句新产生的文字变成语音只能播放预先录好的那几句。做固定提示音可以做智能通知完全不行。所以我最终选了第三条路外挂一颗专用的离线TTS芯片。WT3000TX做的就是这件事——你通过串口发一串文字给它它在内部完成语音合成然后把音频信号直接送出去接一个小功放和喇叭就能出声。好处非常明显不占ESP32的CPU、支持中文/英文混读、音色自然、离线运行完全不需要云端接口。1.3 这套方案的定位和适用边界为了让大家更直观地理解我为什么做这个选型我把当时对比的几个方案整理成了表格方案动态文字播报音质CPU占用开发难度成本ESP32软件TTS支持较差高高低I2S功放预录音频不支持较好低中中ESP32WT3000TX支持较好低低中这套方案最适合的场景是需要把“随时产生的文本信息”转换成语音。典型需求包括智能家居通知、环境监测播报、定时提醒、老人看护设备、工业设备的语音告警。不适合的场景是长篇幅语音朗读播报几分钟的文章和对音质有专业要求的应用比如做音乐播放器那种场景上专用音频编解码方案更合适。2. 接线之前的两个关键问题电平匹配和供电2.1 3.3V和5V的TTL电平问题WT3000TX模块在市面上有两种常见版本一部分是3.3V供电电平也是3.3V TTL另一部分做成5V供电串口电平跟随5V。你买回的模块到底属于哪种一定先看原理图或者问卖家不能想当然直接接。这里有个容易忽略的点ESP32的大部分GPIO并不耐5V。如果你拿一个5V TTL的WT3000TX模块直接接到ESP32的RX引脚轻则读不到数据重则烧掉GPIO。反过来如果模块是5V电平而ESP32是3.3V输出模块侧也可能识别不了低电平信号造成通信不稳定。我自己的做法是统一用3.3V版本的模块一来和ESP32电平匹配二来省去一个电平转换芯片。如果你手头只有5V版本最简单可靠的办法是在中间加一个3.3V转5V的电平转换模块或者用MOS管搭一个双向电平转换电路。用电阻分压凑合接也能用但信号沿会变差高速通信下容易丢字节不建议。2.2 供电设计音频功放是隐藏的电老虎另一个容易踩的坑是供电。TTS芯片本身功耗不大但芯片后面接的功放模块在播报的时候瞬态电流能到几百毫安甚至更高。如果你用ESP32板载的3.3V LDO直接给TTS模块和功放供电播报时电压会瞬间跌落轻则芯片复位从头播报重则整板死机。安全做法是分路供电ESP32用USB/5V输入供电板载稳压给主控自己。TTS芯片如果需要3.3V单独用一个AMS1117-3.3之类的LDO从5V取电。功放模块直接吃5V别让音频功率和主控抢电。所有模块的地线要共地这是串口通信稳定的基础。这套供电方案看起来多花了几块钱但实际跑起来比我最初“一根线全挂板载3.3V”的方案稳定太多了。原来播报声音一大就复位改成独立供电之后问题彻底消失。2.3 完整接线参考以我用的ESP32开发板和3.3V TTL电平的WT3000TX模块为例接线如下ESP32引脚WT3000TX模块说明GPIO17RX模块串口接收ESP32发送数据给TTS芯片GPIO16TX模块串口发送TTS芯片返回状态给ESP32GNDGND必须共地3.3V独立LDOVCC给TTS芯片供电5V功放VCC功放单独从5V取电GPIO16和GPIO17是ESP32的UART2默认引脚我用的是这个组合。你也可以换成其他支持任意引脚映射的UART口但接完之后记得在代码里把引脚号对应上。3. WiFi接入的坑连接、重连与超时3.1 WiFi.begin之后不能一直等ESP32的WiFi连接不是即时的从调用WiFi.begin到连接成功可能需要一两秒甚至更久取决于路由器响应速度。很多人习惯写一个while循环死等直到WiFi.status() WL_CONNECTED才往下走。这个写法最大的问题是如果路由器信号弱、密码错误、或者路由器关掉了2.4G频段while循环会一直卡住。更糟糕的是有些情况下卡在WiFi连接中的死循环会让看门狗超时ESP32直接重启。我用的方式是带超时时间的轮询每100毫秒检查一次状态超过10秒还没连上就放弃先播报一句“网络连接失败”然后进入独立的重连状态。这样设备不会因为网没连上就变成一块砖。3.2 断线重连不能靠侥幸WiFi路由器重启、家里断电再恢复、你拿着设备走远又走回来这些场景都会导致WiFi断开。如果只靠ESP32上电那一次连接后面断了就再也没机会恢复。有三种常见的断线重连策略周期检测在主循环里每隔几秒检查一次WiFi.status()不是WL_CONNECTED就调用WiFi.reconnect()。事件回调用WiFi.onEvent注册WL_DISCONNECTED事件在事件回调里触发重连。组合方案事件回调负责快速响应周期检测作为兜底。我的实际经验是事件回调虽然优雅但回调函数里不能做太多耗时操作否则会影响协议栈工作。所以我一般是事件回调里只设置一个标志位真正的重连动作放在主循环里做避免在中断上下文里搞复杂逻辑。3.3 HTTP请求要设置超时不然卡到你怀疑人生WiFi显示已连接并不代表能访问外网。常见情况是路由器本身能连上但连不上外网域名也可能是路由器用的透明代理拨号断了。这时候如果你直接发HTTP请求可能一直等不到响应整个设备卡死在等待里。我写HTTP请求的时候都会显式设置超时HTTPClient http; http.setTimeout(5000); // 最多等5秒另外如果请求的是HTTPS接口ESP32需要初始化TLS库这个过程比较吃内存而且对老版本的Arduino库兼容性不太好。我的建议是如果只是做简单天气/时间播报优先用HTTP接口避免TLS握手带来的问题如果一定要HTTPS做好内存规划并且用ESP32官方库比较新的版本。这块我从几轮折腾里得到的体会是智能设备“不会说话”的失败根源往往在第一次网络交互就卡死了。把超时和重连逻辑做扎实后面整个系统才能长期稳定跑。4. TTS芯片通信帧格式、文本编码与一次完整播报4.1 串口参数和帧格式WT3000TX这类离线TTS芯片通信接口本质就是UART透传。初始化时注意把串口参数和芯片固件匹配上常见的是波特率9600或115200数据位8位无校验位1位停止位。发送命令的帧格式因芯片型号而异但通用套路是帧头 数据长度 命令字 数据内容 校验。我在项目里先仔细读了一遍模块手册再写的代码不同的批次固件版本都可能不一样一定不要凭感觉照抄网上代码。我封装了一层发送函数后续如果换芯片型号只需要改这一层即可。4.2 中文编码一个隐藏的大坑TTS芯片的文本编码通常有两种支持GBK和UTF-8。而Arduino环境下默认的字符串常量编译后是什么编码取决于编译器的运行环境如果你从HTTP接口动态获取数据返回的内容几乎总是UTF-8。如果芯片只支持GBK而你的文本是UTF-8直接发过去会合成乱码。花了两天时间排查才发现中文编码不匹配才是“语音播报全是乱音”的元凶一度还以为是硬件问题。最简单的处理办法是买支持UTF-8输入的新型TTS芯片省去编码转换。如果芯片只支持GBK那就需要做一次编码转换我写了小的转换函数原理就是维护一张Unicode到GBK的码表逐字符转换。注意不要用系统级的iconv那样体积太大ESP32放不下。4.3 一次播报的状态流转一次完整播报看起来只是“发一句文字给芯片”但实际运行时要考虑芯片的处理时间。芯片收到整帧文本后需要几百毫秒合成语音播报期间如果又收到新数据不同固件的处理方式也不同有的会排队有的直接丢弃。我的做法是做一个简单的播报状态机空闲态没有播报任务可以接受新的文字。发送态把文本按帧格式打包通过串口发给TTS芯片。等待完成态发送完成后不再发新数据等待芯片播报结束。芯片如果有状态输出引脚可以接一个GPIO读取播报完成信号没有的话就根据文本长度估算一个播报时长用延时代替。状态机的好处是主循环不会被播报过程阻塞中途还能继续处理网络请求和按键响应。从整体架构上看这比“直接串口.write完就继续跑”要可靠得多。5. 核心代码骨架从开机到报出第一句话5.1 工程结构和初始化流程我的工程基于Arduino框架因为生态好、调试方便、例程多。整个工程就一个main.cpp通过PlatformIO管理编译比Arduino IDE在库管理和代码跳转上舒服很多。setup函数里做四件事void setup() { Serial.begin(115200); // 调试串口 ttsSerial.begin(9600, SERIAL_8N1, 16, 17); // TTS芯片串口 initWiFiWithTimeout(); // 带超时的WiFi初始化 initNTP(); // 网络时间同步 if (WiFi.status() WL_CONNECTED) { sendTTS(网络连接成功设备已准备就绪); } else { sendTTS(网络连接失败请检查路由器); } }调试串口和TTS串口我用的是不同的UART实例两者互不干扰。initNTP做的是从NTP服务器获取当前时间这样后续整点报时和定时播报才有时间基准。5.2 sendTTS发送函数的封装发送函数是所有语音功能的地基。我封装成下面这样后续所有播报逻辑都调用它void sendTTS(const char* text) { // 构造数据帧并发送帧格式以实际芯片手册为准 uint8_t frame[256]; int len buildTTSPacket(frame, text); // 根据手册把text打包进frame ttsSerial.write(frame, len); ttsSerial.flush(); // 简单等待芯片完成实际项目中建议用状态引脚 delay(estimatePlaybackTime(text) 200); }buildTTSPacket里需要注意文本帧的超长问题。实测下来一次性发送几百字节的长文本有些芯片会因为缓冲区溢出而直接丢弃整帧。稳妥做法是把长文本按标点符号切分成多个短句一句一句发送中间加一点延时。5.3 主循环状态机驱动不阻塞主循环我用了一个简单的时间片轮询结构不在任何一点死等void loop() { handleWiFiReconnectIfNeeded(); // 断线检查 handleButton(); // 按键触发播报 handleSchedule(); // 整点/定时播报 handleHttpNotification(); // 拉取远程通知消息 delay(50); }这样写的好处是任一环节都不能阻塞太久。比如说拉取天气接口设置了5秒超时那这个5秒只是循环里的一个片段不会影响其他任务的响应。实测中整个设备跑了好几周没有出现过需要拔电重启的情况。6. 实测中踩过的四个坑与完整排查链路6.1 播报内容只有第一句后面全部哑火现象设备上电后播报了第一句“网络连接成功”之后就怎么触发都没有声音。排查过程我先加了调试串口打印发现触发播报的函数确实被调用了sendTTS也执行了。再检查串口波形发现第一帧发出去之后第二帧就没有正常到达芯片侧。用逻辑分析仪抓TXD引脚发现第二帧的数据明显比第一帧短像是被截断了。根因这是一个典型的帧发送太快导致的丢帧问题。第一帧发送完底层没有等待足够的时间让芯片处理第二帧紧接着到达芯片还没从合成状态恢复过来数据被丢弃。解决在sendTTS函数末尾增加300毫秒的间隔同时尽量做到“一条播报发完再发下一条”不做并发。改完以后哑火现象消失。6.2 WiFi断开后设备变“砖”现象断电重启、路由器重启后设备无法自动恢复联网播报功能也失去响应。排查过程开始以为是WiFi模块坏了后来打印日志发现设备一直卡在一个重连的死循环里。代码里写的逻辑是“断开就while循环重连”一旦路由器没有重新启动设备就在循环里空转主循环完全被占死播报函数根本没有机会执行。根因死循环重连把整个系统拖进了阻塞状态。WiFi是否连接和语音播报本来应该是两个独立的关注点不该互相滞后。解决改用前面提到的“标志位主循环检测”方案重连动作每次最多持续一个时间片如果没连上先播报“网络未连接”让用户知道设备还活着。改完之后哪怕断网三天设备也会安静地尝试重连不会死机。6.3 播报时有“嘶嘶”底噪现象语音播报时背景有持续的沙沙声听久了耳朵累。排查过程一开始怀疑是TTS芯片输出信号本身质量差后来把喇叭拔掉底噪还在用示波器查电源轨发现播报瞬间3.3V电源上有明显的纹波抖动约等于200mV的噪声。根因音频电路的电源没做干净。TTS芯片和功放的电源线从ESP32板载LDO出来和主控电路共用一条路径数字信号和模拟音频信号在电源上互相干扰。解决重新按第二章的供电思路改板TTS芯片独立供电功放直接吃5V并用一个大容量的电解电容储能彻底分隔模拟地和数字地底噪降到几乎听不见。6.4 发送长字符串时芯片完全无响应现象播报超过一百个字的文本时芯片没反应调试串口也没有报错。排查过程逐段缩减文本长度发现缩到80字以内就能播报超过80字就完全没声音。进一步分析问题出在我一次性把整帧下发芯片串口接收缓冲区装不下导致整帧被丢弃。根因串口缓冲区长度有限长文本一次性发送超过芯片处理上限。解决在sendTTS里增加分片逻辑按句号、逗号把长文本切成小段每段不超过50字逐段发送并加短暂延时。这样一来再长的文本也能稳定播报。现象根因解法只播第一句后续哑火帧间隔太短芯片未就绪增加300ms间隔串行播报WiFi断开后死机死循环重连占死主循环改标志位主循环检测播报有底噪电源纹波干扰音频信号独立供电模拟数字地分离长文本不播报超过串口缓冲区按标点分片发送7. 从一个语音播报节点到整套通知系统做完基础的WiFiTTS之后我马上开始琢磨这套东西还能延伸到哪里。几个实际的扩展方向对大家有参考价值7.1 接入MQTT让服务器主动推送播报HTTP轮询的缺点是实时性差而且每几秒请求一次对服务器不算友好。改用MQTT协议后服务器可以随时推送消息给ESP32设备收到消息就调用sendTTS播报。这个非常适合做家庭告警系统比如家里烟雾传感器报警服务端推一条消息到设备立刻语音播报“厨房烟雾浓度过高”。MQTT接入在ESP32上的做法很成熟使用PubSubClient库只需要指定broker地址、主题、订阅回调在回调函数里调用sendTTS即可。注意回调函数运行在协议栈上下文不要在回调里直接做长延时把播报内容放到一个队列里由主循环去消费。7.2 整点报时与定时提醒用NTP同步完时间后设备就具备了本地时钟。在主循环里判断当前分钟数每到整点触发一次播报“现在是上午九点整”。再配合一个简单的定时任务表可以做成吃药提醒、起床提醒、浇花提醒。这里的关键点是时间逻辑要写在主循环里而不是用delay嵌套避免设备挂起期间错过提醒。7.3 接入温湿度、门磁传感器做本地联动给ESP32接一个DHT22温湿度传感器每五分钟播报一次室内温湿度再接一个门窗磁传感器门开的时候播报“大门已打开”。这些功能本质上是把“联网获取远程信息”和“本地采集信息”统一到同一个播报通道里不需要额外硬件加传感器和判断逻辑就行。从我个人实际使用的角度说这套方案最值得推荐的地方是它的稳定性和极低的维护成本。系统跑起来以后基本是“拧上电就不管了”。唯一需要长期关注的就是供电质量和路由器的2.4G频段设置这两点做好了设备可以连续运行几个月不重启。如果你计划复刻这个项目我的建议是第一先把你手上的TTS芯片手册完整读一遍确认串口波特率和帧格式再开始写sendTTS函数第二接线时优先解决电平匹配和电源分离硬件基础不牢后面软件怎么调都白搭第三代码结构不要用一堆delay串尽量用状态机驱动这样你后续加功能会轻松很多。做好这三点你也能在半天内跑出一个会开口说话的智能通知设备。
返回列表