Engee documentation
Notebook

Verilog generation for 4-FSK modulator

Here we will look at working with 4-FSK (Frequency Shift Keying) in Engee.

Frequency modulation is a type of modulation in which information is encoded by changing the frequency of a signal. 4-FSK, Four Level Frequency Shift Keying, is the type of modulation used in DMR (Digital Mobile Radio) and is optimal for use in PMR (Professional Mobile Radio) systems.

We will also generate Verilog code from this model and verify that it works in Vivado.

Verilog is a hardware description language used to develop electronic systems.

Verilog is needed in the design, verification and implementation of analogue, digital and mixed electronic systems at different levels of abstraction.

Let's declare the auxiliary functions

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)

Analysing the model

We study two variants of the model. One uses standard selection logic implemented by switching. The second variant of the block is implemented using a mathematical formula. Such methods are often used to change algorithms beyond recognition when developing systems in order to optimise their performance in terms of speed or resources.

Below is the table on the basis of which the model was developed.

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

The screenshots below show the developed model. image.png

Source block 4-FSK. image.png

The block implemented using the formula, as we can see, has much less logic than the original block. Besides, all multiplication blocks in it use multiplication by 2, and, accordingly, when generating the code, such logic will represent a shift by one bit.

image.png

We can also notice that this case uses shorter data types than the original block where Int8 was used.

Let's briefly touch upon the topic of fixed-point data types. This data type is specified by the command fi(X, 1, 16, 5), where from left to right the parameters are:

  1. number values;
  2. sign (1-sign, 0-unsigned);
  3. the full bit size of the word;
  4. the size of the fractional part.

Next, let's consider a simple example.

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

As we can see, in the first case the number 7.5 overflowed.

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

We can also see that when adding these two numbers, more memory is allocated for them than was originally allocated.

Checking the performance of the model

Now let's analyse the consistency of the two implementations with each other. First of all, let's run the model.

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")

)

Now let's compare the results. As we can see, both results correspond to the initial table.

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

To verify the final project, we can represent the block from which we are going to generate the code in the form of a formula. Let's make sure that the formula is identical to the model.

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

Let's perform code generation from the 4-FSK modulator block

Let's start with the command for code generation. Below is information about the possibilities of using the generator.

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")

Now let's set the target platform in the model.

image.png

Let's perform code generation. Since the target platform is explicitly set in the model settings, we won't need the targeting string.

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

Working with Vivado

Now let's test the obtained code in Vivado and download the obtained files.

image.png

Let's create an empty project.

image.png

Let's add the generated file.

image.png

Let's define the target platform for our project.

image.png

Now we can look at the final schematic of our project. It turned out to be very simple.

image.png

Let us synthesise and implement the project. As we can see, the timings are not defined. This is due to the fact that the input ports of our block are empty, nothing is fed to them.

image.png

We can verify this by looking at the simulation results as well. All inputs and outputs are undefined.

image.png

Let's fix this and add pipelining to our block by setting the input ports as constants.

image.png

image.png

Now let's repeat the simulation.

image_2.png

The result of the simulation may seem incorrect, but let's analyse the generated logic point by point, provided that a pair of bits [0,1] is input. Let's start by analysing the results using the formula we have developed.

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

Now let's move on to our code:


(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

Now let's move on to the answer. If we take bitwise subtraction, the result is: [1111].

  1. 0010 - 0011 = -1
  2. *Take the modulus: 1: 0001.
  3. Invert the bits: 0001: 1110.
  4. Add 1: 1110 + 1: 1111

Based on the above theses, we can say that our simplified implementation of the 4-FSK modulator works correctly.

Conclusion

In this example we have analysed the possibilities of Verilog code generation and verification in Engee, we have seen that this approach to FPGA system development is applicable and relevant. Moreover, it can significantly speed up the development process due to the ability to instantly edit and test the model.