ST言語で作成したFC、FB スターターセット(IEC61131-3準拠)

はじめに

 ST言語使用していますか?一説によるとPLCメーカー特有のしがらみが少なく、他社のPLCへコードを移設しやすいらしいです。
・・・まあ、実際のところPLCごとに構造体や配列の定義の仕方や内部メモリの挙動が異なったり、●-エンスだとTIME型がなくて国際標準FBが他とちょっと違ったりとか・・・やはり手直しは必要なのが現状です。※2024年夏時点

 理想論は頭の良い人たちに任せて、今回は現実的に汎用性が高そうなFCとFBを公開します。自分が作成したものの中から業界を問わずに使えそうなものをピックアップして微調整しています。実際に私が会社で使用しているコードとは異なるので、動作保証はしません。
 なお、投稿者は三菱、Siemens、キーエンス、ロックウェル、安川などのPLCを実務で使用(主にラダーとST)している電気設計者です。また、以前にWAGO様からCC100プレゼント企画に当選し、家ではCODEYSISで遊んだりしています。(WAGO様ありがとうございます。)
 何かの参考になれば幸いです。ほぼすべてのPLCで動作すると思うのでコピペして改造してみてください。

FC→FBの順で紹介します。FCとFBと違いは別サイトに任せます。実務的には、FCはブレークポイントとかを使用しないとデバグが難しいので、社内でブラックテスト後、現地で内部を見なくても使用できる状態に仕上げておく必要があります。


FC1 Limitvalue:リミット処理

一番簡単な奴から。説明不要でしょうが、値を範囲内に収めるのに使用します。たいていのPLCが標準命令で実装していますが、構文が若干違ったり、正しい入力を代入しないと計算エラーが出たりします。後で出てくるFC内で計算エラーが出るともう追うことができないので、作成しておきます。

FUNCTION  fc_LimitValue: REAL
VAR_INPUT
	inValue: REAL;
	minValue: REAL;
	maxValue: REAL;
END_VAR
VAR_OUTPUT
	outValue: REAL;
	rangeOver: BOOL;
END_VAR

 多くのPLCは変数定義を表で行うでしょうが、noteでの公開の都合でコード形式で表示しています。note君はSTに対応していないのか色使いが寂しいね。
 入力値、出力値のほかに、範囲の最小・最大値の入力、範囲外だったときを知らせる出力を用意しました。

//rage内処理
outValue := inValue;
rangeOver := FALSE;

//range over処理
IF outValue > maxValue THEN
	outValue := maxValue;
	rangeOver := TRUE;
END_IF;

IF outValue < minValue THEN
	outValue := minValue;
	rangeOver := TRUE;
END_IF;

//戻り値(個人的には戻り値は使わないが)
fc_LimitValue := outValue;

メモ
・基本の動作はIF分を2つ使用するだけの簡単な回路
・範囲外の時は範囲外だったとFCの外に教えてあげるビットがあると優しい。FCの中は見ないことが前提なので。
・PLCにおいてFCに戻り値の設定は必ずしもしなくてよい。理由は多くの場合FCはラダーかFBD上で使用され、その場合は戻り値よりもVAR_OUTPUTにあるほうが使いやすいから。


FC2 ScalingAD :A/D変換処理

多くのPLCにはAD変換モジュールがあり、専用のパラメータや命令でスケーリングできる。ただし、値の微調整をしたり、変数の検索やモニタをする必要性からプログラムでスケーリングされることが多いと感じる。おそらく真っ先にファンクション化しようとなる個所だろう。

FUNCTION fc_ScalingAD : int
VAR_INPUT
	inValue: INT;
	minValue: REAL;
	maxValue: REAL;
	offset: REAL;
	resolution: REAL;
END_VAR
VAR_OUTPUT
	outValue: INT;
	outValue_REAL: REAL;
	error: bool;
END_VAR
VAR
	inReal: REAL;
END_VAR

 入力はアナログ値(2^nビットデータなので基本的にINT)と、変換後のスケールの最小・最大値、値を微調整するオフセット値、分解能を用意しました。

inReal := INT_TO_REAL(inValue);

//A/D変換計算
IF resolution = 0 THEN
	outValue_REAL := 0;
	outValue := 0;
	error := TRUE;
	RETURN;	//計算エラーになるので終了
ELSE
	outValue_REAL := (inReal + offset) * (maxValue - minValue) / resolution
					+ minValue;
	error := FALSE;
END_IF;

//LIMIT処理
fc_LimitValue(
	inValue:= outValue_REAL,
	minValue:= minValue, 
	maxValue:= maxValue, 
	outValue=> outValue_REAL, 
	rangeOver=> error	//※入力をint→realにする関係で、入力が分解能maxと等しいとerrorがOnすることもある。(影響は軽微だろう)
);

outValue := REAL_TO_INT(outValue_REAL);
//※min<maxを確認したり、出力か、MAX-MINがINTの範囲内か確認するとより安全。要不要は使い方次第か。

メモ
・オフセットは変換前に入れている(多くの場合、変換前のほうが分解能が高く、微調整しやすい)。
・割り算をするときは分母が0にならないようにIF文を用意して必ず例外処理を行う。その後の計算が不要ならRETURNで戻ってもよい。
・出力がINT、DINT、REAL(LREAL)なのかは会社によって異なるだろう。ただし、必ず引数としてREAL(LREAL)を用意しておきたい。理由は、装置の制御及びHMIへの表示は小数点1桁まででよいが、お客様の予知保全・データ収集として小数点2桁までほしいと後から追加される可能性があるから。
・細かいことだが、INT→REALに変換後、LIMIT処理内でREALで値の比較をしている。そのため、分解能のMIN・MAX値ぴったりの時、範囲内なのにrange overe判定される可能性もある。ただし、A/D変換においてその挙動による問題は生じないだろう。


FC3 ScalingDA :D/A変換処理

 AD変換のFCを作ったら、それを参考にDA変換の回路も作成する。当然の流れですね。

FUNCTION fc_ScalingDA : int
VAR_INPUT
	inValue: INT;
	minValue: REAL;
	maxValue: REAL;
	offset: REAL;
	resolution: REAL;
END_VAR
VAR_OUTPUT
	outValue: INT;
	outValue_REAL: REAL;
	error: bool;
END_VAR
VAR
	inReal: REAL;
END_VAR

 引数はA/D変換と同じ。出力のREAL値は不要なので、内部変数にしてもよいかも。

inReal := INT_TO_REAL(inValue);

//D/A変換計算
IF maxValue - minValue = 0 THEN
	outValue_REAL := 0;
	outValue := 0;
	error := TRUE;
	RETURN;	//計算エラーになるので終了
ELSE
	outValue_REAL := (inReal - minValue) * resolution / (maxValue - minValue)
					+ offset;
	error := FALSE;
END_IF;

//LIMIT処理
fc_LimitValue(
	inValue:= outValue_REAL,
	minValue:= minValue, 
	maxValue:= maxValue, 
	outValue=> outValue_REAL, 
	rangeOver=> error	
);

outValue := REAL_TO_INT(outValue_REAL);

メモ
・計算部分が多少変わっているだけで、あとの使用感はA/D変換と同じなはず。


FC4 JudgePassZone :値が範囲内か判定

 例えば、水位が目標値の±いくらなら次の工程に移行する、などといった場合に現在値が合格範囲内に入ったかを確認する処理がされます。これも簡単な動作ですが、ファンクション化してみます。
 なお、内部のプログラムとしてはFC1のリミット動作とほぼ同じです。FC1を使用してrage over信号のビット反転でも合格判定を行うことは可能です。しかしながら、FC1とFC4では目的とする機能が異なります。機能が異なるのであれば別のファンクションを用意すべきです。

FUNCTION fc_JudgePassZone : bool
VAR_INPUT
	inputValue: INT;
	referenceValue: INT;
	passMin: INT;
	passMax: INT;
END_VAR
VAR_OUTPUT
	pass: BOOL;
END_VAR
VAR
	passZoneMin: INT;
	passZoneMax: INT;
END_VAR

 なんとなく今回はINTで定義。
 入力は、判定される値、判定に使用する参照値、合格範囲の最小・最大値を用意します。出力は合格判定のビットのみ。

//合格範囲を設定
passZoneMin := (referenceValue - passMin);
passZoneMax := (referenceValue + passMax);

//合格か判定
IF passZoneMin <= inputValue AND inputValue <= passZoneMax THEN
	pass := TRUE;
ELSE
	pass := FALSE;
END_IF;


// codesysだとcase文の範囲が定数しかつかえない。なんで?
// CASE inputValue OF passZoneMin..passZoneMax:
// 	pass := TRUE;
// ELSE
// 	pass := FALSE;
// END_CASE;

fc_judgePassZone := pass;

メモ
・HMIで合格範囲を+、-と別で設定したり、±で一つの値で設定したりすることを想定して、Minで引いています。Min値にマイナスの値を入れると変なことになるので注意。
・もともとはCase文で記述しようとしていたが、CODEYSISだとCASE文の範囲は数字か定数でないと設定できなかった・・・。こんな感じでPLCごとにできることがちょっと違うのが少し困る。私が見つけた範囲だと、CODEYSISは(WORDデータ).0のようなBYTE以上のデータのビット指定も定数しか使えなかった。共用体があるとはいえ、移植時に大分困るね・・


FC5 2ModeSelect :モード切替

 例えばセレクトスイッチで手動/自動を切り替えたり、HMIや上位の信号で生産のモードを切り替えたり、モードの切り替えはPLC上で何度も出るかと思います。
 作り方は色々あるし、ファンクションを使用せずともラッチリレーをセットすれば良いという人もいます。しかし、人によって作り方がかなり異なるので、標準化という意味でFC化が有効な箇所です。

FUNCTION fc_2ModeSelect : int
VAR_IN_OUT
	modeMemory: INT;
END_VAR
VAR_INPUT
	mode1Select: BOOL;
	mode2Select: BOOL;
	initialMode: INT := 1;
END_VAR
VAR_OUTPUT
	mode1Active: BOOL;
	mode2Active: BOOL;
END_VAR

 最大の特徴はモードの記憶をVAR_IN_OUTを使用してFCの外部に持たせているところ。もちろん、FB内にVAR_RETAINのビットを用意して現在のモードを保存させることもできるが、いくつかの理由からこの方が汎用性が高いと考えています。

・FC外部の引数であるグローバル変数、例えばファイルレジスタなどに現在のモードを保存させることで、バックアップの作成や復元が容易になる。
・入力がハードワイヤーでないならFC外部からもモードの切り替えができる。例えばレシピの読み書きや上位信号によるモード切替などにも後から対応しやすい。
・グローバル変数のINTにモード情報を持たせておけば、外部通信でINT配列でやり取りするときに楽。
・3つ以上のモード選択であっても、今回のFCをベースに作成できるうえ、モードの保存方法を統一できる。

// initial
// 何もモードが選択されていないという状況を防ぐ
IF modeMemory < 1 OR modeMemory > 2 THEN
	modeMemory := initialMode;
END_IF;

// judge mode
IF mode1Select AND (NOT mode2Select) = TRUE THEN
	modeMemory := 1;
END_IF;
IF mode2Select AND (NOT mode1Select) = TRUE THEN
	modeMemory := 2;
END_IF;

//output
mode1Active := modeMemory = 1;
mode2Active := modeMemory = 2;

メモ
・電源立ち上げ時や内部メモリが飛んだ時などに選択されるモードを用意しておくと、不意の事故が少なくなる。
・外部からもモードを切り替えることができるので、インターロックはしっかり考えて運用する必要がある。例えば、このFCの引数であるENにインターロック信号を入れるなど。


FB1 LowPassFilter :ローパスフィルター

 ここからはファンクションブロック。内部変数の連続性が必要なものです。
 現在はいろんなフィルタがプログラム外のパラメータ設定で使用できるのであまり使われる機会は減っているが、毎回パラメータを書き換えるの面倒なのでプログラム内でフィルタを入れたり、異なるメーカー間で通信を行う際にいい具合のフィルターが入った現在値がもらえなかったりと、出番自体はあると思われます。

FUNCTION_BLOCK fb_LowPassFilter
VAR_INPUT
	inValue: REAL;
	// 0~1
	kValue: REAL := 0.1;
END_VAR
VAR_OUTPUT
	outValue: REAL;
END_VAR
VAR
	oldValue: REAL;
END_VAR

 引数は入出力値とフィルタ係数kだけなのでいたってシンプル。

//kValue : 係数k k=1だと生値になり、0に近づくほどフィルタがかかる
//出力 = k*入力 + (1-k)*1スキャン前の出力
outValue := (kValue * inValue) + ((1 - kValue) * oldValue);
oldValue := outValue;

メモ
・フィルタ係数kの値が0に近づくほど値が丸くなる(高周波数域のノイズが取り除かれる)。
・用途的には入力がINTやDINTである可能性も高いので、型変換をFB内に入れても良いかも。
・値の振れを抑えるという意味では配列を使用したFIFO、ループ処理などを駆使した移動平均も考えられる。しかし、そうした処理はPLC的にはかなり重く、毎サイクルの連続処理には向かない(1敗)。また、配列の値が大きすぎるとメモリの無駄になり、小さすぎるとあまり調整できないなどの問題があり、汎用性が低い。


FB2  ApproachTargetGradually :目標値まで値を増加減

 例えばD/A出力を使用するが、いきなり目標の値を出力すると勢いが強すぎたり、何秒後に目標値に到達するようにゆっくり制御する必要がある場合を想定したFBです。

FUNCTION_BLOCK fb_ApproachTarget
VAR_IN_OUT
	value: INT;
END_VAR
VAR_INPUT
	target: INT;
	zeroReset: BOOL;
	flicker: BOOL;
END_VAR
VAR
	R_TRIG: ARRAY[0..1] OF R_TRIG;
END_VAR

 入力値は目標値と、その目標値に近づけるためのフリッカ(例えば100msクロックなど)、値を0に急減させる用の信号の3つを用意しました。
 出力は連続的に値を変化させるのでVAR_IN_OUTを使用。
 また、内部で国際標準FB(R_TRIG;立ち上がり信号)を使用。国際標準FBの説明は他にお任せします。個人的にはインスタンス変数名は型名と同じにして配列で使用するのが、検索と置換もしやすいし、多めに配列を用意しておけばRUN中に回路を追加しやすいしで気に入っている。でも人によって好き嫌いがあるだろうなとも思っている。

// target > value -> INC value
R_TRIG[0](
	CLK := target > value AND flicker
);
IF R_TRIG[0].Q = TRUE THEN
	value := value + 1;
END_IF;

// target < value -> DEC value
R_TRIG[1](
	CLK := target < value AND flicker
);
IF R_TRIG[1].Q = TRUE THEN
	value := value - 1;
END_IF;

//zero reset
IF zeroReset = TRUE THEN
	value := 0;
END_IF;

メモ
・フリッカは国際標準FBであるTPで内部でも作ることができるが、PLCのシステム変数(三菱でいえばSM412)などを引数として使用するのがより正確だろう。
・このFBはフリッカの入力信号で1ずつ増加減するスタイルだが、フリッカ入力信号の周期から計算した値を増加減させることで、設定した加減速時間で動作させることも可能(その場合はREALで計算か)。


FB3 LongPushButton :長押しボタン

 特にHMI上でよく見かける。ユーザー目線で考えると長押しよりポップアップ表示で2段階操作にしたほうがわかりやすそうに思うかもしれない。しかし、実際は装置のプログラムは装置メーカー以外も色々な人が触ったりするので、その画面内で完結していない動作は後から矛盾したり、ソフト変更によるバグが発生したりしやすい。なので今でもよく使用されていると思っているけど、どうでしょうか。
 入力信号を押すとランプが点滅し、長押しが完了すると点灯するという動作にしている。ランプを考慮しないなら、国際標準FBのTONを使用するだけでよいので・・

FUNCTION_BLOCK fb_LongPushButton
VAR_INPUT
	button: BOOL;
	pushTime: TIME;
	flicker: BOOL;
END_VAR
VAR_OUTPUT
	output: BOOL;
	lamp: BOOL;
END_VAR
VAR
	TON: TON;
END_VAR

 入力信号はボタンと長押し時間とランプ点滅用のフリッカ信号。出力信号は長押し完了信号とランプ。いたってシンプル。

TON(
	IN := button,
	PT := pushTime,
	Q => output
);
//ランプはボタン長押しが完了すると点灯、それまでは点滅する
lamp := TON.Q
		OR (button AND flicker);

メモ
・長押しは便利だが、どのボタンが長押しで動作するか分かりづらくなる。わかりやすいHMI設計をしないと操作が複雑になってしまう。
・センサーが検知したことを確認するランプなどにも使用できるかも。


FB4 IncCounter :信号カウント

 単純に信号をカウントするだけなら各PLCメーカーのカウンタを使用したり国際標準FBのCTUを使用することもできる。しかしながら、DINTで大きなデータをカウントして、リセットボタン長押しで値をリセットして・・と考え出すとFB化したくなる回路じゃないでしょうか。
 また、このFBを応用すればアワータイマー(機器の稼働時間を時分で表示し、メンテナンス時期を知らせるもの)のFBも作成できます。

FUNCTION_BLOCK fb_IncCounter
VAR_INPUT
	pls: BOOL;
	reset: BOOL;
	pushTime: TIME;
	flicker: BOOL;
END_VAR
VAR_OUTPUT
	outValue: DINT;
	resetComp: BOOL;
	resetLamp: BOOL;
END_VAR
VAR
	R_TRIG: R_TRIG;
	fb_LongPushButton: fb_LongPushButton;
END_VAR

 長押し動作を使用するので自作のFBを内部で呼んでいます。PLC間のコードの流用をしやすくするため、抽出や拡張などの機能は使用しません(対応していないメーカーが多いので)。

//入力のパルス化
R_TRIG(
	CLK := pls
);
//increment
IF R_TRIG.Q = TRUE THEN
	outValue := outValue + 1;
END_IF;

//リセットボタンの長押し
fb_LongPushButton(
	button := reset,
	pushTime := pushTime,
	flicker := flicker,
	output => resetComp,
	lamp => resetLAmp
);

//出力のリセット
IF resetComp = TRUE THEN
	outValue := 0;
END_IF;

メモ
・停電保持が必要ならば、FB内にRETAIN変数を用意するか、引数をVAR_IN_OUTにして、FB外で保持する必要がある。
・設定値を用意して設定値以上になったら出力をオンする、などの回路を追加することも可能。
・5ずつ増加させる、などの動作も想定するならば増加値も引数に出すのがよい。ただし、小数が設定できるようにと型をREALに変更した場合、小数によっては増加減させると値がずれていくものがある。それを嫌うならば型はDINTで使用し、整数で増加させてから加工するほうがよい。


FB5 OnOffDevice :自己保持動作をする機器

 ONボタンを押すと運転し、OFFボタンを押すと停止する非常に単純な回路。自己保持はラダーのほうがわかりやすいと思う人もいるかもしれない。しかし、最初は単純だったのに後からモードやインターロックが追加され・・・となった場合、ラダーは最初の形を崩さずに増設する傾向があり、最終的にはよくわからなくなったりもする。
 また、機器ごとにFBを作っていく方が、全体的にはわかりやすいコードになるという理由もある。このFBはいろんな機器のFBの起点になるかも。

FUNCTION_BLOCK fb_OnOffDevice
VAR_INPUT
	il: BOOL;
	onButton: BOOL;
	onSignal: BOOL;
	offButton: BOOL;
	offSignal: BOOL;
END_VAR
VAR_OUTPUT
	onHold: BOOL;
	onLamp: BOOL;
	offLamp: BOOL;
END_VAR
VAR
	RS: RS;
END_VAR

 入力の引数としてON/OFFそれぞれ2つずつ用意しています。理由はどれだけ単純な動作の機器でも手動/自動であったり、ハードボタンとHMIのボタンとだったりで2つは使うことが多いから。また、インターロック信号ilはtrueのときに機器をONすることができます(俗に言うa接)。

//自己保持回路
RS(
	SET := onButton OR onSignal,
	RESET1 := offButton OR offSignal OR NOT il,
	Q1 => onHold
);

//ランプ回路(機器からの信号があれば、運転開始後立ち上がるまで点滅なども可能)
onLamp := onHold;
offLamp := NOT onHold;

メモ
・さらに入力信号を増やせばランプの光らせ方を増やすことも可能。
・構造体としてこのFBの引数に対応するものを用意しておくと、機器のグローバル変数をまとめる際に楽になるかもしれない。


FB6 AlarmMaseter & AlarmDetection :アラーム回路(2つ以上のFBの組み合わせ回路)

 最後に少し難しいものを。アラーム検知FBでアラームを検知し、全体処理FBで異常信号やブザー信号を出すという回路です。
 アラーム回路は会社や業界によってちょっと異なっていますが、シーケンス制御技能士などでも使われる、ブザー停止ボタン→アラームリセットボタンの順番で動くタイプを作りました。
 このように2つ以上のFBの組み合わせで動くタイプは、オブジェクト指向っぽくてよくわかんないけどよさそうというイメージを持つかもしれません。しかし、いざPLCで実装するとかなり注意点があって運用が難しかったりします。
 使いこなせるようになると、通信のマスターのFBとスレーブのFBを作ったり、制御エリアによってFBを分けたり、色々なことができるようになるのは事実。

TYPE DUT_AlarmControlNode :
STRUCT
	reset : BOOL;
	buzzer : BOOL;
	alarmDetect : BOOL;
	alarmGroup :ARRAY[0..15] OF BOOL ;
END_STRUCT
END_TYPE

 まず、FB間での信号のやり取りをどうするかですかですが、構造体を使うのが一般的だと思います。グローバル変数をFB内で使用するのは流用性が低いので避けるべきです(ただし、Siemensなど実質POUとして使用しているFBは除く)。
 次に使用データの使い方を決めて、コメントや構造で分かりやすくする必要があります。今回紹介するものは小規模なので特に何もしていませんが、比較的大規模のプログラムだと、親となるFBを決めて、親→子へ渡すデータと子→親へ渡すデータを分けた構造にしたり、変数名で分かるようにする。といった具合に何かしらルールを決めておかないと、後から見た人が読めないプログラムになります。また、PLCにおいてはスキャンする順番も重要なので、親→子の順にスキャンするようにFBを並べる、などのルールも必要です。

FUNCTION_BLOCK INTERNAL fb_AlarmDetection
VAR_IN_OUT
	controlNode: DUT_AlarmControlNode;
END_VAR
VAR_INPUT
	in: BOOL;
	// 0~15
	alarmNo: UINT;
	detectTime: TIME;
END_VAR
VAR_OUTPUT
	outHold: BOOL;
END_VAR
VAR
	TON: TON;
	RS: RS;
	R_TRIG: R_TRIG;
END_VAR

 まずは検知部のFBから紹介。先ほど作った構造体をVAR_IN_OUTで使用します。FBの外にも同じデータ型のローカル変数を用意し、全てのアラーム用FBの引数に割り当ててやればFB間でデータの受け渡しをすることができます。
 それ以外の信号として、アラーム入力信号と、検知時間(例えば、2秒入力信号がONしたらアラーム)、アラームグループを入力できるようにしています。アラームグループは0~15の16種類用意していますが、コードを変更すればいくらでも増やすことができます。軽故障と重故障を分けたり、ポンプA1を止めるアラームをまとめたり、といった使い方を想定しています。

//入力ををタイマーで検知
TON(
	IN := in,
	PT := detectTime
);

//アラームの保持
RS(
	SET := TON.Q,
	RESET1 := controlNode.reset,
	Q1 => outHold
);

//アラーム発生パルス
R_TRIG(
	CLK := RS.Q1
);

//////////////////////////////////////////////
// controlNode -> fb_AlarmMaseter
//////////////////////////////////////////////
IF RS.Q1 = TRUE THEN
	controlNode.alarmDetect := TRUE;	//alarm
END_IF;
IF R_TRIG.Q = TRUE THEN
	controlNode.buzzer := TRUE;	//buzzer
END_IF;
IF RS.Q1 AND 0 <= alarmNo AND alarmNo <= 15 THEN
	controlNode.alarmGroup[alarmNo] := TRUE;	//alarm group
END_IF;

メモ
・アラームが発生すると構造体に信号を転送する。IF文を使用することで、条件を満たしたときだけ回路が動くようにするのがコツ(そうしないと他のFBで同じ信号をONできない)。
・アラームの保持はRS(リセット優先)を使用しているが、SR(セット優先)でもよい。その場合、入力信号がONし続けているときにアラームリセットをした時の挙動が変わる。
・検知部FBのインスタンス変数は配列で用意しておくとアラームが増えたときにすぐに回路を作成できて便利。

FUNCTION_BLOCK fb_AlarmMaseter
VAR_IN_OUT
	controlNode: DUT_AlarmControlNode;
END_VAR
VAR_INPUT
	alarmReset: BOOL;
	buzzerStop: BOOL;
END_VAR

VAR_OUTPUT
	alarmDetect: BOOL;
	buzzer: BOOL;
	alarmGroup: ARRAY[0..15] OF BOOL;
END_VAR
VAR
	R_TRIG: R_TRIG;
	F_TRIG: F_TRIG;
	iLoop: INT;
END_VAR

 お次はアラームのマスター処理FB。入力はアラームリセットとブザー停止の2つ。出力はアラーム発生中とブザー信号のほかに、どのアラームグループのアラームがONしているのかがわかるビット配列を用意しました。
 また、こちらのFBにも当然作成した構造体を使用します。

//アラームが発生しているか確認
alarmDetect := controlNode.alarmDetect;
alarmGroup := controlNode.alarmGroup;

//ブザー鳴動
buzzer := controlNode.buzzer;

//リセットボタン立ち上がりでアラームをリセット信号
R_TRIG(
	CLK := alarmReset
);
controlNode.reset := R_TRIG.Q;
//リセットボタン立ち下がりでアラームを解除
F_TRIG(
	CLK := alarmReset
);
IF F_TRIG.Q = TRUE THEN
	controlNode.alarmDetect := FALSE;
	FOR iLoop :=0 TO 15  DO
		controlNode.alarmGroup[iLoop] := FALSE;
	END_FOR;
END_IF;

//ブザー停止
IF buzzerStop = TRUE THEN
	controlNode.buzzer := FALSE;
END_IF;

メモ
・2つのFBを同じ信号をON/OFFさせており、立ち上がり&立下りも使用しているので非常にソフトを追いづらい。上記でも説明したように、どっちのFBがその変数に書き込みをするのかを分けた方がわかりやすくなる(その分コードが長くなるが)。
・FOR分は最後に+1してからループを終了するので、モニタした時にiLoopは16になる。これも人によってはソフトを追いづらくするかもしれない。
・アラーム発生時にリセットを押した時の動作は、マスター部でアラームリセット時にfalseを代入して、そのすぐ下の検知部でtrueを代入するという動きになる。なのでそのFBの間に別のプログラムがあると挙動がおかしくなる。

おわりに

 誤記があったりプログラムの動作に変な箇所があったら教えてください。私もここで公開しているプログラムは軽く動作確認しただけでので・・
 また、その他業界を選ぶような変わったプログラムを作成しています(PIDなど)。追加した方がよいプログラムがあったら教えてください。作って公開するかもしれません。


 ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
 以下、あまり関係ない話ですが、最近ラダーは古いとか、全てSTにしたほうが良いとか、AIがコード作ってくれるとか、PLCでラダープログラムを作る仕事はなくなるとか、色んな話を聞きます。
 遠い未来という意味ではおおむね同意ですが、実際のところ、現場では大学でPythonやオブジェクト指向を習った新入社員が、ラダーはすぐに使えるようになるのに、STは苦戦しています。それが現実です。
 STは単純に習得難易度が高いです。見た目だけなんですよ。C言語とかに似ているのは。コンパイルされた機械語としては別物なので、ラダー以上にPLCやサイクル制御や内部メモリに関する深い知識を求められます。PLCでやらなくてもなんとかなる動作、例えば画像処理や検品などのプログラムには強いですが、PLCが本来やるべき基本的な制御をSTでするのは、ラダー以上に経験がものをいう実力主義になる気がします。

この記事が気に入ったらサポートをしてみませんか?