STM32智能家居语音控制系统开源实战:离线语音+Proteus仿真全解析
做智能家居相关项目很多人第一步就卡在方案选择上既想要语音控制的新鲜感又不想投入太多成本同时还得保证稳定性和可复现性。我这次开源的STM32智能家居语音控制系统选的就是一条比较务实的路线——主控用STM32F103C8T6语音识别走离线模块硬件只保留灯光、风扇、温湿度监测这几个核心场景全部资料包括代码、原理图和Proteus仿真工程一起打包。目标是让新手拿到手能看懂、能烧录、能复现同时给有经验的朋友留出二次开发的扩展点。这套系统的价值不在于“炫”而在于把语音、传感、执行、显示这四件事用最扎实的方式串在一起。语音不是简单接个模块读几个字而是通过串口协议把识别结果解析成结构化指令控制不是无脑置高拉低而是用状态机管理多路设备的开关模式和联动逻辑仿真也不是摆个样子而是可以在没有实物的情况下把串口指令、传感器数据、外设响应完整跑通。接下来我从方案选型开始把整个设计逻辑、硬件接线、软件实现和调试过程逐一拆开讲。1. 项目定位与方案选型为什么这套组合最省心1.1 语音识别方案SU-03T 与 LD3320 之争语音识别是整个系统的门面选型直接决定体验。常见的离线方案有两种一个是LD3320一个是SU-03T天问的离线语音模块。LD3320是传统的语音识别芯片需要自己维护词条、做关键词训练配置过程繁琐识别率也比较依赖录音环境SU-03T则是新一代的离线语音模块特点是出厂自带语音识别引擎通过配套的上位机工具可以自由配置唤醒词、命令词和应答语配置完直接生成固件烧进模块就行。我最终选了SU-03T核心原因是它的串口输出格式对单片机非常友好。模块识别到命令后可以直接通过UART输出我们预设的JSON格式字符串比如识别“开灯”就输出{event:light_on}识别“关灯”就输出{event:light_off}。这意味着STM32这边只需要做串口解析不用处理音频特征、不用跑神经网络把复杂的语音识别任务完全隔离在模块内部。SU-03T另一个优势是真正的“离线”。不需要连WiFi、不需要云平台所有识别都在本地完成响应速度在200ms以内隐私性也好。智能家居领域很多人一上来就上云端ASR但一旦断网整个系统就瘫痪对于家庭内部这种相对稳定的环境离线方案反而更可靠。1.2 主控芯片与传感器搭配F103C8T6 的性价比逻辑主控用了STM32F103C8T6也就是大家俗称的“C8T6蓝色 pill”。这颗芯片虽然已经发布十几年但至今还是学生项目、DIY爱好者的首选原因无外乎三个便宜、资料多、够用。72MHz的主频、64KB Flash、20KB SRAM对于语音指令解析、传感器读取、屏幕显示、多路GPIO控制这些任务来说绰绰有余。而且Proteus里有非常成熟的F103系列仿真模型硬件和仿真可以无缝切换。传感器搭配上我选了DHT11温湿度传感器作为环境监测模块。坦白说DHT11的精度并不算高温度±2℃湿度±5%RH但在智能家居这个场景下用户关注的是“大概多少度、要不要开风扇”这个层面的信息而不是精密仪器的读数。DHT11最大的优点是单总线协议、只需要一根数据线非常适合用来演示GPIO模拟时序和底层驱动编写。如果你觉得精度不够硬件电路里我预留了I2C接口后续可以直接替换成SHT30或者AHT21。显示部分用的是0.96寸OLEDI2C接口四根线就能搞定。OLED的好处是自发光、对比度高、显示内容灵活能同时显示温湿度、设备状态、语音指令提示比LCD1602省IO口也比数码管显示的信息量大得多。1.3 系统整体架构与工作流程整个系统的数据流是这样的SU-03T语音模块作为唯一的“输入源”之一识别到用户的语音命令后通过串口把结构化指令发给STM32STM32内部的解析器提取出事件类型交给状态机处理状态机根据当前设备状态决定是开灯、关灯、开风扇、关风扇还是切换模式同时DHT11定时采集环境数据OLED实时刷新显示。本地按键作为备用输入防止语音模块偶尔抽风。我画了一张逻辑框图方便理解这里不是严格意义的电路图而是功能模块的关系[SU-03T语音模块] --UART-- [STM32F103C8T6] --GPIO-- [继电器模块] -- [灯光/风扇] [按键输入] --GPIO-- [STM32F103C8T6] --I2C-- [OLED显示屏] [DHT11] --单总线-- [STM32F103C8T6] --UART-- [调试串口/PC]系统上电后自动进入待机模式OLED显示当前温度和湿度此时你说“小智小智”唤醒语音模块然后说“打开客厅灯”SU-03T匹配命令词后输出JSONSTM32解析出light_on事件把继电器1拉高灯光点亮OLED状态区同步更新。整个过程不需要联网从语音到动作的延迟在300ms左右体验上很跟手。2. 原理图设计每一根线都有讲究原理图这块我花了不少精力很多人觉得C8T6接线简单随便飞几根杜邦线就能跑但实际上要把系统稳定跑起来供电、电平匹配、隔离保护这些细节一个都不能省。开源文件里用的是KiCad工程也导出了PDF版方便直接用AD查看。2.1 最小系统与供电设计STM32F103C8T6的最小系统包含这几个部分8MHz主晶振配两个20pF负载电容、复位电路10kΩ上拉电阻加0.1μF电容、BOOT0下拉到地确保从Flash启动、VDDA和VDD引脚就近放置0.1μF去耦电容、VBAT直接接3.3V。这些都是常规操作但我建议在晶振下方不要铺铜避免寄生电容影响起振稳定性。供电方面系统统一用5V输入经过AMS1117-3.3稳压得到3.3V给STM32、OLED和语音模块供电。这里有个常见坑语音模块的峰值电流能达到200mA以上如果电源纹波太大模块会出现误唤醒或者识别率下降。所以我在AMS1117前后都加了10μF0.1μF的滤波电容5V入口加了一个SS34肖特基二极管做反接保护。继电器部分单独用5V驱动和逻辑电源在布局上分开走线。2.2 语音模块串口连接与电平匹配SU-03T模块的串口电平是3.3V TTL和STM32可以直接互连不需要额外的电平转换芯片。接线很简单SU-03T的TXD接STM32的PA10USART1_RXRXD接PA9USART1_TXGND共地。但这里有一个细节很多人容易忽略SU-03T的TXD在空闲状态是高电平如果你用的是USB转TTL调试器去单独调试语音模块部分调试器是5V电平直接接会损伤模块。稳妥的做法是用3.3V供电的调试器或者在TXD线上串联一个1kΩ电阻做限流保护。另外语音模块的喇叭接口一定要接2W/8Ω左右的喇叭千万别用小蜂鸣器凑合音量会小到听不清唤醒提示。2.3 DHT11与OLED的接线细节DHT11的数据引脚接PB12数据线和VCC之间必须接一个4.7kΩ到10kΩ的上拉电阻。DHT11用的是单总线协议总线空闲状态是高电平主机拉低发起通信时序如果上拉电阻缺失通信时序根本没法定起来。OLED的SCL接PB6I2C1_SCLSDA接PB7I2C1_SDAVCC和GND接3.3V。OLED模块一般自带I2C上拉电阻所以板子上不用重复加。如果遇到OLED显示异常先用I2C扫描程序确认设备地址0x3C最常见再用逻辑分析仪看波形但这两个问题在我开源的项目代码里都有对应的诊断例程。2.4 继电器驱动光耦隔离与续流二极管继电器模块是整个系统中唯一涉及“强电”风险的部分我在原理图设计时做了双重保护。第一重是光电隔离MCU的GPIO输出先经过PC817光耦光耦的输出再驱动三极管S8050控制继电器线圈。这样STM32和负载回路之间没有电气连接即使负载端出问题也不会烧主控。第二重是续流二极管继电器线圈两端并联1N4007方向是反向并联当线圈断电时给感性负载的感生电流提供泄放回路否则瞬间的反向电动势会直接击穿三极管。光照控制用的是低电平触发继电器模块还是一体式继电器我这里用的是高电平触发的裸继电器驱动电路。如果你买的是市面上常见的低电平触发继电器模块那逻辑正好相反需要把代码里GPIO_SetBits改成GPIO_ResetBits这个在代码注释里我专门标了。2.5 原理图绘制与PCB布线心得原理图用KiCad画各功能模块我用分页图区分第一页是STM32最小系统第二页是语音模块和调试串口第三页是传感器和显示第四页是继电器驱动和电源。这样别人看工程时能按模块理解也方便后续增删功能。PCB布线方面虽然是双层板但有几个原则建议遵守主芯片下方尽量铺完整的地平面晶振、去耦电容要靠近对应引脚继电器驱动电路放在板子边缘和天线语音模块拉开距离强电区和弱电区的间距至少5mm以上。我第一版布局时把继电器和语音模块放得太近结果每次继电器吸合语音模块就有概率误触发后来把继电器挪到板角才解决。3. 软件实现从串口解析到状态机控制代码结构上我分成了几个模块main.c负责初始化、主循环调度usart.c负责串口收发和指令解析dht11.c负责温湿度读取oled.c负责显示control.c里是状态机。整个工程用STM32标准外设库编写没有上HAL因为标准库的逻辑更直白适合学习。如果你用CubeMX生成工程IO配置参考表在项目文档里。3.1 串口接收与SU-03T命令帧解析SU-03T通过串口输出的JSON格式可以自定义我的配置是唤醒词“小智小智”命令词包括“打开灯光”“关闭灯光”“打开风扇”“关闭风扇”“查询温湿度”对应输出light_on、light_off、fan_on、fan_off、query_env这些事件字符串。串口接收如果用最简单的单字节中断缓存逻辑清晰且不会丢数据。我定义了接收缓冲区rx_buffer接收中断里把字节存进缓冲区同时检测帧头{和帧尾}完整收到一帧后置标志位主循环解析uint8_t rx_buffer[64]; uint8_t rx_index 0; uint8_t frame_ready 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE)) { uint8_t data USART_ReceiveData(USART1); if (data {) { rx_index 0; rx_buffer[rx_index] data; } else if (data } rx_index 0) { rx_buffer[rx_index] data; rx_buffer[rx_index] \0; rx_index 0; frame_ready 1; } else if (rx_index sizeof(rx_buffer) - 1) { rx_buffer[rx_index] data; } } }解析函数用strstr匹配关键字。这么做不需要引入cJSON库省Flash资源对固定格式的指令也足够用void parse_command(uint8_t *buf) { if (strstr((char*)buf, light_on)) { control_set_device(DEVICE_LIGHT, SET_ON); } else if (strstr((char*)buf, light_off)) { control_set_device(DEVICE_LIGHT, SET_OFF); } else if (strstr((char*)buf, fan_on)) { control_set_device(DEVICE_FAN, SET_ON); } else if (strstr((char*)buf, fan_off)) { control_set_device(DEVICE_FAN, SET_OFF); } else if (strstr((char*)buf, query_env)) { display_show_env(dht11_read_temp(), dht11_read_humi()); } }3.2 核心控制逻辑状态机与模式切换最简单的做法是在主循环里判断指令然后直接置GPIO但我用了状态机因为后续加“定时开关”“联动模式”这些功能会更方便。状态定义typedef enum { MODE_MANUAL, // 手动模式语音/按键直接控制 MODE_AUTO // 自动模式温度高于阈值自动开风扇 } system_mode_t; typedef struct { uint8_t light; // 0-关 1-开 uint8_t fan; // 0-关 1-开 system_mode_t mode; } system_state_t;control_set_device是状态机入口它先判断当前模式手动模式下直接翻转对应位自动模式下语音指令只切换灯光风扇由温度逻辑接管。温度阈值我设成28℃温度每15秒采样一次超过阈值且风扇没开就自动开启低于阈值且风扇没关就自动关闭。状态机的好处是让“输入来源”和“执行动作”解耦。语音、按键、定时器、温湿度变化都可以调用control_set_device而真正的GPIO写操作集中在apply_state函数里每个动作都有明确的日志输出方便排查问题。3.3 DHT11时序读取GPIO模拟DHT11驱动是典型的GPIO模拟时序没有太难的东西但时序要求严格。完整读一次需要经历这些阶段主机发送起始信号拉低至少18ms释放总线等待从机响应从机拉低80μs再拉高80μs然后连续读40bit数据高位在前每bit以50μs低电平开头26~28μs高电平为070μs高电平为1。代码核心部分uint8_t dht11_read_byte(void) { uint8_t val 0; for (int i 0; i 8; i) { while (dht11_read_pin() RESET); delay_us(40); if (dht11_read_pin() SET) { val (val 1) | 1; } else { val (val 1) | 0; } while (dht11_read_pin() SET); } return val; }关键是最后那个while (dht11_read_pin() SET)它的作用是等待当前bit的高电平结束确保下一个bit的低电平时钟信号能被正确识别。如果漏掉这一步数据位就会错位读出来的温湿度完全是乱码。数据位读取完成后校验位应该等于前四个字节之和的低8位。代码里我会校验校验失败就直接返回上一次的缓存值而不是把错误数据显示出来这个细节对用户体验影响很直接。3.4 OLED显示与菜单交互OLED驱动我用的是SSD1306的经典软件I2C实现也可以用硬件I2C但靠软件模拟更稳。主界面布局分三个区域顶部显示系统状态灯光、风扇的开关图标中间大字显示温度底部显示湿度。注意SSD1306显存是1KB128×64 bit整屏刷新一次在模拟I2C下大概需要30ms。如果频繁全屏刷新会有轻微的闪烁感。我的做法是只在数据变化时才刷新对应区域比如温度从25.5变成25.6只更新数字所在的矩形区域这样既流畅又减少CPU开销。按键交互方面我接了两个轻触按键KEY1短按切换灯光KEY2短按切换风扇KEY1和KEY2同时长按3秒切换自动/手动模式。按键扫描放在定时器中断里做消抖20ms采样一次避免在主循环里用delay消抖阻塞其他任务。3.5 编译与烧录设置工程编译环境是Keil MDK 5芯片型号选择STM32F103C8勾选“Use MicroLIB”Flash烧录算法选64KB版本。调试器是ST-Link V2SWD四线接法SWDIO、SWCLK、GND、3.3V。有一个编译相关的坑得提醒一下使用标准外设库时默认的SystemCoreClock是72MHz如果你复制了其他工程没有修改系统时钟初始化串口波特率会漂移。这个项目里我用的是标准库的SystemInit()外部8MHz晶振经过PLL倍频到72MHz串口1的波特率配置为115200和语音模块保持一致。4. Proteus仿真搭建没有硬件也能先跑逻辑这个项目的仿真文件是很多人关注的重点。我必须先说明一个事实Proteus不支持SU-03T语音模块的仿真模型。所以仿真里的做法是画一个STM32F103C8T6的最小系统语音部分用虚拟终端Virtual Terminal代替——你在虚拟终端里手动输入{event:light_on}效果等同语音模块识别后通过串口发数据。4.1 仿真工程与元件清单仿真工程在Proteus 8.9及以上版本打开元件清单包括STM32F103C8T6、LED-RED模拟灯光、LED-GREEN模拟风扇、LM016L或LM044L液晶我用的是LCD1602OLED在Proteus里仿真效果一般液晶更直观、DHT11Proteus自带的传感器模型能模拟温湿度输出、RESISTOR若干、CAPACITOR若干、POT-HG模拟温度调节。DHT11在Proteus里的用法比较特殊它有三个引脚VCC、DATA、GND。双击DHT11模型可以在属性里设定初始温湿度值也可以通过滑动变阻器给DHT11的某个引脚接不同的电压来模拟温度变化。实际仿真时我用一个电位器分压调整电位器DHT11返回的湿度会改变温度则固定在一个设定值。4.2 用虚拟串口模拟语音模块虚拟终端的配置要点首先要在画布上放置一个VIRTUAL TERMINAL双击设置波特率115200、8位数据、无校验、1位停止位。然后把虚拟终端的RXD接到STM32的PA9USART1_TXTXD接到PA10USART1_RX。仿真时先在虚拟终端里点击一下窗口让它获得焦点然后直接输入{event:light_on}注意大小写点击回车发送观察STM32引脚输出PA1控制的LED点亮PA0控制的LED保持熄灭。如果发送{event:fan_on}PA0控制的LED点亮。虚拟终端里显示的内容还会包括STM32发来的日志信息比如[INFO] light on、[INFO] temp25.6 humi60%方便实时监控。有一点需要注意在Proteus里连到STM32 RX引脚的虚拟终端必须在输入完所有字符后按回车系统才会把缓冲区内容一次性送出实际串口模块是一字节一字节发的。这个差异不影响逻辑验证因为接收中断还是一样地逐字节处理。4.3 仿真中常见的问题仿真最容易出问题的就是程序“跑飞”。现象是虚拟终端收不到日志LED没有任何反应。检查顺序先确认晶振设置双击STM32External Crystal Frequency设为8M、再确认芯片型号没有选错最后看代码里的SystemInit是否把时钟超频到Proteus模型不支持的频率。DHT11仿真经常出现湿度一直显示99%的问题这是Proteus模型的一个已知限制。解决方法是双击DHT11在模型属性里调整Humidity项的初始值和范围有时候需要把Moisture输出引脚重新拖拽一下。另外仿真里DHT11的数据线也要求有一个上拉电阻4.7kΩ没有上拉也会导致时序读取超时。还有一个通用的提醒Proteus仿真通过后不代表硬件一定没问题。仿真验证的是“逻辑正确性”比如状态机切换、串口协议解析、传感器数据处理流程但实际硬件的电源纹波、信号干扰、模块兼容性这些问题仿真一概测不出来。仿真和实体原型的定位是互补关系不是替代关系。5. 常见问题与排查技巧实录5.1 ST-Link无法连接目标芯片很多人在下载程序时遇到这个报错error: no stm32 target found! if your product embeds debug authentication。我第一次看到这个报错也懵了一下。这个报错出现在ST-Link尝试通过SWD接口连接目标芯片但没收到有效回应的时候常见原因有四个接线错误、目标供电不足、芯片被读保护、SWDIO和SWCLK接反。排查顺序建议是这样的先确认ST-Link和板子共地这是最容易被忽略的然后用万用表量SWDIO和SWCLK引脚的电压正常空闲状态是3.3V左右如果量到0V多半是引脚虚焊接着看目标板是否单独供电ST-Link的3.3V输出能力很弱只有几百毫安如果板上同时带了OLED、语音模块和继电器经常带不动。如果是芯片被读保护用ST-Link Utility或者STM32CubeProgrammer执行全片擦除Full chip erase可以解除。注意这个操作会清掉Flash里的程序重新烧录前要确保工程保存完毕。5.2 语音模块识别率低和乱码识别率低首先检查供电。SU-03T模块对电源噪声敏感我曾经在开关继电器时观察到模块误唤醒原因就是继电器线圈产生的电磁干扰耦合进了电源线。解决方法是继电器驱动改用光耦隔离同时在语音模块的VCC引脚旁边加一个100μF电解电容0.1μF陶瓷电容实测识别率提升明显。串口乱码则先确认波特率。SU-03T的默认串口波特率在配置软件里可以设置我设置为115200。如果你收到的数据是一堆“éé*”基本上STM32的波特率配置和模块不一致。还有一种情况是TX和RX接反了现象是模块说话正常但STM32完全收不到任何数据用示波器或逻辑分析仪看TXD引脚是否有波形输出。5.3 DHT11读数恒为0硬件上先检查上拉电阻有没有焊接或者接好没有上拉DHT11的data线始终为低读回来的温度肯定是0。软件上检查延时函数精度如果在GPIO翻转之间加了delay_us但延时误差太大比如用了普通循环且编译器优化等级不同DHT11的时序就乱了。解决方法是把延时函数放进定时器中断基准或者用SysTick做微秒延时避免编译器优化带来的误差。还有一点DHT11上电后需要至少1秒的稳定时间才能开始通信。如果程序一上电就立刻初始化DHT11读出来往往是0或者错误。我习惯在DHT11初始化函数里先delay_ms(1500)再执行复位时序这个问题基本就消失了。5.4 继电器误动作与干扰继电器模块的驱动脚如果悬空GPIO在STM32上电配置阶段会有一个短暂的浮空过程可能导致继电器误吸合一下。我在GPIO初始化顺序上做了调整先开启GPIO时钟配置为推挽输出并默认输出低电平再初始化其他外设。同时继电器驱动三极管的基极加了一个10kΩ下拉电阻确保在GPIO没起作用的阶段三极管是截止状态。另一个实际问题继电器吸合瞬间由于感性负载的特性VCC可能会有小幅跌落导致STM32复位。解决办法是继电器电源和MCU电源分开或者至少保证总电源输入端的电解电容容量足够大我用的是470μF。如果条件允许用独立5V电源给继电器供电共地即可。5.5 常见问题速查表现象可能原因处理办法程序烧不进去ST-Link报错SWD接线不对/芯片读保护检查共地、接线用CubeProgrammer全片擦除语音模块不响应电源纹波大/唤醒词未触发加固滤波电容检查喇叭音量重新唤醒串口数据乱码波特率不匹配/TX RX接反检查配置交换TX/RXDHT11读数一直0上拉电阻缺失/上电时长不足检查4.7kΩ上拉初始化前延时1.5sOLED不显示I2C地址不对/接线错误扫描I2C设备地址确认0x3C继电器吸合导致复位电源容量不足分离继电器电源加大电解电容仿真时LED不亮芯片时钟配置不对检查Proteus晶振设置和代码SystemInit6. 扩展思路与个人体悟6.1 从离线语音到物联网这套系统的语音部分已经完全离线工作但智能家居更大的想象空间是“联动”。比如通过ESP8266模块把STM32接入MQTT服务器手机端或Home Assistant端就能看到设备状态、远程下发指令。我建议的扩展路径是在现有串口命令帧的基础上增加一个“网络通道”ESP8266通过串口和STM32通信用已有的JSON帧格式扩展事件类型比如{event:remote_on,device:light}这样语音和远程控制共用一套解析逻辑改动量很小。6.2 我在实际调试中的几个习惯做这类项目这些年我养成了几个习惯供参考。第一每个外设驱动都独立写一个自测函数比如dht11_self_test()会连续读取10次并打印结果OLED有oled_test()绘制全屏图案接线验证的时候直接调用这些函数比反复看代码定位问题快得多。第二串口日志分级我用[INFO]、[WARN]、[ERR]三个级别平时把日志输出全开排完问题后把串口重定向到调试口不影响正常功能。第三每次修改硬件接线都会同步更新原理图和文档哪怕只是把一个引脚从PA0改成PB0否则三个月后回来看项目你会完全想不起当初为什么这么接。6.3 开源项目的文档与版本管理建议既然项目开源了文档就是用户体验的一部分。我这次组织的文件结构是/hardwareKiCad工程和PDF原理图、/firmwareSTM32工程、/simulationProteus仿真、/docs使用说明和接线表、/tools语音模块配置工具和固件备份。版本管理用Git从第一天就拉起来目前已经到v1.3每个版本固定打tag语音模块的固件配置也作为二进制文件一并入库这样任何人拿到仓库都能完整复现出和发布者一致的效果。最后分享一点个人体会做开源项目代码本身只占一半的价值另一半是“别人能不能跑起来”。所以我会刻意把一些坑写进文档比如继电器模块高低电平触发的差异、DHT11上电稳定时间、Proteus里DHT11模型的已知问题等等。一套开源资料如果别人拿到能在一个晚上之内点灯并看到温度显示那这个项目的意义就真正达成了。这套代码我后续还会持续维护如果大家在复现过程中遇到文档里没写到的问题欢迎在评论区交流互相对一下解决思路很多时候比自己闷头查要高效得多。