
PLCにおけるオブジェクト指向(OOP)のサンプルプログラム(CODESYS、シーケンス制御技能士実技試験(令和4年2級))
はじめに
ST言語使用していますか?前回こんな記事を投稿しました。
おそらく多くの人がこの業界で最初に習ったであろう、コンベアのプログラムのST言語版です。ただし、前回の記事は多くの伝統的なPLCで再現できる、べた書きプログラムです。正直STで移植が容易といってもコードの再利用性が高くないです。
そこで今回は、高級言語ではおなじみのオブジェクト指向(OOP)を意識したプログラムにcodesysで挑戦しました。私も初めて作ってみたのでへたくそなプログラムですがご容赦ください。題材は前回と同じシーケンス制御技能士の問題です。過去問をネットで検索すると出てくると思います。
前回に引き続きフルSTで作りましたが、OOPにするだけならLDでもできると思います。ただしコードのほとんどをモニタ出来ないのでどうしてもSTができないという人以外はあまりお勧めできません。
OOPについて
これだけで本1冊以上の内容になります。私も調べるほどよくわからなくなります。抽象的な話は置いておいて、今のPLCと比較しながら簡単にまとめます。
PLCにはFB(クラスみたいなもの。伝統的なPLCの多くはメソッドがなく、継承できない)とFC(関数)があります。これにより、C言語のサブルーチン呼び出しみたいな構造化プロジェクトを作ることができます。
FB(この段落では伝統的なFBをFBと省略します)を使ってみた人ならわかると思いますが、便利ですがFBだけでプログラムしろと言われたら無理だろと言うと思います。FBは繰り返し処理の効率化になりますが、そんなものは大昔のPLCでも自作命令やマクロ機能などで実現できます。FBの問題点は大きくメモリと運用上の2点です。
まず、PLCは静的にコンパイルしたコードをサイクリックに動かし続けるものです。そのためFBのインスタンスの寿命は長いです。FB間のデータのやり取りがイベント的であっても、現状は事前に変数を用意しておく必要があります。なのでFBを階層化させながらコードを書くとメモリをかなり使用します(三菱R04CPUくらいなら簡単にパンクします)。ほかにもstaticな変数を用意できないので、インスタンスごとに引数で同じ変数を入れないとFB間でデータの相互参照ができないとかの問題もあります。伝統的なPLCでFBメインでプログラムを作ることは、とても効率的とは言えないです。
次に、FBには継承などの機能がないので、ライブラリの運用を考えると絶対に変更されない箇所以外FBにしづらいです。バリエーションごとに別のFBを作ると、修正時に同じ個所をたくさん修正する必要があります。しかし、それを嫌がって一つのFBでいろんな使い方ができるようにすると、たくさん条件分岐する複数の機能を持ったFBができます。メモリの無駄遣いでスキャンタイムも悪化し、可読性も低いFBになります。伝統的なPLCでFBメインでプログラムを作ることは、とても保守性が良いとは言えないです。
以上を踏まえてPLCにおけるOOPを、現状問題の解決という視点でまとめると、「メソッド(FB内のFCのようなもの)を活用して変数の寿命を適切に管理し、継承や集約を活用して保守性を高めた、FBメインのプログラム」と言えると思います。これができていればOOPというわけではないですが、なぜPLCでOOPが必要かに触れながら説明するとこんな感じになるかと。
その他の関係しそうなことは箇条書きにしておきます。思いついたことをメモ書き程度に。
・オブジェクトを意識してFBを作る。
・FBのインスタンス生成時にオブジェクトに必要なパラメータを入れる。メソッド実行時にオブジェクトに必要不可欠なパラメータを入れたりsetterを多用するのはよくない(与えるな、命じろ理論)。
・PLCの制御対象は静的なオブジェクトである。また、人命の安全を考慮する必要がある制御対象もある。
・継承は多用しない(極論すべてのコードが継承された依存関係にあるならべた書きコードと変わらない)。
・実績のあるFBはできるだけ中身を変えないように運用する。FBがそのまま使用できないならば、継承して一部追記しながらオーバーライドする。
・codesysは変数にprivate、publicなどの属性はつけられない。変数はVAR_で種類分けする。
・codesysは単一継承。
・そもそも論、PLCは入力→処理→出力と制御する、手続型のモジュールである。
・codesysは変数定義領域と処理のウィンドウが分かれている。また、FBとメソッドもウィンドウが分かれている。なのでTHISがないと変数を追いにくいかも。というかcodesysは変数の予測表示はされないけどTHIS^の後にドットを打つと予測表示されるので、左手デバイスにTHIS^やSUPER^を登録して、全部の変数に修飾子を入れるのが最速かも?
・完全にOOPにすると全く現場でモニタ出来なくなる。PLCはメカロス調整が必要なものも多く、同じ図面同じコードでも同じ動作にならないものを制御することが多い。机上デバグに限界はあるので、最小限現場領域のグローバル変数は必要だと思う。→これのせいでPLCのOOP論も宗教戦争になりそう(争いは繰り返される・・)
簡単にと言いつつ長くなり申し訳ありません(ここまで2000字以上)。いよいよ本題。
1.静的オブジェクトの作成
1.1 まずは部品(デバイス)を作ろう(interface、static、this)
オブジェクトを作る場合、まずは電気図面を見ながら静的オブジェクトをつくるのが楽だと思います。何ならこれだけ作って後はラダーでもOOP名乗れる? 図面に乗っている部品はデバイスと定義し、一意なデバイス名-例えば、LS1-を持っているとします。また、複数のデバイスが合わさって一つの機能を持つ場合、そのデバイスの集約品をアセンブリと定義します。この辺は一般的な電気図面の思想と同じはずです。PLCはハード図面と対応した設計図でプログラムを作るのが良いと思います。
早速デバイスを作るのですが、まずはinterfaceをつくります。これはFBのメソッドを定義する箇所です。電気図面でいえばシンボルでしょうか。以下はリミットスイッチのinterfaceです。
INTERFACE Itf_LimitSwitch
METHOD isPusshedOn : bool
PLCからみて、リミットスイッチに命令して取り出せる情報は、自身が押されているかどうかです。なので、isPusshedOnというメソッドを一つだけ宣言します。次にこのinterfaceをFBに実装します。
FUNCTION_BLOCK PUBLIC LS1 IMPLEMENTS Itf_LimitSwitch
VAR_INPUT
DI1 :BOOL;
END_VAR
VAR_STAT
ls: BOOL;
END_VAR
ls := DI1;
METHOD isPusshedOn : BOOL
isPusshedOn := ls;
これはリミットスイッチのFB、「LS1」です。(実際はFB作成時のウィンドウで設定しますが、)1行目でIMPLEMENTS Itf_LimitSwitchを入れたことでisPusshedOnメソッドが自動で追加されます(後から更新もできます)。
LS1は構成要素として1点のデジタル入力が必要なので、引数VAR_INPUTを使用します。それをFB本体でVAR_STAT変数であるlsに転送しています。VAR_STATはstaticであることを表します。すなわち、プロジェクト上でLS1のFBを複数使用しても同じメモリを見に行きます。おそらくそれぞれのインスタンスのlsはポインタになっているのでしょう。
そしてlsの状態をメソッドで取得します。メソッド内の変数の寿命は短く、呼び出した時だけの一時的なものです。なのでメソッドisPusshedOnをモニタするとlsはモニタ出来ますが、戻り値isPusshedOnは???になってモニタ出来ません。しかし、FBから別のFBに値を渡すだけなので変数の寿命は短くても問題ありませんね。
ぶっちゃけ1点入力機器だとこのつくりにするメリットは少ないですが、通信機器などのデータを配列や構造体でstaticに保持し、必要な情報だけを適切なメソッドで取りに行くのは効率的だと思います。
なお、LS2はLS1をコピペして名前を変えたら完成します。また、interfaceは複数実装できるので、警報接点を出力できるLSならそのinterfaceも実装すればよいだけです(ポリモーフィズム(多様性)などと呼ばれるやつです)。
次に、出力機器としてリレーも見てみましょうか。
INTERFACE Itf_Coil
METHOD isCoilOn : bool
METHOD setCoil : bool
VAR_INPUT
coil :bool;
END_VAR
/////////////////////////////////
FUNCTION_BLOCK RY1 IMPLEMENTS Itf_Coil
VAR_OUTPUT
DO1 :BOOL;
END_VAR
VAR_STAT
coil :BOOL;
END_VAR
DO1 := coil;
METHOD isCoilOn : BOOL
isCoilOn := coil;
METHOD setCoil : BOOL
VAR_INPUT
coil : BOOL;
END_VAR
THIS^.coil := coil;
リレーRY1は1点出力機器です。内部のstatic変数coilの状態をVAR_OUTPUTで出力します。メソッドはcoilの状態を見に行くisCoilOnと、coilの状態を変更するsetCoilの2つを用意しました。
最後の行について説明を。変数名はかぶらせて使用できます。同じ意味なのにかぶらないように別の変数名考えるの面倒なのでありがたいですね。その変数がどこにあるものかは、自身から近いところから探しに行きます。メソッド内のコードなら、自身のメソッド→自身のFBのインスタンス→親FBのインスタンス・・・といった具合に。setCoilメソッドは引数にcoilがあるので、このメソッド内ではcoilとだけ記載されていると、setCoilメソッド引数のcoilのことです。また、THIS^をつけると自身のFBのインスタンス内であることを明示します。したがって、最後の行のTHIS^.coilは、FB内部のstatic変数coilのことを指します。
isCoilOnメソッド内のcoilのように、THIS^はつけなくてもよいところではなくても動作します(上述のように全部につけておくのもありかも)。また、最後の行はRY1.coil:=coil;と記述しても動作は同じです。でもTHIS^じゃないとRY2のFBを作るときに修正箇所が多くなるので、THIS^の方が便利です。
ボタンやランプも同様に作成していきますが、省略します。画像だけ置いておきます。


デバイスは専用のPOUでグローバル変数と引数でやり取りします。
FUNCTION_BLOCK pou_Read_DI
VAR
LS1:LS1;
LS2:LS2;
LS3:LS3;
Ls4:LS4;
Ls5:LS5;
PB1:PB1;
PB2:PB2;
PB3:PB3;
PB4:PB4;
PB5:PB5;
SSW0 :SSW0;
SSW1 :SSW1;
END_VAR
LS1(DI1 := g_DI.in1_LS1);
LS2(DI1 := g_DI.in2_LS2);
LS3(DI1 := g_DI.in3_LS3);
LS4(DI1 := g_DI.in4_LS4);
LS5(DI1 := g_DI.in5_LS5);
PB1(DI1 := g_DI.in6_PB1);
PB2(DI1 := g_DI.in7_PB2);
PB3(DI1 := g_DI.in8_PB3);
PB4(DI1 := g_DI.in9_PB4);
PB5(DI1 := g_DI.in10_PB5);
SSW0(DI_1a := g_DI.in12_SS0);
SSW1(DI_1a := g_DI.in11_SS1);
FUNCTION_BLOCK pou_Write_DO
VAR
RY1 :RY1;
RY2 :RY2;
DPL1 :DPL1;
DPL2 :DPL2;
PL1 :PL1;
PL2 :PL2;
PL3 :PL3;
PL4 :PL4;
END_VAR
RY1(DO1 => g_DO.out21_RY1);
RY2(DO1 => g_DO.out20_RY2);
dpl1(
bit0 => g_DO.out26_DPL1_1,
bit1 => g_DO.out27_DPL1_2,
bit2 => g_DO.out28_DPL1_4,
bit3 => g_DO.out29_DPL1_8
);
dpl2(
bit0 => g_DO.out30_DPL2_1,
bit1 => g_DO.out31_DPL2_2,
bit2 => g_DO.out32_DPL2_4,
bit3 => g_DO.out33_DPL2_8
);
PL1(DO1 => g_DO.out22_PL1);
PL2(DO1 => g_DO.out23_PL2);
PL3(DO1 => g_DO.out24_PL3);
PL4(DO1 => g_DO.out25_PL4);
グローバル変数は物理アドレス(%IX0.0など)と紐づけます。こうしてグローバル変数を制御領域と切り分けます。そして制御領域ではメソッドで値のやり取りをします。
・・・実は変数にはprivate属性はないので、別領域から内部変数、例えばLS1.lsにアクセスすることはできます。codesysでは予測リストに登場しないが打ち込めば正常に動作するといった挙動になります。しかしながら、OOPのカプセル化の原則に反するので、FBの内部変数に直接アクセスするべきではありません。自分で書いておきながら、現場で急遽変更する必要が出たときにアクセスせざるを得なくなりそうに思います。。。
1.2 次に部品を集約してアセンブリを作ろう(composition、method)
デバイスができたら今度はアセンブリを作っていきます。まずはコンベアから。コンベアは2つのリレーと2つのリミットスイッチから構成されます。
FUNCTION_BLOCK Conveyor
VAR
LS_R :LS1;
LS_L :LS2;
RY_R :RY1;
RY_L :RY2;
END_VAR
ほかに必要な構成要素はないので、引数もFB内のコードもないです。ついでに名前もLS1からLS_Rに変更しています(別のLSに変更したときに修正しやすいようにです)。このようにFB(クラス)内に別のFBを内包するのを集約とか合成(composition)とか委譲とか、has-a関係とかそんな感じで呼ばれるようです(多分ちょっとずつ意味合いが異なる)。名前は置いておいてOOPでは重要な思想であり、こんな感じで上手いことFBを階層化させていきます。
次はメソッドを考えます。この装置に命令を出すなら、リミットスイッチを踏むまでコンベアを動かす生産運転、入力がONしている間コンベアを動かすメンテナンス運転、コンベアの停止、あたりでしょうか。また、リミットスイッチが押されているかどうか、コンベアが動いているかどうかも読み出せると使いやすそうです。今回は仕様上必要なメソッドだけ作ります。
METHOD isLimited_L : BOOL
isLimited_L := LS_L.isPusshedOn();
METHOD isLimited_R : BOOL
isLimited_R := LS_R.isPusshedOn();
METHOD moveInching_L : BOOL
VAR_INPUT
start: BOOL := FALSE;
END_VAR
RY_L.setCoil(start AND NOT RY_R.isCoilOn() );
moveInching_L := RY_L.isCoilOn();
METHOD moveInching_R : BOOL
VAR_INPUT
start: BOOL := FALSE;
END_VAR
METHOD moveToLS_L : BOOL
RY_L.setCoil(NOT RY_R.isCoilOn() AND NOT LS_L.isPusshedOn() );
moveToLS_L := RY_L.isCoilOn();
METHOD moveToLS_R : BOOL
RY_R.setCoil(NOT RY_L.isCoilOn() AND NOT LS_R.isPusshedOn() );
moveToLS_R := RY_R.isCoilOn();
METHOD stop : BOOL
RY_R.setCoil(FALSE);
RY_L.setCoil(FALSE);
stop := NOT RY_R.isCoilOn() AND NOT RY_L.isCoilOn();
内包しているFBのメソッド は"インスタンス名".”メソッド名”(”引数”)で実行できます。引数はない場合は()で問題ありません。ここで、FBの引数は(in:="変数1”,out=>"変数2")という具合に引数名と変数名を記述する必要がありますが、メソッドは、(”変数1”,"変数2")と省略した記述ができます。メソッドの方が小分けにした処理を想定しているからでしょうか?
例えば上記コードのRY_L.setCoil(start AND NOT RY_R.isCoilOn() );は、括弧の中をRY_LのsetCoilメソッドである引数coilに値渡しをしています。すなわち、入力のstat信号がONでかつ、RY_RのコイルがONしていないならばRY_LのコイルをONするという意味です。
start: BOOL := FALSE; はstartの初期値が0という意味です。おそらく不要ですが、このコンベアFBは別領域でどういう使い方をされるか知ることはできないので、自衛の初期化をしています。
同様にしてほかのアセンブリも作っていきます。
これはパレットのアセンブリ。
FUNCTION_BLOCK Pallet
VAR
LS_bit0 :LS3;
LS_bit1 :LS4;
LS_bit2 :LS5;
END_VAR
METHOD readNo : int
VAR
no :INT;
END_VAR
no.0 := LS_bit0.isPusshedOn();
no.1 := LS_bit1.isPusshedOn();
no.2 := LS_bit2.isPusshedOn();
readNo := no;
パレットは3つのLSで構成されており、パレット番号を読み出すことしかできません。
次にDPLですが、前回の記事ではミスがあり、DPLは1桁だと思っていたんですが問題文をよく読んだら2桁でした。前回の記事で説明したい内容に影響はないので、記事はそのままにしておきます。まずは7セグ表示器のデバイスDPL1、DPL2を用意します。
INTERFACE Itf_7Seg
METHOD displayedNo : BYTE
METHOD setNo
VAR_INPUT
no :BYTE;
END_VAR
/////////////////////////////////
FUNCTION_BLOCK DPL1 IMPLEMENTS Itf_7Seg
VAR_OUTPUT
bit0 : BOOL;
bit1 : BOOL;
bit2 : BOOL;
bit3 : BOOL;
END_VAR
VAR_STAT
dpl :byte;
END_VAR
bit0 := dpl.0;
bit1 := dpl.1;
bit2 := dpl.2;
bit3 := dpl.3;
METHOD displayedNo : BYTE
displayedNo := dpl;
METHOD setNo
VAR_INPUT
no : BYTE;
END_VAR
IF 0<= no AND no <= 9 THEN
dpl := no;
END_IF
DPL2のコードは省略します。値をセットすると3点の出力でそれを表現するデバイスです。このデバイス2つを合成してDPLを作成します。
FUNCTION_BLOCK DPL
VAR
dig_1 :DPL1;
dig_2 :DPL2;
END_VAR
VAR_TEMP
_no1:BYTE;
_no2:BYTE;
END_VAR
VAR_STAT
no :INT;
END_VAR
VAR_STAT RETAIN
record :ARRAY[0..2] OF INT;
END_VAR
_no1 := dig_1.displayedNo();
_no2 := dig_2.displayedNo();
no := BYTE_TO_INT(_no1) + BYTE_TO_INT(_no2) * INT#10;
まずはFB本体から。DPLは2桁の数字を表示する機器なので、noというstatic変数を用意しました。これはDPL1、DPL2からdisplayedNoメソッドで値を読みだして計算することで求めることができます。計算途中の変数はVAR_TEMPで一時変数にします。メソッドやFCなどと同じく、メモリは一時変数領域を使用するのでモニタ出来なくなります。まあ、計算途中ですからね。
また、DPLにはrecord機能を持たせようと思います。これは問題文にある以前のパレット番号を表示するためのものです。
それではDPLメソッドを下記のようにします。
METHOD readNo : INT
THIS^();
readNo := THIS^.no;
METHOD setNo
VAR_INPUT
no :INT;
END_VAR
VAR
_no1:BYTE;
_no2:BYTE;
END_VAR
//set number
_no1 := INT_TO_BYTE(no MOD INT#10);
_no2 := INT_TO_BYTE(no / INT#10);
dig_1.setNo(_no1);
dig_2.setNo(_no2);
METHOD readRecord : INT
VAR_INPUT
recordNum :INT;
END_VAR
IF recordNum >= 0 AND recordNum <= 2 THEN
readRecord := record[recordNum];
END_IF
METHOD setRecord : BOOL
//set prenumber
record[2] := record[1];
record[1] := record[0];
record[0] := THIS^.readNo();
1つ目のメソッドreadNo は現在値を読み出すものです。THIS^()は自身のFB本体を実行するという意味です。このようにメソッドからFB本体を実行することもできます。これでDPLの数字を更新してから戻り値に入れています。
2つ目のメソッドsetNoは現在値を変更するものです。ちょうどFB本体の逆のような処理を行います。なので変数名もFB本体も同じで作りましたが、上述したように名前がかぶっていても問題ありません。コピペしやすくて助かりますね。
3つ目のメソッドreadRecord はレコードの読み出しです。問題文的には前回値がわかればよいのですが、とりあえずで3点記録できるようにしているので、何番目のレコードを読み出すか入力を必要としています。
4つ目のメソッドsetRecord はレコードの書き込みです。配列を後ろにずらしながらTHIS^.readNo()で現在値をレコードの最前列に格納しています。このようにメソッド内で別のメソッドを実行することもできます。重複コードは極力減らすことで保守性を高めます。実行時は同様の処理をPOU内の色んな所で行うので処理的には無駄が多そうですが、令和のPLCならやってくれるでしょう。
最後にSSWも単体で合成(?)しておきましょう。
FUNCTION_BLOCK ModeSSW
VAR
ssw :SSW0;
END_VAR
METHOD isAutoOn : BOOL
isAutoOn := ssw.isRightSelectedOn();
METHOD isManualOn : BOOL
isManualOn := ssw.isLeftSelectedOn();
私が左右盲だからかもしれませんが、SSWの右がONした、ではわかりにくかったので。こんな感じでメソッドや出力、データ型などが気に入らなかったら気軽にFBに包んで、表層を変えて中身そのものは変えないように作っていきます。
2 ライブラリの作成
2.1 使いまわす処理をFBでまとめよう(return、var_inst)
前回の記事で作ったFB・FCのうち、汎用的に使用できそうなものは四捨五入回路とオルタネイト回路でしょうか。このくらいcodesysのライブラリにあるだろうというのは置いておいて、こうしたライブラリ的なコードもメソッドでまとめておくことには何点かメリットがあります。
1つは単純にライブラリ的関数が散在しているよりも使いやすい点。例えば三角関数などの数学的処理をする一つにFBにまとめたりです。FB名に数字を振ったり、フォルダの階層分けを深くするよりは、一つのFBにまとめる方が楽だと思います。2つ目に重複コードを避けられる点。例えばA→Bという処理とA→Cという処理がある場合、メソッドB、CでメソッドAを呼び出せばコードの重複部がなくなります。3つ目は共通の定数の定義を1か所にまとめることができる点。例えば円周率や単位変換の係数などです。
それではmyLibraryというFBを作ります。
FUNCTION_BLOCK myLibrary
今回は共通定数などはないので、FB本体には何も置きません。
METHOD alternate : BOOL
VAR_INPUT
il :BOOL;
in :BOOL;
END_VAR
VAR_IN_OUT
out :BOOL;
END_VAR
VAR_INST
pls :R_TRIG;
END_VAR
// inter lock
IF NOT il THEN
RETURN;
END_IF
//alternate
pls(CLK := in);
IF pls.Q THEN
IF out THEN
out := FALSE;
ELSE
out := TRUE;
END_IF
END_IF
これはオルタネイト回路の例です。まず、メソッドはテンポラリに実行されるので、通常は前のスキャンの状態を保持することはできません。そのため出力outは戻り値ではなく、VAR_IN_OUTを使用してメソッド外の変数の状態を内部で見るようにしています。また、前のスキャンの状態を保持することができないので、国際標準FBであるR_TRIG、TON、SRなどはそのままでは使用できません。しかしながら、VAR_INSTを使用すれば、メソッド内でもその変数だけ寿命が長くなり国際標準FBを使用できます。ここはFCと大きく異なる点です。
しかしながら、VAR_INSTは多用すべきではないと思います。今回のように単体テストが可能な単純なライブラリのみに使用するなど、ルールを決めた方が良いと思います。私も最初はこの後出てくるコンベア動作のタイマーをメソッドのVAR_INSTにしようしたのですが、モニタ出来ないので使用感は最悪でした。
またインターロックilがONしていないときはRETURNで処理をせずにメソッドを終了させています。STは気軽にジャンプできるのでMESなどイベント処理が増えている昨今のPLCでは、LDよりサイクルタイムは短くなりそうです。LDはジャンプNGですからね。LDが見やすいのはジャンプや割り込みせずに、全てのコードが同じ周期でサイクル制御しているという前提があります。どうしても割り込みや別周期が必要なら値渡し用のアドレスを別途用意しないとモニタしづらいです。
余談ですが、よくLDで常時OFFのa接点を用いて昔の回路をコメントアウトのように残しているプログラムを見ますが、LDは条件がOFFの時もコイルの変数にfalseを格納する必要があるので、普通にサイクルタイムに影響を与えています。
さらに余談ですが、じゃあLDだけだと大型プロジェクトは高速処理できないの?と思う高級言語畑も人も見ているかもしれませんが、その場合はPLCのCPUを複数用意して高速処理を分けたりします。ちな保守性は悪いです。AIだのDXだの、令和の時代にフルLDだとなかなか厳しい性能の装置になるんですが、なぜか国内だと客先からLDのみの指定があったりするんですよね。大丈夫なんでしょかね・・
METHOD roundUp : INT
VAR_INPUT
in:REAL;
END_VAR
roundUp := REAL_TO_INT(in + 0.5);
四捨五入回路は簡単などで特に言うことは無いです。
METHOD blink : BOOL
VAR_INPUT
onTime :TIME;
offTime :TIME;
END_VAR
VAR_INST
onTP :TP;
offTP :TP;
END_VAR
onTP(IN := NOT offTP.Q, PT := onTime);
offTP(IN := NOT onTP.Q, PT := offTime);
blink := onTP.Q;
点滅回路もついでにメソッドにしてみました。国際標準FBであるTP(タイムパルス)で実装できます。これもVAR_INSTですね。
3 ステップ制御の作成
3.1 全体のレイアウトを考える
ようやくシーケンス制御部分に入ります。ここが一番苦労しました。ただの順次制御なんですが、サイクル制御であるPLCでSTを使用すると無駄に難しいです。ここは無理にSTを使用せずに、SFCやFBDを用いるのが道具の上手な使い方な気がします。
さて、静的オブジェクトは簡単に作ることができますが、ステップ制御のオブジェクトって何なんでしょうね。オブジェクトは生き物とか言われますが、私のイメージでは現場での生産担当者さんです。
生産担当者は生産のプロであり、生産条件が整っていることを確認してから、生産を開始します。例えばコンベアを右に動かしたり。しかし、あくまで生産のプロなので、コンベアがサーボで動いているのか、インバータで動いているのかなどは把握していません。あくまでコンベアに動け、と命令するだけです。また、全部の機器を一人で制御しようとすると、その機器の特徴をすべて覚える必要があり、生産担当者はスーパーマンでないと務まらなくなります。なので、各ステップに担当者を置いて生産担当者は各ステップ担当者に指示を出すだけにします。
3.2 各ステップ制御のひな型を作ろう(abstract)
INTERFACE ITF_stepBox
METHOD isEndOn : bool
METHOD start : bool
////////////////////
FUNCTION_BLOCK ABSTRACT stepBox implements ITF_stepBox
VAR
_start :BOOL := FALSE;
_isExeOn :BOOL := FALSE;
_isEndOn :BOOL := FALSE;
_SR :SR;
_exePls :R_TRIG;
END_VAR
//ステップ制御の親FBとして機能する。
//子FBでは"_isExeOn"中か、"_exePLS.Q"時に処理を行い、終了時に_isEndOnを返す。
// startメソッドの起動時に動作する
_exePls(CLK := _start);
IF _exePls.Q THEN
_isEndOn := FALSE;
END_IF
//起動中の変数_isExeOnのフラグは内部で保持してはいけない
_isExeON := _start AND NOT _isEndOn;
METHOD start : BOOL
VAR_INPUT
trig :BOOL;
END_VAR
THIS^._start := trig;
METHOD isEndOn : bool
//end step
isEndOn := THIS^._isEndOn;
ステップ制御のひな型になるstepboxを作ります。このFBにはABSTRACT(抽象)という属性がついており、単体ではFB宣言できなくなっています。継承して使用する予定なので、単体使用を封じて変な使い方をされないようにしています。
コードの中身を説明すると、生産担当者によってstartメソッドのトリガが立つと、内部で_isExeONが起動します。また、_isEndOnが起動するとisEndOnメソッドを生産担当者に返すというものです。_startは内部でOFFにする処理がありません。startメソッドは常時実行してもらい、トリガのON/OFFで処理を行います。あまり直感的でないので微妙と思っていますが、ステップ制御がパイプライン的に動作することも想定してプログラムするとこうなりました。。。
このFBをベースに各ステップを作っていきます。
3.3 ひな型をベースに各ステップを作ろう(extends、super、private)
問題文の自動モードを順番にみていきます。まずはコンベアを左に動かしてパレットを移動させましょう。
FUNCTION_BLOCK STEP_moveToLS_L EXTENDS stepBox implements ITF_stepBox
VAR
conv :conveyor;
END_VAR
SUPER^();
//sequence step
IF SUPER^._isExeOn THEN
conv.moveToLS_L();
END_IF
SUPER^._isEndOn := conv.isLimited_L() ;
METHOD start : BOOL
VAR_INPUT
trig : BOOL;
END_VAR
SUPER^.start(trig);
METHOD isEndOn : BOOL
isEndOn := SUPER^.isEndOn();
EXTENDS stepBoxでstepBoxを継承させています。stepBoxが親FBでSTEP_moveToLS_Lが子FBになります。子FBでは親FBの変数やメソッドが使用できます。名前がかぶっていなければそのままでも使用できますが、わかりにくいので全部SUPER^を付けています。
まずSUPER^()で親FBの中身を実行しています。さらに、親FBと同じインターフェイスを実装し、メソッドもSUPER^.”メソッド名”()で親メソッドを実行しに行きます。オーバーライドというやつですかね。これで共通処理を使いまわせますし、とあるステップだけ少し処理が異なるならば、そのステップの子FBでのみ修正することができます。今回は継承メソッドの中身は全て同じなので今後は省略します。
中身については今までたくさん準備をしてきたので簡単ですね。実行フラグが立ったらコンベアを左に動かして、左LSがONしたら終了させます。
FUNCTION_BLOCK STEP_scanPalletNo EXTENDS stepBox implements ITF_stepBox
VAR
plt :Pallet;
dpl :DPL;
_ton : TON;
END_VAR
VAR_TEMP
_no :INT;
END_VAR
SUPER^();
//initial step
IF SUPER^._exePls.Q THEN
_no := plt.readNo();
dpl.setNo(_no);
dpl.setRecord(); //renewal preset
END_IF
//sequence step
_ton(IN:= SUPER^._isExeOn, PT:= T#2S, Q=> SUPER^._isEndOn);
次にパレット番号を読み出してDPLに表示するステップです。1スキャン処理で良いので、親FBの_exePls.Qが実行したときに処理しています。また、DPLのレコードもこのタイミングで更新しておきます。
タイマーで2秒待ってから終了させます。
FUNCTION_BLOCK STEP_judgePalletNo EXTENDS stepBox implements ITF_stepBox
VAR
plt :Pallet;
dpl :DPL;
_ton : TON;
_type :(ODD,EVEN,ZERO);//for judge method
END_VAR
SUPER^();
//initial step
IF SUPER^._exePls.Q THEN
dpl.setNo(THIS^.judge());
END_IF
//sequence step
_ton(IN:= SUPER^._isExeOn, PT:= T#10MS, Q=> SUPER^._isEndOn);
お次はパレット番号によってDPLの表示を変えるのですが、長いし1スキャン処理なのでこのFB内のメソッドjudgeに処理を移しています。それをこのFB本体でTHIS^.judge()により実行しています。
また、_type :(ODD,EVEN,ZERO)はcodesysの機能の暗黙の列挙体の例です。FBやPOU本体のvarで使用でき、実態はINT型なんですが、ローカル内では列挙体のように使用できます。これを次のメソッドで使用しています。
METHOD PRIVATE judge : INT
VAR
_no :INT;
_judgedNo :INT;
lib :myLibrary;
END_VAR
//Branch process with the read value
//read value
_no := dpl.readNo();
// judge value
IF (_no MOD 2) = 1 THEN
_type := ODD;
ELSE
_type := EVEN;
END_IF;
IF _no = 0 THEN
_type := ZERO;
END_IF;
//process
CASE _type OF
ODD:
_judgedNo := lib.roundUp(INT_TO_REAL(_no)/2);
EVEN:
_judgedNo := _no * 3;
ZERO:
_judgedNo := dpl.readRecord(1) * 3;
END_CASE
//output
judge := _judgedNo;
やっていること自体は前回の記事と変わらないです。四捨五入をライブラリにしたこと、前回のパレット値をDPLのレコード機能にしたこと、列挙体の使用が異なる点でしょうか。
前回のパレット値について。DPLはパレット番号を読み出すたびにレコードを更新するので、配列[0~2]から前回のパレット番号を読み出すためにreadRecordの引数に1を入れています。
FUNCTION_BLOCK STEP_end EXTENDS stepBox implements ITF_stepBox
VAR
dpl :DPL;
END_VAR
SUPER^();
//sequence step
IF SUPER^._isExeOn THEN
dpl.setNo(INT#00);
SUPER^._isEndOn := TRUE;
END_IF
コンベアを右に動かすステップは左をコピペして少し修正するだけなので省略。自動モードの最後はDPLの表示を0に戻す終了ステップです。
このまま手動モードのステップも作りましょうか。
FUNCTION_BLOCK STEP_blink EXTENDS stepBox implements ITF_stepBox
VAR
lib :myLibrary;
PL :PL2;
_ton :TON;
END_VAR
SUPER^();
//sequence step
IF SUPER^._isExeOn THEN
PL.setLump(lib.blink(T#0.5S,T#0.5S));
END_IF
_ton(IN:= SUPER^._isExeOn , PT:= T#3S, Q=> SUPER^._isEndOn);
まずはランプを0.5秒ON、0.5秒OFFで3秒間点滅させます。
FUNCTION_BLOCK STEP_moveInching_L EXTENDS stepBox implements ITF_stepBox
VAR
conv :conveyor;
PL :PL2;
END_VAR
SUPER^();
//sequence step
IF SUPER^._isExeOn THEN
conv.moveInching_L(TRUE);
PL.setLump(TRUE);
END_IF
//SUPER^._isEndOn := conv.isLimited_L() ;
その後、ステップが移行するまでコンベアを左(又は右)に動かしランプを点灯させ続けます。
これでステップは完成したので制御回路を作ります。
3.4 自動・手動モードを作ろう(struct)
私はこのあたりから厳格なOOPの適用をあきらめています。何か良いアイデアがある人は情報ください。
さて、制御領域は現場担当者をイメージしています。レシピの設定やアラーム、インターロックなどはより上位の階層から降ってくる情報で、担当者はただそれに従うだけです。シーケンス制御をする前提条件として、インターロックが必要になります。開始時に必要な条件と、起動中ずっと成り立つ必要がある条件と2つがあり、これが上位からやってくる必要があるので、まずは構造体(struct)を作ります。
TYPE st_Interlock :
STRUCT
start :BOOL;
run :BOOL;
END_STRUCT
END_TYPE
2つのビットをまとめただけですね。PLCにおける構造体の注意点は2つあります。
1つ目は確定しているデータをまとめること。構造体のデータを変更する場合、基本的にPLCは再起動が必要です。PLCはスマホアプリのように簡単に再起動できません。モーターなどの静的な機器をすべて初期条件に戻す必要があるので再起動に時間がかかります。また、排水や排気が別の生産装置とつながっている場合、装置を止めると他の装置の生産条件が変わってしまうので、そもそも装置を止められない時期などもあります。それらを踏まえて、大きな改造をするとき以外は構造体の構成を変えなくてよいようにしておきたいです。例えば上記のインターロックの構造体はまあ変わらないと思います。それ以外だと機器の通信データのまとまりや、HMIのオブジェクト(ボタン、数値入力)のデータのまとまりなど、作成後にやっぱりこのデータも追加しようとならないものが良いです。
2つ目に大きな構造体を作らないことです。極論グローバル変数をきれいに整理したら一つの巨大な構造体を作ることができますが、当然メリットはないです。構造体を参照渡し(var_in_out)でFBに入れる技法もありますが、構造体が巨大だと可読性が悪くなります。まとめると、できるだけデータのまとまりが小さいほうが使いやすいのが原則なので、まとめた方がメリットがある場合に小さく構造体を使おうということですね。
閑話休題。インターロック構造体を引数で制御回路に入れます。インターロックは制御回路の命なので、メソッドではなく引数で入れます(?)。
FUNCTION_BLOCK Sequence_AutoMode
VAR_INPUT
EXE :BOOL;
il :st_Interlock;
END_VAR
VAR
_step: (INITIAL,START,MOVE_L,SCAN,JUDGE,MOVE_R,END):= INITIAL;
conveyor :conveyor;
dpl :DPL;
startPB :PB2;
CycleOnPL :PL2;
STEP_move_L :STEP_moveToLS_L;
STEP_move_R :STEP_moveToLS_R;
STEP_scan :STEP_scanPalletNo;
STEP_judge :STEP_judgePalletNo;
STEP_end :STEP_end;
END_VAR
//initial step
IF NOT EXE THEN
_step := INITIAL;
RETURN;
END_IF
//run interlock
IF NOT il.run THEN
_step := INITIAL;
END_IF
// start step
IF il.start AND il.run AND startPB.isPusshedOn() THEN
_step := START;
END_IF
//sequence step
CASE _step OF
INITIAL:
conveyor.stop();
dpl.setNo(INT#00);
START:
THIS^.next(TRUE,MOVE_L);
MOVE_L:
THIS^.next( STEP_move_L.isEndOn(), SCAN);
SCAN:
THIS^.next( STEP_scan.isEndOn(), JUDGE);
JUDGE:
THIS^.next( STEP_judge.isEndOn(), MOVE_R);
MOVE_R:
THIS^.next( STEP_move_R.isEndOn(), END);
END:
THIS^.next( STEP_end.isEndOn(), INITIAL);
ELSE
_step := INITIAL;
END_CASE
//Step instances
STEP_move_L();
STEP_move_L.start(_step = MOVE_L);
STEP_move_R();
STEP_move_R.start(_step = MOVE_R);
STEP_scan();
STEP_scan.start(_step = SCAN);
STEP_judge();
STEP_judge.start(_step = JUDGE);
STEP_end();
STEP_end.start(_step = END);
//output
CycleOnPL.setLump(THIS^.isCycleOn());
このコードは暗黙の列挙体_stepが肝です。_stepの値によって内部の制御がすべて決まるようにしています。
//initial stepで実行条件を満たしていない場合はステップを初期化してreturnすることで処理を終了させます。
//run interlock、// start stepでインターロックの処理をしながらステップを開始させます。
//sequence stepではCASE文を使用してステップを管理します。次のステップへの移行は繰り返し処理なのでnextメソッドにまとめました。
//Step instancesで各stepのインスタンスを置き、開始メソッドを実行します。どうしても開始処理は立上がりや立下りが見れないと困るので、casae文の外に開始メソッドを置いています。
//outputは制御に絡まないランプの出力です。サイクル実行時にONするのでメソッドで記述してます。
このFBのメソッドはこんな感じ。
METHOD isCycleOn : BOOL
isCycleOn := ( _step >= START);
METHOD PRIVATE next : BOOL
VAR_INPUT
trig :BOOL;
nextStep :INT;
END_VAR
IF trig THEN
THIS^._step := nextStep;
END_IF
METHOD readNowStep : INT
readNowStep := THIS^._step;
next メソッドはこのFBでしか使わないのでPRIVATE を付けています。また、最後のreadNowStepは上位で現在のステップを確認する用途です。
次は手動モード。
FUNCTION_BLOCK Sequence_ManuMode
VAR_INPUT
EXE :BOOL;
il :st_Interlock;
END_VAR
VAR
_step:(INITIAL,START,BLINK,MOVE_L,MOVE_R):= INITIAL;
_isLeftModeOn :BOOL;
_exeOnPls :R_TRIG;
_exeOffPls :R_TRIG;
conveyor :conveyor;
changePB :PB1;
startPB :PB2;
leftModeOnPL : PL1;
moveOnPl :PL2;
lib :myLibrary;
STEP_move_L :STEP_moveInching_L;
STEP_move_R :STEP_moveInching_R;
STEP_blink :STEP_blink;
END_VAR
//initial step
_exeOnPls(CLK := EXE);
_exeOffPls(CLK := NOT EXE);
IF (* _exeOnPls.Q OR *) _exeOffPls.Q THEN
leftModeOnPL.setLump(FALSE);
(* _isLeftModeOn := FALSE; *)
END_IF
IF NOT EXE THEN
_step := INITIAL;
RETURN;
END_IF
//alternate direction
THIS^.changeDirection();
//alarm process
IF NOT il.run THEN
_step := INITIAL;
END_IF
// start step
IF il.run AND il.start AND startPB.isPusshedOn() THEN
_step := START;
END_IF
IF NOT startPB.isPusshedOn() THEN
_step := INITIAL;
END_IF
//sequence step
CASE _step OF
INITIAL:
conveyor.stop();
moveOnPl.setLump(FALSE);
START:
THIS^.next(TRUE,BLINK);
BLINK:
IF _isLeftModeOn THEN //branch step
THIS^.next( STEP_blink.isEndOn(), MOVE_L);
ELSE
THIS^.next( STEP_blink.isEndOn(), MOVE_R);
END_IF
MOVE_L:
//no next
MOVE_R:
//no next
ELSE
_step := INITIAL;
END_CASE
//Step instances
STEP_move_L();
STEP_move_L.start(_step = MOVE_L);
STEP_move_R();
STEP_move_R.start(_step = MOVE_R);
STEP_blink();
STEP_blink.start(_step = BLINK);
自動モードとかなり作りが似ています。ただ継承とかはしていません。現場で変更とかしそうだし・・・
//initial stepでは自動モードと同じreturn処理以外にも、手動モード終了時のPL1のoff処理をしています。問題文からは不明である、手動モード開始時のPL1のoffが必要な場合は(* *)でコメントアウトした回路を有効にするイメージです。
//alternate directionでオルタネイト回路を実行します。変数はFB本体に置きつつ、メソッドで処理を避けました。
その後の処理の考え方は自動モードと同じです。ステップ開始後は点滅ステップに移行し、終了時にコンベアを左右どちらに動かすか分岐します。
METHOD PRIVATE changeDirection : BOOL
lib.alternate(NOT startPB.isPusshedOn(),changePB.isPusshedOn(),_isLeftModeOn);
leftModeOnPL.setLump(_isLeftModeOn);
METHOD PRIVATE next : BOOL
VAR_INPUT
trig :BOOL;
nextStep :INT;
END_VAR
IF trig THEN
THIS^._step := nextStep;
END_IF
METHOD readNowStep : INT
readNowStep := THIS^._step;
手動モードのメソッドはこちらです。下2つは自動モードと同じなので、やはり継承させても良いのかも?
changeDirectionメソッドは、PB2が押されていないときにPB1を押すと_isLeftModeOnのON/OFFが切り替わります。PL1の出力はFB本体においた方が良いかもしれません。
4 全体回路の作成
4.1 グローバル変数を整理しよう(var_global、constant,persistent)
グローバル変数は少ないほうが良いとされていますが、PLCではゼロにはできないんじゃないかと思います。特に物理アドレスのメモリが決まっている変数を用意しておかないと、コンパイル時にメモリが置き換わることで変数が初期値に戻り、それで重要なパラメータが飛んで装置が暴走したら人命に関わることもあります。全部の変数に適切な初期値を入れておけよという話かもしれませんが、運転中の状態を保持しておかないと困る(初期化では対応できない)変数もあると思います。
今回の装置では特に重要なパラメータはないので、下記はグローバル変数リストのイメージです。
{attribute 'qualified_only'}
//constant
VAR_GLOBAL CONSTANT
LENGTH_m : REAL:= 2.3; //no use (spare)
TACT_TIME_s :TIME := T#30S; //no use (spare)
MAX_SPEED_m_s :REAL := 0.7; //no use (spare)
END_VAR
// parameter
VAR_GLOBAL PERSISTENT
PID_P :ARRAY[0..3] OF REAL; //no use (spare)
PID_I :ARRAY[0..3] OF REAL; //no use (spare)
PID_D :ARRAY[0..3] OF REAL := [0,0,50]; //no use (spare) (need initialize)
END_VAR
こんな感じで装置の仕様、生産条件の仕様、装置の重要な調整パラメータはグローバル変数に置いておく必要があるのではと思っています。
constantは定数の定義です。装置のスペック上の最大値など、計算や上下限で使われるイメージです。PERSISTENTはcodesysにおいて、物理メモリのアドレスを指定する方法です。一番変数の寿命が長く、国産PLCのファイルレジスタとかに当たるものだと思っています。逆にPERSISTENTがついていない変数は、コンパイル時にアドレスの引っ越しが必要な場合、retainだろうがデータが初期値に戻ります。それで装置が暴走したり、全く動かなくなることがないように設計したいです。
現場での調整箇所以外にグローバル変数にした方良いものとして、よくモニタされる変数があると思います。グローバル変数は検索が容易なので、フィールドエンジニアやお客様の保守が最低限追う必要があるところまでは必要悪的に存在すべきかなと。皆さんはどう思いますか?
具体的にはアラームとインターロックはグローバル変数の方が使い勝手が良いと思います。ここが確認できないと、アラーム発生時やだんまり停止時に現場の方だけで解決できなくなってしまいます。めちゃくちゃ親切なHMIがPLCの中で何が起きていてどうすればよいのか全て教えてくれれば良いのですが、一品一葉かつリリース後にどんどん改良していくFA装置では難しいように感じます。
{attribute 'qualified_only'}
VAR_GLOBAL
isDetected:ARRAY[0..MAX_ID] OF BOOL;
isOn:ARRAY[0..MAX_ID] OF BOOL;
isOneOn :BOOL;
END_VAR
VAR_GLOBAL CONSTANT
MAX_ID :INT:=99;
END_VAR
上がアラーム用のグローバル変数リストg_Alarmです。ここでアラームを検知するisDetectedと、アラームが発生していることを表すisOnという変数を配列で宣言しています。一番の上の{attribute 'qualified_only'}がついていると、POU内でg_Alarm.isOnのように構造体みたいに使うことができるcodesysの機能です。作成時にデフォルトでついている文言で、削除することもできます。また、codesysでは配列の大きさをconstantで宣言できます。上記の場合、0~99の100個のデータの配列になります。
{attribute 'qualified_only'}
VAR_GLOBAL
autoMode :st_Interlock;
manuMode :st_Interlock;
END_VAR
こちらはインターロック用のグローバル変数リストg_ILです。装置がなんか動かない場合、すぐにインターロックを組んでいる箇所を見つけるための変数です。
4.1 自動モードと手動モードを切り替えよう
グローバル変数の準備ができたので、手動・自動を切り替えるFBを作ります。自動モードと手動モードはSSW0によって切り替わります。今回のコンベアシステム全体を表す言葉としてFB名をsystem1という名前にしていますが、正直わかりづらいと思いました。一般的にエリアでシーケンス領域が分かれると思うのでarea_@@みたいな感じが良いしょうか。
FUNCTION_BLOCK PUBLIC pou_System1
VAR
manual :Sequence_ManuMode;
auto : Sequence_AutoMode;
conveyor :conveyor;
eStop : PB5;
modeSSW : modeSSW;
END_VAR
//manual mode IL
g_IL.manuMode.start := manual.readNowStep() = 0;
g_IL.manuMode.run := NOT g_alarm.isOn[0]; //system1 E-stop
//auto mode IL
g_IL.autoMode.start := auto.readNowStep() = 0 AND
conveyor.isLimited_R();
g_IL.autoMode.run := NOT g_alarm.isOn[0]; //system1 E-stop
//alarm
g_Alarm.isDetected[0] := eStop.isPusshedOn()
OR auto.isCycleOn() AND modeSSW.isManualOn();
//Sequence
manual(
EXE := modeSSW.isManualOn(),
il := g_IL.manuMode
);
auto(
EXE := modeSSW.isAutoOn(),
il := g_IL.manuMode
);
このFBでは手動・自動のインターロック回路とアラーム回路を実行後にそれぞれのシーケンスFBを起動させています。例えば自動モードが動作しない場合、g_IL.autoModeで検索をかけるとここが見つかり、ステップが0(初期状態)でコンベアの右リミットが押されている状態でないと開始できず、アラーム番号0が発生していると実行されないことがわかります。また、アラーム番号0は、PB5が押された時か、自動モードサイクル実行中にSSWが手動になったときに発生することがわかります。
これらの回路は実際のシーケンス領域から分け、メソッドを多用するOOP回路を現場で階層の奥深くまで探させないようにした方が良いのかなと思い、こういった作りにしてみました。
後はアラーム回路ですが、ブザー回路や重故障・軽故障なども考慮した無駄に凝った作りなので説明は省略します。興味ある人は解読してみてください。ポイントはアラーム発生時にそのアラームIDからアラーム発生個所にグローバル変数の検索で飛べるようにしていることくらいです。
FUNCTION_BLOCK pou_Alarm
VAR
_timer :TON;
Alarm :AlarmSet;
resetPB :PB4;
alarmLump :PL4;
END_VAR
VAR CONSTANT
LIGHT :INT:=0;
HEAVY :INT :=1;
END_VAR
//Alarm reset
Alarm.reset(resetPB.isPusshedOn());
//Alarms
_timer(IN := TRUE, PT :=T#5S); //delay detection
IF _timer.Q THEN
g_Alarm.isOn[0] := Alarm.detect(0, g_Alarm.isDetected[0], HEAVY); //System1 E-Stop
//g_Alarm.isOn[1] := Alarm.detect(1, g_Alarm.isDetected[1], LIGHT); //Spare
//.......
END_IF
//instance
Alarm(isOneOn => g_Alarm.isOneOn);
//Output
alarmLump.setLump(g_Alarm.isOneOn);
/////////////////////////////
FUNCTION_BLOCK AlarmSet
VAR_OUTPUT
isOneOn :BOOL;
isLightON :BOOL;
isHeavyOn :BOOL;
END_VAR
VAR
_alarmHold :ARRAY[0..MAX_ID] OF SR;
_alarmProperty :ARRAY[0..MAX_ID] OF INT;
_buzzerHold :RS;
_isNewAlarmOn :BOOL;
_alarmCount :INT:= 0;
_alarmPreCount :INT;
END_VAR
VAR CONSTANT
MAX_ID :INT:=99;
LIGHT :INT:=0;
HEAVY :INT :=1;
END_VAR
VAR_TEMP
_i :INT;
END_VAR
//alarmHold instances
FOR _i := 0 TO MAX_ID BY 1 DO
_alarmHold[_i]();
END_FOR
// alarm check
isOneOn := FALSE;
isLightON := FALSE;
isHeavyOn := FALSE;
_alarmPreCount := _alarmCount;
_alarmCount := 0;
FOR _i := 0 TO MAX_ID BY 1 DO
IF _alarmHold[_i].Q1 THEN
isOneOn := TRUE;
_alarmCount := _alarmCount +1;
CASE _alarmProperty[_i] OF
LIGHT: isLightON := TRUE;
HEAVY: isHeavyOn := TRUE;
END_CASE
END_IF
END_FOR
_isNewAlarmOn := (_alarmCount > _alarmPreCount);
//buzzer
_buzzerHold(
SET := _isNewAlarmOn,
Reset1 := THIS^.buzzerStop
);
METHOD buzzerStop : BOOL
VAR_INPUT
stop :BOOL;
END_VAR
buzzerStop := stop;
METHOD detect : BOOL
VAR_INPUT
id :INT;
in :BOOL;
prop :INT;
END_VAR
IF id > MAX_ID THEN //no process
RETURN;
END_IF
// set alarm
_alarmHold[id].SET1 := in;
//set property
_alarmProperty[id] := prop;
//output
detect := _alarmHold[id].Q1;
METHOD isBuzzerOn : BOOL
isBuzzerOn := THIS^._buzzerHold.Q1;
METHOD reset : BOOL
VAR_INPUT
pb :bool;
END_VAR
VAR
_i :INT;
END_VAR
//本装置はブザー停止とブザーがない。ブザー鳴動中はリセットしないという回路をコメントアウト
// IF _buzzerHold.Q1 THEN
// RETURN;
// END_IF
FOR _i :=0 TO MAX_ID BY 1 DO
_alarmHold[_i].RESET := pb;
END_FOR
4.2 プログラムを完成させよう
最後にprogramのmainを用意して完成です。最後に持ってきましたが、実際はmaiにFBを個別で置きながら単体テストをしていく必要があります。
PROGRAM main
VAR
pou_Alarm:pou_Alarm;
pou_readDI :pou_Read_DI;
pou_writeDO :pou_Write_DO;
pou_system1 :pou_System1;
pou_HMI :pou_HMI;
END_VAR
// Input
IF NOT g_debug.Check_IO THEN
pou_readDI();
END_IF
// CPU_System
//////////// main POU ////////
// alarm
pou_Alarm();
// system1
pou_system1();
//////////// main POU ////////
// Output
IF NOT g_debug.Check_IO THEN
pou_writeDO();
END_IF
//HMI
pou_HMI();
各pouを実行するだけの回路です。pou_HMIはデバグ用なので無視してください。また、pou_readDIとpou_writeDOはIOチェック時に実行しない作りにしておくと、強制On/Offさせたり、便利だと思います。
5 その他の考察
5.1 UML図について
やっつけのクラス図を置いておきます。全体の設計思想がぼんやり分かる程度のことしか書いていません。

あんまり詳細に記述すると絶対に現地で変更されると思うんですがどうなんでしょうね。PLCの場合、FB間のつながりをどうしてもわかりやすくしておきたいなら、そもそもこんなお絵かきしてないでFBDでプログラム作れば?となりそうです。
5.2 HMI、上位制御の設計について
今回は無かったんですが、上位の制御システムはどう協調させればよいのでしょうか。静的オブジェクトを制御する領域と同じところに記述すると保守性が悪そうなので、各pouを継承させてそっちで信号をメソッドを通してシーケンスFBとやり取りさせるのが無難かなと思っています。
しかしながら、上位システムがたくさんあったりすると、とんでもないインスタンス量やメソッド数になりそうに思います。良い方法を知っている方は是非教えてください。
また、SiemensPLCにはフェイスプレートというHMIのオブジェクトのグループに対して構造体を割り当てる機能があります。codesysのHMIはかなりSiemensに似ているのであるのかなと思ったり。このタイプのHMIって国産の”タッチパネル”設計と同じようにできないで、何か良い設計方法があるのか調べてみないと本格的なHMI設計はできませんね。私も詳しくないので、今後の課題です。個人的にはボタンやIOフィールドなどのHMIオブジェクトに対応するインターフェイスやFBがライブラリで存在していればうれしいのですが。
5.3 保守性について
ハード構成とソフトアプリケーションがきれいに分かれている点では保守性が良いと思います。この業界で”前と同じ”と言われてPLCソフトが全く同じで良いなとなる人はいません。電気機器なんて10年もあれば生産中止になって新しくなるので、同じソフトアプリケーションを作るのに結構コードの修正が必要になります。今回のプロジェクトはアプリケーション側からみてメソッドの挙動が同じであれば、ハードの中身はどうであろうが良いので、修正箇所がわかりやすいかなと感じました。
また、ソフトアプリケーションにポリモーフィズムがあるのも保守性が高くなりそうかなと思います。存在しない幻の生き物として有名な”標準装置”ですが、存在しないならあきらめてABSTRACTで作ってお客様ごとに継承させればよいですからね。
一方、現場での保守性はやはり気になります。この業界で単に保守性と言えば、お客様のメンテナンス部隊がメンテナンスしやすいかという意味合いで使われることが多いです。これはどうなんでしょう・・・?国際標準で何パターンかPLC用のデザインパターンを作ってくれれば、EPLANライクの図面が国際標準で見やすいように、各社が同じデザインパターンでPLCプログラムを作れば見やすいんじゃないかと思います。逆に各社が皆自分勝手なOOPを進めていくと、今のアドレスLD以上に見づらくなる懸念があります。
5.4 分業について
従来のPLCプログラムはめちゃくちゃ分業がしにくかったです。そのせいで若手はよくわからないまま現場に向かわされて、炎上して、この業界を去る。そんなこんなでPLCエンジニアは人手不足だったりします。特に現場を生き残った強い生物ではあるが、LD以外できないベテランエンジニアが上手く技術を継承できていないように思います。
今回の装置の作りはハード、制御、上位でレイヤが分かれており、単体テストが可能なので分業はしやすいと思います。若手にハード側を作ってもらって、ベテランがそれをチェックしながら客先仕様に合わせた制御を作成するみたいな。しかし全体的な学習コストはLDのアドレスプログラミングよりも高いです。PLC用のOOPの仕様とサンプルプログラムがまとまった教科書的なものがないと社内に展開できないですね。
5.5 OOPとPLCの相性
PLCの高級言語化の話題になると大抵LD・ST論争になります。しかしながら、極論OOPはLDでもできます。それよりも今回のプログラムを作っていて感じたのは、OOPとサイクル制御との相性の悪さです。ずっともやもやしているんですよ。この作りだったら入出力に関係ある領域だけサイクリックに制御して残りはイベント処理で良くね?と。
そこで出てくるのがIEC61499(IEC61131の拡張)やバーチャルPLCなどと呼ばれるものなんでしょうか?確かこれら次世代のFA制御はブロックをイベント制御するものだったと記憶しています。幸いにも近いうちに某社のバーチャルPLCをちょっとだけ触る機会がありそうなので、また考察を整理していきたいです。
終わりに
3万字を超える長い記事にお付き合いいただきありがとうございました。OOPの話になると抽象的な話が多くなり、誰もPLCでコンベアをどう制御するのか教えてくれませんでした。ほならね、でへたくそながら作りました。
けれど、PLCメーカーだったり、何たら協会だったりが音頭を取ってほしい気持ちもあります。
PLCに限らず電気CADもそうですが、最近ライブラリや機能の拡張をアピールするけどその使い方を提案しない会社が多くなったように感じます。何ムロンとは言わないですが。
ある程度業界全体でPLCのOOPデザインパターンが醸成されないと、中々普及しないんじゃないかなと。こんなところで今年の冬休みの自由研究は終わります。