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.
-
de2bi(x, n)
Converts decimal numbersxto a binary array of lengthn(low-order bits on the right). -
bi2de(bits)
Converts a binary arraybitsto a decimal number (interpreting it as a bit string). -
encode(msg)
Calculates the control bits (CRC) for the messagemsgusing a polynomialPOLYand the Reed-Solomon coding algorithm. -
lc_header_mask(parity)
Applies a maskSTART_MASKto the parity bitsparityusing the XOR operation. -
log_mult(a, b)
Performs multiplication in the Galois field (GF(256)) using logarithmic tablesLOG_TABLEandEXP_TABLE. -
CS5bit(LCcrc)
Calculates a 5-bit checksum for a 72-bit block of dataLCcrc(sums the bytes and takes the remainder of the division by 31). -
HemR(l)
Calculates horizontal (row-by-row) Hamming verification bits for a matrixlthe size is 9x11. -
HemC(l, HR)
Calculates vertical (column-wise) Hamming verification bits for a matrixltaking into account horizontal checksHR. -
typegen(CC)
Generates a 20-bit message type based on a 4-bit codeCCadding a checksum from the tableENCODE_2087.
path = "$(@__DIR__)/dmr_lib.jl"
println("Путь до библиотеки: $path")
include(path)
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.
-
Formation of service fields (FLCO, FID);
-
Address processing;
-
Encoding and checksums;
-
Data preparation for noise-resistant coding;
-
Formation of transmission frames;
-
Transmission sequence control;
-
Return of the finished data packet or LC block.
# Инициализация глобальных переменных для отслеживания состояния
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
Next, we will test the described function. The test consists of three main parts.
-
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)
-
-
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.
-
-
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)
-
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)
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.
indices = ((findall(x -> x == 104, filtered_vector)).-1)/13
Now let's delete LC and check if the rest of the frames are alternating.
# Удаляем все 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 ? "Да" : "Нет")
As we could see, the function is working correctly. Now let's test the models.
Let's run the model for which we have created a binding for our function.
# Подключение вспомогательной функции запуска модели.
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
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.
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)
Now that we've tested the feature itself, we can move on to improving our DMR system model.
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.
display(run_model("DMR_V2"))
println("Суммарное количество ошибок по байтам равно: ",sum(vcat(collect(simout["DMR_V2/err_symbol"]).value...)))
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.
