AnyMath 文档
Notebook

低功耗蓝牙(ble)

此示例演示了Julia语言中蓝牙低功耗(Ble)通道级别的实现–从帧生成到通过完整性验证接收。 该代码模拟了协议的关键阶段:将有用数据打包到帧中,添加服务字段,通过前导码进行同步,以及使用循环冗余码进行错误控制。

使用的技术和方法

-LSB-first是一个位表示,最低有效位在前面,如BLE规范中所示。
-CRC-24-多项式 0x00065B 具有初始值 0x555555,其在帧中提供错误检测。
-前导码的相关性搜索-接收机通过计算比特流与参考序列的最大点积来查找帧的开始 0xAA.
-长度重复-为了增加可靠性,有效载荷的长度被传输两次(在报头和单独的字段中)。

示例显示了什么

  1. 正确的接收在没有失真-CRC收敛,数据被恢复。
  2. 错误检测-一个或两个位的意外反转导致CRC不匹配,并且数据包被丢弃。
  3. CRC-24碰撞-在数据的第二字节的一个位的特别选择的变化(0x65 → 0xE5)不改变校验和。 这说明即使是一个好的CRC也不能提供100%的保证–这种匹配的概率非常低(≈6·10⁻),但它存在。
  4. 在真实数据上的性能-有效载荷"Hello"(字节 48 65 6C 6C 6F)在所有测试中使用。

实现;实现

前两个函数按照BLE规范的要求,以LSB-first顺序执行字节和位之间的相互转换(最低位首先)。

  • bytes_to_bits -将每个字节分解为8个独立的位,从最低(位0)开始。 结果是位(0或1)的平面数组。
  • bits_to_bytes -执行相反的操作:将位按8分组,将它们收集成字节(也是最低位成为组中的第一位)。
In [ ]:
function bytes_to_bits(data::Vector{UInt8})::Vector{Int}
    bits = Int[]
    for byte in data
        for i in 0:7
            push!(bits, (byte >> i) & 1)
        end
    end
    return bits
end

function bits_to_bytes(bits::Vector{Int})::Vector{UInt8}
    bytes = UInt8[]
    for i in 1:8:length(bits)
        byte = 0
        for j in 0:7
            if i+j <= length(bits)
                byte |= bits[i+j] << j
            end
        end
        push!(bytes, byte)
    end
    return bytes
end
Out[0]:
bits_to_bytes (generic function with 1 method)

以下函数根据BLE标准计算CRC‑24:

-使用多项式 0x00065B 和初始值 0x555555.
-对于数据的每个字节:将其移位到CRC寄存器,然后用条件异或多项式执行8次左移(如果设置了最高位)。
-返回一个24位校验和,它被添加到帧的末尾,并在接收时检查以检测错误。

In [ ]:
function crc24_ble(data::Vector{UInt8})::UInt32
    poly = 0x00065B
    crc  = 0x555555
    for byte in data
        crc ⊻= UInt32(byte) << 16
        for _ in 1:8
            crc <<= 1
            if (crc & 0x800000) != 0
                crc ⊻= poly
            end
            crc &= 0xFFFFFF
        end
    end
    return crc
end
Out[0]:
crc24_ble (generic function with 1 method)

功能 build_frame 从有用数据生成BLE帧:

-增加序言 0xAA (8位)进行同步。
-将数据的长度打包成一个报头(16位)和一个单独的长度字段(8位)以保证可靠性。
-将有用的数据转换为比特。
‑从标头,长度和数据(little‑endian)计算CRC-24,并将其(24位)添加到末尾。
-返回组装的比特向量,该比特向量是用于传输的准备帧。

In [ ]:
function build_frame(payload::Vector{UInt8})::Vector{Int}
    preamble = [1, 0, 1, 0, 1, 0, 1, 0]
    len = UInt16(length(payload))
    header = [Int((len >> i) & 1) for i in 0:15]
    len_byte = UInt8(length(payload))
    len_bits = [Int((len_byte >> i) & 1) for i in 0:7]
    data_bits = bytes_to_bits(payload)
    crc_input = vcat(
        [UInt8(len & 0xFF), UInt8((len >> 8) & 0xFF)],
        [len_byte],
        payload
    )
    crc_val = crc24_ble(crc_input)
    crc_bits = [Int((crc_val >> i) & 1) for i in 0:23]
    return vcat(preamble, header, len_bits, data_bits, crc_bits)
end
Out[0]:
build_frame (generic function with 1 method)

功能 find_preamble 通过相关方法搜索比特流中的前导码:

-滑动窗口将流的一个片段与参考前导码进行比较(preamble).
-计算标量积(位的乘积之和)-值越大,匹配越好。
-返回最佳匹配开始的索引。

这是接收机如何确定帧开始的位置并且能够正确地提取服务字段和数据。

In [ ]:
function find_preamble(stream::Vector{Int}, preamble::Vector{Int})::Int
    best_idx = 1
    best_corr = -1
    for i in 1:(length(stream) - length(preamble) + 1)
        corr = sum(stream[i+j-1] * preamble[j] for j in 1:length(preamble))
        if corr > best_corr
            best_corr = corr
            best_idx = i
        end
    end
    return best_idx
end
Out[0]:
find_preamble (generic function with 1 method)

功能 receive_frame 接受并解码来自比特流的BLE帧:

-查找前导码以确定帧的开始。
-从标题和一个单独的字段中提取长度,检查它们的匹配。
-读取有效载荷和CRC(24位)。
-通过标头,长度和数据重新计算CRC,并将其与接受的CRC进行比较。
-返回提取的数据和成功标志(如果CRC匹配为true,否则认为数据包已损坏)。

In [ ]:
function receive_frame(stream::Vector{Int})::Tuple{Vector{UInt8}, Bool}
    preamble = [1, 0, 1, 0, 1, 0, 1, 0]
    start = find_preamble(stream, preamble)
    if start == -1
        return UInt8[], false
    end
    pos = start + length(preamble)
    if pos + 15 > length(stream)
        return UInt8[], false
    end
    len = 0
    for i in 0:15
        len |= stream[pos+i] << i
    end
    pos += 16
    if pos + 7 > length(stream)
        return UInt8[], false
    end
    len_from_byte = 0
    for i in 0:7
        len_from_byte |= stream[pos+i] << i
    end
    pos += 8
    if len != len_from_byte
        return UInt8[], false
    end
    if pos + len*8 - 1 > length(stream)
        return UInt8[], false
    end
    data_bits = stream[pos : pos + len*8 - 1]
    pos += len*8
    data = bits_to_bytes(data_bits)
    if pos + 23 > length(stream)
        return UInt8[], false
    end
    crc_recv = 0
    for i in 0:23
        crc_recv |= stream[pos+i] << i
    end
    crc_input = vcat(
        [UInt8(len & 0xFF), UInt8((len >> 8) & 0xFF)],
        [UInt8(len)],
        data
    )
    crc_calc = crc24_ble(crc_input)
    return data, (crc_calc == crc_recv)
end
Out[0]:
receive_frame (generic function with 1 method)

最后的测试使用传输字符串"Hello"(字节)的示例演示了BLE通道层的操作 48 65 6C 6C 6F). 它按顺序检查四种情况:

  1. 无差错-帧形成,传输无失真,接收器提取数据,CRC收敛-接收数据包。
  2. 一个随机错误-帧中的一个随机位被反转。 在大多数情况下,CRC检测失真并丢弃数据包,这证实了其检测单个错误的能力。
  3. 特殊错误(位#48)-位被反转,这改变了数据的第二个字节从 0x65 上 0xE5. 这是CRC‑24碰撞的一个罕见的情况,当校验和保持不变并且数据包被错误地接收时。 测试清楚地表明,CRC不提供100%的保证,但这种匹配的概率极低(≈6·10⁻⁸)。
  4. 两个随机错误-两个随机位被反转。 CRC几乎总是检测到这样的失真,并且分组被丢弃(如在测试2中)。

该测试既包括常规操作,也包括罕见的边缘情况,证实了实施的正确性,并证明了其局限性。

In [ ]:
payload = UInt8[0x48, 0x65, 0x6C, 0x6C, 0x6F]
frame = build_frame(payload)
println("初始数据: ", join(string.(payload, base=16, pad=2), " "))
println("Длина кадра: $(length(frame)) бит\n")
received, ok = receive_frame(frame)
println("没有错误-> ", ok ? "[OK] Принято: $(join(string.(received, base=16, pad=2), " "))" : "[失败]CRC错误")
println()
noisy = copy(frame)
idx = rand(1:length(noisy))
noisy[idx] = 1 - noisy[idx]
received, ok = receive_frame(noisy)
println("Одна случайная ошибка (бит #$idx) -> ", ok ? "[确定]已接受(未检测到错误)" : "[失败]CRC错误,丢包")
println()
noisy = copy(frame)
noisy[48] = 1 - noisy[48]
received, ok = receive_frame(noisy)
print("特殊错误(位#48)-> ")
if ok
    println("[OK]接受(CRC没有改变!)-这是CRC-24碰撞")
    println("   更改数据: ", join(string.(received, base=16, pad=2), " "))
    println("   (初始数据:48 65 6c6c6f)")
else
    println("[失败]CRC错误(丢包)")
end
println()
noisy = copy(frame)
idxs = rand(1:length(noisy), 2)
for i in idxs
    noisy[i] = 1 - noisy[i]
end
received, ok = receive_frame(noisy)
println("两个随机错误(位 #", join(idxs, ", "), ") -> ", ok ? "[OK]接受(没有发现错误)" : "[失败]CRC错误,丢包")
Исходные данные: 48 65 6c 6c 6f
Длина кадра: 96 бит

Без ошибок -> [OK] Принято: 48 65 6c 6c 6f

Одна случайная ошибка (бит # 48)->[确定]接受(未检测到错误)

Специальная ошибка (бит # 48)->[确定]接受(CRC没有改变!)-这是CRC-24碰撞
   Изменённые данные: 48 e5 6c 6c 6f
   (Исходные данные: 48 65 6c 6c 6f)

Две случайные ошибки (биты # 8,31)->[失败]CRC错误,丢包

测试清楚地证明了通过CRC‑24进行完整性控制的BLE通道级别的操作:

  1. 无错误-帧被正确接收,数据被提取而不失真。 CRC匹配,包通过验证。

  2. 一个随机错误(在这种情况下,位#96,它指的是CRC字段)-检测到错误,丢弃数据包。 这证实了CRC-24在大多数情况下有效地检测到单个失真。

  3. 特殊错误(位#48,从改变数据的第二个字节 0x65 上 0xE5)-由于CRC没有改变,包裹被错误地接受。 这是CRC‑24碰撞的罕见情况:两个不同的消息给出相同的校验和。 这种事件的概率极低(~2⁻2⁴ ≈6*10⁻⁸),但它存在,测试证明了这一点。

  4. 两个随机错误(位#92和#37)-CRC再次检测失真,数据包被丢弃. 这表明,对于两个错误的大多数组合,CRC也可靠地工作。

结论

开发的模型实现了BLE帧生成和接收的基本逻辑:通过前导同步,数据提取和CRC‑24完整性验证。 与此同时,该模型是教育性的,并包含简化:访问地址被省略(4字节),前导码总是等于 0xAA (在现实中,它取决于地址),并且有效载荷的长度被重复用于可靠性(在标准中,长度在PDU报头中指定一次)。 CRC-24提供了高可靠性,但不是绝对的–发现的碰撞说明了循环码的数学限制。 该示例对于研究信道层的原理是有用的,并且可以作为进一步扩展(加扰、噪声容忍编码等)的基础。).