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

DMR: формирование LC и A–E пакетов.

Цифровой мобильный радиопротокол (DMR) остаётся стандартом для надёжной профессиональной связи. Моделирование его поведения важно для тестирования и анализа. В исходном примере уже заложена базовая логика формирования данных и синхронизации. Однако она не включала детальную обработку логики передачи управляющих каналов и типов пакетов.

В этой доработанной версии мы добавили следующие пункты.

  • Полноценную логику формирования LC‑пакетов(Logical Channel Packets).

  • Корректное формирование пакетов типов A, B, C, D и E: это пакеты данных, которые обрамляются служебными заголовками, сигнальными полями, включают синхропоследовательность (А) или краткую форму LC (B,C,D,E), а также имеют в себе контрольные биты.

  • Ещё здесь были усовершенствованы механизмы кадровой синхронизации потока с использованием xcorr.

Такое расширение подходит для полноценного анализа работы протокола, симуляции временных задержек и тестирования на реальном оборудовании.

Реализацию данного проекта начнём с анализа написанной функции формирования пакетов и тестирования этой функции в скриптах для последующего её переноса в модель при помощи блока Engee function.

Для начала подключим файл со вспомогательными функциями. Каждая функция выполняет специфическую задачу в рамках обработки данных, связанной с кодированием, проверкой ошибок или формированием структуры сообщения. Ниже представлен список функций с кратким описанием их назначения.

  1. de2bi(x, n)
    Конвертирует десятичное число x в бинарный массив длины n (младшие биты справа).

  2. bi2de(bits)
    Конвертирует бинарный массив bits в десятичное число (интерпретируя его как битовую строку).

  3. encode(msg)
    Вычисляет контрольные биты (CRC) для сообщения msg с использованием полинома POLY и алгоритма кодирования Рида-Соломона.

  4. lc_header_mask(parity)
    Применяет маску START_MASK к битам четности parity с помощью операции XOR.

  5. log_mult(a, b)
    Выполняет умножение в поле Галуа (GF(256)) с использованием логарифмических таблиц LOG_TABLE и EXP_TABLE.

  6. CS5bit(LCcrc)
    Вычисляет 5-битную контрольную сумму для 72-битного блока данных LCcrc (суммирует байты и берет остаток от деления на 31).

  7. HemR(l)
    Вычисляет горизонтальные (по строкам) проверочные биты Хэмминга для матрицы l размером 9×11.

  8. HemC(l, HR)
    Вычисляет вертикальные (по столбцам) проверочные биты Хэмминга для матрицы l с учетом горизонтальных проверок HR.

  9. typegen(CC)
    Генерирует 20-битный тип сообщения на основе 4-битного кода CC, добавляя контрольную сумму из таблицы ENCODE_2087.

In [ ]:
path = "$(@__DIR__)/dmr_lib.jl"
println("Путь до библиотеки: $path")
include(path)
Путь до библиотеки: /user/start/examples/communication/dmr_v2/dmr_lib.jl
Out[0]:
typegen (generic function with 1 method)

Далее представленная реализованная нами функция Gen_Pkg обрабатывает входные параметры, генерирует и возвращает данные для передачи в виде битовых последовательностей, управляя их отправкой,
формирует кадры данных для передачи в системе связи. Далее представлены основные этапы работы функции.

  1. Формирование служебных полей (FLCO, FID);

  2. Обработка адресов;

  3. Кодирование и контрольные суммы;

  4. Подготовка данных для помехоустойчивого кодирования;

  5. Формирование кадров передачи;

  6. Управление последовательностью передачи;

  7. Возврат готового пакета данных или LC-блока.

In [ ]:
# Инициализация глобальных переменных для отслеживания состояния
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
Out[0]:
Gen_Pkg (generic function with 1 method)

Далее выполним тестирование описанной функции. Тест состоит из трёх основных частей.

  1. Подготовка данных:

    • Создается тестовый массив из 2670 байт (равных 1)

    • Входной массив добавляется нулевыми байтами до размерности, кратной 27 (размеру блока)

  2. Разбиение на блоки и обработка:

    • Разделяет данные на блоки по 27 байт.

    • Для каждого блока вызывает функцию Gen_Pkg(), которая формирует кадр данных, тем самым мы по сути проверяем работу нашей функции в условиях симуляции.

  3. Анализ результатов:

    • Собирает все сгенерированные битовые блоки

    • Вычисляет сумму битов в каждом блоке (для проверки)

    • Фильтрует и выводит только ненулевые суммы (каждый второй кадр отбрасываем)

In [ ]:
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)
[104, 52, 41, 38, 41, 44, 52, 41, 38, 41, 44, 52, 41, 104, 38, 41, 44, 52, 41, 38, 41, 44, 52, 41, 38, 41, 104, 44, 52, 41, 38, 41, 44, 52, 41, 38, 41, 44, 52, 104, 41, 38, 41, 44, 52, 41, 38, 41, 44]

Теперь проверим, с правильной ли периодичностью встречается LC-пакет. Исходя из настройки ELC = 2, мы его встретим раз в 2 суперфрейма, то есть каждый тринадцатый кадр должен быть равен 104.

In [ ]:
indices = ((findall(x -> x == 104, filtered_vector)).-1)/13
Out[0]:
4-element Vector{Float64}:
 0.0
 1.0
 2.0
 3.0

Теперь удалим LC и проверим, чередуются ли остальные кадры.

In [ ]:
# Удаляем все 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 ? "Да" : "Нет")
Повторяется ли комбинация [52, 41, 38, 41, 44]? Да

Как мы могли убедиться, функция работает корректно. Теперь проведём тестирования в моделях.

image.png

Запустим модель, для которой мы сформировали обвязку для нашей функции.

In [ ]:
# Подключение вспомогательной функции запуска модели.
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
Out[0]:
run_model (generic function with 1 method)

Исходя из графиков ниже мы видим, что, во-первых, сигнал управления LC_next предупреждает нас о том, что следующий кадр это LC отрабатывает корректно, а, во-вторых, мы видим, что в модели и в скрипте функция ведёт себя идентично. Плюсом для удобства определения параметров пакета в Engee Function мы используем объявление параметров, тем самым уменьшаем количество входных портов блока.image.png

In [ ]:
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)
Building...
Progress 0%
Progress 8%
Progress 27%
Progress 41%
Progress 55%
Progress 70%
Progress 83%
Progress 98%
Progress 100%
SimulationResult(
    "Сумма элементов.1" => WorkspaceArray{Int64}("test_new_function/Сумма элементов.1")
,
    "Переключатель.1" => WorkspaceArray{Vector{Int64}}("test_new_function/Переключатель.1")

)
Out[0]:

Теперь, когда мы протестировали саму функцию, можно переходить к усовершенствованию нашей системной модели DMR.

image.png

Как мы видим, из скриншота выше, блок 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

Теперь запустим модель и проверим корректность её работы, подсчитав количество ошибок на выходе относительно входа.
Более интересные тесты представлены в первой версии модели. Если у вас есть желание, можете применить их и для этой модели, а мы решили здесь этого не делать, дабы не увеличивать искусственно объёмы разбираемого в данном примере материала.

In [ ]:
display(run_model("DMR_V2"))
println("Суммарное количество ошибок по байтам равно: ",sum(vcat(collect(simout["DMR_V2/err_symbol"]).value...)))
Building...
Progress 0%
Progress 5%
Progress 10%
Progress 15%
Progress 20%
Progress 25%
Progress 30%
Progress 35%
Progress 41%
Progress 46%
Progress 51%
Progress 56%
Progress 62%
Progress 67%
Progress 72%
Progress 77%
Progress 82%
Progress 88%
Progress 93%
Progress 98%
Progress 100%
Progress 100%
SimulationResult(
    "err_symbol" => WorkspaceArray{Vector{Int64}}("DMR_V2/err_symbol")
,
    "Inp" => WorkspaceArray{Vector{Int64}}("DMR_V2/Inp")
,
    "Out" => WorkspaceArray{Vector{UInt32}}("DMR_V2/Out")

)
Суммарное кол-во ошибок по байтам равно: 0

Вывод

В данном примере была разобрана довольно существенная часть протокола DMR. В следующий раз, если вам будет интересна эта тема, мы разберём MAC-уровень, а именно логику ответов на канальные кадры и кадры управления.

Блоки, использованные в примере