「令和5年度後期 シーケンス制御(シーケンス制御作業)【1級】製作等作業試験問題」を組む
はじめに
本記事は、PLC(Programmable Logic Controller)向けのソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studioを使用します。
今回は、装置制御として「令和5年度後期 シーケンス制御(シーケンス制御作業)【1級】製作等作業試験問題」を組んでみます。Sysmac StudioとSTでできるか確認してみました。十分なスペックのPCと使い慣れたキーボード、普段からSTで作り続けてやっとなのではないかというのが感想です。残念ながら私は、コーヒーが必須なのでダメでしょう。
Sysmac Studioは変数定義がテーブルですが、これがテキスト形式であるとして、それが有利に働くのかは分かりません。使い慣れたテキストエディタで編集できるのなら有利かもしれません。また、特定のプログラミングパラダイムで組むことが有利になるのかもはっきりしません。少なくとも初見で即座に組む必要があるので、コントローラがそのプログラミングパラダイムでの構築のためのフレームワークやサポート機能を提供しない限り、設計コスト(考える時間)に見合わないように思われます。
Sysmacプロジェクトは以下にあります。
コード
盤を持っていないので、テストコードとシミュレータでの手動テストしか行っていませんが、意図した動作はします。恐らくですが、実際にはボタンやスイッチをガチャガチャしたり、全部のリミットスイッチを入れたりと結構なことをするんじゃないかと想像しています。配線やコントローラの立ち上げ、動作テストのための時間を考えると持ち時間の6割、80分で組み上げる必要があると考え、それを制限時間にしました。よって、コードは具合がよくありません。コメントは制限時間内には入れていませんでした。分かりにくかったので、ズルして後で入れました。
// HAL In
LS1_IsCnvRight := IOIn[2];
LS2_IsCnvLeft := IOIn[3];
LS3 := IOIn[0];
LS4 := IOIn[1];
LS5 := IOIn[4];
PB1 := IOIn[6];
PB2 := IOIn[7];
PB3 := IOIn[8];
PB4 := IOIn[9];
PB5 := IOIn[5];
SS1_IsIn := IOIn[10];
SS0_IsAuto := IOIn[11];
DSW1 := IOIn[12];
DSW2 := IOIn[13];
DSW3 := IOIn[14];
DSW4 := IOIn[15];
// In
IF NOT (SS0_IsAuto OR SS1_IsIn) THEN
iMode := 10;
ELSIF NOT SS0_IsAuto AND SS1_IsIn THEN
iMode := 20;
ELSIF SS0_IsAuto AND SS1_IsIn THEN
iMode := 30;
ELSE
iMode := 40;
END_IF;
iModeChange := iMode <> iPrevMode;
iByteBits.Bits[0] := LS3;
iByteBits.Bits[1] := LS4;
iByteBits.Bits[2] := LS5;
iProdType := TO_UINT(iByteBits.Value);
iByteBits.Bits[0] := DSW1;
iByteBits.Bits[1] := DSW2;
iByteBits.Bits[2] := DSW3;
iByteBits.Bits[3] := DSW4;
iDSW := TO_UINT(iByteBits.Value);
// Emergency
IF (PB5 AND NOT PL4)
OR (30 < iState AND iState < 36 AND iModeChange)
THEN
PL4 := TRUE;
iTransState := STATE_EMERGENCY;
iState := STATE_RESET;
END_IF;
// Mode change
IF NOT PL4 AND iModeChange THEN
CASE iState OF
10..40:
iTransState := iMode;
iState := STATE_RESET;
END_CASE;
END_IF;
// Main
CASE iState OF
// STATE_INIT
0:
IF PL4 THEN
iTransState := STATE_EMERGENCY;
ELSE
iTransState := iMode;
END_IF;
Inc(iState);
// STATE_RESET
1:
PL1 := FALSE;
PL2 := FALSE;
PL3 := FALSE;
RY1_CnvToLeft := FALSE;
RY2_CnvToRight := FALSE;
DPL1 := 0;
DPL2 := 0;
iAccTimer.In := FALSE;
iTon.In := FALSE;
iState := iTransState;
// STATE_EMERGENCY
5:
IF PB4 THEN
PL4 := FALSE;
iState := iMode;
END_IF;
// STATE_MANUAL
10:
IF PB1 AND PB2 THEN
PL1 := FALSE;
PL2 := FALSE;
RY1_CnvToLeft := FALSE;
RY2_CnvToRight := FALSE;
ELSIF PB1 THEN
PL1 := TRUE;
RY1_CnvToLeft := TRUE;
Inc(iState);
ELSIF PB2 THEN
PL2 := TRUE;
RY2_CnvToRight := TRUE;
iState := iState + 2;
END_IF;
11: // Move to left
IF LS2_IsCnvLeft THEN
PL1 := FALSE;
RY1_CnvToLeft := FALSE;
ELSIF NOT PB1 THEN
PL1 := FALSE;
RY1_CnvToLeft := FALSE;
Dec(iState);
ELSIF PB2 THEN
PL1 := FALSE;
RY1_CnvToLeft := FALSE;
Dec(iState);
END_IF;
12: // Move to right
IF LS1_IsCnvRight THEN
PL2 := FALSE;
RY2_CnvToRight := FALSE;
ELSIF NOT PB2 THEN
PL2 := FALSE;
RY2_CnvToRight := FALSE;
iState := iState - 2;
ELSIF PB1 THEN
PL2 := FALSE;
RY2_CnvToRight := FALSE;
iState := iState - 2;
END_IF;
// STATE_SET
20:
IF LS2_IsCnvLeft THEN
IF PB1 THEN
iProcessTimes[iProdType] := 1;
ELSIF PB2 THEN
iProcessTimes[iProdType] := 2;
ELSIF PB3 THEN
iProcessTimes[iProdType] := 3;
END_IF;
END_IF;
// STATE_PROCESS
30:
IF LS1_IsCnvRight THEN
IF PB2 THEN
PL2 := TRUE;
RY1_CnvToLeft := TRUE;
Inc(iState);
END_IF;
END_IF;
31:
IF LS2_IsCnvLeft THEN
RY1_CnvToLeft := FALSE;
iProcessPrdType := iProdType;
DPL2 := MAX(iProcessTimes[iProdType], UINT#1);
DPL1 := 0;
Inc(iState);
END_IF;
32:
iAccTimer.PT := T#3s;
iAccTimer.In := TRUE;
Inc(iState);
33:
IF iAccTimer.Q THEN
iAccTimer.In := FALSE;
Inc(DPL1);
Inc(iState);
END_IF;
34:
IF PB3 THEN
PL2 := TRUE;
IF DPL1 < DPL2 THEN
iState := iState - 2;
ELSE
DPL1 := 0;
DPL2 := 0;
RY2_CnvToRight := TRUE;
Inc(iState);
END_IF;
ELSE
PL2 := Get1sClk();
END_IF;
35:
IF LS1_IsCnvRight THEN
RY2_CnvToRight := FALSE;
PL2 := FALSE;
PL3 := TRUE;
Inc(iState);
END_IF;
36:
IF PB3 THEN
iPrdOk := TRUE;
END_IF;
IF NOT LS1_IsCnvRight THEN
IF iPrdOk THEN
Inc(iPrdOks[iProcessPrdType]);
ELSE
Inc(iPrdNGs[iProcessPrdType]);
END_IF;
PL3 := FALSE;
iState := 30;
END_IF;
// STATE_SHOW_HISTORY
40:
iTon.In := PB2;
IF iTon.Q THEN
Clear(iPrdOks);
Clear(iPrdNGs);
END_IF;
IF PB1 THEN
IF iPrdOks[iDSW] + iPrdNGs[iDSW] = 0 THEN
i := 0;
ELSE
i := MIN(TO_UINT(TO_REAL(iPrdOks[iDSW]) / TO_REAL(iPrdOks[iDSW] + iPrdNGs[iDSW]) * REAL#100.0), UINT#99);
END_IF;
DPL1 := i MOD 10;
DPL2 := i / 10;
ELSE
DPL1 := 0;
DPL2 := 0;
END_IF;
END_CASE;
iTon(PT:=T#1s);
iAccTimer();
iPrevMode := iMode;
// Out
iByteBits.Value := TO_BYTE(DPL1);
DPL1_1 := iByteBits.Bits[0];
DPL1_2 := iByteBits.Bits[1];
DPL1_4 := iByteBits.Bits[2];
DPL1_8 := iByteBits.Bits[3];
iByteBits.Value := TO_BYTE(DPL2);
DPL2_1 := iByteBits.Bits[0];
DPL2_2 := iByteBits.Bits[1];
DPL2_4 := iByteBits.Bits[2];
DPL2_8 := iByteBits.Bits[3];
// HAL Out
IOOut[4] := RY1_CnvToLeft;
IOOut[5] := RY2_CnvToRight;
IOOut[6] := PL1;
IOOut[7] := PL2;
IOOut[8] := PL3;
IOOut[9] := PL4;
IOOut[10] := DPL1_1;
IOOut[11] := DPL1_2;
IOOut[12] := DPL1_4;
IOOut[13] := DPL1_8;
IOOut[0] := DPL2_1;
IOOut[1] := DPL2_2;
IOOut[2] := DPL2_4;
IOOut[3] := DPL2_8;
構築順序
次のような順序で構築しました。1-7は順当に、8-11は4-7も行き来しながら、12-14は機械的に構築しました。
- IO割付表の信号名の内部変数定義
- In, Out部の実装
- Main/STATE_MANUAL節の実装
- Main/STATE_SET節の実装
- Main/STATE_SHOW_HISTORY節の実装
- Main/STATE_RESET節の実装
- Main/STATE_PROCESS節の実装
- Mode change部の実装
- Emergency部の実装
- Main/STATE_INIT節の実装
- IOマップ用のグローバル変数定義
- HAL In, HAL Out部の実装
- CPUラック構成
- IOマップ
POUの構造は、習慣的に普段使うコード構造を使用しました。次のような構造です。"入力-処理-出力"の基本形です。
// ------------
HAL in
// ------------
In process
// ------------
Ctrl logic
// ------------
Process logic
// ------------
Out process
// ------------
HAL out
// ------------
HAL in
は、I/Oから内部変数への値の受け渡しを行います。変数は1対1で対応するようにします。In process
は、入力値をロジック内で使いやすい値に変換します。例えば、ビット列で渡された値を数値型に変換します。Ctrl logic
は、Process logic
に対して優先的な処理や、Process logic
の実行制御を行います。Process logic
は、処理本体です。Out process
は、ロジック内の出力値をI/Oに渡せる値に変換します。HAL out
は内部変数のI/Oへの受け渡しを行います。HAL in
同様、変数は1対1で対応するようにします。
1. IO割付表の信号名の内部変数定義
概要書を確認し、IO割付表なるものがあるということが分かったので、必要と分かっている変数定義を先に行うことは事前に決めていました。SysmacではLDであっても変数名が可読性を左右します。また、変数は全て内部変数として定義することも決めていました。プログラムはPOUを分けることも、IO割付表をもとに定義する変数をI/Oマップでデバイスに割り付けることもしないと決めていたためです。変数名はシンボルと意味の両方を含むものとしました。タイプが多くなりますが、考える時間がそれほどないので思い出す時間のほうがもったいないと考えました。
2. In, Out部の実装
変換処理のみでロジックが変わらないことが分かっていたため先に組み上げることにしました。複数ビットを数値にするために、適当な共有体を定義しました。焦っていたのでしょう、なぜそうしたのか分からない名称です。
// In
IF NOT (SS0_IsAuto OR SS1_IsIn) THEN
iMode := 10;
ELSIF NOT SS0_IsAuto AND SS1_IsIn THEN
iMode := 20;
ELSIF SS0_IsAuto AND SS1_IsIn THEN
iMode := 30;
ELSE
iMode := 40;
END_IF;
iModeChange := iMode <> iPrevMode;
iByteBits.Bits[0] := LS3;
iByteBits.Bits[1] := LS4;
iByteBits.Bits[2] := LS5;
iProdType := TO_UINT(iByteBits.Value);
iByteBits.Bits[0] := DSW1;
iByteBits.Bits[1] := DSW2;
iByteBits.Bits[2] := DSW3;
iByteBits.Bits[3] := DSW4;
iDSW := TO_UINT(iByteBits.Value);
...
// Out
iByteBits.Value := TO_BYTE(DPL1);
DPL1_1 := iByteBits.Bits[0];
DPL1_2 := iByteBits.Bits[1];
DPL1_4 := iByteBits.Bits[2];
DPL1_8 := iByteBits.Bits[3];
iByteBits.Value := TO_BYTE(DPL2);
DPL2_1 := iByteBits.Bits[0];
DPL2_2 := iByteBits.Bits[1];
DPL2_4 := iByteBits.Bits[2];
DPL2_8 := iByteBits.Bits[3];
3. Main/STATE_MANUAL節の実装
仕様の最初ということもありましたが、内容が分かりやすかったので組み上げてしまうことにしました。IF文のオンパレードになっています。明らかにまとめられると思うかもしれませんが、はじめからまとめることはしません。まとめておいて、後々分けるとなると無駄な作業と間違いを持ち込むことになります。コード品質は問わないようなので、動くことが一番です。
// STATE_MANUAL
10:
IF PB1 AND PB2 THEN
PL1 := FALSE;
PL2 := FALSE;
RY1_CnvToLeft := FALSE;
RY2_CnvToRight := FALSE;
ELSIF PB1 THEN
PL1 := TRUE;
RY1_CnvToLeft := TRUE;
Inc(iState);
ELSIF PB2 THEN
PL2 := TRUE;
RY2_CnvToRight := TRUE;
iState := iState + 2;
END_IF;
11: // Move to left
IF LS2_IsCnvLeft THEN
PL1 := FALSE;
RY1_CnvToLeft := FALSE;
ELSIF NOT PB1 THEN
PL1 := FALSE;
RY1_CnvToLeft := FALSE;
Dec(iState);
ELSIF PB2 THEN
PL1 := FALSE;
RY1_CnvToLeft := FALSE;
Dec(iState);
END_IF;
12: // Move to right
IF LS1_IsCnvRight THEN
PL2 := FALSE;
RY2_CnvToRight := FALSE;
ELSIF NOT PB2 THEN
PL2 := FALSE;
RY2_CnvToRight := FALSE;
iState := iState - 2;
ELSIF PB1 THEN
PL2 := FALSE;
RY2_CnvToRight := FALSE;
iState := iState - 2;
END_IF;
4. Main/STATE_SET節の実装
(1)の次で内容も分かりやすかったので、そのまま組み上げました。品種がいくつあるかとかは気にせず、とりあえず十分であろう配列長を定義して使用しました。問題があれば、後で直そうと考えました。
// STATE_SET
20:
IF LS2_IsCnvLeft THEN
IF PB1 THEN
iProcessTimes[iProdType] := 1;
ELSIF PB2 THEN
iProcessTimes[iProdType] := 2;
ELSIF PB3 THEN
iProcessTimes[iProdType] := 3;
END_IF;
END_IF;
5. Main/STATE_SHOW_HISTORY節の実装
仕様の(3)を見て飛ばすことを決め、(4)を先に組み上げることにしました。(2)同様に分かりやすかったのでそのまま組み上げました。
// STATE_SHOW_HISTORY
40:
iTon.In := PB2;
IF iTon.Q THEN
Clear(iPrdOks);
Clear(iPrdNGs);
END_IF;
IF PB1 THEN
IF iPrdOks[iDSW] + iPrdNGs[iDSW] = 0 THEN
i := 0;
ELSE
i := MIN(TO_UINT(TO_REAL(iPrdOks[iDSW]) / TO_REAL(iPrdOks[iDSW] + iPrdNGs[iDSW]) * REAL#100.0), UINT#99);
END_IF;
DPL1 := i MOD 10;
DPL2 := i / 10;
ELSE
DPL1 := 0;
DPL2 := 0;
END_IF;
6. Main/STATE_RESET節の実装
(5)を確認し、電源断と復帰は力技でいけるので、とりあえずモード変更時は全リセットしておくことにしました。iTransState
に遷移先を設定してこの節に遷移するこでリセット処理を行ってから別のモードへ遷移します。MainのCASE文に並列してもう一つのCASE文を設けて暗黙的に行う方法もあります。
// STATE_RESET
1:
PL1 := FALSE;
PL2 := FALSE;
PL3 := FALSE;
RY1_CnvToLeft := FALSE;
RY2_CnvToRight := FALSE;
DPL1 := 0;
DPL2 := 0;
iAccTimer.In := FALSE;
iTon.In := FALSE;
iState := iTransState;
7. Main/STATE_PROCESS節の実装
飛ばしてあった(3)を組み上げることにしました。節内に内部状態としてのCASE文を作るかどうか悩みましたが、フラットにすることにし、Mainの節として組み上げました。複雑さは無いのですが、組み込み命令に無い周期のクロック信号やエッジ処理が必要となると面倒です。エッジ処理はSTの弱点です。
// STATE_PROCESS
30:
IF LS1_IsCnvRight THEN
IF PB2 THEN
PL2 := TRUE;
RY1_CnvToLeft := TRUE;
Inc(iState);
END_IF;
END_IF;
31:
IF LS2_IsCnvLeft THEN
RY1_CnvToLeft := FALSE;
iProcessPrdType := iProdType;
DPL2 := MAX(iProcessTimes[iProdType], UINT#1);
DPL1 := 0;
Inc(iState);
END_IF;
32:
iAccTimer.PT := T#3s;
iAccTimer.In := TRUE;
Inc(iState);
33:
IF iAccTimer.Q THEN
iAccTimer.In := FALSE;
Inc(DPL1);
Inc(iState);
END_IF;
34:
IF PB3 THEN
PL2 := TRUE;
IF DPL1 < DPL2 THEN
iState := iState - 2;
ELSE
DPL1 := 0;
DPL2 := 0;
RY2_CnvToRight := TRUE;
Inc(iState);
END_IF;
ELSE
PL2 := Get1sClk();
END_IF;
35:
IF LS1_IsCnvRight THEN
RY2_CnvToRight := FALSE;
PL2 := FALSE;
PL3 := TRUE;
Inc(iState);
END_IF;
36:
IF PB3 THEN
iPrdOk := TRUE;
END_IF;
IF NOT LS1_IsCnvRight THEN
IF iPrdOk THEN
Inc(iPrdOks[iProcessPrdType]);
ELSE
Inc(iPrdNGs[iProcessPrdType]);
END_IF;
PL3 := FALSE;
iState := 30;
END_IF;
8. Mode change部の実装
各モードを一通り組み上げた後、モード選択に取り掛かりました。モード変更が生じたら、Mainの状態に関わらず切り替えるようにしました。異常処理と関連するので、Emergency部とは行き来しながらです。切り替えはSTATE_RESET節を経るので、破壊的な出力信号は心配していませんでしたが、STATE_PROCESS節の異常をどのように切り出すかを考える必要がありました。
// Mode change
IF NOT PL4 AND iModeChange THEN
CASE iState OF
10..40:
iTransState := iMode;
iState := STATE_RESET;
END_CASE;
END_IF;
9. Emergency部の実装
(5)に挙がっていた異常は他に優先して処理する必要があるので、Mode change部の前に置きました。異常状態の保持に変数を作るか悩みましたが、短絡的にPL4の保持を有効にして使用することにしました。Mainが(3)を実装したSTATE_PROCESS節であるときの異常は状態値で決定することにしました。
// Emergency
IF (PB5 AND NOT PL4)
OR (30 < iState AND iState < 36 AND iModeChange)
THEN
PL4 := TRUE;
iTransState := STATE_EMERGENCY;
iState := STATE_RESET;
END_IF;
10. Main/STATE_INIT節の実装
起動初回は必ずこの節が実行されるようにしておき、動作テストをして具合がよくなければ変更するつもりです。
// STATE_INIT
0:
IF PL4 THEN
iTransState := STATE_EMERGENCY;
ELSE
iTransState := iMode;
END_IF;
11. I/Oマップ用のグローバル変数定義の実装
概要書からIn、Out共に16点あればよいと分かっていたので定型作業です。個別にBOOL変数を定義することはしません。BOOL型配列でひとまとめにします。
12. HAL In, HAL Out部の実装
概要書の例に従い対応表を作るように組みました。ここは、ハードウェアインターロックを組むように信号のインターロックを構築することもできます。例えば、アクチュエータ動作範囲に限界があり、リミットスイッチでその通知があるのであれば、ここにインターロックを構築することで、雑なロジックでも破壊的な出力信号を出さずに済みます。また、ボタンやスイッチがチャタリングするようなことがあるのか分かりませんが、そうであればここにタイマーを仕込みます。チャタリング対策は、I/Oモジュールの設定でもよいですが、ここで処理たほうが早いかもしれません。
// HAL In
LS1_IsCnvRight := IOIn[2];
LS2_IsCnvLeft := IOIn[3];
LS3 := IOIn[0];
LS4 := IOIn[1];
LS5 := IOIn[4];
PB1 := IOIn[6];
PB2 := IOIn[7];
PB3 := IOIn[8];
PB4 := IOIn[9];
PB5 := IOIn[5];
SS1_IsIn := IOIn[10];
SS0_IsAuto := IOIn[11];
DSW1 := IOIn[12];
DSW2 := IOIn[13];
DSW3 := IOIn[14];
DSW4 := IOIn[15];
...
// HAL Out
IOOut[4] := RY1_CnvToLeft;
IOOut[5] := RY2_CnvToRight;
IOOut[6] := PL1;
IOOut[7] := PL2;
IOOut[8] := PL3;
IOOut[9] := PL4;
IOOut[10] := DPL1_1;
IOOut[11] := DPL1_2;
IOOut[12] := DPL1_4;
IOOut[13] := DPL1_8;
IOOut[0] := DPL2_1;
IOOut[1] := DPL2_2;
IOOut[2] := DPL2_4;
IOOut[3] := DPL2_8;
13. CPUラック構成
CPUをNX1にしたので、IO電源モジュールと入出力それぞれ16点のモジュールにしました。定型作業なのでいつも通りです。CPU含め事前に構成確認は行ってもよいみたいなので、問題は起きないではずです。会場でメモリをリセットするようなので、NX1のデフォルトIPでアクセスできるようにしておくのが無難です。
14. I/Oマップ
これも定型作業です。I/Oマップで変数の自動生成は使用しません。I/Oマップ用のグローバル変数を手動でデバイスに割り付けします。
テストによる動作確認
構築したものをテストしました。手動テストは、シミュレータを起動して信号を切りかえて確認するだけです。(2)と(4)は、値の設定と表示で動作が無いので手動テストで確認しました。気になるようであれば、シミュレータで行ってみてください。問題は、(1)、(3)と(5)です。これらは一連の信号変化があり、確認事項も多いです。そこで、テストを書いて確認することにしました。単体テスト用ですが、以前作成したライブラリを使用しました。
制御コードの入出力は、IOInとIOOut変数だけです。制御コードが動作した状態で、適切な入力を行えばそれに応じた出力があることになります。これをテストします。仕様とそこから推測される振る舞いから一連のテストコードを作成しました。テストコードは異常系から確認し、その後に正常系の確認を行うようにしました。本来、テスト間は十分に独立している必要があるのですが、今回は対象となるPOUがプログラムで手間です。タスクの停止と起動という手もありましたが、対象システムでの異常リセットで済ませることにしました。入力信号は全て制御できるので、対象システムからみた外界は完全に制御できることになります。内部状態は加工回数と加工成否回数です。加工回数はテストに影響しますが、保持変数をあらかじめクリアしておいて未登録(0)でテストを行いました。テストコードの一部を示します。異常系は次のようなテストです。
10:
TestCase(Description:='手動モードでコンベヤ停止中にPB3を押してもコンベヤは動かない。',
Code:=iState);
IOIn[SS0] := FALSE;
IOIn[SS1] := FALSE;
Inc(iState);
11:
IOIn[PB3] := TRUE;
Inc(iState);
12:
AssertBoolEq(Description:='RY1はOFFしている。',
Expected:=FALSE, Value:=IOOut[RY1]);
AssertBoolEq(Description:='PL1はOFFしている。',
Expected:=FALSE, Value:=IOOut[PL1]);
AssertBoolEq(Description:='RY2はOFFしている。',
Expected:=FALSE, Value:=IOOut[RY2]);
AssertBoolEq(Description:='PL2はOFFしている。',
Expected:=FALSE, Value:=IOOut[PL2]);
TestCaseDone();
iReturnState := iState - (iState MOD 10) + 10;
iState := STATE_TEARDOWN;
正常系は次のようなテストです。
40:
TestCase(Description:='手動モードでコンベヤ停止中にPB1を押すとPL1が点灯してコンベヤが左行する。',
Code:=iState);
IOIn[SS0] := FALSE;
IOIn[SS1] := FALSE;
Inc(iState);
41:
IOIn[PB1] := TRUE;
Inc(iState);
42:
AssertBoolEq(Description:='RY1はONする。',
Expected:=TRUE, Value:=IOOut[RY1]);
AssertBoolEq(Description:='PL1はONする。',
Expected:=TRUE, Value:=IOOut[PL1]);
AssertBoolEq(Description:='RY2はOFFする。',
Expected:=FALSE, Value:=IOOut[RY2]);
AssertBoolEq(Description:='PL2はOFFする。',
Expected:=FALSE, Value:=IOOut[PL2]);
TestCaseDone();
iReturnState := iState - (iState MOD 10) + 10;
iState := STATE_TEARDOWN;
50:
TestCase(Description:='手動モードで左行中にPB1を離すとPL1が消灯してコンベヤが停止する。',
Code:=iState);
IOIn[SS0] := FALSE;
IOIn[SS1] := FALSE;
Inc(iState);
51:
IOIn[PB1] := TRUE;
Inc(iState);
52:
IF IOOut[RY1] THEN
IOIn[PB1] := FALSE;
Inc(iState);
END_IF;
53:
AssertBoolEq(Description:='RY1はOFFする。',
Expected:=FALSE, Value:=IOOut[RY1]);
AssertBoolEq(Description:='PL1はOFFする。',
Expected:=FALSE, Value:=IOOut[PL1]);
AssertBoolEq(Description:='RY2はOFFする。',
Expected:=FALSE, Value:=IOOut[RY2]);
AssertBoolEq(Description:='PL2はOFFする。',
Expected:=FALSE, Value:=IOOut[PL2]);
TestCaseDone();
iReturnState := iState - (iState MOD 10) + 10;
iState := STATE_TEARDOWN;
作成したテストは一通りパスするのですが、実機によるテストに勝るものはありません。盤があるのならそれでテストしたほうがよいです。しかしながら、テストが妥当であるならば、シミュレータでのテストも有益です。素早く、かつ、再現性のあるテストを行えるためです。
まとめ
久しぶりにビット主体の制御処理をSTで組みました。ビット主体の制御処理をSTで組むためのフレームについて、何か考えた方がよいと改めて考えさせられました。
Discussion