(はじめに)
Simulink には便利な For Each Subsystem ブロックがありますが、制限もある様です。
筆者はカスケード的な反復計算(同じ計算の多段化)を表現する必要があって、マスクブロックの初期化部分で For 文を使って描画することで、マスクパラメータによって反復回数(段数)をコントロールする表現が作れましたので紹介します。
(概要)
上の図で、マスクブロックの書き方と Simulink モデル(ブロックと配線)の描画コマンドを知っている方は、本記事の内容は把握されたかとも思いますが、概要を説明します。
左は For Each Subsystem でカスケード(縦続)処理を表現したものです。例は分かりやすい様に1段の処理を2倍ゲインとしています。ベクトルサイズ3+1=4で4並列の処理ですが、要素を1つずらしてフィードバックする形で4段カスケードとなります。
右はマスクブロック内の Stage_0 ブロックを、マスクパラメータで指定の4段となる様に、コピーを3つ作成してカスケード接続したものです。(タイトルで"コア"と言っている部分)
最終の出力は左と同様、振幅1の正弦波から2の4乗で振幅16倍の正弦波となります。
一般の For (loop) 文を Unroll した形ですが、今回はカスケード接続専用の方法になります。
マスクエディターの初期化コマンドのフォームに直接スクリプトを書くことができますが、デバッグしやすい様スクリプトファイルにして呼び出しています。
その内容は後半に掲載・説明しますが、処理の概要は下記になります。
・配線 line は全て消去、ブロックは入出力ポートと最初の段のブロック以外を消去
・段数のパラメータに応じて最初のブロックをコピーして並べる
・継続接続になる様、配線を行う
(背景)
筆者はデジタル回路の設計を Verilog HDL 記述や、”DSP Builder for Intel® FPGA”(以下 DSP Builder) を使って行っています。
その関係で、今回のアドベントカレンダーで Intel FPGA コミュニティで下記の記事を投稿させていただきました。
その記事もポイントは同じ回路を多段にカスケード接続する事です。
DSP Builder を使う場合、For Each Subsystem はサポートされていない、または、ループ表現が疑似代数ループ(artificial algebraic loop)となってエラーになる可能性があります。
これを避けるため、カスケード接続の回路を作成しましたが、段数を変更する為に手作業を行うことは非効率で、それも避けたいと当然ながら思いました。
DSP Builder では、ライブラリの一部に、初期化コマンドを含めてマスクブロックとして中身が見えるブロックがあり(例:TappedDelayLine)、パラメータに応じてマスクブロック内部のブロック線図を書き換えるものがあります。本記事はそちらを参考にしています。
DSP Builder や HDL Coder の様に、ハードウェアにマッピングすることが前提のモデルベース設計環境では、この様な Unroll 的に Subsystem ブロックの内容をコピーして接続するマスクブロックの作り方を知っておくと、活用できる機会が色々あると考えています。
下記 Mathworks 社の Help では、反復計算を HDL Coder で実装する場合、コアのカスケードを手動で行う代わりに For Each Subsystem で 表現すると便利、ただし疑似代数ループ防止にパイプラインレジスタを入れる様に言っています。本記事はその代案にもなります。
(描画用 初期化スクリプト の解説)
冒頭の図の右側のマスクブロック初期化用のスクリプト "Draw_Cascade_Subsystem.m" の内容を順に説明します。全体は記事の最後に掲載しています。
細部はコードを読めばわかる、という人はここは読み飛ばしていただければと思います。
説明用に、1入力1出力(マスクブロック、内部 Subsystem とも)の仕様で記述しています。マスクブロックのポート名は "In1", "Out1" で内部ブロックのポート名は任意です。
% num_stage = 4; % mask parameter for the number of stages
PFix_SName = 'Stage_'; % Custom prefix of stage block name
BaseStage = [PFix_SName '0'];
AppStages = [PFix_SName '[1-9][0-9]*']; % (Reguler Expression) Appended Stage names
1行目の num_stage = 4 はデバッグ用の代入です。
スクリプト作成・改造中はデバッグ用にマスクを外したブロック対象とします。(エラー発生個所が表示されるため)その場合、マスクパラメータが使えないので、コメント文字を外してこれで与えます。
デバッグが完了したら、マスクブロックを設定して初期化で呼び出しますので、この名前のマスクパラメータを定義し、先頭行は削除またはコメントアウトします。
とんで3行目でカスケードされる各ステージのブロックの前半共通部分を設定します。
'Stage_' としていますがお好みの名前にしてください。(最後の文字は数字は禁止)
次行で '0' を連接してコピー元のブロック名とします。カスタムする場合これを編集します。
5行目は上記以外のコピーされたブロックを消去対象とするための正規表現です。
block = gcb;
set_param(block, 'MaskSelfModifiable', 'on'); % Set 'SelfModifiable'
7行目の gcb はマスクの初期化ではそのマスクブロックを返します。デバッグ時は、図の様にマスクのない対象ブロック "Cascade Draw Test" を表示、選択(青枠表示)した状態でスクリプトを実行します。それを返します。変数 block に設定して扱います。
9行目はマスクブロックの初期化でブロックの変更を可能にする設定です。
なお、ブロックの内容は In1, Out1 ポートと Stage_0 で構成、Stage_0 の内容は1入力1出力で任意の行いたい処理(例は Gain x2)を表現します。
delete_line(find_system(block, 'SearchDepth', 1,...
'FollowLinks', 'on', 'LookUnderMasks', 'all',...
'FindAll', 'on', 'Type', 'line'));
app_stages = find_system(block, 'SearchDepth', 1,...
'FollowLinks', 'on', 'LookUnderMasks', 'all',...
'RegExp', 'on', ...
'Name', AppStages); % Block name cascaded
if ~isempty(app_stages) delete_block(app_stages); end
11-13 行で信号線をすべて消去、14-18 行で Stage_1 以降のブロックがあれば消去します。
In1, Stage_0, Out1 の、ポートを含む3ブロックは残っています。
stages = 1;
if( num_stage > 1 ) stages = num_stage; end
set_param([block '/' BaseStage], 'Position', [ 70, 20, 110, 80 ]);
set_param([block '/In1'], 'Position', [ 20, 43, 50, 57 ]);
add_line(block, 'In1/1', [BaseStage '/1'], 'AUTOROUTING','ON');
20-21 行は num_stage の値を stages にコピーしますが異常値は回避策で 1 を設定します。
23-24 行の set_param コマンドは In1 と Stage_0 ブロックの座標を設定します。上の図の右上緑枠内の様に、Subsystem を作ったときのデフォルトの座標を使っています。
26 行の add_line で入力ポート In1 から Stage_0 の入力に配線します。
PreStage = BaseStage;
for idx_stg=1:(stages-1)
NewStage = [PFix_SName num2str(idx_stg)];
add_block([block '/' BaseStage], [block '/' NewStage], ...
'Position',[ 70+60*idx_stg, 20, 110+60*idx_stg, 80 ]);
add_line(block, [PreStage '/1'], [NewStage '/1'], 'AUTOROUTING','ON');
PreStage = NewStage;
end
カスケード構成作成の本体部分です。
for 文中で追加ステージを置き、前段(PreStage)と接続する事を必要数反復実行します。
28 行で「最初の」前段として Stage_0 を設定しています。
29-35 行の for 文は(全ステージ数 -1)段を順に追加します。idx_stg が追加ステージの番号になります。add_block で Stage_0 を Stage_(idx_stg) にコピーし、add_line で前段の出力を追加した段の入力に接続します。
if ( stages <= 1 ) idx_stg=0; end
set_param([block '/Out1'], 'Position', [130 + 60*idx_stg, 43, 160 + 60*idx_stg, 57]);
add_line(block, [PreStage '/1'], 'Out1/1', 'AUTOROUTING','ON');
37行目の if 文は stage = 1 だった場合に for 文がスキップされ、idx_stg の値が空のセルになってしまうのでその手当てです。(条件文の不等式がやや弱気/小心ですが...(汗))
最後の 39-40 行で出力ポート Out1 の位置を調整、最後のステージの出力を接続します。
これで、冒頭の図の右下の様に、対象のブロック(Cascade Subsystem / Cascade Draw Test)の中身の Stage_0 がパラメータ num_stage の数だけカスケードされたブロック図になります。
(マスクブロックの作成)
マスクブロックとして運用するためには、対象のブロックにマスクを作成(編集)します。
マスクエディターで編集し、パラメータとダイアログタブで必要なマスクパラメータを作成します。通常の数値であれば [エディット] で良いと思います。
今回の例ではカスケードする段数を "Number of stages" のプロンプト表示で作成、受け取るマスクパラメーター(ダイアログ変数)名はスクリプト内で参照している(デバッグ時と共通の)num_stage にします。デバッグ時のスクリプト先頭の代入文を忘れずにコメントアウトしてください。
初期化タブには初期化コマンドのフィールドに描画用のスクリプトのファイル名(prefix)か、中身を記載します。
アイコンと端子、ドキュメンテーションは特に設定は必須ではありません。
これで、手軽に段数を変更できる様になります。
(入力の分配・出力の統合対応)
冒頭の図の構成で、1つ欠点の様に感じられた方がいらっしゃるかもしれません。
左の For Each Subsystem 方式だとカスケードの途中の段の出力も露出しているので、2,4,8,16 倍の結果がベクトルで取り出せていますが、右のマスクブロック内カスケード方式だと途中出力が見えていません。
また、入力を共有するとかベクトル中のステージ番号の位置の要素をそのステージで使うとかいう構造の場合やはりできない様に見えます。
必要な場合、出力はこちらの図の様に MUX ブロックで途中結果を束ねて出す方法も考えられますが、カスケード構造ではない部分が出てきて初期化の描画がややこしくなってきます。
カスケード構造にこだわれば初期化スクリプトがこれ以上複雑化することはないという視点で、どうすれば途中出力も出していけるかと考えますと、下記の2つの方法があります。
(基本的には、ベクトルと MUX/DEMUX を利用するという点で共通です)
(1)入力を各ステージで共有するための入力スルー信号、途中出力を外部へ出力するために積み上げていく出力用信号もカスケードされる信号として用意する
(2)あくまで1ポートにこだわる場合、1ポートを大きなベクトルとして、(1)の入力共有、出力積み上げ用の部分も含める
背景のところで触れました Intel FPGA コミュニティに出した記事で(1)の方法を使っていますので、ポイントを説明します。(2)はまだ試してはいませんので、良い方法か、かえって面倒か、はわかりません。
拡大しないとわかりづらいてんこ盛りな図ですが、上記記事のメイン処理(シフト+積和演算)をカスケードするマスクブロックの中身です。
入力分配用のパスが各ステージで SVI-SVT-SVI.... となる一番上のチェーンで、各ステージ内で Pass Through になっています。
中段がメイン処理の実際にカスケードになっている PST-NST-PST... のチェーンです。
一番下のチェーン RVI-RVO-RVI... が各ステージの計算結果を MUX で統合して積み上げていくパスで Stage_0 の入力ポートのサイズ(ベクトル幅)をゼロにするわけにはいかないのでダミーを入れています。
(こうして見返すと、入力と出力のチェーンは統合して2チェーンにしても良いかもしれません)
初期化スクリプトは例として提示済の1入力1出力の場合と構造はほぼ同じで、ポートと接続用チェーン信号についての set_param, add_line が1行から3行になっただけ、という形ですので割愛します。
ただし下記の点は注意点になります。
・ダミー入力、廃棄出力の個所のチェーンは対応関係がイレギュラーになる
・ステージ番号を各ステージで認識させるため、各ステージのブロックにもマスクを設定してパラメータを持たせている。さらに下位階層でもブロックの使い勝手の為マスクを設定している(初期化での描画変更はしていない)
・初期化スクリプトから各ステージのブロックにステージ番号を設定する場合、マスクパラメータ(ダイアログ変数)名で渡すのではなく、MaskValues または MaskValueString (下記ヘルプ参照) への設定として渡す。(ダイアログウィンドウの仕組みを経由するので共通の名前を使う、というイメージですかね。)
パラメータ渡しを本記事のサンプルに合わせて書くとすると、例えば下記の様になります。
(パラメータはステージ番号1つだけと仮定します。)
for 文直前に Stage_0 の設定、 for 文中で各ステージの設定をしています。
...
set_param([block '/' BaseStage], 'MaskValueString', ['0'] ); % For 'Stage_0'
for idx_stg=1:(stages-1)
NewStage = [PFix_SName num2str(idx_stg)];
...
set_param([block '/' NewStage], 'MaskValueString', [num2str(idx_stg)] );
PreStage = NewStage;
end
(考察)
時間がないので簡単に...(公開日当日 2:00 頃システム不調で公開してからダラダラ update してすみません...もう 6:30 だ)
・MUX/DEMUX でダミー信号を多用しているため、ハードウェア化すれば最適化で削除されると思いますが、Simulink シミュレーションでは重たくなるかもしれません
・初期化の仕組みはこれ以上に色々凝りようはあるかもしれませんが、個々のモデル作成でアルゴリズムの表現に利用するには、作業性などが悪く Simulink モデルの利点を捨てる面もある為、この程度の定型的な表現にとどめたほうが良い様にも思います
(おわりに)
反復計算をカスケードに unroll 的に表現することが、マスクブロックの初期化でブロック図を描画することで、パラメータ対応で自動作成することができました。
ご閲覧ありがとうございました。
(文中に記載されている会社名、商品名は各社の商標または、登録商標です。)
(参照用:カスケード接続描画用の初期化スクリプトサンプル)
% num_stage = 4; % mask parameter for the number of stages
PFix_SName = 'Stage_'; % Custom prefix of stage block name
BaseStage = [PFix_SName '0'];
AppStages = [PFix_SName '[1-9][0-9]*']; % (Reguler Expression) Appended Stage names
block = gcb;
set_param(block, 'MaskSelfModifiable', 'on'); % Set 'SelfModifiable'
delete_line(find_system(block, 'SearchDepth', 1,...
'FollowLinks', 'on', 'LookUnderMasks', 'all',...
'FindAll', 'on', 'Type', 'line'));
app_stages = find_system(block, 'SearchDepth', 1,...
'FollowLinks', 'on', 'LookUnderMasks', 'all',...
'RegExp', 'on', ...
'Name', AppStages); % Block name cascaded
if ~isempty(app_stages) delete_block(app_stages); end
stages = 1;
if( num_stage > 1 ) stages = num_stage; end
set_param([block '/' BaseStage], 'Position', [ 70, 20, 110, 80 ]);
set_param([block '/In1'], 'Position', [ 20, 43, 50, 57 ]);
add_line(block, 'In1/1', [BaseStage '/1'], 'AUTOROUTING','ON');
PreStage = BaseStage;
for idx_stg=1:(stages-1)
NewStage = [PFix_SName num2str(idx_stg)];
add_block([block '/' BaseStage], [block '/' NewStage], ...
'Position',[ 70+60*idx_stg, 20, 110+60*idx_stg, 80 ]);
add_line(block, [PreStage '/1'], [NewStage '/1'], 'AUTOROUTING','ON');
PreStage = NewStage;
end
if ( stages <= 1 ) idx_stg=0; end
set_param([block '/Out1'], 'Position', [130 + 60*idx_stg, 43, 160 + 60*idx_stg, 57]);
add_line(block, [PreStage '/1'], 'Out1/1', 'AUTOROUTING','ON');