从零实现BLE协议栈(4-3)追逐游戏:实现 CSA#1 跳频算法

追逐游戏:实现 CSA#1 跳频算法

前提知识

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

  • 理解 CONNECT_INDChMHop 字段(第 3-1 篇有详细讲解):跳频算法的所有输入参数——信道图(Channel Map)和跳步数(Hop)——都来自 CONNECT_IND
  • 理解永动机(第 4-2 篇有详细讲解):跳频的核心输入是 Event Counter(或者每次增加的未映射信道)。
  • 能够阅读 C 代码和位操作:本文包含实际代码实现,会解释信道图的 Remapping 逻辑。

不需要提前了解最新 5.0 以后引入的 CSA#2 算法,本文的代码实现专注于经典且基石所在的 CSA#1


一、为什么每次约会都要换个地方

2.4 GHz 是全球非授权频段。Wi-Fi、微波炉、Zigbee、蓝牙经典——全部挤在这 80 MHz 的频带里。如果 BLE 连接固定使用某一个信道,那么一旦这个信道被 Wi-Fi 持续占用,整条连接就会在干扰中瘫痪。

跳频(Frequency Hopping)的解决逻辑很简单:让两端按照同一个规律,每次约会都换一个频道。即使某个频道被干扰,双方同时跳到下一个干净的频道,最多丢失一次数据包,然后连接恢复正常。

BLE 把 2.4 GHz 频段分成 40 个信道:

  • 信道 37、38、39:广播专用信道
  • 信道 0–36:连接数据通信使用的 37 个数据信道

跳频算法的输入是当前的 Connection Event Counter,输出是这次连接事件使用的信道编号。每次连接事件完成后,Counter 自增,下次事件在新信道上进行。


二、信道图:把"有干扰的信道"标记出来

CONNECT_IND 里有一个叫 ChM(Channel Map)的字段,占 5 字节(40 bit)。每个 bit 对应一个信道:bit = 1 表示可用,bit = 0 表示不可用。

Master 的 Host 可以通过 HCI_LE_Set_Host_Channel_Classification 通知 Controller 某些信道受到干扰,此时 Controller 会在下一次参数更新中下发新的 ChM,把这些信道标记为不可用。

在代码里,这 5 字节直接存放在 conn_params.chan_map[5] 里。查一个信道是否可用,只需要:

/* 检查信道 ch 是否在 chan_map 中被标记为可用(bit = 1 表示可用) */
bool chan_available(uint8_t ch) {
    return (conn_params.chan_map[ch >> 3] & (1U << (ch & 7))) != 0;
}

三、CSA # 1:BLE 4.x 的经典跳频算法

3.1 未映射信道计算

每次连接事件后,先用上一次的未映射信道值加上 Hop,对 37 取模:

\[UnmappedChannel = (lastUnmappedChannel + Hop) \bmod 37 \]

Hop 来自 CONNECT_IND,取值范围 5–16。初始值为 0。

3.2 信道可用性检查与映射

如果 \(UnmappedChannel\)ChM 中被标记为可用(bit = 1),就直接使用它。

如果它被标记为不可用(bit = 0),就需要进行映射(Remapping):在 37 个信道中,把所有标记为可用的信道按升序编号,取第 \(UnmappedChannel \bmod N_{used}\) 个:

\[MappedChannel = UsedChannelTable\left[UnmappedChannel \bmod N_{used}\right] \]

这样做的结果是:不可用信道的对应请求,会被均匀分散到其余可用信道上。

3.3 完整 C 实现

/*
 * Channel Selection Algorithm #1(来自 05_phy_anchor/src/ll_pdu.c)
 *
 * 每次调用推进一个 Event,返回本次事件使用的数据信道编号(0–36)。
 */
uint8_t chan_sel_1(void)
{
    uint8_t unmapped;

    /* ① 推进未映射信道:(上次值 + Hop) mod 37 */
    unmapped = (last_unmapped_chan + conn_params.hop) % 37;
    last_unmapped_chan = unmapped;

    /* ② 检查该信道是否可用(chan_map 里对应 bit 是否为 1) */
    if (conn_params.chan_map[unmapped >> 3] & (1U << (unmapped & 7))) {
        return unmapped;           /* 可用:直接返回 */
    }

    /* ③ 不可用:映射到第 (unmapped % N_used) 个可用信道 */
    uint8_t remap_index = unmapped % conn_params.chan_count;
    uint8_t count = 0;

    for (uint8_t i = 0; i < 37; i++) {
        if (conn_params.chan_map[i >> 3] & (1U << (i & 7))) {
            if (count == remap_index) {
                return i;          /* 找到第 remap_index 个可用信道 */
            }
            count++;
        }
    }

    return 0;  /* 不应到达,chan_count > 0 有保证 */
}

3.4 chan_sel_1() 在连接事件中的调用方式

每次进入连接事件时调用 chan_sel_1(),得到本次事件应使用的信道:

/* conn.c: conn_event() 的第一行 */
uint8_t chan = chan_sel_1();
data_chan_set(chan);

注意:即使本次事件因为时序错误被提前放弃(ticks_at_start 已过期),chan_sel_1()必须先调用,以保证 last_unmapped_chan 正确推进:

static bool conn_event(uint32_t ticks_at_start, ...)
{
    /* 检查目标 RTC tick 是否在未来 */
    uint32_t ahead = (ticks_at_start - NRF_RTC0->COUNTER) & HAL_TICKER_CNTR_MASK;
    if (ahead > (HAL_TICKER_CNTR_MASK >> 1) || ahead < RTC0_CMP_OFFSET_MIN) {
        (void)chan_sel_1();           /* ← 即使放弃执行,也必须推进信道计数 */
        conn_event_rx_timeout++;
        conn_event_counter++;
        return false;
    }

    uint8_t chan = chan_sel_1();      /* ← 正常路径:计算并使用本次信道 */
    data_chan_set(chan);
    ...
}

四、Event Counter 同步:最容易出错的细节

跳频的核心是双方的 Event Counter 保持一致。每完成一次 Connection Event,无论这次事件有没有成功收到包,Counter 都必须自增一次。

4.1 普通情况

每次进入 conn_event() 前,conn_event_counter 不需要显式传入,但每次执行完一次事件循环(不论成功与否)都会 conn_event_counter++

/* conn.c: 每次连接事件结束后 */
conn_event_counter++;

4.2 Slave Latency 期间的 Counter 处理

BLE 允许 Slave 跳过若干个连接事件(没有上行数据时)。当 Slave 跳过 N 个事件后醒来,它在这 N 个事件期间没有调用 chan_sel_1(),但 Master 的 Counter 已经加了 N 次。

在真正的 Latency 实现里,Slave 需要在醒来前"追赶"信道计算:连续调用 N 次 chan_sel_1() 或等效地手动快进 last_unmapped_chan,使双方在唤醒时的信道再次对齐。如果忽略这一步,唤醒后 Master 和 Slave 将在不同信道上等待彼此,连接必然超时断开。

4.3 Channel Map 更新的时机

如果 Host 通过 HCI_LE_Set_Host_Channel_Classification 发来了新的信道图,新图不能立刻生效,必须等到 LL_CHANNEL_MAP_IND 控制包中指定的 Instant(某个特定的 Event Counter 值)才能切换。

原因是:如果 Master 在 Counter = 50 时提前切换到了新 ChM,而 Slave 还在用旧 ChM,双方对"哪些信道可用"的理解已经不同,整个 Remapping 逻辑的输出自然也不同,下一次事件双方必定在不同信道上,连接立刻断开。


五、CSA # 2:BLE 5.0 引入的伪随机跳频(原理概述)

CSA #1 的跳频序列有明显的周期性。对于有经验的干扰攻击者,只需监听几次就能推算出完整序列,实施定向干扰。

BLE 5.0 引入了 CSA # 2(Channel Selection Algorithm #2 ),基于伪随机排列,生成看起来随机但双方完全同步的信道序列:

graph TD A[Event Counter] --> D[MAM 运算模块] B[Access Address] --> C[Channel Identifier 计算<br>= AA_high16 XOR AA_low16] C --> D D --> E{检查 Channel Map} E -- 信道可用 --> F[输出最终 Channel] E -- 信道不可用 --> G[Remapping 逻辑<br>与 CSA#1 相同] G --> F

MAM(Multiply, Add, Mod)运算模块对 Channel Identifier 和 Event Counter 的组合进行一系列确定性变换,最终输出 0–36 范围内的信道编号。由于 Access Address 每次连接都不同,即使在同一个区域内的两条连接也会使用完全不同的跳频序列。


本系列教程同款硬件:👇
芯片: nRF 52832 开发板
工具: nRF 52840 BLE Dongle 蓝牙嗅探器
工具: 逻辑分析仪
工具: BPA low energy 蓝牙分析仪

posted @ 2026-03-30 17:00  ixbwer  阅读(57)  评论(0)    收藏  举报