1. 现象复盘:同一个固件,TTN 上安安稳稳,ChirpStack 上几十秒后“哑掉”
1.1 测试平台的完整配置
这次问题的主角是一批基于 STM32WL55 的节点设备,用的是一颗将射频收发器和 Cortex-M4 应用核心集成在一起的 SoC,目标场景是 LoRaWAN 智能门锁。板子跑的是 ST 官方的 LoRaWAN 中间件(LoRaWAN MW)2.5.0,LoRaWAN MAC 协议版本 1.0.4,频段配置在 AU915。网络服务器有两个环境在并行测试:一个跑在 ChirpStack v4.x 上,另一个是 The Things Network(TTN)的 V3 云环境。
节点通过 OTAA 入网,Class A 模式,每 20 到 60 秒上报一次门锁状态、电量、开锁事件。第一批样品在 TTN 上跑了整整一周,一切正常,入网快、上行稳定,几乎没有掉包。然而同一批固件、同一批硬件板子,切到 ChirpStack 之后,刚开始几十秒的表现也正常,前 10 到 20 条上行消息都准时到达,但随后节点就“沉默”了,应用服务器上再也看不到任何上行数据。
这种“会先好一会儿再突然死掉”的现象,比一上来就不能用要难查得多。如果一开始就失败,大概率是频段配置、密钥、MAC 参数哪一项没对齐;但“先正常后故障”通常意味着设备在运行过程中接收到了某个改变运行状态的东西,把状态机打坏了。
1.2 复现步骤与典型表现
我录了一套稳定的复现步骤:
- 节点上电,ChirpStack 下发 Join Accept,OTAA 入网成功;
- 节点按周期上报,前 10 到 30 条上行全部成功,下行窗口也能收到应用下行;
- 在 ChirpStack 设备列表里能看到网络服务器定期发送 MAC 命令,其中有一条 LinkADRReq;
- 从这条 LinkADRReq 发出后的下一个上报周期开始,设备不再有任何上行,网关侧也收不到数据;
- 如果让设备重新上电或者强制重新入网,它又能工作几十秒,然后在网络服务器再次下发设备相关配置后再次卡死。
这中间有个最诡异的地方:设备并没有掉线。在 ChirpStack 的网关和 Network Server 日志里,设备仍然保持“已连接”状态,没有 Join 超时重连,也没有出现 DevNonce 重置。换句话说,节点把 MAC 层状态保存得很好,但它就是不发射了。这基本可以排除硬件损坏、电源异常这类低级问题,问题一定出在 MAC 命令处理链路里。
1.3 为什么这个故障值得单独拎出来写
这个故障最让我头疼的点是“TTN 稳定,ChirpStack 崩”,因为在很多研发团队看来,LoRaWAN 网络服务器都是按 LoRaWAN 规范实现的,换一个服务器不应该导致设备行为不同。可现实是,不同服务器对 ADR(Adaptive Data Rate,自适应速率)策略的差异非常大,而 ADR 又是通过 LinkADRReq 这种 MAC 命令直接干预设备运行参数的机制。设备端固件如果对某些命令组合处理有缺陷,只有某类服务器才会踩中。
尤其在做智能门锁这类产品时,上报链路直接关系到远程开锁、状态同步、安全告警,链路冻结就意味着“锁失控”。排查这个问题的过程,也让我把 LoRaWAN 的 MAC 命令链路重新啃了一遍,里面有很多文档里不写但实际调试时必须知道的东西。
2. 追根:LinkADRReq 到底是什么,AU915 下它在改什么
2.1 AU915 信道规划与 LinkADRReq 相关字段
先补充一点 AU915 频段的基础知识,否则后面讲的根本原因会很难理解。
AU915 是澳大利亚采用的 915MHz 频段计划,和北美 US915 基本同源,主要差异在部分上下行频率和功率限定上。它的上行信道分成两段:
- 64 个 125kHz 信道,编号 0 到 63,中心频率从 902.3MHz 开始,步进 200kHz,到 914.9MHz;
- 8 个 500kHz 信道,编号 64 到 71,中心频率从 923.3MHz 开始,步进 400kHz,到 927.5MHz。
LoRaWAN 节点入网后,可以使用这些信道中的部分或全部。对于大多数节点默认配置,ST 的中间件在 AU915 下会启用一组默认信道,实现上是 Mask 里默认把前 16 个信道或者某个子带打开,具体看 Region 配置。
而 LinkADRReq 是网络服务器向节点下发的一条 MAC 命令,主要用来调整节点的数据速率(Data Rate)、发射功率(TX Power)、信道掩码(Channel Mask)、每个消息的重复次数(NbTrans)等参数。这条命令包含的核心字段拆开是这样的:
- 命令标识符:0x03;
- 前 4 位:Data Rate 索引;
- 后 4 位:TX Power 索引;
- 16 位:ChMask,用来在一个 Channel Mask Control(ChMaskCntl)对应的信道组里做位映射;
- 3 位:Redundancy/ChMaskCntl,决定 ChMask 的语义;
- 1 位 + 4 位:NbTrans 和 RFU。
在 AU915 区域下,ChMaskCntl 的值非常关键:
| ChMaskCntl | 含义 |
|---|---|
| 0 | ChMask 对应信道 0 到 15 |
| 1 | ChMask 对应信道 16 到 31 |
| 2 | ChMask 对应信道 32 到 47 |
| 3 | ChMask 对应信道 48 到 63 |
| 4 | ChMask 对应信道 64 到 71 |
| 5 | 保留 |
| 6 | 保留 |
| 7 | ChMask 位置 0 到 5 表示 8 个 500kHz 信道 64 到 71,位置 6 到 7 留用,ChMask 其余位表示对全部 64 个 125kHz 信道做统一开关 |
这里最容易出问题的就是 ChMaskCntl 等于 4 和 7 的映射。如果设备端中间件实现的 Region 校验函数没有正确展开这些索引,或者错误地把某些保留位当作信道有效位来用,就可能导致节点在接收到 LinkADRReq 之后,把当前发射信道改到一个没有正确初始化的频率上。
2.2 ChirpStack 和 TTN 的 ADR 行为差异
在 TTN 上设备稳定,在 ChirpStack 上崩掉,直接原因就是两边 ADR 算法对 LinkADRReq 的构造方式不一样。
TTN 的 ADR 算法更偏向“保守”:它主要调整 Data Rate 和 TX Power,信道掩码基本保持默认,只有在设备入网时配置了额外信道时才可能去碰 ChMask。在 AU915 这种“默认信道已经足够多”的频段里,TTN 通常会让节点保留在已入网时的信道上跑,不会频繁在 MAC 层去改信道掩码。所以设备端中间件就算对 ChMaskCntl 某些取值处理不好,也根本没机会触发。
ChirpStack 的 ADR 则更“主动”。ChirpStack 会根据设备最近一段时间上报的 RSSI/SNR 统计,动态计算最优的数据速率和信道计划,然后把计算结果通过 LinkADRReq 下发。在我的环境里,ChirpStack 还配置了“设备启用信道不固定”的策略,也就是允许网络服务器根据区域子带情况,把节点调度到其他信道组,甚至可能下发 ChMaskCntl=4 来对 8 个 500kHz 信道做掩码开关。
当 ChirpStack 发出的这条 LinkADRReq 包含 AU915 的 500kHz 信道映射时,STM32WL 中间件 2.5.0 的 Region 处理逻辑就无法正确执行,状态机最终停在了一个“看似入网、实际无法发射”的位置。
2.3 不是所有“省电优化”都能无脑开
很多人会问:既然 ADR 会引起问题,那把设备端 ADR 关了不就行了吗?
这个想法只对了一半。ADR 是 LoRaWAN 网络优化的核心机制之一。打开 ADR,网络服务器可以根据链路质量自动降低发射功耗、提高速率,延长电池寿命。智能门锁这种电池供电设备,出厂时如果面板数据设置好了 ADR,长期运行能省不少电。如果因为一次偶发的兼容性问题就把 ADR 全部关掉,等于放弃了 LoRaWAN 的一个重要优势,后续功耗会产生明显差异。
问题不应该是“要不要开 ADR”,而是“在 ADR 开启时,设备端能否正确处理网络服务器发下来的每一条 LinkADRReq”。所以我们后面修复的落点也是让设备端对 ADR 命令的解释和网络服务器对齐,而不是简单粗暴地关功能。
在真正动手改代码之前,我花了大概一下午做了几组对照实验,把问题锁定到了具体环节。这一步很重要:如果方向错了,后面打的补丁都是瞎忙。
3. 排查过程实录:三步把问题钉死
3.1 先看网络服务器下发命令
第一件事是确认 ChirpStack 到底下发了什么。ChirpStack 的应用服务器和网络服务器是分开的,要拿完整日志得同时开几个组件。
我在 ChirpStack 的设备页面看到了 LinkADRReq 的记录,但只有一条聚合摘要,看不到每条 MAC 命令里 ChMask、ChMaskCntl 的具体值。于是我直接把 ChirpStack Network Server 的日志级别调到 DEBUG,在命令行里实时看:
docker logs -f chirpstack-network-server --since 10m日志里能看到类似这样的内容:
INFO[0015] ADR request received device_addr=260B... dr=3 tx_power=2 ch_mask=... ch_mask_cntl=4 nb_trans=1这里已经出现了一个关键信号:ChMaskCntl 是 4,而我的节点固件里 RegionAU915 的信道初始化掩码根本没有单独考虑 500kHz 信道组。也就是说,网络服务器确实把设备往 500kHz 信道或相关掩码方向上推了一把。
对比 TTN 侧日志,TTN 的 MAC 命令大多是调整 Data Rate 和 TX Power,ChMask 部分基本不动,或者只在入网后下发一次初始的参数设置。这就是两边行为差异的最直接证据。
这一条信息非常重要,因为它告诉我们:问题不是 LoRaWAN 协议本身不兼容,而是 ChirpStack 下发的 ADR 命令格式触发了设备端中间件的某个处理缺陷。
3.2 设备端中间件日志配合抓空口
光看服务器端还不够,设备端到底怎么处理这条 LinkADRReq,必须从固件侧看。
STM32WL 的 LoRaWAN 中间件提供了调试打印宏,比如LORAMAC_PRINT和REGION_PRINT。在 stm32wlxx_lora_mac.c 或者 RegionAU915.c 里把对应的宏打开,串口上就能看到 MAC 命令接收日志。我把打印等级调到最高,复现故障后拿到了设备端的执行路径:
RX: LinkADRReq dr: 3 tx_power: 2 ch_mask: 0x0001 ch_mask_cntl: 4 nb_trans: 1 RegionAU915LinkAdrReq() update channel mask... channel mask updated Return: LORAMAC_STATUS_OK表面上看,设备返回了 OK,LinkADRReq 被接收了。但仔细看后面的射频发射日志会发现,下一次上行周期到来时,中间件没有走到RadioSetChannel的流程,或者走到了但传入的频率不在正确范围内。也就是说,中间件认为“信道已更新成功”,但物理层没有真正切到新的发射频率。
为了确认是不是射频层的问题,我拿了一台频谱分析仪和一台 SX1301 网关抓空口。故障发生后,网关侧确实完全收不到节点在信道上发出的任何前导码。节点中频侧也没有看到 TX RSSI 出现毛刺。换句话说,节点在 MCU 层面可能就是没触发发射,而不是发了但丢包。
再到 IDE 里打断点,发现现象更明确:节点其实进入了某个 LoRaMac 状态机的错误分支,在等待一个永远不会来的 TxDone 事件,或者卡在MLME_Request的返回等待里。本质上,中间件的状态管理被这条 LinkADRReq 打乱了。
3.3 用排除法确认触发条件
到这一步,我已经高度怀疑设备端中间件对 AU915 下 ChMaskCntl=4 的 LinkADRReq 处理有缺陷,但为了严谨,又做了三组对照实验:
- 在 ChirpStack 设备 profile 里关闭 ADR,只保留网络服务器下发基本的 RX 窗口参数和 Join Accept 配置。结果:节点持续稳定运行 4 小时以上,没有出现冻结。
- 保持 ADR 开启,但在设备端把 Region 强行固定为 TTN 风格的默认信道掩码,不响应 ChMaskCntl=4 的信道切换。结果:节点稳定,但网络服务器一直重试下发 LinkADRReq,日志里有很多 MAC 命令重传。
- 保持 ADR 开启,并把设备端中间件换成一个经过补丁的版本,正确处理 ChMaskCntl=4 的信道掩码。结果:节点稳定运行,ChirpStack 的 ADR 也能正常生效。
三组实验下来,触发条件彻底锁定:ChirpStack 在 AU915 频段下发的 ChMaskCntl=4 的 LinkADRReq,会让 STM32WL LoRaWAN 中间件 2.5.0 进入错误状态,上行链路冻结。
4. 根因定位:STM32WL MW 2.5.0 的 AU915 LinkADR 处理缺陷
4.1 中间件版本与 MAC 状态机的坑
STM32WL 的 LoRaWAN 中间件是 ST 在 CubeMX/CubeIDE 生态里提供的一套协议栈实现。2.5.0 这个版本在 AU915 频段已经能完成基本入网和通信,但它的 Region 层代码对 AU915 的特有信道结构和 LinkADRReq 的 ChMaskCntl 值处理得不够细致。
具体问题出在 RegionAU915.c 的 LinkADRReq 处理函数里。LoRaWAN MAC 协议要求,当节点收到 LinkADRReq 时,必须校验 ChMask、ChMaskCntl、Data Rate 和 TX Power 的组合是否合法。校验通过后,需要更新节点的信道掩码、发射频率、速率和功率。在 AU915 下,64 个 125kHz 信道和 8 个 500kHz 信道是两个不同的频率范围,更新方式也不一样。
2.5.0 的实现里,对 ChMaskCntl=4 的处理存在索引错位。ChMask 是一个 16 位掩码,在 ChMaskCntl=4 时,它本应映射到信道 64 到 71,对应 500kHz 信道组。但中间件代码里用的是类似“把 ChMask 当作 0 到 7 的索引,然后直接赋值到某个 channel 数组”的逻辑,没有把索引偏移到 64 到 71 的区间。这就导致节点理解的“新信道”和网络服务器理解的“新信道”完全对不上。
如果只是“新信道”对不上,最坏情况也就是节点在错误的频率上发送,网关肯定收不到。但实际情况更恶劣:中间件在校验阶段会计算“更新后的信道掩码是否有效”,因为索引错位,可能计算出一个“零有效信道”的结果。这个结果会让节点认为没有任何一个可用上行信道,进而拒绝参与任何上行调度。于是 MAC 层既不报错(命令返回 OK),也不发射,状态机卡死。
4.2 ChMaskCntl 处理的逻辑错误在哪
我后来把大概的缺陷逻辑模拟出来了。在 2.5.0 的某个分支中,处理 ChMaskCntl=4 的代码大致是:
if (chMaskCntl == 4) { for (uint8_t i = 0; i < 16; i++) { if ((chMask & (1 << i)) != 0) { if (i < 8) { // 这里本应该把信道索引映射为 64 + i // 但实际代码可能直接使用 i,导致把 500kHz 信道映射到了 0~7 } else { // 超出 8 个 500kHz 信道范围的位置,可能被忽略或报错 } } } }更常见的一种错误是:中间件在收到 ChMaskCntl=4 后,不是把 ChMask 的每一位对应到信道 64 到 71,而是认为这是对“全部 64 个 125kHz 信道的某种扩展控制”,结果把 500kHz 信道的开关状态错误地合并进了 125kHz 信道掩码,导致整体掩码自相矛盾。
还有些派生版本在处理 ChMaskCntl=7 时也有问题:LoRaWAN 规范里 ChMaskCntl=7 代表“ChMask 里最低 6 位控制 500kHz 信道 64 到 71,其余位控制全部 125kHz 信道的统一开关”。如果中间件只解析了低 6 位,忽略了对 125kHz 信道组的统一控制,就会出现在网络服务器把所有 125kHz 信道关闭、只保留 500kHz 信道时,节点却仍然认为一些 125kHz 信道可用,最终选择一个未实际启用的信道发送。
这两个问题叠加起来,就是“接收到 LinkADRReq 之后,设备潜在可用的信道集合被算错,最后选择一个非法信道,或者一个被禁用的信道,导致上行静默”。
4.3 为什么 TTN 恰好“免疫”
TTN 稳定不代表 TTN 没有发过 LinkADRReq,它确实也发,但它的 ADR 策略通常不会在 AU915 下用 ChMaskCntl=4 去切换 500kHz 信道。TTN 在 AU915 区域内,设备默认使用的 125kHz 信道已经能满足多用户共存需求,ADR 算法把主要精力放在调整 Data Rate 和 TX Power 上,对信道掩码的更新比较保守。
这带来的结果是:即使节点中间件有上述缺陷,只要没有服务器用 ChMaskCntl=4(或 7)去碰 500kHz 信道,节点就一直在 125kHz 默认信道上正常通信。于是同一份固件,换了个网络服务器就立刻现形。
这种情况其实很常见:很多协议栈在某一区域的“默认配置路径”上跑得很顺,但一旦网络服务器的调度策略覆盖了中间件没有充分测试的边角路径,问题就暴露了。不能说 TTN 的 ADR 实现就比 ChirpStack 更标准,而是 ChirpStack 更激进地使用了协议里出现频率较低的字段和组合。
5. 修复方案与落地代码
5.1 方案一:升级 LoRaWAN 中间件
最推荐的修复方式其实是升级中间件版本。ST 在后续的 LoRaWAN 中间件版本(比如 2.6.0、3.0.x、4.x)里做了不少 Region 层和 MAC 状态机的修正,其中就包括对 AU915 信道掩码映射的完善。
STM32WL 的用户可以通过 STM32CubeMX 的中间件管理器直接拉新版 LW 中间件,也可以从 ST 的 GitHub 仓库下载。升级之后,需要把 Region 配置和射频配置重新生成,重点检查RegionAU915.c里的默认信道掩码和LoRaMac初始化配置。升级不是万能的,但大概率能解决这个特定问题。
如果你用的是某个带 BSP 和网络协议栈的完整项目,升级之前一定要做完整的回归测试。LoRaWAN 中间件版本升级往往涉及 MAC API 的签名变化,比如LoRaMacMibSetRequestConfirm的参数类型有所调整,应用层代码也要顺手改。
5.2 方案二:手动 patch RegionAU915.c 的 LinkAdrReq 分支
如果产品的量产计划已经拍了,或者你不能轻易改动中间件版本,也可以直接在当前版本上打补丁。以 2.5.0 为基础,在RegionAU915LinkAdrReq函数里,把 ChMaskCntl=4 和 ChMaskCntl=7 的分支做重写。
我的补丁思路是这样的:
- 先按 LoRaWAN 规范把 ChMaskCntl 映射到正确的信道索引区间;
- 更新信道掩码时,把 125kHz 信道和 500kHz 信道分开处理;
- 更新完所有可用的信道后,再重新计算当前的默认数据信道;
- 如果计算结果显示没有任何可用信道,则返回
LORAMAC_STATUS_PARAMETER_INVALID,拒绝执行这条 LinkADRReq,而不是让状态机进入死胡同。
核心补丁代码大约这样:
// 在 RegionAU915LinkAdrReq 中,替换原来的 chMaskCntl 处理分支 if (chMaskCntl == 4) { // 500kHz 信道 64~71 for (uint8_t i = 0; i < 8; i++) { uint8_t channelIndex = 64 + i; if ((chMask & (1 << i)) != 0) { RegionAU915EnableChannel(channelIndex); } else { RegionAU915DisableChannel(channelIndex); } } } else if (chMaskCntl == 7) { // 高 8 位控制 125kHz 信道,低 6 位控制 500kHz 信道 for (uint8_t i = 0; i < 6; i++) { uint8_t channelIndex = 64 + i; if ((chMask & (1 << i)) != 0) { RegionAU915EnableChannel(channelIndex); } else { RegionAU915DisableChannel(channelIndex); } } // 125kHz 信道的统一控制 uint16_t mask125 = (chMask & 0xFF00) >> 8; for (uint8_t i = 0; i < 64; i++) { // mask125 的第 7 位是最高位,可根据设备实际支持的子带选择处理逻辑 // 这里只做演示,真正的实现需要严格按照 AU915 的 ChMaskCntl=7 定义 } }这只是一个示例,实际项目里你必须结合中间件版本的具体函数签名、Region 结构体字段来写。另外,补丁完成后,一定不要只做单次测试,要把 ChirpStack 的 ADR 打开,连续跑 24 小时以上,让它经历各种信道切换组合,确保各种 ChMaskCntl 值都覆盖到。
5.3 方案三:不改固件,只改网络服务器配置
如果你暂时不想动设备端代码,还可以通过 ChirpStack 配置来规避问题。
在 ChirpStack 的设备配置页面,把 ADR 设置为禁用,或者把 ADR 的“漫游周期”调得很大。这样网络服务器不会主动通过 LinkADRReq 去调整节点的信道掩码和速率。最简单的方法是先把 ADR 关掉,然后找一个业务量不大或者对功耗不敏感的时段,再慢慢解决设备端协议栈兼容问题。
也可以在 ChirpStack 的 Network Server 配置里,把 AU915 区域的信道计划限制到设备实际支持的默认信道,不让服务器下发 ChMaskCntl=4 这种会涉及 500kHz 信道组的掩码。这样 ChirpStack 虽然还是会发 LinkADRReq,但命令的内容会落在中间件已经处理得很好的路径里,冻结就不会触发。
这个方案适合验证“是否就是 ADR 引起的问题”,也适合应急恢复业务,但它不是长久的解决办法。长期来说,设备端还是要能正确处理网络服务器的各种合法 ADR 命令。
5.4 针对智能门锁场景的上行保活兜底
智能门锁是一个典型的“低功耗、定期上报、偶尔有下行控制”的场景。对于这类产品,即使我们把 ADR 链路修复了,我还是建议在应用层加一重保活机制,防止未来遇到其他网络服务器、其他协议栈版本时再次出现“静默死亡”。
比较简单可靠的做法是:连续 N 次上行消息没有收到网络服务器 ACK 或确认,就触发强制重新入网。LoRaWAN 的 Class A 节点可以通过周期性地发送未确认消息,并观察网络服务器是否回下行数据来判断链路是否仍然健康。如果长时间没有任何下行回调,应用层就重新执行 OTAA 流程。
还有一招是在应用层做“注册确认”。设备每次上报状态时,网络服务器在合适窗口回一个应用层的确认包。设备如果在 3 到 5 个周期内收不到确认,就认为链路异常,主动执行重新入网。这样即使 MAC 层状态机卡住,应用层也能兜底,把设备从“冻结”中拉回来。
对于门锁这种安全属性强的设备,我还会在固件里加一个看门狗,把“在指定时间内没有成功完成任何上行且没有收到任何下行”作为一个异常条件触发复位。但要注意,看门狗复位后如果还收到同一条病态的 LinkADRReq,可能又会冻结,所以还是要配合中间件升级或补丁一起做,才能形成一个完整的闭环。
6. 最后分享一点经验:别迷信“同一个 LoRaWAN 版本号”
这次问题解决之后,我最大的感受是:在 LoRaWAN 这种生态里,设备端和网络服务器端都声称“支持 LoRaWAN 1.0.4”,不代表它们对每一条 MAC 命令的每个字段都做到了完全一致的理解。
不同网络服务器的 ADR 策略、默认参数、子带选择策略,可能差别很大。TTN 上稳如老狗,不代表 ChirpStack 上就一定稳;反过来说,也不能因为某个平台问题就全盘否定某个中间件版本。关键是,出现问题时要能快速把“上层业务故障”一层层剥离到“MAC 命令处理环节”这个层面,然后针对特定命令做回归和补丁。
我现在的习惯是:新项目拿到一块 LoRaWAN 模块/SoC 后,不会只在一个网络服务器上做过夜测试。至少要在 ChirpStack、TTN 各跑一轮长时间稳定性测试,故意把 ADR 打开,让网络服务器尽量多地触发各种 MAC 命令组合,把协议栈的边角路径都压一遍。条件允许的话,再搭一个私有的 LoRaWAN 网络服务器,把 LinkADRReq 的 ChMaskCntl 人工枚举一遍,直接验证设备端对每一种合法取值的行为。
如果你手里现在正好有一批 STM32WL 产品,在 ChirpStack 上遇到了类似“入网正常、ADR 后静默”的问题,建议你按这个顺序排查:先看网络服务器下发命令内容,再在设备端开启 Region 调试日志,然后把 ADR 临时关掉做二分确认。确认是 LinkADRReq 的 ChMaskCntl 处理差异之后,优先升级 LoRaWAN 中间件到新版本;升级受限就手写补丁;补丁都来不及就先用网络服务器配置稳住业务,再找窗口时间发货。
产品在产线多跑一分钟测试,到了用户手里就能少一个半夜被叫醒的售后问题。这是我这次踩坑最大的价值。