TechCraft – エンジニアのためのスキルアップメモ

エンジニアのスキルアップを少しでも加速する技術ブログ

ST言語で学ぶオブジェクト指向

ST言語で学ぶOOPオブジェクト指向)実践ガイド

〜IEC 61131-3(Structured Text)のクラス設計・継承・インターフェース・ポリモーフィズム

1. はじめに

PLCの世界でも、IEC 61131-3 第3版以降はオブジェクト指向OOPの要素が標準化され、Structured Text(ST)でのカプセル化・継承・インターフェース・メソッド・プロパティが使える環境が増えてきました(CODESYS、TwinCAT 3、TIA Portal(SCL近縁)など)。
本記事は、STでOOPを設計・実装・テストするための実践的な書き方と設計指針をまとめます。現場の可読性向上・再利用性・テスト容易性を狙う方に。


2. STでのOOPキーワード総覧

OOP対応のST処理系(例:CODESYS、TwinCAT 3)でよく使う構文・概念を一覧で把握します。

  • FUNCTION_BLOCK … 事実上の「クラス」。インスタンス(オブジェクト)を生成して状態を保持できる。
  • METHOD … メソッド(振る舞い)。
  • PROPERTY … プロパティ(ゲッター/セッターによる属性アクセス)。
  • INTERFACE … インターフェース(契約)。IMPLEMENTSで実装。
  • EXTENDS … 継承。FUNCTION_BLOCK Child EXTENDS Parent
  • THIS^ … 暗黙の自己参照。処理系によりTHIS^/THISなど。
  • SUPER^ … 基底の呼び出し(対応処理系のみ)。
  • アクセス修飾PUBLIC/INTERNAL/PRIVATE(処理系により差)。
  • 名前空間NAMESPACEやライブラリ単位での整理(処理系依存)。
  • ユニットテストフレームワークやテストPOUを用意してメソッド単位で試験。

以降のコードはTwinCAT 3/CODESYSに親和性の高い書法で示します(細部は環境により要調整)。


3. 最小のOOP:FBをクラスとして使う

要点FUNCTION_BLOCK を「クラス」とみなし、内部状態(VAR)とメソッド(METHOD)で振る舞いを定義。

3.1 最小例(カウンタ)

FUNCTION_BLOCK FB_Counter
VAR_PRIVATE
    _count : DINT := 0;
END_VAR

METHOD PUBLIC Reset : VOID
_count := 0;

METHOD PUBLIC Inc : VOID
_count := _count + 1;

PROPERTY PUBLIC Count : DINT
GET
    Count := _count;
END_PROPERTY
  • 状態 _countPRIVATE に隠蔽(カプセル化)。
  • 操作は Inc() / Reset() メソッド。
  • 読み取りは Count プロパティで提供(書き込みを許さない設計)。

3.2 呼び出し側

PROGRAM MAIN
VAR
    cnt : FB_Counter;
END_VAR

cnt.Inc();
cnt.Inc();
IF cnt.Count >= 10 THEN
    cnt.Reset();
END_IF

効果:生のグローバル変数より、意図の明確なAPIで扱える。


4. 継承(EXTENDS)と共通化

OOPの威力は「共通化された抽象」を現実の機器別に拡張できること。モータ制御を例に。

4.1 抽象的な基底クラス(FB)

FUNCTION_BLOCK PUBLIC FB_MotorBase
VAR_PRIVATE
    _speedCmd : REAL;
    _running  : BOOL;
END_VAR

METHOD PUBLIC Start : VOID
_running := TRUE;

METHOD PUBLIC Stop : VOID
_running := FALSE;

PROPERTY PUBLIC SpeedCmd : REAL
GET SpeedCmd := _speedCmd;
SET _speedCmd := Value;

METHOD PUBLIC IsRunning : BOOL
IsRunning := _running;

4.2 具体機種の派生クラス

FUNCTION_BLOCK PUBLIC FB_ServoMotor EXTENDS FB_MotorBase
VAR_PRIVATE
    _kp : REAL := 1.0;
    _ki : REAL := 0.0;
    _kd : REAL := 0.0;
END_VAR

METHOD PUBLIC Tune(pidKp : REAL; pidKi : REAL; pidKd : REAL) : VOID
_kp := pidKp; _ki := pidKi; _kd := pidKd;

METHOD PUBLIC ExecuteCycle : VOID
// ここで SpeedCmd, IsRunning を利用してサーボ制御演算を行う
IF THIS^.IsRunning() THEN
    // 例:速度指令に対し簡易制御(実際はI/OやドライブAPIを呼ぶ)
END_IF

ポイント

  • 共通APIStart/Stop/SpeedCmd)に対して、具体制御は派生側(ExecuteCycle)に集約。
  • 実際のI/Oマッピングやドライブ呼び出しは派生側で実装するのが自然。

5. インターフェース(INTERFACE)とポリモーフィズム

UIや上位レイヤは「モータっぽいもの」を扱えればよく、具体機種を知らないのが理想です。そのためにインターフェースを定義します。

5.1 インターフェース定義

INTERFACE PUBLIC IMotor
METHOD Start : VOID
METHOD Stop  : VOID
METHOD IsRunning : BOOL
PROPERTY SpeedCmd : REAL
END_INTERFACE

5.2 実装宣言(IMPLEMENTS)

FUNCTION_BLOCK PUBLIC FB_ServoMotor EXTENDS FB_MotorBase IMPLEMENTS IMotor
// 既に Start/Stop/IsRunning/SpeedCmd を提供しているので契約を満たす

5.3 ポリモーフィズム(配列・リストで抽象的に扱う)

PROGRAM MAIN
VAR
    motors : ARRAY[1..3] OF IMotor; // インターフェース配列
    m1 : FB_ServoMotor;
    m2 : FB_MotorBase;   // 直接IMotorを満たすなら可
    m3 : FB_ServoMotor;
    i  : INT;
END_VAR

// 紐づけ(処理系により参照の扱いに差。必要に応じPOINTER/REFERENCEを利用)
motors[1] := m1;
motors[2] := m2;
motors[3] := m3;

FOR i := 1 TO 3 DO
    motors[i].SpeedCmd := 1500.0;
    motors[i].Start();
END_FOR

利点:呼び出し側は具体型を意識しない=依存の反転。部品差し替えが容易。


6. コンストラクタ(初期化)・ライフサイクル

STでは「暗黙初期化」「明示Initメソッド」「属性による初期化」など処理系差があります。TwinCAT/CODESYSでは以下パターンが実用的。

FUNCTION_BLOCK PUBLIC FB_Filter
VAR_INPUT
    CutoffHz : REAL := 10.0; // デフォルト値
END_VAR
VAR_PRIVATE
    _prev : REAL := 0.0;
    _alpha : REAL;
END_VAR

METHOD PUBLIC Init : VOID
// サンプリング周期Tsは外部から与える想定
_alpha := 0.1; // 簡易例

METHOD PUBLIC Apply : REAL
VAR_INPUT x : REAL;
Apply := _alpha * x + (1.0 - _alpha) * _prev;
_prev := Apply;

運用インスタンス生成後に Init() を1回だけ呼ぶ、Apply() を周期呼び出し。


7. 例題:搬送ラインをOOPでモデル化

要件:複数の搬送機(ベルト、ローラ)を同一UI/上位制御で扱いたい。安全停止や速度制限は共通化したい。

7.1 インターフェース

INTERFACE PUBLIC IConveyor
METHOD Start : VOID
METHOD Stop  : VOID
METHOD SetSpeed : VOID
VAR_INPUT rpm : REAL; END_VAR
METHOD GetSpeed : REAL
METHOD EStop  : VOID
PROPERTY Name : STRING
END_INTERFACE

7.2 基底(安全・共通機能)

FUNCTION_BLOCK PUBLIC FB_ConveyorBase IMPLEMENTS IConveyor
VAR_PRIVATE
    _name : STRING(32) := 'Conv';
    _rpmCmd : REAL;
    _running : BOOL;
    _estop   : BOOL;
END_VAR

PROPERTY PUBLIC Name : STRING
GET Name := _name;
SET _name := Value;

METHOD PUBLIC Start : VOID
IF NOT _estop THEN _running := TRUE; END_IF

METHOD PUBLIC Stop : VOID
_running := FALSE;

METHOD PUBLIC EStop : VOID
_estop := TRUE; _running := FALSE;

METHOD PUBLIC SetSpeed : VOID
VAR_INPUT rpm : REAL; END_VAR
IF rpm > 2000.0 THEN _rpmCmd := 2000.0; // 制限
ELSIF rpm < 0.0 THEN _rpmCmd := 0.0;
ELSE _rpmCmd := rpm;
END_IF

METHOD PUBLIC GetSpeed : REAL
GetSpeed := _rpmCmd;

7.3 具体機種(ベルト)

FUNCTION_BLOCK PUBLIC FB_BeltConveyor EXTENDS FB_ConveyorBase
VAR_PRIVATE
    _pwmOut : REAL; // 実機ならI/Oマップへ
END_VAR

METHOD PUBLIC Cycle : VOID
IF THIS^.GetSpeed() > 0.0 AND THIS^.IsRunning() THEN
    // GetSpeed/IsRunningは基底のAPI(IsRunningを追加で実装しても良い)
    _pwmOut := THIS^.GetSpeed() / 2000.0;
ELSE
    _pwmOut := 0.0;
END_IF

7.4 監視側(抽象配列で管理)

PROGRAM MAIN
VAR
    belts : ARRAY[1..2] OF IConveyor;
    b1 : FB_BeltConveyor;
    b2 : FB_BeltConveyor;
    i  : INT;
END_VAR

belts[1] := b1;
belts[2] := b2;

FOR i := 1 TO 2 DO
    belts[i].Name := CONCAT('Belt', DINT_TO_STRING(i));
    belts[i].SetSpeed(rpm := 1200.0);
    belts[i].Start();
END_FOR

狙い:機種追加のたびに上位のロジックをいじらない。差し替え可能性=保守性が上がる。


8. 設計指針(現場で効くOOPルール)

1) データ隠蔽(VAR_PRIVATE):外から直接触らせない。メソッド/プロパティ経由に統一。
2) インターフェース駆動:UI/上位層はINTERFACEで受ける。実体は現場差し替え。
3) 薄い継承・厚い委譲:過剰な多段継承は避け、基底は最小限、機能はコンポジションで。
4) エラー伝搬の方針:メソッドはBOOL戻り+LastErrorプロパティ or ResultCode列挙体など統一
5) 状態機械:FB内部を明示的ステートマシンにして異常遷移を抑止。
6) I/O分離:ハードI/O呼び出しをアダプタFBに分離し、純ロジックはモックでテスト可能に。
7) 命名規約FB_(クラス風FB)、IF_(インターフェース)、ST_(ステート)、ERR_(エラー)等で一貫性。


9. テストとモック(OOPの旨味)

  • モック実装IConveyorのテスト用モックFBを用意し、速度コマンドへの応答を仮想化。
  • シミュレーションPOU:サイクル呼び出しで期待値検証。
  • 境界値テスト:プロパティの上限/下限、異常状態(E-Stop)を網羅。
  • CI連携:ソフトPLC仮想ランタイムでユニットテストを定期実行(処理系により手順化)。

10. よくある落とし穴と回避策

  • 継承が深いデバッグ困難。1〜2段以内を推奨。
  • グローバル乱用 → 依存が絡み、順序問題が頻発。コンストラクタ/Initに寄せる。
  • 可視性ゆるすぎ → なんでも公開すると壊される。PRIVATE基準。
  • I/O直結ロジック → テスト不能。I/Oアダプタ化し、論理と物理を分離。
  • 例外的ベンダ拡張乱用 → 移植が困難。標準のINTERFACE/METHOD/PROPERTY優先。

11. まとめ

  • STのOOPは、現場でも通用する「読みやすさ・差し替えやすさ・テストしやすさ」をもたらす。
  • 中心は FUNCTION_BLOCK=クラスINTERFACE=契約
  • 継承は薄く、インターフェースと委譲で拡張する設計が長持ちする。
  • I/Oとロジックを分け、モックでテストできる構成に。

12. 付録:サンプル(インターフェース+実装+テスト・スケルトン)

12.1 インターフェース

INTERFACE PUBLIC IDevice
METHOD Init : BOOL
METHOD Start : BOOL
METHOD Stop  : BOOL
METHOD Health : BOOL
PROPERTY Name : STRING
END_INTERFACE

12.2 汎用デバイス(基底)

FUNCTION_BLOCK PUBLIC FB_DeviceBase IMPLEMENTS IDevice
VAR_PRIVATE
    _name : STRING(32) := 'Device';
    _inited, _running : BOOL;
END_VAR

PROPERTY PUBLIC Name : STRING
GET Name := _name;
SET _name := Value;

METHOD PUBLIC Init : BOOL
_inited := TRUE; Init := TRUE;

METHOD PUBLIC Start : BOOL
IF _inited THEN _running := TRUE; Start := TRUE; END_IF

METHOD PUBLIC Stop : BOOL
_running := FALSE; Stop := TRUE;

METHOD PUBLIC Health : BOOL
Health := _inited AND _running;

12.3 実装(センサ)

FUNCTION_BLOCK PUBLIC FB_TempSensor EXTENDS FB_DeviceBase
VAR_PRIVATE
    _last : REAL;
END_VAR

METHOD PUBLIC Read : REAL
// ここで入力チャネルを読む代わりに疑似値を返す
_last := _last + 0.1;
Read := _last;

12.4 テストPOU(疑似)

PROGRAM PRG_DeviceTest
VAR
    s : FB_TempSensor;
    ok : BOOL;
END_VAR

ok := s.Init();
ok := ok AND s.Start();

IF NOT ok THEN
    // 失敗処理
END_IF

IF s.Health() THEN
    // 読み取り
    ; // s.Read();
END_IF

参考:キー概念のクイックリファレンス

  • カプセル化VAR_PRIVATE/PROPERTYで外部からの直接変更を禁止
  • 継承EXTENDSで共通APIを再利用
  • 抽象化INTERFACEで呼び出し側の依存を除去
  • 多態性INTERFACE配列やREFERENCEで多機種を横並び制御
  • テスト容易性:I/O分離とモックでユニットテスト

――以上、ST言語でのOOPを現場で活かすための設計・実装・運用の勘所でした。必要があれば、TwinCAT 3/CODESYS 向けの完全ビルド可能テンプレートも用意できます。