4. LoRaWAN MAC层帧结构与消息类型

LoRaWAN 的物理层只负责把比特可靠地送出去,真正决定“谁能发、什么时候发、发完要不要回执、网络怎么下发参数”的,是 MAC 层。理解 MAC 帧结构,是后面计算网络开销、评估容量的前提:每一字节的 MAC 头、MIC、FOpts 都会占用空中时间,而空中时间是 LoRaWAN 最稀缺的资源。

一、PHYPayload 的整体结构

LoRaWAN 上行和下行报文在 MAC 层统称 PHYPayload,由三部分组成:MHDR(1 字节)、MACPayload(可变长)、MIC(4 字节)。MHDR 的高 3 位是 MType,低 3 位是 Major 版本号(当前为 0),中间保留。MType 直接决定了这条消息是入网请求、数据上行、确认下行还是 MAC 命令,是解析一切帧的入口。

MACPayload 内部又分三段:FHDR(帧头)、FPort(端口,可省略)、FRMPayload(应用载荷)。FHDR 包含 4 字节 DevAddr、1 字节 FCtrl、2 字节 FCnt 以及 0~15 字节的 FOpts。FCtrl 是控制核心:ADR 位表示是否启用自适应速率,ADRACKReq 用于请求网络下发下行确认,ACK 位表示本帧是对上一条确认帧的应答,FPending 位(仅下行)表示网络还有数据待发,最后 4 位 FOptsLen 指明 FOpts 长度。

这里有一个必须记住的约束:FOpts 与 FRMPayload 不能同时非空。若 FOptsLen>0,则 FPort 必须为 0,FRMPayload 为空;若 FPort>0,则 FOptsLen 必须为 0。原因是二者共用同一套加密与完整性保护机制,混用会导致密钥和计数器语义冲突。这条规则直接影响 MAC 命令的发送方式——要么搭在数据帧的 FOpts 里“顺路”下发,要么单独发一条 FPort=0 的 MAC 命令帧。

LoRaWAN PHYPayload 帧结构与字段长度 MHDR 1 B MACPayload(可变长) FHDR + FPort + FRMPayload MIC 4 B MACPayload 展开 FHDR(帧头) DevAddr(4) + FCtrl(1) + FCnt(2) + FOpts(0~15) FPort 0 或 1 B FRMPayload 0 ~ N B(受 DR 限制) FCtrl 位域(1 字节) ADR ADRACKReq ACK FPending FOptsLen(4b) 互斥约束 FOptsLen > 0 → FPort = 0 FPort > 0 → FOptsLen = 0 MType 取值与消息类型 000 Join-Request 001 Join-Accept 010 Unconfirmed Up 011 Unconfirmed Down 100 Confirmed Up 101 Confirmed Down 110 RFU 111 Proprietary

二、上行与下行帧的差异

上行帧(Unconfirmed/Confirmed Uplink)的 FHDR 中,FCtrl 的 FPending 位无意义,ADRACKReq 由终端置位以“催”网络回一条下行;FCnt 是终端自己的上行帧计数器,32 位,但空中只传低 16 位,高 16 位由两端各自维护,靠 MIC 校验来发现失步。下行帧(Unconfirmed/Confirmed Downlink)的 FCtrl 中 FPending 有效,表示网络还有后续下行排队;FCnt 是网络侧的下行计数器,与上行计数器相互独立。

帧计数器是安全与容量的双重关键。每次发送 FCnt 递增,接收端拒绝重复计数,从而防重放。但 32 位计数器在高速率下会耗尽:若终端以 SF7、每分钟 1 帧持续发送,约 2^32 分钟(约 8000 年)才回绕,实际可忽略;真正的问题是 16 位空中字段在丢帧后容易失步,需要靠 MIC 校验和重同步机制恢复。

三、MAC 命令与 FOpts 的取舍

MAC 命令(MACCommand)用于网络与终端之间的控制交互,常见的有 LinkCheckReq/Ans、LinkADRReq/Ans、DutyCycleReq/Ans、RXParamSetupReq/Ans、DevStatusReq/Ans、NewChannelReq/Ans、RXTimingSetupReq/Ans、TxParamSetupReq/Ans、DlChannelReq/Ans 等。每条命令由 1 字节 CID 加若干参数字节组成。

MAC 命令有两种承载方式:搭在数据帧的 FOpts 中,或单独作为 FPort=0 的帧发送。前者不额外消耗一次发送机会,但 FOpts 最多 15 字节,且会挤占应用载荷空间;后者灵活但增加一次完整的空中传输。工程上的经验是:把 LinkADRReq、RXParamSetupReq 这类“顺路”命令尽量塞进 FOpts,把需要长参数的 NewChannelReq 单独发。

下面给出一个 FOpts 承载 MAC 命令的示例,展示 LinkADRReq 的 4 字节参数如何拼装:

// LinkADRReq 载荷(CID=0x03,共 4 字节参数)
// DataRate_TXPower: 高4位=DR,低4位=TXPower
// ChMask: 16位信道掩码
// Redundancy: 高4位=ChMaskCntl,低4位=NbTrans
uint8_t linkAdrReq[5];
linkAdrReq[0] = 0x03;              // CID
linkAdrReq[1] = (dr << 4) | txp;   // 例: DR=5, TXP=0 -> 0x50
linkAdrReq[2] = chMask & 0xFF;     // ChMask 低字节
linkAdrReq[3] = (chMask >> 8) & 0xFF;
linkAdrReq[4] = (chMaskCntl << 4) | nbTrans; // 例: 0x01

// 放入上行帧 FOpts,FCtrl.FOptsLen = 5
// 此时 FPort 必须为 0,FRMPayload 为空

四、确认帧与入网流程

Confirmed 帧要求接收方回一条 ACK=1 的帧。注意:ACK 不是独立消息类型,而是搭在反向帧的 FCtrl.ACK 位上。终端发 Confirmed Uplink 后,网络在下一个下行窗口回一条 ACK=1 的下行;若终端在 RX1/RX2 都没收到,会按 NbTrans 重传。重传次数直接放大网络开销,因此 Confirmed 帧应谨慎使用,通常只用于关键配置或告警。

入网流程分 OTAA 与 ABP。OTAA 的 Join-Request 由 JoinEUI(8B)+DevEUI(8B)+DevNonce(2B) 组成,用 AppKey 做 MIC;Join-Accept 由 AppNonce(3B)+NetID(3B)+DevAddr(4B)+DLSettings(1B)+RxDelay(1B)+可选 CFList 组成,用 AppKey 加密。一次 OTAA 至少消耗 1 次上行 + 1 次下行,且 Join-Request 使用固定 DR,是网络启动阶段的主要开销来源。

容量估算提示:一条 16 字节应用载荷的上行帧,加上 MHDR(1)+FHDR(7)+FPort(1)+MIC(4)=13 字节 MAC 开销,实际 PHYPayload 约 29 字节。在 SF7/125kHz 下空中时间约 60~70 ms,而在 SF12 下可达 1.5 s 以上。MAC 开销占比虽小,但速率档位对容量的影响是数量级的。

五、对网络开销与容量的影响

把上述机制折算成容量账:假设某网关在 SF7 下每小时可容纳约 5 万条 29 字节上行(仅为示例量级,实际取决于占空比与并发),若其中 10% 使用 Confirmed 帧并平均重传 2 次,等效上行量增加约 10%;若大量终端在入网阶段集中发 Join-Request,则短时间内会挤占数据信道。因此调优的三个抓手是:控制 Confirmed 帧比例、把 MAC 命令尽量并入 FOpts、合理设置 NbTrans 与 ADR。ADR 让靠近网关的终端升到高速率,是提升整网容量最有效的手段,而 ADRACKReq 与 LinkADRReq 正是通过 FCtrl 和 MAC 命令协同完成的。

常见误区:把 FPort 当作“消息类型”使用。FPort 只标识应用层端口,0 专用于 MAC 命令,1~223 为应用自定义。若在 FPort=0 的帧里塞应用数据,会被网络当作 MAC 命令解析而丢弃。