4、Class A状态机设计:上行发送窗口、接收窗口1、接收窗口2、休眠周期、状态转移条件

好,咱们今天来啃一块硬骨头——Class A的状态机设计。

说实话,Class A是所有LoRaWAN终端设备的基础模型。你想想看,市面上90%以上的电池供电设备,用的都是这个模式。我当年刚入行时,第一个量产的项目就是Class A的温湿度传感器,那会儿踩了不少坑,今天一并分享给你们。

4.1 状态机总览:五个核心状态

Class A的状态机,说白了就是五个状态的循环。我习惯把它们画成一个圆环:

  • 休眠状态(Sleep):设备最省电的阶段
  • 上行发送窗口(TX):设备主动发起数据上传
  • 接收窗口1(RX1):发送结束后的第一个监听窗口
  • 接收窗口2(RX2):第一个窗口没收到,再开第二个
  • 回到休眠:两个窗口都结束,继续睡

嗯,这里要注意:Class A的接收窗口,永远是由上行发送触发的。服务器不能主动找设备,只能等设备先说话。这是省电的核心逻辑。

4.2 上行发送窗口(TX)

设备上电后,或者定时器到期,就会进入TX状态。这个阶段做什么?

  • 组装MAC层数据包(包括FCnt、MIC校验等)
  • 选择发送通道和频率
  • 设置发射功率
  • 发送数据帧

我建议你在设计时,给TX窗口加一个超时保护。为什么?

我曾经遇到过一个情况:射频模块偶尔会卡死在发送状态,电流一直维持在120mA。如果不做超时处理,电池半天就耗光了。后来我在代码里加了个看门狗,发送超过2秒就强制复位。

关键参数:

  • 发送持续时间:通常几十毫秒到几百毫秒
  • 发送完成后,立即启动接收窗口定时器
  • 发送期间电流:典型值80-150mA(取决于功率)

4.3 接收窗口1(RX1)

发送完成后,设备会等待RECEIVE_DELAY1秒,然后打开RX1窗口。这个延迟默认是1秒。

RX1的接收频率和发送频率是关联的。具体来说:

  • RX1的频率 = 发送频率对应的下行频率(有固定的映射关系)
  • RX1的数据速率 = 发送数据速率 - 某个偏移量(由RX1DROffset决定)

举个例子:你在868.1MHz用SF12发送,RX1就会在对应的下行频率上用SF11或SF10监听。这个映射关系在LoRaWAN规范的表里写得清清楚楚。

我个人习惯在RX1窗口打开前,先把接收缓冲区清空,避免收到上一包残留数据。嗯,这个细节很多人会忽略。

避坑指南:

我曾经在RX1窗口只开了20微秒就关闭了,结果服务器明明发了数据,设备就是收不到。后来发现是窗口打开时间太短,射频模块还没来得及锁定频率。建议RX1窗口至少开50微秒以上。

4.4 接收窗口2(RX2)

如果RX1没收到数据,设备会等待RECEIVE_DELAY2秒,然后打开RX2窗口。默认延迟是2秒(从发送结束算起)。

RX2和RX1最大的区别是什么?

  • RX2的频率是固定的(默认868.1MHz,但可以配置)
  • RX2的数据速率也是固定的(默认SF12,但可以配置)
  • RX2的窗口长度通常比RX1长一些

你想想看,为什么RX2要用固定参数?

说白了,这是给服务器一个「兜底」的机会。如果RX1因为频率冲突或干扰没收到,服务器可以在RX2这个「安全通道」上再试一次。我见过有些网关配置,RX2故意用更低的速率,提高接收灵敏度。

参数 RX1 RX2
延迟时间 RECEIVE_DELAY1(默认1s) RECEIVE_DELAY2(默认2s)
频率 与发送频率关联 固定频率(默认868.1MHz)
数据速率 发送速率 - 偏移量 固定速率(默认SF12)
窗口长度 通常较短 通常较长

4.5 休眠周期

两个接收窗口都结束后,设备进入休眠状态。这是Class A的精髓——大部分时间都在睡觉

休眠期间:

  • MCU进入低功耗模式(比如STM32的Stop模式)
  • 射频模块完全断电
  • 只保留RTC定时器用于唤醒

我测过一些模块,休眠电流可以做到1.5微安以下。但要注意,有些廉价的LDO在休眠时反而会漏电,我遇到过休眠电流高达20微安的情况,排查了好久才发现是电源芯片的问题。

注意:

休眠周期不是无限长的。设备需要定期醒来发送数据,否则服务器会认为设备离线。典型的周期是:

  • 温湿度传感器:5-15分钟
  • 智能水表:1-6小时
  • 资产追踪器:30秒-2分钟

4.6 状态转移条件

状态机怎么跳转?我画个简单的逻辑:

// 伪代码示例
void classA_state_machine(void) {
    switch(current_state) {
        case SLEEP:
            if (send_timer_expired) {
                // 定时器到期,准备发送
                prepare_tx_packet();
                current_state = TX;
            }
            break;
            
        case TX:
            if (tx_complete) {
                // 发送完成,启动RX1定时器
                start_rx1_timer(RECEIVE_DELAY1);
                current_state = RX1;
            }
            break;
            
        case RX1:
            if (rx1_timer_expired) {
                // RX1窗口打开
                open_rx1_window();
            }
            if (data_received) {
                process_downlink();
                current_state = SLEEP;
            } else if (rx1_window_closed) {
                // 没收到数据,准备RX2
                start_rx2_timer(RECEIVE_DELAY2 - RECEIVE_DELAY1);
                current_state = RX2;
            }
            break;
            
        case RX2:
            if (rx2_timer_expired) {
                open_rx2_window();
            }
            if (data_received) {
                process_downlink();
            }
            // 不管收没收到,都去睡觉
            current_state = SLEEP;
            break;
    }
}

这里有个细节:RX1和RX2的定时器是连续启动的。RX1结束后,如果没收到数据,立即启动RX2的定时器,而不是等RX1完全关闭再算。这样可以精确控制窗口时序。

4.7 实际项目中的经验总结

最后,我分享几个实战中容易踩的坑:

  1. 时钟漂移问题:休眠时用内部RC振荡器,醒来后切换到外部晶振。如果切换不及时,接收窗口的时序会偏移,导致收不到数据。我建议在发送前就提前切换时钟。
  2. 接收窗口重叠:如果RECEIVE_DELAY1设置得太短,RX1还没关,RX2就开了。两个窗口同时打开,射频模块会混乱。确保RX1关闭后再开RX2。
  3. MIC校验失败:收到下行数据后,一定要校验MIC。我遇到过网关发来的数据包被篡改,设备直接执行了错误指令。MIC校验不过,直接丢弃。
  4. 休眠唤醒后的状态恢复:有些MCU在休眠唤醒后,外设寄存器会丢失配置。我习惯在唤醒后重新初始化射频模块,确保状态机干净。

嗯,Class A的状态机设计,说白了就是「发-等-睡」三个动作的循环。但要把这三个动作做对、做稳、做省电,里面的门道还真不少。你们在实际开发时,建议先用逻辑分析仪抓一下时序,看看窗口打开的时间点对不对。

下一章,咱们聊聊Class B的时隙同步,那个更有意思。