4、MAC层帧结构:上行/下行帧格式、帧头字段详解、MIC与CRC校验

聊到LoRaWAN的MAC层帧结构,我脑子里第一个蹦出来的词就是「精打细算」。你想想看,一个终端设备可能靠两节电池撑好几年,每一比特的空中传输时间都得省着用。所以LoRaWAN的帧结构设计得非常紧凑,没有半点冗余。

我个人习惯把MAC帧比作一个「信封」——信封外面写着收件地址(设备地址),信封里面装着你要寄的信(数据载荷),信封口还贴了防伪标签(MIC和CRC)。嗯,咱们今天就一层层拆开这个信封看看。

4.1 上行帧与下行帧的通用格式

不管是上行还是下行,LoRaWAN的MAC帧都遵循同一个基本骨架。我直接上结构图:

+----------------+----------------+----------------+----------------+----------------+
|  MAC Header    |  Frame Header  |  Frame Port    |  Frame Payload |  MIC           |
|  (MHDR) 1字节  |  (FHDR) 可变   |  (FPort) 1字节 |  (FRMPayload)  |  4字节         |
+----------------+----------------+----------------+----------------+----------------+

这个结构看起来简单,但每个字段都有讲究。咱们从上往下捋一遍。

4.1.1 MHDR(MAC Header)—— 帧的身份证

MHDR只有1个字节,但它决定了整个帧的类型和版本。它的结构是这样的:

Bit 7-5: MType(消息类型)
Bit 4-2: RFU(保留位,必须为000)
Bit 1-0: Major(LoRaWAN主版本号)

MType有8种取值,但实际常用的就几种。我列个表:

MType值 名称 说明
000 Join-Request 入网请求(上行)
001 Join-Accept 入网接受(下行)
010 Unconfirmed Data Up 非确认上行数据
011 Unconfirmed Data Down 非确认下行数据
100 Confirmed Data Up 确认上行数据
101 Confirmed Data Down 确认下行数据
110 RFU 保留
111 Proprietary 私有用途

Major字段目前只定义了00(LoRaWAN R1),其他值都是保留的。我在项目中见过有人把RFU位填了非零值,结果网关直接丢弃了这些帧——嗯,这个坑我踩过。

4.1.2 FHDR(Frame Header)—— 帧头的核心

FHDR是可变长度的,最短7个字节,最长可以到...嗯,看你怎么用。它的结构如下:

+----------------+----------------+----------------+----------------+
|  DevAddr       |  FCtrl         |  FCnt          |  FOpts         |
|  4字节         |  1字节         |  2字节         |  0-15字节      |
+----------------+----------------+----------------+----------------+

DevAddr(设备地址):4字节,32位。这是终端在网络中的唯一标识。注意,它不是DevEUI(全球唯一),而是网络分配的短地址。同一个设备在不同网络里DevAddr可能不同。

FCtrl(帧控制字节):这个1字节的字段,上下行的含义不一样。我分别说:

上行FCtrl:

Bit 7:   ADR(自适应数据速率)
Bit 6:   ADRACKReq(ADR确认请求)
Bit 5:   ACK(确认位)
Bit 4-1: RFU(保留)
Bit 0:   FOptsLen(FOpts字段长度)

下行FCtrl:

Bit 7:   ADR
Bit 6:   RFU
Bit 5:   ACK
Bit 4:   FPending(服务器还有数据待发)
Bit 3-1: RFU
Bit 0:   FOptsLen

这里有个细节我特别想强调:FPending位只在下行帧中出现。当服务器设置了这个位,终端就知道「嘿,还有数据等着你,别急着睡」。我在做智能水表项目时,就靠这个位实现了批量数据下发,效果很好。

FCnt(帧计数器):2字节,从0开始递增。每次上行或下行都会加1。服务器和终端各自维护两个计数器:FCntUp和FCntDown。为什么要有计数器?说白了就是为了防重放攻击。如果收到一个FCnt比之前还小的帧,直接丢弃。

FOpts(帧选项):0到15字节。这里可以携带MAC命令,比如链路检查、信道状态等。注意,如果FOptsLen=0,那FOpts字段就不存在。如果FOptsLen>0,那FPort字段就不能为0——这是协议规定的,别问我为什么,问就是设计如此。

4.1.3 FPort(帧端口)

1字节,取值范围0~255。它的作用类似于TCP/UDP的端口号:

  • FPort=0:表示FRMPayload里只包含MAC命令,没有应用数据
  • FPort=1~223:应用数据端口,由应用层自行定义
  • FPort=224~255:保留给未来标准化用途

我记得有个项目,开发人员把传感器数据放在FPort=0的帧里,结果网关直接不认——因为协议规定FPort=0只能传MAC命令。这个坑,我帮他们排查了整整一天。

4.1.4 FRMPayload(帧载荷)

这是真正的数据部分。长度可变,但受限于每个地区的最大载荷长度(比如EU868是51字节,US915是11字节)。如果启用了加密,FRMPayload是经过AES-128加密的密文。加密的细节咱们后面章节再聊。

4.2 MIC(消息完整性校验)

MIC是4字节的校验值,放在帧的最后。它的作用有两个:

  • 完整性校验:确保帧在传输过程中没有被篡改
  • 源认证:确认帧确实来自合法的终端或网络服务器

MIC的计算涉及整个MAC帧(包括MHDR、FHDR、FPort、FRMPayload),再加上一些辅助信息(比如方向位、设备地址等)。算法用的是AES-128-CMAC。我直接给个伪代码:

// 计算上行帧的MIC
input:  NwkSKey (网络会话密钥), msg (整个MAC帧, 不含MIC)
output: mic (4字节)

// 1. 构建B0块
B0 = 0x49 || 0x00 || 0x00 || 0x00 || 0x00
     || Dir (0x00 for uplink) || DevAddr || FCntUp || 0x00 || len(msg)

// 2. 计算CMAC
cmac = AES128_CMAC(NwkSKey, B0 || msg)

// 3. 取前4字节
mic = cmac[0:4]

这里Dir字段很关键:上行是0x00,下行是0x01。如果搞反了,MIC校验一定失败。我在调试时经常用这个来排查是终端还是服务器的问题——如果上行MIC校验失败,多半是终端侧的NwkSKey不对;如果下行MIC校验失败,那问题就在服务器侧。

⚠️ 注意:MIC计算时,msg部分不包含MIC自身。也就是说,发送方先计算MIC,再把MIC追加到帧尾。接收方收到后,先去掉最后4字节,用同样的算法重新计算MIC,然后比对。如果不一致,直接丢弃。

4.3 CRC(循环冗余校验)

CRC和MIC的区别,我打个比方你就明白了:

  • MIC:防篡改、防伪造,需要密钥
  • CRC:防传输错误,不需要密钥

在LoRaWAN中,CRC是在物理层(PHY层)完成的,不是MAC层。但既然咱们聊帧结构,我还是提一嘴。

上行帧在发送时,物理层会在MAC帧后面加上2字节的CRC。网关收到后先校验CRC,如果CRC不对,直接丢弃,不会上报给网络服务器。下行帧呢?嗯,下行帧没有CRC——因为LoRaWAN认为下行信道相对可靠,而且有MIC兜底。

我曾经在实验室做过测试:把上行帧的CRC故意改错,网关确实不认。但把MIC改错而CRC正确,网关会转发给服务器,服务器在MIC校验时才会丢弃。所以,CRC是物理层的「第一道防线」,MIC是MAC层的「最后一道防线」

4.4 上下行帧的差异总结

我把上下行帧的关键差异整理成一张表,方便你对照:

特性 上行帧 下行帧
FCtrl中的FPending 不存在 存在,表示还有数据待发
CRC 有(2字节,PHY层)
FCnt FCntUp FCntDown
MIC计算方向 Dir=0x00 Dir=0x01
接收窗口 无(终端主动发送) RX1/RX2窗口(终端监听)
💡 个人经验:在调试LoRaWAN通信时,我建议你先抓一下空中的原始数据包。用Wireshark配合LoRaSniffer,可以清晰看到MHDR、DevAddr、FCnt、MIC这些字段。有一次我排查一个终端频繁掉线的问题,就是通过抓包发现FCnt出现了跳变——原来是终端重启后FCnt没有持久化保存,从0重新开始了。服务器端做了FCnt防回滚检查,直接丢弃了这些帧。

好了,MAC层帧结构就聊到这儿。下一节咱们会深入MAC命令,看看那些藏在FOpts里的「小纸条」到底在传递什么信息。