週末!プログラミング部

ソフトウェア開発ネタを中心に自分でいろいろ調べた内容を自分の勝手な解釈で思うがままに書いくためのブログ。サンプルソースコード、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と交信するよりは高速に動作するような気もします。
なにか使い道があれば幸いです。

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

今回はMATLABC#で共有メモリを使ったプロセス間通信を試みたいと思います。

MATLAB同士で共有メモリを使ったプロセス間通信としてmemmapfileを使う例があります。
しかし、memmapfileはページファイル内のメモリマップの読み書きはできません。
また、メモリマッピング用のMATLAB関数はいまのところmemmapfileだけのようです。
したがってMATLAB単体ではページファイルのメモリマップにアクセスすることができないため、CのMapViewOfFile()やC#のSystem.IO.MemoryMappedFilesとの連携は難しそうです。

そこで以前行った、「CとC#で共有メモリを使ったプロセス間通信」と 「MATLABからCライブラリを呼び出し」を組み合わせて、 MATLABC#の間にCライブラリを挟むことで、間接的に共有メモリによるプロセス間通信をさせてみたいと思います。

共有メモリ通信を行うCライブラリを作る

まずは、ヘッダファイルです。
共有メモリ上に配置するデータ構造体やライブラリ関数のプロトタイプを定義してあります。

#ifndef SHMEM_H
#define SHMEM_H

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

__declspec(dllexport) void OpenSharedMemory(void);
__declspec(dllexport) void CloseSharedMemory(void);
__declspec(dllexport) SHARED_MEMORY_DATA* GetSharedMemoryObject(void);

#endif

続いて、共通メモリ通信を行うライブラリ実装です。
やや手抜きですがお許しください(; ・`ω・´)

#include <mex.h>
#include <windows.h>
#include "shmem.h"

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

/* 共有メモリを開く */
__declspec(dllexport) void OpenSharedMemory(void)
{
    gSharedMemoryHandle = OpenFileMapping( FILE_MAP_ALL_ACCESS,
                                           FALSE,
                                           "shmem" );
    
    if( gSharedMemoryHandle == NULL )
    {
        mexErrMsgTxt("not opened map file...");
    }
    
    gMappingObject = (SHARED_MEMORY_DATA*)MapViewOfFile( gSharedMemoryHandle,
                                        FILE_MAP_ALL_ACCESS,
                                        0,
                                        0,
                                        sizeof(SHARED_MEMORY_DATA));
}

/* 共有メモリを閉じる */
__declspec(dllexport) void CloseSharedMemory(void)
{
    if(gMappingObject != NULL)
    {
        UnmapViewOfFile(gMappingObject);
    }
    
    if(gSharedMemoryHandle != NULL)
    {
        CloseHandle(gSharedMemoryHandle);
    }
}

/* 共有メモリへのポインタを取得 */
__declspec(dllexport) SHARED_MEMORY_DATA* GetSharedMemoryObject(void)
{
    return (gMappingObject);
}

/* MEX関数 */
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray*prhs[])
{
    /* 何も行わない */
}
OpenSharedMemory()

共有メモリをオープンする関数です。
共有メモリのオープンにはwindowsAPIのOpenFileMapping()とMapViewOfFile()を使用しています。
MapViewOfFile()で取得した共有メモリへのポインタはグローバル変数に保持します。
※あとは関数の返値で共有メモリがオープンできたかどうかを返そうかと考えていましたが今は手抜きしてます笑

CloseSharedMemory()

共有メモリをクローズする関数です。
グローバル変数に保持している共有メモリのハンドルやポインタを解放します。

GetSharedMemoryObject()

共有メモリへのポインタを取得を取得する関数です。
グローバル変数に保持されている共有メモリへのポインタへのポインタを返します。

動かしてみる

C#側は、「CとC#で共有メモリを使ったプロセス間通信」で作ったプログラムを再利用します。
実行手順は以下のとおりです。

(1) MATLABでCライブラリをビルドしてCライブラリをロードする。

>> mex shmem.c
>> loadlibrary('shmem','shmem.h');

(2) C#側のプログラムを起動させるて共有メモリに書き込みを行う

共有メモリ書き込み
-----------------------
567.987
0
10
-----------------------

(3) MATLABでCライブラリのOpenSharedMemory()を呼び出す。

>> calllib('shmem', 'OpenSharedMemory');

(4) MATLABでCライブラリのGetSharedMemoryObject()を呼び出した後、p.Valueで共有メモリの値を読み込んでみる。
※ここまでくれば、MATLABから共有メモリの読み書きはポインタオブジェクトを使って行うことができます。

>> p = calllib('shmem', 'GetSharedMemoryObject');
>> p.Value

ans = 

  フィールドをもつ struct:

    p1: 567.9870
    p2: 0
    p3: 10

(5) MATLABで共有メモリの値を書き換えてみる。

>> p.Value.p1 = 123.456;

(6) C#で共有メモリの値を読み取ってみる。

共有メモリ読み込み
-----------------------
123.456
0
10
-----------------------

(7) MATLABワークスペースをクリアしてCライブラリを解放する

>> clear p;
>> calllib('shmem', 'CloseSharedMemory');
>> unloadlibrary('shmem');

以下動作イメージです。 f:id:NATSU_NO_OMOIDE:20201229233524g:plain

こんな感じで動いていたら成功です!
MATLAB単体では外部プロセスと共有メモリを使用したプロセス間通信はできませんでしたが Cライブラリでワンクションを挟むことで期待したことが実現できました。
(実行速度とかの問題はさておき。)
なにかの参考になれば幸いです(・`ω´・)b

MATLABからCライブラリを呼び出してみる

前回は、MEX関数を使ってMATLABからCコードを呼び出してみました。

今回は、すこしだけ発展させて、MEX関数をライブラリ化してMATLABから呼び出してみたいと思います。 https://jp.mathworks.com/help/matlab/call-c-library-functions.html
MATLABが呼び出すCライブラリのサンプルは、MATLABをインストールしたときに一緒にインストールされています。
以下のコマンドを使うことでサンプルプログラムがインストールされているパスを調べることができます。 今回はこのサンプルプログラムを参考にしました。

>> fullfile(matlabroot,'extern','examples','shrlib')

簡単なライブラリを作ってみる

以下はライブラリの実装例です。
構造体TEST_DATAのポインタを引数にもらって、そのメンバ変数を加算するadd()と減算するsub()を持っています。
MEX関数を作ったときと同様にmex.hをincludeしてmexFunction()を定義していますが、mexFunction()ではなにも行いません。

#include <mex.h>
#include "test.h"

__declspec(dllexport) double add(TEST_DATA* data)
{
    double ret = 0;
    
    ret = data->p1 + data->p2 + data->p3;
    
    return ret;
}

__declspec(dllexport) double sub(TEST_DATA* data)
{
    double ret = 0;
    
    ret = data->p1 - data->p2 - data->p3;
    
    return ret;
}

/* MEX関数インタフェース */
void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray*prhs[])
{
    /* なにも行わない */
}

こちらはライブラリのヘッダファイルです。
構造体とMATLABから実行したい関数のプロトタイプを定義しておきます。

#ifndef TEST_H
#define TEST_H

typedef struct
{
    double   p1;
    double   p2;
    double   p3;
} TEST_DATA;

__declspec(dllexport) double add(TEST_DATA* data);
__declspec(dllexport) double sub(TEST_DATA* data);

#endif

動かしてみる

実行はMATLABコマンドラインから以下のように実行します。

% Cライブラリをビルド
>> mex test.c

'Microsoft Visual C++ 2019 (C)' でビルドしています。
MEX は正常に完了しました。

% Cライブラリをロード
>> loadlibrary('test', 'test.h');

% 構造体作成
>> st.p1 = 1;
>> st.p2 = 2;
>> st.p3 = 3;

% ポインター オブジェクトを作成
>> p = libpointer('TEST_DATA', st);

% 加算関数実行
>> a = calllib('test', 'add', p);
>> a

a =

     6

% 減算関数実行
>> a = calllib('test', 'sub', p);
>> a

a =

    -4

% ワークスペースの変数をクリア
>> clear

% Cライブラリをアンロード
>> unloadlibrary('test');

前回と同様に、mexを使用してビルドします。
ライブラリ名は同様にファイル名となります。

ビルド後はloadlibraryでライブラリを読み込みます。
このときMATLABから見えるCのインタフェースとしてヘッダファイルを指定します。

Cで定義したライブラリ関数はTEST_DATA型のポインタを引数に持ちます。
したがってMATLABではポインター オブジェクトを作成してやる必要があります。
ポインター オブジェクトはlibpointerで作成することができます。

ライブラリ関数はcalllibで呼び出すことができます。
引数にはライブラリ名、ライブラリ関数、ライブラリ関数が使用する引数を指定します。

ライブラリを使い終えたらunloadlibraryで開放します。
このときポインター オブジェクトを作成してライブラリ関数に引数として渡している、またはライブラリ関数の返値がポインター オブジェクトだった場合、 ワークスペースをクリアしない状態でunloadlibraryを呼ぶと以下のようなエラーが出ます。
したがってclear <変数名>を実行しておきます。

エラー: unloadlibrary
未解決オブジェクトをもつライブラリを解放できません。


以上がざっくりした使い方になります。
もうすこし詳しい使い方を知りたい場合は、サンプルのソースコードと公式ドキュメントを見れば、大体わかると思います!

MATLABからCコードを呼び出してみる

MATLABではコマンドラインからユーザー独自のC/C++プログラムを組み込み関数のように呼び出すことができ、 これらのプログラムはMEX関数と呼ばれているようです。
https://jp.mathworks.com/help/matlab/call-mex-files-1.html

MEX関数はMATLABを高速化させるためによく利用されているようなので使いこなせるようになれば色々と役に立ちそうです。 なので今回はMEX関数というものがどんな感じなのか味見してみようと思います。

やり方は公式サイトの説明を見るのもよいですが以下の方々が書かれた説明のほうがわかりやすかったので参考にさせていただきました。

簡単なMEX関数を作ってみる

以下は、可変引数をもらって、それらの値を合計するMEX関数の例です。
ここではファイル名をtest.cとしてあります。
ファイル名がそのままMEX関数名となります。

#include <mex.h>

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray* prhs[])
{
    int   i = 0;
    double sum = 0;
    
    /* 引数の個数確認 */
    if (nrhs <= 0)
    {
        mexErrMsgTxt("引数がありません。");
    }
    
    /* 返値の個数確認 */
    if (nlhs != 1)
    {
        mexErrMsgTxt("返値がありません。");
    }
    
    /* 引数の個数だけ繰り返し */
    for( i = 0; i < nrhs; i++ )
    {
        /* 引数を加算 */
        sum += (double)mxGetScalar(prhs[i]);
    }
    
    /* 引数を加算して返値にする */
    plhs[0] = mxCreateDoubleScalar(sum);
}

最初におまじないを書いておきます。

#include <mex.h>

続いてMEX関数のインタフェース(MATLABとCの橋渡しをしてくれる関数)としてmexFunctionを定義します。

void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray* prhs[])

mexFunctionの引数は以下のとおりです。

  • 第1引数はMATLABへの返値の個数
  • 第2引数はMATLABへの返値配列
  • 第3引数はMATLABからの引数の個数
  • 第2引数はMATLABからの引数配列

この引数を使ってMATLABでやらせたい処理をCで書いていく感じです。

動かしてみる

MATLABコマンドライン上で以下のような感じでコマンドを実行します。
指定した引数で合計がちゃんと計算されていれば成功です!

>> mex test.c
'Microsoft Visual C++ 2019 (C)' でビルドしています。
MEX は正常に完了しました。

>> a = test(3, 2)

a =

     5

>> a = test(3, 2, 1)

a =

     6

>> a = test(3, 2, 1, 99)

a =

   105

>> 

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

ちょっとCとC#で共有メモリを使ったプロセス間通信が必要になったので調べてみました。
共有メモリを使用したプロセス間通信は昔からあるので、ネットで探せばやり方がたくさん出てきます。
今回は特にわかりやすかった以下のサイトを参考にさせていただき、自分が使いやすいように若干アレンジを加えました。
https://qiita.com/kkent030315/items/38559687812924c279a9

C側ソースコード

まずはC側からです。
対向はC#なのでWindowsで動作する環境を想定しています。
したがってWindowsAPI(windows.h)を使用します。

/************************************************************
 * インクルードファイル
 ***********************************************************/
#include <stdio.h>
#include <windows.h>

/************************************************************
 * 構造体宣言
 ***********************************************************/
typedef struct
{
    double   p1;
    short    p2;
    int      p3;
} SHARED_MEMORY_DATA;

/************************************************************
 * 大域変数宣言
 ***********************************************************/
LPCTSTR gSharedMemoryName   = L"shmem";
HANDLE  gSharedMemoryHandle = NULL;
SHARED_MEMORY_DATA* gMappingObject = NULL;

/************************************************************
 * ファイルマッピングオブジェクトを開く
 ***********************************************************/
int OpenSharedMemoryMappingObiect(void)
{
    int ret = 1;

    /* ファイルマッピングオブジェクトを開く */
    gSharedMemoryHandle = OpenFileMapping( FILE_MAP_ALL_ACCESS, 
        FALSE,
        gSharedMemoryName);

    /* ファイルマッピングオブジェクトが開けなかった場合 */
    if (gSharedMemoryHandle == NULL)
    {
        /* ファイルマッピングオブジェクトを作成 */
        gSharedMemoryHandle = CreateFileMapping(
            INVALID_HANDLE_VALUE,
            NULL,
            PAGE_READWRITE,
            0,
            sizeof(SHARED_MEMORY_DATA),
            gSharedMemoryName);

        if (gSharedMemoryHandle == NULL)
        {
            ret = 0;
        }
    }

    return (ret);
}

/************************************************************
 * メモリマップトオブジェクトを取得
 ***********************************************************/
int GetSharedMemoryMappingObiect()
{
    int ret = 1;

    /* メモリ */
    if (gSharedMemoryHandle != NULL)
    {
        gMappingObject = (SHARED_MEMORY_DATA*)MapViewOfFile(
            gSharedMemoryHandle,
            FILE_MAP_ALL_ACCESS,
            0,
            0,
            sizeof(SHARED_MEMORY_DATA));

        if (gMappingObject == NULL)
        {
            ret = 0;
        }
    }
    else
    {
        ret = 0;
    }

    return (ret);
}

/************************************************************
 * エントリポイント
 ***********************************************************/
int main()
{
    SetConsoleTitle(L"C側のプログラム");

    /* 共有メモリマップトファイルが開けなかった場合 */
    if (OpenSharedMemoryMappingObiect() != 1)
    {
        printf("not opened memory-mapped object.");
    }
    /* メモリマップトオブジェクトを取得できなかった場合 */
    else if (RefSharedMemoryMappingObiect() != 1)
    {
        printf("not referenced memory-mapped object.");
    }
    /* メモリマップトオブジェクトが作成できた場合 */
    else
    {
        /* 共有メモリ読み込み */
        printf("共有メモリ読み込み\r\n");
        printf("-----------------------\r\n");
        printf("p1 : %f\r\n", gMappingObject->p1);
        printf("p2 : %d\r\n", gMappingObject->p2);
        printf("p3 : %d\r\n", gMappingObject->p3);
        printf("-----------------------\r\n");

        getchar();  /* キー入力:C#側待ち合わせ */

        /* 共有メモリ書き込み */
        gMappingObject->p1 = 123.456;
        gMappingObject->p2 = 256;
        gMappingObject->p3 = -100;
        printf("共有メモリ書き込み\r\n");
        printf("-----------------------\r\n");
        printf("p1 : %f\r\n", gMappingObject->p1);
        printf("p2 : %d\r\n", gMappingObject->p2);
        printf("p3 : %d\r\n", gMappingObject->p3);
        printf("-----------------------\r\n");

        getchar();
    }

    /* リソースを解放 */
    if (gMappingObject != NULL)
    {
        UnmapViewOfFile(gMappingObject);
    }

    if (gSharedMemoryHandle != NULL)
    {
        CloseHandle(gSharedMemoryHandle);
    }
}

OpenSharedMemoryMappingObiect()

この関数ではOpenFileMapping()でファイルマッピングオブジェクトをオープンします。
もしオープンできなければCreateFileMapping()でファイルマッピングオブジェクトを作っています。
CreateFileMapping()はページングファイル上のメモリ領域に対するファイルマッピングオブジェクトを作ってくれます。
http://nienie.com/~masapico/api_CreateFileMapping.html

ここではC#側のプログラムがファイルマッピングオブジェクトを作ってくれてればOpenFileMapping()で開けて、 なければCreateFileMapping()で作ってC#側のプログラムで開いてもらうことを想定しています。

RefSharedMemoryMappingObiect()

この関数では開いた(または作成した)ファイルマッピングオブジェクトにアクセスするためにMapViewOfFile()を使っています。
MapViewOfFile()はマップしたメモリ領域へのポインタを返してくれます。
このポインタをSHARED_MEMORY_DATA*にキャストすれば、構造体メンバを読み書きすることで共有メモリにアクセスできるようになります。
http://nienie.com/~masapico/api_MapViewOfFile.html

C#ソースコード

続いてC#側です。

SharedMemoryクラス

共有メモリにアクセスする箇所はSharedMemoryクラスにまとめてみました。
C#(.NET)でもSystem.IO.MemoryMappedFilesというアセンブリを使用することでC側のプログラムと同様にページング領域にファイルをマッピングしたりアクセスすることができます。
System.IO.MemoryMappedFilesは.NET 4.0から使用できるようになりました。
https://codezine.jp/article/detail/4279

using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;

namespace shmem_test
{
    class SharedMemory<T> where T : struct
    {
        private MemoryMappedFile mMemoryMappedFile;
        private MemoryMappedViewAccessor mMemoryMappedViewAccessor;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SharedMemory()
        {
            mMemoryMappedFile = null;
            mMemoryMappedViewAccessor = null;
        }

        /// <summary>
        /// デストラクタ
        /// </summary>
        ~SharedMemory()
        {
            close();
        }

        /// <summary>
        /// 共有メモリオープン
        /// </summary>
        /// <param name="sharedMemoryName"></param>
        /// <returns></returns>
        public bool open(string sharedMemoryName)
        {
            bool result = true;

            try
            {
                try
                {
                    mMemoryMappedFile = 
                        MemoryMappedFile.OpenExisting(sharedMemoryName);
                }
                catch (FileNotFoundException ex)
                {
                    mMemoryMappedFile = 
                        MemoryMappedFile.CreateOrOpen(sharedMemoryName,
                                                      Marshal.SizeOf<T>());
                }

                mMemoryMappedViewAccessor =
                    mMemoryMappedFile.CreateViewAccessor();
            }
            catch
            {
                result = false;
            }

            return (result);
        }

        /// <summary>
        /// 共有メモリクローズ
        /// </summary>
        /// <returns></returns>
        public bool close()
        {
            bool result = true;

            mMemoryMappedViewAccessor?.Dispose();
            mMemoryMappedFile?.Dispose();

            return ( result );
        }

        /// <summary>
        /// 共有メモリ読み込み
        /// </summary>
        /// <param name="structure"></param>
        /// <returns></returns>
        public bool read(ref T structure)
        {
            bool result = true;

            if (mMemoryMappedViewAccessor == null)
            {
                result = false;
            }
            else
            {
                mMemoryMappedViewAccessor.Read(0, out structure);
            }

            return result;
        }

        /// <summary>
        /// 共有メモリ書き込み
        /// </summary>
        /// <param name="structure"></param>
        /// <returns></returns>
        public bool wrtie(T structure)
        {
            bool result = true;

            if (mMemoryMappedViewAccessor == null)
            {
                result = false;
            }
            else
            {
                mMemoryMappedViewAccessor.Write(0, ref structure);
            }

            return result;
        }
    }
}
open()

このメソッドではOpenExisting()を使ってファイルマッピングオブジェクトをオープンします。
もしオープンできなかった場合、C側のプログラムと違いFileNotFoundException例外をスローしてくれます。
なのでFileNotFoundException例外の中でCreateOrOpen()を使ってファイルマッピングオブジェクトを作ります。
ファイルマッピングオブジェクトができたらCreateViewAccessor()でファイルマッピングオブジェクトにアクセスするためのオブジェクトを作っています。

close()

C側のプログラムと同様に不要となったリソースオブジェクトをDispose()で破棄しています。

read()

MemoryMappedViewAccessorを使ってファイルマッピングオブジェクトを読み取ります。
streamなので他のstramクラスと同様な感じで使用できそうです。
ただよくわからなかったのはC側のプログラムと同様に構造体のメンバ変数単位でアクセスする方法がわかりませんでした。。
なにかよい方法があるのかなぁ(・´ω`・;)

write()

ファイルマッピングオブジェクトの書き込みもMemoryMappedViewAccessorを使って行います。
read()同様に書き込みも構造体のメンバ変数単位でアクセスする方法が不明。。。

エントリポイントクラス

こちらはSharedMemoryクラスを使うクラスです。
使い方は以下のとおりです!

using System;

namespace shmem_test
{
    public struct Sh_mem_data_st
    {
        public double p1;
        public short  p2;
        public int    p3;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.Title = "C#側のプログラム";

            SharedMemory<Sh_mem_data_st> shmem
                = new SharedMemory<Sh_mem_data_st>();
            shmem.open("shmem");

            Sh_mem_data_st st = new Sh_mem_data_st();
            st.p1 = 567.987;
            st.p2 = 0;
            st.p3 = 10;
            shmem.wrtie(st);
            Console.WriteLine("共有メモリ書き込み");
            Console.WriteLine("-----------------------");
            Console.WriteLine(st.p1);
            Console.WriteLine(st.p2);
            Console.WriteLine(st.p3);
            Console.WriteLine("-----------------------\r\n");

            Console.ReadKey();  // キー入力:C側待ち合わせ

            Sh_mem_data_st result = new Sh_mem_data_st();
            shmem.read(ref result);
            Console.WriteLine("共有メモリ読み込み");
            Console.WriteLine("-----------------------");
            Console.WriteLine(result.p1);
            Console.WriteLine(result.p2);
            Console.WriteLine(result.p3);
            Console.WriteLine("-----------------------");

            Console.ReadKey();
        }
    }
}

実行結果

参考にしたサイトさんのやり方をまねてコンソールタイトルにそれぞれどちら側のプログラムか表示するようにしてみました。
今回のプログラムの実行手順としては以下のようになります。

  1. C#側プログラムを起動(先に共有メモリに書き込むので)
  2. C側プログラムを起動(共有メモリの値を読み込み)
  3. C側プログラムでキー入力(共有メモリに値を書き込み)
  4. C#側プログラムでキー入力(共有メモリの値を読み込み)

結果は以下のとおり。 f:id:NATSU_NO_OMOIDE:20201229230647g:plain

小さな開発用マシンを作ってみる

DeskMiniを購入してみた

最近、開発専用のマシンが欲しくなりました(´・ω・`)
なにかちょっと試したいとき、巨大なデスクトップを起動させるほどでもないけどRaspberry Piみたいなシングルボードだとパワー不足で心もとない。
そんなとき、小さくて気軽に使える開発専用のマシンが欲しいなぁと思っていました。

そこで見つけたのが「DeskMini」です(・`ω´・)b
DeskMiniはASRock製のベアボーンPCキットです。
簡単に小型PCが自作できるということで以前から気になっていました!
今回はDeskMiniを使って開発専用のマシンを作ってみたいと思います(^ω^)/
ちなみにDeskMiniは300シリーズ(AMD)と310シリーズ(Intel)があります。
私はもっぱらIntel派なのでDeskMini310を購入しました!!

ほかに購入したもの

まずはCPUから。
DeskMini310はソケットがLGA1151です。
コストを最も抑えるにはCeleronですが、ぶっちゃけ安いラップトップパソコンと性能差がさほど変わらなくなるので今回は不採用としました。
せめてCore iシリーズがよいですがi5やi7はやや予算が。。。
そこでちょっとランクを落としてCore i3-8100にしました。
Core i3-8100でも3.6GHzで4コア4スレッドあり、お値段も14,000円前後とコスパはなかなか良い気がします。
なおDeskMiniに付属しているCPUクーラはうるさいらしいのでリテールクーラ付きのボックスを購入することにしました。
ちなみにDeskMiniにはグラフィックス機能はなくグラボも取り付けにくいため、出力はIntel HD graphicsに頼らざるを得ません。
なので画面出力をしたい方はCPU名の末尾にFが付いているモデルは避けたほうがよいと思います。
https://pssection9.com/archives/intel-cpu-alphabet-suffix-meaning.html


お次はメモリです。
DeskMiniにはDDR4-2666のSO-DIMMが2レーンあります。
今回はコストを抑えるために8GBのものを1枚だけ購入しました。
当面、開発用途なら8GBあれば十分ですし、不足したら拡張すればいいかなと( ^ω^;)

最後はSSDです。
DeskMiniにはM.2が1スロットあります。
SATAの2.5インチHDDが大量に余っていたので再利用しようかとおもいましたが せっかくなのでM.2 SSDを初購入してみました。

必要なものはこのくらいです。
安いところを探し回って大体50,000円くらいで揃えました!

開封の儀

購入してからわずか1日弱で届きました笑 f:id:NATSU_NO_OMOIDE:20201222235150j:plain

さっそくDeskMiniを開封
本体にACアダプタ、セットアップガイド等が入っています。 f:id:NATSU_NO_OMOIDE:20201222235145j:plain

袋から出してみました。
筐体はほぼほぼメッシュです。 f:id:NATSU_NO_OMOIDE:20201222235139j:plain

裏面はこんな感じです。 f:id:NATSU_NO_OMOIDE:20201222235132j:plain

ACアダプタは事前にネットで調べていたとおり大きい。。。
筐体ほどではないですが結構な存在感です(^ω^;) f:id:NATSU_NO_OMOIDE:20201222235011j:plain

組み立ててみる

裏面のネジを外してMini-STXボードを引っ張り出します。
ほぼほぼCPUソケットとメモリスロットだけあるマザボですね f:id:NATSU_NO_OMOIDE:20201222235126j:plain

CPUを取り付けます
取り付け方もATXボードと変わらず。 f:id:NATSU_NO_OMOIDE:20201222235057j:plain

リテールクーラを取り付けます。
ボードが小さいせいでクーラがとても大きく感じます笑 f:id:NATSU_NO_OMOIDE:20201222235052j:plain

メモリとM.2 SSDを取り付けます。
M.2 SSDはビスで固定するようです。 f:id:NATSU_NO_OMOIDE:20201222235029j:plain

あとは筐体にボードを格納します。
御覧の通り片手で持てるコンパクトさ。
組み立て時間もわずか15分程度でした!! f:id:NATSU_NO_OMOIDE:20201222235022j:plain

Ubuntuを入れてみる

今回はOSを買わなかったのでUbuntuを入れてみます。
インストールはUSBから行いました。
インストールUSBの作り方は以下が参考になります。
https://diagnose-fix.com/topic2-003/

インストールUSBができたらPCに刺して電源ONします。 f:id:NATSU_NO_OMOIDE:20201222235209j:plain

すぐにインストール画面が表示されました! f:id:NATSU_NO_OMOIDE:20201222235005j:plain

画面に従っていけばすぐにインストールできます^^ f:id:NATSU_NO_OMOIDE:20201222235157j:plain


・・・とこんな感じでした。
ACアダプタがやや大きいですがそれでもサイズはそんなに気にならず。
むしろ程よい大きさで可愛らしいPCができて嬉しいです笑
リテールクーラを使用していますが音もそんなに気になりません。
いまはSSHでつないで開発目的で使っていますがなかなかサクサク動いてくれているので性能的にも大満足です!!
興味がある方はぜひ作ってみてはいかがでしょうか?

RPiでAMP環境を構築してみる

今回は、Crosstool-NGで作ったRPiのベアメタル開発環境を使用して、Raspberry Pi(以降、RPi)でAMPシステムを作ってみたいと思います(・`ω´・)b

AMPシステムとは

AMPとはAsymmetrical Multi Processingの略で、マルチコアプロセッサにおいてそれぞれの演算コアで別々のプログラムを動作させることを言います。
最近のRPi(RPi2、RPi3、RPi4のModel B、およびB+など)には、ARM Cortex-AシリーズというMPUが搭載されています。
さらに、これらのMPUクアッドコアになっているので、これらの演算コアを使用して別々のプログラムを動かしてみたいと思います。
https://ja.wikipedia.org/wiki/Raspberry_Pi


これが今回構築するAMPシステムのイメージです。 f:id:NATSU_NO_OMOIDE:20201220184754p:plain
今回は、4つの演算コアの内、3コアを使用してRaspberry Pi OSを動作させ、その上でユーザプログラムを動作させます。
※ちなみに複数の演算コアを使用して同じプログラム動作させる方式はSMP(Symmetric Multi Processing)と呼ばれているようです。
残りの1コアを使用してベアメタルアプリケーションを実行します。
さらにRaspberry Pi OSで動作しているユーザプログラムと別コアで動作しているベアメタルアプリケーション間で共有メモリを使用してIPC(Inter Process Communication)させることを目指します。
ちなみに今回はRPi3を使って、32bit環境を対象にシステム構築したいと思います。

なおAMPとSMPの説明は以下がとても分かりやすかったです。 https://news.mynavi.jp/article/20071205-mp/3

Raspberry Pi OSをインストールしてみる

まずは普通にRaspberry Pi OSをインストールします。インストール自体は以下を参考にしてやりました。
最近はより簡単にインストールできるRaspberry Pi Imagerというツールが公式から登場しました。
とても簡単にインストールすることができますヽ(*´∀`)ノ
※そのほか、SSHを使用できるようにしたり、vimをインストールしたりしましたがここでは説明を割愛します。
https://www.indoorcorgielec.com/resources/raspberry-pi/raspberry-pi-osのインストール

準備ができたらRPiにRaspberry Pi OSをインストールしたSDカード、HDMIケーブル、LANケーブル、USBキーボードを接続してUSBケーブルをPCにつないで電源ONします。
f:id:NATSU_NO_OMOIDE:20201220191824p:plain

電源が入ると起動画面が表示されます。
このときベリーの絵が4つ表示されました。
これはRaspberry Pi OSが演算コアを4つ使用して動作していることを示しているようです。(・・・と、どこかに書いてあったような気がします。)
f:id:NATSU_NO_OMOIDE:20201220191819p:plain

一応、以下のコマンドを使用してメモリ使用量とCPUコア使用数を確認しておきます。

$ free
             total       used         free    shared buffer/cache    available
Mem:        948304      50288       819920      6308        78096       840372
Swap:       102396          0       102396
$ cat /proc/cpuinfo
processor       : 0
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 1
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 2
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 3
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

これによるとメモリを1GBフル活用(RPi3の実装RAMは1GB)しており、CPUも4コア使用していることがわかります。
これでRaspberry Pi OSのインストールは完了です(・`ω´・)b

Raspberry Pi OSが使用するメモリとCPUコア数を制限してみる

さて、Raspberry Pi OSにリソースを制限させて動作させるためには、起動時に使用するメモリの量や範囲、演算コアの個数を伝えてやる必要があります。
そのために今回はu-Bootを使用することにしました。
U-Bootは、多種のプラットフォームに対応したブートローダです。当然RPiもサポートされています。
https://ja.wikipedia.org/wiki/Das_U-Boot
https://elinux.org/RPi_U-Boot

u-bootをビルドする

まずはu-bootをビルドします。
コンパイラCrosstool-NGで作ったRPiのベアメタル開発環境を使います。
ソースはgitから落とします。本家のgit://git.denx.de/u-boot.gitもありますが、ダウンロードが遅いです(・´ω`・)

$ git clone git://github.com/swarren/u-boot.git
$ make clean
$ make distclean
$ make rpi_3_32b_defconfig
$ make all


これでRPi3のu-boot(boot.bin)ができます。
なおmakeするとき、使用したいRPiのモデルを指定します。
指定できるdefconfigには以下のものがあります。

  • rpi_defconfig : 初代Rpi用
  • rpi_2_defconfig : RPi2用
  • rpi_3_32b_defconfig : 32bit RPi3用
  • rpi_3_defconfig : 64bit RPi3用
  • rpi_4_32b_defconfig : 32bit RPi4用
  • rpi_4_defconfig : 64bit RPi4用

bootスクリプトを書く

続いてu-bootが使用するbootスクリプトを書きます。
このu-bootはこのスクリプトを使ってRaspberry Pi OSをbootします。

スクリプトは以下のような感じになります。

mmc dev 0
fatload mmc 0:1 ${kernel_addr_r} kernel7.img
fatload mmc 0:1 0x19000000 bcm2710-rpi-3-b.dtb
setenv bootargs earlyprintk console=tty0 console=ttyAMA0 root=/dev/mmcblk0p2 rootfstype=ext4 rootwait noinitrd mem=512M maxcpus=3
bootz ${kernel_addr_r} - 0x19000000

2行目でロードする.imgファイル(Raspberry Pi OSカーネル)を指定しています。
.imgファイルはRaspberry Pi OSをインストールしたときSDカードの/bootに作られます。
カーネルには以下の種類があります。今回は32bit RPi3用のkernel7.imgを指定します。

  • kernel8.img :64bit RPi3、RPi4用
  • kernel7l.img :32bit RPi4用(LPAE付)
  • kernel7.img :32bit RPi3、RPi4用(LPAEなし)
  • kernel.img :上記以外のRPi

3行目はデバイスツリーを配置するアドレスを指定しています。
バイスツリーもRaspberry Pi OSをインストールしたときSDカードに作られるものを流用します。
今回は1GBあるメモリの内、lowerの512MBをRaspberry Pi OS用、upperの512MBをベアメタルアプリケーション用にしたいと考えています。
そこでデバイスツリーの配置アドレスは、あえて0x19000000(400MB付近)にしました。

4行目では、Raspberry Pi OSの起動オプションを指定しています。
mem=512M maxcpus=3を指定することで、メモリサイズを512MB、使用するCPUのコア数を3個にしてRaspberry Pi OSを起動できるようにはずです。

5行目では、bootzコマンドでカーネルとデバイスツリーを配置させます。
${kernel_addr_r}はたしか0x80000からだったと思います。
したがってここから4行目で指定したmem(=512MB)がカーネルが使用できるメモリ範囲になると思います。
(めんどくさいのでデバイスツリーの開始アドレスを指定していますが(;^_^A

上記スクリプトが書けたらboot.txtとして保存し、以下のコマンドを使用してboot.scrを作成します。

mkimage -A arm -O linux -T script -C none -n boot.script -d boot.txt boot.scr


最後に作成したboot.binとboot.scrをSDカードの/boot直下にコピーします。
そして/boot/config.txtに以下の行を追加して準備完了です。

kernel=u-boot.bin


再びRaspberry Pi OSを起動してみる

これでSDカードをRPiに入れて電源ONすると同じようにRaspberry Pi OSが立ち上がるはずです。
ただし起動プロセスとしてはu-bootが先に起動してRaspberry Pi OSをキックするような流れになっていると思います。
(本当にu-bootが起動しているかはUARTを有効にするなどして確かめるとよいかと思います。)

起動後は再びSSHで接続し、メモリとCPUコア使用数を確認しておきます。

$ free
             total       used         free    shared buffer/cache    available
Mem:        506152      40940       397744      3500        67468       411052
Swap:       102396          0       102396
$ cat /proc/cpuinfo
processor       : 0
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 1
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

processor       : 2
model name      : ARMv7 Processor rev 4 (v71)
BogoMIPS        : 38.40
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xd03
CPU   revision  : 4

これによるとメモリ使用量が512MB(起動時のメッセージを見ると0x80000から配置されていると思います)で 使用CPUコア数が3個になっていることがわかります。
これでRaspberry Pi OSの準備は完了です。

ベアメタルアプリケーションを作成する

ここまででRaspberry Pi OSが3コアで動作するようになったので、残りの1コアでベアメタルアプリケーションを動作させるようにしていきます。
が、まずはベアメタルアプリケーションがちゃんと動くか確認します。

Lチカコードを作成する

コンパイラは同じくCrosstool-NGで作ったRPiのベアメタル開発環境を使います。
ベアメタルアプリケーションの動作確認は単純なLEDチカチカプログラムを使って行います。 以下、ソースコードです。

#define GPFSEL1 0x3F200004 /* GPIO のピン設定をするためのレジスタ */
#define GPSET0  0x3F20001C /* GPIO を HIGH にするためのレジスタ */
#define GPCLR0  0x3F200028 /* GPIO を LOW  にするためのレジスタ */

typedef unsigned char bool;

#define TRUE  1
#define FALSE 0

#define WAIT_COUNT 3000000

/*
 * @fn wait_count だけビジーウェイトする
 */
void busy_wait(int wait_count);

/*
 * @fn Lチカする
 */
int main(void)
{
    // GPIO 出力に設定。
    *(volatile unsigned int*)GPFSEL1 = (1 << (18));

    // セットして待つ、クリアして待つ、を繰り返す。
    while (1) 

        *(volatile unsigned int*)GPSET0 = (1 << 16);
        busy_wait(WAIT_COUNT);
        *(volatile unsigned int*)GPCLR0 = (1 << 16);
        busy_wait(WAIT_COUNT);
    }

    return 0;
}

/*
 * @fn delay関数
 */
void busy_wait(int wait_count)
{
    volatile unsigned int i;

    for (i = 0; i < wait_count; i++);
}


このプログラムではGPIO16(pin36)を制御してH/Lを繰り返します。
したがってpin36に抵抗を挟んでLEDを接続しておきます。
f:id:NATSU_NO_OMOIDE:20201220221858p:plain

スタートアップコードを作成する

続いてスタートアップコードをアセンブラで書きます。
スタックポインタは、ARM Coreの実行開始アドレスが0x0008 0000で、spはそこより若いアドレスの領域を指定します。

mov sp, #0x80000
bl  main

ビルドして動かしてみる

コードができたら以下のコマンドでビルドします。
今回はリンカスクリプトを作成しないのでldの-Tオプションを使用してtextアドレスを決めまています。

$ aarch64-rpi3-elf-as -o start.o start.S
$ aarch64-rpi3-elf-gcc -c -o main.o main.c
$ aarch64-rpi3-elf-ld -Ttext 0x80000 -o metal.elf start.o main.o
$ aarch64-rpi3-elf-objcopy -O binary metal.elf metal.img


ビルドが成功したらmetal.imgをSDカードの/bootにコピーし、config.txtのkernelパラメータを以下のように変更します。

kernel=metal.img


あとは再びSDカードをRPiに入れて電源をONします。
LEDがチカチカすれば成功です。
f:id:NATSU_NO_OMOIDE:20201220221902p:plain

ベアメタルアプリケーションをAMPで動かす

ここまでで、SMP動作するRaspberry Pi OSとベアメタルアプリケーションができました。 次はベアメタルアプリケーションをAMPで動作させるようにしていきます。

ベアメタル アプリケーションを改造する

まずはベアメタルアプリケーションを改造していきます。
start.Sを以下のようにしてスタックポインタの初期位置を変更します。
1GBあるメモリの内、upper512MBをベアメタルアプリケーションの領域としたので適当に0x30000000(768MB付近)としました。

mov sp, #0x30000000
bl  main

変更できたらビルドします。
前回同様、リンカスクリプトは作っていないので-Ttextを使用してupper512MBの先頭アドレス0x20000000にプログラムを配置するようにしています。

$ aarch64-rpi3-elf-as -o start.o start.S
$ aarch64-rpi3-elf-gcc -c -o main.o main.c
$ aarch64-rpi3-elf-ld -Ttext 0x20000000 -o metal.elf start.o main.o
$ aarch64-rpi3-elf-objcopy -O binary metal.elf metal.img

metal.imgができたらSDカードの/bootにコピーしておきます。
なおconfig.txtのkernelパラメータはu-bootが起動するようにしておきます。
これでベアメタルアプリーケーションの準備完了です。

ベアメタルアプリケーションをメモリに展開するプログラムを作成する

最初はRaspberry Pi OSからベアメタルアプリケーションをメモリに展開するプログラムを作成します。
このプログラムは0x20000000番地にベアメタルアプリケーションの実行ファイルを展開します。
ただ0x20000000番地はRaspberry Pi OSの管理対象外です。
なのでmmapを使用してファイルを展開するようにしています。

#include <stdio.h>    
#include <stdlib.h>  
#include <fcntl.h>   
#include <sys/mman.h>    
    
int main (int argc, char * argv [])  
{   
    int fd_mem;
    void *load_address;
    unsigned long fileLen;
    FILE *file;
    printf ("Opening %s\n",argv[1]);
    file=fopen(argv[1],"rb");
    
    fseek(file, 0, SEEK_END);
    fileLen=ftell(file);
    fseek(file, 0, SEEK_SET);
    printf ("File lenght %d\n",fileLen);
    
    printf ("Opening Mem %x\n",0x20000000);
    fd_mem = open("/dev/mem", O_RDWR);
    load_address = mmap(NULL, fileLen,PROT_READ|PROT_WRITE, MAP_SHARED, fd_mem, 0x20000000);
    
    fread(load_address, fileLen, 1, file);
    fclose(file);
}   

ソースが書けたらビルドして実行してみます。 以下のようになれば成功です。

$ gcc loadmetal.c -o loadmetal
$ sudo ./loadmetal metal.img
Opening metal.img
File lenght 160
Opening Mem 20000000

特定のメモリアドレスを読み書きするプログラムを作成する

お次は特定のメモリアドレスを読み書きするためにdevmemを用意します。
ソースコードは以下を流用しました。
https://bootlin.com/pub/mirror/devmem2.c
以下のコマンドでソースをダウンロードしてビルドします。

$ wegt https://bootlin.com/pub/mirror/devmem2.c
$ gcc devmem2.c -o devmem
$ sudo ./loadmetal metal.img

Raspberry Pi OSからベアメタルアプリケーションをキックしてみる

ここまでできたら、ようやくRaspberry Pi OSからベアメタルアプリケーションをキックさせます。
Raspberry Pi OSからCore 3をkickするためにメールボックスを使用します。
Core3_MBOX3_SETレジスタ= 0x400000BCに0x20000000を書き込むと、 Core 3はその位置にロードした実行可能ファイルにジャンプするようです。
メールボックスの仕様は以下が参考になりました。
https://qiita.com/toshinaga/items/e65004cc207a46d17b7b

devmemを使用して0x400000BCに0x20000000を書き込みます。
これでRaspberry Pi OSからベアメタルアプリケーションがキックされ、ベアメタルアプリケーションがGPIOを制御し始めます。
以下のコマンドを実行した直後にLEDがチカチカすれば成功です!

$ sudo ./devmem 0x400000bc w 0x20000000
/dev/mem opened.
Memory mapped at address 0x76f080000.
Value at address 0x400000BC (0x76f080bc): 0xACA0000
Written 0x20000000; readback 0xACA0000

Raspberry Pi OSとベアメタルアプリケーションで共有メモリを試す

最後に共有メモリを使用して、Raspberry Pi OSとベアメタルアプリケーション間のIPCをテストしてみます。

再びベアメタル アプリケーションを改造する

まずはベアメタルアプリケーションを改造します。

#define GPFSEL1 0x3F200004 /* GPIO のピン設定をするためのレジスタ */        
#define GPSET0  0x3F20001C /* GPIO を HIGH にするためのレジスタ */     
#define GPCLR0  0x3F200028 /* GPIO を LOW  にするためのレジスタ */     
        
typedef unsigned char bool;     
        
#define TRUE  1       
#define FALSE 0       
        
#define WAIT_COUNT 3000000        
        
volatile char delay = 0x54;      
volatile char count = 0x00;      
        
/*     
 * @fn wait_count だけビジーウェイト
 */     
void busy_wait(int wait_count);       
        
/*     
 * @fn Lチカする       
 */     
int main(void)        
{       
        
    // GPIO 出力に設定。     
    *(volatile unsigned int*)GPFSEL1 = (1 << (18));

    // セットして待つ、クリアして待つ、を繰り返す。
    while (1) {      
        *(volatile unsigned int*)GPSET0 = (1 << 16);
        busy_wait(WAIT_COUNT);      
        *(volatile unsigned int*)GPCLR0 = (1 << 16);
        busy_wait(WAIT_COUNT);      
            
            if( count == 0xFF )
            {
                count = 0x00;
            }
            else
            {
                count++;
            }
    }       
        
    return 0;        
}       
        
/*     
 * @fn delay関数     
 */     
void busy_wait(int wait_count)
{       
    volatile unsigned int i;
        
    for (i = 0; i < delay * 10000; i++);
}

このプログラムでは、delayとcountという変数を追加しました。
delay変数の値でLチカの速度が変化し、Lチカされる度にcount変数が加算されていきます。
ビルドが成功してバイナリができたら、以下のコマンドを実行して変数シンボルのアドレスを確認しておきます。

$nm metal.elf
20000000 t $a
20000008 t $a
2000009c t $a
20010119 b $d
20000088 t $d
20000114 t $d
20000118 t $d
2001011c B __bss_end__
20010119 B __bss_start
20010119 B __bss_start__
20010118 D __data_start
2001011c B __end__
2001011c B _bss_end__
20010119 D _edata__
2001011c B _end
00080000 N _stack
2000009c T busy_wait
20010119 B count
20010118 D delay
20000008 T main

ここではdelay変数は0x20010118に配置されました。

共有メモリで通信できるか試してみる

バイナリができたらこれまでと同様にSDカードにコピーし、loadmetalを使用してメモリに展開しておきます。
そしてdevmemでdelay変数のアドレスの値を読み込んでみます。
プログラムで定義した値(delay = 0x54)になっていれば成功です。

$ sudo ./devmem 0x20010118 b
/dev/mem opened.
Memory mapped at address 0x76fb1000
Value at address 0x20010118 (0x76fb1118): 0x54

続いてdelay変数の値をdevmemを使用して書き換えてみます。

sudo ./devmem 0x20010118 b 0x30
sudo ./devmem 0x20010118 b 0x20
sudo ./devmem 0x20010118 b 0x10

0x30→0x20→0x10に下げていくと高速点滅し、 逆に0x10→0x20→0x30に上げていくと低速点滅すれば成功です。
これでRaspberry Pi OSからメモリを介してベアメタルアプリケーションが制御できたことがわかります。


まだ共有メモリのキャッシュやコヒーレントなどの問題が残っているかもですが、とりあえず思っていたAMPシステムが構築できました。
かなり長くなりましたが、ここまで読んでくださりありがとうございますm(_ _)m
なにかの参考になれば幸いです。