Представляю вашему вниманию перевод статьи моего коллеги Михаила, посвященной методам вызова разделяемых библиотек в Simulink. Зачем она была создана вообще? Дело в том, что у многих компаний уже есть множество легаси-моделей, которые хотелось бы переиспользовать и нам часто задают вопросы «А как мне легаси интегрировать в Simulink? А если мое легаси в виде DLL?» Поэтому-то и была написана оригинальная статья.
Под катом рассматривается несколько способов по вызову разделяемых библиотек в Simulink. Исходники и модели доступны на File Exchange
Все изменения в код внесены с разрешения Михаила и сделаны, что бы не перегружать статью.
В данной статье показано, как использовать функции из разделяемых библиотек в моделях 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 <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.
ссылка на оригинал статьи https://habr.com/ru/post/503908/
Добавить комментарий