🌶️

「令和5年度後期 シーケンス制御(シーケンス制御作業)【1級】製作等作業試験問題」を組む

2025/01/08に公開

はじめに

本記事は、PLC(Programmable Logic Controller)向けのソフトウェア開発に従事、または関心のある方で、Structured Text(ST)による開発に興味がある方向けです。OMRON社のSysmac Studioを使用します。

今回は、装置制御として「令和5年度後期 シーケンス制御(シーケンス制御作業)【1級】製作等作業試験問題」を組んでみます。Sysmac StudioとSTでできるか確認してみました。十分なスペックのPCと使い慣れたキーボード、普段からSTで作り続けてやっとなのではないかというのが感想です。残念ながら私は、コーヒーが必須なのでダメでしょう。

Sysmac Studioは変数定義がテーブルですが、これがテキスト形式であるとして、それが有利に働くのかは分かりません。使い慣れたテキストエディタで編集できるのなら有利かもしれません。また、特定のプログラミングパラダイムで組むことが有利になるのかもはっきりしません。少なくとも初見で即座に組む必要があるので、コントローラがそのプログラミングパラダイムでの構築のためのフレームワークやサポート機能を提供しない限り、設計コスト(考える時間)に見合わないように思われます。

Sysmacプロジェクトは以下にあります。

https://github.com/kmu2030/R5CnvCtrl

コード

盤を持っていないので、テストコードとシミュレータでの手動テストしか行っていませんが、意図した動作はします。恐らくですが、実際にはボタンやスイッチをガチャガチャしたり、全部のリミットスイッチを入れたりと結構なことをするんじゃないかと想像しています。配線やコントローラの立ち上げ、動作テストのための時間を考えると持ち時間の6割、80分で組み上げる必要があると考え、それを制限時間にしました。よって、コードは具合がよくありません。コメントは制限時間内には入れていませんでした。分かりにくかったので、ズルして後で入れました。

POU/プログラム/CnvCtrl
// 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は機械的に構築しました。

  1. IO割付表の信号名の内部変数定義
  2. In, Out部の実装
  3. Main/STATE_MANUAL節の実装
  4. Main/STATE_SET節の実装
  5. Main/STATE_SHOW_HISTORY節の実装
  6. Main/STATE_RESET節の実装
  7. Main/STATE_PROCESS節の実装
  8. Mode change部の実装
  9. Emergency部の実装
  10. Main/STATE_INIT節の実装
  11. IOマップ用のグローバル変数定義
  12. HAL In, HAL Out部の実装
  13. CPUラック構成
  14. IOマップ

POUの構造は、習慣的に普段使うコード構造を使用しました。次のような構造です。"入力-処理-出力"の基本形です。

POU/プログラム/CnvCtrl
// ------------
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)でテストを行いました。テストコードの一部を示します。異常系は次のようなテストです。

POU/ファンクションブロック/Test_CnvCtrl_ManualMode
  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;

正常系は次のようなテストです。

POU/ファンクションブロック/Test_CnvCtrl_ManualMode
  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