在 BLE 连接里,主从双方每过一个连接间隔就要在 数据信道 上通信一次。37 个数据信道(0~36)中,哪些能用、每次事件跳到哪个,由 信道选择算法(Channel Selection Algorithm,CSA) 负责决定。它的目标只有两个:把数据均匀铺在可用信道上、并自动避开被标记为“坏”的信道。BLE 定义了两套:CSA#1(4.0 起默认)和 CSA#2(5.0 引入,更强)。本文讲清两者原理与差异,并给一份纯 C 的参考实现。
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 本质是一个 基于跳频因子(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,序列就往前推一格。
优点:
eventCounter × hop mod 37 会 完美遍历 0~36,每个数据信道都被公平命中。缺点:
unmappedChannel mod N;N 不是 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 规则)
优点:
代价:比 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,而不是“更均匀”。
下面是一份 无依赖、可直接 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):