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 实际项目中的经验总结
最后,我分享几个实战中容易踩的坑:
- 时钟漂移问题:休眠时用内部RC振荡器,醒来后切换到外部晶振。如果切换不及时,接收窗口的时序会偏移,导致收不到数据。我建议在发送前就提前切换时钟。
- 接收窗口重叠:如果RECEIVE_DELAY1设置得太短,RX1还没关,RX2就开了。两个窗口同时打开,射频模块会混乱。确保RX1关闭后再开RX2。
- MIC校验失败:收到下行数据后,一定要校验MIC。我遇到过网关发来的数据包被篡改,设备直接执行了错误指令。MIC校验不过,直接丢弃。
- 休眠唤醒后的状态恢复:有些MCU在休眠唤醒后,外设寄存器会丢失配置。我习惯在唤醒后重新初始化射频模块,确保状态机干净。
嗯,Class A的状态机设计,说白了就是「发-等-睡」三个动作的循环。但要把这三个动作做对、做稳、做省电,里面的门道还真不少。你们在实际开发时,建议先用逻辑分析仪抓一下时序,看看窗口打开的时间点对不对。
下一章,咱们聊聊Class B的时隙同步,那个更有意思。