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

Интеграция C кода в модели Engee с использованием блока C Function

В данном примере представлены различные способы интеграции C кода в модели Engee с использованием блока C Function.

Перед началом работы подключим необходимые библиотеки:

In [ ]:
using Plots
using MATLAB
plotlyjs();

И определим переменные, которые потребуются для запуска симуляций:

In [ ]:
Ts = 0.001;
Q = 1e-3;
R = 1e-4;

processNoisePower = Q * Ts;
measurementNoisePower = R * Ts;

Используем макрос @__DIR__ для того, чтобы узнать папку, в которой лежит интерактивный скрипт:

In [ ]:
demoroot = @__DIR__
Out[0]:
"/user/start/examples/base_simulation/ccodeintegration"

Основы использования блока C Function

Рассмотрим простые примеры использования блока C Function, иллюстрирующие основы интеграции C кода в модель Engee.

1. Работа с многомерными сигналами, передача параметров и использование директивы #define

В модель cCodeIntegration_basics.engee добавлен блок C Function, на вход которого поступает синусоидальный сигнал in:

basics_1.PNG

Предположим, что мы хотим увеличить амплитуду синусоиды in в gain раз и сдвинуть её на величину bias. То есть выходной сигнал должен определяться по формуле out = gain * in + bias.

Перейдём в настройки блока C Function, где во вкладке Ports мы ознакомимся с определением входа и выхода блока. Здесь связываются переменные исходного кода с портами блока C Function:

  1. Скалярная переменная x типа double связывается с первым входным портом in;
  2. Векторная (3 элемента) переменная y типа double связывается с первым выходным портом out.

image_2.png

Массив gain также определяется в настройках блока C Function (вкладка Parameters):

image.png

Стоит отметить, что после передачи переменной в качестве параметра она становится:

  1. Глобальной - к gain можно обратиться из любой вкладки редактора исходного кода;
  2. С квалификатором const - при попытке изменения значения gain будет получена ошибка.

Величина смещения bias определяется идентификатором #define bias, который задается во вкладке Build options:

image.png

В данном примере смещение равно 5.

Перейдём во владку Main настроек блока C Function и нажмём кнопку "Редактировать исходный код". В открывшемся редакторе исходного кода на вкладке OutputCode приведён код, исполняемый на каждом шаге расчёта модели:

image.png

В цикле for (для каждого из трёх выходных сигналов) на каждом шаге расчёта модели определяется значение переменной y.

Промоделируем систему:

In [ ]:
cCodeIntegration_basics = engee.load("$demoroot/models/cCodeIntegration_basics.engee", force = true)
simulationResults_basics = engee.run(cCodeIntegration_basics)
Out[0]:
Dict{String, DataFrames.DataFrame} with 1 entry:
  "out" => 1001×2 DataFrame

Закроем модель:

In [ ]:
engee.close(cCodeIntegration_basics, force = true);

Импортируем результаты моделирования:

In [ ]:
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];

Построим график:

In [ ]:
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!("Амплитуда")
Out[0]:

2. Использование целочисленных типов данных и статических переменных

В модели cCodeIntegration_integers.engee реализуем при помощи блока C Function простой счётчик, выходом которого будет положительное целое число, обозначающее номер текущей итерации:

ccodeintegration_integers_1.PNG

Проанализируем содержимое вкладок Ports и Build Options в настройках блока C Function:

image.png

image.png

Названия целочисленных типов данных в блоке C Function задаются в соответствии с заголовочным файлом <stdint.h> стандартной библиотеки языка C. Этот файл подключается в строке Headers вкладки Build options.

Во вкладке Ports связывается 64-битовая переменная без знака out (тип задаётся как uint64_t, а не long, см. предыдущий абзац про <stdint.h>) исходного кода с первым выходным портом блока C Function.

Во вкладке Output code редактора исходного кода блока C Function приведён код, исполняемый на каждом шаге моделирования:

image.png

Статическая переменная counter инициализируется в 0 на первом шаге симуляции, после чего на каждой итерации её значение увеличивается на 1 и присваивается переменной out.

Промоделируем систему:

In [ ]:
cCodeIntegration_integers = engee.load("$demoroot/models/cCodeIntegration_integers.engee", force = true)
simulationResults_integers = engee.run(cCodeIntegration_integers)
Out[0]:
Dict{String, DataFrames.DataFrame} with 1 entry:
  "counter" => 1001×2 DataFrame

Закроем модель:

In [ ]:
engee.close(cCodeIntegration_integers, force = true);

Импортируем результаты моделирования:

In [ ]:
cCodeIntegration_integers_t = simulationResults_integers["counter"].time;
cCodeIntegration_integers_y = simulationResults_integers["counter"].value;

Построим график:

In [ ]:
plot(cCodeIntegration_integers_t, cCodeIntegration_integers_y, legend = false)

title!("Выходной сигнал блока C Function <br> (Целочисленные типы и статические переменные)")
xlabel!("Время, [с]")
ylabel!("Номер итерации")
Out[0]:

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

Интеграция внешнего исходного кода

Постановка задачи

Рассмотрим более интересную инженерную задачу.

Предположим, что нам необходимо, учитывая шум измерений, управлять устойчивым объектом второго порядка, на который воздействуют внешние возмущения. Следовательно, нам потребуется решить задачу фильтрации выходного сигнала объекта управления, и, к счастью, в Engee есть инструмент, что отлично подходит для этого!

В файлах alphabetafilter.c и alphabetafilter.h предложена реализация альфа-бета фильтра на языке C.

Содержимое файла alphabetafilter.c:

#include "alphabetafilter.h"
#include <math.h>

double dt = 0.01;
static double xk_1, vk_1;

Parameters determineAlphaBeta(double processNoiseVariance, double measurementNoiseVariance)
{
    double lambda, r;
    Parameters 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);

    return p;
}

double alphaBetaFilter(double value, Parameters p)
{
    double xk, vk, rk;

    xk = xk_1 + (vk_1 * dt);
    vk = vk_1;

    rk = value - xk;

    xk += p.alpha * rk;
    vk += (p.beta * rk) / dt;

    xk_1 = xk;
    vk_1 = vk;

    return xk;
}

void initializeStaticVariables()
{
    xk_1 = 0;
    vk_1 = 0;
}

Содержимое файла alphabetafilter.h:

typedef struct {
    double alpha;
    double beta;
} Parameters;

Parameters determineAlphaBeta(double, double);
double alphaBetaFilter(double, Parameters);
void initializeStaticVariables();

Интегрируем данный исходный код в нашу модель Engee.

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

Модель cCodeIntegration_source.engee:

source_1.PNG

Данная модель состоит из:

  1. Объекта управления Plant;
  2. Подсистемы, реализующей ПИД-регулятор второго порядка Controller;
  3. Блоков Band-Limited White Noise ProcessNoise и MeasurementNoise, моделирующих внешнее воздействие и шум измерений;
  4. Блока C Function Filter, реализующего альфа-бета фильтр.

Параметр Sample Time блока Filter равен шагу решателя модели Ts:

image.png

Проанализируем содержимое вкладки Build options настроек блока Filter:

image.png

В строках этой вкладки:

  • Source files - подключен файл исходного кода alphabetafilter.c;
  • Iinclude directories - задан путь до папки, содержащей заголовочный файл;
  • Headers - подключен заголовочный файл alphabetafilter.h.

Во вкладке Ports настроек блока связываются переменные исходного кода с входными и выходными портами блока Filter.

В исходном коде блока Filter на вкладке OutputCode приведён циклически исполняемый код:

image.png

На каждом шаге расчёта модели последовательно вызываются функции determineAlphaBeta() и alphabetafilter(), после чего отфильтрованное значение сохраняется в переменную out, соответствующую выходному порту x блока Filter.

Запуск симуляции

Промоделируем систему:

In [ ]:
cCodeIntegration_source = engee.load("$demoroot/models/cCodeIntegration_source.engee", force = true)
simulationResults_source = engee.run(cCodeIntegration_source)
Out[0]:
Dict{String, DataFrames.DataFrame} with 2 entries:
  "filtered" => 6001×2 DataFrame…
  "noisy"    => 6001×2 DataFrame

Закроем модель:

In [ ]:
engee.close(cCodeIntegration_source, force = true);

Результаты моделирования

Импортируем результаты моделирования:

In [ ]:
cCodeIntegration_source_noisy_t = simulationResults_source["noisy"].time;
cCodeIntegration_source_noisy_y = simulationResults_source["noisy"].value;
In [ ]:
cCodeIntegration_source_filtered_t = simulationResults_source["filtered"].time;
cCodeIntegration_source_filtered_y = simulationResults_source["filtered"].value;

Построим график:

In [ ]:
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!("Амплитуда")
Out[0]:

Интеграция статических и динамических библиотек

Постановка задачи

Зачастую исходный код поставляется не в явном виде, а в составе статических или динамических библиотек. Библиотеки с интерфейсом C API интегрируются в модели Engee также легко, как и внешний исходный код!

Скомпилируем статическую (alphabetafilter.a) и динамическую (alphabetafilter.so) библиотеку из файлов alphabetafilter.c и alphabetafilter.h с использованием MakeFile.

Содержимое файла 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 $@
    
clean:
	rm -f *.o *.a *.so

Запустим компиляцию библиотек:

In [ ]:
run(`make -C $demoroot/source`)
make: Entering directory '/user/start/examples/base_simulation/ccodeintegration/source'
make: Nothing to be done for 'all'.
make: Leaving directory '/user/start/examples/base_simulation/ccodeintegration/source'
Out[0]:
Process(`make -C /user/start/examples/base_simulation/ccodeintegration/source`, ProcessExited(0))

Интегрируем статическую и динамическую библиотеку в нашу модель Engee.

Статическая библиотека: Анализ модели

По своей структуре модель cCodeIntegration_library_static.engee не отличается от рассмотренной ранее cCodeIntegration_source.engee; отличаются лишь директивы на вкладке Build options настроек блока Filter. Проанализируем её содержимое: