ARTICLE DETAIL

资讯详情

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

自己动手:STM32+SU-03T打造离线语音控制智能家居

自己动手:STM32+SU-03T打造离线语音控制智能家居 自己动手做一套能听懂人话的智能家居系统这事儿听着挺唬人其实拆开看就那么几块一个负责“听”的语音模块一个负责“想”的单片机再加几个干活儿的继电器和传感器。我这次选的主控是STM32F103C8T6也就是大家常说的“蓝丸”搭配离线语音识别模块做了个能用语音控制灯光、风扇、窗帘舵机模拟和温湿度播报的小系统。这个项目很适合刚学完STM32基础、想拿一个完整项目练手的同学也适合做课程设计或毕业设计的参考。它不像网上那些纯教程一样只点个流水灯而是把语音识别、串口通信、GPIO控制、传感器采集、OLED显示这几块常用技能全部串在了一起。做完这一套你对“一个嵌入式产品是怎么从零到一搭起来的”会有一个非常具体的体感。这篇东西我就按自己实际动手的顺序来写从方案选型、硬件接线、软件框架到联调排坑尽量把关键步骤和踩过的坑都说明白。1. 项目整体设计与方案选型1.1 语音识别方案怎么选做语音控制摆在面前的第一道选择题就是语音识别模块用什么。目前市面上主流的做法大概有三种LD3320离线词条方案、SU-03T离线自学习方案、ESP32配合云端识别方案百度、讯飞之类。我直接说结论这个项目选SU-03T。方案开发难度离线可用识别率成本适合场景LD3320中是一般受词条和发音影响大约25元固定词条、追求低成本的场合SU-03T低是较好可自定义唤醒词约30元快速开发、中小词条量项目ESP32云高否高约40元ESP32需要自然语言理解、扩展场景LD3320我最早也试过它需要在代码里写入拼音词条比如开灯要写“k e i d e n g”写起来别扭不说识别率还容易翻车尤其是环境里有点噪声的时候经常要喊好几遍才反应过来。ESP32走云端识别识别效果确实最好但一来依赖网络二来要自己去对接云平台SDK写语音鉴权、JSON解析这些开发量一下子大了不少。对第一次做语音项目的朋友来说我还是推荐SU-03T。SU-03T最大的好处是它有个配套的图形化配置平台叫智能公元。你在网页上把唤醒词和命令词填进去就像填表格一样系统自动帮你生成固件下载下来直接烧录到模块里就能用。它支持自定义唤醒词我设的是“小智管家”识别成功率比LD3320强太多。模块输出走串口直接发十六进制命令给STM32逻辑上非常简单。1.2 为什么主控选STM32F103C8T6“为什么不用ESP32直接一把梭”这个问题我被问过很多次。ESP32确实带WiFi和蓝牙功能强但正因为功能多初学者反而容易被各种外设和网络协议绕晕。STM32F103C8T6这颗芯片是STM32家族里最经典的入门型号Cortex-M3内核72MHz主频64KB Flash20KB RAM有3个串口、2个I2C、2个SPI、37个GPIO。对这个语音控制系统来说资源完全够用而且资料极其丰富。你搜“STM32F103C8T6”随便就能找到上千篇教程和例程遇到问题基本都能搜到答案。更重要的是STM32的GPIO操作、串口中断、定时器这些基本功在工业控制、车载电子等领域依然大量使用学会了它后面换其他型号的MCU也就是看数据手册的事。整个系统的架构其实很简单语音模块输入→ STM32大脑→ 继电器/舵机输出→ 传感器环境感知→ OLED状态反馈。语音模块收到命令后通过串口把指令发给STM32STM32解析出来后控制对应的执行机构同时把状态显示在屏幕上再通过语音播报反馈给用户。数据流很清晰每一块都可以单独调试。1.3 功能规划先做什么后做什么我建议你把功能分成基础版和进阶版两块来做先跑通基础版再考虑扩展。基础版功能语音控制一路LED灯模拟客厅灯、语音控制一路风扇继电器模拟空调/风扇、语音控制舵机转动模拟窗帘开关、温湿度采集DHT11传感器并在OLED上显示、语音询问温湿度时自动播报。进阶版功能加ESP8266实现手机APP远程控制、加红外发射模块控制空调电视、加人体感应模块实现人来灯亮、多个房间节点通过RS485或CAN组网。我这次做的是基础版ESP8266预留接口的配置。为什么建议先做基础版因为语音识别出问题的时候排查链路越短越好。先保证“语音→串口→GPIO→LED亮”这条链路完全可靠再往上面叠加其他功能否则所有模块一次性接上去出了bug你根本不知道是哪个环节出的问题。2. 硬件设计与接线要点2.1 核心器件清单器件型号/规格作用单价参考主控板STM32F103C8T6最小系统板系统大脑15元语音模块SU-03T语音识别与指令输出30元温湿度传感器DHT11采集环境温湿度5元OLED屏0.96寸 I2C SSD1306显示状态和温湿度12元继电器模块5V 1路配三极管驱动控制大功率设备5元舵机SG90模拟窗帘开合5元风扇/灯泡12V/5V均可被控负载自备USB-TTL模块CH340程序烧录与调试8元语音喇叭8Ω 1W小喇叭语音播报2元降压模块LM2596/AMS1117-3.3供电转换3元整体成本不含烙铁之类的工具大概100元上下就能搞定。所有模块都是常见的淘宝货没什么采购壁垒。有一点提醒一下继电器模块买的时候注意看是“低电平触发”还是“高电平触发”两种模块的代码逻辑是反的买回来先确认一下。2.2 电源设计方案别让继电器把MCU拉死电源是这套系统里最容易出问题的部分我甚至愿意把它排在“硬件设计第一条”。一开始我偷懒直接用电脑USB口给STM32供电板子上的3.3V和5V给语音模块和继电器供电结果一启动就遇到灵异现象STM32上电正常语音模块一唤醒系统就重启继电器吸合瞬间整个系统黑屏。原因很简单USB口最大输出电流一般只有500mA而继电器吸合瞬间的浪涌电流可能到1A以上直接把USB口的电压拉垮了。STM32虽然自己有稳压芯片但输入电压一跌它也跟着复位。正确的做法是外部用5V/2A以上的电源适配器手机充电头就行供电经USB-TTL模块或直接接到开发板的5V引脚然后系统内部分两路走——STM32、语音模块、OLED这些低功耗设备走5V/3.3V稳压继电器模块的驱动电流直接从电源输入端取电不要和单片机共用一根细线。另外LM2596降压模块如果你用的是12V供电就先用它降到5V再通过AMS1117稳到3.3V给STM32和语音模块。分层供电的好处是即使继电器动作导致大电流波动也只会局限在5V以后的电路域里不会直接影响MCU的核心电压。2.3 继电器驱动GPIO绝对不能直接接线圈GPIO口输出能力很弱STM32的GPIO最大也就输出20mA左右而且供电电压是3.3V。常用的小型5V继电器线圈电阻大概在70Ω左右吸合电流需要70mA以上直接接GPIO的话要么驱动不了要么把引脚烧了。正确方案有两个一是买成熟继电器模块内部已经集成了三极管驱动和续流二极管方便省事二是自己在洞洞板上搭一个驱动电路。如果是第二种需要一个NPN三极管8050就行或达林顿管ULN2003然后注意在继电器线圈两端反向并联一个1N4007二极管这个二极管叫续流二极管作用是在继电器断电瞬间吸收线圈产生的反向感应电动势防止高压打坏三极管和单片机。接线逻辑很简单STM32的GPIO通过1k电阻接到三极管基极三极管集电极接继电器线圈一端线圈另一端接5V发射极接地续流二极管并在线圈两端。GPIO输出高电平三极管导通继电器吸合GPIO输出低电平继电器释放。2.4 各模块接线对照模块VCC/GND信号引脚STM32引脚SU-03T5V/GNDTXD模块发送PA10USART1_RXSU-03T5V/GNDRXD模块接收PA9USART1_TXDHT113.3V/GNDDATAPB0OLED3.3V/GNDSCL/SDAPB6/PB7I2C1继电器5V/GNDINPA1舵机5V/GNDPWM信号PA2TIM2_CH3这里有个细节SU-03T的串口电平是3.3V TTL电平STM32也是3.3V逻辑所以可以直接互连但必须共地——就是两边的GND接在一起否则串口通信会收到一堆乱码。DHT11可以接受3.3V供电但有些廉价模块对电压比较敏感如果你发现读出来的湿度始终是0%试着给它单独供5V电数据引脚通过串联电阻接回STM32。3. 软件架构与核心代码实现3.1 程序整体框架软件我用的是STM32标准库写的没上HAL库。不是说HAL不好而是标准库逻辑更直白寄存器操作看得清清楚楚对理解原理帮助更大而且网上例程资源最多遇到问题方便对照。程序整体框架分三层初始化层时钟配置、GPIO初始化、串口配置、I2C配置、定时器配置业务逻辑层串口指令解析、设备状态管理、传感器读取与显示刷新中断层串口接收中断语音指令入口、定时器中断系统节拍用于超时保护我建议你在写代码前先画一个指令状态表把语音模块能识别什么词、对应发什么指令、STM32要做什么动作、OLED显示什么内容全部列出来。比如“小智管家”唤醒后说“打开客厅灯”SU-03T发FD 00 01STM32收到后把PA1置低电平OLED第一行显示“客厅灯开”语音模块播报“好的已开灯”。有了这张表再写代码逻辑清晰很多。3.2 SU-03T语音模块配置流程我第一次用SU-03T的时候以为和LD3320一样要写代码后来才发现原来是在网页上配置。打开智能公元平台注册登录新建产品选择SU-03T芯片然后就是核心的“命令词”配置界面。这里有一个关键选择对话模式。SU-03T支持“唤醒词命令词”和“直接命令”两种模式。我强烈建议选“唤醒词命令词”模式唤醒词设为“小智管家”这样模块平时处于低功耗待听状态不会把电视声、说话声误当成指令。配置命令词的时候有几个坑要避开命令词不要超过5个字越短识别率越高“打开客厅灯”比“亲爱的管家帮我打开客厅的灯”好使一万倍同一个命令词不要设置太多条比如“开灯”“打开灯”“把灯打开”这三条其实可以合并成一条“打开灯”识别率反而更高因为词条太多会稀释识别引擎的区分度。语音播报内容也在这里配置。我给每个命令都配了播报反馈比如执行开灯后播报“客厅灯已打开”。播报内容建议写完整、自然的句子但不要太长1到2秒以内的短句最佳。配置完成后点击编译平台会生成一个固件下载下来用USB-TTL模块烧录到SU-03T。烧录时注意SU-03T不是直接进入烧录模式的需要在上电前按住模块上的BOOT键然后插入USB/TTL再松开才会进入下载状态。这个步骤我翻了半天才找到官方文档写得很隐蔽。// SU-03T烧录时USB-TTL与模块接线 // USB-TTL RXD -- SU-03T TX // USB-TTL TXD -- SU-03T RX // USB-TTL 5V -- SU-03T 5V // USB-TTL GND -- SU-03T GND // 上电前按住BOOT键插入USB松开BOOT键3.3 STM32串口解析状态机比直接判断更稳SU-03T通过串口发出的指令是十六进制帧格式比如FD 00 01表示“打开客厅灯”FD 00 02表示“关闭客厅灯”。这种帧一般有固定格式帧头、数据位、校验位。SU-03T的默认帧格式是帧头FD、长度、命令字、校验。但在实际项目里不同家的语音模块协议不一样所以我建议你写一个通用的串口状态机解析器不依赖特定帧长。// 串口接收状态机示例 // 状态定义 #define FRAME_STATE_IDLE 0 #define FRAME_STATE_HEAD1 1 #define FRAME_STATE_HEAD2 2 #define FRAME_STATE_LENGTH 3 #define FRAME_STATE_DATA 4 #define FRAME_STATE_CHECK 5 uint8_t rx_buffer[64]; uint8_t rx_index 0; uint8_t frame_state FRAME_STATE_IDLE; uint8_t frame_length 0; uint8_t frame_check 0; void UART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); switch (frame_state) { case FRAME_STATE_IDLE: if (data 0xFD) // 帧头1 frame_state FRAME_STATE_HEAD1; break; case FRAME_STATE_HEAD1: if (data 0x00) // 帧头2 frame_state FRAME_STATE_LENGTH; else if (data ! 0xFD) frame_state FRAME_STATE_IDLE; break; case FRAME_STATE_LENGTH: frame_length data; // 数据长度 frame_check 0xFD 0x00 data; rx_index 0; frame_state FRAME_STATE_DATA; break; case FRAME_STATE_DATA: rx_buffer[rx_index] data; frame_check data; if (rx_index frame_length) frame_state FRAME_STATE_CHECK; break; case FRAME_STATE_CHECK: if (frame_check data) // 校验通过 { parse_voice_command(rx_buffer, frame_length); } frame_state FRAME_STATE_IDLE; break; default: frame_state FRAME_STATE_IDLE; break; } } }如果你用的SU-03T配置平台里可以选择输出格式有“简易协议”和“标准协议”两种。简易协议就是固定发一个字节的命令字比如0x01表示开灯、0x02表示关灯解析起来更简单。但标准协议带校验更可靠。我最后用了标准协议因为抗干扰能力强——家里有电磁干扰的时候多一个校验能避免误操作。3.4 指令执行与状态管理解析出指令后就是纯GPIO操作的事了。但直接GPIO_ResetBits(GPIOA, GPIO_Pin_1)虽然能亮灯工程上不够优雅。我建议你建一个设备状态表统一管理所有设备的状态。比如typedef struct { uint8_t device_id; // 设备ID uint8_t state; // 0:关 1:开 void (*turn_on)(void); // 开函数指针 void (*turn_off)(void); // 关函数指针 } Device_t; Device_t device_table[] { {0x01, 0, led_on, led_off}, // 客厅灯 {0x02, 0, fan_on, fan_off}, // 风扇继电器 {0x03, 0, curtain_open, curtain_close}, // 窗帘舵机 };为什么用函数指针表因为当你从3路设备扩展到10路设备的时候主循环里的switch-case会膨胀得没法看而表格驱动的写法只需要往数组里加一行。这也是很多嵌入式产品代码的核心风格——数据驱动逻辑逻辑驱动硬件。舵机控制我用了TIM2的PWM输出50Hz频率0.5ms脉宽对应0度2.5ms脉宽对应180度。SG90舵机的控制比较简单但要注意给舵机单独供电或者至少使用5V、1A以上的电源。如果直接用STM32板载的稳压5V舵机启动瞬间电流会拉低板载电压导致OLED花屏或系统复位。4. 联调过程与功能验证4.1 分步联调先串口再GPIO最后挂负载联调是最考验耐心的一步我的建议是“一次只接通一条链路验证一条链路”。千万别把语音模块、OLED、DHT11、继电器全部焊好再接电否则一旦出问题你连方向都没有。第一步先单独测试语音模块。USB-TTL接电脑打开串口助手波特率设成9600SU-03T默认对着模块说唤醒词“小智管家”看串口助手能不能收到唤醒词对应的指令帧。多说几次观察指令帧是不是稳定。如果收不到检查接线和波特率。这个阶段不要碰STM32。第二步让STM32只接收语音模块的数据收到指令后不控制继电器而是通过一个LED指示灯来表现。比如收到开灯指令就让板载LED灭板载LED一般是低电平点亮收到关灯指令就让它亮。这一步验证的是STM32串口解析逻辑是否正确。第三步验证GPIO控制继电器。用按键或直接烧一段测试代码让PA1输出电平翻转观察继电器是否正常吸合/释放。这一步不要接大功率负载先用万用表量继电器输出端的通断状态。第四步把所有模块接在一起整体联调。加上DHT11读取和OLED显示最后把灯泡/风扇接上。4.2 功能验证清单测试项操作预期结果实测结果语音唤醒说“小智管家”语音模块亮灯并播报“我在”正常开客厅灯说“打开客厅灯”继电器吸合灯泡亮OLED显示开正常关客厅灯说“关闭客厅灯”继电器释放灯泡灭OLED显示关正常开风扇说“打开风扇”风扇转OLED显示开正常查询温湿度说“当前温度”OLED显示数值语音播报“当前温度25度”正常窗帘打开说“打开窗帘”舵机转到90度OLED显示开正常误语音说“打开窗帘”时先不唤醒模块不响应正常4.3 识别场景的三个实战经验实际用下来SU-03T的识别效果和评测视频里还是有差距的差距主要在环境噪声和说话距离上。我统计了一下1米以内、安静环境下识别率大概90%以上3米开外开电视的情况下识别率会掉到70%左右。三个改善口诀词条短、声音稳、背景静。词条短前面说过了声音稳是指语速不要忽快忽慢尤其是唤醒词“小智管家”说太快了模块容易听漏背景静是物理条件的限制没有办法完全克服但可以通过调整唤醒词来缓解——选一个发音辨识度高的词比如“小智管家”里的“管”字是第三声第一声组合比较有辨识度不要选“你好小度”这种全是索音的词。另外如果你想提升远场识别率最直接的办法不是换模块而是加一个麦克风阵列板和降噪算法——但那就不是这个项目的范畴了属于另一个玩法的东西。5. 常见问题与排查技巧实录5.1 问题速查表现象可能原因排查思路解决方法上电后STM32反复复位电源供电能力不足量5V和3.3V电压是否稳定换5V/2A适配器继电器单独供电串口收到乱码波特率不匹配 / 没共地检查波特率设置和GND接线统一设为9600所有模块共地语音模块无任何反应固件没烧录成功 / BOOT没进重新进入烧录模式烧录按住BOOT键再上电识别率低命令词太长 / 环境噪声大缩短词条、靠近麦克风测试命令词控制在2~4个字继电器吸合时系统重启继电器线圈没有续流二极管检查驱动电路线圈两端并联1N4007反向二极管STM32能收指令但继电器不动GPIO口配置错误用万用表量GPIO输出电压检查GPIO是推挽输出还是开漏输出DHT11读数一直为0上拉电阻不足或电压不够示波器/万用表量数据线电平加4.7K上拉电阻改5V供电OLED花屏I2C线太长或电压不稳缩短杜邦线检查供电改用短接线加100nF电容去耦5.2 最容易忽略的“软件重启”问题我遇到最诡异的一个bug是系统跑着跑着语音模块突然就不响应了但STM32还在正常运行。后来发现是SU-03T的固件在长时间运行后有极低概率进入死锁状态这个情况官方的论坛也有反应。我的解决办法有两个层面。硬件层面SU-03T用STM32的一个GPIO控制它的电源引脚如果超过20秒没有收到语音模块的任何串口数据STM32就把语音模块断电再重新上电实现软重启。这个功能需要写一个“看门狗”逻辑用一个定时器每100ms检查一次语音模块的“心跳”标志位只有在收到有效串口帧时才刷新这个标志。// 看门狗逻辑示例 uint16_t voice_watchdog_counter 0; uint8_t voice_heartbeat_flag 0; void SysTick_Handler(void) { // 每1ms进一次中断 if (voice_heartbeat_flag) { voice_watchdog_counter 0; voice_heartbeat_flag 0; } else if (voice_watchdog_counter 20000) { // 20秒没有收到语音模块数据执行软重启 voice_module_power_reset(); voice_watchdog_counter 0; } }软件层面指令解析要容错。语音模块偶尔会把一条指令拆成两段发出来如果你在主循环里判断“收到完整帧才执行”中间状态就会丢包。所以我上面的状态机解析是逐字节处理的天然能处理粘包和半包问题。5.3 硬件布线上的教训做这个项目让我重新理解了“共地”这两个字的分量。有一次语音模块和STM32的数据线对不上我查了半天才发现是两块板子的地没有接在一起串口信号没有参考电平自然就乱码了。从那以后我养成了“所有模块先接地再接信号线”的习惯。另外杜邦线是个好东西但也是不稳定因素的温床。模块多起来之后飞线乱飞信号互相干扰。建议有条件的朋友直接画一块PCB打样如果暂时不方便画板至少把杜邦线剪短一点不要让它在桌上拖一长串。OLED和语音模块之间的信号线如果过长也可能导致I2C通信失败——我在20cm的杜邦线上就遇过随机花屏缩短到10cm以内就正常了。写在最后的小建议做完整套系统后我最大的体会是嵌入式项目不怕功能多怕的是每一条链路都不扎实。你能让LED灯亮起来不代表你能让继电器稳定工作你能读到DHT11的温度不代表OLED显示不会因为电源纹波而花屏。语音智能家居这个项目正好把这些“你以为会了其实还差一点”的细节全部暴露出来这恰恰是它最大的学习价值。按照这套方案做出来之后后续的扩展方向也挺多的给STM32挂一个ESP8266就能通过网络接口用手机APP和语音一起控制加一个红外发射管就能把家里的电视空调都纳入进来把节点做成RS485总线就可以在每个房间放一个STM32语音模块由一个中央控制板统一管理。我个人目前的打算是加一个京东云或阿里云的物联网平台对接把家里的传感器数据同步到云端做远程监控。最后再分享一个让作品显得专业的小细节买一个几块钱的亚克力透明外壳或者用3D打印机打一个外壳把底板和模块固定好插排线尽量整齐。功能和代码只是基本功一个清清爽爽的外壳往往能让别人对你的项目打分高出一截——毕竟在这个领域“能跑”和“像产品”之间差的往往就是这些看起来不起眼的工程细节。
返回列表