Bluetooth Low Energy
Bluetooth Low Energy (BLE)
Этот пример демонстрирует реализацию уровня канала Bluetooth Low Energy (BLE) на языке Julia – от формирования кадра до его приёма с проверкой целостности. Код моделирует ключевые этапы работы протокола: упаковку полезных данных в кадр, добавление служебных полей, синхронизацию по преамбуле и контроль ошибок с помощью циклического избыточного кода.
Используемые технологии и методы
- LSB‑first – битовое представление с младшим битом впереди, как в спецификации BLE.
- CRC‑24 – полином
0x00065Bс начальным значением0x555555, обеспечивающий обнаружение ошибок в кадре. - Корреляционный поиск преамбулы – приёмник находит начало кадра, вычисляя максимальное скалярное произведение битового потока с эталонной последовательностью
0xAA. - Дублирование длины – для повышения надёжности длина полезных данных передаётся дважды (в заголовке и отдельном поле).
Что показывает пример
- Корректный приём в отсутствие искажений – CRC сходится, данные восстанавливаются.
- Обнаружение ошибок – случайное инвертирование одного или двух битов приводит к несовпадению CRC, и пакет отбрасывается.
- Коллизию CRC‑24 – специально подобранное изменение одного бита во втором байте данных (
0x65 → 0xE5) не меняет контрольную сумму. Это иллюстрирует, что даже хороший CRC не даёт 100% гарантии – вероятность такого совпадения крайне мала (≈6·10⁻⁸), но она существует. - Работоспособность на реальных данных – полезная нагрузка "Hello" (байты
48 65 6C 6C 6F) используется во всех тестах.
Реализация
Первые две функции выполняют взаимное преобразование между байтами и битами в LSB-first порядке (младший бит идёт первым), как того требует спецификация BLE.
bytes_to_bits– раскладывает каждый байт на 8 отдельных битов, начиная с младшего (бит 0). Результат – плоский массив битов (0 или 1).bits_to_bytes– выполняет обратное действие: группирует биты по 8, собирая их в байты (также младший бит становится первым в группе).
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
Следующая функция вычисляет CRC‑24 по стандарту BLE:
- Использует полином
0x00065Bи начальное значение0x555555. - Для каждого байта данных: сдвигает его в регистр CRC, затем 8 раз выполняет сдвиг влево с условным XOR полиномом (если установлен старший бит).
- Возвращает 24‑битную контрольную сумму, которая добавляется в конец кадра и проверяется при приёме для обнаружения ошибок.
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
Функция build_frame формирует кадр BLE из полезных данных:
- Добавляет преамбулу
0xAA(8 бит) для синхронизации. - Упаковывает длину данных в заголовок (16 бит) и отдельное поле длины (8 бит) – для надёжности.
- Преобразует полезные данные в биты.
- Вычисляет CRC‑24 по заголовку, длине и данным (little‑endian) и добавляет его (24 бита) в конец.
- Возвращает собранный битовый вектор – готовый кадр для передачи.
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
Функция find_preamble ищет преамбулу в битовом потоке методом корреляции:
- Скользящим окном сравнивает фрагмент потока с эталонной преамбулой (
preamble). - Вычисляет скалярное произведение (сумму произведений битов) – чем больше значение, тем лучше совпадение.
- Возвращает индекс начала наилучшего совпадения.
Так приёмник определяет, где начинается кадр, и может правильно извлечь служебные поля и данные.
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
Функция receive_frame принимает и декодирует кадр BLE из битового потока:
- Находит преамбулу, чтобы определить начало кадра.
- Извлекает длину из заголовка и отдельного поля, проверяет их совпадение.
- Читает полезные данные и CRC (24 бита).
- Пересчитывает CRC по заголовку, длине и данным, сравнивает с принятым.
- Возвращает извлечённые данные и флаг успеха (true, если CRC совпал, иначе пакет считается повреждённым).
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
Итоговый тест демонстрирует работу уровня канала BLE на примере передачи строки "Hello" (байты 48 65 6C 6C 6F). Он последовательно проверяет четыре сценария:
- Без ошибок – кадр формируется, передаётся без искажений, приёмник извлекает данные, CRC сходится – пакет принят.
- Одна случайная ошибка – инвертируется случайный бит в кадре. В большинстве случаев CRC обнаруживает искажение и отбрасывает пакет, что подтверждает его способность детектировать одиночные ошибки.
- Специальная ошибка (бит #48) – инвертируется бит, который меняет второй байт данных с
0x65на0xE5. Это редкий случай коллизии CRC‑24, когда контрольная сумма остаётся неизменной, и пакет ошибочно принимается. Тест наглядно показывает, что CRC не даёт 100% гарантии, но вероятность такого совпадения крайне мала (≈6·10⁻⁸). - Две случайные ошибки – инвертируются два случайных бита. CRC почти всегда обнаруживает такие искажения, и пакет отбрасывается (как и в тесте 2).
Тест охватывает как штатный режим работы, так и редкие пограничные случаи, подтверждая корректность реализации и одновременно демонстрируя её ограничения.
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, пакет отброшен")
Тесты наглядно демонстрируют работу уровня канала BLE с контролем целостности через CRC‑24:
-
Без ошибок – кадр принимается корректно, данные извлекаются без искажений. CRC совпадает, пакет проходит проверку.
-
Одна случайная ошибка (в данном случае бит #96, который относится к полю CRC) – ошибка обнаружена, пакет отброшен. Это подтверждает, что CRC‑24 эффективно детектирует одиночные искажения в большинстве случаев.
-
Специальная ошибка (бит #48, меняет второй байт данных с
0x65на0xE5) – пакет ошибочно принят, так как CRC не изменился. Это редкий случай коллизии CRC‑24: два разных сообщения дают одинаковую контрольную сумму. Вероятность такого события крайне мала (~2⁻²⁴ ≈ 6·10⁻⁸), но она существует, и тест это демонстрирует. -
Две случайные ошибки (биты #92 и #37) – CRC снова обнаруживает искажения, пакет отброшен. Это показывает, что для большинства комбинаций двух ошибок CRC также работает надёжно.
Вывод
Разработанная модель реализует базовую логику формирования и приёма кадров BLE: синхронизацию по преамбуле, извлечение данных и проверку целостности CRC‑24. При этом модель является учебной и содержит упрощения: опущен Access Address (4 байта), преамбула всегда равна 0xAA (в реальности зависит от адреса), а длина полезной нагрузки дублируется для надёжности (в стандарте длина указывается однократно в заголовке PDU). CRC‑24 обеспечивает высокую надёжность, но не абсолютную – найденная коллизия иллюстрирует математическое ограничение циклических кодов. Пример полезен для изучения принципов канального уровня и может служить основой для дальнейших расширений (скремблирование, помехоустойчивое кодирование и т.д.).