本节摘要:LoRaWAN 的接收窗口毫秒级开关、发射时序与超时重传,决定了协议栈必须是"事件驱动 + 精准定时"的架构。本节给出典型任务划分、时间预算表与两种调度模型(裸机前后台 vs RTOS)的取舍。位置:4.3 结了安全账,本章把前面所有账本翻译成软件架构。
| 事件 | 时间预算 | 超时后果 |
|---|---|---|
| 上行完成 → RX1 开启 | ±20ms(依赖 32kHz 时钟) | 下行窗口错过 |
| RX1 窗口内监听 | 5 符号(SF12 约 164ms) | 漏包或费电 |
| RX1 → RX2 切换 | 1s 间隔(RX2 延迟 − RX1 延迟) | 配置不一致全落空 |
| 发射完成 → 睡眠 | 毫秒级 | 无谓耗电 |
| 占空比记账更新 | 发射前必须完成 | 超限发射违规 |
结论:协议栈不能被阻塞式代码卡住毫秒级以上。所有会阻塞的操作(Flash 写、传感器读取、网络等待)都要拆分或移出实时路径。
| 维度 | 裸机(前后台循环) | RTOS(如 FreeRTOS、Zephyr) |
|---|---|---|
| 复杂度 | 低,栈体积小 | 中高,内核开销 |
| 定时精度 | 依赖定时器中断,可控 | 任务调度延迟,需高优先级任务 |
| 资源占用 | 极小(适合内存受限 MCU) | 需栈空间,通常 4KB+ RAM |
| 可维护性 | 状态机手写,易乱 | 任务划分清晰,便于多人协作 |
| 适用 | 简单单设备、内存 <8KB | 复杂设备、多外设并发 |
社区主流参考实现 LoRaMac-node 是裸机风格:一个主循环按状态机推进,接收窗口由定时器中断精确开启。Zephyr 等 RTOS 用独立 LoRaWAN 子系统任务封装同样逻辑。选择标准:设备外设是否多、团队是否熟悉 RTOS、内存余量是否足够——不要为了用 RTOS 而用 RTOS。
推荐的调度形态是"事件队列 + 定时器列表":所有外部事件(射频中断、定时器到期、传感器完成)投递到事件队列,主循环按优先级处理,定时器列表管理所有超时(重传、占空比窗口、信标等待)。这套形态在裸机与 RTOS 下都能落地,且天然支持测试注入(用假事件表驱动状态机)。
/* 伪代码:主循环(事件驱动) */ for (;;) { ev = dequeue_event(); /* 阻塞或轮询事件队列 */ switch (ev.type) { case EV_RADIO_IRQ: handle_radio(ev); break; case EV_TIMER: handle_timer(ev); break; case EV_APP: handle_app(ev); break; } if (rx_window_pending()) { /* 窗口临近,快速切换射频 */ open_rx_window(); } }
把接收窗口的调度实现走一遍,是理解实时性要求的捷径。以下行接收为例:
t = 0 上行发射完成(射频中断触发) t + 100µs 关发射、清中断、读寄存器保存发射状态 t + 1s RX1 窗口临近:提前 3ms 进入"窗口预备" - 把射频切到 RX 模式(配置频率与 SF 与上行相同) - 打开接收中断 t + 1s 窗口开启:开始监听(5 个符号时间) t + 1.2s 未收到 → 关窗口 t + 2s RX2 窗口:同上,频率与 SF 用 RX2 参数 t + 2.2s 关窗口 → 进入低功耗休眠
三个时间点必须用硬件定时器:RX1 开启时刻、RX2 开启时刻、窗口超时关闭。任何软件循环轮询都会抖动,抖动超过符号时间窗口就错过下行。参考实现的做法是在定时器中断里直接操作射频寄存器(中断上下文足够短、可抢占主循环)。
逻辑分析仪挂射频的 BUSY/DIO 引脚,看收发切换的时序是否符合预期;示波器量 32kHz 时钟,确认休眠唤醒后的时钟连续性;协议栈内置事件打点(如 GPIO 翻转标记窗口开关时刻),与网络服务器侧的下行时刻对比,能直接定位"窗口提前/延后了多少"。实时性问题靠猜是猜不出来的,测量先行。
要点回顾:接收窗口是毫秒级硬约束,架构必须事件驱动;裸机与 RTOS 各有适用,参考实现偏裸机;事件队列 + 定时器列表是通用骨架。下一节算内存这笔账——嵌入式里最贵的资源。
LoRaMac-node 的裸机实现值得拆解:它把整个协议栈做成一个"单状态机 + 事件标志"的结构。主循环每次迭代做三件事:处理事件(射频中断置位的事件标志)、推进状态机(当前状态 + 事件 → 下一状态与动作)、检查定时器(最近的超时是否到期)。接收窗口用定时器中断直接操作射频,保证毫秒级精度。
/* 状态机的核心循环形态(参考 LoRaMac-node 的简化) */ typedef enum { IDLE, TX, WAIT_RX1, RX1, WAIT_RX2, RX2, SLEEP } mac_state_t; mac_state_t state = IDLE; uint32_t timer_ms; /* 下一个事件时刻 */ void mac_loop(void) { for (;;) { uint32_t now = get_ms(); if (timer_ms && now >= timer_ms) { /* 定时器到期 → 按状态转移 */ switch (state) { case TX: /* 发射完成,安排 RX1 */ schedule_rx(now + RECEIVE_DELAY1); state = WAIT_RX1; break; case WAIT_RX1: /* 开 RX1 窗口 */ radio_start_rx(rx1_freq, rx1_sf); state = RX1; break; case RX1: /* RX1 窗口结束 */ schedule_rx(now + (RECEIVE_DELAY2 - RECEIVE_DELAY1)); state = WAIT_RX2; break; case RX2: /* RX2 窗口结束 → 睡眠 */ radio_sleep(); state = SLEEP; break; default: break; } } handle_events(); /* 射频中断置位的标志 */ } }
这个模式的价值:状态转移全部由"定时器到期"驱动,天然避免"软件轮询抖动窗口"的问题;每个状态的动作是独立的 switch 分支,测试时可以注入假时刻表驱动状态机走完整路径。RTOS 移植时,把 mac_loop 放进一个专用任务,事件与定时器对接 RTOS 的队列与定时器,状态机本身不动——这就是"协议栈与平台解耦"的经典做法。
小结与速查:接收窗口毫秒级硬约束,架构必须事件驱动;事件队列 + 定时器列表是通用骨架;射频事件最高优先级;窗口开启要预留射频切换提前量;调试靠逻辑分析仪量 DIO/BUSY 与 32kHz 时钟,靠猜解决不了实时性问题。