4. MAC层协议与固件设计:CSMA/CA机制、时隙与帧结构设计、ACK与重传机制、低功耗监听(LPL/PSM)实现
各位同学,今天我们来聊聊MAC层。说实话,很多做无线SoC的工程师,硬件功底都不错,但一碰到MAC层协议就头疼。我当年刚入行时也是这样,总觉得MAC层不就是个调度器嘛,有什么难的?直到第一次流片回来,发现两个节点死活连不上,才意识到——MAC层才是无线通信的灵魂。
MAC层协议,说白了就是解决「谁先说话」的问题。你想想看,无线信道是共享的,大家都在同一个频率上喊话,没人管的话就乱套了。CSMA/CA机制就是干这个的。
4.1 CSMA/CA机制:先听再说,不行就等
CSMA/CA的全称是载波侦听多路访问/冲突避免。我习惯把它理解成「文明人的排队规则」——说话前先听听有没有人在说,没人说你再开口。
具体怎么做?分三步:
- CCA(空闲信道评估):节点在发送前,先检测信道是否空闲。一般检测8个符号时间,大约128μs。
- 随机退避:如果信道忙,就随机等一段时间再试。这个随机数很重要,否则两个节点同时等、同时发,永远撞车。
- 帧间间隔(IFS):不同优先级的帧,等待时间不一样。比如ACK帧的等待时间最短,数据帧稍长。
避坑指南:我曾经在一个项目中,CCA阈值设得太低,导致节点把噪声当成有效信号,一直不敢发数据。后来发现,阈值要根据实际环境噪声底噪来调,不能照搬参考设计。
这里我画了一张CSMA/CA的流程图,方便大家理解整个决策过程:
4.2 时隙与帧结构设计:把时间切成片
时隙(Slot)这个概念,我刚开始学的时候总觉得抽象。后来做了一款TSCH(时隙跳频)芯片,才真正理解——时隙就是把时间轴切成等长的小块,每个节点在自己的时隙里说话,互不干扰。
帧结构怎么设计?我给大家看一个典型的IEEE 802.15.4帧格式:
| 前导码 | SFD | PHR | PSDU(MAC帧) | |||
|---|---|---|---|---|---|---|
| 4字节 | 1字节 | 1字节 | 帧控制 | 序列号 | 地址域 | 载荷 |
| 2字节 | 1字节 | 4-20字节 | 可变 | |||
嗯,这里要注意:前导码和SFD是物理层的事,MAC层不关心。但PHR后面的PSDU,就是咱们MAC层要操心的了。
个人经验:我建议在帧控制字段里留几个保留位,用于扩展。有一次客户要求加一个「紧急数据」标志,幸好我留了保留位,改固件就搞定了。要是没留,就得改硬件,那代价就大了。
4.3 ACK与重传机制:丢了就补,但别死磕
ACK机制,说白了就是「你发我回,没回重发」。但这里有个坑——什么时候重发?重发几次?
我见过一个产品,重传次数设了10次,结果信道一忙,节点就死命重传,功耗飙升,还把其他节点都堵死了。后来我建议改成3次,配合自适应退避,效果反而更好。
重传策略一般分两种:
- 立即重传:没收到ACK,马上重发。适合低延迟场景。
- 延迟重传:等一个随机时间再重发。适合避免拥塞。
代码实现上,我习惯用状态机来管理:
// 重传状态机示例
typedef enum {
TX_IDLE,
TX_WAIT_CCA,
TX_SENDING,
TX_WAIT_ACK,
TX_RETRY
} tx_state_t;
void mac_tx_state_machine(void) {
switch (g_tx_state) {
case TX_IDLE:
if (tx_buffer_ready) {
g_retry_count = 0;
g_tx_state = TX_WAIT_CCA;
}
break;
case TX_WAIT_CCA:
if (cca_result == CHANNEL_IDLE) {
start_tx();
g_tx_state = TX_SENDING;
} else {
// 退避后重试
start_backoff();
}
break;
case TX_SENDING:
if (tx_done) {
start_ack_timer(ACK_TIMEOUT);
g_tx_state = TX_WAIT_ACK;
}
break;
case TX_WAIT_ACK:
if (ack_received) {
g_tx_state = TX_IDLE;
} else if (ack_timeout) {
if (++g_retry_count < MAX_RETRY) {
g_tx_state = TX_RETRY;
} else {
// 彻底失败,上报上层
tx_fail_callback();
g_tx_state = TX_IDLE;
}
}
break;
case TX_RETRY:
start_backoff();
g_tx_state = TX_WAIT_CCA;
break;
}
}
警告:ACK超时时间不能设得太短。我曾经设了200μs,结果在干扰环境下经常误判丢包,导致大量无效重传。建议至少留500μs的余量,考虑射频开关切换时间和接收机启动时间。
4.4 低功耗监听(LPL/PSM):让节点学会「打盹」
低功耗监听,这是物联网芯片的命门。你想想看,一个电池供电的传感器节点,如果一直开着接收机,几天就没电了。所以必须让节点「打盹」——大部分时间睡觉,定时醒来听一听有没有人叫它。
两种主流方案:
- LPL(低功耗监听):节点周期性醒来,检测信道是否有前导码。如果有,就继续接收;没有,继续睡。
- PSM(节能模式):节点和协调器约定好唤醒时间,到点了一起醒来通信。
我个人更倾向于LPL,因为它不需要严格的时间同步,实现起来简单。但LPL有个问题——前导码长度要够长,否则节点醒来时前导码已经发完了。
前导码长度怎么算?我给大家一个公式:
前导码长度 > 节点休眠周期 × 2
举个例子:
节点每500ms醒来一次,前导码至少要发1000ms。
这样无论节点什么时候醒来,都能抓到前导码的尾巴。
避坑指南:我曾经在一个项目中,前导码长度设得刚好等于休眠周期,结果测试时发现总有1%的包丢失。后来一查,是节点醒来时正好在前导码的边界上,差了几个微秒没抓到。改成2倍后,丢包率降到0.01%以下。
PSM的实现稍微复杂一些,需要维护一个时隙表:
| 节点ID | 唤醒时隙 | 通信窗口 | 状态 |
|---|---|---|---|
| 0x01 | 时隙5 | 10ms | 活跃 |
| 0x02 | 时隙12 | 10ms | 休眠 |
| 0x03 | 时隙20 | 10ms | 活跃 |
实际项目中,我通常把LPL和PSM结合起来用。比如,节点大部分时间用LPL,每隔一段时间同步一次时隙,切换到PSM模式进行批量数据传输。这样既省电,又能保证吞吐量。
好了,MAC层协议这块内容不少,但核心就三点:怎么避免冲突、怎么确认送达、怎么省电。把这三点吃透了,大部分无线SoC的MAC层设计你都能搞定。