Документация Engee
Notebook

Bluetooth Low Energy (BLE)

Этот пример демонстрирует реализацию уровня канала Bluetooth Low Energy (BLE) на языке Julia – от формирования кадра до его приёма с проверкой целостности. Код моделирует ключевые этапы работы протокола: упаковку полезных данных в кадр, добавление служебных полей, синхронизацию по преамбуле и контроль ошибок с помощью циклического избыточного кода.

Используемые технологии и методы

  • 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) используется во всех тестах.

Реализация

Первые две функции выполняют взаимное преобразование между байтами и битами в LSB-first порядке (младший бит идёт первым), как того требует спецификация BLE.

  • 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)

Следующая функция вычисляет CRC‑24 по стандарту BLE:

  • Использует полином 0x00065B и начальное значение 0x555555.
  • Для каждого байта данных: сдвигает его в регистр CRC, затем 8 раз выполняет сдвиг влево с условным XOR полиномом (если установлен старший бит).
  • Возвращает 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 бит) – для надёжности.
  • Преобразует полезные данные в биты.
  • Вычисляет CRC‑24 по заголовку, длине и данным (little‑endian) и добавляет его (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 по заголовку, длине и данным, сравнивает с принятым.
  • Возвращает извлечённые данные и флаг успеха (true, если CRC совпал, иначе пакет считается повреждённым).
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)

Итоговый тест демонстрирует работу уровня канала BLE на примере передачи строки "Hello" (байты 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), " "))" : "[FAIL] Ошибка CRC")
println()
noisy = copy(frame)
idx = rand(1:length(noisy))
noisy[idx] = 1 - noisy[idx]
received, ok = receive_frame(noisy)
println("Одна случайная ошибка (бит #$idx) -> ", ok ? "[OK] Принято (ошибка не обнаружена)" : "[FAIL] Ошибка 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 6c 6c 6f)")
else
    println("[FAIL] Ошибка 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] Принято (ошибки не обнаружены)" : "[FAIL] Ошибка CRC, пакет отброшен")
Исходные данные: 48 65 6c 6c 6f
Длина кадра: 96 бит

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

Одна случайная ошибка (бит #48) -> [OK] Принято (ошибка не обнаружена)

Специальная ошибка (бит #48) -> [OK] Принято (CRC не изменился!) – это коллизия CRC-24
   Изменённые данные: 48 e5 6c 6c 6f
   (Исходные данные: 48 65 6c 6c 6f)

Две случайные ошибки (биты #8, 31) -> [FAIL] Ошибка CRC, пакет отброшен

Тесты наглядно демонстрируют работу уровня канала BLE с контролем целостности через CRC‑24:

  1. Без ошибок – кадр принимается корректно, данные извлекаются без искажений. CRC совпадает, пакет проходит проверку.

  2. Одна случайная ошибка (в данном случае бит #96, который относится к полю CRC) – ошибка обнаружена, пакет отброшен. Это подтверждает, что CRC‑24 эффективно детектирует одиночные искажения в большинстве случаев.

  3. Специальная ошибка (бит #48, меняет второй байт данных с 0x65 на 0xE5) – пакет ошибочно принят, так как CRC не изменился. Это редкий случай коллизии CRC‑24: два разных сообщения дают одинаковую контрольную сумму. Вероятность такого события крайне мала (~2⁻²⁴ ≈ 6·10⁻⁸), но она существует, и тест это демонстрирует.

  4. Две случайные ошибки (биты #92 и #37) – CRC снова обнаруживает искажения, пакет отброшен. Это показывает, что для большинства комбинаций двух ошибок CRC также работает надёжно.

Вывод

Разработанная модель реализует базовую логику формирования и приёма кадров BLE: синхронизацию по преамбуле, извлечение данных и проверку целостности CRC‑24. При этом модель является учебной и содержит упрощения: опущен Access Address (4 байта), преамбула всегда равна 0xAA (в реальности зависит от адреса), а длина полезной нагрузки дублируется для надёжности (в стандарте длина указывается однократно в заголовке PDU). CRC‑24 обеспечивает высокую надёжность, но не абсолютную – найденная коллизия иллюстрирует математическое ограничение циклических кодов. Пример полезен для изучения принципов канального уровня и может служить основой для дальнейших расширений (скремблирование, помехоустойчивое кодирование и т.д.).