DMR: формирование LC и A–E пакетов.¶
Цифровой мобильный радиопротокол (DMR) остаётся стандартом для надёжной профессиональной связи. Моделирование его поведения важно для тестирования и анализа. В исходном примере уже заложена базовая логика формирования данных и синхронизации. Однако она не включала детальную обработку логики передачи управляющих каналов и типов пакетов.
В этой доработанной версии мы добавили следующие пункты.
Полноценную логику формирования LC‑пакетов(Logical Channel Packets).
Корректное формирование пакетов типов A, B, C, D и E: это пакеты данных, которые обрамляются служебными заголовками, сигнальными полями, включают синхропоследовательность (А) или краткую форму LC (B,C,D,E), а также имеют в себе контрольные биты.
Ещё здесь были усовершенствованы механизмы кадровой синхронизации потока с использованием xcorr.
Такое расширение подходит для полноценного анализа работы протокола, симуляции временных задержек и тестирования на реальном оборудовании.
Реализацию данного проекта начнём с анализа написанной функции формирования пакетов и тестирования этой функции в скриптах для последующего её переноса в модель при помощи блока Engee function.
Для начала подключим файл со вспомогательными функциями. Каждая функция выполняет специфическую задачу в рамках обработки данных, связанной с кодированием, проверкой ошибок или формированием структуры сообщения. Ниже представлен список функций с кратким описанием их назначения.
de2bi(x, n)
Конвертирует десятичное числоx
в бинарный массив длиныn
(младшие биты справа).bi2de(bits)
Конвертирует бинарный массивbits
в десятичное число (интерпретируя его как битовую строку).encode(msg)
Вычисляет контрольные биты (CRC) для сообщенияmsg
с использованием полиномаPOLY
и алгоритма кодирования Рида-Соломона.lc_header_mask(parity)
Применяет маскуSTART_MASK
к битам четностиparity
с помощью операции XOR.log_mult(a, b)
Выполняет умножение в поле Галуа (GF(256)) с использованием логарифмических таблицLOG_TABLE
иEXP_TABLE
.CS5bit(LCcrc)
Вычисляет 5-битную контрольную сумму для 72-битного блока данныхLCcrc
(суммирует байты и берет остаток от деления на 31).HemR(l)
Вычисляет горизонтальные (по строкам) проверочные биты Хэмминга для матрицыl
размером 9×11.HemC(l, HR)
Вычисляет вертикальные (по столбцам) проверочные биты Хэмминга для матрицыl
с учетом горизонтальных проверокHR
.typegen(CC)
Генерирует 20-битный тип сообщения на основе 4-битного кодаCC
, добавляя контрольную сумму из таблицыENCODE_2087
.
path = "$(@__DIR__)/dmr_lib.jl"
println("Путь до библиотеки: $path")
include(path)
Далее представленная реализованная нами функция Gen_Pkg
обрабатывает входные параметры, генерирует и возвращает данные для передачи в виде битовых последовательностей, управляя их отправкой,
формирует кадры данных для передачи в системе связи. Далее представлены основные этапы работы функции.
Формирование служебных полей (FLCO, FID);
Обработка адресов;
Кодирование и контрольные суммы;
Подготовка данных для помехоустойчивого кодирования;
Формирование кадров передачи;
Управление последовательностью передачи;
Возврат готового пакета данных или LC-блока.
# Инициализация глобальных переменных для отслеживания состояния
global E = 0 # Счетчик кадров
global l_block = 0 # Текущий блок данных
function Gen_Pkg(block, AFLCO, BFID, Switch, AdrP, AdrI, Ecstro, Shiroko, OVCM, Pr, ELC, CC)
global E, l_block
# 1. Формирование FLCO (Functional Logical Channel Organization)
FLCO = get(Dict(
1 => [0,0,0,0,0,0], # Тип 1
2 => [0,0,0,0,1,1], # Тип 2
3 => [0,0,0,1,0,0], # Тип 3
4 => [0,0,0,1,0,1], # Тип 4
5 => [0,0,0,1,1,0], # Тип 5
6 => [0,0,0,1,1,1], # Тип 6
7 => [0,0,1,0,0,0] # Тип 7
), AFLCO, zeros(Int,6)) # По умолчанию - нули
# 2. Формирование FID (Feature ID) в зависимости от Switch
FID = Switch == 0 ? get(Dict(
1 => [0,0,0,0,0,0,0,0], # Профиль 1
2 => [0,0,0,0,0,1,0,0], # Профиль 2
3 => [0,1,1,1,1,1,1,1] # Профиль 3
), BFID, zeros(Int,8)) : zeros(Int,8) # Если Switch=1 - нули
# 3. Преобразование адресов в битовые векторы
AdrP_vec = (AdrP == 0) ? de2bi(1, 24) : de2bi(AdrP, 24) # Адрес получателя
AdrI_vec = (AdrI == 0) ? de2bi(1234, 24) : de2bi(AdrI, 24) # Адрес источника
# 4. Формирование полного LC-блока (72 бита)
FullLC = vcat([0, 0], FLCO, FID, [Ecstro], [0, 0, 0], [Shiroko], [OVCM],
de2bi(Pr - 1, 2), AdrP_vec, AdrI_vec)
# 5. Кодирование и вычисление контрольных сумм
FullLCdec = [bi2de(FullLC[i:i+7]) for i in 1:8:72] # Разбивка на байты
parity = encode(FullLCdec) # Кодирование Рида-Соломона
CRC = lc_header_mask(parity) # Применение маски к контрольной сумме
LCcrcDec = vcat(FullLCdec[1:9], CRC) # Объединение данных и CRC
LCcrc = vcat([reverse(digits(b, base=2, pad=8)) for b in LCcrcDec]...) # В биты
# 6. Подготовка данных для BPTC (Block Product Turbo Code)
R = [0, 0, 0, 0] # Резервные биты
I = vcat(reverse(R[1:3]), LCcrc) # Формирование информационного блока
l = reshape(I[1:99], 11, 9)' # Матрица 9x11
CS = CS5bit(LCcrc) # 5-битная контрольная сумма
HR = HemR(l) # Горизонтальные проверки Хэмминга
HC = HemC(l, HR) # Вертикальные проверки Хэмминга
type20bit = typegen(CC) # Генерация 20-битного типа сообщения
# 7. Управление передачей кадров
E = E == 0 ? ELC * 12 + 1 : E + 1 # Обновление счетчика кадров
Enabled = mod(E, 2) == 0 ? 1 : 0 # Флаг активности передачи
LCs = 0 # Флаг LC-блока
LC_next = false # Флаг следующего LC-блока
# Логика управления передачей
if E == ELC * 12 + 1
LC_next = true
elseif E == ELC * 12 + 2
Enabled = 0
LCs = 1
elseif E == ELC * 12 + 3
E = 1
Enabled = 0
end
# 8. Формирование LC-блока (288 бит)
LC_block = zeros(Int, 288)
LC_block[121:168] = [1,1,0,1,0,1,0,1,1,1,0,1,0,1,1,1,1,1,1,1,0,1,1,1,0,1,1,1,1,1,1,1,1,1,0,1,0,1,1,1,0,1,0,1,0,1,1,1] # Синхропоследовательность
LC_block[111:120] = type20bit[1:10] # Первые 10 бит типа
LC_block[169:178] = type20bit[11:20] # Последние 10 бит типа
# 9. Формирование BPTC матрицы (13x15)
BPTC = zeros(Int, 13, 15)
BPTC[1:9, 1:11] .= l # Основные данные
BPTC[10:13, 1:15] .= HC # Вертикальные проверки
BPTC[1:9, 12:15] .= HR # Горизонтальные проверки
BPTCl = vec(permutedims(BPTC)) # Преобразование в вектор
# 10. Перемежение данных
LCper = zeros(Int, 195)
for i in 1:195
idx = mod(i * 181, 196)
idx == 0 && (idx = 196)
LCper[idx] = BPTCl[i] # Алгоритм перемежения
end
# 11. Заполнение LC-блока
LC_block[14:110] .= LCper[1:97] # Первая часть данных
LC_block[179:276] .= LCper[98:195] # Вторая часть данных
LC_block[13] = 0 # Резервный бит
# 12. Формирование быстрых данных
FullLC = LCcrc[1:72]
CC = reverse([1, 0, 0, 0]) # Код коррекции
QR = [1, 0, 0, 0, 1, 0, 1, 1, 1] # Быстрые данные
Pi = 0 # Флаг PI
QR_rev = reverse(QR[1:8]) # Обратные быстрые данные
# 13. Формирование BPTC для быстрых данных (8x16)
BPTC = zeros(Int, 8, 16)
BPTC[1:2, 1:11] = reshape(reverse(FullLC[51:72]), 2, 11) # Часть данных
BPTC[3:7, 1:10] = reshape(reverse(FullLC[1:50]), 5, 10) # Основные данные
BPTC[1:7, 12:16] = [1 0 1 1 1; 0 0 0 0 0; 0 1 0 0 0; 0 0 0 0 0; 1 1 1 1 0; 0 0 0 0 0; 0 0 0 0 1] # Контрольные биты
BPTC[8, :] = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1] # Чередующиеся биты
BPTC[3:7, 11] = CS # Контрольная сумма
# 14. Формирование битового блока из входных данных
bit_block = zeros(Int, 288)
for i in 1:27
bits = reverse(digits(block[i], base=2, pad=8)) # Байт в биты
if i < 14
bit_block[(1:8) .+ ((i-1)*8) .+ 12] = bits # Первые 13 байт
elseif i == 14
bit_block[117:120] = bits[1:4] # Особый случай для 14-го байта
bit_block[169:172] = bits[5:8]
else
bit_block[(1:8) .+ ((i-1)*8) .+ 12 .+ 48] = bits # Остальные байты
end
end
# 15. Формирование кадров данных
DataFrames = [
# Синхрокадр
vcat(zeros(120), [0,1,1,1,0,1,0,1,1,0,0,1,0,0,0,1,1,0,1,1,1,0,1,1,0,1,0,1,0,0,1,1,0,0,0,0,1,0,1,0,0,1,1,1,1,1,0,0], zeros(120)),
# Кадры данных 1-4
vcat(zeros(120), CC, [Pi], [0,1], [QR[9]], vec(BPTC[:, 1:4]), QR_rev, zeros(120)),
vcat(zeros(120), CC, [Pi], [1,1], [QR[9]], vec(BPTC[:, 5:8]), QR_rev, zeros(120)),
vcat(zeros(120), CC, [Pi], [1,1], [QR[9]], vec(BPTC[:, 9:12]), QR_rev, zeros(120)),
vcat(zeros(120), CC, [Pi], [1,0], [QR[9]], vec(BPTC[:, 13:16]), QR_rev, zeros(120))
]
# 16. Формирование итогового пакета
package = zeros(Int, 288)
if Enabled == 1
l_block += 1
package = copy(DataFrames[l_block])
package[13:120] .= bit_block[13:120] # Данные первой части
package[169:276] .= bit_block[169:276] # Данные второй части
if l_block == 5
l_block = 0 # Сброс счетчика блоков
end
end
# Возврат LC-блока или пакета данных и флага LC_next
return LCs == 1 ? copy(LC_block) : package, LC_next
end
Далее выполним тестирование описанной функции. Тест состоит из трёх основных частей.
Подготовка данных:
Создается тестовый массив из 2670 байт (равных 1)
Входной массив добавляется нулевыми байтами до размерности, кратной 27 (размеру блока)
Разбиение на блоки и обработка:
Разделяет данные на блоки по 27 байт.
Для каждого блока вызывает функцию
Gen_Pkg()
, которая формирует кадр данных, тем самым мы по сути проверяем работу нашей функции в условиях симуляции.
Анализ результатов:
Собирает все сгенерированные битовые блоки
Вычисляет сумму битов в каждом блоке (для проверки)
Фильтрует и выводит только ненулевые суммы (каждый второй кадр отбрасываем)
bytes = Int.(ones(2670))
remainder = length(bytes) % 27
bytes = vcat(bytes, remainder == 0 ? Int[] : zeros(Int, 27 - remainder))
bit_blocks = Vector{Vector{Int}}()
buffer = Int[]
pending_block = nothing
i = 1
while i <= length(bytes)
block = bytes[i:min(i+26, length(bytes))]
data_bits, LC_next = Gen_Pkg(block, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2, [0, 0, 0, 1])
push!(bit_blocks, data_bits)
i += 27
end
sum_bit = sum.(bit_blocks)
filtered_vector = filter(x -> x != 0, sum_bit)
println(filtered_vector)
Теперь проверим, с правильной ли периодичностью встречается LC-пакет. Исходя из настройки ELC = 2, мы его встретим раз в 2 суперфрейма, то есть каждый тринадцатый кадр должен быть равен 104.
indices = ((findall(x -> x == 104, filtered_vector)).-1)/13
Теперь удалим LC и проверим, чередуются ли остальные кадры.
# Удаляем все 104(LC)
filtered_vector = filter(x -> x != 104, filtered_vector)
# Искомая комбинация
pattern = [52, 41, 38, 41, 44] # A, B, C, D, E
function check_pattern(vec, pat)
# Обрезаем вектор до ближайшей подходящей длины
pattern_length = length(pat)
suitable_length = div(length(vec), pattern_length) * pattern_length
trimmed_vec = vec[1:suitable_length]
# Проверяем соответствие шаблону
for i in 1:pattern_length:length(trimmed_vec)
if trimmed_vec[i:i+pattern_length-1] != pat
return false
end
end
return true
end
# Проверяем filtered_vector
is_correct = check_pattern(filtered_vector, pattern)
println("Повторяется ли комбинация $pattern? ", is_correct ? "Да" : "Нет")
Как мы могли убедиться, функция работает корректно. Теперь проведём тестирования в моделях.
Запустим модель, для которой мы сформировали обвязку для нашей функции.
# Подключение вспомогательной функции запуска модели.
function run_model( name_model)
Path = (@__DIR__) * "/" * name_model * ".engee"
if name_model in [m.name for m in engee.get_all_models()] # Проверка условия загрузки модели в ядро
model = engee.open( name_model ) # Открыть модель
model_output = engee.run( model, verbose=true ); # Запустить модель
else
model = engee.load( Path, force=true ) # Загрузить модель
model_output = engee.run( model, verbose=true ); # Запустить модель
engee.close( name_model, force=true ); # Закрыть модель
end
sleep(0.1)
return model_output
end
Исходя из графиков ниже мы видим, что, во-первых, сигнал управления LC_next предупреждает нас о том, что следующий кадр это LC отрабатывает корректно, а, во-вторых, мы видим, что в модели и в скрипте функция ведёт себя идентично. Плюсом для удобства определения параметров пакета в Engee Function мы используем объявление параметров, тем самым уменьшаем количество входных портов блока.
display(run_model("test_new_function"))
gr()
display(plot(vcat(collect(simout["test_new_function/Переключатель.1"]).value...)))
plot(sum_bit, seriestype = :steppre, linewidth=3, legend = false)
plot!(vcat(collect(simout["test_new_function/Сумма элементов.1"]).value...), seriestype = :steppre, linewidth=3, legend = false)
Теперь, когда мы протестировали саму функцию, можно переходить к усовершенствованию нашей системной модели DMR.
Как мы видим, из скриншота выше, блок Gen_Pkg
управляет входом данных, внутри блока DMR Physical Layer
находится логика, описанная нами в предыдущей версии этого примера, а в блоке Frame_Synchronization
расположены две функции, которые как раз выполняют кадровую синхронизацию нашего потока. Рассмотрим эти блоки, начнём с Xcorr
.
# Глобальная переменная для хранения задержки
global Delay = 0
# Основная функция обработки блока данных
function (c::Block)(t::Real, BitSignal)
global Delay
US = false # Флаг успешной синхронизации
# Эталонная синхропоследовательность (48 бит)
Data_Sync = [-1, 1, 1, 1, -1, 1, -1, 1, 1, -1, -1, 1, -1, -1, -1, 1,
1, -1, 1, 1, 1, -1, 1, 1, -1, 1, -1, 1, -1, -1, 1, 1,
-1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, 1, -1, -1]
# Вычисление взаимной корреляции между входным сигналом и синхропоследовательностью
MS = xcorr_simple((2*BitSignal.-1), Data_Sync)
# Поиск пиков корреляции (порог > 46)
Peak = findall(MS .> 46)
if !isempty(Peak)
P = Peak[1] # Берем первый значимый пик
# Проверяем, находится ли пик в допустимом диапазоне
if P > 120 && P < 456
# Вычисляем задержку в зависимости от положения пика
if P > 288
Delay = P-408
else
Delay = P-120
end
end
US = true # Устанавливаем флаг успешной синхронизации
end
D = Delay # Текущее значение задержки
return US, D, MS # Возвращаем статус синхронизации, задержку и массив корреляции
end
Теперь перейдём к блоку Selector
.
# Глобальные переменные для управления состоянием обработки:
global U = false # Флаг активности обработки сигнала
global j = 0 # Счетчик временных интервалов
# Основная функция обработки блока данных
function (c::Block)(t::Real, BitSignal, US, Delay)
global U, j # Доступ к глобальным переменным
# Инициализация выходного сигнала (216 бит)
Sig = zeros(216)
# Флаг разрешения обработки
Enable = false
# Обработка сигнала синхронизации (US):
# Если получен сигнал синхронизации (US != 0), сбрасываем состояние
US != 0 ? (U = US; j = 0) : nothing
# Если обработка активна (U == 1)
if U == 1
j += 1 # Увеличиваем счетчик
# Обрабатываем только нечетные интервалы
if isodd(j)
# Формируем выходной сигнал, выбирая биты из BitSignal с учетом задержки:
# - Блок 1: биты 13-120 (108 бит)
# - Блок 2: биты 169-276 (108 бит)
# Общая длина: 216 бит
Sig .= BitSignal[Int.([collect(13:120); collect(169:276)] .+ Delay)]
Enable = true # Активируем флаг разрешения
end
end
# Сброс состояния после 12 интервалов
j == 12 ? (U = false; j = 0) : nothing
# Возвращаем:
# - Enable: флаг наличия валидных данных
# - Sig: обработанный сигнал (216 бит)
return Enable, Sig
end
Теперь запустим модель и проверим корректность её работы, подсчитав количество ошибок на выходе относительно входа.
Более интересные тесты представлены в первой версии модели. Если у вас есть желание, можете применить их и для этой модели, а мы решили здесь этого не делать, дабы не увеличивать искусственно объёмы разбираемого в данном примере материала.
display(run_model("DMR_V2"))
println("Суммарное количество ошибок по байтам равно: ",sum(vcat(collect(simout["DMR_V2/err_symbol"]).value...)))
Вывод¶
В данном примере была разобрана довольно существенная часть протокола DMR. В следующий раз, если вам будет интересна эта тема, мы разберём MAC-уровень, а именно логику ответов на канальные кадры и кадры управления.