简介:这份资源面向电子信息、嵌入式与物联网方向的毕业设计学生及DIY爱好者,提供一套可基本实现功能的语音控制智能家居完整工程,解决从语音识别、蓝牙通信到上位机控制的整链路实现问题。压缩包共2358个文件,约33.89MB,以class、dex、apk等Android编译产物和json、xml配置为主,辅以h、c、s等单片机源码及jar、properties等工程文件,覆盖安卓上位机与下位机固件两端。已有1805人学习下载,具备一定参考基础。读者可从中获得Android应用源码、32位单片机控制程序与语音模块程序的初版实现,理解蓝牙BLE指令下发、GPIO驱动家居设备、UART/SPI连接语音模块等关键环节,并借鉴MVC或MVVM的界面分层与中断处理、RTOS响应设计,适合作为毕设框架与联调排错的对照参考。
1. 语音控制智能家居:一个能跑通闭环的入门级实战项目
去年帮一个做硬件的朋友救场,他拿着一块语音识别模块和一堆继电器,说想做个“对着空气喊一声就开灯”的演示,结果卡在串口数据解析上三天没动。这类语音控制智能家居的项目,网上资料一抓一大把,但真正能从头到尾跑通、代码结构还看得下去的并不多。这份资源就是冲着“可基本实现功能”去的:语音指令采集、关键词识别、蓝牙下发控制、继电器执行,一条链路走完,不追求工业级稳定性,但每个环节都有能跑起来的代码。适合谁?适合刚接触嵌入式与上位机联调、想拿一个完整闭环练手的开发者,也适合需要快速搭一个演示原型去交差的人。它不解决“识别率 99%”这种玄学问题,但能让你在半天内看到灯真的亮了。
2. 技术栈与目录结构:先看清这套东西由哪几块拼起来
2.1 整体架构:语音端、控制端、执行端三段式
这个项目的骨架非常清晰,属于典型的“感知—决策—执行”三段式。语音端负责拾音和关键词识别,通常跑在一块带麦克风的开发板或者语音模块上;控制端是大脑,负责把识别结果翻译成具体指令,再通过蓝牙把指令发给执行端;执行端就是继电器加负载,灯、风扇、插座都行。三段之间用串口和蓝牙两条链路连接,串口走的是语音模块到主控,蓝牙走的是主控到执行节点。
我一般会把这种结构画成三个圈,中间用箭头连起来,箭头旁边标注协议和波特率。这样做的好处是,调试的时候一旦某个环节不动,你能立刻定位是“没识别到”还是“指令没发出去”还是“继电器没吸合”。很多新手一上来就盯着代码看,结果灯不亮查了半天发现是蓝牙根本没配对,血泪经验就是先把链路图贴在显示器边上。
从技术选型上看,语音识别部分用的是离线关键词识别方案,不依赖网络,响应快,代价是词条有限,一般支持十几个自定义关键词。蓝牙部分用的是经典蓝牙串口透传,主控和执行端各挂一个蓝牙模块,配对后当成无线串口用,编程上跟操作普通串口没区别。执行端如果只是点灯,一块最便宜的单片机加继电器模块就够,不需要上复杂的 RTOS。
2.2 目录结构:按端拆分,别混在一起
这份资源的目录组织是按“端”来拆的,不是按“功能”拆,这点我很认可。按功能拆容易出现一个文件里既有语音解析又有蓝牙发送,改一处牵动全身。按端拆之后,每个目录职责单一,编译和烧录也互不干扰。典型的目录长这样:
voice-smart-home/ ├── voice_node/ # 语音端固件 │ ├── main.c # 主循环:拾音、识别、串口上报 │ ├── voice_driver.c # 语音模块驱动,负责配置词条 │ └── voice_driver.h ├── control_node/ # 控制端固件(主控) │ ├── main.c # 串口收指令、解析、蓝牙转发 │ ├── protocol.c # 指令协议封装与校验 │ └── bt_link.c # 蓝牙串口初始化与发送 ├── exec_node/ # 执行端固件 │ ├── main.c # 蓝牙收指令、驱动继电器 │ └── relay.c # 继电器控制与状态保持 ├── tools/ # 上位机调试脚本 │ └── serial_monitor.py # 串口抓包与指令模拟 └── docs/ └── protocol.md # 指令格式说明这个结构里最值得说的是protocol.c和tools/serial_monitor.py。前者把指令格式统一了,后者让你在没有语音模块的时候也能模拟发指令,调试执行端特别有用。我见过太多项目把协议散落在各个文件里,最后自己都记不清哪个字节是命令字,翻车往往就翻在这种地方。
2.3 指令协议:一个字节的命令字加一个字节的参数
协议设计得很朴素,帧头加命令字加参数加校验,总共 5 个字节。帧头固定0xAA,命令字区分设备类型,参数是开关或档位,最后一位是前面几个字节的异或校验。这种设计谈不上优雅,但对于一个演示项目来说足够,解析代码十几行就能写完,出错也容易抓。
// protocol.c 指令封装 #define FRAME_HEAD 0xAA // 组帧:head + cmd + param + xor校验 void build_frame(uint8_t *buf, uint8_t cmd, uint8_t param) { buf[0] = FRAME_HEAD; buf[1] = cmd; buf[2] = param; buf[3] = buf[0] ^ buf[1] ^ buf[2]; // 异或校验,简单但够用 } // 解析:返回0成功,-1帧头错,-2校验错 int parse_frame(const uint8_t *buf, uint8_t *cmd, uint8_t *param) { if (buf[0] != FRAME_HEAD) return -1; if ((buf[0] ^ buf[1] ^ buf[2]) != buf[3]) return -2; *cmd = buf[1]; *param = buf[2]; return 0; }build_frame里校验位放在最后,解析时先验帧头再验校验,顺序不能反,否则噪声数据会被误判成合法帧。cmd的取值我一般约定0x01是灯、0x02是风扇、0x03是插座,param用0x00和0x01表示关和开。这套约定要写进docs/protocol.md,不然过两周自己都忘了。校验用异或而不是 CRC,是因为异或计算量小、代码短,在演示场景下误码率可以接受,真要做产品再换 CRC16 也不迟。
3. 核心模块拆解:从一句语音到继电器吸合
3.1 语音端:关键词识别与串口上报
语音端的核心逻辑是“一直听,听到关键词就上报”。大多数离线语音模块的工作方式是:你先把关键词和对应的编号烧录进去,模块识别到之后通过串口吐出一个固定格式的编号。所以语音端固件其实很薄,主要工作是初始化串口、配置模块、循环读取识别结果。
// voice_node/main.c 主循环节选 void app_main(void) { uart_init(VOICE_UART, 9600); // 语音模块默认9600 voice_load_keywords(); // 下发词条配置 while (1) { uint8_t id; if (voice_read_result(&id) == 0) { // 读到一次识别结果 uint8_t frame[4]; build_frame(frame, CMD_VOICE_EVENT, id); uart_send(VOICE_UART, frame, 4); // 上报给控制端 } delay_ms(20); // 别裸奔死循环,留点余量 } }voice_read_result内部做的是按帧头找包、校验、取编号,跟第 2 章的parse_frame是同一套思路。这里有个细节:delay_ms(20)不是随便加的,语音模块吐数据有间隔,循环太快会重复读到同一帧,导致一条指令触发两次。我一般会在读到结果后清一次接收缓冲,双保险。关键词配置那块,不同模块的指令集不一样,资源里voice_driver.c把配置命令封装成了voice_load_keywords,换模块时只改这一个函数,上层不用动。
3.2 控制端:串口收指令、翻译、蓝牙转发
控制端是整个项目的中枢,它要同时管两个串口:一个接语音模块,一个接蓝牙模块。逻辑是收到语音事件后,查一张映射表,把“关键词编号”翻译成“设备命令”,再通过蓝牙发出去。映射表用数组实现,改起来直观。
// control_node/main.c 指令映射与转发 typedef struct { uint8_t voice_id; // 语音模块上报的编号 uint8_t cmd; // 设备命令字 uint8_t param; // 开/关 } voice_map_t; static const voice_map_t map[] = { {1, 0x01, 0x01}, // “开灯” -> 灯,开 {2, 0x01, 0x00}, // “关灯” -> 灯,关 {3, 0x02, 0x01}, // “开风扇” -> 风扇,开 }; void handle_voice_event(uint8_t voice_id) { for (int i = 0; i < sizeof(map)/sizeof(map[0]); i++) { if (map[i].voice_id == voice_id) { uint8_t frame[4]; build_frame(frame, map[i].cmd, map[i].param); bt_send(frame, 4); // 蓝牙透传,等同串口发送 return; } } // 没匹配到就丢弃,别乱发 }映射表的好处是加设备只加一行,不用改逻辑。bt_send底层就是往蓝牙串口写数据,配对成功之后跟普通串口没区别。这里容易踩的坑是蓝牙模块的波特率和主控对不上,现象是发出去没反应,查半天以为是协议错。我的习惯是初始化时先发一个测试帧,用tools/serial_monitor.py在执行端那侧抓,抓不到就先查波特率和配对状态,别急着怀疑代码。
3.3 执行端:蓝牙收帧与继电器驱动
执行端最单纯:蓝牙收到帧,解析,根据命令字和参数控制对应 GPIO。继电器模块一般是高电平吸合,但市面上也有低电平触发的,接线前必须确认,否则会出现“喊开灯结果灯灭”的反直觉现象。
// exec_node/main.c 收帧与执行 void on_bt_frame(const uint8_t *buf, uint8_t len) { uint8_t cmd, param; if (len < 4) return; if (parse_frame(buf, &cmd, ¶m) != 0) return; // 校验不过直接丢 switch (cmd) { case 0x01: relay_set(RELAY_LIGHT, param); break; case 0x02: relay_set(RELAY_FAN, param); break; case 0x03: relay_set(RELAY_SOCKET,param); break; default: break; } } void relay_set(uint8_t ch, uint8_t on) { gpio_write(ch, on ? RELAY_ACTIVE_LEVEL : !RELAY_ACTIVE_LEVEL); }RELAY_ACTIVE_LEVEL这个宏就是用来适配高/低电平触发的,改一个宏就能换模块,不用动逻辑。parse_frame校验不过直接丢,这是执行端的安全底线,宁可不动也不能乱动。另外继电器吸合瞬间电流冲击大,如果主控和执行端共用电源,建议加个电容,不然偶尔会出现蓝牙断连,这种玄学问题查起来很折磨人。
4. 跑起来:编译、烧录与联调顺序
4.1 环境准备与三个节点的烧录
三个节点各自独立编译,工具链看芯片型号,资源里没绑定具体芯片,但代码是标准 C,移植到常见的单片机开发环境都不难。烧录顺序建议先执行端、再控制端、最后语音端,因为执行端最独立,先把它调通,后面联调时心里有底。
# 以常见的命令行编译为例,具体工具链按芯片替换 cd exec_node && make && make flash # 先烧执行端 cd ../control_node && make && make flash # 再烧控制端 cd ../voice_node && make && make flash # 最后烧语音端烧录前确认三件事:串口号别选错、波特率跟代码里一致、蓝牙模块已经配对。这三件事任何一件出问题,现象都是“没反应”,新手很容易在这里卡住。我的做法是每烧完一个节点,就用serial_monitor.py单独验一下它能不能正常收发,别三个一起上再排查。
4.2 用调试脚本先模拟指令,再上语音
联调最忌讳一上来就对着麦克风喊。正确顺序是:先用tools/serial_monitor.py往控制端注入一条模拟语音事件,看蓝牙有没有发出去;再往执行端注入一条命令帧,看继电器有没有吸合;两段都通了,再接语音模块。
# tools/serial_monitor.py 模拟一条“开灯”语音事件 import serial ser = serial.Serial('COM3', 9600, timeout=1) frame = bytes([0xAA, 0x01, 0x01, 0xAA ^ 0x01 ^ 0x01]) ser.write(frame) print("sent:", frame.hex())这段脚本里0xAA ^ 0x01 ^ 0x01就是校验位,跟固件里的算法必须一致,不一致就会被执行端丢掉。COM3换成你实际的串口。先跑通这条,再跑语音,能把“识别问题”和“链路问题”彻底分开,省下大量瞎猜的时间。
4.3 联调检查表:按链路顺序逐段验证
联调时我习惯按链路顺序过一遍,每段都有明确的观察点:语音端喊话后串口有没有输出、控制端收到后蓝牙有没有发出、执行端收到后继电器有没有动作、负载有没有通电。任何一段断了,就停在那段单独查,不要跳着查。这套检查表看着笨,但能避免“灯不亮就重烧所有固件”这种无效操作。资源里的docs/protocol.md建议对照着看,指令对不上时第一时间翻它。
5. 避坑与排查:五个真实踩过的坑
5.1 喊了没反应,串口却有数据
现象是语音模块明明上报了,控制端却没动作。原因多半是映射表里的voice_id跟模块实际吐出的编号对不上,模块出厂编号从 0 开始还是从 1 开始,不同批次可能不一样。解决办法是先用串口工具把模块吐出的原始字节打出来,对照映射表逐个核对,别凭文档想当然。
5.2 指令发出去,执行端偶尔不动作
现象是十次里有一两次继电器不动。原因通常是蓝牙透传丢包或者帧被截断,尤其在主控和执行端距离远、中间有遮挡时。解决办法是在执行端加一个简单的重发机制,控制端发完等一个 ACK,超时重发一次;同时检查电源,蓝牙模块供电不稳也会导致偶发丢包。
5.3 继电器吸合但灯不亮
现象是能听到继电器“咔哒”声,灯却没反应。原因一般是继电器接的是常开还是常闭搞反了,或者负载供电和继电器回路没共地。解决办法是万用表量继电器输出端通断,确认吸合时是哪两个端子导通,再对照负载接线,别只看模块丝印。
5.4 一条指令触发两次
现象是喊一次“开灯”,灯开了又关。原因是语音模块连续吐出两帧相同结果,或者主循环读得太快重复处理。解决办法是读到结果后清接收缓冲,并在控制端加一个短时间窗去重,比如 500 毫秒内相同命令只执行一次。
5.5 蓝牙配对成功但收不到数据
现象是手机或电脑显示已配对,执行端就是没数据。原因是经典蓝牙串口透传需要建立串口通道,光配对不够,有些模块还要发 AT 指令进入透传模式。解决办法是查模块手册确认透传配置,用serial_monitor.py在执行端串口上抓,抓不到就回头查模块状态,别怀疑协议代码。
6. 二次开发:把演示原型往可用方向推
这套代码拿来交演示够了,但要往“日常能用”推,有几个改造点值得先做。第一是加状态回传,现在只有下行没有上行,喊了开灯但灯到底开没开,控制端并不知道。做法是在执行端动作后回一帧状态,控制端收到后更新本地状态表,这样才谈得上“智能”。第二是把映射表从代码里挪到配置文件,加设备不用重新编译,改一行配置就行,这在设备一多的时候差别巨大。
// 状态回传帧:cmd | 0x80 表示这是状态上报 void report_state(uint8_t cmd, uint8_t param) { uint8_t frame[4]; build_frame(frame, cmd | 0x80, param); bt_send(frame, 4); }用命令字最高位区分“控制”和“状态”,解析时先看最高位再分发,改动量很小,但链路一下子完整了。第三是给语音端加个“确认音”,识别到关键词后蜂鸣器响一声,用户知道自己被听到了,体验上的提升比识别率从 90% 提到 92% 更明显。
验证改造是否成功,我一般用两个指标:一是连续喊 20 次,统计成功率和误触发次数;二是拔掉语音模块,只用调试脚本注入指令,确认控制链路本身不依赖语音端。这两个都过了,才算改到位。
说个自己的教训:早些年做类似项目,我总想一步到位把协议设计得很“完备”,结果字段越加越多,调试时反而不知道哪一帧是哪一帧。后来我给自己定了个规矩,任何演示项目的第一版协议不超过 5 个字节,先把闭环跑通,再谈扩展。从那以后每次加字段前,我都强制走一遍“这字段现在真的需要吗”的自问,省下不少返工。希望这份拆解能帮到你,先把灯点亮,剩下的慢慢来。
本文还有配套的精品资源,点击获取