こちらは新しいシリーズで、OTeeというVirtual PLCを使用し様々な記事を展開します。第5話ではOTee Platform上でFunction Blockを作成します。Function Blockを使用することによって、コードを再利用でき、プログラムの可読性をアップし、同じロジックを“部品化”して何度でも使えに繋がります。
また、これからはOTeeのVirtual PLCをBerghofのMC-Pi Proにインストールし、記事を展開します。
さ、FAを楽しもう。

前書き
いつも私の技術ブログとYouTubeチャンネルをご覧いただき、心より感謝申し上げます。また、いまFullさん(full@桜 八重 (@fulhause) / X)と共に毎週水曜日の夜にお届けしている「高橋クリス」ラジオ番組を運営しています。
技術は独り占めせず、届けるもの
私たちは工場の生産技術や制御に関する技術情報を、ブログや動画などで無料公開しています。「知識は誰でもアクセスできるべき」という信念のもと、現場で役立つ具体的なノウハウやトラブル事例などを発信してきました。すべて無料で続けているのは、「知らなかったせいで困る人」を少しでも減らしたいからです。
また、もしあなたの現場で…
- 「このPLCとデバイスの組み合わせ、ちゃんと動くのかな?」
- 「EtherCAT通信でうまくいかない部分を検証してほしい」
- 「新しいリモートI/Oを試したいけど社内に検証環境がない」
など、困っている構成や試してみたいアイデアがあれば、ぜひお知らせください。機器の貸出や構成の共有が可能であれば、検証し、記事や動画で発信します(ご希望に応じて匿名対応も可能です)。
支援のかたち
現在、私達の活動はほぼ無償で続けており、記事や動画の制作には、時間と検証環境の整備が必要です。この活動を継続的にコンテンツを提供するためには、皆様の温かいご支援が大変重要です。
メンバーシップ(ラジオの応援)
Fullさんとのラジオをより充実させるための支援プランです。
https://note.com/fulhause/membership/join
Amazonギフトリスト
コンテンツ制作に必要な機材・書籍をリストにしています。
https://www.amazon.co.jp/hz/wishlist/ls/H7W3RRD7C5QG?ref_=wl_share
Patreon(ブログ・動画活動への応援)
月額での小さなご支援が、記事の執筆・検証環境の充実につながります。
https://www.patreon.com/user?u=84249391
Paypal
小さな支援が大きな力になります。
https://paypal.me/soup01threes?country.x=JP&locale.x=ja_JP
知ってたら助かること、届けたいだけです
あなたの応援が、知識の共有をもっと自由で持続可能なものにしてくれます。これからもどうぞよろしくお願いします。
soup01threes*gmail.com
技術はひとりじゃもったいない。
Reference Link
http://soup01.com/ja/category/otee/
MC-Pi Pro?
Berghof社製のMC-Pi Proは、クアッドコアCPUと完璧に調整されたハードウェアとソフトウェアにより、優れたリアルタイム動作で感動を与えます。最先端のRaspberry Piテクノロジー(CM4)と一流のBerghofエンジニアリングにより、産業用アプリケーションで最高のパフォーマンスを発揮します。
2 GB RAM、8 GB Flash、200 kB Retainを搭載したエントリーレベルのモジュールでも、幅広いアプリケーションに対応可能です。

なぜFBを使うのか?
FBは「状態を持つ再利用部品」で、同じロジックを“部品化”して何度でも使え、各インスタンスが自分の内部状態(メモリ)を保持します。
状態保持(メモリを持てる)
タイマ、カウンタ、ラッチ、シーケンスの段管理などは“前回値”が必要。FBは内部変数に自分の状態を持てます(関数は持てない)。
再利用性と一貫性
1回作ればどの装置でも流用可。バグ修正や仕様変更はFB本体を直すだけで全インスタンスに効く→品質とスピードが上がる。
多重配置(インスタンス化)
同一FBをモータ1〜モータ200へ横展開。各インスタンスが独立動作するので、コピペ地獄とヒューマンエラーを回避できます。
抽象化(インタフェース化)
複雑な処理を「入力→出力」だけに見せて中身を隠す。上位ロジックは“何をしたいか”に集中でき、下位の実装差(メーカー差・機種差)を吸収できます。
テスト容易性
単体テストしやすく、シミュレーションも楽。I/Oをモック化すれば現場に持ち込む前に動作確認ができる。
保守性と拡張性
仕様追加・パラメータ拡張が容易。プロジェクトが大きくなっても構造は壊れない。
FBがFCと違うところ?
- 関数(FUNCTION): 入力→出力の“計算”。内部状態を保持しない”使い捨て”。
- FB(FUNCTION BLOCK): 入力→出力に加え、内部状態を保持する“オブジェクト”。
なぜ配列を使うのか?
配列(ARRAY)は「同じ型の値を、連番の“箱”に並べて入れる固定長のデータ入れ」で、メモリ上は連続して並び、インデックスで要素にアクセスする。
横展開の自動化(スケール)
センサ/モータ/バルブなど“同型チャンネル”が多数ある時、配列+FORで同一ロジックを一括処理でき、コピペ地獄とヒューマンエラーを回避可能です。
一貫性・保守性
仕様変更はループ本体を1か所直すだけで、数多くあっても修正漏れが起きにくいんです。
I/O・通信との親和性
フィールドバスやレジスタは連続アドレス(WORD配列)で来ることがおおく、配列でそのままマップできます。
レシピ/チューニングの表形式化
しきい値・時間・ゲイン等を配列にまとめると、HMIから行(チャンネル)ごとに編集しやすいです。
Implementation1
Implementation1は実際自分の手でFBを作成する手順を説明します。
新しいFBの追加
それではOTee PlatformからFunction Blockを追加していきます。

Entityの追加画面が表示されます。

Typeー>Function Blockを選択します。

次はName欄にFunction Block名を入力します。

最後はSaveボタンで設定を保存します。

Done!新しいFBが追加されました。

インターフェース
次は右側にあるInterfaceで入出力パラメータを設定しましょう。今回の記事ではInput/Outputという少し特別なパラメータを使用します。

VARINPUT
こちらはImplementation1のFBで定義した入力パラメータです。
- xSensorInputSide1:センサー1の入力信号
- xSensorInputSide2:センサー2の入力信号
- tSensor1ONTime:センサー1の信号遅延設定、信号の揺れ防止
- tSensor1ONTime:センサー2の信号遅延設定、信号の揺れ防止

VAR
こちらはFB内部のメモリを保持する変数です。
- fbTON1:センサー1遅延用のTON タイマー
- fbTON1:センサー2遅延用のTON タイマー

VAROUTPUT
こちらはImplementation1のFBで定義した出力パラメータです。
- xSensorInSide1:センサー1の信号出力(遅延あり)
- xSensorInSide2:センサー1の信号出力(遅延あり)

VARTEMP
こちらはFB内部の一時的な変数(保持なし)です。
- _tfbTON1ET:センサー1の遅延経過時間
- _tfbTON2ET:センサー2の遅延経過時間
- _xfbTON1q:センサー1の遅延したあとの出力信号
- _xfbTON2q:センサー2の遅延したあとの出力信号

プログラム
こちらはImplementation1のFBプログラムです。
(**)
fbTON1(
IN := xSensorInputSide1,
PT := tSensor1ONTime,
Q => _xfbTON1q,
ET => _tfbTON1ET
);
(**)
fbTON2(
IN := xSensorInputSide2,
PT := tSensor2ONTime,
Q => _xfbTON2q,
ET => _tfbTON2ET
);
xSensorInSide1:=_xfbTON1q;
xSensorInSide2:=_xfbTON2q;
MAIN
次はMAINプログラムで先ほど定義したFBを呼び出しましょう。
VAR
Localー>VARー>Add variableで先程追加したFBをデータ・タイプとして定義しましょう。今回は”fbCylinder1Sensor”として定義します。

次はEditorで先程宣言した”fbCylinder1Sensor”を呼び出します。

Done!OTeeのPlatformで自動的にすべての入出力パラメータもTemplateとして自動生成します。

プログラム
”fbCylinder1Sensor”の各パラメータを適切に割り付けましょう。
fbCylinder1Sensor(
xSensorInputSide1 := xSensorFwSide,
xSensorInputSide2 := xSensorBwSide,
tSensor1ONTime := T#1s,
tSensor2ONTime := T#0.5s,
xSensorInSide1 => xSensorInFW,
xSensorInSide2 => xSensorInBw
);
結果
Done!私達で作成したFBが無事にMAINプログラムから呼び出されました。

FB内部のタイマーにもちゃんと動作しています。

Implementation2
次は新しいFBを作成し、Implementation1で作成したFBの起動を呼び出し→拡張します。
新しいFBの追加
OTee Platform→+で新しいFBを追加します。

FB内で別のFBを呼び出し
新しいFBに先Implementation1で作成したFBを宣言します。

配列パラメータの定義
今回のImplementationではInput/Outputパラメータで配列パラメータを宣言します。

ARRAYを選択します。

OTeeから配列の設定画面が表示されます。

Array Typeをクリックし→配列のデータ・タイプを設定します。

次はLower・Upperで配列のサイズを定義します。

そして定義された配列のデータ・タイプを変更したい場合は、データ・タイプのところに右クリックします。

そして変更するデータ・タイプを設定します。今回の実装では実数タイプに変更します。

Done!配列変数のデータ・タイプはREALに変更できました。

インターフェース
次は右側にあるInterfaceで入出力パラメータを設定しましょう。

VARINOUT
こちらはImplementation2のFBで定義した入出力パラメータです。
- arrrParameters:FB内部のパラメータを外部から設定できるように宣言した配列パラメータです。

VARINPUT
こちらはImplementation2のFBで定義した入力パラメータです。
- xSensorInputSide1:センサー1の入力信号
- xSensorInputSide2:センサー2の入力信号
- xReset:センサー1とセンサー2がONしたCounter数を0にリセットする
- xAlarmReset:FBのアラームをリセットする

VAR
こちらはFB内部のメモリを保持する変数です。
- _fb2SideSensor:Implementation1で定義したFB
- _xSensorInputSide1:センサー1の入力遅延信号(FB内部用)
- _xSensorInputSide2:センサー2の入力遅延信号(FB内部用)

こちらもFB内部のメモリを保持する変数です。
- _udiSensorSide1Counter:センサー1のOFF→ONのCounter
- _udiSensorSide2Counter:センサー1のOFF→ONのCounter
- _fbRTRIGReset:Reset信号の立ち上げ検知
- _xReset:リセット信号の立ち上げ出力
- _fbRTRIGSide1Up:センサー1の立ち上げ検知げ
- _fbRTRIGSide2Up:センサー2の立ち上げ検知げ
- _xSensor1CountUp:センサー1の立ち上出力
- _xSensor2CountUp:センサー2の立ち上出力
- _fbAlarm1:アラームの検知タイマー
- _xfbAlarm1Q:アラームの出力

VAROUTPUT
こちらはImplementation2のFBで定義した出力パラメータです。
- xSensorInSide1:センサー1の信号出力(遅延あり、アラームなし)
- xSensorInSide2:センサー1の信号出力(遅延あり、アラームなし)
- xAlarmSensorBothSideON:センサー1とセンサー2同時ONアラーム
- udiSensorSide1Counter:センサー1がOFF→ONした数
- udiSensorSide2Counter:センサー2がOFF→ONした数

プログラム
こちらはImplementation2のプログラムです。
- xResetの立ち上がりでカウンタ2つを0クリア(_fbRTRIGReset→_xResetを1スキャンだけTRUE)。
- _fb2SideSensorでセンサー値を取得します。
- それぞれの立上り検出(_fbRTRIGSide1Up/2Up)でカウントアップ。
- 両側ONが一定時間連続したら(_fbAlarm1のQ)、xAlarmSensorBothSideONをラッチ。
- xAlarmResetで解除。
- 最後に外部公開用の出力・カウントへ反映。
(*Counter Reset*)
_fbRTRIGReset(
CLK := xReset,
Q=>_xReset
);
if _xReset then
_udiSensorSide1Counter:=0;
_udiSensorSide2Counter:=0;
end_if;
(*Sensor Input FB*)
_fb2SideSensor(
xSensorInputSide1 := xSensorInputSide1,
xSensorInputSide2 := xSensorInputSide2,
tSensor1ONTime := REAL_TO_TIME(IN := arrrParameters[0]),
tSensor2ONTime := REAL_TO_TIME(IN := arrrParameters[1]),
xSensorInSide1 => _xSensorInSide1,
xSensorInSide2 => _xSensorInSide2
);
(*Counter UP Operation*)
_fbRTRIGSide1Up(
CLK := _xSensorInSide1,
Q=>_xSensor1CountUP
);
_fbRTRIGSide2Up(
CLK := _xSensorInSide2,
Q=>_xSensor2CountUP
);
if _xSensor1CountUP then
_udiSensorSide1Counter:=_udiSensorSide1Counter+1;
end_if;
if _xSensor2CountUP then
_udiSensorSide2Counter:=_udiSensorSide2Counter+1;
end_if;
(*Alarm Detection*)
_fbAlarm1(
IN := _xSensorInSide1 and xSensorInputSide2,
PT := REAL_TO_TIME(IN := arrrParameters[2]),
Q => _xfbAlarm1Q
);
(*Counter Reset*)
if _xfbAlarm1Q then
xAlarmSensorBothSideON:=True;
end_if;
if xAlarmReset then
xAlarmSensorBothSideON:=False;
end_if;
xSensorSide1:=_xSensorInSide1;
xSensorSide2:=_xSensorInSide2;
udiSensorSide1Counter:=_udiSensorSide1Counter;
udiSensorSide2Counter:=_udiSensorSide2Counter;
新しいConfigurationの追加
最後はプロジェクトに複数のプログラムを追加し→新しいタスクに割り付けます。
右側にあるToolbarからConfigurationをクリックします。

Add a Taskで新しいタスクを追加します。

Done!次はTask名を変更します。

Intervalでタスクの周期を定義し、今回の例では4msに設定します。

次はAdd an instanceでプログラムを割り付けます。

Done!新しいInstanceが追加されました。

次は下図のプログラム欄をクリックします。

そして呼び出したいプログラムを設定しましょう。

Done!

MAIN Program
最後はMAINプログラムです。先程定義したfb2SideSensorAlarmsをLocal変数として宣言します。

VAR
こちらはImplementation2で定義した内部変数です。

External
そしてExternal欄でグローバル変数とLinkさせます。

Configuration→Global Variablesでアプリケーションに合わせてグローバル変数を宣言しましょう。

プログラム
こちらは今回のImplementation2のMAINプログラムです。
(**)
garrrParameters1[0]:=0.1;
garrrParameters1[1]:=0.1;
garrrParameters1[2]:=0.5;
_fbCylinder2(
xSensorInputSide1 := xSensorInputSide1,
xSensorInputSide2 := xSensorInputSide2,
xReset := xReset,
xAlarmReset := xAlarmReset,
arrrParameters := garrrParameters1,
xSensorSide1 => xSensorSide1,
xSensorSide2 => xSensorSide2,
xAlarmSensorBothSideON => xAlarmSensorBothSideON,
udiSensorSide1Counter => udiSensorSide1Counter,
udiSensorSide2Counter => udiSensorSide2Counter
);
結果
Done!私達で作成したFBが無事にMAINプログラムから呼び出されました。

センサーCounterがセンサー入力により加算されます。


そしてSensor入力1とSensor入力2同時ONするとアラームがトリガーします。

また、xResetがOFF→ONするとアラームがリセットされます。
