01-实时性与任务调度的时间预算


5.1 实时性与任务调度的时间预算

本节摘要:LoRaWAN 的接收窗口毫秒级开关、发射时序与超时重传,决定了协议栈必须是"事件驱动 + 精准定时"的架构。本节给出典型任务划分、时间预算表与两种调度模型(裸机前后台 vs RTOS)的取舍。位置:4.3 结了安全账,本章把前面所有账本翻译成软件架构。

必须满足的实时性硬约束

事件 时间预算 超时后果
上行完成 → RX1 开启 ±20ms(依赖 32kHz 时钟) 下行窗口错过
RX1 窗口内监听 5 符号(SF12 约 164ms) 漏包或费电
RX1 → RX2 切换 1s 间隔(RX2 延迟 − RX1 延迟) 配置不一致全落空
发射完成 → 睡眠 毫秒级 无谓耗电
占空比记账更新 发射前必须完成 超限发射违规

结论:协议栈不能被阻塞式代码卡住毫秒级以上。所有会阻塞的操作(Flash 写、传感器读取、网络等待)都要拆分或移出实时路径。

裸机前后台 vs RTOS

维度 裸机(前后台循环) 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 翻转标记窗口开关时刻),与网络服务器侧的下行时刻对比,能直接定位"窗口提前/延后了多少"。实时性问题靠猜是猜不出来的,测量先行。

易错点补充

  • 中断服务函数里做重活:射频中断里只做"状态记录 + 置标志",解析与协议处理放主循环,中断里跑 AES 会让窗口抖动;
  • 休眠前不关中断:深睡前的最后一步要禁用射频中断并确认状态,否则唤醒后第一次中断就把栈打乱;
  • 定时器精度与功耗的权衡:32kHz 时钟省电但漂移大,长时间休眠后要对时,休眠时长超过数分钟要考虑周期唤醒校准。

易错点清单

  • 在实时路径里做阻塞 IO:Flash 写、串口打印会卡住窗口切换,全部移到非实时上下文;
  • 定时器用软件计数漂移:窗口依赖硬件定时器,软件 tick 计数经不起中断抖动;
  • 任务优先级设计颠倒:射频事件必须最高优先级,把业务任务放前面会让窗口频繁错过;
  • 忘记"窗口临近"的提前量:射频切换本身要时间(约 1–3ms),窗口开启要在理论时刻前预留,否则每次都晚一拍。

要点回顾:接收窗口是毫秒级硬约束,架构必须事件驱动;裸机与 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 的队列与定时器,状态机本身不动——这就是"协议栈与平台解耦"的经典做法。

易错点补充

  • 状态机与定时器耦合太紧:把"定时器到期动作"散落在各函数里,改一个状态就要翻遍代码,状态转移必须集中在状态机 switch 里;
  • 事件标志与状态机不一致:射频中断置位了"RX 完成"标志,但状态机还在 TX 状态,处理要按状态过滤事件,不能见事件就处理;
  • 定时器单例不够用:重传超时、占空比窗口、Class B 信标等待可能同时进行,定时器要支持多个并发条目。

本节速查

小结与速查:接收窗口毫秒级硬约束,架构必须事件驱动;事件队列 + 定时器列表是通用骨架;射频事件最高优先级;窗口开启要预留射频切换提前量;调试靠逻辑分析仪量 DIO/BUSY 与 32kHz 时钟,靠猜解决不了实时性问题。

作者与出处
原作者:灏天文库
来源:平台策划编纂
整理人: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U