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

Генерация Verilog для 4-FSK модулятора

Здесь мы рассмотрим работу с 4-FSK (Frequency Shift Keying) в Engee.

Частотная модуляция – это вид модуляции, при котором информация кодируется изменением частоты сигнала. 4-FSK, четырехуровневая частотная манипуляция, – это тип модуляции, применяемый в DMR (Digital Mobile Radio), и он оптимален для использования в системах PMR (Professional Mobile Radio).

Также мы выполним генерацию кода Verilog из этой модели и проверим её работоспособность в Vivado.

Verilog — это язык описания аппаратуры, используемый для разработки электронных систем.

Verilog нужен в проектировании, верификации и реализации аналоговых, цифровых и смешанных электронных систем на различных уровнях абстракции.

Объявим вспомогательные функции

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.01)
    return model_output
end
Out[0]:
run_model (generic function with 1 method)

Анализ модели

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

Ниже приведена таблица, на основе которой разрабатывалась модель.

In [ ]:
symbol = [-3, -1, 1, 3]
bits = [[0, 0], [0, 1], [1, 0], [1, 1]]

println(join(["bits: $bit, symbol: $f" for (f, bit) in zip(symbol, bits)], "\n"))
bits: [0, 0], symbol: -3
bits: [0, 1], symbol: -1
bits: [1, 0], symbol: 1
bits: [1, 1], symbol: 3

На скриншотах ниже показана разработанная модель. image.png

Исходный блок 4-FSK. image.png

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

image.png

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

Давайте коротко затронем тему типов данных с фиксированной точкой. Этот тип данных задаётся командой fi(X, 1, 16, 5), где слева направо параметры:

  1. значения числа;
  2. знак (1-знаковый, 0-беззнаковый);
  3. полная разрядность слова;
  4. размер дробной части.

Далее рассмотрим простой пример.

In [ ]:
x = fi(7.5, 1, 7, 5)
y = fi(7.5, 1, 7, 3)
println("x: $x")
println("y: $y")
x: 1.96875
y: 7.5

Как мы видим, в первом случае число 7.5 ушло в переполнение.

In [ ]:
x+y
Out[0]:
fi(9.46875, 1, 10, 5)

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

Проверка работоспособности модели

Теперь проведём анализ соответствия двух реализаций между собой. Для начала запустим модель.

In [ ]:
bit_1 = 1; bit_2 = 1;
println("Inp_bit: $([bit_1, bit_2])")

println()
@time run_model("FSK_V") # Запуск модели.
Inp_bit: [1, 1]

Building...
Progress 0%
Progress 7%
Progress 16%
Progress 26%
Progress 34%
Progress 43%
Progress 49%
Progress 59%
Progress 66%
Progress 72%
Progress 80%
Progress 86%
Progress 96%
Progress 100%
Progress 100%
  3.472636 seconds (81.98 k allocations: 6.529 MiB)
Out[0]:
SimulationResult(
    "4-FSK modulator math.Symbol" => WorkspaceArray{Fixed{1, 4, 0, Int8}}("FSK_V/4-FSK modulator math.Symbol")

)

Теперь сравним результаты. Как мы видим, оба результата соответствуют исходной таблице.

In [ ]:
Symbol_math = collect(Symbol_sim).value[end]
println("Symbol_math: $Symbol_math")
Symbol_switch = collect(Symbol_sim_switch).value[end]
println("Symbol_switch: $Symbol_switch")
Symbol_math: -1.0
Symbol_switch: -1

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

In [ ]:
Symbol_ref = 2 * (2 * bit_1 + bit_2) - 3
println("Symbol_ref: $Symbol_ref")
println("Symbol_sim: $Symbol_math")
Symbol_ref: -1
Symbol_sim: -1.0

Выполним генерацию кода из блока 4-FSK модулятора

Начнём с команды для генерации кода. Ниже приведена информация о возможностях использования генератора.

In [ ]:
? engee.generate_code
Out[0]:
engee.generate_code(path/to/modelname.engee::String, path/to/output_dir::String; subsystem_name=subsystem_path::String, subsystem_id=subsystem_id::String, target::String, jl_path::String)

Генерирует код на указанном языке для модели или подсистемы.

Аргументы

  • path/to/modelname.engee::String: абсолютный или относительный путь к модели из которой генерируется код. В качестве аргумента может выступать объект модели (объект типа model, полученный функцией engee.gcm).
  • path/to/output_dir::String: абсолютный или относительный путь к директории, в которую сохранится сгенерированный код. Если директории output_dir не существует — она будет создана автоматически.
  • subsystem_name=path/to/subsystem::String: полный путь к атомарной подсистеме из которой генерируется код.
  • subsystem_id=subsystem_id::String: уникальный идентификатор атомарной подсистемы из которой генерируется код.
  • target::String: указание языка для генерации кода. Поддерживаемые языки — Си (по умолчанию) или Verilog.
  • jl_path::String: абсолютный или относительный путь к файлу .jl, содержащему шаблон для генерации кода.

Примеры

engee.generate_code("/user/newmodel_1.engee", "/user/newmodel_1/Subsystem") # генерация кода для подсистемы

engee.generate_code("/user/newmodel_1.engee", "/user/newmodel_1/codegen_output") # генерация через абсолютный путь к модели

engee.generate_code("newmodel_1.engee", "newmodel_1/codegen_output") # генерация через относительный путь к модели

m = engee.gcm()  # получение текущей открытой модели
engee.generate_code(m, "/user/newmodel_1/codegen_output")

Теперь зададим в модели целевую платформу.

image.png

Выполним генерацию кода. В связи с тем, что целевая платформа явно задана в настройках модели, строка выбора таргета нам не понадобится.

In [ ]:
engee.generate_code(
"$(@__DIR__)/FSK_V.engee",
"$(@__DIR__)/V_Code",
subsystem_name="4-FSK modulator math",
# target="verilog"
)
[ Info: Generated code and artifacts: /user/my_projects/Demo/A_In_Work/Seminar/FSK/V_Code

Работа с Vivado

Теперь выполним тестирование полученного кода в Vivado и скачаем полученные файлы.

image.png

Создадим пустой проект.

image.png

Добавим сгенерированный файл.

image.png

Определим целевую платформу для нашего проекта.

image.png

Теперь мы можем посмотреть на итоговую схематику нашего проекта. Она получилась очень простой.

image.png

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

image.png

Мы можем убедиться в этом, посмотрев еще и результаты симуляции. Все входы и выходы не определены.

image.png

Давайте исправим это и добавим обвязку к нашему блоку, задав входные порты как константы.

image.png

image.png

Теперь повторим симуляцию.

image_2.png

Результат симуляции может показаться некорректным, но давайте по пунктам разберём сгенерированную логику при условии, что на вход поступает пара бит [0,1]. Начнём с анализа результатов при помощи разработанной нами формулы.

In [ ]:
Symbol_ref = 2*(2*0+1)-3 #[0,1]
println("Ожидаемый результат: $Symbol_ref")
Ожидаемый результат: -1

Теперь перейдём к нашему коду:


(io_Symbol = {{1'h0, io_Bit_1, 1'h0} + {2'h0, io_Bit_2}, 1'h0} - 4'h3)


  1. {1'h0, 0, 1'h0}: 000
  2. {2'h0, 1}: 001
  3. {0,0,0} + {0,0,1}: 001
  4. {001, 0}: 0010
  5. 4'h3: 0011

Теперь перейдём к ответу. Если брать битовое вычитание, то результат равен: [1111].

  1. 0010 - 0011 = -1
  2. Берём модуль: 1: 0001
  3. Инвертируем биты: 0001: 1110
  4. Добавляем 1: 1110 + 1: 1111

Опираясь на описанные выше тезисы, можно утверждать, что наша упрощённая реализация модулятора 4-FSK работает корректно.

Вывод

В данном примере мы разобрали возможности генерации и верификации Verilog кода в Engee, убедились, что такой подход к разработке систем под ПЛИС применим и актуален. Более того, он может значительно ускорить процесс разработки за счёт возможности мгновенного редактирования и тестирования работы модели.