先日、MATALB Expo2020にて下記のLTをしました。
SimulinkとMATLABをつなぐ魔法の言葉
スライド内容にある通り、実態としては「SimilinkがMATLABを服従させる」方法の紹介となります。このためLTの運営から「Simulink過激派」と呼称される顛末となりました。いえ~い
MATLABユーザーにとって、何かを実装しようと思った時にSimulinkでやるか、MATLABでやるかは大きな選択肢となります。どちらを選択するかは個人の判断と思いますが、制御屋である私個人としては断然Simulinkを選択したくなります。
ところでMATLAB・Simulinkの大きな特徴として、ハードウェアとの連携が得意なことが挙げられます。特に下記でも紹介している通り、BLEを使ったMATLABとハードウェアの連携は大変ハードルが低く、micro:bitやM5Stackと言ったBLE機能を有したハードであればすぐに繋げられます。
MATLABとmicro:bitをBLEで繋げて遊ぶ
上記の連携機能ですが、現状MATLABにしか準備されていません。Simulink過激派の私としては、当然ながら「Simulinkでもなんとかならんのか?」というお気持ちになります。micro:bitやM5Stackにはボタンが付いていますので、このボタンを押したらSimulinkの画面上で何かが起きて欲しくなりますよね? だって、なったら素敵じゃないですか。
ボタンを押したら何かが起きる、というのをプログラミング的に言うと「イベント」になります。そんなわけで本記事ではタイトルにある通り、その実現方法について解説するものとします。完成イメージとしては下記のものとなります。
「M5StackとMATLABをBLEで接続、割り込み関数内でSimulinkAPI叩いてトリガ生成してStateFlow動かす」という、恐らく世界中で私しかやらんだろうMATLAB芸をしている。もはや集大成感ある。 pic.twitter.com/By2tfn824n
— モータ制御マン (@motorcontrolman) September 12, 2020
以降ではまず実現に向けた基本的な考え方を述べ、その次に具体的な方法についてStep by Stepで示すものとします。
#0. 実現に向けた基本的な考え方
正直なところ、MATLABおよびSimulinkでよく遊んでいる人であればそんなに難しいことはありません。「イベント発生はMATLAB側で検出し、Simulink APIを使うことでそれをSimulinkに受け渡す」ただそれだけになります。図で表すと下記。
コールバック関数は何かしらのイベントに反応して実行される関数のことで、詳細については MATLAB でゲームを作る ~1. コールバック編~ を参照下さい。外部ハードからSimulinkにイベント発生させる上では、その外部ハードに関わるコールバック関数をMATLABにて設定できることが求められます。
BLEはその一例であるため、冒頭で挙げたmicro:bitやM5Stackでのイベント発生が可能となっています。
#1. メイン関数にてコールバック関数を設定する
上記の図でも表現した通り、コールバック関数の設定自体はメインの関数内にて行う必要があります。これまた詳細は MATLAB でゲームを作る ~1. コールバック編~ に記述があるため省略しますが、例えばキー入力に対するコールバック関数の設定は下記のようにします。
fig.KeyPressFcn = @my_callback; % コールバックとして my_callback 関数を設定
上記にてfig
はコールバック関数を設定する親オブジェクトのハンドルとなります。KeyPressFcn
はキー入力時のコールバック関数プロパティであり、そこに関数ハンドル@my_callback
を割り当てることでコールバック関数の設定を行っています。
コールバック関数プロパティは親オブジェクトによって異なるので注意が必要ですが、分かっていればそこまで難しくはありません。
参考までにBLEの場合は下記となります。コールバック関数の設定は一番下のみで、他の行は周辺設定となる点に注意。
b = ble("m5-stack"); % BLEにてハードウェアに接続
ch = characteristic(b, "413C0CE6-45B8-11EA-B77F-2E728CE88125", "4CEE54E0-45B8-11EA-B77F-2E728CE88125");
% アクセス対象となる特性を設定したハンドルの作成
subscribe(ch); % 上記特性によるイベント発生の有効化
ch.DataAvailableFcn = @my_callback; % コールバックとして my_callback 関数を設定;
上記のようにすることで、イベント発生によって関数my_callback
が実行されるようになります。
#2. コールバック関数内でSimulink APIを呼び出し、イベント発生させる
Simulink APIの詳細については、Simulink API で MATLAB から Simulink を操る を参照頂ければと思います。コールバック関数内で呼ぶうえでの注意点などは特になく、普通に書けばよいだけです。
肝心のイベント発生ですが、普通に考えると「Simulink内にイベント発生させるってどうやるんだ?」と悩む人が大半と思います。実際私もこれで結構悩みました。
もしかしたらイベント自体を発生させる方法もあるかも知れませんが、今回はもっと簡単な方法を紹介します。それは、Simulink内の任意のブロックの設定値を変化させることで、「値の変化」というイベントを発生させる方法です。使用するSimulink API関数としてはset_param
だけで十分という、非常に簡単な方法となります。
一例としてsimulinkモデルmodel.slx
内のConstantブロックの値を1加算するコールバック関数の内容は下記となります。
function my_callback
global num
num = num + 1;
set_param('model/Constant','Value', num2str(num));
end
コールバック関数からMATLABのワークスペースに直接アクセスすることは出来ないので、num
をグローバル変数として定義しておいて加算しています。上記はConstantブロックを使った場合ですが、Gainブロックでも同様の事が実現可能です。もっとも、Constantブロックを使った場合が最もモデルをシンプルにできますが。
なお別のSimulink API関数であるget_param
を使うことで「Simulinkブロックの現在の設定値を取得する」ことができるためグローバル変数の定義が不要となります。こちらのほうがスマートと言えそうですね。
function my_callback
target = 'model/Constant';
prevVal = str2double(get_param(target, 'Value'));
set_param(target,'Value', num2str(prevVal + 1));
end
ちなみにですが、上記の処理はMathworks公式の配信するMATLAB芸であるsf_tetris2 を参考にしています。詳細な解説は 【MATLAB】Stateflow テトリスのモデルを頑張って解読する を参照ですが、この本MATLAB芸でも本記事同様、MATLABでのイベント発生をSimulink APIを使うことでSimulinkに受け渡しています。
ちなみに上記のテトリスですが、数年前からあったモデル例となります。このためSimulink APIを使ったイベントの受け渡しはMathworksとしても推奨する方法であることが言えるんじゃなかろうかと。
さらに深く考察すると、私のように「制御のためのソフトウェアをどうしてもSimulink(もしくはStateflow)で作りたい!」という過激な思想を持った人が世の中に一定数いるということになります。いいぞもっとやれ。
#3. Simulink内にて「値の変化」を検出する
Simulinkユーザーであれば下記のブロック図で伝わると思います、えいやっと。
これにて外部ハードにてイベント発生した際にTriggred Subsystemが呼び出され、Simulink内にて何らかのアクションを起こすことができます。例としてはDashboadでランプを転倒させたり、他のハードウェアに接続したモータを動かしたり。ユーザーの発想次第でなんでもできますね。
4. まとめ
外部ハードのボタンを押したらSimulinkに何かが起きる。ただそれだけの事ではありますが、実際にやってみると結構素敵なことになります。皆様是非試してみて下さい、そしてSimulink過激派への道を共に歩もう。
おまけ① 一般的なプログラミングで「値が変化したら、関数を呼び出す」の処理をどう書くか
上記モデルを書いていて「やっぱSimulinkってすげーや。やりたいことがすぐ出来る」と思ったので参考までに。
だいたいこんな感じで書くと思うのですよ。
if(Input != prevInput)
{
function();
}
prevInput = Input;
上記のような処理をSimulinkだとUnit DelayブロックやTriggred Subsystemを使うことでササっと書ける点がやっぱりいいなと。
(ただし何でもかんでもSimulinkにやらせようとした結果、いろいろな手間が発生することに関しては目をつぶるものとする)
おまけ① Stateflowだと更に簡単な実装が可能
これまたsf_tetris2 での実装例となりますが、Stateflow内にてhasChanged関数を使うことで前回値との差分を取る必要すら無くなります。
sf_tetris2では、Stateflowの入力として各キー入力によって変化するConstantブロックが繋がっています。
本記事の3. 同様、Unit Delayを使用し前回値のとの差分を取ることで「値の変化」を検出してもよいのですが、hasChanged関数を使うことでこの処理すら不要となります。
恥ずかしながら、Stateflowを何年か趣味で使っておきながらこんな関数があることを知りませんでした。今後使い倒していきたいですね。