使用 C 功能块将 C 代码集成到 Engee 模型中
本例展示了使用C 功能块将 C 代码集成到恩吉模型中的不同方法。
在开始之前,我们先连接必要的库:
using Plots
using MATLAB
plotlyjs();
使用宏@__DIR__ 查找交互式脚本所在的文件夹:
demoroot = @__DIR__
使用 C 功能块的基础知识
让我们来看一些使用 C 功能块的简单示例,以说明将 C 代码集成到 **Engee ** 模型中的基础知识。
1.处理多维信号、传递参数和使用 #define 指令
在模型cCodeIntegration_basics.engee 中,我们添加了C 函数块,输入正弦信号in :
假设我们要将正弦波in 的振幅增加一个系数gain ,并将其移位一个值bias 。也就是说,输出信号应由公式out = gain * in + bias 确定。
让我们进入C 功能块的设置,在Ports 标签中,我们将熟悉块输入和输出的定义。在这里,源代码变量与 **C 功能块的端口相连:
1.将double 类型的标量变量x 与第一个输入端口in 绑定;
2.类型为double 的矢量(3 个元素)变量y 与第一个输出端口out 绑定。
gain 数组也在 C 功能块(Parameters 标签)的设置中定义:
值得注意的是,变量一旦作为参数传递,就会变成
1.全局变量 -gain 可以从源代码编辑器的任何选项卡中访问;
2.带有限定符const - 尝试更改gain 的值时会出现错误。
偏移值bias 由标识符#define bias 决定,该标识符在选项卡Build options 中设置:
在本例中,偏移量为5 。
让我们进入 C 功能块设置的Main ,点击 "编辑源代码 "按钮。在打开的源代码编辑器OutputCode 标签中,给出了模型计算每一步执行的代码:
在环路for 中(针对三个输出信号中的每一个信号),在模型计算的每一步都要确定变量y 的值。
让我们来模拟一下系统:
cCodeIntegration_basics = engee.load("$demoroot/models/cCodeIntegration_basics.engee", force = true)
simulationResults_basics = engee.run(cCodeIntegration_basics)
关闭模型
engee.close(cCodeIntegration_basics, force = true);
导入模拟结果
cCodeIntegration_basics_t = simulationResults_basics["out"].time;
cCodeIntegration_basics_y1 = [y[1] for y in simulationResults_basics["out"].value];
cCodeIntegration_basics_y2 = [y[2] for y in simulationResults_basics["out"].value];
cCodeIntegration_basics_y3 = [y[3] for y in simulationResults_basics["out"].value];
绘制图表
plot(cCodeIntegration_basics_t, cCodeIntegration_basics_y1)
plot!(cCodeIntegration_basics_t, cCodeIntegration_basics_y2)
plot!(cCodeIntegration_basics_t, cCodeIntegration_basics_y3)
title!("Выходной сигнал блока C Function <br> (Многомерные сигналы, параметры и #define)")
xlabel!("Время, [с]")
ylabel!("Амплитуда")
2.使用整数数据类型和静态变量
在模型cCodeIntegration_integers.engee 中,我们使用C 函数块实现了一个简单的计数器,其输出是一个表示当前迭代次数的正整数:
让我们分析一下 C 功能块设置中Ports 和Build Options 选项卡的内容:
C 语言标准库的头文件<stdint.h> 定义了C 函数块中整数数据类型的名称。该文件连接在Headers 行的选项卡Build options 中。
选项卡Ports 将源代码中的一个不带符号的 64 位变量out (类型设置为uint64_t ,而不是long ,参见前一段关于<stdint.h> 的内容)绑定到 C 功能块的第一个输出端口。
在 C 功能块源代码编辑器的Output code 选项卡中,包含了仿真过程中每一步执行的代码:
在模拟的第一步,静态变量counter 初始化为 0,之后每次迭代,其值都会以1 的形式递增,并分配给变量out 。
让我们对系统进行模拟:
cCodeIntegration_integers = engee.load("$demoroot/models/cCodeIntegration_integers.engee", force = true)
simulationResults_integers = engee.run(cCodeIntegration_integers)
关闭模型
engee.close(cCodeIntegration_integers, force = true);
导入模拟结果
cCodeIntegration_integers_t = simulationResults_integers["counter"].time;
cCodeIntegration_integers_y = simulationResults_integers["counter"].value;
绘制图表
plot(cCodeIntegration_integers_t, cCodeIntegration_integers_y, legend = false)
title!("Выходной сигнал блока C Function <br> (Целочисленные типы и статические переменные)")
xlabel!("Время, [с]")
ylabel!("Номер итерации")
有关源代码编辑器选项卡的用途、绘图选项和可用数据类型的详细信息,请参阅 文档。Engee.
整合外部源代码
任务说明
让我们考虑一个更有趣的工程问题。
假设我们需要控制一个稳定的二阶对象,在测量噪声的作用下,该对象会受到外部干扰。因此,我们需要解决对控制对象的输出信号进行滤波的问题,幸运的是,Engee有一个非常适合的工具!
alphabetafilter.c 和alphabetafilter.h 文件提供了[alpha-beta 滤波器]的 C 语言实现(https://en.wikipedia.org/wiki/Alpha_beta_filter)。
文件alphabetafilter.c :
#include "alphabetafilter.h"
#include <math.h>
double dt = 0.01;
static double xk_1, vk_1;
参数 determineAlphaBeta(double processNoiseVariance, double measurementNoiseVariance)
{
双 lambda, r;
参数 p;
lambda = (processNoiseVariance * pow(dt, 2)) / measurementNoiseVariance;
r = (4 + lambda - sqrt(8 * lambda + pow(lambda, 2)))/ 4;
p.alpha = 1 - pow(r, 2);
p.beta = 2 * (2 - p.alpha) - 4 * sqrt(1 - p.alpha);
返回 p;
}
double alphaBetaFilter(double value, Parameters p)
{
double xk, vk, rk;
xk = xk_1 + (vk_1 * dt);
vk = vk_1;
rk = 值 - xk;
xk += p.alpha * rk;
vk += (p.beta * rk) / dt;
xk_1 = xk;
vk_1 = vk;
return xk;
}
void initialiseStaticVariables()
{
xk_1 = 0;
vk_1 = 0;
}
文件alphabetafilter.h 的内容:
typedef struct {
double alpha;
double beta;
} 参数;
参数 determineAlphaBeta(double, double);
double alphaBetaFilter(double, Parameters);
void initialiseStaticVariables();
将这些源代码整合到我们的 Engee 模型中。
分析模型
模型cCodeIntegration_source.engee :
该模型包括
控制对象Plant ;
2.实现二阶 PID 控制器的子系统Controller ;
3. 限带白噪声块ProcessNoise 和MeasurementNoise ,模拟外部影响和测量噪声;
4.块 C 功能Filter ,实现阿尔法-贝塔滤波器。
程序块Filter 的参数Sample Time 等于模型求解器Ts 的步长:
让我们来分析一下Filter 区块设置中Build options 标签的内容:
在该选项卡的行中:
*Source files - 连接的源代码文件alphabetafilter.c ;
*Iinclude directories - 设置包含头文件的文件夹路径;
*Headers - 连接的头文件alphabetafilter.h 。
在程序块设置的Ports 标签中,源代码变量与程序块的输入和输出端口相连Filter 。
在Filter 程序块的源代码中,OutputCode 标签包含循环可执行代码:
在模型计算的每一步中,函数determineAlphaBeta() 和alphabetafilter() 被依次调用,之后滤波值被存储在变量out 中,与模块Filter 的输出端口x 相对应。
模拟开始
让我们模拟系统:
cCodeIntegration_source = engee.load("$demoroot/models/cCodeIntegration_source.engee", force = true)
simulationResults_source = engee.run(cCodeIntegration_source)
关闭模型
engee.close(cCodeIntegration_source, force = true);
模拟结果
导入模拟结果:
cCodeIntegration_source_noisy_t = simulationResults_source["noisy"].time;
cCodeIntegration_source_noisy_y = simulationResults_source["noisy"].value;
cCodeIntegration_source_filtered_t = simulationResults_source["filtered"].time;
cCodeIntegration_source_filtered_y = simulationResults_source["filtered"].value;
绘制图表
plot(cCodeIntegration_source_noisy_t, cCodeIntegration_source_noisy_y, label = "Исходный сигнал")
plot!(cCodeIntegration_source_filtered_t, cCodeIntegration_source_filtered_y, label = "Отфильтрованный сигнал")
plot!(legend = :bottomright)
title!("Альфа-бета фильтр <br> (Интеграция внешнего исходного кода)")
xlabel!("Время, [с]")
ylabel!("Амплитуда")
整合静态和动态图书馆
任务说明
源代码通常不是明确提供的,而是作为静态或动态库的一部分。C API 库与外部源代码一样,可轻松集成到Engee模型中!
让我们使用MakeFile 从文件alphabetafilter.c 和alphabetafilter.h 编译一个静态 (alphabetafilter.a) 和动态 (alphabetafilter.so) 库。
文件Makefile 的内容:
all: alphabetafilter.a alphabetafilter.so
alphabetafilter.o: alphabetafilter.c
$(CC) -c -fPIC $^ -o$@
alphabetafilter.a: alphabetafilter.o
ar rcs $@$^
alphabetafilter.so: alphabetafilter.o
$(CC) -shared$^ -o $@
清除:
rm -f *.o *.a *.so
让我们开始编译库:
run(`make -C $demoroot/source`)
将静态库和动态库整合到我们的 Engee 模型中。
静态库:分析模型
从结构上看,模型cCodeIntegration_library_static.engee 与之前考虑的cCodeIntegration_source.engee 没有区别;只有Filter 块设置的Build options 选项卡上的指令不同。让我们来分析一下它的内容:
这里定义了以下结构选项:
*Include directories - 指定包含头文件的文件夹路径;
*Library directories - 设置包含静态库的文件夹路径;
*Headers - 连接头文件alphabetafilter.h ;
*Libraries - 连接静态库alphabetafilter.a 。
静态库:开始模拟
让我们模拟系统:
cCodeIntegration_library_static = engee.load("$demoroot/models/cCodeIntegration_library_static.engee", force = true)
simulationResults_library_static = engee.run(cCodeIntegration_library_static)
关闭模型
engee.close(cCodeIntegration_library_static, force = true);
静态库:建模结果
导入模拟结果
cCodeIntegration_library_static_noisy_t = simulationResults_library_static["noisy"].time;
cCodeIntegration_library_static_noisy_y = simulationResults_library_static["noisy"].value;
cCodeIntegration_library_static_filtered_t = simulationResults_library_static["filtered"].time;
cCodeIntegration_library_static_filtered_y = simulationResults_library_static["filtered"].value;
绘制图表
plot(cCodeIntegration_library_static_noisy_t, cCodeIntegration_library_static_noisy_y, label = "Исходный сигнал")
plot!(cCodeIntegration_library_static_filtered_t, cCodeIntegration_library_static_filtered_y, label = "Отфильтрованный сигнал")
plot!(legend = :bottomright)
title!("Альфа-бета фильтр <br> (Интеграция статической библиотеки)")
xlabel!("Время, [с]")
ylabel!("Амплитуда")
动态库:模型分析
从结构上看,模型cCodeIntegration_library_dynamic.engee 与之前的cCodeIntegration_source.engee 没有区别;只有Filter 块设置的Build options 选项卡上的指令不同。让我们来分析一下它的内容:
这里定义了以下结构选项:
*Include directories - 指定包含头文件的文件夹路径;
*Library directories - 设置包含动态链接库的文件夹路径;
*Headers - 连接头文件alphabetafilter.h ;
*Libraries - 连接的动态链接库alphabetafilter.so 。
在Filter 块源代码编辑器的开始代码选项卡中,函数initializeStaticVariables() 被调用:
函数initializeStaticVariables() 在文件alphabetafilter.h 中声明,用于初始化动态链接库中使用的静态变量xk_1 和vk_1 。这是必要的,这样在重新启动仿真时,变量不会保存上次计算的值,而是重置为初始值 (在本例中为0 )。
动态库:运行模拟
让我们模拟一下系统:
cCodeIntegration_library_dynamic = engee.load("$demoroot/models/cCodeIntegration_library_dynamic.engee", force = true)
simulationResults_library_dynamic = engee.run(cCodeIntegration_library_dynamic)
关闭模型
engee.close(cCodeIntegration_library_dynamic, force = true);
动态图书馆:建模结果
导入模拟结果
cCodeIntegration_library_dynamic_noisy_t = simulationResults_library_dynamic["noisy"].time;
cCodeIntegration_library_dynamic_noisy_y = simulationResults_library_dynamic["noisy"].value;
cCodeIntegration_library_dynamic_filtered_t = simulationResults_library_dynamic["filtered"].time;
cCodeIntegration_library_dynamic_filtered_y = simulationResults_library_dynamic["filtered"].value;
绘制图表
plot(cCodeIntegration_library_dynamic_noisy_t, cCodeIntegration_library_dynamic_noisy_y, label = "Исходный сигнал")
plot!(cCodeIntegration_library_dynamic_filtered_t, cCodeIntegration_library_dynamic_filtered_y, label = "Отфильтрованный сигнал")
plot!(legend = :bottomright)
title!("Альфа-бета фильтр <br> (Интеграция динамической библиотеки)")
xlabel!("Время, [с]")
ylabel!("Амплитуда")
整合 Simulink 模型生成的代码
问题陈述
让我们从 Simulink 模型alphabetafilter.slx 生成 C 代码,并将其集成到我们的 Engee 模型cCodeIntegration_cg_simulink.engee 中。
实现阿尔法-贝塔滤波器的模型alphabetafilter.slx 是在Simulink中由简单模块构建的,由子系统determineAlphaBeta 和filterInputSignal 组成。
模型的顶层:
子系统determineAlphaBeta :
子系统filterInputSignal :
让我们使用Simulink 嵌入式编码器生成代码:
cgPath = joinpath(demoroot,"cg/simulink")
cgModel = joinpath(demoroot,"cg/simulink/alphabetafilter")
mat"""
model = load_system($cgModel);
path = $cgPath;
set_param(0, 'CacheFolder', path)
set_param(0, 'CodeGenFolder', path)
slbuild(model)
set_param(0, 'CacheFolder', '')
set_param(0, 'CodeGenFolder', '')
"""
模型分析
从结构上看,模型cCodeIntegration_cg_simulink.engee 与之前考虑的cCodeIntegration_source.engee 并无不同;只有Filter 块设置的Build options 选项卡上的指令不同。让我们来分析一下它的内容:
这里定义了以下结构选项:
*Source files - 连接源代码文件alphabetafilter.c 和alphabetafilter_data.c ;
*Include directories - 设置包含头文件的文件夹路径;
*Headers - 连接头文件alphabetafilter.h 、alphabetafilter_types.h 和rtwtypes.h 。
可循环执行的代码位于Filter 块源代码编辑器的Output code 选项卡上:
模型的每一步都要进行计算:
-
使用变量
in、processNoiseVariance和measurementNoiseVariance(输入信号)对结构alphabetafilter_U进行初始化; -
调用函数
alphabetafilter_step(); -
将存储在结构
alphabetafilter_Y中的结果x(输出信号)赋值给变量out。
在Filter 块源代码编辑器的开始代码选项卡中,函数alphabetafilter_initialize() 被调用:
在Filter 程序块源代码编辑器的终止代码选项卡中,函数alphabetafilter_terminate() 被调用:
模拟开始
让我们模拟系统:
cCodeIntegration_cg_simulink = engee.load("$demoroot/models/cCodeIntegration_cg_simulink.engee", force = true)
simulationResults_cg_simulink = engee.run(cCodeIntegration_cg_simulink)
关闭模型
engee.close(cCodeIntegration_cg_simulink, force = true);
模拟结果
导入模拟结果:
cCodeIntegration_cg_simulink_noisy_t = simulationResults_cg_simulink["noisy"].time;
cCodeIntegration_cg_simulink_noisy_y = simulationResults_cg_simulink["noisy"].value;
cCodeIntegration_cg_simulink_filtered_t = simulationResults_cg_simulink["filtered"].time;
cCodeIntegration_cg_simulink_filtered_y = simulationResults_cg_simulink["filtered"].value;
绘制图表
plot(cCodeIntegration_cg_simulink_noisy_t, cCodeIntegration_cg_simulink_noisy_y, label = "Исходный сигнал")
plot!(cCodeIntegration_cg_simulink_filtered_t, cCodeIntegration_cg_simulink_filtered_y, label = "Отфильтрованный сигнал")
plot!(legend = :bottomright)
title!("Альфа-бета фильтр <br> (Интеграция кода, сгенерированного из модели Simulink)")
xlabel!("Время, [с]")
ylabel!("Амплитуда")
整合 Engee 模型生成的代码
任务声明
让我们从模型 Engeealphabetafilter.engee 生成 C 代码,该代码由简单的块构建,实现了阿尔法-贝塔滤波器,并将其集成到我们的模型 EngeecCodeIntegration_cg_engee.engee 中。
模型的顶层:
子系统determineAlphaBeta :
子系统filterInputSignal :
让我们使用 Engee 代码生成器生成代码:
engee.generate_code("$demoroot/cg/engee/alphabetafilter.engee", "$demoroot/cg/engee/alphabetafilter_cg")
模型分析
从结构上看,模型cCodeIntegration_cg_engee.engee 与之前考虑的cCodeIntegration_source.engee 没有区别;只有Filter 块设置的Build options 选项卡上的指令不同。让我们来分析一下它的内容:
这里定义了以下结构选项:
*Source files - 连接源代码文件alphabetafilter.c ;
*Include directories - 设置包含头文件的文件夹路径;
*Headers - 连接的头文件alphabetafilter.h 。
可循环执行的代码位于Filter 块源代码编辑器的Output code 选项卡上:
模型的每一步都要进行计算:
-
使用变量
in、processNoiseVariance和measurementNoiseVariance(输入信号)对结构alphabetafilter_U进行初始化; -
调用函数
alphabetafilter_step(); -
将存储在结构
alphabetafilter_Y中的结果x(输出信号)赋值给变量out。
在Filter 块源代码编辑器的开始代码选项卡中,函数alphabetafilter_init() 被调用:
在Filter 程序块源代码编辑器的终止代码选项卡中,函数alphabetafilter_term() 被调用:
模拟开始
让我们模拟系统:
cCodeIntegration_cg_engee = engee.load("$demoroot/models/cCodeIntegration_cg_engee.engee", force = true)
simulationResults_cg_engee = engee.run(cCodeIntegration_cg_engee)
关闭模型
engee.close(cCodeIntegration_cg_engee, force = true);
模拟结果
导入模拟结果:
cCodeIntegration_cg_engee_noisy_t = simulationResults_cg_engee["noisy"].time;
cCodeIntegration_cg_engee_noisy_y = simulationResults_cg_engee["noisy"].value;
cCodeIntegration_cg_engee_filtered_t = simulationResults_cg_engee["filtered"].time;
cCodeIntegration_cg_engee_filtered_y = simulationResults_cg_engee["filtered"].value;
绘制图表
plot(cCodeIntegration_cg_engee_noisy_t, cCodeIntegration_cg_engee_noisy_y, label = "Исходный сигнал")
plot!(cCodeIntegration_cg_engee_filtered_t, cCodeIntegration_cg_engee_filtered_y, label = "Отфильтрованный сигнал")
plot!(legend = :bottomright)
title!("Альфа-бета фильтр <br> (Интеграция кода, сгенерированного из модели Engee)")
xlabel!("Время, [с]")
ylabel!("Амплитуда")
结论
本例演示了使用C 功能块的功能,以及将 C 代码集成到Engee模型中的不同方法:
- 集成外部源代码; 2;
- 集成静态和动态库; 3;
- 集成从Simulink模型生成的代码;
- 整合从Engee模型生成的代码。