自己动手做一套能听懂人话的智能家居系统,这事儿听着挺唬人,其实拆开看就那么几块:一个负责“听”的语音模块,一个负责“想”的单片机,再加几个干活儿的继电器和传感器。我这次选的主控是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 Flash,20KB RAM,有3个串口、2个I2C、2个SPI、37个GPIO。
对这个语音控制系统来说,资源完全够用,而且资料极其丰富。你搜“STM32F103C8T6”,随便就能找到上千篇教程和例程,遇到问题基本都能搜到答案。更重要的是,STM32的GPIO操作、串口中断、定时器这些基本功,在工业控制、车载电子等领域依然大量使用,学会了它,后面换其他型号的MCU也就是看数据手册的事。
整个系统的架构其实很简单:语音模块(输入)→ STM32(大脑)→ 继电器/舵机(输出)→ 传感器(环境感知)→ OLED(状态反馈)。语音模块收到命令后通过串口把指令发给STM32,STM32解析出来后控制对应的执行机构,同时把状态显示在屏幕上,再通过语音播报反馈给用户。数据流很清晰,每一块都可以单独调试。
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-03T | 5V/GND | TXD(模块发送) | PA10(USART1_RX) |
| SU-03T | 5V/GND | RXD(模块接收) | PA9(USART1_TX) |
| DHT11 | 3.3V/GND | DATA | PB0 |
| OLED | 3.3V/GND | SCL/SDA | PB6/PB7(I2C1) |
| 继电器 | 5V/GND | IN | PA1 |
| 舵机 | 5V/GND | PWM信号 | PA2(TIM2_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 01,STM32收到后把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接电脑,打开串口助手,波特率设成9600(SU-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打印机打一个外壳,把底板和模块固定好,插排线尽量整齐。功能和代码只是基本功,一个清清爽爽的外壳往往能让别人对你的项目打分高出一截——毕竟在这个领域,“能跑”和“像产品”之间,差的往往就是这些看起来不起眼的工程细节。