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
- 状態
_countを PRIVATE に隠蔽(カプセル化)。 - 操作は
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
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
ポイント
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 向けの完全ビルド可能テンプレートも用意できます。