☰
ZigBee温湿度采集实战:组网、上报、网关解析与校准排障
2026/11/4 8:47:12 网站建设 项目流程

简介:面向物联网无线传感网学习者与开发者的ZIGBEE温湿度采集工程包,围绕DHT10温湿度传感器与ZIGBEE协议栈展开,可用于理解节点数据采集、无线组网传输和协调器接收处理等典型流程,适合课程设计、毕业设计与入门级项目复现。压缩包共637个文件,约26.18MB,以C源文件、H头文件和r51/s51/lst等编译链接产物为主,并含eww/ewp等工程配置、库文件、少量配置说明与工具文件,整体保留了较完整的工程目录与构建痕迹。已有1317人学习下载。读者可据此梳理ZIGBEE协议栈在采集节点中的应用方式,结合DHT10驱动、网络配置和数据处理代码,比较不同构建输出与工程配置,形成从传感器读取到无线传输再到上位展示的排错思路,也可作为二次开发与实验验证的参考基础。

1. 40 个温湿度测点不拉一根线:ZigBee 采集到底解决什么问题

一个中型冷库要布四十多个温湿度测点,走 RS485 拉线,线材加人工的报价顶得上传感器本身三倍,穿保温层打孔还会破坏库体密封;换 WiFi 模组,穿墙衰减和 AP 带机量又成了新麻烦。这是我第一次把方案换成 ZigBee 温湿度采集的现场。ZigBee 跑在 2.4GHz 的 IEEE 802.15.4 之上,单信道 250kbps,靠协调器建网、路由器中继、终端节点睡眠采集,天生适合“测点密、单包小、要电池供电、要自愈”的场景。它不追求带宽,追求的是几十上百个节点在一个网里安安稳稳活几个月不换电池。这篇按选型组网、传感器读取与报文封装、网关解析入库、现场排障、校准漂移的顺序,把 ZigBee 温湿度采集这条链路讲透,适合要做小规模无线环境监测的嵌入式与上位机开发者。

2. 选型与组网:先定信道和 PAN ID,再谈节点能不能进网

很多人做 ZigBee 温湿度采集,第一步就扎进传感器驱动里,结果节点能读数据却死活入不了网。顺序反了。温湿度采集这条链路上,射频和组网是地基,传感器只是地基上的一颗螺丝。这一章先把三条可选路线摆清楚,再把信道、PAN ID、节点角色、轮询周期这四个决定成败的参数定下来。

2.1 三条落地路线:Z-Stack 自研、串口透传模组、裸 802.15.4

做温湿度采集,市面上能走的路其实只有三条,选错了后面全是返工。

第一条是拿 CC2530 这类 8051 内核的 ZigBee SoC,配 Z-Stack 协议栈自己写应用层。模块成本低,一个节点十几到二十几块钱,功耗和睡眠模式完全可控,自定义属性、OTA、绑定都能做。代价是要啃 OSAL 的事件轮询、NV 存储、ZCL 属性表,入门那两周会比较难受。

第二条是买带透传固件的 ZigBee 模组,串口发一条 AT 指令就把温湿度传出去了。三天能出 demo,但它把报文格式锁死在厂商私有协议里,你要加个电量、加个 RSSI、改个上报间隔,都得看固件脸色。测点数量一上去、要和别人的网关对接,就开始难受。

第三条是自己写裸 802.15.4 点对点,不用 ZigBee 协议栈。代码量最小,但你也就没有了路由、没有了自组网、没有了入网认证,节点一多就得自己造轮子,通常只在两三个点的实验场景里划算。

维度Z-Stack 自研串口透传模组裸 802.15.4
上手时间1~2 周1~3 天3~5 天
单节点成本低中低
自组网与路由有有(固件内)无
报文可定制完全可控基本不可改完全可控
低功耗可控性高中高
适合规模20~200 点10 点以内3 点以内

我一般会选第一条。理由不是技术情怀,是温湿度采集项目后期几乎一定会加东西——加低电量告警、加上报间隔远程下发、加现场校准系数,这些在透传模组上都是死路。

2.2 2.4GHz 上先躲 WiFi:ZigBee 信道的位图配置与实测

ZigBee 的 16 个信道(11~26)和 WiFi 的 2.4GHz 频段是硬重叠的。WiFi 一个 20MHz 信道就压住四五个 ZigBee 信道,现场只要有一个大功率 AP 在旁边,温湿度数据就开始周期性丢包,看起来像“玄学”,其实是频谱打架。

WiFi 信道中心频率大致占用建议避开的 ZigBee 信道
12412 MHz2402~242211、12、13
62437 MHz2427~244715、16、17
112462 MHz2452~247219、20、21
132472 MHz2462~248221、22、23

实战里被验证过比较干净的是 ZigBee 信道 15(2425MHz)、20(2450MHz)、25(2475MHz),其中 25 最靠边,和 WiFi 1/6/11 都隔得开,我默认用它。配置在协议栈的f8wConfig.cfg里改,信道是个位图,channel n 对应 bit(n-11):

/* f8wConfig.cfg —— 组网三件套,改完必须全片擦除重烧,否则 NV 里的旧信息会覆盖 */ -DDEFAULT_CHANLIST=0x00004000 /* bit14 = channel 25 (2475MHz),只用这一个信道 */ -DZDAPP_CONFIG_PAN_ID=0x1A62 /* 固定 PAN ID,绝不留 0xFFFF 让节点随机选 */ -DNV_RESTORE=TRUE /* 掉电保存网络信息,重启后自动回到原网络 */

位图这个坑值得单独说一句:0x00004000是 2 的 14 次方,对应信道 25;很多人抄了0x00000800以为是信道 25,其实那是 bit11,对应信道 22,正好压在 WiFi 11 的边上。改完配置一定要擦除芯片的 NV 区再烧,否则节点带着旧信道信息上电,协调器在 25、它在 22,两边互相听不见,现象就是“节点上电闪两下灯就再也不动了”,非常像硬件坏。

2.3 协调器、路由器、终端:节点角色分错,组网就是玄学

ZigBee 网里有三种角色,分工不能乱。

协调器全网红只有一个,负责建网、分配 16 位短地址、维护绑定表,通常挂在网关的串口上常供电。它是单点,所以位置要尽量放在测区几何中心,别塞进配电箱铁壳里。

路由器是常供电节点,负责中继和转发,可以带子节点。测区跨度超过 30 米、或者中间隔了两道墙,就必须有路由器,否则边缘节点只能靠提高发射功率硬顶,功耗和稳定性都难看。

终端节点是电池供电的温湿度采集点,大部分时间在睡,醒来读一次传感器、poll 一次父节点、把数据发出去再睡。终端不能转发别人的数据,也不要让它当路由。

这里最容易翻车的是把电池节点错配成路由器。路由器必须常收常听,接收机一直开着,CR2032 撑不过三天。反过来,把常供电的节点配成终端,它又睡又醒,反倒让下游节点失去中继。

规模上有个经验值:单个协调器直连的子节点控制在 20~30 个以内比较稳,超过就靠路由器分层。协议栈里MAX_NEIGHBOR_ENTRIES(邻居表)和MAX_RTG_ENTRIES(路由表)这两个宏决定了网络能撑多大,默认值各版本不一样,别照抄网上的数字,翻一下自己那版配置文件再定。表满了的表现是“新节点入网失败但老节点一切正常”,很隐蔽。

2.4 轮询周期换成电池寿命:4 组实测参数对照

终端节点的功耗几乎全部来自“多久醒一次”。轮询周期(poll rate)决定了它多久问一次父节点“有没有我的数据”,也直接决定了电池寿命。

以 CR2032(约 220mAh)为例,单次唤醒收发约 30ms、平均电流 20mA,睡眠电流按 2µA 算:

轮询周期占空比平均电流理论寿命
1 s3%≈0.62 mA约 15 天
10 s0.3%≈62 µA约 5 个月
30 s0.1%≈22 µA约 13 个月
60 s0.05%≈12 µA约 2 年

这张表是理想值,实际还要算上入网重试、路由发现、电池自放电,打个七折比较靠谱。所以温湿度采集这种分钟级需求,轮询周期设 30 秒是甜点:电池能撑一年,数据延迟也在可接受范围。

但这里埋着一个致命参数:协调器为睡眠终端缓存下行报文的时间是有限的(协议栈里对应间接消息超时,常见默认值在 7 秒量级)。你把轮询设成 60 秒,终端一小时才醒来问一次,协调器早就把要下发给它的“配置上报间隔”命令扔了。下行命令永远收不到,上行数据却一切正常——这种单向故障最费时间。

3. 从 SHT30 到空中报文:温湿度采集的读取、校验与封装

传感器读取和报文封装是这条链路上唯一“写代码能立刻看到效果”的部分,也是最容易被草率对待的部分。温湿度值出现 0、出现 65535、出现来回跳变,八成不是射频问题,是这一层少了一步校验或者少了一次延时。

3.1 单次测量 6 个字节:SHT30 的 I2C 时序与 CRC8 校验

数字温湿度传感器(SHT30/SHT31 这一档)走 I2C,默认地址 0x44(ADDR 拉低)或 0x45。单次测量发两个字节命令,回六个字节:温度高、温度低、温度 CRC、湿度高、湿度低、湿度 CRC。CRC 多项式 0x31、初值 0xFF,这段校验不是可选项,是必须项——I2C 线上一个毛刺就能让高字节错位,算出来的温度能偏十几度。

#define SHT30_ADDR 0x44 /* CRC8: 多项式 0x31 (x^8+x^5+x^4+1),初值 0xFF,覆盖 2 字节 */ static uint8_t crc8(const uint8_t *d, uint8_t n) { uint8_t crc = 0xFF; while (n--) { crc ^= *d++; for (uint8_t i = 0; i < 8; i++) crc = (crc & 0x80) ? (uint8_t)((crc << 1) ^ 0x31) : (uint8_t)(crc << 1); } return crc; } /* 返回 0 成功;1 I2C 写失败;2 读失败;3 CRC 校验失败 */ uint8_t sht30_read(int16_t *t_001c, uint16_t *rh_001pct) { uint8_t cmd[2] = {0x2C, 0x06}; /* 高重复性 + 时钟拉伸,最大转换 15ms */ uint8_t buf[6]; if (!i2c_write(SHT30_ADDR, cmd, 2)) return 1; delay_ms(20); /* 必须大于 15ms,8051 上别用忙等凑 */ if (!i2c_read(SHT30_ADDR, buf, 6)) return 2; if (crc8(buf, 2) != buf[2] || crc8(buf + 3, 2) != buf[5]) return 3; uint16_t raw_t = ((uint16_t)buf[0] << 8) | buf[1]; uint16_t raw_h = ((uint16_t)buf[3] << 8) | buf[4]; /* 定点化:T = -45 + 175*S/65535,RH = 100*S/65535,统一放大 100 倍存整数 */ *t_001c = (int16_t)(-4500 + (int32_t)17500 * raw_t / 65535); *rh_001pct = (uint16_t)((uint32_t)10000 * raw_h / 65535); return 0; }

几个参数值得掰开说。命令0x2C06表示高重复性、开启时钟拉伸,转换时间最长 15ms,所以delay_ms(20)是安全余量;如果换成0x2400(免拉伸、中等重复性),延时可以压到 10ms 以内,但读数噪声会大一点。整段代码刻意用int32_t做中间运算、结果放大 100 倍存整数——8051 上没有硬件浮点,一个 float 除法能吃掉几百个周期,而且串口打印浮点还会额外拉长唤醒时间,直接烧电池。

0xFF/0xFFFF这类脏数据,绝大多数就是这里返回了 3 却没人检查返回值,把没通过 CRC 的 buf 直接拿去算了。

3.2 别自己造簇:用 0x0402/0x0405 标准簇封装温湿度

新手最容易犯的错,是自己定义一个 0xFC00 私有簇,把温湿度塞进自定义结构体。自己玩没问题,一旦要接第三方网关、要接通用抓包工具、要接别人写好的上位机,就全废了。

ZigBee 的 ZCL 里本来就有 Measurement and Sensing 这一组,温度和湿度是标准簇:

簇 ID名称属性 ID属性名数据类型单位
0x0402Temperature Measurement0x0000MeasuredValueint160.01 ℃
0x0405Relative Humidity Measurement0x0000MeasuredValueuint160.01 %RH
0x0402Temperature Measurement0x0001MinMeasuredValueint160.01 ℃
0x0405Relative Humidity Measurement0x0001MinMeasuredValueuint160.01 %RH

用标准簇的好处是抓包工具能直接把数值翻译成人话,第三方网关不用改一行代码就能认。上报命令是 ZCL 的 Report Attributes(命令字 0x0A),一帧典型报文长这样:

08 1C 0A 00 00 29 29 09 00 00 29 2E 16 │ │ │ └──┬──┘ │ └──┬──┘ └──┬──┘ │ │ │ │ │ │ └─ 湿度 0x162E = 5678 → 56.78 %RH │ │ │ │ │ └───────── 温度 0x0929 = 2345 → 23.45 ℃ │ │ │ │ └─────────────── 数据类型 0x29 = int16 / 0x21 = uint16 │ │ │ └──────────────────── 属性 ID 0x0000(小端) │ │ └────────────────────────── 命令字 0x0A = Report Attributes │ └───────────────────────────── 序列号 └──────────────────────────────── 帧控制 0x08:服务端→客户端,禁用默认响应

注意帧控制字节0x08里那位“禁用默认响应”。温湿度是周期上报,每条都回一个 Default Response 会让空口流量翻倍,电池节点尤其吃不消。这条在协议栈里对应AF_DISABLE_DEFAULT_RSP,默认是关的,得手动打开。

3.3 上报节奏怎么定:周期上报、变化上报和心跳保底

上报策略有三种,实际项目里通常混着用。

纯周期上报最好实现,代码里一个定时器,每 60 秒读一次、发一次。缺点是温湿度平稳时也在发,浪费电;突变时又要等下一个周期,反应慢。

纯变化上报省电,但有个致命缺陷:如果环境一直很稳,节点可能几小时不发一包,上位机就会误判它离线。所以必须配一个最大间隔兜底。

ZCL 标准的做法是 Configure Reporting,三个参数分别是最小间隔、最大间隔、可上报变化量。比如温度设“最小 10 秒、最大 300 秒、变化超过 0.5℃ 才报”,既能抓住突变,又保证 5 分钟必有一包心跳。

/* 端侧自己控节奏,不依赖 ZCL Reporting,逻辑更直白 */ #define REPORT_MIN_MS 10000UL /* 最快 10s 一次,防止抖动时刷屏 */ #define REPORT_MAX_MS 300000UL /* 最慢 5min 一次,作为心跳兜底 */ #define TEMP_DELTA 50 /* 0.5℃(单位 0.01℃)才触发 */ #define RH_DELTA 200 /* 2%RH 才触发 */ static uint32_t last_ms = 0; static int16_t last_t = 0; static uint16_t last_rh = 0; void report_task(void) { int16_t t; uint16_t rh; if (sht30_read(&t, &rh) != 0) return; /* 读失败直接跳过,等下一轮 */ uint32_t now = millis(); uint32_t dt = now - last_ms; int16_t dT = (int16_t)ABS(t - last_t); uint16_t dH = (uint16_t)ABS(rh - last_rh); if (dt < REPORT_MIN_MS) return; if (dt < REPORT_MAX_MS && dT < TEMP_DELTA && dH < RH_DELTA) return; zcl_report_temp_humi(t, rh); last_ms = now; last_t = t; last_rh = rh; }

REPORT_MAX_MS别超过 15 分钟。上位机的离线判定阈值一般设成“最大间隔 × 2.5”,你把最大间隔设成半小时,那节点掉线要一个多小时才报警,现场已经出事了。

3.4 序号、时间戳与离线判定:让每条数据都能追溯

终端节点上通常没有 RTC,也养不起一个 RTC 的静态电流。所以正确做法是:终端只负责携带一个自增的 1 字节或 2 字节序号,时间戳由网关在收到报文的那一刻打上。

这里要区分两个时间:采集时间和入库时间。两者之间可能差几秒(重传、路由发现),排查问题时这个差值就是线索。序号的作用是让网关能识别丢包——序号从 100 跳到 103,中间就丢了两包。哪些是射频丢的、哪些是串口丢的,看序号和串口帧序号能不能对上就知道了。

4. 协调器网关落地:串口帧协议、Python 解析与入库

协调器把空口收到的数据从串口吐出来,这一侧的工程质量和射频侧一样重要。串口是字节流,没有边界,帧协议没设计好,上位机就会陷入“收到半包—解析失败—丢数据”的死循环,而且这种问题在大流量下才暴露,测的时候一切正常。

4.1 先定帧格式:14 字节定长包的字段与 CRC16

我一般用定长帧,比变长帧好解析得多,代价是多占几个字节。温湿度采集一条数据就十几个字节,完全浪费得起。

偏移字节字段类型说明
02SOF—固定 0x5A 0xA5,帧头
21VERuint8协议版本,当前 0x01
31CMDuint80x01 温湿度上报,0x02 心跳,0x03 配置下发
42SEQuint16 LE全网关自增,回绕,用于检测丢帧
62ADDRuint16 LE节点 16 位网络短地址
82TEMPint16 LE0.01 ℃,范围 -4000~12500
102RHuint16 LE0.01 %RH,0~10000
121RSSIint8dBm,负数
131LQIuint8链路质量 0~255
142CRC16uint16 LEMODBUS 多项式 0xA001,覆盖 0~13 字节

整帧 16 字节。CRC 用 MODBUS 那套 0xA001 反序多项式,实现简单、查表方便。注意 CRC 只覆盖 0~13 字节,不包括自己。RSSI 和 LQI 一定要塞进帧里,这是后面现场排障唯一的窗口,没有它你就只能靠猜。

4.2 Python 侧拆帧状态机与 SQLite 落库

解析的关键不是struct.unpack,是“怎么在字节流里找到正确的帧起点”。用bytes.find(帧头)加 CRC 校验、校验失败只丢一个字节重找,这套逻辑能扛住绝大多数粘包和半包。

import struct, sqlite3, serial, time SOF = b'\x5a\xa5' FRAME_LEN = 16 FMT = '<2sBBHHhHbB' # 14 字节负载,不含末尾 CRC def crc16_modbus(data: bytes) -> int: crc = 0xFFFF for b in data: crc ^= b for _ in range(8): crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1 return crc def parse_stream(buf: bytearray): """从字节流里切出完整帧,返回 (记录列表, 剩余缓冲)""" out = [] while True: i = buf.find(SOF) if i < 0: buf.clear() break if i > 0: del buf[:i] # 丢掉帧头之前的垃圾字节 if len(buf) < FRAME_LEN: break # 半包,等下一批数据 frame = bytes(buf[:FRAME_LEN]) if crc16_modbus(frame[:-2]) != struct.unpack('<H', frame[-2:])[0]: del buf[:1] # 只丢 1 字节,保留可能存在的真帧头 continue _, ver, cmd, seq, addr, t, rh, rssi, lqi = struct.unpack(FMT, frame[:-2]) if cmd == 0x01: out.append(dict(seq=seq, addr=addr, temp=t / 100.0, rh=rh / 100.0, rssi=rssi, lqi=lqi)) del buf[:FRAME_LEN] return out def open_db(path='th.db'): db = sqlite3.connect(path) db.execute('PRAGMA journal_mode=WAL') # 边写边读,查询不会阻塞采集 db.execute('''CREATE TABLE IF NOT EXISTS th ( ts INTEGER NOT NULL, -- 网关收到时刻,秒 addr INTEGER NOT NULL, temp REAL NOT NULL, rh REAL NOT NULL, rssi INTEGER, lqi INTEGER, seq INTEGER)''') db.execute('CREATE INDEX IF NOT EXISTS idx_ts ON th(ts)') return db

几个参数的选择理由:PRAGMA journal_mode=WAL让写入和查询不互相锁,长期跑不会因为一个慢查询卡住采集线程;seq存进去是为了事后统计丢帧率;温度存 REAL 只是展示方便,原始整数也应该在帧里保留一份,方便回溯精度问题。

采集中间层里,串口读到的字节直接喂给parse_stream,返回的缓冲写回去继续用。上线前拿一个假数据发生器灌十万条,看内存是不是稳定、有没有帧被悄悄吞掉——这一步能提前抓到 90% 的拆帧 bug。

4.3 序号空洞与丢包:补点还是不补点

网关这边迟早会遇到序号不连续。我的做法是:不插值、不伪造,只在库里记一条 gap 事件。

原因很实在。温湿度是物理量,室温不会在 30 秒里跳 5℃,但如果你插一个线性插值出来,这条假数据会被后续的告警规则、报表统计当真数据用,出了问题根本查不出来。宁可让曲线断一截,让运维看到“这里丢了 3 包”,也不要让数据看起来漂亮。

真要补,就在查询层做,并且明确标记interpolated=1,和实测数据分表存。这个习惯是用血泪换来的:早期一个项目为了让曲线好看做了插值,结果某次制冷机组故障时的温度骤升被插值抹平了,报警晚了四十分钟。

4.4 网关自愈:串口掉线、协调器复位、数据积压

网关跑在工控机或者树莓派上,最常见的三种故障是串口设备名变了、协调器被静电打复位、数据库写不进去导致缓冲区爆掉。

串口不要写死/dev/ttyUSB0,用 udev 规则按芯片序列号绑定成/dev/zigbee-gw,插拔顺序变了也不会错。协调器复位后串口会重新枚举,读线程必须能捕获异常并重连,不能一崩到底。

def reader_loop(port='/dev/zigbee-gw', baud=115200): buf = bytearray() while True: try: with serial.Serial(port, baud, timeout=0.5) as ser: while True: chunk = ser.read(256) if chunk: buf.extend(chunk) for rec in parse_stream(buf): rec['ts'] = int(time.time()) db_sink.put(rec) # 有界队列,满了就丢最旧并计数 except (serial.SerialException, OSError): time.sleep(2) # 串口掉线,2 秒后重连

db_sink用有界队列,队列满时丢最旧的数据并单独计数报警。无界队列看着“不丢数据”,实际是等数据库卡住时把内存吃光,进程被系统杀掉,丢得更多。

5. 现场排查:ZigBee 温湿度采集最容易翻车的 5 个地方

温湿度采集上现场之后,问题往往不是“完全不工作”,而是“偶尔不好使”——一小时丢几包、某个角落的节点三天掉一次线、电池莫名其妙撑不到三个月。这类问题最难查,因为没有明确的报错。下面五条是反复踩过的。

5.1 五条血泪记录:现象、原因、解决

第一条:数据里偶尔冒出 0.00℃ 或者 655.35%RH。

现象:曲线每隔几小时出现一个尖刺,值明显不合理,其余时间正常。

原因:I2C 读取偶发失败(走线太长、上拉电阻偏大、和射频发射瞬间的电源波动耦合),sht30_read返回了错误码,但上层代码没检查返回值,把没通过 CRC 的缓冲区直接拿去换算。

解决:三层防护一起上。第一层,sht30_read返回值必须检查,非 0 就丢弃本次采样;第二层,连续 3 次读取失败才上报一个“传感器故障”状态位,避免单次抖动误报;第三层,I2C 上拉从 4.7k 改到 2.2k,传感器电源脚加 100nF 加 10µF 去耦。改完之后尖刺基本消失。

第二条:节点断电重启后,再也入不了网。

现象:换电池、断电重启之后,节点指示灯闪几下就静止,协调器那边看不到任何入网请求。

原因:开了NV_RESTORE,节点把上次的网络信息(PAN ID、信道、父节点地址)存在 NV 里,重启后直接尝试 rejoin。如果这期间协调器换过信道、改过 PAN ID,或者原父节点已经不在了,rejoin 就会一直失败,而很多默认配置里 rejoin 失败后不会自动回退到主动扫描入网。

解决:协调器和所有节点的信道、PAN ID 必须写死并保持一致;应用层加一个兜底逻辑——连续 rejoin 失败超过 5 次就调用网络层复位、清空 NV 重新入网。另外,批量改配置时一定要全片擦除,只烧固件不擦 NV 是最常见的自坑操作。

第三条:数据能上来,但延迟十几秒甚至几十秒,下行配置命令从来收不到。

现象:上位机看到的温度是“过去时”,而且从网关下发的“改上报间隔”命令,节点一次都没响应过。

原因:终端 poll 周期设得太长(比如 60 秒),而协调器为睡眠子节点缓存下行报文的超时时间默认只有 7 秒左右。缓存过期,报文被丢弃,节点的 poll 请求自然什么也拿不到。上行是节点主动发的,所以不受影响——这就是典型的单向故障。

解决:两条路选一条。要么把 poll 周期压到缓存超时以内(比如 5 秒),代价是功耗上去;要么把间接消息超时时间调大到覆盖 poll 周期。我一般选前者配合业务上的“配置命令只在节点上报后的窗口内下发”,因为改协议栈超时会增加协调器的内存占用,节点一多就顶不住。

第四条:电池标称能用一年,实际三周就没电。

现象:拆下来的电池电压还有 2.6V,但节点已经工作不正常。

原因:三个隐蔽的耗电源头。一是调试串口没关,UART 外设和电平转换芯片静态拉走几百微安;二是传感器没进掉电模式,SHT30 空闲时虽然只有零点几微安,但板上的 LDO 静态电流可能到几十微安;三是节点被误配成路由器,接收机常开。

解决:量产固件里用宏把调试串口整个编译掉;LDO 换成静态电流 1µA 级的型号;用电流表实测睡眠电流,这一步不能省——标称 2µA 的板子实测 200µA 是常事,多出来的往往是没配置的 GPIO 漏电或者 LED 限流电阻。测睡眠电流时记得把万用表串在电池上,且要等节点真正进入深睡后再读数。

第五条:节点超过 30 个以后,整个网开始不稳。

现象:20 个点的时候很稳,加到 35 个点以后,边缘节点频繁掉线、丢包率跳升。

原因:单信道容量、协调器的邻居表和路由表同时接近上限,加上所有节点几乎同时上电入网,入网请求风暴把有限的空口时间占满,路由发现包互相挤。

解决:分三步。第一步,把测区按物理位置分区,每个区放 1~2 个常供电路由器,减少单跳距离和路由深度;第二步,节点上电时加 0~30 秒的随机延时再发起入网,避开风暴;第三步,上报时刻也加 ±10% 的随机抖动,别让几十个节点在同一毫秒发包。

5.2 没有抓包仪时怎么定位:RSSI、LQI 和入网日志

不是每个现场都配得起专用抓包设备。这时候就靠两样东西:帧里带的 RSSI/LQI,和节点自己的入网日志。

指标含义健康范围异常时的典型表现
RSSI接收信号强度-40 ~ -75 dBm低于 -85 dBm 开始明显丢包
LQI链路质量指示180 ~ 255低于 100 说明误码率高,多半有干扰或距离过远
入网耗时从上电到拿到短地址小于 5 秒超过 30 秒说明信道拥堵或无可用父节点

RSSI 好看但 LQI 很差,基本可以断定是干扰而不是距离问题——信号够强,只是被噪声糊了。反过来 RSSI 很差 LQI 还行,那就是距离或遮挡,加个路由器就能解决。这两个指标配合看,能省掉大半的上门排查。

节点侧我还习惯在入网成功、rejoin、掉线三个时刻各打一条带时间戳的日志到串口(调试固件里),现场只要接一次串口看五分钟,就知道它到底经历了什么。

5.3 上线前的参数检查表

批量部署前,这张表我每次都会过一遍:

检查项期望值不检查的后果
信道固定 25,全网红统一与 WiFi 冲突,周期性丢包
PAN ID固定非 0xFFFF,全网一致节点随机选网,互相看不见
NV_RESTORE打开掉电后需重新入网,电池白白消耗
轮询周期≤ 5 s(要下行命令时)下行命令永远收不到
最大上报间隔≤ 300 s上位机误判离线
调试串口量产固件中关闭静态电流偏高,电池寿命腰斩
睡眠电流实测 ≤ 5 µA标称一年的电池三周就没
入网上电随机延时0~30 s批量上电时入网风暴

6. 校准与长期漂移:把 ±3%RH 压到能验收的水平

温湿度采集最后一道关是精度验收。数字传感器出厂标称的湿度精度在 ±2%RH 到 ±3%RH(20%~80%RH 区间)、温度 ±0.3℃,这个数字在冷链、仓储、种植这类场景往往卡在验收线上,得靠校准把系统误差拉回来。

我一般做两点校准,而不是单点偏移。单点只能修零点,斜率偏了照样超差。做法是把同一批节点和一支已溯源的手持表一起放进稳定环境,分别在低湿(比如 33%RH,饱和氯化镁溶液环境)和高湿(75%RH,饱和氯化钠溶液环境)各静置 30 分钟,记录每个节点的读数,然后对每个节点做线性拟合:

import numpy as np def fit_two_point(ref_low, ref_high, raw_low, raw_high): """ref_*: 标准表读数;raw_*: 待校准节点读数(同点位平均值)""" a = (ref_high - ref_low) / (raw_high - raw_low) # 斜率 b = ref_low - a * raw_low # 截距 return a, b a, b = fit_two_point(33.1, 75.3, 35.6, 73.8) print(f'校准: y = {a:.4f} * x + {b:.3f}') print(f'回代 50%RH 点误差: {a * 50 + b - 50:+.2f} %RH')

拟合出的a、

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询