週末!プログラミング部

ソフトウェア開発ネタを中心に自分でいろいろ調べた内容を自分の勝手な解釈で思うがままに書いくためのブログ。サンプルソースコード、API、プラットフォーム、プログラミング言語、開発環境などを調査、分析して追求いく予定です。

SimulinkとC#で共有メモリを使ったプロセス間通信をさせてみる

前回は、MATLABC#で共有メモリを使ったプロセス間通信をさせてみました。
今回は、SimulinkC#で共有メモリを使ったプロセス間通信をさせてみたいと思います。

SimulinkからCコードを呼び出すにはS-Functionブロックというものがあります。
また、S-Functionブロックをより簡単に作ることができるS-Function Builderというものもあります。
https://jp.mathworks.com/help/simulink/c-c-s-functions.html
https://jp.mathworks.com/help/simulink/s-function-builder.html

今回は、前回のCライブラリをS-Function Builderを使ってブロック化してSimulinkから利用してみたいと思います。

モデル内で共有メモリをどのようにして試すか

以下は、Signal Generatorブロックを使用して正弦波を出力するとても簡単なモデル例です。
シミュレーション時間は、モデルが動き続けるようにするために「inf」にしてあります。
f:id:NATSU_NO_OMOIDE:20201230135833g:plain

この例では、Constantブロックの値( = 1)をProductブロックで乗算して正弦波の振幅を変えられるようにしています。
Constantブロックの値は、モデルを実行すると変更できなくなってしまいます。
そこで、ConstantブロックをS-Functionブロックに置き換えて、共有メモリを使ってモデル実行中でも正弦波の振幅を変えられるようにしたいと思います。

共有メモリを操作するブロックを作る

モデルを改造してConstantブロックをS-Function Builderに置き換えています。
S-Functionブロックは、入力があったときに実行されます。
したがって、Signal Generatorブロックの信号をS-Functionブロックの入力にして実行契機を与えています。
S-Functionブロックの出力は、Productブロックに接続してSignal Generatorブロックの出力に乗算するようにしました。

f:id:NATSU_NO_OMOIDE:20201230142629p:plain

S-Function BuilderをダブルクリックするとEditerが起動してCコードを書けます。
<system_name>Start_wrapper、<system_name>Terminate_wrapper、<system_name>_Outputs_wrapperの中身を実装していきます。
https://jp.mathworks.com/help/simulink/sfg/s-function-builder-dialog-box.html
以下はコード例です。いつもどおり手抜きです笑

/* Includes_BEGIN */
#include <math.h>
#include <windows.h>
/* Includes_END */

/* Externs_BEGIN */
/* extern double func(double a); */

typedef struct
{
    double   p1;
    short    p2;
    int      p3;
} SHARED_MEMORY_DATA;

static HANDLE              gSharedMemoryHandle = NULL;
static SHARED_MEMORY_DATA* gMappingObject      = NULL;
/* Externs_END */

void shmem_block_Start_wrapper(void)
{
/* Start_BEGIN */
gSharedMemoryHandle = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, "shmem");
    gMappingObject = (SHARED_MEMORY_DATA*)MapViewOfFile( gSharedMemoryHandle, 
                                                         FILE_MAP_ALL_ACCESS,
                                                         0,
                                                         0,
                                                         sizeof(SHARED_MEMORY_DATA));
/* Start_END */
}

void shmem_block_Outputs_wrapper(const real_T *u0,
                                 real_T *y0)
{
/* Output_BEGIN */

 /* C#から受け取った値 */
 y0[0] = gMappingObject->p3;
    
 /* C#に送る値 */
 gMappingObject->p1 = (double)u0[0];
 gMappingObject->p2++;
 if(gMappingObject->p2 > 100)
 {
     gMappingObject->p2 = 0;
 }
/* Output_END */
}

void shmem_block_Terminate_wrapper(void)
{
/* Terminate_BEGIN */
/*
 * カスタム終了コードをここに配置します。
 */
    UnmapViewOfFile(gMappingObject);
    CloseHandle(gSharedMemoryHandle);
/* Terminate_END */
}
SHARED_MEMORY_DATA構造体

今回は少し手抜きですがextern宣言をする箇所に共有メモリマッピング構造体SHARED_MEMORY_DATAの宣言をしています。
SHARED_MEMORY_DATA構造体は、p1(double型)、p2(short型)、p3(int型)のメンバ変数を持ちます。

<system_name>_Start_wrapper

シミュレーションの開始時に呼び出される関数です。
ここで共有メモリのオープンとマッピングオブジェクトを作成しています。

<system_name>_Outputs_wrapper

ブロックに入力があったときに呼び出される関数です。
この関数の引数であるu0がブロックに入力された信号、y0がブロックが出力する信号に該当します。
ここではSHARED_MEMORY_DATA構造体のメンバ変数p3をそのままブロックの出力にしました。
またSHARED_MEMORY_DATA構造体のメンバ変数p2にはブロックが呼び出される度にカウンタした値を設定し、 メンバ変数p1にはSignal Generatorブロックからの入力信号をそのまま設定しています。

<system_name>_Terminate_wrapper

シミュレーションの終了時に呼び出される関数です。
ここで共有メモリのクローズとマッピングオブジェクトを解放しています。

C#アプリケーション

C#側はMATLABとC#で共有メモリを使ったプロセス間通信で作成したSharedMemoryクラスを再利用するこにします。
でも、これだけでは味気ないので以下のような簡単なGUIを作成してみましたヽ( ´¬`)ノ f:id:NATSU_NO_OMOIDE:20201230174529p:plain

以下、ソースコードです。
System.Windows.Forms.Timerを使って100ms毎に呼び出されるイベントハンドラ内で共有メモリからp1、p2の値を呼び出しています。
またボタンが押下されたら共有メモリのp3の値を更新しています。

using System;
using System.Windows.Forms;

namespace shmem_gui
{
    public struct sh_mem_data_st
    {
        public double p1;
        public short p2;
        public int p3;
    }

    public partial class Form1 : Form
    {
        private SharedMemory<sh_mem_data_st> shmem = null;

        public Form1()
        {
            InitializeComponent();

            // 共有メモリオープン
            shmem = new SharedMemory<sh_mem_data_st>();
            shmem.open("shmem");
        }

        /// <summary>
        /// 書き込みボタン押下イベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void write_button_Click(object sender, EventArgs e)
        {
            sh_mem_data_st st = new sh_mem_data_st();
            st.p3 = int.Parse(textBox3.Text);
            shmem.wrtie(st);
        }

        /// <summary>
        /// 100ms tickイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void timer1_Tick(object sender, EventArgs e)
        {
            sh_mem_data_st result = new sh_mem_data_st();
            shmem.read(ref result);
            textBox1.Text = result.p1.ToString();
            textBox2.Text = result.p2.ToString();
        }
    }
}

動かしてみる

以下、動作イメージです。

f:id:NATSU_NO_OMOIDE:20201230175456g:plain

シミュレーションを開始するとsimulinkが共有メモリを更新します。
その結果、C#アプリケーションのp1とp2の値も更新されます。
またC#アプリケーションでp3の値を変更すると、simulinkが描画している正弦波の振幅が変化するはずです。
こんな風に動けば成功です。

ただ、以前も書いたとおりC#側SharedMemoryクラスでは構造体のメンバ変数単位で共有メモリの読み書きを行うように作っていません。
そのためC#アプリケーションで書き込みを行う際にp3の値だけでなく、p1、p2の値も同時に書き込んでしまうため、simulinkが更新した値を上書きしてしまう可能性があります。
ここは改良の必要ありですね。。。
また、共有メモリからの読み込みはSystem.Windows.Forms.Timerを使用していますが、このTimerの精度は50ms程度(・・・だとどこかで読んだ気がします)のようです。
したがって、読み込んだ値を描画しようとした場合、かなりカクつくことが想定されます。
しかし、そんな制限はいろいろとあるものHTTPインタフェースを使用してMATLABと交信するよりは高速に動作するような気もします。
なにか使い道があれば幸いです。