☰
天问ASR-PRO与ESP32串口通信:离线语音控制智能家居DIY实战
2026/10/3 6:00:57 网站建设 项目流程

1. 项目背景:为什么把语音识别板和一个WiFi模组凑到一起

先说清楚这个项目到底在做什么:用天问ASR-PRO做本地语音识别,识别到指令之后通过串口把数据发给ESP32,ESP32拿到数据之后做两件事——按协议执行对应动作,同时把结果通过语音合成播报出来。整个链路里没有云、没有服务器、没有App,全是本地化处理。

做这个项目的初衷很实际。家里或者小工作室里总有一些设备,开关灯、控制风扇、播报温湿度,场景不算复杂,但用手机App控制总觉得绕。喊一嗓子就能解决的问题,没必要掏出手机解锁、打开App、等页面加载。天问ASR-PRO这块板子的优势是离线语音识别,不需要联网,识别速度也够快;ESP32则是便宜、生态成熟、MicroPython写起来快,两者通过串口交互,各干各擅长的活。

这类组合其实在智能家居DIY圈子里很常见,但网上能找到的教程大多数只讲了一半:要么只讲天问ASR-PRO怎么训练模型、怎么配置语音指令,要么只讲ESP32怎么用串口接收数据。真正把两端串起来、把协议定清楚、把数据格式对齐、把播报逻辑跑通的完整案例反而比较少。这篇就把整个链路从头到尾捋一遍。

适合看这篇的人分三种:第一种,手里有天问ASR-PRO,想让它干点除了播放固定音频之外的事;第二种,玩ESP32和MicroPython,想给它加一个语音交互入口;第三种,对串口通信协议设计感兴趣,想看看两个单片机之间到底怎么"说话"才不容易出乱子。

2. 硬件的坑与选型的门道

2.1 天问ASR-PRO的串口资源分配

天问ASR-PRO这块板子,说白了是一个以离线语音识别为核心的主控板,板载麦克风阵列,也带了功放可以驱动喇叭。它本身算是一个完整的单片机系统,不是单纯的语音模块,这点很重要——因为它的串口资源是有限的,而且有些引脚默认被占了。

在做这个项目之前,我在天问Block图形化编程环境里折腾过好一阵。ASR-PRO的默认配置里,P2和P3是调试串口,P6和P7也可以配置成串口使用,还有P10、P11等引脚也可以复用。但是,板子上如果接了功放驱动喇叭,部分引脚会被占用;如果用到了LED灯、按键这些外设,也会吃掉一部分引脚资源。所以第一步不是急着写代码,而是先把引脚规划清楚。

我的实际接法是:天问ASR-PRO的P18作为发送脚(TX),P19作为接收脚(RX),波特率固定在9600。为什么选P18和P19,因为这两个引脚在默认配置下没有被音频电路占用,而且在天问Block的串口配置组件里可以直接选择,不需要做复杂的引脚映射。

2.2 ESP32侧选哪个开发板

ESP32系列的型号很多,ESP32、ESP32-S3、ESP32-C3价格和特性各不相同。就这个项目而言,核心需求是:一路UART、一个MicroPython固件、能跑基本的GPIO控制,对性能要求不高。所以最普通的ESP32 DevKitC就够了。

但如果要兼顾后续扩展,比如加屏幕、加传感器、加摄像头,那建议直接上ESP32-S3,引脚更多,PSRAM版本还能跑更复杂的应用。

我手里常备的是ESP32 DevKitC和ESP32-S3-DevKitC-1两块板子,这个项目用的是前者,纯粹是因为手头正好有空闲的。需要注意一个细节:ESP32 DevKitC的USB转串口芯片有两种版本,老款是CP2102,新款可能是CH340,两者在Windows下的驱动不一样,烧录失败大概率就是驱动没装对。这个后面会专门说。

2.3 电平匹配:3.3V与5V的坑

天问ASR-PRO的工作电压是5V,而ESP32是3.3V IO。两块板子的串口直接对接,电平不匹配,轻则通信不稳定,重则烧坏引脚。

实际测试中,天问ASR-PRO的TX输出高电平是5V,直接接到ESP32的RX引脚上,ESP32的GPIO是容忍5V输入的(数据手册里写的是部分引脚支持),但为了稳妥,我加了一个电平转换模块,用的是一块几块钱的双向电平转换板,把5V侧的TX/RX转成3.3V再进ESP32。

这里多说一句:网上有人说"直接接也没事",我试过,短时间确实没烧,但串口数据偶尔会出现乱码,尤其是天问ASR-PRO播报时电流波动大,电平容易被拉偏。所以别省这个电平转换,几块钱的事,能省掉很多排查时间。

2.4 供电方案的注意事项

天问ASR-PRO板载功放驱动喇叭时,峰值电流不小,如果用同一个USB口给两块板子供电,播报语音的瞬间会导致电压跌落,ESP32可能会重启。我的做法是:天问ASR-PRO用单独5V电源供电(可以用充电头或者电池),ESP32用USB口供电,两块板子共地。

共地是串口通信的基本要求,不共地的话,信号线的参考电位不一致,数据必然乱。实际接线时,两块板子的GND用一根杜邦线连起来。

3. 天问ASR-PRO端的配置细节:从图形化到代码生成的转换

3.1 天问Block的工程配置流程

天问ASR-PRO的官方编程环境是天问Block,图形化拖拽的方式,支持串口组件、语音识别组件、播报组件等。整个项目的语音侧配置可以分成三步:

第一步:新建工程,选择主控芯片。在天问Block首页新建工程时,选择天问ASR-PRO对应的芯片型号,会自动生成一个带语音识别和播报能力的模板工程。

第二步:配置语音识别指令。左侧组件栏里找到"语音识别"相关组件,新建识别词列表。比如我这个项目里,识别词是"打开灯""关闭灯""温度""湿度"这些。注意,每个识别词会对应一个ID,比如"打开灯"是1,"关闭灯"是2,这个ID后面要用来作为串口发送的数据内容。

模板工程的代码区域会有语音识别回调函数,当识别到指令时会进入对应的回调,里面默认是串口打印,需要改成串口发送。

第三步:配置串口组件。左侧组件栏里找到"串口"组件,设置端口、波特率。这里波特率要和ESP32保持一致,我用的9600。发送数据时,可以直接发送字符串,也可以发送十六进制数据。我选择发送十六进制数据,因为协议解析更明确。天问ASR-PRO在图形化组件里发送十六进制数也很方便,直接把识别词ID转换成对应的指令字节即可。

3.2 定义一套简单可靠的通信协议

这是整个项目里我认为最关键、但在网上教程里经常被忽略的一步。

语音识别结果到设备执行指令之间,数据格式不能想怎么发就怎么发。定义一种帧格式,两端按约定解析,才能保证不串数据、不丢数据、不乱码。

我用的协议格式比较简单:

帧头(2字节) + 数据长度(1字节) + 指令码(1字节) + 校验(1字节) + 帧尾(1字节)

具体约定如下:

  • 帧头用0xAA 0x55
  • 数据长度为指令码+校验的字节数,也就是2
  • 指令码按功能定义:
    • 0x01:打开灯
    • 0x02:关闭灯
    • 0x03:查询温湿度
    • 0x04:播放指定音频(比如"当前温度为xx度"这类场景,纯播报场景)
  • 校验用的是和校验,也就是"数据长度 + 指令码"之后取低8位
  • 帧尾固定0x0D 0x0A(也就是\r\n),方便终端调试查看

举个例子,发送"打开灯"指令时,数据帧是:

AA 55 02 01 03 0D 0A

其中:

  • AA 55是帧头
  • 02是数据长度
  • 01是指令码
  • 03是校验和(0x02 + 0x01 = 0x03)
  • 0D 0A是帧尾

这个协议非常简单,但足够用。为什么不直接发一个字节?因为将来扩展指令会很多,没有帧头帧尾的协议,解析端很难判断一个完整帧从哪里开始、到哪里结束。有了帧头和帧尾,ESP32端接收时就可以做状态机解析,保证即使数据中间有杂质,也能重新同步。

3.3 天问ASR-PRO端发送指令的实现方式

在天问Block中,可以创建一个函数,专门用来发送上面这个帧。

具体操作是在"串口"组件里找到"发送十六进制数组"之类的组件,然后输入数组内容。但天问Block的图形化界面对数组处理不算友好,一个个填非常麻烦。我后来是这么处理的:直接在天问Block的代码视图里修改,用代码方式定义发送函数。

天问Block支持在图形化界面和代码界面之间切换,代码视图里可以写C语言风格的代码。定义一个发送函数,函数参数是指令码,函数内组帧发送。

这里有一个天问ASR-PRO的实际操作细节:天问Block生成代码后,串口发送功能默认可能是以字符串方式发送的,如果发送的是不可见字符(比如0xAA这种),要注意发送函数的类型,确保是HEX模式而不是文本模式。否则ESP32端收到的会是ASCII字符,而不是原始字节。

这个坑我踩过。最开始没注意,ESP32那边收到的是一串奇怪的字符串,比如ªU这样的,解析帧头永远对不上。排查了半天才发现是天问侧按文本模式发了。

3.4 语音识别回调函数里做状态判断

语音识别部分还有一个需要注意的点:天问ASR-PRO的语音识别回调会被频繁触发,尤其是在连续说多个词或者误唤醒的时候。如果在回调里直接发送串口指令,很容易出现重复发送、连续发送的情况。

我的做法是在回调函数里加了一个简单的去抖逻辑:记录上一次识别到指令的时间,如果两次识别间隔小于1秒,就忽略这次回调。这个通过天问Block的延时判断或者系统时间戳可以做。删除抖逻辑后,语音控制才真正变得可用——不然说一次"打开灯",灯可能闪两下,音箱也跟着播报两遍。

4. ESP32端MicroPython串口接收和协议解析

4.1 固件选择和基本引脚规划

ESP32端用MicroPython开发,第一步是烧录固件。去MicroPython官网下载适用于ESP32的固件bin文件,比如esp32-20230426-v1.20.0.bin这样的版本,然后用esptool烧录。

烧录命令大致是:

esptool.py --port COM3 --baud 460800 erase_flash esptool.py --port COM3 --baud 460800 write_flash 0x1000 esp32-20230426-v1.20.0.bin

注意如果烧录失败,大概率是板子没有进入下载模式。ESP32开发板通常一键按BOOT再按EN可以进入下载模式,但具体看板子。也有一个更省事的方法:很多ESP32 DevKitC在检测到串口打开时会自动进入下载模式,只需要在烧录工具里直接点烧录就能成功,不需要手动按键。这个因板而异,多试试。

引脚规划上,我用UART2作为接收串口,因为UART0默认连接USB转串口芯片,用来输出日志,UART1一般不推荐用于外部串口。

ESP32 DevKitC上UART2对应的引脚是GPIO16(RX)和GPIO17(TX)。我只用接收功能,所以接GPIO16即可。天问ASR-PRO的TX接ESP32的RX(GPIO16),天问ASR-PRO的RX接ESP32的TX(GPIO17),共地。

4.2 MicroPython中UART初始化和串口接收

MicroPython的machine.UART类使用很简单:

from machine import UART, Pin import time uart = UART(2, baudrate=9600, tx=Pin(17), rx=Pin(16)) uart.init(baudrate=9600, bits=8, parity=None, stop=1, timeout=50)

这里初始化UART2,波特率9600,8位数据、无校验、1位停止位,timeout设置为50毫秒,表示接收超时时间。

接收数据的方式有两种:轮询读取和中断回调。MicroPython中串口中断回调的支持在部分固件版本上有差异,我用的是最简单的轮询方式,在主循环里不断检查是否有数据到达。

def read_serial(): if uart.any(): data = uart.read() if data: return data return None

uart.any()返回当前接收缓冲区中的字节数,uart.read()读取所有可用数据。

4.3 帧解析状态机的实现

直接读字节不行,因为串口数据是流式的,可能一帧数据被拆成两段到达,也可能两帧数据粘连在一起。所以帧解析必须用状态机。

我写了一个简单的状态机,状态依次是:

  • WAIT_HEADER1:等待帧头第一个字节0xAA
  • WAIT_HEADER2:等待帧头第二个字节0x55
  • WAIT_LEN:等待数据长度
  • WAIT_DATA:等待指令码及其余数据
  • WAIT_CHECK:等待校验
  • WAIT_TAIL:等待帧尾

代码实现大致如下:

class FrameParser: def __init__(self): self.state = 0 self.buffer = bytearray() self.frame_len = 0 self.data_len = 0 self.check_sum = 0 def parse(self, byte): # 状态机解析 if self.state == 0: if byte == 0xAA: self.state = 1 else: self.state = 0 elif self.state == 1: if byte == 0x55: self.state = 2 else: self.state = 0 elif self.state == 2: self.data_len = byte self.buffer = bytearray() self.buffer.append(byte) self.state = 3 elif self.state == 3: self.buffer.append(byte) if len(self.buffer) == self.data_len: self.state = 4 else: self.state = 3 elif self.state == 4: self.check_sum = byte self.state = 5 elif self.state == 5: if byte == 0x0D: self.state = 6 else: self.state = 0 elif self.state == 6: if byte == 0x0A: return self._decode() else: self.state = 0 return None

这里稍微简化了。实际项目中缓冲区处理、校验计算要分开写,核心思路就是逐字节喂给状态机,状态机在里面转,转完整帧就返回一个解码结果。如果中间某个字节不符合预期,状态机回到初始状态重新等帧头。

用状态机的好处很直接:不管天问ASR-PRO什么时候发、发多少字节,ESP32都能稳定地解析出完整指令,不会因为拆包、粘包导致解析失败。

4.4 解析成功后的指令分发和执行

解析得到指令码之后,需要做指令分发。基本逻辑就是:

if cmd == 0x01: led.value(1) speak("灯已打开") elif cmd == 0x02: led.value(0) speak("灯已关闭") elif cmd == 0x03: temp = read_temperature() speak("当前温度是{}度".format(temp))

这里的speak是ESP32端通过串口回传给天问ASR-PRO的播报指令。因为天问ASR-PRO自带语音合成播报能力,所以ESP32执行完动作后,可以回送一条播报指令,让天问ASR-PRO播放对应的音频。

这里有个设计细节:如果不让天问ASR-PRO播报,ESP32自己也可以用喇叭播放简单的音频文件,但语音合成的效果远不如天问ASR-PRO自带的好,而且ESP32播放中文语音需要额外的TTS库。所以整个项目里,语音播报全交给天问ASR-PRO做,ESP32只负责发送播报指令。

5. 语音播报回传链路的实现

5.1 回传指令协议设计

回传播报指令和上行控制指令共用同一套帧协议。为了让两端的代码逻辑统一,我定义了下行播报指令码范围:

  • 0x10:播报"灯已打开"
  • 0x11:播报"灯已关闭"
  • 0x12:播报"当前温度为xx度"
  • 0x13:播报"当前湿度为xx%"
  • 0x14:播报"没有识别到有效指令"

这里有一个小技巧:播报内容不是由ESP32直接传字符串给天问ASR-PRO,而是传指令码,由天问ASR-PRO端根据指令码播放预置好的音频。这样有两个好处:

  • ESP32不用处理中文编码,MicroPython处理UTF-8字符串虽然没问题,但传输字符串容易出错,而且要定义字符串协议,解析复杂
  • 天问ASR-PRO端可以预先把文字转成语音音频,离线直接播放,延迟更低

温度这种动态数据怎么播报?方案是这样的:ESP32把温度数值通过串口以单独的数据帧传过去,天问ASR-PRO端在接收到温度数值后,用板载的TTS或者数字播报组件把数字拼到语音里。天问ASR-PRO支持播报动态数字,比如"当前温度是"加一个数字变量,这个在天问Block里可以很方便地配置。

5.2 ESP32端发送回传指令的代码

MicroPython端发送一个完整的帧,代码可以这样写:

def send_cmd(cmd_code, payload=b""): data_len = 1 + len(payload) check = (data_len + cmd_code) & 0xFF frame = bytearray([0xAA, 0x55, data_len, cmd_code]) + payload + bytes([check, 0x0D, 0x0A]) uart.write(frame)

这个函数会在UART2上发送一个标准帧。注意这里的校验和是简单和校验,天问ASR-PRO端解析时同样计算并比对。

5.3 天问ASR-PRO端解析回传指令

天问ASR-PRO端在接收到ESP32发来的帧之后,需要解析指令码,并根据指令码播放对应的音频。在天问Block中,可以设置串口接收回调,当收到完整的数据帧时触发,再在里面判断指令码并调用"播报"组件播放相应内容。

天问Block的串口接收配置有两种模式:一种是"接收任意数据"触发回调,一种是"按协议接收"。如果天问Block支持按协议接收模式,可以配置帧头和帧尾,它会自动帮做协议解析;如果不支持自定义协议,就只能在回调中裸数据判断。

考虑到天问Block的图形化限制,我在实际项目里把串口协议配置放在天问ASR-PRO端的固件里用代码方式实现,图形化界面只是作为辅助配置。天问Block生成的代码是基于C的,支持在代码中直接操作串口寄存器。具体到"播报动态温度"的场景,可以预先在代码中准备一个语音播报缓冲区,把"当前温度是"这几个字对应的音频ID和一个动态数字拼接起来,再调用播报接口。

这里有一个实际经验:天问ASR-PRO的播报功能依赖于音频资源文件,如果你在工程里没有添加对应的音频资源,调用播报接口也不会有声音。所以需要提前在天问ASR-PRO的音频管理组件里录制或上传好"灯已打开""灯已关闭""当前温度是""度""百分之"这些音频片段,然后在代码中按顺序播放拼接。

6. 温度传感器接入和异常处理的实战记录

6.1 温湿度传感数据采集

设备端除了开关灯,我还加了一个温湿度查询功能,用的传感器是DHT11(后来换了DHT22,精度高一些)。DHT22接在ESP32的GPIO4上,MicroPython采集数据很简单:

import dht d = dht.DHT22(Pin(4)) def read_temp_hum(): try: d.measure() temp = d.temperature() hum = d.humidity() return temp, hum except OSError as e: print("DHT sensor error:", e) return None, None

实际使用时,DHT22的读取间隔必须大于2秒,否则会读到错误数据或直接不响应。这个传感器本身比较慢,轮询太快没有意义。

6.2 串口接收异常情况与排查

把这个项目实现完后,实际跑测试时遇到了几个典型的异常情况,记录在这里,给大家做个参考。

第一个问题是串口数据乱码。天问ASR-PRO播报语音的时候,喇叭大功率工作,5V电压跌落,可能影响串口电平,导致ESP32收到乱码。解决方法是前面说的独立供电+共地,如果还不行,就加一个100uF以上的电解电容在5V电源上,作为缓冲。实测加了电容后,播报瞬间电压跌落明显减轻,串口乱码几乎消失。

第二个问题是帧解析偶尔失败。排查下来是帧头和帧尾之间的校验不对。天问ASR-PRO发数据时,默认在发送HEX数据时也可能自动追加了额外的字节,比如\r\n。如果ESP32端解析时不把帧尾的0D 0A当作约定的一部分,而是等待一个纯字节序列,那就会一直解析失败。解决办法是两端协议必须严格一致,不能只约定数据内容不约定帧边界。

第三个问题是ESP32在点击串口烧录时失败。这个很常见,尤其是Windows下。CH340驱动没装,或者驱动版本不对,都会导致烧录失败。我自己的解决步骤是:先检查设备管理器里是否识别到串口,没有的话安装CH340驱动,有但烧录失败的话,按住BOOT键不放再点烧录,等到烧录工具显示连接成功后再松手。这个方法基本能解决90%的烧录失败问题。

6.3 完整的主循环逻辑设计

ESP32端主循环的逻辑设计完成版本大概是这样:

def main(): parser = FrameParser() while True: if uart.any(): data = uart.read() for b in data: result = parser.parse(b) if result: handle_command(result) # 执行其他状态机任务 time.sleep_ms(10)

主循环必须加time.sleep_ms(10),不然会一直空转,占满CPU,导致传感器读取和串口数据接收互相干扰。加了这个延时之后,MicroPython的调度更平稳。

这里还有一个细节:uart.any()的返回值可能大于1,比如一次收到多个字节,所以要循环逐字节喂给状态机。千万不要只读一个字节就退出循环,否则数据会积压。

7. 项目最终效果和几个可以继续扩展的方向

整个项目跑通之后,最终效果是:对着天问ASR-PRO说"打开灯",大约0.5秒内灯亮起,同时天问ASR-PRO播报"灯已打开";说"温度",ESP32读取DHT22温度,通过串口把温度值发给天问ASR-PRO,天问ASR-PRO拼接播报"当前温度是26.3度";说"关闭灯",灯灭,播报"灯已关闭"。

整个过程完全离线,不需要任何云服务,延迟主要在语音识别和传感器读取上,体感很流畅。

这个项目还可以扩展的方向:

  • 把串口协议从9600波特率提高到115200,响应更快,但要注意线路长度和电平转换模块的稳定性
  • 在ESP32端接入更多传感器,比如人体红外、光照传感器,实现"人进灯亮""光线暗自动开灯"等联动逻辑
  • 用ESP32接入MQTT,语音指令通过串口到ESP32,再由ESP32通过MQTT控制其他智能设备
  • 把天问ASR-PRO的识别词表扩展成多轮对话模式,比如"查询空调状态""设置温度到25度"
  • 加一块小屏幕在ESP32侧,把语音指令和传感器数据显示在屏幕上,方便调试

但不管怎么扩展,有一个核心原则不要变:两端之间的串口协议是整个系统最底层的地基,协议不稳定,上面所有功能都白搭。做任何扩展之前,先把通信协议想清楚、写规范、做好状态机解析。

我自己在做的过程中最深的一个体会是:两个廉价开发板之间的串口通信,看似简单,真正做扎实需要花不少心思——要考虑电平匹配、供电稳定性、协议设计、异常恢复。但只要把地基打牢,整个系统就能稳定运行很久。这也是这类DIY项目最有趣的地方:你不是在拼积木,是在造一个真正懂你的小设备。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询