ST言語で作成した、シーケンス制御技能士実技試験(令和4年2級)のサンプルプログラム(IEC61131-3準拠)
はじめに
ST言語使用していますか?前回こんな記事を投稿しました。
STでのファンクションブロックの作り方はわかった。でも、実際のPLCのラダー(以下、LD)とST言語がどう対応するのか分からないという人も多いと思います。私もその一人ですし。
なので、多くの人がイメージしやすいであろうシーケンス制御技能士の問題のプログラムをフルSTで作った場合のイメージを公開します。問題は令和4年の2級を使用しています。中身は各自「シーケンス制御技能士 過去問」とかで検索して探してみてください(問題そのものは著作権がありますのでここでは載せません)。
今回もCodesysでプログラムを作っています。ですが、他の伝統的なメーカーのPLCでも動作するような作りにしています。すなわちオブジェクト指向的ではなく、ただの構造化プログラムです。
なお、実際に試験本番で作成しようとしても絶対に時間内に作れないと思います。あの試験はPLCソフトの立ち上げ、コンパイル、通信時間も試験時間内なので、三菱GW Works2によるノーコメントのラダーべた書きが圧倒的に有利です。。。
また、私自身も今回のコードがこんな感じでよいのか、そこまでわかっていません。この問題のような回路は今までLDで実装してきました。なのでもっと良い方法を知っている方はご教示お願いいたします。
1. まずは全体のレイアウトから
1.1 グローバル変数
まず初めに、試験問題を見ながらグローバル変数を用意しましょう。
・グローバル変数リスト:「g9_DI」
VAR_GLOBAL
in1_LS1:BOOL;
in2_LS2:BOOL;
in3_LS3:BOOL;
in4_LS4:BOOL;
in5_LS5:BOOL;
in6_PB1:BOOL;
in7_PB2:BOOL;
in8_PB3:BOOL;
in9_PB4:BOOL;
in10_PB5:BOOL;
in11_SS1:BOOL;
in12_SS0:BOOL;
in13_DSW_1:BOOL;
in14_DSW_2:BOOL;
in15_DSW_4:BOOL;
in16_DSW_8:BOOL;
END_VAR
・グローバル変数リスト:「g9_DO」
VAR_GLOBAL
out20_RY2:BOOL;
out21_RY1:BOOL;
out22_PL1:BOOL;
out23_PL2:BOOL;
out24_PL3:BOOL;
out25_PL4:BOOL;
out26_DPL1_1:BOOL;
out27_DPL1_2:BOOL;
out28_DPL1_4:BOOL;
out29_DPL1_8:BOOL;
out30_DPL2_1:BOOL;
out31_DPL2_2:BOOL;
out32_DPL2_4:BOOL;
out33_DPL2_8:BOOL;
END_VAR
これらの変数が各PLCの入出力モジュールに紐づくイメージです。紐づけは今回省略しています。Codesysの場合はグローバル変数はリストごとにまとまっており、使用する際は「g9_DI.in1_LS1」のように構造体で表記するようです。SiemensのグローバルDBがイメージ近いですかね。
地味にこれ記述するだけで結構時間がかかるので、電気CADの機能で図面のIOコメントをリストアップして、いい具合に自動化できると実用的ですね。ChatGPTやCopilotなども活用して上手いことやりたいところ。
1.2 メインプログラム
構造化プログラムなので、サイクル制御として呼び出すPOUはメインの一つだけにして、その内部で各機能ごとのPOUを呼ぶようにしたいです。今回の問題ではプログラムの作り方は大きく2つ思いつきました。
①待機状態、自動停止状態、サイクル状態1_コンベヤ左移動、・・・のように全ての状態をリストアップし、状態遷移図を作成してからそれに沿った作りにする。
②問題文(仕様書)に合わせて、手動モード、自動モード、共通モードに分けてプログラムを作成する。
今回は②でいきます。①の作りについては、参考になりそうな他の方の記事があったのURLを載せさせていただきます。
Codesysで課金するとUMLでそのまま実装できるようです。前からPLCソフト内にドキュメントを入れたいと思っていましたが、そのままUML図で動くなら外部ドキュメント不要で良いですね。エクセルでデバイスリスト作っている時代じゃないですね。
・・・
さて、メインプログラムはこんな感じ。
PROGRAM p0_main
VAR
p1_manual:p1_manual;
p2_auto:p2_auto;
p3_common:p3_common;
END_VAR
//(_) Input
////////////
//(_) System
////////////
//(1) manual
p1_manual(
Start := NOT G9_DI.in12_SS0
);
//(2) Auto
p2_auto(
Start := G9_DI.in12_SS0
);
//(3) Common
p3_common(
start := TRUE
);
//(_) Output
/////////////
メモ
・Input/OuputというPOU(FC)内でグローバル変数を各PLCのモジュールに紐づけするイメージですが、今回は省略しています。
・SystemというPOU(FB)内でPLCのシステム変数をユーザーのグローバル変数に置き換えるイメージですが、今回は省略しています。例えば「AlwaysON」、「FirstScanON」など。そうしておけばPLCのメーカーが変わってもメイン回路が流用しやすくなると思います。
・手動、自動、共通の3つのFBをPOUとして用意しました。それぞれ引数にStartという入力とBUSYという出力(今回は使用していない)を用意しています。手動、自動モードはSS0によって切り替わるので、Startにその信号を渡しています。共通部は常時動いてほしいので、TRUEを渡しています。「AlwaysON」という変数があるならこちらを使用した方がわかりやすいですね。
簡単なコードですが、LDに慣れている人が最初に思いつくコードは以下じゃないでしょうか?
/////////////
IF NOT G9_DI.in12_SS0 THEN
//(1) manual
p1_manual();
ELSE
//(2) Auto
p2_auto();
END_IF;
//(3) Common
p3_common();
SS0がOFFなら手動、ONなら自動なのでIF文で作ったバージョンです。これ自体は絶対にダメではありませんが、今回は状態遷移をイメージしてPOUを作っていないので、「手動モードに入った瞬間」、「手動モードが終わった瞬間」などの細かい状態を、どこにどう記載するかが問題になります。
STがLDと大きく違う点、というよりLDの大きな特徴としてコイル命令があります。コイル命令に割り当てられている変数は、条件が満たされていないとき勝手にOFFになってくれます。コイル命令はハードのリレーと同じ動作をするものですが、一般的な言語にはありません。なので、LDのA接点間隔でIF文を使うと、IF文が動いていないときをどうするかを考えないといけません。
例えば、IF文内のFB内部変数によってリレーの出力変数にTRUEを代入しているとしましょう。この状態でIF文の条件が切れてもリレーの出力はONのままです。FALSEを代入していませんからね。また、この状態でIF文の条件がもう一度成立したとき、イニシャル処理で内部変数をリセットしないならリレーの出力はONから始まります。FB内の内部変数はTRUEをずっと保持していますので。
まとめると、よく考えずにIF文内にFBを置くと面倒ということです。POU用のFBは常時動くようにし、引数でPOUがONする信号を渡した方が作りやすいと思います。
1.3 POUを作る順番
作る順番も結構大事です。LDではまずは手を動かさないと完成しないという理由でできそうなところから作っていく根性プログラミングが主流だと思いますが(?)、STは変数の思想があっていないとすごく読みづらくなります(LDのようにラダーの形で統一感を出すことができない)。一般的にはコーディングルールがあり、ライブラリや過去装置の残骸を使いまわすのでそれ等の思想に則ればよいのですが、今回はそうしたものがないと仮定します。
この場合、他のPOUにも影響を大きく及ぼすところから作成します。今回の問題では共通部という名のアラーム回路ですね。まあ、LDでもインターロックが一番重要なのでアラーム回路が優先して作成されると思いますが。
次に手動と自動ですが、これは手動回路を自動回路と思想を合わせるかどうかで変わってくると思います。合わせないならサクッと作れる手動が先で、合わせるなら自動が先のほうがやりやすいと思います。思想を合わせると、POUの統一感が出るので読みやすくなり、後で変更する際に変更箇所がわかりやすくなりますが、反面、手動回路が冗長な書き方になります。今回は思想を合わせたいので自動を先に作成します。手動を完全に別で作成するなら汎用ライブラリなどを使用していきたいですね。
2. 次に共通部を作る
2.1 POU
それでは共通部プログラムを作っていきましょう。試験問題でいうところの(3)ですね。
変数はこんな感じ。
FUNCTION_BLOCK p3_common
VAR_INPUT
Start:BOOL;
END_VAR
VAR_OUTPUT
Busy:BOOL;
END_VAR
VAR
_r_trig :ARRAY[0..3] OF R_TRIG;
_RS :ARRAY[0..3] OF RS;
_TON :ARRAY[0..3] OF TON;
_inAlarm :BOOL;
END_VAR
入出力の引数は上記で説明した通り、「Start」、「Busy」の2つです。
今回はローカル変数はアンダーバーから始めるタイプにしてみました。アンダーバーの後は大文字でも小文字でもどっちでもよさそうな感じで作っています。変数の区別は色々な手法があります。特定の英文字を最初に入れたり、大文字小文字を使い分けるやり方もありますが、個人的にはプロジェクト内である程度統一感があればお好きにといった感じです。
また、国際標準FBのうちよく使うパルス(F_TRIG)、FF(RS)、タイマー(TON)は配列で用意しています。正直コードが読みづらくなるんですが、現場でパルス一つ増やすたびに変数を宣言していたらやってられなくなります。なりました。
(*三菱PLCだとFBは配列にできないので注意(2024年現在)。ton_1、ton_2・・と用意するしかないですね*)
後でわかりますが、「_inAlam」という変数以外は他のPOUと共通になるんですよね。じゃあabstractとかでまとめるか、といった具合にプロジェクトがどんどんオブジェクト指向的になっていくんだと思います。べた書きラダーからオブジェクト指向にワープ進化するのは難しそうです。
//00:Alarm Input
_r_trig[0](
CLK := G9_DI.in10_PB5
);
_r_trig[1](
CLK := NOT G9_DI.in12_SS0
);
_inAlarm := _r_trig[0].Q(*PB5_TRIG*)
OR G0_MAIN.CycleState AND _r_trig[1].Q(*not_SS0_TRIG*);
//01:Alarm Hold
_RS[0](
SET := _inAlarm,
RESET1 := G9_DI.in9_PB4 OR NOT start,
Q1 => G0_MAIN.AlarmState
);
//02:Output
G9_DO.out25_PL4 := G0_MAIN.AlarmState;
//99:END
Busy := Start;
次にプログラム内部です。アラーム回路は簡単なので、汎用FBだけで作ることができました。
・00:Alarm Input
アラームの入力条件をまとめています。PB5がOnした時か、サイクル運転中にSS0がOFFした時なので、こんな感じかと。R_TRIG.Qをそのまま使用するときはコメントで何のパルスか書いておかないと読めないですね。
また、サイクル運転中というグローバル変数「g0_main.CycleState」を用意しています。今回はグローバル変数を少なめにしてすべてmainという変数リストにすべて入れます。実際はグローバル変数はきちんと整理した方がよいのですが、その話は後述。
・01:Alarm Hold
汎用FBのRSでグローバル変数「g0_main.AlarmState 」をセットします。この変数がOnしたら動作を中断するようにしていきます。アラームの保持がSR(セット優先)かRS(リセット優先)かは会社にもよるでしょうが、今回の問題ではどっちでも良さそうなのでRSで。
また、動作中にずっとOnする変数はそれがわかる変数名の方がわかりやすいです。「-ing」、「-Hold」などなど。
・02:Output
アラーム中はPL4がONするとのことでそのまま記載。
・99:END
未使用なので省略。
これで共通部は完成です。
3. そしてメインの自動回路を作る
3.1 Initial
自動回路は長いので少しずつ見ていきましょう。試験問題の(2)ですね。以下変数宣言部は省略して、まずはイニシャル処理。
//00:Initial
_r_trig[0](CLK := Start);
IF _r_trig[0].Q THEN
G0_MAIN.CycleState := FALSE;
END_IF;
ここにPOUの入力引数StartがOnした瞬間の処理を記載します。問題文を読む限り特に必要なものはなさそうですが、サイクル状態をOFFにしておきましょうか(後の回路的には不要ですが説明用に)。
3.2 Cycle State
//01:Cycle State
_RS[0](
SET := G9_DI.in1_LS1 AND G9_DI.in7_PB2,
RESET1 := G0_MAIN.AlarmState OR (_step >= 999) OR NOT start,
Q1 => G0_MAIN.CycleState
);
_r_trig[1](CLK := G0_MAIN.CycleState);
//02:Step Start
IF _r_trig[1].Q THEN(*CycleState_TRIG*)
_step := 100;
END_IF;
サイクル運転はLDで実装する場合は自己保持の積み重ねが一般的だと思います。非常にグラフィカルでLDの良さが出ている書き方ですが、STではステップ制御の方がわかりやすいと思います。「_step」という変数の数字をずらしながら制御していきます。この数字は、数字べた書きよりもenumやconstantを上手く活用した方が見やすくなりますが、今回はべた書きで。constantとかを使用する場合は変数は全部大文字にしたいですね。
3.3 Step Sequence
お次は本問題のメインシーケンス部です。
//10:Step Sequence
CASE _step OF
100://10:Left Move
(* FB: _leftMove ON *)
IF _leftEnd THEN
_step := 110;
END_IF;
110://11:Scacn Pallet No
_DPL_No := fc20_scanPalletNo(
bit0:= G9_DI.in3_LS3,
bit1:= G9_DI.in4_LS4,
bit2:= G9_DI.in5_LS5
);(*3.5のFC*)
_step := 120;
120://12:Timer
(* FB: _TON ON *)
IF _TON[0].Q THEN
_step := 130;
END_IF;
130://13:Judge Pallet No
_DPL_No := fc21_judgePalletNo(
no := _DPL_No,
noMemory := G0_MAIN.DPL_NoMemory
);(*3.6のFC*)
_step := 140;
140://14:Timer
(* FB: _TON ON *)
IF _TON[1].Q THEN
_step := 150;
END_IF;
150://15:Right Move
(* FB: _rightMove ON *)
IF _rightEnd THEN
_step := 160;
END_IF;
160://16:Cycle End
_step := 999;
END_CASE;
//20:Step FB
_leftMove(
il := NOT G0_MAIN.AlarmState,
in := _step = 100,
ls := G9_DI.in2_LS2,
out => _leftRY,
end => _leftEnd
);(*3.4のFB*)
_rightMove(
il := NOT G0_MAIN.AlarmState,
in := _step = 150,
ls := G9_DI.in1_LS1,
out => _rightRY,
end => _rightEnd
);(*3.4のFB*)
_TON[0](
IN := _step = 120,
PT := T#2S
);
_TON[1](
IN := _step = 140,
PT := T#0.3S
);
サイクル運転のシーケンスは、
100:右に動いて、
110:パレット番号を読み取りDPLに表示して、
120:2秒待って、
130:パレット番号の値を基にDPLの表示を変更して、
140:少し待って、(問題文にはないですが、DPL表示後に右移動なので)
150:右に動いて、
160:運転終了
です。
現実的には右に動いてからパレット番号を読み取るのにちょっとディレイがあった方がよさそうですね。そういう時は現場でステップ105を追加するとして今は見なかったことにします。
FBはCASE文の外に置いて常時動くようにしています。汎用FB以外にいくつか作成しているFB・FCがあるので見ていきましょうか。
3.4 FB:autoMove
//Move Start
_RS[0](
SET := in,
RESET1 := NOT il OR end,
Q1 => out
);
//Move End
_RS[1](
SET := out AND ls,
RESET1 := NOT ls,
Q1 => end
);
左右の移動「_leftMove」、「_rightMove」は共通のFBを使用しています。入力「in」が入ったら出力「out」をセットする。その状態でリミット「ls」を踏んだらステップ終了信号「end」をOnして出力「out」をリセットするという動作です。「end」は1スキャンOn信号でも別に良いですが、なんとなくリミット踏んでいる間ずっとOnさせています。アラーム信号で動作終了するようにインターロック「il」の入力も用意しています。
3.5 FC:scanPalletNo
_palletNo.0 := bit0;
_palletNo.1 := bit1;
_palletNo.2 := bit2;
fc20_scanPalletNo := _palletNo;
紹介するほどでもないですが、LSを読み取ってINT型のパレット番号に変換します。今回はフルSTということで、FCの出力は用意せずに戻り値を使用しています。
3.6 FC:judgePalletNo
// judge
IF (no MOD 2) = 1 THEN //odd
_tmpR := INT_TO_REAL(no)/2;
_palletNo := fc00_roundUp(in:= _tmpR);
ELSE //even
_palletNo := no * 3;
END_IF;
IF no = 0 THEN
_palletNo := noMemory * 3;
END_IF;
//output
fc21_judgePalletNo := _palletNo;
noMemory := _palletNo;
読み取ったパレット番号が奇数か、偶数か、0かによってDPLの表示を変える処理です。2で割って余りが0か1かで判断するのが楽でしょうか。今回はMODで余りを判断しています。このMOD、PLCによってはMOD(A,B)に表記するものもあるのが困りどころ。
奇数での処理は数値を2で割って切り上げに変換します。LDであればさっき2で割った値が格納されている変数があるので、それに1を足すのが楽でしょうか。しかしながら、処理に注目してきちんと問題文通りに切り上げ処理をしておくのが最終的にはベストです。なのでREAL型に変換して2で割り、切り上げ処理を行います。RoundUpは標準で実装されているPLCもありますが、今回は自作です。0.5足してINTに戻すだけですが。
(下記がFCの中身)
fc00_RoundUp := REAL_TO_INT(in + 0.5);
偶数は現在値を3倍にします。
また、0の時は前回の値を3倍にします。問題文を読む限り電源立ち上げ時にリセットされても問題なさそうですが、今回は停電時保持するイメージで作っています。保持するといってもFC内では保持していません。VAR_IN_OUTの引数を用意して外部で保持するイメージです。なぜかというと変数のRetainはPLCの中身を大きく変えると値が飛びます。なので階層化させたプログラム内のローカル変数をRetainにすると、どういう時にどの値が飛ぶのか(バックアップを取っておく必要があるのか)の管理が面倒になります。
今回は「G0_MAIN.DPL_NoMemory」がRetainのイメージで作っていますが、CodesysならRetainでなくPersistentを使ったり、キーエンスならファイルレジスタFMを使用したり、とにかくプログラムの外で保持しておく設計をお勧めします。
3.7 Step End~
話は戻して自動モードの残りのコードを見てみましょう。
//30:Step End
IF NOT G0_MAIN.CycleState THEN
_step := 0;
_DPL_No := 0;
END_IF;
//40:Output
IF start THEN
G9_DO.out20_RY2 := _leftRY;
G9_DO.out21_RY1 := _rightRY;
G9_DO.out23_PL2 := G0_MAIN.CycleState;
END_IF;
fc22_convertDPL(
no:= _DPL_No,
bit1=> G9_DO.out26_DPL1_1,
bit2=> G9_DO.out27_DPL1_2,
bit4=> G9_DO.out28_DPL1_4,
bit8=> G9_DO.out29_DPL1_8
);
//99:END
Busy := Start;
・30:Step End
サイクル制御中でないときはステップを0に、DPLを0にします。
ステップを0にするのはステップ制御の後でないと途中の動作がおかしくなる回路もあったり。今回は大丈夫なはずですが・・
・40:Output
ここで問題が出てくるのですが、RY1、RY2、PL2は手動モードでもONする必要があります。なので常時Onで変数を代入すると、ダブルコイルみたいに最後のコードに上書きされます。なので今回は「Start」のIF文の中に入れていますが、全部この中に入れれば良いわけではなく、手動モードでもOnする変数だけをIF文内に入れる必要があります。非常に変更しづらいですよね。こうならないためにはどうすればよかったのか。
原因は一つのグローバル変数を複数個所で書き込もうとしているからです。なのでこのグローバル変数はどこのPOUで書き込むかを明示して、それ以外のPOUでは読み込みのみで使用するつくりにした方が良いです。今回の例の場合、自動モードでRY1をOnする変数と手動モードでRY1をOnする変数を別に設けてOutputのPOUで並列に処理してやるのが良いと思います。
こうしてグローバル変数を整理していくと、POUやグローバル変数に番号を付けたり、構造体を使用したりしていく流れになると思います。
DPLの出力のFCは説明要りますかね?一応下記のようにしています。入力が0~15の値の範囲であることを確認して、エラー出力をOnしたりするとより良さそうです。
bit1 := no.0;
bit2 := no.1;
bit4 := no.2;
bit8 := no.3;
4. 最後に手動回路を作る
ようやく試験問題の(1)です。自動回路をコピペして、不要な箇所を削除すればほぼ完成します。なのでさっくりと紹介。
4.1 POU
//00:Initial
_r_trig[0](CLK := Start);
//01:Step Start
IF _r_trig[0].Q THEN(*Start_TRIG*)
_step := 100;
END_IF;
//10:Step Sequence
CASE _step OF
100:
//11:PL1
_inAlternate := G9_DI.in6_PB1 AND NOT G9_DI.in7_PB2;
//12:Left
_inLeft := G9_DI.in7_PB2 AND G9_DO.out22_PL1;
//13:Right
_inRight := G9_DI.in7_PB2 AND NOT G9_DO.out22_PL1;
END_CASE;
//20:Step FB
_alternate(
il:= TRUE,
in:= _inAlternate AND _step = 100,
out=> G9_DO.out22_PL1
);(*4.2のFB*)
_leftMove(
il := NOT G0_MAIN.AlarmState(*and not [RY1]*),
in := _inLeft AND _step = 100,
pl => _leftPL,
out => _leftRY
);(*4.3のFB*)
_rightMove(
il := NOT G0_MAIN.AlarmState(*and not [RY1]*),
in := _inRight AND _step = 100,
pl => _rightPL,
out => _rightRY
);(*4.3のFB*)
//30:Step End
_r_trig[1](CLK := NOT Start);
IF _r_trig[1].Q THEN(*NOT Start_TRIG*)
_step := 0;
G9_DO.out22_PL1 := FALSE;
END_IF;
//40:Output
IF start THEN
G9_DO.out20_RY2 := _leftRY;
G9_DO.out21_RY1 := _rightRY;
G9_DO.out23_PL2 := _leftPL OR _rightPL;
END_IF;
//99:END
Busy := Start;
ステップが一つしかないステップ回路なのでかなり冗長な書き方ですが、まあ統一感はあるかなといった感じ。ポイントだけピックアップすると、以下の通りです。
・基本的には条件を満たした状態でボタンを押したらFBの入力変数をOnする回路です。
・コンベヤ左・右移動は同時に動作すべきではないので、インターロックを入れるのが正しいです。ただし、ハードと設計思想の異なるインターロックを入れると話がややこしくなるので、必ずハードの思想に合わせる必要があります。今回だとコメントのようにFBの入力に入れるか、もしくはできるだけハードに近いOutputのPOUでインターロックを入れるかでしょうか。
・コンベヤが左・右移動中にPL2が点灯する仕様ですが、変数は左移動用「_leftPL」と右移動用「_rightPL」で2つ必要です。
4.2 FB:alternate
自作FBはオルタネイト回路とコンベヤ手動移動回路の2つです。
オルタネイト回路はLDの自己保持で考えると結構面倒ですが、汎用FBにFF(SR)があるのでそこまで面倒ではないです。
_r_trig(CLK := in);
_sr(
SET1 := _r_trig.Q AND NOT out AND il,
Reset := _r_trig.Q AND out OR NOT il,
q1 => out
);
入力「in」の立ち上がりでSRをセットしたりリセットするだけですね。本問では不要ですが一応インターロック「il」がOffの時は出力「out」がOffで固定される作りにしています。
この問題ではPL1点灯状態で自動モードに切り替えるとPL1がOffという仕様ですが、その状態で手動モードに戻した時にPL1が点灯すべきかが不明です。なのでインターロック信号を設けて、PL1が点灯すべきなら常時on「TRUE」を、消灯すべきなら手動モード最初のスキャン「_r_trig[0].Q 」だけOffの信号で渡してやるイメージで作っています。
4.3 FB:manualMove
3秒長押しでリレーOn、長押し中はランプ点滅と、前回の記事で作った長押しボタンFBのような動作ですね。
//flicker
_tp1(IN := NOT _tp2.Q,PT := T#0.5S);
_tp2(IN := NOT _tp1.Q,PT := T#0.5S);
_flicker := _tp1.Q;
//main
_ton(
IN := in AND il,
PT := T#3S
);
pl := _ton.Q OR (in AND il AND _flicker );
out := _ton.Q;
フリッカー信号「_flicker」は入力引数にせず、内部で作成しています。理由は二つあり、このプロジェクトではシステム変数を用意していないのが理由の一つ。もう一つはシーケンス制御技能士1級だと0.6秒On、0.4秒Offとかの点滅が出てくるのでタイマーで実装することを想定していそうだからです。タイマーで実装すると厳密にはスキャンタイムの関係で点滅の切り替わりが遅れるから微妙なんですけどね・・・
汎用FBのタイムパルス「TP」を2つ使うことでフリッカー回路は作ることができます。この場合はそれぞれのパルス時間を引数に出した方が使いやすいかも。
それ以外は前回の長押しボタンとほとんど一緒です。
5. プロジェクト完成
完成したプロジェクトの見た目はこんな感じ。
実機はないのでHMI画面を作成してちょっとだけ動作確認をしています。マウスのクリックだとボタン同時押しとかできないので、完璧ではないですが、仕様通りの動作はしているんじゃないかと思います。
終わりに
昔PLCのeラーニングを終えたばかりで実製番を任されていないころ、シーケンス制御技能士2級の問題をラダーで作成するのに1時間くらいかかっていたと思います。今だと15分くらいでしょうか。当然コメントが一切ない巻物ラダーですが。
あれから10年近く経ちますが、STで作成するのにかなり時間がかかりました。1日数時間使って5日くらい?かかりました・・・。かなりの時間をより良いコードへの修正や確認に使ったとはいえ、やはりSTは難しいですね。
しかしながら、STは変数名がしっかりしていれば読みやすいと思います。特に構造体がルールが整備されている状態で使われていると変数のグルーピングがわかりやすいですね。でもLDにせよ、FBDにせよ、グラフィカルな言語と上手いこと組み合わせながら使っていきたいです。結局UML図書くくらいなら、全体の流れがFBDで確認できて、その中のFBがSTとかの方が良いのでは考えています。
・・・しかし、今回フルSTとは言え、Codesysの機能ほとんど使っていないんですよね。きちんと機能を使うとオブジェクト指向のプロジェクトも作れそうです。PLCでオブジェクト指向が流行るのか、それともPC制御が主流になってPLCの制御領域が減るのか、例えばPLCは安全インターロックを担当しMESが自動モードを操作するようになるか。。。
未来はわかりませんが、オブジェクト指向にせよ、構造体やUnion、EnumとかPLCがPCのように高級言語に寄っていくと、PLCらしさであるRUN中書き込みが制限されるのが現状です。OPC UAも構造体が必要になりますが、こういった機能を使うほど、ちょっと変更するだけでPLCの再起動が必要になる問題は今後どうするんでしょうかね?そのたびに生産を止めても良いような工場を目指すんでしょうか。
また、PLCプログラムは絶対に現場調整が必要になります。メカロスを机上デバグで完璧に考慮するのは現実的ではないので。フィールドエンジニアが減少していくことが想定される世の中ですが、フィールドエンジニアに社内のオブジェクト指向の思想を理解しろというのも無理なので、現場調整領域とそれ以外を分けないと現場の人がPLCを触れなくなってしまいそうです。でもそうすると高級言語のようなシステムをPLCに入れず、別コントローラで制御すれば良くない?となりそう。
とりとめのない話ですが、現状PLCはLDがメインです。今回の国家資格もそうですし、国産メーカーのサイト見てもSTのやる気が感じられません。これからPLCはどうなっていくんでしょうかね、という思いを込めた記事でした。
長文ですがここまで読んでいただきありがとうございました。