4. MAC层协议与固件设计:CSMA/CA机制、时隙与帧结构设计、ACK与重传机制、低功耗监听(LPL/PSM)实现

各位同学,今天我们来聊聊MAC层。说实话,很多做无线SoC的工程师,硬件功底都不错,但一碰到MAC层协议就头疼。我当年刚入行时也是这样,总觉得MAC层不就是个调度器嘛,有什么难的?直到第一次流片回来,发现两个节点死活连不上,才意识到——MAC层才是无线通信的灵魂。

MAC层协议,说白了就是解决「谁先说话」的问题。你想想看,无线信道是共享的,大家都在同一个频率上喊话,没人管的话就乱套了。CSMA/CA机制就是干这个的。

4.1 CSMA/CA机制:先听再说,不行就等

CSMA/CA的全称是载波侦听多路访问/冲突避免。我习惯把它理解成「文明人的排队规则」——说话前先听听有没有人在说,没人说你再开口。

具体怎么做?分三步:

  1. CCA(空闲信道评估):节点在发送前,先检测信道是否空闲。一般检测8个符号时间,大约128μs。
  2. 随机退避:如果信道忙,就随机等一段时间再试。这个随机数很重要,否则两个节点同时等、同时发,永远撞车。
  3. 帧间间隔(IFS):不同优先级的帧,等待时间不一样。比如ACK帧的等待时间最短,数据帧稍长。

避坑指南:我曾经在一个项目中,CCA阈值设得太低,导致节点把噪声当成有效信号,一直不敢发数据。后来发现,阈值要根据实际环境噪声底噪来调,不能照搬参考设计。

这里我画了一张CSMA/CA的流程图,方便大家理解整个决策过程:

有数据要发送 执行CCA:检测信道是否空闲 信道空闲? 等待IFS帧间间隔 发送数据帧 执行随机退避 退避时间到 等待ACK确认 收到ACK? 发送成功 重传

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层设计你都能搞定。


专注资料整理