
1. 现象复现Class-C 会话已激活射频中断却整整静默前一段时间调一个基于 RAK3172 的 FUOTA 升级方案卡了整整两天光看现象能把人逼疯。事情的背景很简单设备端已经通过 RUI3 把 LoRaWAN Class 切换到 C协议栈日志里也确认Class-C session activeChirpStack 侧的多播组创建正常FUOTA 任务的分片一个个往下发下行帧计数持续增长——但设备端完全没有任何反应。最诡异的是射频中断日志干干净净没有 RxDone没有 RxTimeout也没有 RxError。三个事件一个都不出现。你可能会说收不到包可不就是没中断吗但真正做过 LoRaWAN Class-C 开发的人应该知道这句话本身就是最可疑的地方。因为 Class-C 设备在正常状态下radio 绝大部分时间都处于连续接收模式哪怕收到一个地址完全不匹配的广播包RX_DONE也会第一时间触发。现在三个事件全部静默说明问题根本不在多播地址过滤这个层级而是更底层的东西出了问题。我当时的调试环境是这样的硬件用 RAK3172 模块芯片是 STM32WLE5JC内部集成了 SX126x 射频内核固件用 RUI3 V4.x网络侧是 ChirpStack v4 全套服务包括 ChirpStack Gateway Bridge、Network Server、Application Server 和 FUOTA 模块。整个升级链路是 OTAA 入网之后设备进入 Class-C然后在 ChirpStack 创建 multicast group并通过 FUOTA 任务下发固件分片。换句话说这是一个很典型的多播固件升级场景但偏偏卡在了最开始的接收这一步。1.1 我观测到的三层静默为了把话说清楚我把当时的观测结果拆成了三层来记录。第一层是协议栈层。RUI3 的日志显示 LoRaWAN 协议栈的状态已经切换到 Class-C网络侧也确认设备在 Class-C 模式下注册没有异常上报。也就是说从 LoRaWAN 协议栈的角度看设备已经知道自己是 Class-C 设备并期待随时接收下行数据。第二层是网络侧。ChirpStack 的 FUOTA Server 任务日志显示分片已逐一调度下行队列里的多播帧计数在增加没有报超时或失败。网络侧认为自己已经把数据发出去了。第三层是射频底层——也就是真正出问题的层。设备端 radio 相关的 IRQ handler 一次都没被调用我最初为了排查方便在各个 IRQ 入口都加了 GPIO 翻转和串口打印结果串口一个字符都没有。这三层现象放一起就形成了一个很反直觉的画面协议栈觉得一切正常服务器觉得已下发但射频底层完全没有动静。中间的断点在哪儿就成了第一个要解决的问题。1.2 零IRQ和收到但被丢掉是两码事我先说一个很多初学者容易混淆的点RX_DONE是射频层事件而多播地址过滤、MIC 校验是 MAC 层逻辑。两者之间有明显的界限。具体来说SX126x 内核收到一个数据包只要接收完成帧头解析就会置IRQ_RX_DONE状态并通过 DIO1 引脚拉高通知 MCU。此时 MCU 读取数据后LoRaWAN MAC 层才会去检查DevAddr是否匹配、MIC 是否正确。如果 DevAddr 对应不上多播组MAC 层会静默丢弃但射频层的RX_DONE已经触发过了这在日志里是能看到的。我见过不少人把收不到上行 ACK和radio 没收到数据混为一谈。实际上如果你在设备日志里能看到RX_DONE但应用层不处理那是 MAC 层过滤或上层逻辑的问题如果连RX_DONE都没有那就是射频链路本身没在工作或者中断通路被截断了。这两个方向排查起来完全不同——前者要查多播密钥、组地址、AppSKey后者要查 radio 状态机、IRQ mask 和引脚映射。我当时把这两条路线想清楚之后直接跳过了多播配置的各种猜测直奔射频层。2. 为什么Class-C 会话 active不等于radio 真的在听定位到射频层之后我做的第一件事是查 RUI3 源码里 Class-C 切换的实现逻辑。虽然中间很多细节被固件封装了但核心机制是可以从现象倒推出来的。2.1 协议栈状态和射频状态是两层状态机这里必须先建立一个认知框架LoRaWAN 协议栈的 Class 状态和 radio 芯片的工作状态是两层独立的状态机。协议栈层关心的是网络语义比如这个设备当前是 Class A、Class B 还是 Class C什么时候该开接收窗口什么时候可以发送上行数据。而射频层关心的是芯片当前实际在干什么——是处于RX_CONTINUOUS连续接收还是SLEEP休眠还是STANDBY待机或者正在发送。理想情况下这两层状态应该严格对应。设备协议栈进入 Class-Cradio 就应该立刻进入连续接收模式且长时间保持。但工程实现里这两层状态之间是由代码衔接的衔接处一旦出问题就会出现上层以为在听底层根本没听的情况。我打一个比方协议栈是公司的前台它对外承诺我们 24 小时有人接待Class-C active但射频层是门口的保安保安可能因为换岗交接没做好已经回休息室睡觉了。前台不知道保安不在访客来了当然也没人开门。RAK3172 的 RUI3 固件里这个衔接通常是在lmh_set_class()或者等价的 class 切换函数内部完成的。函数会修改协议栈状态然后调用 radio 驱动层的接收接口。如果固件在这个流程里存在时序问题或者被其他中断、任务抢占就很容易出现状态已切换、射频没启动的情况。2.2 一次上行发送之后RX 窗口可能没被重新打开Class-C 设备并不是 100% 时间都在连续接收。每一次上行发送radio 都要从接收状态切换出去完成 TX 后再切回来。这个切回来的动作是 Class-C 设备最容易出问题的地方。具体流程是这样的正常情况下设备在 Class-C 模式下一直处于RX_CONTINUOUS。当应用层需要上报数据时radio 先切到STANDBY再进入 TX数据发完后radio 硬件会产生TX_DONE中断MCU 在中断处理里需要再次调用radio_set_rx_continuous()让设备重新回到连续接收状态。问题出在哪儿呢这个TX_DONE 之后重新打开 RX的动作依赖的是中断回调函数里代码的正确执行。如果回调里恰好有耗时操作或者固件在某次特殊流程比如 FUOTA 状态上报后紧接着处理多播命令里没有走到重新打开 RX 的代码分支radio 就会一直停在STANDBY甚至SLEEP状态。这个状态下协议栈依然认为自己是 Class-C——因为协议栈状态在lmh_set_class之后就再也没变过并没有任何标志说明 radio 已经离开接收模式。但射频层面芯片确实没在接收信号自然一个 IRQ 都不会产生。我在调试时怀疑过这个原因但当时直接看日志没法确认。因为 RUI3 固件层没有把radio 当前工作模式暴露到应用层日志。我后来的做法是专门发了一条上行数据然后用逻辑分析仪抓 DIO1 引脚的电平变化再对比 TX 结束后 radio 是否重新打开接收窗口。抓到的现象确实很可疑TX 完成之后DIO1 的 RX_DONE 中断并没有在我预期的时间窗内出现radio 状态也没能自动回到连续接收。2.3 IRQ mask 被覆盖是零IRQ的头号嫌疑除了状态机衔接另一个比它更能完美解释零IRQ现象的原因是 IRQ mask 被覆盖。SX126x 射频内核的中断机制是这样的芯片内部有一个 16 位的 IRQ 状态寄存器记录各种事件RX_DONE、TX_DONE、CRC_ERROR、PREAMBLE_DETECTED、SYNC_WORD_VALID、RX_TX_TIMEOUT 等。但这个状态寄存器不会直接把 CPU 打断是否真的产生外部中断信号还取决于一个单独的IRQ mask配置。mask 相当于一个总开关。只有 mask 中置位的那些事件才能通过 DIO1 引脚拉高来通知 MCU。如果某个时刻 mask 被全部清零那不管芯片内部状态寄存器如何变化DIO1 都不会翻转MCU 当然收不到任何 IRQ。这就产生了一个极其迷惑的现象射频芯片可能确实在连续接收、也收到了数据、状态寄存器里也有 IRQ_RX_DONE 标记但 CPU 完全感知不到。什么情况下 mask 会被清零常见的有三种芯片复位后的默认值可能是全零某些固件在初始化时没有正确设置 mask或者某个库函数为了做纯寄存器轮询主动关闭了 DIO 映射结束后没有恢复。RAK3172 用的是 STM32WLE5内部射频寄存器操作虽然有 HAL 层封装但底层逻辑和 SX126x 完全一致。如果 RUI3 固件的某个流程比如进入低功耗前的准备、或者 radio 初始化时的默认配置没有把 IRQ mask 写对那后面的接收动作就全部是哑巴操作——芯片在干活但不会说话。3. 从 DIO1 引脚到 IRQ 寄存器的完整排查链路现在把范围缩小到了射频层接下来的问题就变成怎么确认到底是状态机没进接收、还是 IRQ mask 被关了我当时的排查路径分了三步每一步都对应一种可能的故障层级。3.1 第一步让 radio 进入主动接收模式验证 DIO1 物理链路这一步的核心目的是把射频芯片是否在听和中断通路是否完好这两个因素先拆开。做法不需要改业务逻辑直接让设备进入一个明确可控的接收测试状态即可。我在 RAK3172 上临时配置了一个测试固件用 radio 驱动接口把设备设为RX_CONTINUOUS模式不做任何 LoRaWAN 协议处理纯粹让射频内核保持抓取前导码的状态。然后用另一台 RAK3172 作为发射端在相隔几米的地方持续发送 LoRa 数据包。逻辑分析仪挂在被测模块的 DIO1 引脚上同时串口打印RX_DONE中断回调。这个测试如果看到 DIO1 有脉冲、串口有输出说明射频前端、DIO1 物理通路、MCU 中断入口都是好的问题大概率出在 LoRaWAN 协议栈切换到 Class-C 之后radio 没有真正进入连续接收如果测试中 DIO1 依然完全静默那就要继续查射频内核的中断配置或者驱动层代码。我这边测试结果是 DIO1 正常翻转——这其实是个坏消息因为说明问题不在硬件通路而在软件状态迁移。也就是说设备在我认为 Class-C和实际上 radio 在工作之间确实存在某个断点。3.2 第二步直接读 IRQ 状态寄存器验证芯片是否真的见过数据既然 DIO1 通路没问题那就需要确认芯片在静默期间内部 IRQ 状态寄存器到底有没有变化。这一步也很有讲究。SX126x 提供GetIrqStatus操作码通过 SPI 总线可以读出当前 16 位 IRQ 状态。在 STM32WLE5 上底层通过 SUBGHZ 无线电接口访问。RAK3172 的 RUI3 固件默认不开放这个寄存器的直接读写但可以通过自定义固件的方式调用 HAL 层函数来读取。我在测试固件里放了一个定时任务每隔 1 秒读一次GetIrqStatus结果串口打印出来。同时从发射端持续发数据。这里有个非常关键的区分逻辑如果GetIrqStatus始终为 0说明射频内核确实没有检测到任何信号——要么没在接收状态要么频率/参数不匹配数据包根本没进到芯片。如果GetIrqStatus能读到IRQ_RX_DONE但 DIO1 没有触发中断那就是 IRQ mask 的问题mask 里没把 RX_DONE 映射到 DIO1。我很庆幸做了这一步因为结果直接指向了一个方向状态寄存器的 IRQ_RX_DONE 不为 0但 DIO1 没反应。这基本坐实了 IRQ mask 的问题。具体来说芯片收到了数据、解析了帧头、也在状态寄存器中记录了 RX_DONE 事件但中断输出 mask 没有让这个事件映射到 DIO1 引脚。3.3 第三步用抓包器确认下行帧真的到了射频前端在这一步之前还有一个容易忽略的因素要先排除ChirpStack 服务器是不是真的把多播帧发出来了万一是服务器调度器的问题根本没发出去那前面做的一切都白搭。验证方法有两个。一个是在网关侧看日志确认 ChirpStack Gateway Bridge 确实收到了 Network Server 下发的多播帧并且已经通过 SX130x 调制发送到射频前端另一个更直接——用一个 LoRa 嗅探器或者在现场放一台第二块 RAK3172 配成 Sniffer 模式抓取空中的多播数据包。我用的是 SX126x 嗅探器工具抓到了0x40多播数据帧。这表明链路从服务器到网关、再到空中都是通的。多播分片确实在空气中飞着只是目标设备的射频层视而不见。到这里整个排查链路的结论就很清晰了下行帧存在、射频芯片在物理上能收到、中断通路完好但 IRQ mask 的配置导致 RX_DONE 事件没有真正触发 DIO1。4. FUOTA 多播场景里三个更容易踩中的隐蔽坑在解决 IRQ mask 问题之前我还排查过另外几个方向虽然最后不是根因但在 FUOTA 多播这个具体场景里它们都是高频陷阱我觉得有必要单独列出来给别人一个参考。4.1 隔离测试先用单播下行验证 Class-C 链路我强烈建议遇到多播分片收不到的问题第一步先做单播下行测试。具体做法是保持设备在 Class-C 模式下不启动 FUOTA 任务直接通过 ChirpStack 给设备发一条单播下行消息比如设置一个应用层的下行计数器或者用 Application Server 的下行队列。这个测试的价值在于隔离变量。如果单播下行能正常触发 RX_DONE说明设备的 Class-C 接收链路整体没问题问题大概率在多播组配置上如果单播下行同样零 IRQ那问题就在设备自身的 Class-C 状态机或者射频接收链路上这时候去折腾多播密钥、多播组地址就是白费力气。我自己调试时设备的单播下行确认一直正常这也让我一度困惑了很久——单播能收多播却完全没反应。后来分析 IRQ 状态才明白这并不是多播包有多么特殊而是因为 FUOTA 流程启动后设备为了处理多播会话执行了一些额外的 radio 配置操作这才把 IRQ mask 覆盖掉了。单播测试正常是因为它不含这些配置流程。4.2 ChirpStack 多播组参数与设备实际参数的错位多播收不到很多人第一反应就是去查 ChirpStack 多播组的配置。这个方向没错但需要注意ChirpStack 的多播组参数和 RAK3172 设备实际配置的参数是两个独立的部分两者必须完全对齐。重点核对这几项核对项ChirpStack 侧设备侧多播组 DevAddr多播组配置里的 DevAddr设备通过多播会话加入后保存的 DevAddr多播会话密钥McSKey或 Separate Multicast Key设备保存的 McSKey数据速率 DR多播组配置的 DR设备当前 LoRaWAN 速率频率/频段多播组配置的 Frequency设备当前打开接收的频率如果 ChirpStack 侧的多播组 DR 和设备当前实际使用的 DR 不一致或者频段选择对不上设备射频层确实接收不到对应速率和频率的信号表现出来同样是零 IRQ。但这里要再强调一次如果是这种原因设备至少会处于正在接收但收不对参数的状态。你可以通过查看设备在静默期间有没有检测到前导码、有没有SYNC_WORD_VALID事件来进一步区分。这些事件在 IRQ 状态寄存器里都能看到。4.3 RUI3 固件版本对 FUOTA 多播会话的兼容性第三个坑是固件版本。RAK3172 的 RUI3 固件迭代很快不同版本对 LoRaWAN 1.0.4 的 FUOTA multicast session 支持程度有明显差异。我早期用的一个 RUI3 版本在 Class-C 模式下处理多播会话时存在已知的状态切换问题设备加入多播组之后radio 的接收配置在某些时序下会被重置。如果你遇到的问题是切到 Class-C 能收单播但一跑 FUOTA 或一加入多播组就收不到分片优先怀疑固件版本的兼容性是一个合理的排查方向。我当时的验证方式是先查阅 RUI3 的 release notes看是否有针对 multicast/Class-C 的修复记录然后用多个版本做对照测试。最终在某个较新版本上IRQ mask 的问题不再复现。不是说所有问题都一定能靠升级固件解决但在排查这类问题时固件版本差异是一个必须纳入考量的变量。5. 定位与修复:如何让多播分片真正触发 RxDone回到我自己遇到的问题。通过 GetIrqStatus 读到IRQ_RX_DONE存在、但 DIO1 不翻转之后方向已经很明确。接下来需要做的是确认 RUI3 到底在哪里把 IRQ mask 覆盖了以及如何在应用层规避这个问题。5.1 强制重置 radio 接收状态机我首先做了一件事在设备从 FUOTA 多播会话初始化回到正常接收流程之后主动调用一次 radio 接收重置接口显式设置RX_CONTINUOUS模式并重新配置 IRQ mask。在 RUI3 固件中应用层没法直接操作 SX126x 寄存器但可以通过自定义固件的方式利用底层 HAL 接口实现。STM32WLE5 的 HAL 库提供了Radio.SetDioIrqParams()这样的接口可以在初始化时指定让哪些事件映射到 DIO1。关键配置如下我在进入 Class-C 接收流程后显式做了这样一次设置/* 示例代码重新设置 IRQ 映射确保 RX_DONE 能触发 DIO1 */ uint16_t irq_mask IRQ_RX_DONE | IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR | IRQ_HEADER_ERROR; /* 将上述事件全部映射到 DIO1DIO1 才能向 MCU 上报中断 */ Radio.SetDioIrqParams(irq_mask, irq_mask, IRQ_RADIO_NONE, IRQ_RADIO_NONE); /* 确保芯片真正进入连续接收模式 */ Radio.SetOpMode(RADIO_OPMODE_RX_CONTINUOUS);这段代码的核心逻辑是把希望接收到的 IRQ 事件全部 mask 进 DIO1 映射。实测在这个配置之后FUOTA 多播分片的下行开始正常触发RxDone回调之前处理器在等但大脑没通知的怪异现象直接消失。这里有个容易踩的坑有些初始化代码里为了让某个寄存器轮询操作不被中断打扰会临时把 IRQ mask 置为 0之后忘了恢复。所以我在排查代码时也特意搜索了所有对SetDioIrqParams的调用点确认是哪一个流程把 mask 覆盖掉。如果你也遇到类似问题第一步就是要检查是否有任何地方在无意中把 mask 清空。5.2 核对 IRQ mask 与 DIO1 映射的关键逻辑我把 IRQ mask 的工作原理再说透一点这样你在自己的设备上排查时能少走弯路。SX126x 的SetDioIrqParams实际上有四个参数irqMask、dio1Mask、dio2Mask、dio3Mask。其中irqMask用来选择当前需要使能哪些 IRQ 源dio1Mask/dio2Mask/dio3Mask用来指定哪些事件分别映射到 DIO1、DIO2、DIO3 引脚上。在 STM32WLE5 内部DIO1 对应 RFDIO1是芯片内部的中断信号最终进入 EXTI 控制器。如果你在使用 RUI3 自定义固件这个 EXTI 通道的使能、优先级、中断服务函数注册都必须正确。如果这里有问题即使射频内核有 IRQMCU 端的中断入口也无法触发。我调试时专门抓了 EXTI 中断计数器的值。发现它始终为 0而 GetIrqStatus 有值这基本就是 mask 或 DIO1 映射的问题。把映射修正之后EXTI 中断计数器立刻开始增长和预期一致。5.3 ChirpStack 侧多播配置核对清单设备侧修好之后服务器侧的核对也不能省。我整理了一份清单每次做 FUOTA 多播时都会逐项确认设备当前 LoRaWAN Class 是否为 C且 ChirpStack 设备配置里的 class 属性与之一致。多播组的DevAddr、McSKey、DR、Frequency是否和设备当前实际参数一致。多播会话和 FUOTA 任务的启动顺序是否正确——设备必须先加入多播组再接分片顺序反了会收不到。分片下发时设备有没有在上行发送周期内如果设备正在发送大量上行数据下行窗口的衔接可能受影响。ChirpStack FUOTA 任务的分片大小和碎片数是否超过设备的接收 buffer 上限。这五项里第一项最容易犯迷糊。ChirpStack 设备配置里虽然可以设置 Class但在实际运行过程中设备自身的协议栈状态可能因为重启、入网流程等被重置为 Class-A而服务器侧还认为它是 Class-C。如果两边状态不同步多播下行就可能被调度在设备不监听的窗口表现同样是收不到。5.4 完整 FUOTA 升级流程验证修复之后不能只看能收一个包就收工。我最后做的验证是完整的 FUOTA 升级流程从 ChirpStack 上传新固件镜像创建 FUOTA 任务设备端逐步接收分片、上报进度到最终重组完成并重启运行新固件。这期间我重点观察了三件事设备是否逐个收到所有分片每个分片都能触发 RX_DONE 且没有 CRC 错误设备是否按照预期轮询状态上报给服务器服务器的任务进度是否最终显示完成升级后设备是否正常运行版本号是否符合预期。这个完整链路验证通过才算排障真正结束。如果只是收到几个包就认为修复了后面还有重组失败、镜像校验失败等一堆问题等着你。6. 个人经验这类问题要按什么顺序去查写到这里我想把这次排障最有价值的部分总结成一条排查顺序给同样在 RAK3172 ChirpStack FUOTA 方案上踩坑的人做个参考。第一优先级先分清现象层级。零 IRQ 和收到但丢弃完全是两条路。你需要先确认设备端到底有没有 RX_DONE如果有查多播密钥、组地址、MAC 层过滤如果没有查射频链路本身。这一步判断错误后面全白做。第二优先级验证 DIO1 物理通路和 IRQ mask。用主动接收测试 GetIrqStatus 的方式把芯片有没有收到数据和中断有没有送到 CPU拆开看。我遇到的情况是芯片收到了但 DIO1 不通知这类 bug 最隐蔽因为你从应用层看完全就是什么都没发生。第三优先级确认你的 Class-C 切换是不是一个假激活。不要只看协议栈日志要确认 radio 真的在连续接收。尤其注意上行发送之后接收窗口有没有重新打开。这个坑在数据量大的 FUOTA 场景里更容易触发。第四优先级再回到业务配置层。单播正常吗多播组参数对齐了吗固件版本对吗很多时候折腾到最后才发现问题根本不在多播组而是底层的中断配置被某个流程覆盖了。从我个人的经验看这类Class-C 已激活但射频零中断的问题九成不是天线没焊好也不是硬件坏掉而是软件状态机衔接处出了漏洞。解决办法也不复杂——把 radio 状态机和 IRQ mask 的配置显式控制起来不要依赖固件默认行为。你说芯片收不到包和芯片收到了包但不告诉你是完全不同的两码事。能分辨出这两者你就已经解决了排障过程中最难的那一步。