Engee documentation

Code generation based on custom templates

Engee allows you to generate code by usage of templates. Templates are text files with .cgt extension (codegen template) that use a special syntax to automatically add data and generate code. Code generation using templates is supported for C and Chisel languages.

The code generator checks the correctness of C, Chisel and Verilog code. If the code for the selected language is incorrect, e.g. a required character is missing in Chisel or the template language is not specified, the C language will be selected by default.

There are two types of custom templates - for blocks and for main function:

  • Block templates allow you to generate code for individual model components (functions or modules) that implement specific logic. They provide usage of code at the level of individual operations, which is preferable in the development of complex models with a large number of blocks.

  • The pattern for the `main' function focuses on the overall execution structure of the model, including initialisation, step-by-step processing and termination. This approach is preferred for sequencing task execution in multitasking models.

The choice between these approaches depends on the level of detail and scale: block templates are used to fine-tune the logic, while the `main' template is used to control the entire model.

Code generation for blocks

Custom templates can be used to generate code for blocks of Engee models. After adding the template to the path, click the "Generate Code" button codegen icon 1. In the results folder you will find the generated file in the selected language.

If the template is in a folder (any directory other than /user), you need to add it to path. To do this, select the folder with the template, right-click on it and add it to the path. Once added, the folder will be displayed with a blue icon blue directory icon. This is necessary so that the code generator can traverse all the specified paths, find the template files and use them to create block code.
The code generator analyses all templates added to the path. In case of finding several templates, the code generator will use the last added template.
For generation to work, the template file for blocks must have .cgt extension.

Block template syntax

The template syntax is based on comments. They can be single-line, starting with //!, or multi-line, enclosed in /*! */. Any valid Julia code can be written inside comments. Comments are used so that the template code is executed, but not in the final generated file.

The uncommented parts of the template are directly written to the file. These areas must contain C or Chisel code. For dynamic generation, the $() syntax is used. Inside the brackets, you can specify any working code in Julia that will be executed, and its result will replace this construct in the final file. If the expression is clear and unambiguous, you can use $ without brackets, but the bracketed version is always recommended.

The $() syntax uses special functions only for handling inputs, outputs, and states, but not for block parameters (except for the state_addr function).

The special functions supported for code generation within $() are summarised below:

List of supported functions
Category Function Description

Functions for inputs

input.

Returns the input port of the block

input_datatype_name

Returns the data type of the input port

input_size

Returns the size of the input port

input_ndims

Returns the number of input port dimensions

input_iscomplex

Returns the complexity of the input port

input_sampletime

Returns the step of input port calculation

input_ports_num

Returns the number of input ports of the block

Functions for outputs

output

Returns the output port of the block

output_datatype_name

Returns the data type of the output port

output_size

Returns the size of the output port

output_ndims

Returns the number of output port dimensions

output_iscomplex

Returns the complexity of the output port

output_sampletime

Returns the step of output port calculation

output_ports_num

Returns the number of output ports of the block

Functions for states

state

Returns the state of the block

state_datatype_name

Returns the state data type

state_size

Returns the size of the state

state_ndims

Returns the number of state dimensions

state_iscomplex

Returns the complexity of the state

state_num

Returns the number of states of the block

Functions for working with paths to the block

get_blockname

Returns block name

Functions for working with the scheduler

block_sampletime.

Returns the block calculation step (you can use output_sampletime(1) if the block has an output)

get_rates_num

Returns the number of sampling rates in the model

get_baserate

Returns the base sampling rate of the model

get_model_rates

Returns an array of ratios of base and other sampling rates, e.g. [1 3 5]

is_singlerate_model.

Returns true if the model is single frequency

is_singletask_model.

Returns true if the model is singleletasked

Code generation in templates is performed by usage of special metacode macros, which determine in which parts of the final code the necessary fragments will be added (switch the output of the code generator to the corresponding buffer). These macros are used to structure the code and control its placement. The list of supported macros includes:

List of metacode macros.
Category Macro Description

Template buffers

@Definitions.

Used to define global variables, data types and macros. The contents of the buffer are emitted at the beginning of the file after all #include.

@Epilogue.

Adds code to the end of the generated file. Typically used to terminate or add comments.

@Init.

Contains code that is executed when the model is initialised. The buffer is emitted in the model_init function.

@Inports.

Used to generate a description of the block’s input ports. The buffer is included in the appropriate section of the model code.

@Outports.

Contains code to describe the output ports of the block. It is generated in the section related to outputs processing.

@Prologue.

Adds code to the beginning of the file, before the @Definitions section. May contain pre-definitions or settings.

@States.

Used to describe block states, including their sizes, data types and initialisation. Generated in the states section of the model.

@Step.

The main buffer into which code is written by default, unless another macro is specified. This code is executed at each modelling step.

@Term.

Contains the code executed when the model is terminated. The buffer is emitted in the model_term function.

@Toplevel.

Adds code to be placed at the very beginning of the model, immediately after #include.

The global data structure param is used to handle block parameters in templates. This is a named Julia tuple that contains a description of all block parameters, including signal-like parameters (for example, the value of the Gain parameter in the Gain block) and all other block characteristics.

To examine the contents of the param structure, you can add a comment with the $param expression in the template. During code generation, this expression will be replaced with the full description of parameters and characteristics of the block. After that you can refer to individual fields of the param structure.

Example:

/*!
 * BlockType = :SomeBlockINeedToExplore
 * TargetLang = :C
 */

/* Let's see the block description:
$param
*/

After the code generation is done, the comment with $param will be replaced with a description of the block parameters, allowing you to explore its structure and properties.

Template example

Let’s consider an example template for the following model:

codegen model 1

//! BlockType = :Product
//! TargetLang = :C

//! @Step
//! if contains(param.Inputs,"/")
    /* Выполняется деление, если в параметрах блока указан символ "/" */
    /* Чтобы избежать ошибки деления на ноль, добавлена проверка входного значения */
    $(output_datatype_name(1)) $(output(1)) = $(input(2)) == 0 ? $(input(1)) : $(input(1)) / $(input(2));
//! else
    /* Выполняется умножение, если деление не задано в параметрах */
    $(output_datatype_name(1)) $(output(1)) = $(input(1)) * $(input(2));
//! end

The above example shows an updated template for generating the block code Divide in C. In this code:

  • BlockType = :Product: Specifies the type of block for which the code is generated;

  • TargetLang = :C: Specifies the target language for generation, in this case C.

The template code uses the @Step section, which defines the calculations performed at each step of the calculation:

#! @Step
if contains(param.Inputs,"/")
    /* Кастомное деление с проверкой деления на ноль */
    $(output_datatype_name(1)) $(output(1)) = $(input(2)) == 0 ? $(input(1)) : $(input(1)) / $(input(2));
else
    /* Ветка для случая, если это блок умножения */
    $(output_datatype_name(1)) $(output(1)) = $(input(1)) * $(input(2));
end

In this code:

  • The if contains(param.Inputs,"/") condition checks whether the Inputs parameter contains the string "/". If it does, a division operation is performed:

    • $(input(2)) == 0 - checks for division by zero. If the denominator is zero, the result is taken equal to $(input(1)).

    • Otherwise the division $(input(1)) / $(input(2)) is performed.

  • If division is not used, multiplication $(input(1)) * $(input(2)).

Working principle:

  • The code of the @Step section is executed at each calculation step, determining the behaviour of the block depending on the configuration of parameters.

  • The usage of $(input(n)) and $(output(n)) allows the input and output ports of the block to be accessed dynamically.

  • Output port data type variables are defined via $(output_datatype_name(1)), making the code universal across data types.

For C Function and Engee Function blocks, the template syntax is different. Instead of the standard specification of the block type via BlockType, the format used is //!BlockType = :EngeeFunction!EngeeFunction, where ! is followed by the block name from the model, converted for the target language. If the specified BlockType is incorrect, the system will generate a generation error.

The usage of the template for the model newmodel_1 with the block Divide will generate the file newmodel_1.c with the following content:

#include "newmodel_1.h"

/* External inputs */
Ext_newmodel_1_U newmodel_1_U;

/* External outputs */
Ext_newmodel_1_Y newmodel_1_Y;

/* Model initialize function */
void newmodel_1_init() {
	/* (no initialize code required) */
}

/* Model terminate function */
void newmodel_1_term() {
	/* (no terminate code required) */
}

/* Model step function */
void newmodel_1_step() {

	/* Product: /Product incorporates:
	 *  Inport: /In2
	 *  Inport: /In1
	 */
	const double Product = newmodel_1_U.In2 * newmodel_1_U.In1;
	/* Product: /Divide incorporates:
	 *  Inport: /In1
	 *  Inport: /In1
	 */
	const double Divide = newmodel_1_U.In1 / newmodel_1_U.In1;
	/* Outport: /Out1 incorporates:
	 *  Product: /Divide
	 */
	newmodel_1_Y.Out1 = Divide;
	/* Outport: /Out2 incorporates:
	 *  Product: /Product
	 */
	newmodel_1_Y.Out2 = Product;

}

The generated code implements the main functions for model operation: initialisation (newmodel_1_init), calculation step execution (newmodel_1_step) and termination (newmodel_1_term).

The newmodel_1_step function performs the key operations of the model. In this example, two calculations are implemented: multiplication (Product) and division (Divide). The results of these operations are written to the corresponding model output parameters Out1 and Out2. To calculate the values, the data coming through the inputs In1 and In2 defined in the structure Ext_newmodel_1_U are used.

The initialisation (newmodel_1_init) and termination (newmodel_1_term) functions specify /* (no initialize code required) / and / (no terminate code required) */. This means that for this model, no additional custom code had to be added at the code generation stage to perform initialisation or termination operations. These functions are left empty, but are still included in the code to support the syntax of the pattern.

In the generated code, the init and term functions are always created, even if their sections are not explicitly added in the template (@Init or @Term). This is done to keep the structure of the generated code unified and ready for adding new logic in the future.

For example, in the above template, there is only @Step section for the Product block. As a result:

The init and term functions will also be present in the generated code, but will remain empty, with the comments /* (no initialise code required) / and / (no terminate code required) */. The contents of the @Step section will be inserted into the step function, as it is the only section defined in the template.

Thus, space is always reserved for each section in the generated code, but their usage depends entirely on the content of the template.

Generating the main function

The main function represents the entry point for program execution and serves as a wrapper to manage the model lifecycle. In Julia, the main function is often used to organise the start of a program, where key steps are defined: initialising the model, executing the simulation steps and terminating.

The main function is created using a Julia template, where the structure of the generated code is formed using C-style comments (/* …​ *// as described in the syntax section above), and the engee.generate_code function on the command line or script editor. The generate_code function signature is as follows:

@with_exception_handling function generate_code(
    model_path::String,
    output_path::String;
    subsystem_id::Maybe{String} = nothing,
    subsystem_name::Maybe{String} = nothing,
    target::Maybe{String} = nothing,
    template_path::Maybe{String} = nothing
)

To generate code for the main function, the engee.generate_code function requires the template path to be specified using the named parameters template_path.

Function call example:

engee.generate_code(
    "/user/codegen_model.engee", # путь к модели из которой генерируется код
    "/user/codegen_dir"; # путь куда будет сгенерирован код
    template_path = "/user/main_template.jl" # путь к шаблону функции main
)

Example template

Let’s consider an example of a template for generating the main function:

Template for generation
function main_template() :: String
	#=%
		#include "$model_name.h"

		void do_step(void){
	%=#
	if !is_singlerate_model && !is_singletask_model
		zeroes = "0"
		for ji = 2:rates_num zeroes *= ", 0" end
		#=%
				static bool OverrunFlags[$rates_num] = {$zeroes};
				static bool eventFlags[$rates_num] = {$zeroes};
				static int taskCounter[$rates_num] = {$zeroes};

				if (OverrunFlags[0]) return;
		%=#
		for ji = 2:rates_num
			i = ji - 1
			rate_ratio = Int(rates[ji] / get_baserate())
			#=%
					if (taskCounter[$i] == 0) {
						if (eventFlags[$i]) {
							OverrunFlags[0] = false;
							OverrunFlags[$i] = true;
							/* Sampling too fast */
							return;
						}
						eventFlags[$i] = true;
					}
					taskCounter[$i]++;
					if (taskCounter[$i] == $rate_ratio) {
						taskCounter[$i] = 0;
					}
					/* Step the model for base rate */
					$(model_substep(0))
					/* Indicate task for base rate complete */
					OverrunFlags[0] = false;
			%=#
		end
		for ji = 2:rates_num
			i = ji - 1
			#=%
					/* If task {0} is running, do not run any lower priority task */
					if (OverrunFlags[$i]) return;
					/* Step the model for subrate */
					if (eventFlags[$i]) {
						OverrunFlags[$i] = true;
						/* Step the model for subrate $i */
						$(model_substep(i))
						/* Indicate task complete for subrate */
						OverrunFlags[$i] = false;
						eventFlags[$i] = false;
					}
			%=#
		end
	else
		#=%
				static bool OverrunFlag = false;

				/* Check for overrun */
				if (OverrunFlag) {
					return;
				}

				OverrunFlag = true;

				/* Step the model */
				$model_step

				/* Indicate task complete */
				OverrunFlag = false;
		%=#
	end

	print_sig(s) = "$(s.ty) $(s.full_qual_name)$(dims(s))"
	#=%
		}

		int main(int argc, char *argv[])
		{
			(void) argc;
			(void) argv;

			/* Initialize model */
			$model_init

			while (1) {
				/* Perform application tasks here */
				// Signals available:
				// INS
				// $(print_sig.(ins))
				// OUTS
				// $(print_sig.(outs))
			}

			/* Terminate model */
			$model_term
			return 0;
		}

	%=#
end

here:

  • At the beginning of the template, the model header file is connected via #include "$model_name.h" to access the model functions.

  • The main modelling step is executed by calling $(model_substep(0)) and resetting the status flags. For sub-frequency tasks, execution is monitored, steps are processed using $(model_substep(i)) and completion flags are updated.

  • For single-task models, overload control is implemented via the OverrunFlag and model execution is invoked via $model_step.

  • In the main function, $model_init is called to prepare the model for execution.

  • Inside the infinite loop, model tasks and signals available for interaction are processed.

  • Before terminating, $model_term is called to correctly terminate the model and release resources.