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校验失败,那问题就在服务器侧。
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窗口(终端监听) |
好了,MAC层帧结构就聊到这儿。下一节咱们会深入MAC命令,看看那些藏在FOpts里的「小纸条」到底在传递什么信息。