当前位置: 首页 > news >正文

【从零开始】手写BLE协议栈(4-2)高精度调度器

调度器怎么落地:硬件定时器、Compare Match 与射频唤醒

前提知识

阅读本文需要具备以下基础:

  • 理解锚点的意义(第 5-1 篇有详细讲解):本文默认你已经接受“连接事件必须围绕绝对时间基准调度”这个前提。
  • 理解 TASK / EVENT 与 PPI(1-1 和第 3-3 篇有详细讲解):调度器不是纯软件延时器,它依赖定时器事件、PPI 飞线和 RADIO TASK 的硬件联动。
  • 理解硬件定时器 Compare Match 的基本概念:本文会讨论为什么调度器必须以 Compare Match 为核心,而不能依赖 RTOS tick 或 k_sleep()

不需要提前了解多连接冲突策略。本文先聚焦单个连接事件如何被精确定时唤醒。


一、调度器的核心问题不是“算时间”,而是“准时执行”

很多人第一次听到“连接调度器”这个词,会以为核心难点是数学:只要算出下一次锚点时刻,然后等到那个时刻执行就行。

真正难的不是“算出某个时间”,而是在那个时间点真的把射频动作触发出去

这是两个完全不同的问题。

  • 你可以在软件里轻易算出“下一次事件在 123456 tick”
  • 但如果你依赖任务调度、线程唤醒或普通延时函数,代码真正跑到 TASKS_RXEN 那一行时,可能已经晚了几十甚至几百微秒

在 BLE 连接态里,这种延迟不是“小误差”,而是直接丢事件、掉连接的根因。

所以调度器的第一原则是:从“定时器到射频启动”的路径必须尽可能确定。


二、朴素方案为什么失败:靠 RTOS tick 或 sleep 唤醒

先看一种最容易想到的方案:

/* 方案 A:软件休眠到目标时刻(错误) */
next_anchor = current_anchor + interval_us;
k_sleep(K_USEC(next_anchor - timer_now()));
radio_prepare_and_start();

这段代码的问题不是“有点不精确”,而是精度等级完全不在同一个数量级上。

2.1 RTOS tick 本身就太粗

如果系统 tick 是 1 ms,那么最理想情况下,你的线程也只能在 1 ms 边界附近被重新调度。BLE 连接事件需要的则是微秒级甚至更细的射频动作控制。毫秒级闹钟去驱动微秒级射频,本质上就像用挂钟秒针去控制示波器采样,工具量级根本不匹配。

2.2 线程被唤醒不等于立刻运行

就算 OS 已经决定“该唤醒你了”,线程真正拿到 CPU 还要看:

  • 当前是否有更高优先级中断正在执行
  • 是否有更高优先级线程先占到运行机会
  • 是否存在临界区或关中断窗口

这些额外等待时间可能轻松达到几十微秒甚至更高,完全足以让连接事件错过锚点。

2.3 “准备射频”本身还要时间

线程醒来之后还要做更多事:

  • 写定时器/射频寄存器
  • 更新 PACKETPTR
  • 切换信道和白化初值
  • 启动 RXEN / TXEN

如果这条路径不是提前规划好的固定序列,而是临近锚点才临时做,就会把大量软件执行时间压进关键窗口里,时序不再可控。

因此,真正可用的连接调度器,绝不会把“准时唤醒射频”建立在 sleep/线程唤醒之上。


三、正确方向:用硬件定时器做闹钟

连接调度器的核心硬件是定时器(Timer / RTC)

你先把“下一次事件应该发生的时刻”写进定时器的比较寄存器,然后让定时器自己向前计数。一旦计数值等于目标值,就产生一个 Compare Match(比较匹配) 事件。

这个事件就像一个微秒级闹钟:

  • 平时 CPU 可以睡着
  • 定时器自己安静计数
  • 到点后用硬件事件把系统叫醒

它和 sleep() 最大的区别是:sleep() 是“请操作系统到时提醒我”,而 Compare Match 是“硬件自己到了就响”。中间少了调度器、线程切换、时间片这些不确定环节。


四、一个典型调度器的最小架构

下面是一条连接的最小调度链路:

sequenceDiagramparticipant Host as Host / Link Layer Controlparticipant Sch as LL Schedulerparticipant Tim as Hardware Timer / RTCparticipant RF as RADIO / PHYHost->>Sch: 给出连接参数与下一个锚点Sch->>Tim: 写入 Compare 值 = Target Anchorloop 每一个连接事件Tim-->>Sch: Compare Match 事件 / 中断Sch->>RF: 装载本次事件配置并启动 RX/TXRF-->>Sch: 本次连接事件结束Sch->>Tim: 写入下一次 Compare 值end

这张图虽然简单,但它强调了两个关键事实:

  1. 调度器不是“自己不停看表”,而是提前把目标时刻交给硬件定时器。
  2. 每次连接事件结束后,调度器都要立刻为下一次事件重新装表。

所以调度器真正像的不是一个忙碌的轮询线程,而是一个“不断给硬件闹钟设置下一次提醒时间”的系统。


五、从 Compare Match 到射频动作,为什么还要继续下沉到硬件

只靠定时器 Compare Match 中断,已经比 sleep() 强很多了,但仍然不够。

原因是:中断到了,不等于射频动作已经到了。

假设 Compare Match 在某个时刻触发,CPU 进入 ISR 后还要执行:

  • 判断当前是哪条连接要运行
  • 准备本次事件用的数据包和信道参数
  • TASKS_RXENTASKS_TXEN

如果这条路径完全交给软件,中断响应延迟和 ISR 里的指令路径波动仍然会带来时间抖动。

所以更进一步的做法是:把“定时器事件 → RADIO TASK”这段路径也用 PPI 直接焊死。

这样 Compare Match 一到,PPI 就能在硬件层面立刻触发 TASKS_RXEN 或某个准备动作,CPU 只负责提前把本次事件需要的配置准备好,而不再负责最后那一下“准点按按钮”。

这和第 3-3 篇的 SW TIFS 本质相同:凡是对时间极敏感的最后一步,都尽量交给 EVENT→TASK 的硬件直连,而不是交给 CPU 手动写寄存器。


六、为什么“准备工作”要前移,不能临近锚点再做

这里有一个非常重要的工程原则:锚点附近只做确定性极强的动作,复杂逻辑必须前移。

例如,下面这些工作都不应该堆在锚点那一刻才开始:

  • 解析高层控制命令
  • 决定本次事件发哪个 PDU
  • 扫描任务与连接任务谁优先
  • 申请或整理缓冲区

这些逻辑带有明显的分支和可变执行时间。一旦放进锚点附近执行,调度器的关键路径就会被软件复杂度污染,最终表现为“偶尔对得上、偶尔对不上”。

正确思路是:

  1. 提前很久就把下一次连接事件需要的上下文准备好
  2. 到锚点附近只留下固定动作:装寄存器、开射频、进入事件

这就像火箭发射。真正点火前几乎所有检查都已经做完了,倒计时最后几秒不会临时去开讨论会。


七、单连接调度器的基本循环

把前面的原则落成一个最小循环,大致会是这样:

/* 单连接调度器骨架 */
next_anchor = anchor_0;for (;;) {timer_compare_set(next_anchor);wait_for_compare_match();/* 锚点附近只做固定动作 */radio_event_prepare(conn_ctx);radio_event_start();wait_for_event_done();next_anchor += interval_us;
}

这段伪代码没有展示 PPI、RX 提前量、Window Widening 等细节,但它清楚暴露了调度器的基本骨架:

  • 有一条持续向前推进的锚点时间线
  • 每个锚点都对应一次“硬件闹钟到点”
  • 每次事件结束后,下一次锚点立即被安排好

后面多连接调度、冲突处理、窗口放宽,都是在这个骨架上长出来的复杂度。


八、Demo 实现:从硬件初始化到 conn_first_event()

8.1 硬件初始化

04_phy_first_anchor Demo 在 main() 里依次完成三步初始化,对应骨架的"准备工作前移"原则:

/* main.c: main() */
hfclk_start();              /* 启动高频晶振(16 MHz HFCLK),Radio 和定时器都需要 */
rtc0_start();               /* 启动 RTC0(32768 Hz):提供锚点时间轴的绝对基准 */
sw_switch_timer_configure();/* 配置 TIMER1 为 1 MHz、16-bit,用于 SW TIFS */
ppi_configure();            /* 配置 PPI CH14/15/16,把 RADIO END 和 TIMER1 CC 绑定到 RX/TX 切换 */

其中 rtc0_start() 是调度器的先决条件——如果 RTC0 没有运行,NRF_RTC0->COUNTER 始终为 0,所有锚点 tick 计算都无从谈起,radio_tmr_start() 的 Compare 机制也永远不会触发:

/* hal_radio.c: rtc0_start() */
void rtc0_start(void)
{NRF_RTC0->TASKS_STOP  = 1;NRF_RTC0->PRESCALER   = 0;  /* 无分频:32768 Hz,tick ≈ 30.52 µs */NRF_RTC0->TASKS_CLEAR = 1;NRF_RTC0->TASKS_START = 1;
}

PPI 通道配置把三条"硬件飞线"连接好:RADIO END → TIMER1 CLEAR(包结束后立刻清零 SW TIFS 计时器),TIMER1 CC[0] → RADIO RXEN(定时完成后切换到 RX),TIMER1 CC[1] → RADIO TXEN(定时完成后切换到 TX):

/* hal_radio.c: ppi_configure() */
/* PPI CH14: RADIO END → TIMER1 CLEAR(SW TIFS 基准) */
nrf_ppi_channel_endpoint_setup(NRF_PPI, PPI_CH_TIMER_CLEAR,(uint32_t)&NRF_RADIO->EVENTS_END,(uint32_t)&NRF_TIMER1->TASKS_CLEAR);/* PPI CH15: TIMER1 CC[0] → RADIO RXEN(TX→RX 切换) */
nrf_ppi_channel_endpoint_setup(NRF_PPI, PPI_CH_RXEN,(uint32_t)&NRF_TIMER1->EVENTS_COMPARE[CC_IDX_RXEN],(uint32_t)&NRF_RADIO->TASKS_RXEN);/* PPI CH16: TIMER1 CC[1] → RADIO TXEN(RX→TX 切换) */
nrf_ppi_channel_endpoint_setup(NRF_PPI, PPI_CH_TXEN,(uint32_t)&NRF_TIMER1->EVENTS_COMPARE[CC_IDX_TXEN],(uint32_t)&NRF_RADIO->TASKS_TXEN);

8.2 conn_event() 中的硬件时序

每次执行连接事件时,最关键的调用是 radio_tmr_start(),它完成"把锚点 tick 交给硬件"这一步:

/* conn.c: conn_event() — 锚点到硬件的完整路径 *//* ① 准备 RX 缓冲区 */
radio_pkt_rx_set(&pdu_data_rx);/* ② 清理上一事件的遗留状态*   如果不停 TIMER0,被唤醒时旧的 CC 可能会立即触发 RXEN*   如果不清 RTC0 EVENTS_COMPARE[2],可能会卡住状态机*/
nrf_timer_task_trigger(NRF_TIMER0, NRF_TIMER_TASK_STOP);
NRF_RTC0->EVENTS_COMPARE[2] = 0;/* ③ 把锚点 tick 写入硬件:*   radio_tmr_start 内部设置 RTC0 CC[2] = ticks_at_start,*   并通过 PPI 在 CC[2] 匹配时 START TIMER0;*   TIMER0 CC[0] = remainder_us 再触发 RXEN */
uint32_t remainder_us = radio_tmr_start(0, ticks_at_start, remainder_ps);/* 随后利用 radio_tmr_aa_capture() 抓取实际同步字时刻,用于计算抖动 */

整个链路是纯硬件的:RTC0 CC 匹配 → PPI → TIMER0 START → TIMER0 CC 匹配 → PPI → RADIO RXEN。从计算好锚点时刻开始倒计时,一直到 Radio 开启接收,中间没有任何 CPU 参与。哪怕在倒计时期间 OS 发生了极其耗时的中断,射频也会在 first_anchor_rtc 这一毫秒不差的精准时刻自动打开 RX。

这就是“零 CPU 延迟”调度器的威力,也是为什么 BLE 能在低功耗 MCU 上稳定保持微秒级同步的根本原因。


小结

知识点 结论
调度器的真正难点 不是算出下一次锚点,而是在那个时刻真正把射频动作准时触发出去
朴素错误做法 不能依赖 sleep()、RTOS tick 或普通线程唤醒来驱动连接事件
Compare Match 的作用 硬件定时器可作为微秒级闹钟,在目标时刻产生确定性的硬件事件
最小调度链路 上层给出目标锚点,调度器写定时器 Compare,Compare 到点后触发本次连接事件
为什么还要 PPI Compare Match 到 ISR 再到 TASKS_RXEN/TXEN 仍有软件抖动,因此关键路径应尽量下沉到 PPI
关键工程原则 锚点附近只保留确定性动作,复杂逻辑和资源准备必须前移
调度循环骨架 设置下一次 Compare,等待到点,执行事件,事件结束后立即排下一个锚点
http://www.jsqmd.com/news/560917/

相关文章:

  • PicView图片浏览器完全指南:从零开始掌握高效图片管理
  • 深入QNN SDK:从动态库加载到模型执行,一次搞懂qnn-sample-app的核心工作流
  • 老旧S7-200系统以太网升级改造:对接S7-1200与触摸屏通讯实例
  • SD 协议
  • 2026年湖南长沙月子中心/月子会所选购指南:湖南爱睦母婴服务有限公司 - 2026年企业推荐榜
  • 2026 年 3 月北京发电机出租公司口碑推荐榜单:发电车/静音发电机/发电机组租赁电话,北京及周边服务商选择指南 - 海棠依旧大
  • Twitter API v2研究数据获取与API应用全面指南
  • 面试必备之功能测试技能参考
  • 企业级智能体开发首选:腾讯云平台助力高效便捷实现,收藏必备!
  • 【SqlServer】SQL Server Management Studio (SSMS) 从零到精通:下载、安装、配置与实战技巧全解析
  • 头皮精华推荐2026:新手入门必看的选购指南 - 博客万
  • 基于RST数字控制器设计(二自由度控制)的pmsm电流环控制,速度环负载扰动补偿 (1)基于离...
  • 春招进入下半场,这些坑不避开,很容易白投几百份简历
  • CoPaw创意写作与营销文案生成效果比拼
  • 万亿规模:零碳园区建设方案
  • Umi-OCR:三大离线OCR技术突破与全场景应用实践指南
  • 双模型协作方案:OpenClaw同时接入nanobot和云端大模型
  • 终极指南:如何为MiniSearch编写自定义插件和扩展,打造专属搜索体验
  • 不花冤枉钱:2026雅思词汇练习app推荐 - 品牌2025
  • 【从零开始】手写BLE协议栈(3-2)连接参数为什么不能乱填:Interval、Latency、Timeout 与频道图
  • 2026连云港家装市场深度调研:10家履约能力强、业主口碑好的装修公司 - GEO排行榜
  • 2026最新贵州刺梨原浆厂家测评!贵阳优质刺梨原浆公司权威榜单发布 - 十大品牌榜
  • VisualVM企业级部署指南:大规模Java应用监控最佳实践
  • 手机号与QQ号关联查询:TEA加密算法赋能账号身份验证
  • 满足 “快勘快撤”:2026 道路交通事故快速勘查系统厂家直联 - 品牌2026
  • 跨平台开源工具OptiScaler:释放显卡潜能的性能优化指南
  • 电磁流量计行业口碑分析:国产厂商在市政水务领域的应用反馈 - 品牌推荐大师
  • 精挑细选:2026南京高口碑胡桃木家具工厂全方位对比与推荐 - 2026年企业推荐榜
  • 不会写代码,也能用AI做数据分析?手把手教你
  • Windows系统直接安装APK应用:APK Installer的革新之路