Engee documentation
Notebook

DMR: formation of LC and A–E packets.

The Digital Mobile Radio Protocol (DMR) remains the standard for reliable professional communications. Modeling its behavior is important for testing and analysis. In the original example the basic logic of data generation and synchronization has already been laid down. However, it did not include detailed processing of the logic of transmission of control channels and packet types.

In this revised version, we have added the following points.

  • Full‑fledged logic of formation of LC packets (Logical Channel Packets).

  • Correct formation ** of packets of types A, B, C, D and E**: these are data packets that are framed by service headers, signal fields, include a sync sequence (A) or a short form LC (B,C,D,E), and also contain control bits.

  • The mechanisms of frame synchronization of the stream using xcorr have also been improved here.

This extension is suitable for full-fledged analysis of the protocol, simulation of time delays and testing on real hardware.

We will begin the implementation of this project by analyzing the written packet generation function and testing this function in scripts for its subsequent transfer to the model using the Engee function block.

First, we'll connect a file with auxiliary functions. Each function performs a specific task within the framework of data processing related to encoding, error checking, or message structure formation. Below is a list of functions with a brief description of their purpose.

  1. de2bi(x, n)
    Converts decimal numbers x to a binary array of length n (low-order bits on the right).

  2. bi2de(bits)
    Converts a binary array bits to a decimal number (interpreting it as a bit string).

  3. encode(msg)
    Calculates the control bits (CRC) for the message msg using a polynomial POLY and the Reed-Solomon coding algorithm.

  4. lc_header_mask(parity)
    Applies a mask START_MASK to the parity bits parity using the XOR operation.

  5. log_mult(a, b)
    Performs multiplication in the Galois field (GF(256)) using logarithmic tables LOG_TABLE and EXP_TABLE.

  6. CS5bit(LCcrc)
    Calculates a 5-bit checksum for a 72-bit block of data LCcrc (sums the bytes and takes the remainder of the division by 31).

  7. HemR(l)
    Calculates horizontal (row-by-row) Hamming verification bits for a matrix l the size is 9x11.

  8. HemC(l, HR)
    Calculates vertical (column-wise) Hamming verification bits for a matrix l taking into account horizontal checks HR.

  9. typegen(CC)
    Generates a 20-bit message type based on a 4-bit code CC adding a checksum from the table 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)

The following is the function we implemented Gen_Pkg processes input parameters, generates and returns data for transmission in the form of bit sequences, controlling their sending, and
generates data frames for transmission in the communication system. The following are the main stages of the function.

  1. Formation of service fields (FLCO, FID);

  2. Address processing;

  3. Encoding and checksums;

  4. Data preparation for noise-resistant coding;

  5. Formation of transmission frames;

  6. Transmission sequence control;

  7. Return of the finished data packet or LC block.

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)

Next, we will test the described function. The test consists of three main parts.

  1. Data preparation:

    • A test array of 2670 bytes (equal to 1) is being created

    • The input array is added in zero bytes to a multiple of 27 (block size)

  2. Partitioning and processing:

    • Divides the data into blocks of 27 bytes.

    • Calls the function for each block Gen_Pkg(), which forms a data frame, thereby we essentially check the operation of our function under simulation conditions.

  3. Analysis of results:

    • Collects all generated bit blocks

    • Calculates the sum of the bits in each block (for verification)

    • Filters and outputs only non-zero amounts (we discard every second frame)

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]

Now let's check whether the LC packet occurs with the correct frequency. Based on the ELC = 2 setting, we will see it every 2 superframes, that is, every thirteenth frame should be 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

Now let's delete LC and check if the rest of the frames are alternating.

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]? Да

As we could see, the function is working correctly. Now let's test the models.

image.png

Let's run the model for which we have created a binding for our function.

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)

Based on the graphs below, we see that, firstly, the LC_next control signal warns us that the next frame is working correctly, and secondly, we see that the function behaves identically in the model and in the script. Plus, for the convenience of defining package parameters in the Engee Function, we use parameter declaration, thereby reducing the number of input ports of the block.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]:

Now that we've tested the feature itself, we can move on to improving our DMR system model.

image.png

As we can see from the screenshot above, the block Gen_Pkg controls the data input, inside the block DMR Physical Layer the logic described in the previous version of this example is located, and in the block Frame_Synchronization There are two functions that perform the frame synchronization of our stream. Let's look at these blocks, let's start with Xcorr.

# A global variable for storing the delay
global Delay = 0

# The main function of processing the data block
function (c::Block)(t::Real, BitSignal)
    global Delay
    US = false # Successful synchronization flag
    
    # Reference sync sequence (48 bits)
    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]
    
    # Calculation of the cross-correlation between the input signal and the synchronization sequence
    MS = xcorr_simple((2*BitSignal.-1), Data_Sync)
    
    # Search for correlation peaks (threshold > 46)
Peak = findall(MS.> 46)
    
    if !isempty(Peak) 
        P = Peak[1] # Take the first significant peak
        
        # Check if the peak is in the acceptable range
        if P > 120 && P < 456
            # Calculate the delay depending on the position of the peak
            if P > 288
                Delay = P-408
            else
                Delay = P-120    
            end
        end
        US = true # Setting the successful synchronization flag
to end
    
    D = Delay # Current delay value
    return US, D, MS  # We return the synchronization status, delay, and
the end correlation array.

Now let's move on to the block Selector.

# Global variables for managing the processing state:
global U = false # Flag of signal processing activity
global j = 0 # Time Interval counter

# The main function of processing the data block
function (c::Block)(t::Real, BitSignal, US, Delay)
global U, j # Access to global variables
    
    # Initialization of the output signal (216 bits)
    Sig = zeros(216)
    
    # Processing permission flag
    Enable = false
    
    # Synchronization signal processing (US):
# If a synchronization signal is received (US != 0), reset the status
    US != 0 ? (U = US; j = 0) : nothing
    
    # If processing is active (U == 1)
if U == 1
        j += 1 # Incrementing the counter
        
        # We only process odd intervals
        if isodd(j)
# We generate the output signal by selecting bits from BitSignal, taking into account the delay:
            # - Block 1: bits 13-120 (108 bits)
            # - Block 2: bits 169-276 (108 bits)
            # Total length: 216 bits
            Sig .= BitSignal[Int.([collect(13:120); collect(169:276)] .+ Delay)]
Enable = true # Activating the permission flag
        end
    end
    
    # Reset the status after 12 intervals
    j == 12 ? (U = false; j = 0) : nothing
    
    # We are returning:
    # - Enable: flag for valid data
    # - Sig: processed signal (216 bits)
    return Enable, Sig
end

Now let's run the model and check its correctness by calculating the number of errors at the output relative to the input.
More interesting tests are presented in the first version of the model. If you wish, you can apply them to this model as well, but we decided not to do so here, so as not to artificially increase the volume of the material being analyzed in this example.

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

Output

In this example, a fairly significant part of the DMR protocol was analyzed. Next time, if you are interested in this topic, we will analyze the MAC layer, namely the logic of responses to channel frames and control frames.