
简介基于ZigBee与STM32的智能家居系统完整源代码包面向嵌入式、单片机及物联网方向的学习者与开发者解决智能家居环境监控与终端控制的原型实现问题。上位机基于Qt编写可实时监控室内温度、湿度、烟雾浓度并通过LED灯模拟控制家用灯具适合课程设计、毕业设计或入门级项目二次开发。压缩包共267个文件、大小仅3.57MB包含51个.h头文件、49个.cpp源文件、22个.ui界面文件、多个.pri工程配置及资源文件另有97张png图片和20个wav音频用于界面与提示2个数据库文件提供数据存储支撑整体目录清晰便于按模块阅读。已有2557人学习下载社区关注度较高。通过阅读源代码可了解ZigBee数据采集、串口通信qextserialport、Qt图表绘制qcustomplot及SQLite数据管理等关键环节的整合方法获得一套从底层硬件到上层交互的完整参考实现有助于快速掌握STM32ZigBee架构下的智能家居开发流程。1. 为什么智能家居源码要纠结 Zigbee 与 STM32 谁做主控拿到《基于 zigbee 和 stm32 的智能家居系统源代码.zip》这类压缩包第一件事不是解压进 Keil而是先确认“Zigbee 网络层跑在哪颗芯片上STM32 业务层跑在哪颗芯片上”。两种芯片的分工直接决定代码怎么组织、要编译几个工程、主控与射频之间走串口还是 SPI以及按一次开关要经过哪几层回调。Zigbee 自组网负责低功耗节点的邻居发现、路由和断线重连STM32 负责按键、继电器、传感器这些和无线协议无关的硬件逻辑两边各管一段源码可读性才会出来。这套系统的难点不在把灯点亮而在把“设备动作抽象成消息”的框架。读源码时先找三个文件串口初始化、命令分发、设备驱动按这个顺序看下去后面遇到参数和报错就不会懵。2. Zigbee 协议栈与 STM32 业务层智能家居源码的分层边界2.1 ZCL、ZDO、APS 与 802.15.4先分清四层再动手智能家居代码里提到的 Zigbee通常默认是 Zigbee 3.0 这一整套协议栈而不是 802.15.4 裸射频。最底层的 802.15.4 由 PHY 和 MAC 组成负责 2.4GHz 频段的载波侦听、冲突退避和链路层确认再往上是 Zigbee 联盟定义的网络层 NWK负责节点加入、离开、路由查找和邻居表维护APS 层做端点到端点的绑定关系最顶端的 ZDO 则确认设备角色协调器、路由器、终端设备的分工就是在这里定义的。STM32 应用侧真正接触不到上面这些业务代码面向的是 ZCL也就是 Zigbee Cluster Library它把“开灯、调亮度、读温度”这类通用操作统一成 cluster 加 command 的格式。很多刚从单片机转过来的人第一反应是用 socket 的方式去拼报文但在 STM32 这一侧真正需要组装的只有十来个字节的 ZCL 头部。以最常用的 On/Off 集群为例源码里一般会把它定义成下面这样的结构体typedef struct { uint16_t dst_addr; /* 目标短地址协调器固定为 0x0000 */ uint8_t src_endpoint; /* 源端点模块默认 0x01 */ uint8_t dst_endpoint; /* 目标端点与目标设备描述符一致 */ uint16_t cluster_id; /* 0x0006 表示 On/Off 集群 */ uint8_t frame_ctrl; /* bit2 置 1 表示客户端发起请求 */ uint8_t seq_no; /* 自增序列号抓包时用来配对请求和响应 */ uint8_t cmd; /* 0x00 关、0x01 开 */ uint8_t payload[8]; /* 扩展参数On/Off 通常为空 */ } zcl_msg_t;这个结构体里值得留意的字段有三个。frame_ctrl的 bit0 决定这一帧要不要 APS 确认如果不置位设备执行了但应用层没收到响应的情况很难排查seq_no由发送端自增抓包时靠它把请求和响应配成一对cluster_id才是真正区分业务类型的地方0x0006 是开关0x0008 是调光。把这三个字段读明白基本就能看懂协议栈侧代码其余比如端点号通常由模块内部的 endpoint descriptor 固定不常改。还有一个边界值得强调ZCL 以下的路由、重传、父节点切换对 STM32 完全是透明的业务代码不需要去读邻居表也没必要。所以判断一份智能家居源码封装得是否合理就看 STM32 代码里是否出现原始 802.15.4 帧。如果出现说明作者把 Zigbee 当成了裸射频在用工程维护成本会明显偏高。2.2 协调器在 STM32、协议栈在 Zigbee 芯片两种主从关系标题里把 Zigbee 和 STM32 并列大概率是双芯片方案Zigbee SoC 跑协议栈STM32 跑业务逻辑。常见的 Zigbee SoC 有 CC2530、CC2652、EFR32MG 这类STM32 负责采集和驱动两者通过串口或 SPI 交换帧。这种方案立项时往往有一个现实原因项目里已经有一块成熟的 STM32 底板带 I2C 传感器、继电器、LCD 屏换射频芯片比整个平台迁移成本低得多或者源码是从毕业设计扩展来的原先的按键和显示驱动都在 STM32 上写成型了。双芯片方案里还要再分两种源码性质完全不同。第一种是 Host 加 NCPZigbee 芯片只跑协议栈STM32 直接下发组帧命令例如指定 PAN ID、打开入网许可、发起 On/Off 控制接口是二进制的帧级命令CRC 错误通常发生在帧解析层。第二种是模块出厂时已经固化成某个角色STM32 只发字符串指令接口是 AT 指令级。读代码时先确认是哪种因为排错入口完全不同。前者的故障点是帧格式后者则要查串口回显和超时。我在拿到压缩包时一般直接搜索串口发送函数里是字节数组还是 ASCII 字符串。如果看到的是USART_SendData(USART1, 0x5A)这类连续字节大概率是帧级接口如果看到printf(ATZBJOIN\r\n)就是指令级接口。这两种工程不能混着维护串口波特率配置倒是可以共用。2.3 支持协调器和终端的源码通常按角色分目录一份完整的智能家居源码 zip解压后往往不是单个 Keil 工程而是协调器工程、终端设备工程、文档各占一块。角色不同固件不同即使两端都用 STM32也要分开编译。常见布局如下zigbee-smart-home/ ├── 01_Coordinator/ │ ├── App/main.c # 主循环、事件时间片 │ ├── Hardware/usart1.c # 与 Zigbee 模块的串口 │ ├── Zigbee/zb_interface.c # 帧封装与命令分发 │ └── Doc/ ├── 02_EndDevice/ │ ├── Sensors/sht30.c │ ├── Hardware/relay.c │ └── sys/ ├── 03_Gateway/ │ └── mqtt_bridge/ # 可选接 HA 或云端这种结构把相同的外设驱动分布在两个工程里首次打开时会觉得重复但它的好处是协调器和终端设备可以独立升级。优先读三个位置main.c主循环看它的周期时间片怎么分配usart1.c的中断回调看接收数据挂到哪个队列zb_interface.c的 switch-case 分支看一共认识多少种命令。命令码数量越少越容易在串口终端手工模拟调试效率也越高。3. 在 STM32 串口上跑通最小 Zigbee 灯控节点主循环与帧分发3.1 最小硬件接线与串口参数用 STM32F103C8T6 这类小容量主控接 Zigbee 模块串口的接线只有四根线但稳定性经常出在电源上。Zigbee 模块发射瞬间电流会到 30 到 45mA如果和 STM32 共用一颗 LDO电容不够时引脚会产生毛刺表现是模块反复复位、入网到一半掉线。接法参考下面的表格VCC 与 GND 之间至少并一颗 10uF 电容模块离 STM32 超过 10cm 时就不要再用杜邦线飞线了。STM32 引脚Zigbee 模块引脚方向说明3V3VCC电源两者必须同电压GNDGND地共地后再通电PA9 / USART1_TXRXDSTM32 到模块模块串口输入PA10 / USART1_RXTXD模块到 STM32模块串口输出串口参数一般用 115200-8-N-1部分老模块默认 38400接线前先看源码里MX_USART1_UART_Init这段配置。硬件流控 RTS/CTS 在这种场景通常不接但模块如果默认开了流控二线接法会导致收能发不能此时要先把模块恢复出厂设置或者把波特率调回源码里写死的那一档再继续排查。3.2 STM32 串口接收分解为命令帧的状态机源码里最容易被改坏的就是串口接收。直接在主循环里调用HAL_UART_Receive阻塞等待会把整个业务循环卡住入网事件稍有延迟传感器轮询就断。常见做法是帧级接口加一个非常轻的状态机用串口中断逐字节收帧格式约定成[帧头][长度][命令][负载][CRC]。volatile uint8_t rx_done 0; uint8_t rx_byte; uint8_t rx_buf[64]; uint8_t rx_index 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { rx_buf[rx_index] rx_byte; if (rx_index 1 rx_buf[0] ! 0x5A) { rx_index 0; /* 帧头错误丢弃重来 */ } else if (rx_index 2 rx_buf[1] 0) { rx_index 0; /* 长度字段非法 */ } else if (rx_index 2 rx_buf[1] 1) { rx_done 1; /* 整帧收齐 */ rx_index 0; } HAL_UART_Receive_IT(huart1, rx_byte, 1); } }这里rx_index的复用有个关键点第 2 字节是负载长度整个帧长等于 2 加长度字段再加 1 字节 CRC。状态机的处理时间被压缩到中断里执行的几个判断负载的 CRC 校验移到主循环再算。中断里只置标志位不要在回调里去动 GPIO 或调用HAL_Delay。主循环侧再按命令码分发灯光控制这类实时性要求不高的动作放到这里执行while (1) { if (rx_done) { rx_done 0; if (rx_buf[2] 0x01) { /* 命令 0x01开灯 */ HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); } else if (rx_buf[2] 0x02) { /* 命令 0x02关灯 */ HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); } } zigbee_poll(); /* 非阻塞查询 */ }zigbee_poll()是这类源码里常见的调度入口它负责把上层要发送的数据写入发送缓冲也读取模块主动上报的入网状态。把发送和接收都收敛到串口之外灯控逻辑就只剩下一个switch后续加新设备时不容易改崩协议层。3.3 协调器创建网络与终端入网的最小步骤双芯片方案里协调器创建网络的步骤通常是先固定 PAN ID再打开发网许可最后等模块上报网络状态。顺序不能反先开网络再改 PAN ID会导致终端已经加入的节点全部掉线。下面这个函数只在初始化阶段调用一次后面交给zigbee_poll处理回包。static void zigbee_nwk_start(role_t role) { if (role COORDINATOR) { module_send_frame(CMD_SET_PANID, 0x1234); module_send_frame(CMD_NWK_START, 0); /* 等待模块上报 0x81网络建立成功 */ } else { module_send_frame(CMD_SET_PANID, 0x1234); module_send_frame(CMD_NWK_JOIN, 0); /* 等待模块上报 0x82入网成功 */ } }终端入网时模块需要进行至少一次信道扫描耗时通常在 1 到 3 秒之间这期间串口不会有任何数据返回。不要在发送CMD_NWK_JOIN后立刻用阻塞方式等应答否则会把zigbee_poll卡死。正确做法是把入网状态转成一个全局枚举主循环确认超时时间由HAL_GetTick来管避免陷入无响应死循环。4. 入网耗时、重传与低功耗Zigbee 组网的 3 个可调参数4.1 参数速查表PAN ID、信道、入网允许时长、重发次数智能家居源码里真正要调的参数不多多数是写在配置头文件里的宏。调试时改得最多的几个它们的取值范围和影响面可以参照下表参数常见取值默认值影响PAN ID0x0001 到 0xFFFE0x1234同一空间内区分不同网络信道掩码11 到 260x07FFF800避开 WiFi 干扰频段入网允许时长0 到 254 秒180只在新设备入网时短暂开启最大重发次数2 到 53越高越可靠时延和功耗同步上升APS 确认开 / 关开控制命令可靠性关键PAN ID 设置成 0xFFFF 表示自动选择这个值在源码里容易被当成固定 ID 用结果每次复位模块都换网络终端永远找不到协调器。入网允许时长不是一直开着的参数设计上应该只在配对模式开放否则周围的 Zigbee 设备随时可能加进来占用邻居表容量。信道掩码在 WiFi 干扰严重的环境里要显式裁剪只允许 20、24、25 这几个信道能明显减少重传。4.2 用 HAL_GetTick 测入网耗时并校正重发次数入网耗时不靠肉眼估给 STM32 上电后记 tick等模块上报入网成功事件再打印是最直接的验证手段。写法和超时保护一起做避免设备卡死在第一次组网上。uint32_t t0 HAL_GetTick(); while (nwk_status ! JOINED) { zigbee_poll(); if (HAL_GetTick() - t0 15000) { printf(join timeout\r\n); break; } } printf(join time: %lu ms\r\n, (unsigned long)(HAL_GetTick() - t0));干扰环境下入网时间从几百毫秒跳到十秒以上往往不是模块性能问题而是重发次数设置过高。终端在每个信道上驻留的时间片是固定的重发次数再把每个失败帧拉长一倍全部扫完才会停止。先缩小信道掩码再决定是否调高重发次数顺序不要反过来。常态双路由链路的入网时间如果超过 8 秒优先级最高的排查项是信号强度和天线匹配而不是协议配置。4.3 低功耗的代价休眠终端对源码的限制低功耗只适合 Zigbee 终端设备路由器和协调器必须保持接收否则子节点找不到父设备。源码里开启休眠通常是在模块配置里设置rx_on_when_idle 0终端只在设置的休眠间隔里醒来发送数据或轮询父节点缓存的数据其余时间射频全部关闭。这时候有两个常见的坑。第一个是 STM32 还被唤醒源约束如果 STM32 和模块都在休眠必须是模块的 TX 线能唤醒主控串口中断优先级要高于定时器用轮询方式检测模块输出主控永远醒不过来。第二个是入网阶段不能直接开休眠设备处于未关联状态时它的父节点是未知的Zigbee 协议规范要求先以rx_on_when_idle 1入网入网成功后再切换成休眠模式。5. 组网失败与离线丢包的排错清单先看串口再看抓包5.1 第一步确认 STM32 发出的帧到了 Zigbee 模块串口上组网失败的现场经常是这样协调器串口打印了一堆发送日志但终端设备没有入网协调器的网络也没建立。这时候先不要怀疑 Zigbee 射频先确认 STM32 发的数据字节是否原样到了模块。最简单的方法是把模块的 TXD 和 RXD 单独引出来用逻辑分析仪抓USART1_TX引脚对比rx_buf里的帧头。模块不响应时逐项检查三件事。第一波特率与源码配置是否一致很多老模块默认 9600代码里却写的 115200第二接线是否接了反模块 RXD 接 STM32 TX 是对的但付邮购买的转接板可能印反了丝印第三CRC 算法是否有移位和异或顺序差异。CRC 在多数代码里是简单异或不是 CRC16直接套 CRC16 库会全部判错uint8_t frame_crc(uint8_t *buf, uint8_t len) { uint8_t crc 0; for (uint8_t i 1; i len; i) { crc ^ buf[i]; /* 从长度字段开始异或 */ } return crc; }如果逻辑分析仪上能看到完整帧说明 STM32 侧收发没问题问题在 Zigbee 模块自身状态进入下一步。如果连帧头发都不齐全那就回到HAL_UART_RxCpltCallback看是不是中断回调里处理太慢或者HAL_UART_Receive_IT只启动了一次。5.2 第二步确认协调器网络是否真的建立入网失败不一定发生在射频层协调器根本没创建网络的情况很常见。尤其使用固定 PAN ID 时PAN ID 冲突会让协调器反复尝试终端加入时永远匹配不上。此时用串口向协调器模块查询两种状态当前 PAN ID 和允许入网标志。如果这两个状态都没问题再看终端方面有没有出现“join failed”事件回调这类事件的 event id 在源码里会有定义直接搜索JOIN前缀的宏即可。排查时保持一个原则一个节点一个节点测不要同时打开所有终端。多个终端同时请求入网协调器邻居表会瞬间被打满表现为终端都收到了 beacon但后续的关联请求得不到响应。先把协调器的邻居表清空再逐个验证否则分不清是参数问题还是并发容量问题。5.3 用 Zigbee Sniffer 抓包定位“灯亮了但网关显示离线”这类问题最典型控制端发送开灯命令后灯确实亮了但上位机或 HA 里一直显示离线。这不是执行失败而是响应帧丢了或者配对信息不一致。用串口日志看不出原因需要抓空中的 Zigbee 包。常见做法是用 CC2531 这类 Sniffer 硬件配合 Wireshark把信道设置成与网络一致然后过滤短地址。在 Wireshark 里输入过滤表达式只保留目标设备相关的交互zbee_nwk.dst 0x1234 || zbee_src 0x1234观察请求帧和确认帧的时间差。如果控制请求发出后灯侧有 APS ACK但网关侧没有收到响应说明问题出在网关与协调器的串口交互而不是 Zigbee 无线链路。如果连 ACK 都没有再看源端是否做了重发重发次数是否耗尽。这种排查方式比猜“模块坏了”有效得多也是源码里看不到、但实战里最常遇到的一类坑。6. 拿到源码后的四个进阶改动上报、透传、OTA 与重连6.1 周期上报与差值上报传感器节点别只会定时发终端传感器节点最常见的错误是把温湿度数据每秒都上报一次功耗全耗在射频上了。室温和湿度变化是缓慢量采样频率可以低上报更应做差值判断。在 STM32 侧维护一份上次上报值超过阈值才触发发送if (abs(temp_new - temp_last) 1) { zcl_msg_t msg {0}; msg.cluster_id 0x0402; /* 温度测量集群 */ msg.cmd 0x00; /* 读属性响应 */ memcpy(msg.payload, temp_new, 2); zigbee_send(msg); temp_last temp_new; }周期上报作为保底差值上报作为主路径两者同时存在于main.c的不同时间片。电源供电的插座节点周期可以短电池供电的传感器节点周期至少放到 30 秒以上否则低功耗设计的成果被上报频率毁掉一半。6.2 通用透传把 ZCL 帧翻译成 STM32 应用消息读源码最费劲的地方是协议字段和应用逻辑耦合在同一个结构体里。二次开发前建议加一层翻译把收到的 ZCL 帧统一转成只含cmd和data的应用消息业务层不再感知 cluster 的概念。typedef struct { uint8_t dev_id; /* 设备编号 */ uint8_t cmd; /* 0x10 开、0x11 关、0x12 上报 */ uint16_t data_len; uint8_t data[8]; } app_msg_t;这样传感器驱动、继电器驱动、按键扫描依赖的都是同一个消息结构。以后要换 Zigbee 芯片或改走 Thread只需要改zb_interface.c上层驱动一行不动。这个抽象层看起来多写几十行后期维护省下的时间远大于此。6.3 OTA 与掉线重连最能影响实际体验的两处时序Zigbee OTA 升级最大的坑不是传输失败而是升级过程中终端设备恰好进入休眠。触发 OTA 前要先把终端切到频繁唤醒模式升级完成后再恢复原来的休眠周期。没有 OTA 功能的源码至少也要保留一个远程重启命令让现场设备可以脱离预埋的升级工具做恢复。掉线重连和 OTA 在时间上要错开这是最容易忽略的细节。如果终端既配置了每 5 分钟重连一次又在同一时间窗口检查固件版本两个操作会抢同一个串口优先级导致 Zigbee 模块收到错帧。重连间隔放到 2 分钟OTA 检查放到整点和半点两者不重叠整个系统才不会有“一到某个时刻就集体离线”的诡异问题。本文还有配套的精品资源点击获取