Appeler les bibliothèques partagées de Similink

Salut, Habr!
Je vous présente la traduction d'un article de mon collègue Mikhail sur les méthodes d'invocation des bibliothèques partagées dans Simulink. Pourquoi a-t-il été créé? Le fait est que de nombreuses entreprises ont déjà de nombreux modèles hérités que nous aimerions réutiliser et on nous pose souvent des questions: «Comment puis-je intégrer l'héritage dans Simulink? Et si mon héritage se présente sous la forme d'une DLL? " Par conséquent, l'article d'origine a été écrit.
Sous le chat, plusieurs façons d'appeler des bibliothèques partagées dans Simulink sont discutées.

Toutes les modifications apportées au code ont été apportées avec la permission de Mikhail et faites pour ne pas surcharger l'article.


Cet article montre comment utiliser les fonctions des bibliothèques partagées dans les modèles Simulink.
Disons que nous devons intégrer la bibliothèque exlib dans Simulink. Son code est contenu dans les sources d' exlib.c et d' exlib.h . Il s'agit d'une bibliothèque très simple contenant trois fonctions:
void exlib_init (void) - ouvre un fichier pour l'écriture.
void exlib_print (float data) - écrit des données dans un fichier.
void exlib_term (void) - ferme le fichier.

Assemblage de bibliothèque


Tout d'abord, compilez la bibliothèque.
Cette étape n'est pas nécessaire si la bibliothèque est précompilée et livrée sous forme de fichier binaire. Dans ce cas, vous pouvez immédiatement passer à l'étape 3, mais n'oubliez pas que pour travailler avec la bibliothèque, vous devez obtenir le fichier .dll (ou .so pour Linux) et le fichier d'en-tête correspondant (.h) auprès du fournisseur de bibliothèque. Pour Windows, vous aurez également besoin d'un fichier .lib, qui n'est pas une bibliothèque statique, mais la soi-disant bibliothèque d'importation. Cette bibliothèque d'importation est requise pour la liaison implicite.
Tout d'abord, nous utiliserons la commande MATLAB mex, qui appelle le compilateur hôte actif actuel pour compiler la bibliothèque partagée (exlib.dll sous Windows et exlib.so sous Linux). Il est important de s'assurer que la commande a été exécutée en premier
mex -setup


pour sélectionner un compilateur pris en charge .
Utilisez maintenant mex pour compiler la bibliothèque:

Code de construction d'une bibliothèque
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.



Vérification de la bibliothèque compilée


Après la compilation, vous devez vous assurer que la bibliothèque résultante peut être chargée et utilisée dans MATLAB:

Code de vérification de la bibliothèque
% 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


Maintenant que nous nous sommes assurés que tout fonctionne, commençons avec Simulink!

Appel d'une bibliothèque partagée à l'aide des fonctions S


Les fonctions S vous permettent d'utiliser le code C dans Simulink. Cette approche permet à l'utilisateur de développer tout code C nécessaire pour charger la bibliothèque et appeler ses fonctions. Ensuite, ce code est compilé dans un fichier binaire reconnu par Simulink et lié à une bibliothèque partagée. Ce binaire est appelé fonction S. Simulink a un bloc pour appeler cette fonction S. Il existe plusieurs approches pour créer des fonctions S dans Simulink.

Utilisation de l'outil de code hérité


La première approche consiste à utiliser l' outil de code hérité , un outil qui vous aide à créer automatiquement des fonctions S selon les spécifications créées à l'aide de MATLAB. Dans cet exemple, la fonction S est liée à la bibliothèque d'importation (sous Windows, nous avons besoin du fichier * .lib correspondant) et les fonctions de la bibliothèque sont appelées. Cette approche est appelée liaison implicite.
Tout d'abord, vous devez initialiser la structure de l'outil de code hérité
Code pour initialiser la structure de 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';



Créez une fonction S, compilez-la et liez-la:
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


Enfin, créez le bloc Simulink:
legacy_code('slblock_generate',specs);


Exécutez la simulation:
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


Écriture manuelle des fonctions S


La deuxième approche consiste à écrire vos propres fonctions S pour charger une bibliothèque partagée et appeler des fonctions à partir de cette bibliothèque. Cette approche utilisera une liaison explicite, également appelée liaison d'exécution. La solution la plus simple consiste à adapter la fonction S qui a été générée automatiquement précédemment par l'outil de code hérité. Dans cet exemple, les fonctions mdlStart , mdlOutputs et mdlTerminate sont modifiées .
Voici à quoi ressemblent ces fonctions après modification:
Afficher le code
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
}



Compilez la fonction S résultante:
if isunix
    mex('sfun_exlib_dyn.c','-ldl');
else
    mex('sfun_exlib_dyn.c');
end
Building with 'gcc'.
MEX completed successfully.


Et lancez la simulation:
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


Utilisation de S-Function Builder


Une autre approche consiste à utiliser le bloc S-Function Builder . Il s'agit d'une unité spéciale qui peut être considérée comme un croisement entre l'outil de code hérité et les fonctions S manuscrites en termes de complexité. S-Function Builder fournit une interface graphique qui décrit les caractéristiques de votre fonction S, et la fonction S est créée automatiquement.
Il existe des limitations de performances et des problèmes liés à l'utilisation de S-Function Builder. Actuellement, cela est considéré comme une approche obsolète pour créer des fonctions S. Il est recommandé d'utiliser le bloc de fonction C, qui est apparu dans la version de R2020a et est discuté plus tard.

Appel d'une bibliothèque partagée à l'aide de la fonction MATLAB


Le bloc fonction MATLAB vous permet d'utiliser le langage MATLAB pour décrire un algorithme personnalisé dans Simulink.
Pour appeler la bibliothèque partagée à partir de la fonction MATLAB, utilisez la fonction coder.ceval , qui ne peut être utilisée que dans le corps de la fonction MATLAB (et non MATLAB). Coder.ceval ne nécessite pas MATLAB Coder pour fonctionner.
Code du bloc fonction MATLAB appelant la bibliothèque partagée:
Afficher le code
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



Cette approche a un inconvénient - le manque d'un bon moyen d'appeler la fonction d'achèvement à la fin de la simulation (par rapport au bloc du système MATLAB).
Vous pouvez définir la fonction d'achèvement du modèle en définissant exlib_term (); dans les paramètres du modèle dans la catégorie Cible de simulation -> Code personnalisé -> Fonction d'arrêt .

REMARQUE . Si le paramètre «Importer du code personnalisé» est défini dans les paramètres du modèle, vous devez spécifier toutes les dépendances de code dans le panneau «Cible de simulation» (au lieu d'utiliser coder.cinclude et coder.updateBuildInfo ). Si ce paramètre n'est pas défini, vous pouvez combiner les paramètres de Simulation Target, coder.cinclude et coder.updateBuildInfo.


Une autre façon consiste à maintenir les dépendances à l'aidecoder.cinclude et coder.updateBuildInfo , et appelez exlib_term () par convention, comme illustré dans l'exemple ci-dessus.
Exécutez la simulation:
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


Appel d'une bibliothèque partagée à partir de Stateflow


Si vous souhaitez utiliser les fonctions de bibliothèque partagée dans les diagrammes Stateflow , ces fonctions doivent être appelées directement à partir de Stateflow. La documentation Stateflow contient des exemples qui montrent comment procéder.
L'appel d'une fonction externe dans Stateflow est très simple - vous devez spécifier le nom de la fonction dans le diagramme Stateflow:


En outre, vous devez configurer les paramètres du modèle afin que Stateflow sache où chercher ces fonctions externes. Dans les paramètres de Simulation Target -> Custom Code -> Libraries, vous devez saisir exlib.lib (ou exlib.so sous Linux) . Dans Cible de simulation -> Code personnalisé -> Fichier d'en-tête, vous devez saisir #include "exlib.h" . Il est également important de ne pas oublier de spécifier la fonction d'achèvement. ÀCible de simulation -> Code personnalisé -> La fonction de terminaison doit spécifier exlib_term (); .
Exécutez la simulation:
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


Notez que les informations contenues dans le présent article aux diagrammes Stateflow qui utilisent le langage d'action C . Si le diagramme Stateflow utilise le langage d'action MATLAB , alors coder.ceval est requis , comme c'est le cas avec la fonction MATLAB.

Appel d'une bibliothèque partagée à l'aide du bloc système MATLAB


Le bloc système MATLAB vous permet d'utiliser des objets système dans Simulink. Plus d'informations sur ce bloc peuvent être trouvées dans la documentation.
La prise en charge des objets système est apparue dans Simulink dans la version R2013b. De nombreuses personnes utilisent des objets système car ils facilitent la définition des fonctions d'initialisation, de simulation et d'achèvement. De plus, les objets système peuvent utiliser le code MATLAB auxiliaire pour ces fonctions - par exemple, pour les données de prétraitement et de post-traitement d'une fonction d'étape - le tout sans écrire de code C.
Voici à quoi ressemble l'objet système utilisé dans le bloc système MATLAB:
Code d'objet système
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



Exécutez la simulation:
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


Appel de bibliothèques partagées à l'aide du bloc C Caller


Le bloc C Caller vous permet d'appeler des fonctions C directement depuis Simulink. Plus d'informations sur ce bloc peuvent être trouvées dans la documentation.
Il s'agit d'une approche relativement nouvelle qui est apparue pour la première fois dans MATLAB R2018b. Son objectif principal est de rendre les appels de fonction et les bibliothèques C dans Simulink extrêmement simples. Mais il a des limites, que vous pouvez lire dans la documentation de ce bloc.
Une fois exlib.so/exlib.lib ajouté à Simulation Target -> Libraries et #include "exlib.h" dans Simulation Target -> Header dans les paramètres du modèle, cliquez simplement sur le bouton "Refresh custom code" dans le bloc C Caller, pour voir toutes les fonctions contenues dans la bibliothèque.
Après avoir sélectionné la fonction exlib_print , la boîte de dialogue de spécification de port est remplie automatiquement:

Et encore une fois, vous devez ajouter les appels de fonction exlib_init et exlib_term à la cible de simulation. Vous pouvez également ajouter quelques autres blocs C Caller pour appeler directement les fonctions d'initialisation et de terminaison. Ces blocs d'appel C doivent être placés dans les sous-systèmes Initialize Function et Terminate Function. Vous pouvez également considérer l'exemple suivant de Stateflow: Planifier des sous-systèmes à exécuter à des moments spécifiques
Exécutez la simulation:
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


Appel de bibliothèques partagées à l'aide du bloc fonction C


Le dernier ajout à l' intégration de code externe dans Simulink est le C Fonction bloc . Il est apparu dans le R2020a.
Il ressemble au bloc C Caller en termes de facilité d'utilisation, mais vous permet de créer un wrapper C autour des fonctions importées (ainsi, ce bloc ressemble à S-Function Builder). Mais, très probablement, le scénario principal pour utiliser le bloc fonction C n'est pas d'appeler les fonctions C existantes, mais d'écrire de petits morceaux de code C, si un tel code est nécessaire dans l'application. Par exemple, vous devrez peut-être accéder aux registres matériels ou aux fonctions intégrées du compilateur.
N'oubliez pas d'ajouter exlib.so/exlib.lib aux paramètres "Simulation Target -> Libraries" et #include "exlib.h"dans les paramètres "Simulation Target -> Header file" dans les paramètres du modèle.
Après cela, dans les paramètres du bloc fonction C, vous devez ajouter un caractère pour les données d'entrée avec le type de données unique et spécifier le code de sortie, de début et de fin:


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


Appel de bibliothèques partagées générées à l'aide d'Embedded Coder


L'un des scénarios d'utilisation d'Embedded Coder est la génération automatique de code C à partir du modèle Simulink et le conditionnement de ce code dans une bibliothèque partagée. La documentation contient également un exemple qui montre comment générer automatiquement une bibliothèque partagée et l'appeler à partir d'une application externe écrite en C.
Notez que si vous avez juste besoin d'exécuter l'algorithme ou une partie de l'algorithme en tant que code C dans le même modèle Simulink, il est préférable d'utiliser Fonctions S de Simulink Coder ou simulation logicielle en boucle. Cependant, si la bibliothèque partagée générée par le codeur intégré est utilisée pour la modélisation semi-naturelle, vous devrez peut-être intégrer la même bibliothèque dans le modèle plus grand utilisé par d'autres développeurs et protéger ainsi votre propriété intellectuelle.
Travailler avec une bibliothèque partagée générée par Embedded Coder n'est pas différent de travailler avec une bibliothèque partagée, qui était utilisée dans l'article. Considérons un modèle simple qui a deux entrées et une sortie:
open_system('simlib_test_ert');
snapnow;



Après avoir assemblé le modèle, nous obtenons un fichier .dll (.so sous Linux), un fichier .lib (une bibliothèque d'importation pour .dll) et un fichier .exp (un fichier d'exportation pour communiquer avec .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


La bibliothèque partagée générée exporte les caractères suivants:
ex_init
double ex_step(double, double)
simlib_test_ert_terminate


Par défaut, les entrées et les sorties sont exportées en tant que caractères globaux et les fonctions d'initialisation, d'étape et d'achèvement suivent la convention de dénomination du modèle. Si nécessaire, vous pouvez configurer des prototypes de fonctions, des noms de symboles, etc. (une discussion sur ces paramètres dépasse le cadre de cet article). Dans ce modèle, le prototype de la fonction pas à pas du modèle est défini comme Out1 = ex_step (In1, In2) .
Pour appeler ces fonctions, vous devez utiliser l'une des méthodes répertoriées ci-dessus. Par exemple, vous pouvez utiliser la fonction MATLAB (pour simplifier, nous appelons uniquement la fonction step):
Afficher le code
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);



Exécutez la simulation et regardez ses résultats:
open_system('simlib_test_callert');
sim('simlib_test_callert');
snapnow;



résultats


Cet article décrit différentes approches pour appeler des bibliothèques partagées à partir de modèles Simulink. Des liens implicites et explicites ont été pris en compte. Toutes les méthodes ont leurs avantages et leurs inconvénients, et leur pertinence dépend du flux de travail, des exigences et des objectifs spécifiques.
L' approche Legacy Code Tool fonctionne mieux lors de l'implémentation d'un lien implicite avec une bibliothèque partagée, car dans ce cas, nous pouvons simplement appeler des fonctions directement à partir de la bibliothèque, et l'éditeur de liens se chargera du reste.
Le bloc système MATLAB est une autre approche qui offre les avantages suivants:
  • Excellente conformité au paradigme des fonctions d'initialisation, d'étape et d'achèvement, vous permettant de maintenir toutes ces fonctions au sein du bloc lui-même, et non à l'échelle du modèle

  • MATLAB MATLAB
  • MATLAB System (, S-)

L'inconvénient de l'utilisation du bloc fonction MATLAB est que son utilisation peut entraîner une surcharge supplémentaire lors de la génération de code. Ainsi, le Legacy Code Tool et les fonctions S sont toujours préférés pour générer du code de production.
Une fonction S manuscrite est la mieux adaptée pour implémenter un lien explicite vers une bibliothèque partagée. Dans ce cas, vous devez utiliser des fonctions telles que LoadLibrary / dlopen , GetProcAddress / dlsym , FreeLibrary / dlclose pour pouvoir appeler des fonctions.
Le bloc C Caller introduit dans R2018b est de loin le moyen le plus simple d'appeler des fonctions C, mais il a ses limites. La même chose peut être dite pour le bloc fonction C.qui est le plus récent ajout au R2020a.

Téléchargez les sources et les modèles ici

All Articles