Engee 文档

根据自定义模板生成代码

*Engee*允许您使用模板生成代码。模板是以 .cgt 为扩展名的文本文件(codegen template),使用特殊语法自动添加数据并生成代码。C 和 Chisel 语言支持使用模板生成代码。

代码生成器会检查 C、Chisel 和 Verilog 代码的正确性。如果所选语言的代码不正确,例如 Chisel 中缺少一个必要字符或未指定模板语言,则默认选择 C 语言。

自定义模板有两种类型:块模板和 "主 "函数模板:

  • 块模板允许您为实现特定逻辑的单个模型组件(函数或模块)生成代码。它们提供了单个操作层面的代码使用,这在开发具有大量块的复杂模型时更为可取。

  • 主 "函数模式侧重于模型的整体执行结构,包括初始化、逐步处理和终止。这种方法适用于多任务模型中的任务执行排序。

这些方法之间的选择取决于详细程度和规模:块模板用于微调逻辑,而 "主 "模板用于控制整个模型。

区块代码生成

自定义模板可用于生成*Engee*模型块的代码。将模板添加到路径后,点击 "生成代码 "按钮codegen icon 1 。在结果文件夹中,您可以找到以所选语言生成的文件。

如果模板在文件夹中(除 `/user`外的任何目录),则需要将其添加到路径。为此,请选择包含模板的文件夹,右键单击该文件夹并将其添加到路径中。添加后,文件夹将显示一个蓝色图标blue directory icon 。这样代码生成器才能遍历所有指定路径,找到模板文件并使用它们创建块代码。
代码生成器会分析添加到路径中的所有模板。如果找到多个模板,代码生成器将使用最后添加的模板。
区块的模板文件必须以 .cgt 为扩展名,生成器才能工作。

程序块模板语法

模板语法基于注释。它们可以是以 //! 开头的单行注释,也可以是以 //.任何有效的 Julia 代码都可以写在注释中。使用注释可以执行模板代码,但不会出现在最终生成的文件中。

模板中未注释的部分会直接写入文件。这些区域必须包含 C 或 Chisel 代码。对于动态生成,使用 $() 语法。在括号内,您可以指定将被执行的任何 Julia 工作代码,其结果将在最终文件中取代该结构体。如果表达式清晰明确,可以使用不带括号的 $,但建议使用带括号的版本。

$()`语法仅在处理输入、输出和状态时使用特殊函数,而不用于处理块参数("state_addr "函数除外)。

下面总结了在 $() 中生成代码所支持的特殊函数:

支持的函数列表
类别.s 功能.s 说明

输入功能

输入

返回程序块的输入端口

输入数据类型名称

返回输入端口的数据类型

输入端口大小

返回输入端口的大小

输入尺寸

返回输入端口的尺寸数

输入复杂度

返回输入端口的复杂度

输入采样时间

返回输入端口计算的步长

输入端口数

返回程序块的输入端口数

输出函数

输出

返回程序块的输出端口

输出数据类型名称

返回输出端口的数据类型

输出端口大小

返回输出端口的大小

输出尺寸

返回输出端口的尺寸数

输出复杂度

返回输出端口的复杂度

输出采样时间

返回输出端口计算的步长

输出端口数

返回程序块的输出端口数

各州的功能

状态

返回区块的状态

状态数据类型名称

返回状态数据类型

状态大小

返回状态的大小

状态尺寸

返回状态维数

状态复杂度

返回状态的复杂度

状态数

返回程序块的状态数

用于处理数据块路径的函数

获取区块名称

返回区块名称

使用调度程序的功能

block_sampletime.

返回块计算步长(如果块有输出,可以使用 output_sampletime(1)

获取采样率个数

返回模型中采样率的个数

获取采样率

返回模型的基本采样率

获取模型采样率

返回基本采样率和其他采样率的比率数组,例如`[1 3 5]

is_singlerate_model`.

如果模型是单频,则返回 `true

is_singletask_model`.

如果模型是单任务的,则返回 `true

模板中的代码生成使用特殊的元代码宏(metacode macros)执行,这些宏决定最终代码的哪些部分将添加必要的片段(将代码生成器的输出切换到相应的缓冲区)。这些宏用于构建代码并控制其位置。支持的宏包括

元代码宏列表.
类别.s .s 说明

模板缓冲区

@Definitions.

用于定义全局变量、数据类型和宏。缓冲区的内容会在文件开头的所有 #include 之后发出。

@Epilogue.

在生成文件的末尾添加代码。通常用于终止或添加注释。

@Init.

包含模型初始化时执行的代码。该缓冲区在 model_init 函数中输出。

@Inports.

用于生成块的输入端口说明。缓冲区包含在模型代码的相应部分。

@Outports.

包含描述程序块输出端口的代码。它在与输出处理相关的部分生成。

@Prologue.

将代码添加到文件开头的 @Definitions 部分之前。可能包含预定义或设置。

@States.

用于描述程序块的状态,包括其大小、数据类型和初始化。在模型的状态部分生成。

@Step.

主缓冲区,除非指定了其他宏,否则代码默认写入该缓冲区。该代码会在每个建模步骤中执行。

@Term.

包含模型终止时执行的代码。该缓冲区在 model_term 函数中输出。

@Toplevel.

添加紧接在 #include 之后、位于模型开头的代码。

为了在模板中处理块参数,使用了 param 全局数据结构。它是一个已命名的元组 Julia,包含所有块参数的描述,包括信号类参数(例如,Gain 块中 Gain 参数的值)和所有其他块特性。

要查看 param 结构的内容,可以在模板中添加注释,并在注释中加入 $param 表达式。在代码生成过程中,该表达式将被块参数和特性的完整描述所取代。之后,您就可以查看 param 结构的各个字段了。

示例

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

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

代码生成完成后,带有 $param 的注释将替换为块参数的说明,以便您检查其结构和属性。

模板示例

让我们来看看以下模型的模板示例:

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

上面的示例显示了用 C 语言生成块代码分割 的更新模板。在这段代码中

  • BlockType = :Product: 指定生成代码的代码块类型;

  • TargetLang = :C:指定生成代码的目标语言,本例中为 C 语言。

模板代码使用 @Step 部分,该部分定义了计算过程中每一步执行的计算:

#! @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

在这段代码中

  • if contains(param.Inputs,"/")条件检查`Inputs`参数是否包含字符串`"/"。如果包含,则执行除法运算:

    • $(input(2)) == 0 - 检查除数是否为零。如果分母为零,则结果等于 $(input(1))

    • 否则执行除法运算 $(input(1)) / $(input(2))

  • 如果不使用除法,则执行乘法运算 $(input(1))* $(输入(2))

工作原理:

  • 在每个计算步骤中执行 @Step 部分的代码,根据参数配置决定程序块的行为。

  • 使用 $(input(n))$(output(n)) 可以动态访问程序块的输入和输出端口。

  • 输出端口数据类型变量通过 `$(output_datatype_name(1))`定义,使代码在不同数据类型之间通用。

对于 C 功能Engee 功能 块,模板语法有所不同。使用的格式是`//!BlockType = :EngeeFunction!EngeeFunction,` 而不是通过`BlockType`来指定块类型的标准格式,其中`!后面是模型中的块名称,并根据目标语言进行了转换。如果指定的 `BlockType 不正确,系统将生成错误。

使用带有分割 块的 newmodel_1 模型模板后,将生成包含以下内容的 newmodel_1.c 文件:

#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;

}

生成的代码实现了模型运行的主要功能:初始化 (newmodel_1_init)、计算步骤执行 (newmodel_1_step)和终止 (newmodel_1_term)。 newmodel_1_step 函数执行模型的关键操作。在本例中,执行了两个计算:乘法 (Product) 和除法 (Divide)。这些运算的结果被写入相应的模型输出参数 Out1 和 Out2。要计算这些值,需要使用结构 Ext_newmodel_1_U 中定义的输入 In1 和 In2 中的数据。

初始化(newmodel_1_init)和终止(newmodel_1_term)函数指定了`/(无需初始化代码)//(无需终止代码)/`。这意味着,对于该模型,无需在代码生成阶段添加额外的自定义代码来执行初始化或终止操作。这些函数留空,但仍包含在代码中,以支持该模式的语法。

在生成的代码中,"init "和 "term "函数总是被创建,即使它们的部分没有明确添加到模板中("@Init "或"@Term")。这样做是为了保持生成代码的结构统一,并为将来添加新逻辑做好准备。

例如,在上述模板中,Product 块只有 @Step 部分。因此

init "和 "term "函数也会出现在生成的代码中,但仍然是空的,注释为"/(不需要初始化代码)/和"/(不需要终止代码)/@Step 部分的内容将被插入到 step 函数中,因为它是模板中唯一定义的部分。

因此,在生成的代码中,总会为每个部分预留空间,但它们的使用完全取决于模板的内容。

生成主函数

main "函数代表程序执行的入口点,是管理模型生命周期的封装器。在 Julia 中,"main "函数通常用于组织程序的开始,其中定义了关键步骤:初始化模型、执行模拟步骤和终止。

main "函数使用 Julia 模板创建,生成代码的结构由 C 风格注释("/* …​ *//",如上文语法部分所述)和命令行或脚本编辑器上的 "engee.generate_code "函数构成。生成代码 "函数的签名如下:

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

要为`main`函数生成代码,`engee.generate_code`函数需要使用命名参数`template_path`指定模板路径。

函数调用示例:

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

模板示例

让我们来看一个生成 main 函数的模板示例:

生成模板
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

这里:

  • 在模板的开头,通过 #include "$model_name.h" 连接模型头文件以访问模型函数。

  • 主建模步骤通过调用 `$(model_substep(0))`和重置状态标志来执行。对于子频率任务,会对执行情况进行监控,使用 `$(model_substep(i))`对步骤进行处理,并更新完成标志。

  • 对于单任务模型,通过 OverrunFlag 实现过载控制,并通过 $model_step 调用模型执行。

  • main 函数中,会调用 $model_init 为执行模型做准备。

  • 在无限循环中,将处理模型任务和可用于交互的信号。

  • 在终止之前,会调用 $model_term 以正确终止模型并释放资源。