Вызов разделяемых библиотек из Simulink

Автор оригинала: Михаил Песельник
  • Перевод
Привет, хабр!
Представляю вашему вниманию перевод статьи моего коллеги Михаила, посвященной методам вызова разделяемых библиотек в Simulink. Зачем она была создана вообще? Дело в том, что у многих компаний уже есть множество легаси-моделей, которые хотелось бы переиспользовать и нам часто задают вопросы «А как мне легаси интегрировать в Simulink? А если мое легаси в виде DLL?» Поэтому-то и была написана оригинальная статья.
Под катом рассматривается несколько способов по вызову разделяемых библиотек в Simulink.

Все изменения в код внесены с разрешения Михаила и сделаны, что бы не перегружать статью.


В данной статье показано, как использовать функции из разделяемых библиотек в моделях Simulink.
Допустим, нам надо встроить в Simulink библиотеку exlib. Ее код содержится в исходниках exlib.c и exlib.h. Это очень простая библиотека, содержащая три функции:
void exlib_init(void) – открывает файл на запись.
void exlib_print(float data) – записывает данные в файл.
void exlib_term(void) – закрывает файл.

Сборка библиотеки


Для начала скомпилируем библиотеку.
Этот шаг не нужен, если библиотека предварительно скомпилирована и поставляется в виде двоичного файла. В этом случае сразу можно перейти к шагу 3, но надо помнить, что для работы с библиотекой нужно получить файл .dll (или .so для Linux) и соответствующий файл заголовка (.h) у поставщика библиотеки. Для Windows также понадобится файл .lib, который представляет собой не статическую библиотеку, а так называемую библиотеку импорта. Эта библиотека импорта потребуется для неявной линковки.
Сначала, будем использовать команду MATLAB mex, которая вызывает текущий активный компилятор хоста для компиляции общей библиотеки (exlib.dll в Windows и exlib.so в Linux). Важно убедиться, что сначала была выполнена команда
mex -setup


для выбора поддерживаемого компилятора.
Теперь используем mex для компиляции библиотеки:

Код для сборки библиотеки
mingw = strfind(mex.getCompilerConfigurations('C','Selected').Name,'MinGW64 Compiler');
if isunix
    % GCC
    mex('LDEXT=.so','LINKEXPORT=','LINKEXPORTVER=','LINKLIBS=','exlib.c');
elseif mingw
    % MinGW builds static shared library, so dll and lib files are the same
    % loadlibrary uses the dll file, while legacy code tool looks for the lib file
    mex('LDEXT=.lib','LINKEXPORT=','LINKEXPORTVER=','LINKLIBS=','exlib.c');
    mex('LDEXT=.dll','LINKEXPORT=','LINKEXPORTVER=','LINKLIBS=','exlib.c');
else
    % Visual
    mex('LDEXT=.dll','LINKEXPORT=','LINKEXPORTVER=','CMDLINE300="del exlib.exp exlib.dll.manifest"','exlib.c');
end
Building with 'gcc'.
MEX completed successfully.



Проверка скомпилированной библиотеки


После компиляции необходимо удостоверится в том, что полученная библиотека может быть подгружена и использована в MATLAB:

Код для проверки библиотеки
% Load the library
[~,~] = loadlibrary(['exlib',system_dependent('GetSharedLibExt')],'exlib.h');
% Display functions available in the library
libfunctionsview('exlib');
% Initialize
calllib('exlib','exlib_init');
% Step
for i = 1:10
    calllib('exlib','exlib_print',single(i));
end
% Terminate
calllib('exlib','exlib_term');
% Unload the library
unloadlibrary('exlib');
% Show contents of generated file
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Теперь, когда мы удостоверились, что все работает, начнем работать с Simulink!

Вызов разделяемой библиотеки с помощью S-функций


S-функции позволяют использовать C-код в Simulink. Этот подход позволяет пользователю разработать любой код на C, необходимый для загрузки библиотеки и вызова ее функций. Затем, этот код компилируется в бинарный файл, который распознается Simulink и связывается с общей библиотекой. Этот двоичный файл называется S-функцией. В Simulink есть блок для вызова этой S-функции. Существует несколько подходов для создания S-функций в Simulink.

Использование Legacy Code Tool


Первым подходом является использование Legacy Code Tool, инструмента, который помогает автоматически создавать S-функции по спецификациям, созданным с помощью MATLAB. В этом примере S-функция линкуется с библиотекой импорта (в Windows нам потребуется соответствующий файл * .lib) и происходит вызов библиотечных функций. Этот подход называется неявной линковкой.
Сначала, требуется инициализировать структуру для Legacy Code Tool
Код для инициализации структуры Legacy Code Tool
specs = legacy_code('initialize');
% Prototype for the initialization function
specs.StartFcnSpec = 'exlib_init()';
% Prototype for the step function
specs.OutputFcnSpec = 'exlib_print(single u1)';
% Prototype for the terminate function
specs.TerminateFcnSpec = 'exlib_term()';
% Shared library to link with (.so on Linux and .dll on Windows)
specs.HostLibFiles = {['exlib',strrep(system_dependent('GetSharedLibExt'),'.dll','.lib')]};
% We must supply header file when linking with shared library, otherwise
% compiler might make wrong assumptions about function's prototype.
specs.HeaderFiles = {'exlib.h'};
specs.SFunctionName = 'sfun_exlib';



Создадим S-функцию, скомпилируем и слинкуем ее:
legacy_code('generate_for_sim',specs);
### Start Compiling sfun_exlib
    mex('sfun_exlib.c', '-I/tmp/simulink_shrlib_fex', '/tmp/simulink_shrlib_fex/exlib.so')
Building with 'gcc'.
MEX completed successfully.
### Finish Compiling sfun_exlib
### Exit


Наконец, создадим блок Simulink:
legacy_code('slblock_generate',specs);


Запустим симуляцию:
open_system('simlib_test');
snapnow;
sim('simlib_test');
% Observe the results:
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Написание S-функций вручную


Второй подход заключается в самостоятельном написании собственных S-функций для загрузки разделяемой библиотеки и вызова функций из этой библиотеки. В этом подходе будет использоваться явная линковка, также называемая линковкой во время выполнения. Самым простым решением является адаптация S-функции, автоматически сгенерированной ранее Legacy Code Tool. В этом примере изменяются функции mdlStart, mdlOutputs и mdlTerminate.
Вот как эти функции выглядят после модификации:
Посмотреть код
void mdlStart(SimStruct *S)
{
  /*
   * Load the dynamic library
   */

  #if defined(__GNUC__) && !defined(__MINGW32__)
    void (*exlib_init_ptr)(void);
    dllHandle = dlopen("./exlib.so",RTLD_LAZY);
    exlib_init_ptr = dlsym(dllHandle, "exlib_init");
  #else
    exlib_init_type exlib_init_ptr = NULL;
    dllHandle = LoadLibrary("exlib.dll");
    exlib_init_ptr = (exlib_init_type)GetProcAddress(dllHandle,"exlib_init");
  #endif

  exlib_init_ptr();
}


void mdlOutputs(SimStruct *S, int_T tid)
{
  real32_T *u1 = 0;

  #if defined(__GNUC__) && !defined(__MINGW32__)
    void (*exlib_print_ptr)(float);
    exlib_print_ptr = dlsym(dllHandle,"exlib_print");
  #else
    exlib_print_type exlib_print_ptr = NULL;
    exlib_print_ptr = (exlib_print_type)GetProcAddress(dllHandle,"exlib_print");
  #endif

  /*
   * Get access to Parameter/Input/Output/DWork/size information
   */
  u1 = (real32_T *) ssGetInputPortSignal(S, 0);

  /*
   * Call the function from library
   */
  exlib_print_ptr( *u1);
}


void mdlTerminate(SimStruct *S)
{
  /*
   * Unload the dynamic library
   */

  #if defined(__GNUC__) && !defined(__MINGW32__)
    void (*exlib_term_ptr)(void);
    exlib_term_ptr = dlsym(dllHandle,"exlib_term");
    exlib_term_ptr();
    dlclose(dllHandle);
  #else
    exlib_term_type exlib_term_ptr = NULL;
    exlib_term_ptr = (exlib_term_type)GetProcAddress(dllHandle,"exlib_term");
    exlib_term_ptr();
    FreeLibrary(dllHandle);
  #endif
}



Скомпилируем полученную S-функцию:
if isunix
    mex('sfun_exlib_dyn.c','-ldl');
else
    mex('sfun_exlib_dyn.c');
end
Building with 'gcc'.
MEX completed successfully.


И запустим симуляцию:
open_system('simlib_test_dyn');
sim('simlib_test_dyn');
% Observe the results:
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Использование S-Function Builder


Еще один подход заключается в использовании блока S-Function Builder. Это специальный блок, который может рассматриваться как нечто среднее между Legacy Code Tool и написанными вручную S-функциями с точки зрения сложности. S-Function Builder предоставляет графический интерфейс, в котором описываются характеристики вашей S-функции, и S-функция создается автоматически.
При использовании S-Function Builder существуют некоторые ограничения и проблемы с производительностью. В настоящее время это считается устаревшим подходом к созданию S-функций. Рекомендуется использовать блок C function, который появился в релизе R2020а и обсуждается позже.

Вызов разделяемой библиотеки с помощью MATLAB Function


Блок MATLAB Function позволяет использовать язык MATLAB для описания пользовательского алгоритма в Simulink.
Для вызова разделяемой библиотеки из функции MATLAB используется функция coder.ceval, которую можно использовать только в теле MATLAB Function (а не MATLAB). Для работы coder.ceval не требуется наличие MATLAB Coder.
Код для блока MATLAB Function, вызывающий разделяемую библиотеку:
Посмотреть код
function fcn(u)
%#codegen

% Keep track of initialization and runtime count
persistent runTimeCnt

% Generate library path on the fly (current directory in this case)
coder.extrinsic('pwd','system_dependent');
libpath = coder.const(pwd);
% Shared library to link with
libname = coder.const(['exlib',strrep(system_dependent('GetSharedLibExt'),'.dll','.lib')]);
% Add the external library. Mark it as precompiled, so it won't appear as
% makefile target during code generation.
coder.updateBuildInfo('addLinkObjects',libname,libpath,1000,true,true);
coder.updateBuildInfo('addIncludePaths',libpath);
coder.cinclude('exlib.h');

if isempty(runTimeCnt)
    % Initialize
    coder.ceval('exlib_init');
    runTimeCnt = 0;
end
% Step
coder.ceval('exlib_print',single(u));
runTimeCnt = runTimeCnt+1;
% Terminate on the 10th step
if (runTimeCnt == 11)
    coder.ceval('exlib_term');
end



Данный подход имеет один недостаток – отсутствие хорошего способа вызвать функцию завершения при окончании симуляции (по сравнению с блоком MATLAB System).
Можно определить функцию завершения для модели, поставив exlib_term (); в настройках модели в категории Simulation Target -> Custom Code -> Terminate function.

ПРИМЕЧАНИЕ. Если в настройках модели установлен параметр «Import custom code», то требуется указать все зависимости кода на панели «Simulation Target» (вместо использования coder.cinclude и coder.updateBuildInfo). Если этот параметр не установлен, то можно объединить настройки из Simulation Target, coder.cinclude и coder.updateBuildInfo.


Другой способ — поддерживать зависимости, используя coder.cinclude и coder.updateBuildInfo, и вызывать exlib_term() по условию, как продемонстрировано в примере выше.
Запустим симуляцию:
open_system('simlib_test_mlf');
sim('simlib_test_mlf');
% Observe the results:
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Вызов разделяемой библиотеки из Stateflow


Если требуется использовать функции разделяемой библиотеки в диаграммах Stateflow, то эти функции следует вызывать напрямую из Stateflow. В документации Stateflow есть примеры, которые показывают, как это сделать.
Вызов внешней функции в Stateflow очень прост – требуется указать имя функции в диаграмме Stateflow:


Дополнительно, необходимо настроить параметры модели, чтобы Stateflow знал, где искать эти внешние функции. В настройках Simulation Target -> Custom Code -> Libraries требуется ввести exlib.lib (или exlib.so в Linux). В Simulation Target -> Custom Code -> Header File требуется ввести #include «exlib.h». Так же важно не забыть указать функцию завершения. В Simulation Target -> Custom Code -> Terminate function необходимо указать exlib_term() ;.
Запустим симуляцию:
if isunix
    set_param('simlib_test_sf','SimUserLibraries','exlib.so');
end
sim('simlib_test_sf');
% Observe the results:
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Обратите внимание, что информация в этом разделе относится к диаграммам Stateflow, использующим язык действий C. В том случае, если диаграмма Stateflow использует язык действий MATLAB, то требуется использовать coder.ceval, как и для MATLAB Function.

Вызов разделяемой библиотеки с помощью блока MATLAB System


Блок MATLAB System позволяет использовать системные объекты в Simulink. Более подробную информацию об этом блоке можно найти в документации.
Поддержка системных объектов появилась в Simulink в релизе R2013b. Множество людей используют системные объекты, поскольку они позволяют легко определять функции инициализации, шага симуляции и завершения. Также системные объекты могут использовать вспомогательный код MATLAB для этих функций — например, для пред- и постобработки данных функции шага — и все это без написания кода на C.
Вот как выглядит системный объект, используемый в блоке MATLAB System:
Код системного объекта
classdef exlib < matlab.System
    % Call exlib shared library
    %
    % This example shows how to call shared library from Simulink using
    % MATLAB System block.
    properties (Nontunable,Access=private)
        libName = exlib.getLibName;
        libPath = pwd;
        libHeader = 'exlib.h';
    end
    
    methods (Static)
        function libName = getLibName
            if isunix
                libName = 'exlib.so';
            else
                libName = 'exlib.lib';
            end
        end
    end
    
    methods (Access=protected)
        function setupImpl(obj, ~)
            % Initialize.
            coder.updateBuildInfo('addLinkObjects',obj.libName,obj.libPath,1000,true,true);
            coder.updateBuildInfo('addIncludePaths',obj.libPath);
            coder.cinclude(obj.libHeader);
            coder.ceval('exlib_init');
        end
        
        function stepImpl(~, u)
            % Step.
            coder.ceval('exlib_print',u);
        end
        
        function releaseImpl(~)
            % Terminate.
            coder.ceval('exlib_term');
        end
    end
end



Запустим симуляцию:
open_system('simlib_test_mls');
sim('simlib_test_mls');
% Observe the results:
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Вызов разделяемых библиотек с помощью блока C Caller


Блок C Caller позволяет вызывать функции C напрямую из Simulink. Более подробную информацию об этом блоке можно найти в документации.
Это относительно новый подход, который впервые появился в MATLAB R2018b. Его основная цель — сделать в Simulink вызовы функций и библиотек на C черезвычайно простыми. Но у него есть ограничения, о которых вы можете прочитать в документации по этому блоку.
После того, как exlib.so/exlib.lib были добавлены в Simulation Target -> Libraries и #include «exlib.h» в Simulation Target -> Header в настройках модели, достаточно нажатия кнопки «Refresh custom code» в блоке C Caller, чтобы увидеть все функции, содержащиеся в библиотеке.
После выбора функции exlib_print, диалог спецификации портов заполняется автоматически:

И снова, требуется добавить вызовы функций exlib_init и exlib_term, в Simulation Target. Также можно добавить пару других блоков C Caller для непосредственного вызова функций инициализации и завершения. Данные блоки C Caller потребуется поместить в подсистемы Initialize Function и Terminate Function. Так же можно рассмотреть следующий пример из Stateflow: Schedule Subsystems to Execute at Specific Times
Запустим симуляцию:
open_system('simlib_test_ccaller');
if isunix
    set_param('simlib_test_ccaller','SimUserLibraries','exlib.so');
end
sim('simlib_test_ccaller');
% Observe the results:
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Вызов разделяемых библиотек с помощью блока C Function


Новейшим дополнением к интеграции внешнего кода в Simulink является блок C Function. Он появился в R2020a.
Он напоминает блок C Caller с точки зрения простоты использования, но позволяет создавать код-обертку C вокруг импортируемых функций (таким образом, данный блок напоминает S-Function Builder). Но, скорее всего, основной сценарий использования блока C Function заключается не в вызове существующих функций C, а в написании небольших фрагментов кода на языке C, если такой код необходим в приложении. Например, может потребоваться доступ к аппаратным регистрам или встроенным функциям компилятора.
Не забудем добавить exlib.so/exlib.lib в настройки «Simulation Target -> Libraries» и #include «exlib.h» в настройки «Simulation Target -> Header file» в настройках модели.
После этого в настройках блока C Function надо добавить символ для входных данных с типом данных single и указать код вывода, запуска и завершения:


open_system('simlib_test_cfunction');
if isunix
    set_param('simlib_test_cfunction','SimUserLibraries','exlib.so');
end
sim('simlib_test_cfunction');
% Observe the results:
type('exlib.txt');
0.000000
1.000000
2.000000
3.000000
4.000000
5.000000
6.000000
7.000000
8.000000
9.000000
10.000000


Вызов разделяемых библиотек сгенерированных с помощью Embedded Coder


Одним из сценариев использования Embedded Coder является автоматическая генерация C-кода из модели Simulink и упаковка этого кода в разделяемую библиотеку. В документации также есть пример, который показывает, как автоматически генерировать разделяемую библиотеку и вызывать ее из внешнего приложения, написанного на C.
Обратите внимание, что если требуется просто запуск алгоритма или части алгоритма в виде C кода в той же модели Simulink, то лучше применить S-функции Simulink Coder или симуляции в режиме «ПО-в-контуре» (Software-in-the-Loop). Однако, если сгенерированная с помощью Embedded Coder разделяемая библиотека, используется для полунатурного моделирования, то может потребоваться интегрировать эту же библиотеку в более крупную модель, используемую другими разработчиками и таким образом защитить свою интеллектуальную собственность.
Работа с разделяемой библиотекой, сгенерированная Embedded Coder, ничем не отличается от работы с разделяемой библиотекой, которая была использована в статье. Рассмотрим простую модель, которая имеет два входа и один выход:
open_system('simlib_test_ert');
snapnow;



После сборки модели мы получим файл .dll (.so в Linux), файл .lib (библиотека импорта для .dll) и файл .exp (файл экспорта для связи с .dll).
if isunix
    set_param('simlib_test_ert','CustomHeaderCode','#include <stddef.h>');
end
rtwbuild('simlib_test_ert');
### Starting build procedure for: simlib_test_ert
### Successful completion of build procedure for: simlib_test_ert


Сгенерированная разделяемая библиотека экспортирует следующие символы:
ex_init
double ex_step(double, double)
simlib_test_ert_terminate


По умолчанию входы и выходы экспортируются как глобальные символы, а функции инициализации, шага и завершения следуют соглашению об именах моделей. При необходимости можно настроить прототипы функций, имена символов и прочее (обсуждение данных настроек выходит за рамки данной статьи). В этой модели прототип функции шага модели задан как Out1 = ex_step (In1, In2).
Для вызова этих функций нужно применить один из перечисленных выше методов. Например, можно использовать MATLAB Function (для простоты вызовем только функцию шага):
Посмотреть код
function y = fcn(u1, u2)
%#codegen

% Generate library path on the fly
coder.extrinsic('RTW.getBuildDir','fullfile');
buildDir = coder.const(RTW.getBuildDir('simlib_test_ert'));
libpath = coder.const(buildDir.CodeGenFolder);
incpath = coder.const(fullfile(buildDir.BuildDirectory,'simlib_test_ert.h'));
% Shared library to link with
if isunix
    ext = '.so';
    libname = ['simlib_test_ert',ext];
else
    ext = '.lib';
    libname = ['simlib_test_ert_',computer('arch'),ext];
end
% Add the external library. Mark it as precompiled, so it won't appear as
% makefile target during code generation.
coder.updateBuildInfo('addLinkObjects',libname,libpath,1000,true,true);
coder.updateBuildInfo('addIncludePaths',libpath);
coder.cinclude(incpath);

% Initialize output
y = 0;
% Step
y = coder.ceval('ex_step',u1,u2);



Запустим симуляцию и посмотрим на ее результаты:
open_system('simlib_test_callert');
sim('simlib_test_callert');
snapnow;



Выводы


В этой статье описаны различные подходы к вызову разделяемых библиотек из моделей Simulink. Были рассмотрены как неявная, так и явная линковки. Все методы имеют свои плюсы и минусы, и их пригодность зависит от конкретного рабочего процесса, требований и целей.
Подход Legacy Code Tool лучше всего подходит при реализации неявной линковки с разделяемой библиотекой, поскольку в этом случае мы можем просто вызывать функции непосредственно из библиотеки, а компоновщик позаботится обо всем остальном.
Блок MATLAB System — это еще один подход, который обеспечивает следующие преимущества:
  • Отличное соответствие парадигме функций инициализации, шага и завершения, позволяя поддерживать все эти функции внутри самого блока, а не в масштабе модели
  • Позволяет добавлять пользовательский код к вызываемым внешним функцям
  • Позволяет вам оставаться в MATLAB и писать только код MATLAB
  • Блок MATLAB System является автономным и не требует дополнительных шагов для совместного использования (например, нет необходимости компилировать S-функцию)

Недостатком использования блока MATLAB Function является то, что он его применение может повлечь дополнительный оверхэд во время генерации кода. Таким образом, Legacy Code Tool и S-функции по-прежнему являются предпочтительными для генерации производственного кода.
Написанная вручную S-функция лучше всего подходит для реализации явной линковки с разделяемой библиотекой. В этом случае требуется использовать такие функции, как LoadLibrary/dlopen, GetProcAddress/dlsym, FreeLibrary/dlclose, чтобы иметь возможность вызывать функции.
Блок C Caller, появившийся в R2018b, безусловно, самый простой способ вызова функций C, но он имеет свои ограничения. То же самое можно сказать и о блоке C Function, который является новейшим дополнением в R2020a.

Скачать исходники и модели можно здесь

UPD:
Ссылка на скачивание материалов была обновлена

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое