• Регистрация
Nikolay
Nikolay +2194.92
н/д

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

01.06.2020

Представляю вашему вниманию перевод статьи посвященной методам вызова разделяемых библиотек в Simulink. Зачем она была создана вообще? Дело в том, что у многих компаний уже есть множество моделей, которые хотелось бы переиспользовать. При этом, зачастую исходный код моделей недоступен и сам модели - это DLL или *.so, то есть разделяемые библиотеки. Поэтому и была написана оригинальная статья. Рассматривается несколько способов вызова разделяемых библиотек в 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

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 ');
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. Скачать исходники и модели можно здесь

Теги

    01.06.2020

    Комментарии