尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

51单片机软串口实现离线语音识别模块LU-ASR01通信实战

51单片机软串口实现离线语音识别模块LU-ASR01通信实战 做语音控制类的单片机项目最让人头疼的不是语音识别模块本身而是串口。51单片机上常见的型号比如STC89C52只有一组硬件串口平时烧录程序要它、接蓝牙模块要它、接LU-ASR01语音模块还要它一个串口根本不够用。我这次用天问Block搭开发环境用LU-ASR01做离线语音识别和语音播报中间就是靠软串口解决了多设备抢串口的冲突问题。这篇文章把从原理到代码、从图形化积木到调试排错的全过程梳理一遍给打算在51平台上做语音交互的朋友一个可直接参考的模板。文章涉及的代码以STC8/STC15系列1T内核为主因为天问Block配套的开发板大多是这两类芯片。如果你用的是STC89C52原理完全一样只是定时器初值和延时函数需要按12MHz双周期去换算。我会在对应位置把换算关系标出来。1. 项目起源当语音模块遇上51单片机串口不够用的现实问题1.1 LU-ASR01是什么它擅长解决什么LU-ASR01是一款离线语音识别模块也带语音播报功能。它最大的特点是所有识别都在本地完成不需要联网也没有云端延迟所以非常适合51单片机这种资源有限的嵌入式平台。具体来说LU-ASR01支持两种工作模式一种是自学习模式用户直接对着模块说话模块把语音特征存下来以后识别到这个特征就触发对应动作另一种是词条配置模式用上位机软件把固定的命令词烧录进模块比如打开灯关闭灯播放音乐每个词条对应一个命令ID。模块识别到语音后会通过串口把命令ID发送给MCU。我选它的原因很直接51单片机跑不了复杂的神经网络模型单纯靠MCU做语音识别不现实。LU-ASR01把识别这部分全包了MCU只需要通过串口接收命令再执行工作量瞬间降下来。而且这块模组还支持TTS语音合成播报MCU发给它一个播报指令它就能把对应的语音内容读出来。1.2 为什么选择天问Block做开发而不是纯手写C天问Block是面向STC系列单片机的图形化编程工具操作方式类似Scratch拖拽积木就能生成C代码同时也支持在代码区直接写函数再通过自定义积木的方式封装起来。我选它的原因主要有三个。第一开发效率高。语音模块的协议调试是个反复试错的过程用纯C手写串口解析代码不是不行但每改一次都要重新编译烧录。天问Block的图形化界面让初始化配置一目了然尤其是串口波特率、定时器模式这些繁琐的寄存器设置积木拖出来就能跑通省去大量翻数据手册的时间。第二调试交互好。天问Block有串口监视器能直接看到MCU发出的数据包这对排查LU-ASR01通信问题帮助非常大。相比用示波器一个个数波形可视化调试要友好太多。第三代码和图形可混编。软串口底层驱动没办法完全用积木表达但天问Block支持写C函数然后用一条UDF积木去调用。也就是说底层驱动用C写保证性能业务逻辑用积木搭保证可读性两边的好处都占了。1.3 软串口在整套方案里的定位现在回到标题里的核心词软串口。什么是软串口就是用普通IO口通过程序精确控制电平翻转时序模拟出一路串口收发数据。相比之下51单片机的硬件串口是芯片内部的外设收发数据不占用CPU。那为什么非要用软串口因为我的项目里有两个设备需要走串口LU-ASR01语音模块和蓝牙下载/调试模块。硬件串口只有一组如果都往硬件串口上挤就得做多路切换不仅电路复杂软件上还得处理总线冲突。用软串口把LU-ASR01挂到另外两个IO上硬件串口留给下载和调试两路互不打扰。软串口牺牲的是CPU时间但语音交互这个场景本身对实时性要求不高识别结果几百毫秒才来一次完全够用。这个取舍是值得的。2. 软串口跑起来的底层原理定时器、起始位中断与9600波特率2.1 串口时序拆解一个字节长什么样要搞懂软串口先得明白串口物理层的一个字节长什么样。串口在空闲状态时TX引脚保持高电平。发送一个字节时先拉低电平一个位时间这叫起始位告诉接收端我要发数据了然后依次发8个数据位从最低位开始最后再拉高一个位时间叫停止位。如果后面还有数据可以直接接下一帧的起始位如果没有就保持高电平空闲。一个完整的字节帧结构是1位起始位 8位数据位 1位停止位总共10个位时间。波特率9600的意思是每秒传输9600个位每个位的时间是1/9600秒约104.17微秒。软串口的本质就是发送端用程序控制IO口按这个时序翻转电平接收端用程序按这个时序去采样电平。硬件串口是芯片内部电路自动完成的软串口则全靠CPU一条一条指令执行。2.2 发送方向翻转电平不复杂难在精确延时软串口发送相对简单核心就是一个按位输出的循环void soft_uart_send_byte(unsigned char byte) { unsigned char i; // 起始位拉低 TX_PIN 0; delay_half_bit(); // 1个位时间 delay_half_bit(); // 8个数据位从LSB开始 for (i 0; i 8; i) { if (byte 0x01) { TX_PIN 1; } else { TX_PIN 0; } byte 1; delay_half_bit(); delay_half_bit(); } // 停止位拉高 TX_PIN 1; delay_half_bit(); delay_half_bit(); }这里最容易翻车的就是延时函数的精度。9600波特率下每个位时间是104微秒左右如果延时偏差太大累积起来接收端就会采到错误的位。我用的是STC8的1T内核主频24MHz一个指令周期只有1/24微秒可以用定时器来做精确定时。如果用的是12MHz晶振的STC89C52每个机器周期是1微秒12MHz下9600波特率每个位约104个机器周期直接for循环空转计数就能做到近似精确。但for循环会受编译器优化影响保险做法还是用定时器中断翻转或者用nop()指令精确填充。2.3 接收方向外部中断检测下降沿定时器半位采样发送好搞接收才是软串口的重头戏。接收电路不知道对方什么时候发数据必须时刻监听RX引脚。一旦检测到起始位的下降沿电平从高变低就要掐准时间把8个数据位依次采出来。业内最常用的方案是外部中断 定时器组合把RX引脚接到51单片机的外部中断INT0或INT1引脚配置为下降沿触发。空闲时RX是高电平不会触发中断。当对方发送起始位时RX从高拉低触发外部中断。进入中断后先延时1.5个位时间此时正好落在第一个数据位的正中间读取IO电平作为第0位。之后每隔1个位时间再采一次连续采8次得到完整字节。为什么要在位的正中间采样因为数据位电平在位的边界处可能还在翻转跳动中间是最稳定的时刻。就好像接人下车你等他站定了再去接比他在车门边一脚踩地一脚踩车的时候去拉他靠谱得多。核心代码框架如下// 外部中断0服务函数RX引脚接INT0 void ext0_isr(void) interrupt 0 { unsigned char i; // 关闭外部中断防止接收过程中再次触发 EX0 0; // 延时1.5个位时间让采样点落在第一个数据位中间 delay_half_bit(); // 0.5 bit delay_half_bit(); // 0.5 bit delay_half_bit(); // 0.5 bit合计1.5 bit // 连续采样8个数据位 for (i 0; i 8; i) { soft_rx_buf | (unsigned char)(RX_PIN) i; // LSB first delay_half_bit(); delay_half_bit(); // 1 bit } // 重新开中断准备接收下一帧 EX0 1; }这个方案有个隐患如果中途接收出错比如误触发中断、位采样错误后续的数据都对不上了。所以实际项目里我会加一个简单的超时保护在一帧数据接收完成后如果发现停止位不是高电平就判为错误帧丢弃重新进入监听状态。2.4 用哪个定时器、什么晶振最省心51单片机做软串口选定时器有个原则用硬件串口不用的那个定时器。因为硬件串口本身就要定时器T1做波特率发生器软串口再用T1就打架了。所以硬件串口用T1软串口就用T0。还要注意中断优先级的分配。外部中断检测起始位是时间敏感的不能被打断太久。我的做法是把外部中断0设为高优先级定时器0软串口节拍设为低优先级硬件串口中断居中。这样即使主程序正在处理别的事情语音模块发来的起始位也能第一时间响应。晶振建议优先选11.0592MHz或者22.1184MHz。这个频率是串口波特率的标准频率因为11.0592MHz可以被常见的9600、115200整除硬件串口算出来的波特率误差为零。虽然软串口对晶振没那么敏感但硬件串口还需要它选这个频率能省掉很多波特率偏差的坑。3. 图形化搭建与底层驱动天问Block里的双向通信框架3.1 先在积木里把硬件串口跑通再做软串口在天问Block里开发我习惯先把硬件串口调通再碰软串口。原因很简单软串口出问题时需要硬件串口打日志来辅助定位如果两个通信链路同时是坏的排查起来非常痛苦。天问Block的串口初始化积木很直观。选择对应的串口比如UART1设置波特率9600、8位数据、无校验、1位停止位拖出来就行。我会在初始化后加一条自我回环测试MCU发送一个0x55正常情况下串口调试助手上应该看到0x55。如果能看到说明硬件串口链路没问题接下来软串口调试就有了可靠的观察通道。3.2 驱动封装把软串口收发封装成UDF积木天问Block里没有专门的软串口积木但可以通过UDF用户自定义函数功能把C代码封装成图形化积木。我用两个函数SoftUART_Init和SoftUART_SendByte以及一条接收查询函数SoftUART_CheckReceived。封装好的积木在图形化界面里可以直接拖出来用后面写业务逻辑就像搭积木一样// UDF: 初始化软串口 void SoftUART_Init(void) { // 设置引脚模式为准双向口 P_SW2 | 0x80; // 访问扩展SFR使能STC8 P1M0 ~0x0C; P1M1 ~0x0C; // 配置外部中断0为下降沿触发接P3.2引脚 IT0 1; // 下降沿触发 EX0 1; // 开外部中断0 EA 1; // 开总中断 // 初始化接收环形缓冲区指针 soft_rx_head 0; soft_rx_tail 0; }业务逻辑里需要判断模块是否来了新数据只需要查询缓冲区即可。天问Block的循环积木里加一个如果判断当接收标志位置位就说明有一帧完整数据到达。3.3 双向通信协议设计帧格式、命令表与CRC校验串口是物理层真正让两个设备有条不紊地说话还得靠协议。我给MCU和LU-ASR01之间定义了一套简单但完整的帧格式帧头1帧头2类型命令字数据长度数据区校验和0xAA0x550x010x01~0x10NN字节累加和帧头用0xAA 0x55两个字节是为了防止数据区里偶尔出现相同字节导致错位。校验和把所有字节从帧头到数据区末尾累加取低8位接收端重算一遍做比对不一致就丢弃整帧。最关键的还是命令字定义我把命令表整理如下方向命令字含义数据区说明MCU → 模块0x01播报指定语音语音ID1字节MCU → 模块0x02设置音量音量值0~100模块 → MCU0x10上报识别词条词条ID1字节模块 → MCU0x11上报唤醒成功无数据模块 → MCU0x12上报休眠事件无数据这个协议的好处是扩展方便。以后想加播放TF卡歌曲停止播放等功能只需要在命令表里追加命令字数据区的定义相应增加协议主体不用改。4. 完整实战离线语音控制设备主动播报一整套代码逻辑4.1 需求定义与词条编号映射为了让案例不仅有理论还有画面感我设计了两个实际场景场景A语音控制用户对着LU-ASR01说打开灯模块识别到词条ID 0x01通过串口发给MCUMCU控制继电器吸合用户说关闭灯词条ID 0x02MCU断开继电器。这里词条的ID分配是在LU-ASR01上位机软件里预先配置好的。场景B主动播报系统有一个温度传感器我就用51的ADC采集模拟量模拟了一下温度超过阈值时MCU通过软串口给LU-ASR01发送播报指令模块读出温度过高请注意。这样等于给设备装了张嘴能把内部状态说出来。在LU-ASR01上位机里我做了如下词条映射用户说的话词条ID模块串口输出打开灯0x01发送0x10 0x01关闭灯0x02发送0x10 0x02播报温度0x03发送0x10 0x03模块串口输出的内容也通过上位机配置识别到词条后上位机会按照预设的串口帧格式输出。我配置的是前面定义的帧格式模块接收到词条后自动组帧发出。4.2 语音识别方向模块收到指令后控制设备MCU这边主循环的逻辑非常清晰查软串口缓冲区有数据就解析解析出命令就执行。void main(void) { // 硬件串口初始化用于调试 UART1_Init(9600); // 软串口初始化接LU-ASR01 SoftUART_Init(); // LED初始化 P2M0 | 0x03; P2M1 ~0x03; LED_PIN 0; while (1) { // 查询软串口是否收到完整帧 if (SoftUART_CheckReceived()) { unsigned char cmd soft_rx_buf[3]; unsigned char param soft_rx_buf[5]; if (soft_rx_buf[0] 0xAA soft_rx_buf[1] 0x55) { // 校验和验证 if (check_sum(soft_rx_buf, soft_rx_len) 0) { if (cmd 0x10) { // 识别结果上报 if (param 0x01) { LED_PIN 1; // 开灯 } else if (param 0x02) { LED_PIN 0; // 关灯 } } } } SoftUART_ClearBuffer(); } } }在天问Block图形化界面里这个逻辑用重复执行如果那么积木就能搭出来。SoftUART_CheckReceived和SoftUART_ClearBuffer是前面封装的UDF积木主循环拖着走就行。4.3 MCU主动播报方向按键触发串口回调主动播报方向我做了两个触发源一个是外部按键按一下播报当前状态另一个是ADC温度超过阈值时自动播报。给MCU发送播报指令的底层函数void LU_ASR_Play(unsigned char voice_id) { // 按协议组帧 unsigned char frame[7]; frame[0] 0xAA; frame[1] 0x55; frame[2] 0x01; // 类型命令 frame[3] 0x01; // 命令字播报 frame[4] 0x01; // 数据长度 frame[5] voice_id; // 语音ID frame[6] 0; // 校验占位 unsigned char sum 0; for (i 0; i 6; i) { sum frame[i]; } frame[6] sum; // 通过软串口发送 for (i 0; i 7; i) { SoftUART_SendByte(frame[i]); } }这块逻辑我建议放在天问Block的UDF积木里做成一个函数业务层直接调用。图形化里拖一个按键按下积木里面调用LU_ASR_Play(1)整条逻辑就通了。4.4 完整代码框架与天问Block工程结构完整工程的天问Block代码结构大致是这样的主程序 ├─ 串口初始化硬件串口9600 ├─ 软串口初始化P3.2接收P1.2发送 ├─ LED/继电器GPIO初始化 └─ 主循环 ├─ 查询软串口数据 │ ├─ 校验帧头 │ ├─ 校验校验和 │ └─ 解析命令并执行 ├─ 查询按键状态 │ └─ 触发播报 └─ 查询ADC温度 └─ 超过阈值触发播报底层驱动文件里放的是软串口收发函数、环形缓冲区和协议解析函数。上层主程序只关注业务逻辑。这种分层方式对51这种小资源单片机也很适用代码清晰后期维护不痛苦。5. 调试阶段踩过的坑电平、误码率、词条配置逐个击破5.1 LU-ASR01与51单片机电平匹配问题第一个大坑是电平匹配。LU-ASR01模块我用的是3.3V供电版本串口逻辑电平也是3.3V而51单片机如果供电是5VIO口输出高电平接近5V直接接模块的RX引脚轻则识别异常重则烧毁模块。排查过程是这样的模块上电后我发送播报指令模块完全没有反应。用万用表量模块RX引脚电压发现MCU发送空闲时是高电平5V而模块数据手册明确写的是3.3V电平。这差出来的1.7V就导致模块IO口保护二极管导通信号被拉偏。解决方案有三种电平转换模块用TXS0108或者简单的分立元件电平转换电路最稳妥。电阻分压MCU TX到模块RX串一个2.2K电阻模块RX到GND再串一个3.3K电阻分压后大概3.3V。直接换3.3V供电的51STC8系列部分型号支持2.0V~5.5V宽电压统一用3.3V供电自然不需要转换。我最后选择了第三种因为STC8稳压到3.3V整个系统都统一电平后面接传感器也不用再关心电平分组。5.2 软串口误码的常见原因与排查链路第二个大坑是软串口偶尔收到错误数据。现象是LU-ASR01明明只发了一帧数据MCU解析出来却时不时多一个字节或者校验和不对。排查链路我从三方面入手。第一步确认波特率是否一致。我用示波器抓了模块TX引脚的波形测出实际位时间再和MCU软串口配置的位时间对照。发现模块实际波特率是9600但MCU侧由于用了12MHz晶振做软串口延时实际波特率只有9400左右误差约2%。9600波特率下允许的最大误差大约是2.5%在临界状态环境一波动就容易误码。这个问题的根源是晶振频率后来换成11.0592MHz彻底解决。第二步看中断是否被打断。软串口接收期间如果硬件串口中断或者定时器中断插进来且处理时间过长就会错过位采样点。我把外部中断0设为最高优先级同时在软串口接收的中断服务函数里禁止了其他中断嵌套问题明显改善。第三步检查信号边沿是否干净。如果RX线走线太长或者靠近强干扰源起始位的下降沿可能不够陡峭导致外部中断触发时刻有偏差。我的处理是在RX引脚对地加了一个10310nF电容实测边沿变缓但触发反而更稳定了因为它滤掉了高频毛刺引起的误触发。5.3 识别词条更新不生效的细节第三个坑来自LU-ASR01本身上位机的配置。我修改了词条后模块总是识别旧词条。后来发现是LU-ASR01上位机下发配置后模块需要重新上电才会加载新词表单纯复位MCU没用。还有一次我把词条内容从打开灯改成打开吊灯模块识别一直不准确。反复测试后发现问题出在词条之间的相似度太高词表里同时有打开灯和打开吊灯语音特征接近模块容易混淆。解决方法是把相近词条分开或者在词表里增加上下文区分词比如打开客厅吊灯。5.4 一个容易忽略的坑MCU上电时序比模块快最后这个坑很隐蔽。系统上电时MCU复位运行后会立刻执行软串口初始化然后尝试给LU-ASR01发送一条初始化指令。但LU-ASR01模块自身的启动时间比MCU长大约需要1~2秒MCU的指令发过去时模块还没准备好这条指令就丢了。后来我在主程序里加了一个延时等待逻辑上电后先延时2秒再给模块发送查询或者初始化指令。这个延时在图形化里拖一个延时积木就能实现虽然简单但我最初确实没考虑到模块启动时序走了不少弯路。6. 软串口方案的性能边界与怎么继续扩展6.1 软串口的资源占用与速率极限实测下来用软串口跑9600波特率发送一个字节大约需要1毫秒接收一帧数据时CPU基本被占满主循环在这个时间片内跑不了其他重任务。如果项目里还有实时性要求高的操作比如PWM输出、数码管动态扫描就需要仔细分配时序。软串口的速率上限我用STC8实测到38400波特率还能稳定工作再高到57600就容易出错。不是说51跑不了更高波特率而是位时间太短外部中断响应延迟、定时器中断嵌套这些微小的时间抖动就会导致采样点偏移。所以我建议软串口老老实实用9600稳定优先。语音模块本身也就是9600级别的吞吐需求没必要追求高波特率。6.2 如果项目再复杂一点换硬件多串口方案如果项目变得复杂比如同时接语音模块、蓝牙、GPS模块软串口就不是最佳选择了。STC8系列很多型号自带2~4组硬件串口STC15系列也有多串口型号。直接换个多串口芯片每个设备挂一组硬件串口CPU负担小得多通信可靠性也更高。天问Block对STC8的多串口支持很完善UART1、UART2、UART3、UART4都有对应的初始化积木。选型时如果预算允许优先选带至少2组硬件串口的芯片哪怕暂时只用到一组也给自己留了升级空间。6.3 我的最终建议什么场景下用软串口什么场景下别用根据这次项目经验我总结了两条选择标准。适合用软串口的场景硬件串口被下载调试占用、需要临时接一个低速设备、不想改PCB只想飞线验证方案。这些情况用软串口能快速满足需求而且通讯速率要求不高完全可以接受。不适合用软串口的场景通讯数据量大、波特率要求115200以上、设备需要长时间高强度收发、项目要量产长期运行。这些情况还是老老实实上硬件多串口芯片软串口省下的那点成本远远弥补不了可靠性的损失。我在这次实际项目里的体会是软串口是一个性价比极高的救火队员但不是一个适合长期依赖的方案。它帮我用最少的硬件代价打通了MCU和LU-ASR01之间的双向语音交互链路让我把更多精力放在业务逻辑上。如果你也在做51单片机的语音交互项目希望这篇记录能帮你少踩几个我踩过的坑。
返回列表