はじめに...
第5回は、カスタムブロックの作成とSTF_make_rtw_hook.m ( 第1回 参照) について記した後、構築した環境でコード生成を行います。
対象ハードウェア向けカスタム ブロック
対象ハードウェア向けカスタム ブロック (以降、デバイスブロックと記します) は、MEX用Cソースファイルと tlc ファイルで異なる動作を記述した S-Function ブロックです。
デバイスブロックは通常、シミュレーション中に何の動作もしません。これは Simulink Support Package for Arduino Hardware で提供されるブロックをモデルへ配置して、シミュレーションを行っている状況を思い浮かべれば合点がいくと思います。そして、シミュレーション中のブロックの振る舞いは MEX 用 C ソースファイル内にある mdlOutputs() や mdlUpdate() で決まります。ですから、これらを「何もしない」ように編集します。次は「何もしない」mdlOutputs() の例です。
static void mdlOutputs(SimStruct *S, int_T tid)
{
/* 何もしない */
}
他方、デバイスブロックを配置したモデルからコードを生成する場合は、デバイスブロックに対応する C 関数を生成コード内で呼び出さなくてはなりません。例えば、モデル内にDigital Output ブロックが在れば、model_step() 内で digitalWrite() を呼ぶ、というようにです。モデルからコードを生成する際の、ブロックに関するコード展開を担うのが 「ブロックターゲットファイル」と呼ばれる tlc ファイルです。
デバイスブロックを model_step() 内にコード展開する記述は tlc ファイルの Outputs() にあり、次はその例です。
%function Outputs (block, system) Output
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
%else
%assign p1_val = LibBlockParameter(p1, "", "", 0)
%assign u1_val = LibBlockInputSignal(0, "", "", 0)
digitalWrite(%<p1_val>, %<u1_val>);
%endif
%endfunction
デバイスブロックの作成
それではデバイスブロックを作ります。作成するブロックは、連載最後で L チカを行うので digitalWrite() 用とします。
C MEX S-Function ブロックの作成は、ご存知のように次の何れかで行います。
1)Legacy Code Tool
2)S-Function Builder
3)MEX 用 C ファイルと tlc ファイルを手書きする
1→2→3の順に難易度と自由度が上がります。おのおのの詳細は、以下のMATLAB ドキュメンテーション、ならびにそのリンクをご覧ください。
-
Legacy Code Tool
https://jp.mathworks.com/help/simulink/sfg/integrating-existing-c-functions-into-simulink-models-with-the-legacy-code-tool.html -
S-Function Builder
https://jp.mathworks.com/help/simulink/sfg/s-function-builder-dialog-box.html -
MEX 用 C ファイルと tlc ファイルを手書きする
https://jp.mathworks.com/help/simulink/sfg/example-of-a-basic-c-mex-s-function.html
1) MEX 用 C ファイルと tlc ファイルの生成
ここで作るデバイスブロックは、Arduino ライブラリ関数の pinMode() と digitalWrite() を呼び出すだけの簡単なものですから、Legacy Code Tool を用います。
lct = legacy_code('initialize');
lct.SFunctionName = 'sfn_digitalWrite';
Legacy Code オブジェクトを新規作成し、S-Function 名を指定します。
lct.StartFcnSpec = 'pinMode(uint8 p1)';
ブロックが必要とする初期化処理は StartFcnSpec で指定します。ここに記述したステートメントは、model_initialize() 内へ反映されます。
ここで pinMode() の引数が足りない点に気付かれたと思います。Legacy Code オブジェクトのプロパティ ○○○FcnSpec では、引数に定数値を記述できないため、このようにして一旦 MEX 用 C ソースファイルと tlc ファイルを生成し、その後で編集するという手順を踏みます。
lct.OutputFcnSpec = 'digitalWrite(uint8 p1, uint8 u1)';
model_step() 内へ反映させたい処理は OutputFcnSpec プロパティに指定します。
legacy_code('sfcn_cmex_generate', lct);
legacy_code('sfcn_tlc_generate', lct);
MEX用Cソースファイルと tlc ファイルを生成します。カレント作業フォルダに sfn_digitalWrite.c と sfn_digitalWrite.tlc が作られます。
2) sfn_digitalWrite.c を編集
2-1) mdlStart()
/* Function: mdlStart ================================================
* Abstract: ...
*/
static void mdlStart(SimStruct *S)
{
/* Get access to Parameter/Input/Output/DWork data */
uint8_T* p1 = (uint8_T*) ssGetRunTimeParamInfo(S, 0)->data;
/* Call the legacy code function */
pinMode(*p1);
}
static void mdlStart(SimStruct *S)
{
}
{ と } で囲まれたコードを全て削除します。
2-2) mdlOutputs()
/* Function: mdlOutputs ===============================================
* Abstract: ...
*/
static void mdlOutputs(SimStruct *S, int_T tid)
{
/* Get access to Parameter/Input/Output/DWork data */
uint8_T* p1 = (uint8_T*) ssGetRunTimeParamInfo(S, 0)->data;
uint8_T* u1 = (uint8_T*) ssGetInputPortSignal(S, 0);
/* Call the legacy code function */
digitalWrite(*p1, *u1);
}
static void mdlOutputs(SimStruct *S, int_T tid)
{
}
{ と } で囲まれたコードを全て削除します。
3) sfn_digitalWrite.tlc を編集
3-1) BlockTypeSetup()
%% Function: BlockTypeSetup ===========================================
%function BlockTypeSetup(block, system) Output
%if ::GenCPP==1 && !IsModelReferenceSimTarget()
...
%endif
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
...
%else
%endif
%endfunction
%function BlockTypeSetup(block, system) Output
%if ::GenCPP==1 && !IsModelReferenceSimTarget()
...
%endif
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
%% 全て削除
%else
%if EXISTS(::_DONE_ARDUINO_H_SETUP_) == 0
%assign ::_DONE_ARDUINO_H_SETUP_ = 1
%assign srcFile = LibGetModelDotCFile()
%openfile tmpBuf
#include "Arduino.h"
%closefile tmpBuf
%<LibSetSourceFileSection(srcFile,"Includes",tmpBuf)>
%endif
%endif
%endfunction
%if と %else で挟まれた箇所
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
...
%endif
は、アクセラレータ モード シミュレーションに関するコードです。全て削除します。sfn_digitalWrite.tlc の他のパートも同様です。
編集後の
%if EXISTS(::_DONE_ARDUINO_H_SETUP_) == 0
%assign ::_DONE_ARDUINO_H_SETUP_ = 1
%assign srcFile = LibGetModelDotCFile()
%openfile tmpBuf
#include "Arduino.h"
%closefile tmpBuf
%<LibSetSourceFileSection(srcFile,"Includes",tmpBuf)>
%endif
は、model.c ファイルに「#include "Arduino.h"」を書き出す記述です。
BlockTypeSetup() はコード生成時、ブロックのタイプごとに一度だけ実行されます。モデルに同じブロックが複数存在していても、この関数が実行されるのは1回です。しかし、ブロックの種類が異なれば、おのおのの BlockTypeSetup() が実行されるため、「_DONE_ARDUINO_H_SETUP_」の有無を確かめ、インクルード ステートメントの多重な記述を防ぎます。
「_DONE_ARDUINO_H_SETUP_」はユーザ定義の変数 ーつまり、変数名は任意ー で、二連コロン「::」はグローバルスコープを意味します。
%assign srcFile = LibGetModelDotCFile()
LibGetModelDotCFile() で model.c ファイルへの参照 (ハンドル) を取得して、
%<LibSetSourceFileSection(srcFile,"Includes",tmpBuf)>
そのインクルード セクションへ tmfBuf の内容をセットします。
3-2) BlockInstanceSetup()
%% Function: BlockInstanceSetup =========================================
%function BlockInstanceSetup(block, system) Output
%assign uint8Type = LibGetDataTypeNameFromId(::CompiledModel.tSS_UINT8)
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
...
%else
%<LibBlockSetIsExpressionCompliant(block)>
%endif
%endfunction
%function BlockInstanceSetup(block, system) Output
%assign uint8Type = LibGetDataTypeNameFromId(::CompiledModel.tSS_UINT8)
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
%% 全て削除
%else
%<LibBlockSetIsExpressionCompliant(block)>
%endif
%endfunction
%if と %else で挟まれた箇所を削除します。編集はこれのみです。
3-3) Start()
%% Function: Start =================================================
%function Start(block, system) Output
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
...
%else
%assign p1_val = LibBlockParameter(p1, "", "", 0)
pinMode(%<p1_val>);
%endif
%endfunction
%function Start(block, system) Output
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
%% 全て削除
%else
%assign p1_val = LibBlockParameter(p1, "", "", 0)
pinMode(%<p1_val>,OUTPUT);
%endif
%endfunction
%if と %else で挟まれた箇所を削除し、pinMode() の第2引数を補います。
3-4) Outputs()
%% Function: Outputs ===============================================
%function Outputs(block, system) Output
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
...
%else
%assign p1_val = LibBlockParameter(p1, "", "", 0)
%assign u1_val = LibBlockInputSignal(0, "", "", 0)
digitalWrite(%<p1_val>, %<u1_val>);
%endif
%endfunction
%function Outputs(block, system) Output
%if IsModelReferenceSimTarget() || CodeFormat=="S-Function" || ::isRAccel
%% 全て削除
%else
%assign p1_val = LibBlockParameter(p1, "", "", 0)
%assign u1_val = LibBlockInputSignal(0, "", "", 0)
digitalWrite(%<p1_val>, %<u1_val>);
%endif
%endfunction
%if と %else で挟まれた箇所を削除します。編集はこれのみです。
4) MEX ファイルの生成
legacy_code('compile', lct);
MEX 用 C ソースファイルから mex ファイルを生成します。カレント作業フォルダに sfn_digitalWrite.mex64 が作られます。
5) デバイスブロックの生成
legacy_code('slblock_generate', lct);
マスク化 S-Function ブロックを生成します。新規のモデルウィンドウが立ち上がり、その中に作成したデバイスブロックが置かれます。
モデルを任意のファイル名で保存します。ここでは untitled.slx としました。
見栄えを良くする場合は、MATLAB ドキュメンテーション「ブロック マスクの作成」のリンクを参考ください。
6) カスタム ライブラリの作成
copyfile sfn_digitalWrite.* z:\qiita\mytarget\blocks
copyfile untitled.slx z:\qiita\mytarget\blocks
出来上がった以下のファイルを mytarget¥blocks フォルダへコピーします。
- sfn_digitalWrite.c
- sfn_digitalWrite.tlc
- sfn_digitalWrite.mex64
- untitled.slx (sfn_digitalWrite ブロックが置かれたモデル)
cd z:\qiita\mytarget\blocks
if ~contains(path,pwd,IgnoreCase=true)
addpath(pwd);
savepath;
end
mytarget¥blocks フォルダへ移動し、blocks フォルダが検索パスに含まれているかを確認します。含まれていなければ、検索パスへ追加します。
hs = load_system('untitled');
hd = new_system('mytarget_lib','Library');
add_block('untitled/sfn_digitalWrite','mytarget_lib/sfn_digitalWrite');
set_param("mytarget_lib","EnableLBRepository","on");
set_param("mytarget_lib","Lock","on")
save_system(hd);
close_system(hd);
close_system(hs);
空のライブラリ「mytarget_lib」を作成し、untitled.slx の sfn_digitalWrite ブロックをコピーして貼り付けます。
ライブラリ モデルのプロパティ EnableLBRepository と Lock を「on」にして、保存します。
mytarget_lib.slx と untitled.slx を閉じます。
次のスクリプト m ファイルを「slblocks.m」として mytarget¥blocks フォルダに保存します。
function blkStruct = slblocks
Browser.Library = "mytarget_lib";
Browser.Name = "mytarget for Arduino";
blkStruct.Browser = Browser;
end
Simulink エディタを立ち上げて、ライブラリ ブラウザを開きます。ライブラリ リストに slblocks.m の [Browser.Name] で指定した名前が載っているかを確認します。
mytarget_make_rtw_hook.m
第1回 で紹介した STF_make_rtw_hook.m です。定型なのでテンプレートを利用します。組み込み向けのテンプレートファイルは [matlabroot]¥toolbox¥coder¥embeddedcoder¥ert_make_rtw_hook.m です。これをコピーして、Target フォルダ (mytarget¥mytarget) へ「mytarget_make_rtw_hook.m」というファイル名で保存します。
ファイルを開き、関数名を編集します。編集箇所はこれのみです。
- 編集前:ert_make_rtw_hook
- 編集後:mytarget_make_rtw_hook
function mytarget_make_rtw_hook(hookMethod, ...
modelName, ...
rtwroot, ...
templateMakefile, ...
buildOpts, ...
buildArgs, ...
buildInfo) %#ok<INUSL,INUSD>
% Copyright 1996-2021 The MathWorks, Inc.
switch hookMethod
case 'error'
msg = DAStudio.message('RTW:makertw:buildAborted', modelName);
disp(msg);
case 'entry'
msg = DAStudio.message('RTW:makertw:enterRTWBuild', modelName);
disp(msg);
option = LocalParseArgList(buildArgs);
if ~strcmp(option,'none')
ert_unspecified_hardware(modelName);
cs = getActiveConfigSet(modelName);
cscopy = cs.copy;
ert_auto_configuration(modelName,option);
locReportDifference(cscopy, cs);
end
case 'before_tlc'
case 'after_tlc'
case 'before_make'
case 'after_make'
case 'exit'
if strcmp(get_param(modelName,'GenCodeOnly'),'off')
msgID = 'RTW:makertw:exitRTWBuild';
else
msgID = 'RTW:makertw:exitRTWGenCodeOnly';
end
msg = DAStudio.message(msgID,modelName);
disp(msg);
end
end
...
ここでは省略していますが、引数についてはこのファイルで詳しく書かれていますので、そちらをお読みください。
コードを生成する
これでカスタムラピッドプロトタイピング環境によるコード生成の準備が整いました。簡単なモデルでコード生成を行います。
まずは検索パスを設定します。
addpath('z:\qiita\mytarget');
addpath('z:\qiita\mytarget\mytarget');
addpath('z:\qiita\mytarget\blocks');
savepath;
(※) mytarget¥blocks フォルダは既に検索パスが通っていますから、行わなくても良いです。
モデルを入れる適当なフォルダを作成します。ここでは次としました。
次のモデルを作成します。ファイル名は blink.slx としました。
ブロック:Repeating Sequence Stair (Simulink ⇒ Sources)

ブロック:sfn_digitalWrite (Arduino for Mytarget)

コンフィギュレーションパラメータ

[ソルバー] ペイン
[ソルバーの選択] カテゴリ: [タイプ] ...「固定ステップ」
[ソルバーの選択] カテゴリ: [ソルバー] ...「離散 (連続状態なし)」
[ソルバーの詳細] カテゴリ: [固定ステップ時間 (基本サンプル時間)] ... 適宜

[コード生成] ペイン
[ターゲットの選択] カテゴリ: [システムターゲットファイル] ...「mytarget.tlc」
[ビルドプロセス] カテゴリ: [コード生成のみ] ... 「チェックあり」
[詳細設定パラメータ] カテゴリ: [詳細なビルド (コマンド表示)] ... 「チェックあり」

[コード生成]-[インターフェイス] ペイン
[ソフトウェア環境 サポート] カテゴリ:
[浮動小数点数]、[非有理数]、[複素数]、[絶対時間]、[連続時間]、[可変サイズの信号] ... 「チェックなし」
次をタイプしてコードを生成します。
slbuild('blink')
ビルドメッセージがコマンドウィンドウに表示されます。当方ビルドメッセージを参考までに次に載せます。
>> slbuild('blink')
### ビルド プロセスを開始中: blink
### 'モデル固有' フォルダー構造へコードとアーティファクトを生成中
### コードをビルド フォルダーに生成しています: Z:\qiita\model\blink_mytarget_ert
### Invoking Target Language Compiler on blink.rtw
### Using System Target File: Z:\qiita\mytarget\mytarget\mytarget.tlc
Converting 'Z:\qiita\mytarget\mytarget\mytarget.tlc' to UTF-8
### Loading TLC function libraries
.......
### Generating TLC interface API for custom data
### Initial pass through model to cache user defined code
.
### Caching model source code
........................... Converting 'Z:\qiita\mytarget\mytarget\mytarget_srmain.tlc' to UTF-8
.
### Writing header file blink_types.h
### Writing source file blink.c
### Writing header file blink.h
.
### Writing header file rtwtypes.h
### Writing header file blink_private.h
### Writing source file blink_data.c
### Writing source file ert_main.c
### TLC code generation complete (took 1.779s).
.### バイナリ情報のキャッシュを保存しています。
.
### テンプレート makefile Z:\qiita\mytarget\mytarget\mytarget.tmf を処理しています
### makefile Z:\qiita\model\blink_mytarget_ert\blink.mk を作成しました
### コード生成が正常に完了: blink
### 'Z:\qiita\model\blink.slxc' に 'blink' の Simulink キャッシュ アーティファクトが作成されました。
ビルド概要
最上位モデルターゲット:
モデル ビルドの理由 ステータス ビルド期間
========================================================================
blink 情報キャッシュ フォルダーまたはアーティファクトが見つかりませんでした。 コードが生成されました。 0h 0m 6.3603s
1/1 ビルドされたモデル (既に最新のモデル 0)
ビルド期間: 0h 0m 7.0028s
model フォルダ下に「blink_mytarget_ert」というフォルダが作られているので、その中の blink.c を開き、「#include "Arduino.h"」、「pinMode(...)」、「digitalWrite(...)」のステートメントを確認します。
#include "Arduino.h"
#include "blink.h"
#include "rtwtypes.h"
DW_blink_T blink_DW;
static RT_MODEL_blink_T blink_M_;
RT_MODEL_blink_T *const blink_M = &blink_M_;
void blink_step(void)
{
uint8_T rtb_FixPtSum1;
rtb_FixPtSum1 = blink_ConstP.Vector_Value[blink_DW.Output_DSTATE];
digitalWrite(((uint8_T)13U), rtb_FixPtSum1);
rtb_FixPtSum1 = (uint8_T)(blink_DW.Output_DSTATE + 1);
if (rtb_FixPtSum1 > 3) {
blink_DW.Output_DSTATE = 0U;
} else {
blink_DW.Output_DSTATE = rtb_FixPtSum1;
}
}
void blink_initialize(void)
{
pinMode(((uint8_T)13U),OUTPUT);
}
void blink_terminate(void)
{
}
終わりに...
第5回は以上です。コードは無事に生成できたでしょうか。
blink.c のコードは、コンフィギュレーションパラメータの設定で異なることもあります。Arduino.h、pinMode()、digitalWrite() がこのように記述されていれば十分です。
これまでに登場したファイル (今回登場したファイルは薄黄色) とその格納先フォルダを示して、本回の締めくくりとします。最後までお読みくださり、ありがとうございました。
