1. 为什么我选择用 ESP32-C6 折腾一台 Zigbee 空气质量仪家里那台老式的 Wi-Fi 空气质量检测仪用了快两年最近实在忍不了了。倒不是传感器坏了而是它每隔几分钟就往路由器上发一次数据家里 IoT 设备一多2.4G 频段挤得跟早高峰地铁一样偶尔还会把几个 Wi-Fi 灯泡搞得离线。更烦的是它必须依赖厂商云服务断网就变砖数据还得先上传到别人的服务器再绕回来。作为一个把 Home Assistant 当家庭中枢的人这种数据先出家门再回家的架构我实在接受不了。后来我把目光投向了ESP32-C6这颗芯片。它是乐鑫第一款带802.15.4 射频的 RISC-V 芯片也就是说它原生支持Zigbee和Thread同时还能跑 Wi-Fi 6 和蓝牙 5。这意味着什么意味着我可以做一台本地直连、低功耗、不占 Wi-Fi 带宽的空气质量监测仪通过 Zigbee 协议直接接入 Home Assistant 的 ZHA 或 Zigbee2MQTT全程数据不出局域网。对于我这种对隐私和稳定性都有点强迫症的人来说这几乎是理想方案。这篇文章我会把整个项目从选型、原理、硬件搭建、固件开发到接入 HA 的全过程拆开讲清楚。适合谁看如果你已经有一台跑着 Home Assistant 的设备手里有 Zigbee 协调器比如常见的 CC2652 或 EFR32 方案又想自己动手做一台真正属于自己的环境监测节点那这篇内容基本可以照着抄。哪怕你之前没碰过 Zigbee 开发只要会点 C 语言和 ESP-IDF 的基本操作跟着走也能跑通。我会把踩过的坑、参数怎么算、为什么这么选全都摊开讲。2. 方案整体设计与核心思路拆解2.1 为什么是 Zigbee 而不是 Wi-Fi 或蓝牙先说说协议选型这件事因为这决定了整个项目的天花板。Wi-Fi 方案最大的问题是功耗和信道占用。一颗 Wi-Fi 传感器如果保持长连接平均功耗轻松上到几十毫安电池供电基本没戏就算用深度睡眠定时上报每次唤醒重连路由也要几百毫秒到几秒功耗峰值很高。蓝牙 BLE 倒是省电但它的星型拓扑决定了你必须有个常驻的网关做桥接而且 BLE 的连接间隔和广播机制在多点组网时体验一般。Zigbee 的优势在于它是为低功耗网状网络设计的。终端设备大部分时间处于休眠只在需要上报时唤醒功耗可以压到微安级。更关键的是Zigbee 走的是 802.15.4 的 2.4G 频段但它和 Wi-Fi 的信道是错开的——Wi-Fi 常用的 1、6、11 信道对应到 802.15.4 的信道规划里可以避开实际干扰比想象中小得多。我实测下来家里十几个 Zigbee 设备同时在线响应延迟基本在几十毫秒级别比某些云云互联的方案稳多了。ESP32-C6 的 802.15.4 射频支持 Zigbee 3.0理论上可以直接作为Zigbee 终端设备End Device加入现有网络。这里有个关键点它不能直接当协调器用虽然硬件支持但官方 SDK 对协调器角色的支持有限所以你的 HA 那边还是需要一个独立的 Zigbee 协调器。这一点在动手前必须搞清楚否则会走弯路。2.2 传感器选型为什么我最终用了 SCD41 PMS5003 组合空气质量监测的核心是传感器。市面上常见的方案有几类温湿度用 SHT3x 或 AHT20颗粒物用 PMS5003/PMS7003 或攀藤系列CO2 用 SCD4x 或 MH-Z19。我最终选的是SCD41CO2 温湿度加PMS5003PM1.0/2.5/10的组合理由如下。SCD41 是 Sensirion 的 photoacoustic CO2 传感器精度 ±(50ppm 5%)比 MH-Z19 那种 NDIR 方案体积小、功耗低而且它自带温湿度补偿输出的是经过校准的 CO2 值。MH-Z19 虽然便宜但需要预热、漂移大长期稳定性差一些。SCD41 走 I2C接线简单ESP-IDF 有现成驱动可以参考。PMS5003 是激光散射式颗粒物传感器UART 输出数据帧格式固定解析起来不复杂。它的缺点是风扇有噪音、寿命有限官方标称连续工作约 8000 小时但对于家用监测来说够用。如果你对噪音敏感可以考虑 PMS7003 或更小的 PMSA003接口和协议基本兼容。温湿度我本来想单独加一颗 SHT40但 SCD41 本身就能输出温湿度虽然精度略逊于专用传感器但作为环境参考足够了。少一颗芯片就少一份功耗和布线麻烦这个取舍我觉得划算。2.3 供电与功耗预算的粗略计算既然是 Zigbee 终端设备供电方案直接影响使用体验。我一开始想用电池但算了一笔账之后放弃了。SCD41 在单次测量模式下平均电流约 15mA测量持续约 5 秒PMS5003 工作时电流约 100mA风扇 激光ESP32-C6 在 Zigbee 上报时峰值约 80mA。如果每 60 秒上报一次每次活跃时间约 8 秒那么平均电流大概是活跃期15mA 100mA 80mA ≈ 195mA取重叠峰值占空比8s / 60s ≈ 13.3%平均电流 ≈ 195mA × 0.133 ≈ 26mA这还没算休眠时的漏电流。用 2000mAh 的 18650 电池理论续航只有 2000 / 26 ≈ 77 小时也就是三天左右。这显然不现实。所以最终我选择了USB 5V 常供电用一颗 AMS1117-3.3 或更高效的 DC-DC 降压到 3.3V。如果你真的想做电池版那就得砍掉 PMS5003只保留 SCD41并且把上报间隔拉长到 5 分钟以上续航才能到几周级别。提示ESP32-C6 的 Zigbee 终端在休眠时的电流可以做到几十微安但前提是你要正确配置esp_zb_sleep_enable()和 GPIO 唤醒源。如果传感器一直供电休眠省下来的电会被传感器吃掉这点要提前想清楚。3. 硬件搭建与关键细节解析3.1 核心物料清单与选型理由先把物料列清楚方便你对照采购。以下是我实际用的型号括号里是替代方案。物料型号作用备注主控ESP32-C6-DevKitC-1主控 Zigbee 射频官方开发板带 USB 转串口CO2/温湿度SCD41CO2、温度、湿度I2C 接口3.3V颗粒物PMS5003PM1.0/2.5/10UART 接口5V 供电降压MP1584EN5V 转 3.3V效率比 AMS1117 高电平转换无PMS5003 TX 是 3.3V实测直连可用外壳3D 打印固定传感器留进气孔ESP32-C6-DevKitC-1 的好处是自带 USB-C 和串口芯片烧录调试都方便而且板载天线在 2.4G 频段表现不错。如果你要打板做成品可以换成 ESP32-C6-WROOM-1 模组但要注意模组的 802.15.4 射频引脚和天线匹配。SCD41 我买的是带 breakout 的小板已经焊好了电平转换和上拉电阻直接插杜邦线就行。如果你买的是裸芯片记得 I2C 的 SDA/SCL 要各接一个 4.7k 上拉到 3.3V。PMS5003 的供电是 5V但它的 UART TX 输出电平是 3.3V所以可以直接接 ESP32-C6 的 RX不需要额外电平转换。这一点我一开始还担心用示波器量了一下确认是 3.3V才敢直连。3.2 接线方案与引脚分配接线这块我踩过一个小坑ESP32-C6 的默认 I2C 引脚和某些 GPIO 有复用冲突选引脚的时候要避开 Strapping 引脚GPIO8、GPIO9、GPIO15和 USB 相关的引脚。我最终的分配如下SCD41SDA → GPIO6SCL → GPIO7VCC → 3.3VGND → GNDPMS5003TX → GPIO4ESP32 的 RXRX → GPIO5ESP32 的 TXVCC → 5VGND → GND状态灯GPIO2 接一颗 LED用于指示配网和上报状态这里要注意PMS5003 的 RX 其实可以悬空因为我们只需要读取它的数据不需要发指令。但如果你想控制它的休眠PMS5003 支持通过指令进入低功耗模式那就得接上 TX 并发送命令帧。注意ESP32-C6 的 GPIO4 和 GPIO5 在默认情况下是 JTAG 引脚如果你要用 JTAG 调试就得换引脚。我不用 JTAG所以直接拿来用了。如果你需要调试建议换到 GPIO10/GPIO11 这类普通 IO。3.3 电源部分的处理经验电源是很多 DIY 项目翻车的地方。我一开始用 AMS1117-3.3 从 5V 降压结果发现 PMS5003 风扇启动瞬间的电流冲击会让 3.3V 轨电压跌落导致 ESP32-C6 复位。后来换成 MP1584EN 这颗 DC-DC输入 5V 输出 3.3V最大 3A效率 90% 以上问题就解决了。另外SCD41 和 PMS5003 的电源入口我都加了 100uF 的电解电容和 0.1uF 的陶瓷电容做去耦。PMS5003 的风扇是感性负载启动时会有反向电动势加个续流二极管比如 1N4148在风扇两端会更稳妥。这些细节看起来不起眼但实际调试时能省掉很多莫名其妙的复位问题。4. 固件开发从 ESP-IDF 到 Zigbee 终端4.1 开发环境搭建与工程结构固件我用的是ESP-IDF v5.1因为它对 ESP32-C6 的 Zigbee 支持比较完整。安装步骤这里不展开官方文档写得很清楚。重点说一下工程结构我建议按下面的方式组织air_quality_sensor/ ├── main/ │ ├── main.c # 入口初始化各模块 │ ├── scd41.c/.h # SCD41 驱动 │ ├── pms5003.c/.h # PMS5003 驱动 │ ├── zigbee_task.c/.h # Zigbee 终端逻辑 │ └── app_config.h # 引脚和参数配置 ├── CMakeLists.txt └── sdkconfig.defaultssdkconfig.defaults里要打开几个关键配置CONFIG_ZB_ENABLEDy、CONFIG_ZB_ZEDy终端设备、CONFIG_ESP_COEX_SW_COEXIST_ENABLEy如果同时用 Wi-Fi 和 Zigbee 才需要。我不用 Wi-Fi所以 coexistence 没开。4.2 SCD41 驱动实现要点SCD41 的 I2C 地址是 0x62。它的测量流程是发送start_periodic_measurement命令然后每隔 5 秒读取一次数据。数据是 9 字节CO2 高字节、CO2 低字节、CRC、温度高、温度低、CRC、湿度高、湿度低、CRC。CRC 校验多项式是 0x31初始值 0xFF。温度换算公式是-45 175 * raw / 65535湿度是100 * raw / 65535。这两个公式在数据手册里有但要注意 raw 是无符号 16 位别搞成有符号了。我封装了一个scd41_read()函数返回结构体包含 co2、temperature、humidity 三个 float。读取失败时返回错误码主循环里做重试。实测下来SCD41 上电后需要约 5 秒稳定第一次读数可能偏高建议丢弃前两次数据。4.3 PMS5003 数据帧解析PMS5003 每 200ms 到 800ms 主动输出一帧 32 字节的数据帧头是0x42 0x4D。第 3、4 字节是帧长度固定 0x001C第 5 到第 30 字节是数据最后两字节是校验和。校验和的计算方式是前 30 字节之和。关键数据位置PM1.0 在字节 10-11PM2.5 在 12-13PM10 在 14-15都是高字节在前。我只需要这三个值其他字段比如不同粒径的颗粒数可以忽略。解析的时候要注意UART 接收要用环形缓冲区因为数据是连续流式的不能假设一次 read 就能读到完整帧。我的做法是维护一个 32 字节的滑动窗口检测到帧头后开始填充填满后校验通过就更新数据。4.4 Zigbee 终端设备的关键配置这是整个项目最核心的部分。ESP-IDF 的 Zigbee SDK 提供了esp_zb_*系列 API。要让 ESP32-C6 作为一个 Zigbee 终端设备加入网络需要做几件事第一设置设备类型为终端。在esp_zb_cfg_t里把device_type设为ESP_ZB_DEVICE_TYPE_ED并把install_code_policy设为 false除非你要用 install code 配网。第二定义端点Endpoint和簇Cluster。空气质量数据通常映射到Analog Input Cluster (0x000C)或者自定义簇。我用了两个 Analog Input一个给 CO2一个给 PM2.5温湿度用Temperature Measurement (0x0402)和Relative Humidity Measurement (0x0405)。这样 Home Assistant 的 ZHA 能自动识别出对应的实体。第三实现上报逻辑。Zigbee 终端设备可以用esp_zb_zcl_set_attribute_val()更新属性值然后调用esp_zb_zcl_report_attr_cmd_req()主动上报。上报间隔我设的是 60 秒和传感器采样周期对齐。第四处理配网。设备上电后如果没入网要调用esp_zb_bdb_start_top_level_commissioning(ESP_ZB_BDB_MODE_NETWORK_STEERING)进入配网状态。配网成功后esp_zb_app_signal_handler()会收到ESP_ZB_BDB_SIGNAL_DEVICE_FIRST_START或ESP_ZB_BDB_SIGNAL_STEERING信号这时候就可以开始正常工作了。提示Zigbee 终端设备有一个父节点的概念所有通信都要经过父节点转发。如果父节点掉线终端会尝试重新寻找父节点。ESP-IDF 的 SDK 会自动处理这个过程但你要确保esp_zb_sleep_enable()的配置和父节点保活机制配合好否则容易出现设备假死。4.5 主循环与任务调度设计整个固件我用了 FreeRTOS 的多任务结构。main.c里创建三个任务传感器采集任务、Zigbee 上报任务、状态指示任务。传感器任务每 60 秒唤醒一次依次读取 SCD41 和 PMS5003把数据放进一个全局结构体用互斥锁保护。Zigbee 任务等待信号量收到新数据后更新属性并上报。状态灯任务用不同的闪烁模式表示配网中、已入网、上报成功等状态。这里有个细节SCD41 的测量周期是 5 秒PMS5003 是连续输出。如果我在一个任务里同步读取两个传感器总耗时可能超过 6 秒。为了不阻塞 Zigbee 任务我把传感器读取放在独立任务里用队列传递数据。这样即使传感器读取慢也不会影响 Zigbee 的响应。5. 接入 Home Assistant 与调试实录5.1 ZHA 配网与实体识别Home Assistant 这边我用的是ZHAZigbee Home Automation因为它是官方集成配置简单。前提是你已经有一个 Zigbee 协调器比如 Sonoff ZBDongle-E 或 Conbee II。在 ZHA 界面点击添加设备然后给 ESP32-C6 上电它会自动进入配网模式。正常情况下 30 秒内就能配对成功。配对成功后ZHA 会根据你定义的簇自动创建实体。CO2 和 PM2.5 会显示为sensor.xxx_co2和sensor.xxx_pm25温湿度也会自动识别。如果某个实体没出来大概率是簇定义和 ZHA 的 quirk 不匹配。这时候可以在 ZHA 的管理设备里手动查看簇属性确认数据有没有上报。我遇到的一个问题是 PM2.5 的值显示为 0。排查后发现是 Analog Input Cluster 的present_value属性类型设成了 int16而 PM2.5 的值可能超过 32767虽然实际不会但 ZHA 解析时按有符号处理导致异常。改成 uint16 后就正常了。5.2 常见问题排查速查表现象可能原因解决方法设备无法配网协调器信道不匹配检查协调器信道确保在 11-26 范围内配网成功但无数据簇定义错误核对 ZHA 识别的簇和属性数据偶尔丢失父节点信号弱靠近协调器或加路由设备ESP32 频繁复位电源电流不足换 DC-DC加去耦电容SCD41 读数异常未预热或 CRC 错误丢弃前两次数据检查 CRCPMS5003 无输出供电不足或 TX/RX 接反确认 5V 供电TX 接 ESP32 RX5.3 实测数据与稳定性观察设备连续跑了大概两周我记录了一些数据。CO2 在关窗的卧室里晚上会升到 1200ppm 左右开窗后 10 分钟内降到 500ppm 以下响应很及时。PM2.5 在做饭时能冲到 80 微克每立方米以上平时在 10 以下。Zigbee 上报的成功率我粗略统计了一下两周内没有出现丢包导致的实体不可用稳定性比之前的 Wi-Fi 方案好很多。功耗方面用 USB 电流表测的整机平均电流约 28mA和之前估算的 26mA 很接近。如果你要做电池版这个数字就是你的续航基准。5.4 几个我踩过的坑和独家技巧第一个坑是Zigbee 信道冲突。我家路由器 2.4G 用的是信道 6而协调器默认在信道 15。理论上不冲突但实际测试发现丢包率偏高。后来把协调器换到信道 25丢包率明显下降。建议你用 Wi-Fi 分析仪看一下周围信道占用选一个最干净的。第二个坑是SCD41 的自动校准。SCD41 默认开启 ASC自动基线校准它假设设备每天会接触到 400ppm 的新鲜空气。如果你的设备放在密闭空间ASC 会把基线拉偏。我的做法是关闭 ASC手动做一次强制校准在室外新鲜空气中运行perform_forced_calibration。第三个技巧是PMS5003 的休眠控制。如果你不需要连续监测可以给 PMS5003 发休眠指令让它停止风扇和激光能省不少电。指令帧是0x42 0x4D 0xE4 0x00 0x00 0x01 0x73唤醒是0x42 0x4D 0xE4 0x00 0x01 0x01 0x74。我实测休眠后电流从 100mA 降到 10mA 以下。第四个技巧是Zigbee 上报的节流。不要每次传感器更新就上报那样会占满网络带宽。我的做法是设置一个变化阈值CO2 变化超过 50ppm 或 PM2.5 变化超过 5 才立即上报否则按 60 秒周期上报。这样既保证实时性又不浪费带宽。6. 后续扩展与个人体会这台监测仪跑通之后我又做了几个扩展。一个是加了BME680做 VOC 检测通过 I2C 挂在同一条总线上地址不冲突。另一个是把固件改成支持OTA 升级这样以后改参数不用拆机插 USB。OTA 在 Zigbee 终端上稍微麻烦一点因为带宽有限我最后是用 Wi-Fi 做 OTAZigbee 只负责数据上报两者通过 coexistence 共存。如果你也想做类似的项目我的建议是先从最小系统开始只接 SCD41跑通 Zigbee 配网和上报确认 HA 能收到数据再逐步加传感器。一上来就把所有东西焊上去出了问题很难定位。另外ESP-IDF 的 Zigbee 示例代码esp_zigbee_*系列是最好的起点先跑通官方示例再往里面加自己的逻辑。最后分享一个我在调试时常用的小方法用esp_zb_zcl_set_attr_val()之前先打印一下属性的 cluster ID、attribute ID 和数据类型确认和 ZHA 那边识别的一致。很多数据不显示的问题根源都是类型或 ID 对不上。这个习惯帮我省了不少来回烧录的时间。