
如果你在用 NUCLEO-WL55JC1 这颗板子跑 ST 官方的 LoRaWAN_End_Node_LBM 例程调试时在串口日志里看到Join Accept Failed with Code 14然后去 ChirpStack 那边翻网关日志又什么都看不出来大概率已经在论坛和 issue 里泡了两三天了。先说结论这个 Code 14 并不是 ChirpStack 返回的错误码而是 ST 官方 LoRaWAN 协议栈在LORAMAC_EVENT_INFO_STATUS里定义的枚举值。它表示的是 end-device 在接收到 Join Accept 消息之后MIC 校验失败或者 MAC 头部的 DevAddr/会话密钥派生结果对不上。翻译成人话就是板子已经收到了网络侧下发的 Join Accept 报文但校验后认为这个包是非法或伪造的于是拒绝入网。但问题往往不在链路本身而是出在入网参数和 ChirpStack 后台配置之间的隐性不匹配上。这篇文章我把排查过程、原理、参数计算、以及最终解决方式完整写出来帮后来人少走弯路。1. 先搞清楚 Code 14 到底是谁报的错很多人第一次看到这个错误会下意识以为是 ChirpStack 那边拒绝了你实际上不是。Code 14 来自 ST 的 LoRaWAN 协议栈源码具体定义在LoRaMacTypes.h或LoRaMac.h的LoRaMacEventInfoStatus_t枚举中对应的值含义是LORAMAC_EVENT_INFO_STATUS_CRYPTO_FAIL 14这个状态码出现在MacProcessJoinAccept()函数处理流程中也就是设备在发送 Join Request 之后进入了 RX1 或 RX2 接收窗口成功捕获到 Join Accept 帧但是在执行LoRaMacVerifyJoinAccept()时失败具体是 MIC 计算不一致。要理解这个 MIC 校验失败得先看 LoRaWAN 1.0.x 协议里 Join Accept 的加密和完整性保护机制。1.1 Join Accept 消息的 MIC 是怎么算出来的在 LoRaWAN 1.0.3 协议里Join Accept 消息由网络服务器Network Server使用 AppKey 进行加密生成其结构从前到后依次是MHDR1 字节固定为 0x20AppNonce3 字节NetID3 字节DevAddr4 字节DLSettings1 字节RxDelay1 字节CFList可选0 或 16 字节MIC4 字节其中 MIC 的计算方式如下MIC aes128_cmac(Key AppKey, Message MHDR | AppNonce | NetID | DevAddr | DLSettings | RxDelay | CFList)[0..3]也就是说MIC 是对 Join Accept 的整个明文头字段不包括 MIC 本身做 AES-CMAC 计算取前 4 个字节。如果服务器端派生 MIC 使用的 AppKey 和 ST 协议栈本地保存的 AppKey 不一致那么计算出的 MIC 必然不同协议栈就会判定为LORAMAC_EVENT_INFO_STATUS_CRYPTO_FAIL也就是 Code 14。1.2 为什么会出现 AppKey 不一致这里就要说到 ST 的LoRaWAN_End_Node_LBM例程里AppKey 的存储和使用机制了。在 ST 的 LBMLoRaWAN Basic Mode例程中入网参数分为两种来源硬编码在LoRaWAN_App.h中通过LORAWAN_APP_JOIN_BY_OTAA、LORAWAN_APP_DEVICE_EUI、LORAWAN_APP_JOIN_EUI、LORAWAN_APP_APP_KEY等宏定义。从 Secure Element 读取如果启用了SECURE_ELEMENT_SUPPORT协议栈会优先从 SE 芯片如 STSAFE-A1SX中读取密钥。很多开发者踩坑的地方在于在LoRaWAN_App.h中改了自己的 DevEUI / JoinEUI / AppKey但工程默认开启了 Secure Element 支持真正运行时协议栈用的是 SE 里预置的测试密钥而不是你在头文件里定义的密钥。这是 Code 14 最常见、也最容易忽略的根因之一。因为 Log 里你看到的 DevEUI 是从 SE 读取的和你配置文件中写入的完全不同但串口日志不会主动提醒你这一点。1.3 还有一种情况ChirpStack 侧的 JoinEUI 不匹配另一种高频原因是 ChirpStack 的设备配置。在 ChirpStack 中LoRaWAN 1.0.x 设备注册时有两个关键参数Device EUI (DevEUI)设备唯一标识必须与板子实际发送的 DevEUI 一致。Join EUI (AppEUI)在 LoRaWAN 1.0.x 中称为 AppEUI在 ChirpStack 的设备配置界面显示为Join EUI。如果 ChirpStack 侧填写的 JoinEUI 和板子里配置的不一样网络服务器在收到 Join Request 时会用设备配置里绑定的 AppKey 去解密和计算 MIC。由于 Join Request 的 MIC 校验在服务器端就已经失败服务器根本不会下发 Join Accept。但这里有个特例ChirpStack 的设计是即使 Join Request 的 MIC 校验失败了在某些版本或配置下它依然会发送一个 Join Accept带有无效 MIC给设备。设备收到后计算 MIC发现不匹配就会报 Code 14。所以从现象上看你确实收到了服务器的响应但内容的完整性校验不通过这往往就是两端密钥或 EUI 配置错位导致。2. 定位问题从串口日志一步步反推先来看一下典型的故障日志长什么样。在 NUCLEO-WL55JC1 上跑官方 LBM 例程正常流程中你会看到类似下面的输出###### LoRaWAN LBM Join Sample ###### ###### Waiting for user to press the Joining button ###### ###### ###### Joining... Join Accept Failed with Code 14有些版本会输出更详细的日志包含设备 EUI、Join EUI、AppKey 等调试信息。如果你的固件没有打印这些可以先通过LoRaWAN_App.h中的调试开关打开详细日志或者直接用 ST-LINK 在线调试在MacProcessJoinAccept()处打断点查看本地计算的 MIC 期望值和实际接收值的差异。2.1 快速检查清单我建议按以下顺序快速排查不要一上来就去动协议栈源码检查项方法常见问题DevEUI 是否一致对比串口打印或调试器中实际值与 ChirpStack 设备列表板子实际读取的是 SE 中预置的 EUI不是头文件里的JoinEUI 是否一致对比板子配置与 ChirpStack 设备配置大小端字节序处理错误是重灾区AppKey 是否一致对比板子配置与 ChirpStack 设备配置大小端或 Hex 格式解析错误ChirpStack 版本兼容性确认是 LoRaWAN 1.0.x 还是 1.1.x1.1 的 JoinEUI 和 AppKey 衍生机制不同网关与频率匹配确认上行 Join Request 已到达服务器频率计划和信道掩码配置错误会导致服务器收不到包其中大小端问题值得展开说明一下。2.2 大小端字节序LoRaWAN 参数配置最隐蔽的坑LoRaWAN 协议栈底层报文传输使用大端Big-Endian序也就是最高有效字节在前。但 ST 例程的LoRaWAN_App.h中DevEUI、JoinEUI、AppKey 的书写方式是按照数组下标顺序[0]到[7]排列的。举例来说如果你的 DevEUI 在 LoRaWAN 注册平台上显示为AA BB CC DD EE FF 00 11那么在 ST 的配置数组中它应该是#define LORAWAN_APP_DEVICE_EUI { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11 }注意这里数组的下标顺序是[0]AA、[1]BB依次类推。这个顺序本身和某些平台如 The Things Network上显示的十六进制字符串是一致的但在 ChirpStack 的管理界面中DevEUI 输入框通常是一个十六进制字符串且 API 层统一按大端处理。问题出在什么地方呢在于STM32WL 的射频部分从 Join Accept 报文解析字段并构建 AppNonce、DevAddr 时使用的是小端逐字节拷贝而 LoRaWAN 协议要求这些多字节字段在报文中以网络字节序传输。如果你在寄存器调试时发现MacProcessJoinAccept()里的内存值与协议文档中不一致不要慌这通常是字节序转换导致的不代表数据错误。真正容易出错的还是 AppKey。ST 例程中 AppKey 的声明是这样的#define LORAWAN_APP_APP_KEY { 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C }而你在 ChirpStack 设备配置里输入的 AppKey 是一个 32 位的十六进制字符串例如2B7E151628AED2A6ABF7158809CF4F3C这两者本质上是同一个东西不存在字节序反转的问题因为 AppKey 在协议里就是一个 16 字节的密钥没有多字节数值的语义。但如果你从某个导出的lora/credentials文件里复制了带空格、逗号、或反斜杠转义的格式粘到 ChirpStack 的输入框时没有去除格式符就会导致密钥解析错误。2.3 如何验证 AppKey 是否真的匹配一个快捷的验证方法是使用 Python 的cryptography库或在线 AES-CMAC 工具手动计算一次 Join Accept 的期望 MIC再和串口日志或 Wireshark 抓包中的实际 MIC 做对比。我个人的实践是写一个小的 Python 脚本模拟 LoRaWAN 1.0.x 的 Join Accept 处理逻辑输入 AppKey、AppNonce、NetID、DevAddr、DLSettings、RxDelay 和 CFList计算 MIC然后和服务器返回的值比较。如果本地算出来的 MIC 和服务器下发的不一致那就说明前后端密钥不匹配。这里给一个参考片段用于计算 LoRaWAN 1.0.x Join Accept 的 MICfrom cryptography.hazmat.primitives.cmac import CMAC from cryptography.hazmat.primitives.ciphers import algorithms app_key bytes.fromhex(2B7E151628AED2A6ABF7158809CF4F3C) # Join Accept 消息中的内容不含 MIC mhdr bytes([0x20]) app_nonce bytes.fromhex(010203) # 3 bytes net_id bytes.fromhex(000001) # 3 bytes dev_addr bytes.fromhex(260B1F40) # 4 bytes dl_settings bytes([0x00]) rx_delay bytes([0x01]) msg mhdr app_nonce net_id dev_addr dl_settings rx_delay cmac CMAC(algorithms.AES(app_key)) cmac.update(msg) mic cmac.finalize()[:4] print(Expected MIC:, mic.hex().upper())注意这段代码只演示 1.0.x 且没有 CFList 的最简情况实际使用中 CFList 需要加进来MIC 计算范围也会更复杂。但核心思路就是这样通过独立计算来验证密钥匹配关系是最稳妥的。3. 核心实操修改配置并绕过 Secure Element 干扰现在我们把重点放到修复上。如果你的问题确认是 Secure Element 或配置不一致导致按以下步骤操作。3.1 关闭 Secure Element强制使用软件密钥在 STM32CubeWL 的 LoRaWAN_End_Node_LBM 工程中Secure Element 的支持开关在LoRaWAN_App.h和stm32wlxx_hal_conf.h中均有体现。首先打开LoRaWAN_App.h确认以下宏#define SECURE_ELEMENT_SUPPORT LORAWAN_DISABLE如果你的工程里没有这个宏那它在stm32wlxx_hal_conf.h中#define SECURE_ELEMENT_SUPPORT LORAWAN_DISABLE将其改为LORAWAN_DISABLE并确保LORAWAN_APP_JOIN_BY_OTAA为 1。接下来检查LoRaWAN_App.c中的GetDevEui()、GetJoinEui()、GetAppKey()三个函数。当 Secure Element 关闭时这些函数会返回我们在头文件中定义的数组。如果有条件编译分支#if (SECURE_ELEMENT_SUPPORT LORAWAN_DISABLE) memcpy1(DevEui, LORAWAN_APP_DEVICE_EUI, 8); #else /* 从 Secure Element 读取 */ #endif确保你走的是LORAWAN_DISABLE这个分支。3.2 修改 LoRaWAN_App.h 中的三要素以我调试时使用的参数为例假设我在 ChirpStack 中创建的设备信息如下DevEUI:006B5F8F7A1A3B4CJoinEUI:0000000000000000AppKey:112233445566778899AABBCCDDEEFF00那么LoRaWAN_App.h中应该这样配置#define LORAWAN_APP_JOIN_BY_OTAA 1 #define LORAWAN_APP_DEVICE_EUI { 0x00, 0x6B, 0x5F, 0x8F, 0x7A, 0x1A, 0x3B, 0x4C } #define LORAWAN_APP_JOIN_EUI { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } #define LORAWAN_APP_APP_KEY { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 }注意 DevEUI 数组的顺序与十六进制字符串完全一致不需要做倒序。这一点我在很多项目中反复验证过ST 的代码在组装 Join Request 时会按数组顺序发送而 ChirpStack 展示的 DevEUI 字符串也是按这个顺序显示的。3.3 在 ChirpStack 中确认设备配置登录 ChirpStack 后台进入你的 Application - Device检查以下字段Device EUI与板子的 DevEUI 数组完全一致Join EUI与板子的 JoinEUI 数组完全一致1.0.x 模式下称为 AppEUI/JoinEUIAppKey与板子的 AppKey 数组完全一致Region / Frequency Plan与你的网关 Region 一致比如 CN470 或 EU868LoRaWAN MAC Version选择1.0.3或1.0.4不要选1.1.xRevision通常默认即可很多人在LoRaWAN MAC Version这里踩坑。如果板子上跑的是 LoRaWAN 1.0.3 协议栈但 ChirpStack 中设备配置成了 1.1.xJoin Accept 的加密和 MIC 计算方式就不一样了设备端必然报 Code 14。ST 官方 LBM 例程默认是 1.0.3 协议因此 ChirpStack 侧必须选 1.0.3。3.4 修改后重新编译下载配置修改后重新编译工程make clean make或者如果你的环境是 STM32CubeIDE直接右键工程选择Clean然后Build。下载程序时建议使用 ST-Link 的 Full Erase 选项避免旧配置残留。我在调试时还习惯把LORAWAN_APP_DEVICE_EUI、LORAWAN_APP_JOIN_EUI和LORAWAN_APP_APP_KEY通过串口打印出来方便逐一比对。3.5 重新入网验证修改完成后复位开发板按下板载的 Joining 按钮通常是蓝色用户按键观察串口输出##### Joining ##### ##### Joined successfully #####如果依旧报 Code 14不要急继续看下面的排查手段。4. 进阶排查通过 Meshtastic / Wireshark 抓包定位问题如果修改配置后问题依旧那说明问题不在配置的静态匹配层面而可能在协议栈版本、传输数据损坏、或网关频率参数上。4.1 抓取空中数据包最直接的验证方式是抓空中包。常用的工具是 SX126x 系列射频模块 开源软件Meshtastic 自带的抓包模式如果你身边有类似设备SX126x LoRa Sniffer基于 SX1261/1262 的抓包固件Wireshark LoRaTap 或者专用嗅探器如果你手头没有专用硬件也可以在 ChirpStack 网关日志中打开 Network Server 的调试输出。在 ChirpStack 的配置文件中将 Log Level 设为 Debug[log] level debug然后重启 ChirpStack观察日志中 Join Request 和 Join Accept 的原始数据。重点看Join Request received DevEUI: 006B5F8F7A1A3B4C JoinEUI: 0000000000000000确认服务器收到的 DevEUI 和你预期的一致。随后日志中应该会输出 Join Accept 相关的 MIC 计算信息ChirpStack 会记录是否成功派生出会话密钥。4.2 检查网关的 RX 窗口配置有一个现象值得注意如果网关的 RX1 Delay 和设备的 RX1 Delay 配置不一致设备可能在错误的时间窗口开启接收导致抓到的 Join Accept 报文不完整从而 MIC 计算失败。在 ChirpStack Gateway Bridge 配置中rx1_delay通常取0表示使用 Join Request 中携带的 DLSettings。ST 例程中的LORAWAN_APP_RX_DELAY默认是 1秒而 ChirpStack 的DLSettings会通过 Join Accept 告知设备 RX1 延迟。如果你在网关或网络服务器侧强制指定了 RX1 Delay 为 5 秒但设备等待的窗口只有 1 秒设备可能收到射频信号但解调不完整。解决方法是在 ST 例程中调整#define LORAWAN_APP_RX_DELAY 1保持和网络服务器配置一致。一般默认 1 是标准值不建议随意改动。4.3 协议栈版本的微妙差异ST 的 LBM 例程在不同版本的 HAL 中LoRaWAN 协议栈实现细节有差异。早期版本对 Join Accept 的 CFList 处理存在 bug在某些国家频段下如果服务器下发了 CFList设备端在解析时会把额外字节算进 MIC导致计算失败。解决方式是在 ChirpStack 中禁用 CFList或确认你的频段是否要求 CFList。升级 STM32CubeWL 的包版本到最新。STM32CubeWL 的更新可以在 STM32CubeMX 的 Package Manager 中直接安装。我建议一直保持较新的中间件版本因为 LoRaWAN 协议栈的 bug 修复通常只有在升级包中才会包含。4.4 极端情况RTC 时钟和随机数异常还有一类比较隐蔽的问题出现在使用外部 RX/TX 切换开关或调试器连接导致射频时序异常时。LoRaWAN 入网流程本身依赖稳定的 RTC 定时器来计算接收窗口的开启时间如果调试模式下 CPU 被频繁中断可能导致接收窗口的采样点偏移进而收到前后导码不完整的 Join Accept。这类问题通常只在连接调试器时出现拔掉 ST-Link用 USB 独立供电后正常入网。如果遇到这种诡异情况可以先把调试器断开短按复位键重新入网试试。5. 实操心得这个 Code 14 问题的坑全部踩过一遍之后最后分享一些个人经验按排查频率从高到低排列希望对大家有帮助。5.1 串口日志不打印 DevEUI 时如何确认板子实际在用什么参数如果你在代码里改了 DevEUI但串口打印的依旧是老的一串那基本可以断定 Secure Element 在起作用。ST 的 NUCLEO-WL55JC1 板载了 STSAFE-A1SX 安全芯片出厂预置了测试密钥。快速验证方法在main.c里在LoRaWAN_Init()之后马上调用GetDevEui()然后将它通过串口打印出来。如果打印值和你在LoRaWAN_App.h中设置的不一致就说明代码走的是 Secure Element 分支。此时最省事的做法就是把SECURE_ELEMENT_SUPPORT设为LORAWAN_DISABLE强制走软件密钥分支。毕竟对于普通项目STSAFE-A1SX 的安全启动和密钥注入流程相当繁琐用软件密钥是完全够用的。5.2 记得看一下 ChirpStack 的 Device Activation 状态当设备成功入网后ChirpStack 的设备详情页会从Never更新为最近激活时间并且会出现Device Keys的派生信息。如果你看到 Activation 状态一直不更新说明 Join Accept 没有成功回到设备侧问题可能在射频链路或密钥不匹配。5.3 测试时建议把 Region 和 Frequency 固定避免漫游干扰在实验室环境下我建议在 ChirpStack 中把设备分配一个固定的 Region 和频段不要使用自动漫游。同时 ST 例程中也要设置固定的ACTIVE_REGION。在LoRaWAN_App.h中#define ACTIVE_REGION LORAMAC_REGION_EU868这样可以在调试阶段排除区域选择相关的频点漂移问题。如果你使用的是中国频段需要设置LORAMAC_REGION_CN470同时要注意 CN470 是 频分双工模式频点和信道的上下行映射规则与 EU868 有所不同。5.4 最容易忽略的一个小参数TX Power 和 AntennaNUCLEO-WL55JC1 板载天线是 PCB 天线匹配网络已经做好一般不需要额外调整。但如果你使用外部天线注意不要使用大增益天线长时间近距离测试否则可能造成接收饱和反而导致 Join Accept 收不到或解调出错。我早期踩过一次这样的坑近距离用高增益天线测试时完全无法入网换回板载天线后一次就成功。原因是接收饱和导致前导码检测失败表现为 Join Request 发出后没有任何响应连带出现超时错误。6. 额外补充一次典型的完整调试过程复盘最后用一个完整案例来串联上面的排查流程也是我自己实际调通过的一个过程帮助大家建立整体排查感。设备环境板子NUCLEO-WL55JC1例程LoRaWAN_End_Node_LBM网关单通道 SX1268 LoRaWAN 网关EU868 频率服务器ChirpStack v4.x现象按下 Joining 键后串口输出Join Accept Failed with Code 14第一步我先打开LoRaWAN_App.h确认SECURE_ELEMENT_SUPPORT是LORAWAN_DISABLE结果发现是LORAWAN_ENABLE此时板子用的是 ST 预设的测试密钥。我改成LORAWAN_DISABLE重新编译上传。第二步重新入网依旧报 Code 14。这时候我把串口打印的 DevEUI、JoinEUI、AppKey 逐一记录下来去 ChirpStack 设备配置里对比发现 ChirpStack 中设备的 LoRaWAN MAC Version 设置成了1.1.0而板子是 1.0.3。将 MAC Version 改为1.0.3后重新尝试。第三步依旧失败。打开 ChirpStack Debug 日志发现服务器在收到 Join Request 后没有输出 MIC 错误说明服务器已经成功解密了 Join Request并下发了 Join Accept。问题确实在设备侧验证失败。第四步用 SX126x Sniffer 抓包抓到 Join Accept 的原始数据用 Python 脚本重新计算 MIC发现本地计算值和抓到的 MIC 不一致。进一步发现服务器端配置的设备 AppKey 实际输入时多了两个空格导致密钥解析错误。修正空格后再次入网成功。整个排查过程耗时约半天最大的教训是Code 14 不一定是射频或信号问题绝大多数情况是密钥或协议版本配置不匹配。先把软件配置核对准确再考虑空中数据问题。如果按照上面的流程走完仍然无法解决还有一个后备思路直接在协议栈中加入打印在LoRaMacCrypto.c的LoRaMacComputeJoinAcceptMic()函数中打印计算 MIC 时使用的 AppKey 和消息内容。这样就能在源码层面看到底是哪个阶段出现了偏差。这个方法虽然笨但对于协议栈二次开发阶段来说往往是最有效的兜底手段。我在实际开发中还有一个小习惯就是在调试版本中把 AppKey 打印出来但发布版本一定要关闭打印避免安全信息泄露。这一点提醒大家务必注意LoRaWAN 的 AppKey 一旦泄露等于设备网络密钥全部暴露。