连接机制

BLE 信道选择算法:CSA#1 与 CSA#2 原理、差异与纯 C 实现

发布于 2026-07-22 · 来自「嵌入式江湖」技术专栏

在 BLE 连接里,主从双方每过一个连接间隔就要在 数据信道 上通信一次。37 个数据信道(0~36)中,哪些能用、每次事件跳到哪个,由 信道选择算法(Channel Selection Algorithm,CSA) 负责决定。它的目标只有两个:把数据均匀铺在可用信道上、并自动避开被标记为“坏”的信道。BLE 定义了两套:CSA#1(4.0 起默认)和 CSA#2(5.0 引入,更强)。本文讲清两者原理与差异,并给一份纯 C 的参考实现。

一、CSA 解决什么问题

BLE 数据信道有 37 个(0~36,注意没有广播用的 37/38/39)。连接双方维护一张 Channel Map(37 bit,1=可用),并各自按同一套算法、用同一个 eventCounter(连接事件计数,从 0 开始)算出“本次事件用哪个信道”。

如果算法只在 37 个信道里做 rand(),一旦某些信道被 Wi-Fi 干扰要剔除(map 里置 0),剩余信道的“被选中概率”就会不均匀。CSA#1 的主要软肋不是均匀性(它静态很均匀),而是 序列可预测、周期短,容易被针对性干扰;CSA#2 用强置换修复了这两点,同时对动态 Channel Map 更鲁棒。

二、CSA#1 原理

CSA#1 本质是一个 基于跳频因子(hopIncrement)的线性递推,是经典线性同余跳频,与 Access Address 无关

hopIncrement:来自 CONNECT_IND 的 Hop 字段(5 位,范围 1~36,与 37 互质)
unmappedChannel = (eventCounter × hopIncrement) mod 37     // 闭式;递推等价 (prev + hop) mod 37
if 信道 unmappedChannel 在 Channel Map 中可用:
    实际信道 = unmappedChannel
else:
    remappingIndex = unmappedChannel mod N     // N = 可用信道数
    实际信道 = Channel Map 中第 remappingIndex 个“可用”信道

注意:CSA#1 完全不使用 Access Address。所有输入就是 eventCounter、hopIncrement(连接建立时由 Hop 字段固定)和 Channel Map。每次连接事件 eventCounter 加 1,序列就往前推一格。

优点

  1. 静态分布极佳。因为 37 是质数、hopIncrement 与它互质,eventCounter × hop mod 37完美遍历 0~36,每个数据信道都被公平命中。
  2. 实现极简单,一次乘法一个取模,适合资源受限的老芯片。

缺点

  1. 序列可预测。hopIncrement 是固定常数,unmappedChannel 只是 eventCounter 的线性函数——只要嗅到少量包推算出 hop,就能预测后续所有信道,可针对性干扰。
  2. 周期短。序列每 37 个连接事件就完整重复一次(约 0.28 s @ 7.5 ms 间隔),给了干扰方充足窗口。
  3. 剔信道后靠 remap 兜底。当某信道被标记为不可用,该事件改投 unmappedChannel mod N;N 不是 2 的幂时会有轻微余数偏斜(但整体仍较均匀)。

三、CSA#2 原理

CSA#2 在 5.0 引入,第一次把 Access Address 纳入计算(这是它与 #1 的关键区别)。它先由 32 位 Access Address 派生出一个 16 位 Channel Identifier

CI = AA[31:16] XOR AA[15:0]      // 高 16 位异或低 16 位

再用 CI 与 eventCounter 经过一组基于 S-box 的 强置换(Substitution-Permutation) 生成 16 位伪随机数,迭代若干轮:

prn = CI                          // 种子来自 Access Address
每轮:prn = permute(prn XOR eventCounter)   // 把事件计数卷入,打破线性
迭代若干轮后得 16 位 prn(一次强伪随机置换)
unmappedChannel = prn mod 37(可用则用,否则同 #1 的 remap 规则)

优点

  1. 更不可预测。S-box 置换破坏了线性结构,且 Access Address 是 32 位随机值(每连接不同),比 #1 的固定 hop 难推算得多。
  2. 对动态 Channel Map 鲁棒。强置换以 AA 为种子、输出空间大,不像 #1 那样是固定的线性序列,应对 map 频繁更新更稳。
  3. 抗 Wi-Fi 干扰能力显著强于 #1。

代价:比 CSA#1 多几十行代码和几次查表/移位,但对 MCU 仍是微秒级开销。

四、两者差异速查

维度 CSA#1 CSA#2
引入版本 4.0 5.0
是否用 Access Address (仅 eventCounter + hop + map) (派生 CI = AA[31:16] XOR AA[15:0])
核心结构 线性递推(LCG 风格) S-box 强置换(SPN 风格)
静态均匀性 极佳(hop 与 37 互质则完美遍历 0~36) 同级(强置换同样遍历 0~36)
可预测性 强(线性、hop 固定、周期 37 事件) 弱(强置换 + 每连接随机 AA)
代码量 几行 约几十行
默认使用 4.x 及 5.0 与旧设备互联时 5.0+ 双方均支持时协商启用

协商细节:连接双方都声明支持 CSA#2 时优先用 #2,否则回退 #1。注意:两者静态均匀性接近,CSA#2 的真正优势在抗预测 + 每连接随机 AA,而不是“更均匀”。

五、纯 C 参考实现

下面是一份 无依赖、可直接 gcc 编译运行 的 DEMO。CSA#1 严格按规范(基于 hopIncrement,不碰 Access Address);CSA#2 给出完整算法骨架与可运行示例(S-box 与轮次说明见代码注释,请以 Core Spec Vol 6 Part B §4.5.8.3 为准)。

#include <stdio.h>
#include <stdint.h>

/* ---------- Channel Map 工具 ---------- */
/* map: 5 字节,bit i (0..36) 置 1 表示信道 i 可用 */
static int chan_map_count(const uint8_t map[5]) {
    int n = 0;
    for (int i = 0; i < 37; i++)
        if (map[i >> 3] & (1u << (i & 7))) n++;
    return n;
}

/* 把“未映射索引”(0..N-1) 映射回真实信道号 */
static int chan_remap(int idx, const uint8_t map[5]) {
    int seen = 0;
    for (int i = 0; i < 37; i++) {
        if (map[i >> 3] & (1u << (i & 7))) {
            if (seen == idx) return i;
            seen++;
        }
    }
    return -1; /* 不应发生 */
}

/* ---------- CSA#1(严格按规范:基于 hopIncrement,不用 Access Address)---------- */
static int csa1_channel(uint16_t eventCounter, uint8_t hopIncrement,
                        const uint8_t map[5]) {
    int unmapped = (int)((eventCounter * (uint32_t)hopIncrement) % 37); /* 闭式,等价 (prev+hop)%37 */
    if (map[unmapped >> 3] & (1u << (unmapped & 7)))
        return unmapped;                          /* 该信道可用,直接用 */
    int N = chan_map_count(map);
    return chan_remap(unmapped % N, map);         /* 不可用则按 N 重映射 */
}

/* ---------- CSA#2(CI 来自 Access Address,S-box 非线性层 + 强扩散,演示用)---------- */
/* 规范 CSA#2 用一组 4 位->4 位 S-box 的置换(详见 Core Spec Vol 6 Part B §4.5.8.3)。
 * 下方用“运行时生成完整 AES S-box”作为强非线性层 + 位级扩散,结构(非线性 S-box
 * + 扩散 + 迭代)与规范一致。若需逐位符合规范用于互通测试,请把 permute 换成
 * §4.5.8.3 的官方 S-box 表。 */

static uint8_t aes_sbox[256];

static uint8_t gf8_mul(uint8_t a, uint8_t b) {
    uint8_t p = 0;
    for (int i = 0; i < 8; i++) {
        if (b & 1) p ^= a;
        uint8_t hi = (uint8_t)(a & 0x80);
        a = (uint8_t)(a << 1);
        if (hi) a ^= 0x1B;
        b = (uint8_t)(b >> 1);
    }
    return p;
}

static uint8_t gf8_inv(uint8_t a) {
    if (a == 0) return 0;
    for (int i = 1; i < 256; i++)
        if (gf8_mul(a, (uint8_t)i) == 1) return (uint8_t)i;
    return 0;
}

static void gen_aes_sbox(void) {
    for (int i = 0; i < 256; i++) {
        uint8_t inv = gf8_inv((uint8_t)i);
        uint8_t s = inv;
        s ^= (uint8_t)(((inv << 1) | (inv >> 7)) & 0xFF);
        s ^= (uint8_t)(((inv << 2) | (inv >> 6)) & 0xFF);
        s ^= (uint8_t)(((inv << 3) | (inv >> 5)) & 0xFF);
        s ^= (uint8_t)(((inv << 4) | (inv >> 4)) & 0xFF);
        aes_sbox[i] = (uint8_t)(s ^ 0x63);
    }
}

/* 一轮置换:两字节各过 AES S-box(双射、非线性),再跨字节位扩散,最后再过一次 S-box */
static uint16_t csa2_permute(uint16_t x) {
    uint8_t b0 = aes_sbox[x & 0xFF];
    uint8_t b1 = aes_sbox[(x >> 8) & 0xFF];
    uint32_t v = (uint32_t)(b0 | (b1 << 8));
    v ^= (v << 13) | (v >> 3);          /* 跨字节位扩散 */
    v ^= (v >> 7)  | (v << 9);
    b0 = aes_sbox[v & 0xFF];
    b1 = aes_sbox[(v >> 8) & 0xFF];
    return (uint16_t)(b0 | (b1 << 8));  /* 再各过一次 S-box,彻底非线性 */
}

static int csa2_channel(uint16_t eventCounter, const uint8_t aa[4],
                        const uint8_t map[5]) {
    int N = chan_map_count(map);
    /* CI = AA[31:16] XOR AA[15:0]:高 16 位(aa[2]|aa[3]<<8) 异或 低 16 位(aa[0]|aa[1]<<8) */
    uint16_t ci = (uint16_t)((aa[0] | (aa[1] << 8)) ^ (aa[2] | (aa[3] << 8)));
    uint16_t prn = ci;                        /* 种子 = Channel Identifier(来自 Access Address) */
    for (int r = 0; r < 6; r++) {            /* 迭代轮数,规范以 §4.5.8.3 为准 */
        prn = csa2_permute((uint16_t)(prn ^ eventCounter)); /* 每轮把事件计数卷入,破线性 */
    }
    int unmapped = (int)(prn % 37);
    if (map[unmapped >> 3] & (1u << (unmapped & 7)))
        return unmapped;                          /* 可用直接用 */
    return chan_remap(unmapped % N, map);         /* 否则按 N 重映射 */
}

/* ---------- 演示:统计分布,直观对比两者差异 ---------- */
int main(void) {
    gen_aes_sbox();   /* 生成 AES S-box(仅 CSA#2 需要)*/
    /* Access Address 示例(真实连接由链路层随机生成) */
    uint8_t aa[4] = {0x8E, 0x89, 0xBE, 0xD6};
    /* hopIncrement 示例(来自 CONNECT_IND 的 Hop 字段,1~36 且与 37 互质) */
    uint8_t hop = 7;

    /* 场景:剔除 3 个信道(模拟干扰),N=34,非 2 的幂 */
    uint8_t map_drop[5];
    for (int i = 0; i < 5; i++) map_drop[i] = (i == 4) ? 0x1F : 0xFF;
    map_drop[0] &= ~(1u << 5);  /* 关信道 5  */
    map_drop[1] &= ~(1u << 2);  /* 关信道 10 */
    map_drop[2] &= ~(1u << 4);  /* 关信道 20 */

    const int EVENTS = 3700; /* 每信道平均约 100 次 */
    int cnt1[37] = {0}, cnt2[37] = {0};

    for (int e = 0; e < EVENTS; e++) {
        int c1 = csa1_channel((uint16_t)e, hop, map_drop);
        int c2 = csa2_channel((uint16_t)e, aa, map_drop);
        if (c1 >= 0) cnt1[c1]++;
        if (c2 >= 0) cnt2[c2]++;
    }

    printf("Channel Map(剔除 5/10/20), 事件数=%d, 算法对比(每信道命中次数):\n", EVENTS);
    printf("%4s %8s %8s\n", "CH", "CSA#1", "CSA#2");
    for (int i = 0; i < 37; i++) {
        if (map_drop[i >> 3] & (1u << (i & 7)))
            printf("%4d %8d %8d\n", i, cnt1[i], cnt2[i]);
    }
    return 0;
}

六、运行结果怎么看

编译 gcc csa_demo.c -o csa_demo && ./csa_demo,观察“每信道命中次数”一列(3700 次事件、34 个可用信道,理论均值约 109):

  • CSA#1 完全不读 Access Address。把 AA 换成完全不同的值,CSA#1 的命中分布纹丝不动——因为它只用 eventCounter 与 hop=7。只要 hop 与 37 互质,unmappedChannel 完美遍历 0~36,每信道约 100 次(全 37 信道可用时标准差为 0)。
  • CSA#2 把 Access Address 当种子。CI = AA[31:16] XOR AA[15:0] 决定置换起点,AA 每连接随机生成;换不同 AA 时 CSA#2 分布随之改变,而 CSA#1 不变。
  • 两者静态均匀性其实接近。在“剔除 3 个信道、N=34”的干扰场景里,两者标准差都在同一量级(约 30)——因为被剔除信道的事件都要走 remap,remap 本身的余数偏斜对两种算法一视同仁。把 CSA#2 说成“比 #1 均匀得多”是不准确的。
  • 真正的差异在可预测性。CSA#1 的 unmappedChannel = (eventCounter × 7) mod 37,是一条斜率固定的直线,嗅到两三个包反推出 hop=7 就能预测全部后续信道,且每 37 事件完整重复;CSA#2 经 AA 种子强置换,序列既不可反推、又随每连接随机 AA 变化,这才是 5.0 引入它的根本动机,也是对动态 Channel Map 更新更鲁棒的原因。

七、小结

  • CSA#1 是基于 hopIncrement 的线性递推跳频,不使用 Access Address;静态分布极佳(完美遍历),但序列线性、可预测、每 37 事件重复,适合老设备 / 约束极强的场景。
  • CSA#2 在 #1 基础上引入 Access Address 派生的 Channel Identifier 做 S-box 强置换,两者静态均匀性接近,CSA#2 的真正优势在抗预测 + 每连接随机 AA,是 5.0+ 的推荐默认。
  • 两者最后一步(unmappedChannel 映射到实际信道 + Channel Map 重映射)完全相同,差异全在“如何生成那个未映射信道索引”。
  • DEMO 里 CSA#1 严格符合规范(无 AA);CSA#2 给出可运行骨架(CI = AA 高 16 异或低 16 + 运行时 AES S-box + 位扩散),S-box 表与轮次请以 Core Spec Vol 6 Part B §4.5.8.3 的官方定义替换后再用于互通测试。
更多 BLE / 鸿蒙星闪 / 芯片选型 / MCU 实战,持续更新