从零实现BLE协议栈(4-2)精度挑战:微秒级精准锚点触发
精度挑战:微秒级精准锚点触发
前提知识
阅读本文需要具备以下基础:
- 理解锚点的意义(第 4-1 篇有详细讲解):本文默认你已经接受“连接事件必须围绕绝对时间基准调度”这个前提。
- 理解 TASK / EVENT 与 PPI(1-1 和第 3-3 篇有详细讲解):调度器不是纯软件延时器,它依赖定时器事件、PPI 飞线和 RADIO TASK 的硬件联动。
- 理解硬件定时器 Compare Match 的基本概念:本文会讨论为什么调度器必须以 Compare Match 为核心,而不能依赖 RTOS tick 或
k_sleep()。
不需要提前了解多连接冲突策略。本文先聚焦单个连接事件如何被精确定时唤醒。
一、调度器的核心挑战:精确触发射频动作,而非只算出时刻
在上一篇文章中,我们已经知道了如何计算锚点,但是实际上在蓝牙开发中,真正难的不是“算出某个时间”,而是在那个时间点真的把射频动作触发出去。这是两个完全不同的问题。你可以在软件里轻易算出“下一次事件在 123456 tick”,但如果你依赖任务调度、线程唤醒或普通延时函数,代码真正跑到那一行时,可能已经晚了几十甚至几百微秒。在 BLE 连接态里,这种延迟不是“小误差”,而是直接丢事件、掉连接的根因。
所以调度器的第一原则是:从“定时器到射频启动”的路径必须尽可能确定。
二、定时器 Compare Match:比 sleep 更确定的微秒级触发机制
连接调度器的核心硬件是定时器(Timer / RTC)。
你先把“下一次事件应该发生的时刻”写进定时器的比较寄存器,然后让定时器自己向前计数。一旦计数值等于目标值,就产生一个 Compare Match(比较匹配) 事件。
它和 sleep() 最大的区别是:sleep() 是“请操作系统到时提醒我”,而 Compare Match 是“硬件自己到了就响”。中间少了调度器、线程切换、时间片这些不确定环节。
下面是一条连接的最小调度链路:
这张图虽然简单,但它强调了一个关键事实:调度器不是"自己不停看表",而是提前把目标时刻交给硬件定时器,让硬件在那一刻自己触发射频动作。
三、从 Compare Match 到射频动作,为什么还要继续下沉到硬件
只靠定时器 Compare Match 中断,已经比 sleep() 强很多了,但仍然不够。
原因是:中断到了,不等于射频动作已经到了。
假设 Compare Match 在某个时刻触发,CPU 进入 ISR 后还要执行:
- 判断当前是哪条连接要运行
- 准备本次事件用的数据包和信道参数
- 写
TASKS_RXEN或TASKS_TXEN
如果这条路径完全交给软件,中断响应延迟和 ISR 里的指令路径波动仍然会带来时间抖动。
所以更进一步的做法是:把“定时器事件 → RADIO TASK”这段路径也用 PPI 直接焊死。
这样 Compare Match 一到,PPI 就能在硬件层面立刻触发 TASKS_RXEN 或某个准备动作,CPU 只负责提前把本次事件需要的配置准备好,而不再负责最后那一下“准点按按钮”。
四、Demo 实现:从硬件初始化到 conn_first_event()
4 .1 硬件飞线配置 (PPI)
通过 PPI 将 RADIO_END 与 TIMER1 绑定,实现硬件级的 \(T\_IFS\) 自动切换。
/* 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);
4.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 上稳定保持微秒级同步的根本原因。
本系列教程同款硬件:👇
芯片: nRF 52832 开发板
工具: nRF 52840 BLE Dongle 蓝牙嗅探器
工具: 逻辑分析仪
工具: BPA low energy 蓝牙分析仪
本文版权归作者:ixbwer所有,转载请注明原文链接:https://www.cnblogs.com/ixbwer/p/19796676,否则保留追究法律责任的权利。

浙公网安备 33010602011771号