IEC 61131-3 ST言語

はじめに

IEC 61131-3

IEC 61131-3は、FA(Factory Automation)のプログラミング言語に関する国際規格です。次の特徴があります。

  1. 論理型・整数型・実数型に加え、ビット列型・時間型・時刻型・列挙型・構造体型などの豊富なデータ型
  2. Java™言語ライクなオブジェクト指向もできるマルチパラダイムプログラミング言語
  3. テキスト言語ST・IL、グラフィカル言語LD・FBD・SFCの計5言語を定義。 ※Ed.4ではIL言語が削除され4言語構成。
  4. 静的型付けと動的型付けの混在
  5. 動的メモリ確保なし
  6. 物理デバイスのメモリにアクセスする構文
  7. サイクリック実行があることを前提とする言語要素

一方、安全性を重視しているため、関数の再帰呼出しを制限していたり、関数ポインタを含むポインタ型を代替する機能が制約されていたりと機能的な自由度は高くありません。「APPENDIX. IEC 61131-3にないもの」も参照ください。

このページの目的

日本のFAにおけるIEC 61131-3の浸透は十分とは言えません。従来のアドレスプログラミングやラダーによる巻物スタイルのプログラミングは根強く残っており、IEC 61131-3が良いだろうことは感じていても、約20年もの間、IEC 61131-3への移行は進んでいません。

これには、IEC 61131-3規格側の問題、ベンダー側(PLCメーカーなどIEC 61131-3プログラミング環境提供側)の問題、およびユーザー側の様々な事情があります。

  1. ユーザー側に非IEC 61131-3の膨大な資産があり、IEC 61131-3に切り替えてしまうと教育も含め、生産性が急激に下がる。
  2. IEC 61131-3のプログラムを実行したり試してみる手頃な環境がなく、導入を検討する人や学ぼうとする人の機会損失している。
  3. IEC 61131-3仕様の理解が困難であり、ベンダーのIEC 61131-3に対する準拠度合いも高くなく、やりたいことができるのか分からない。
  4. IEC 61131-3を活用したときの正しい設計・実装に関する疑問や問題を解消する主な手段がベンダー側の偏った情報(セミナー、マニュアル、電話)しかない。

これらの問題が解消されることで、IEC 61131-3の導入障壁が下がり、IEC 61131-3のレベルが全体的に底上げされていくものと考えます。

本サイトは、上記4.の一助にするために作りました。IEC 61131-3の正しい詳細情報であったり、何ができるのか・できないのかといった情報を提供します。

このページの範囲

主としてIEC 61131-3 3rd Editionを範囲とします。一部、IEC 61131-3 4th Editionの新機能・変更点にも言及します。

基本

プログラミング言語入門で最初の例として示されるHello worldのプログラムです。

program Main
	var text: string[128]; end_var
	//
	text := 'Hello, world!';
end_program

IEC 61131-3に標準入出力の定義はないため、文字列型変数に代入しているだけにしています。文字列型変数をモニタすることでHello, world!を確認してください。このプログラムを実行するためには、実行するプログラム(例では、MainプログラムPOU)をタスクに割り付け、ビルドし、PLCにダウンロードする必要があります。

プログラムの実行とタスク

特定の関数から始まるといったエントリ関数のようなものはありません。複数のプログラムインスタンスがタスクの優先度に従い並列実行されます。。。これが、IEC 61131-3の規定ですが、各ベンダーの実装はそうではありません。優先度の高い一つのタスク、もしくはそのベンダーにとって"メインのタスク"があり、そのタスクに割り付けたプログラムインスタンスがタスクごとに上から順番に実行されます。

プログラムの実行とタスクの実装
IEC 61131-3 ベンダーの実装
タスク割付
プログラム1 with 定周期タスク1(優先度高)
プログラム2 with 定周期タスク1(優先度高)
プログラム3 with 定周期タスク2(優先度低)
プログラム4 with 定周期タスク2(優先度低)
プログラム5 with 定周期タスク3(優先度低)
プログラム6 with 定周期タスク3(優先度低)
プログラム7 with イベントタスク1
プログラム8 with イベントタスク2
定周期タスク1(優先度高):
  - プログラム1
  - プログラム2
定周期タスク2(優先度低):
  - プログラム3
  - プログラム4
定周期タスク3(優先度低):
  - プログラム5
  - プログラム6
イベントタスク1:
  - プログラム7
イベントタスク2:
  - プログラム8
動作 どの定周期タスクのプログラムが最初に実行されるかは不確定。 プログラム1、3、5のいずれかが最初に実行される。

プログラムインスタンスには、プログラム間でデータをやりとりするための引数があります。。。これが、IEC 61131-3の規定ですが、各ベンダーの実装はそうではありません。プログラム間でデータをやりとりするには、グローバル変数を使用します。

コメント

ST言語には、1つの行コメントと、2つの複数行コメントがあります。

RightsideIsLinecomment := true; // これは行コメントです。

(* これは
複数行コメント
です。 *)
AbovesideIsBlockcomment := true;

RightsideIsAnotherBlockcomment := true; /* これは
もう一つの
複数行コメント
です。 */

文字集合・エンコーディング

プログラムやコメントで使用できる文字集合は、ISO/IEC 10646に従います。エンコーディングは、各ベンダーの実装依存です。※1

※1 ISO/IEC 10646を符号化文字集合と解釈するか、文字符号化方式と解釈するかで違ってきますが、規格において、"Character set"の項に記載あるため、ここでは前者と解釈します。

キーワード

キーワードは、プログラム全体を構成するための文字列です。大文字・小文字を区別しない、キーワードを識別子として使用できない、などの特性があります。

IEC 61131-3規格にキーワードの具体的な定義はなく、構文規則から推測されるキーワードは次のすべての文字列です。※1

abstract, action, and, array, at, bool, by, byte, case, char, class, configuration, constant, continue, date, date_and_time, dint, do, dt, dword, else, elsif, end_action, end_case, end_class, end_configuration, end_for, end_function, end_function_block, end_if, end_interface, end_method, end_namespace, end_program, end_repeat, end_resource, end_step, end_struct, end_transition, end_type, end_var, end_while, exit, extends, final, for, from, function, function_block, f_edge, if, implements, initial_step, int, interface, internal, ldate, ldate_and_time, ldt, lint, lreal, ltime, ltime_of_day, ltod, lword, method, mod, namespace, non_retain, not, null, of, on, or, overlap, override, private, program, protected, public, read_only, read_write, real, ref, ref_to, repeat, resource, retain, return, r_edge, sint, step, string, struct, super, task, then, this, time, time_of_day, to, tod, transition, type, udint, uint, ulint, until, using, usint, var, var_access, var_config, var_external, var_global, var_input, var_in_out, var_output, var_temp, wchar, while, with, word, wstring, xor

※1 IEC 61131-3規格上は、IL演算子のCALもキーワードと読み取れますが、本書ではIL演算子をキーワードに含めていません。

識別子

識別子は、変数やデータ型などを識別するための名前であり、次のすべてを満たさなければいけません。

  1. 文字・数字・アンダースコアで構成される。
  2. 数字で始まってはいけない。
  3. アンダースコアで終端してはいけない。
  4. 連続するアンダースコアを含んではいけない。
  5. キーワードではない。

例えば、`abc`、`D2E_F3`、`_hi_jk_lmn`は識別子になりえますが、`5op`、`__qr`、`s__t`、`uvw_`は識別子ではありません。

また、識別子の大文字・小文字を区別しません。`Abc`、`abc`、`ABC`は同じ識別子と解釈されます。

パス

パス、又はパス文字列は、以下の要素で登場する%で始まる文字列です。

🛈パス文字列全体の解析処理の比重を字句解析器(Lexer)におくのが良いか、構文解析器(Parser)におくのが良いかは難しい問題です。IEC 61131-3規定に適合するパス文字列の正規表現の例を以下に示しておきます。どちらも下にあるほど文法が厳しいです。字句解析・構文解析では広めにとっておいて意味解析で詳細な意味付けや検査することをお勧めします。

プラグマ

プラグマは、{ ~ }で囲われた要素であり、IEC 61131-3で規定しない要素のために使用します。個々のプラグマの仕様は、IEC 61131-3規格では定義しておらず、IEC 61131-3実装者が定義します。

プラグマは、コメントや文字列リテラルを除く様々な場所に登場できると規定されており、以下の例のように「警告を無視する」「要素のドキュメントを埋め込む」「変数に追加属性を与える」「コンパイラの設定を変更する」など様々な用途での活用が想定されます。

{disable-warning: *}

{document: mainプログラムは装置全体を制御します。}
program Main
	var
		v: lint {register};
	end_var
	//
	{line: 1}
	:
end_program

プラグマに関するIEC 61131-3の規定については、上述した程度の言及しかなく次に示すことは実装依存です。

※1 プラグマ内も文字列リテラルと同じく$でエスケープするのが受け入れられやすいと考えます。
※2 未知のプラグマについては、無視してもコンパイルエラーとならず、プログラムの意味が変わらない範囲での活用が望ましいと考えます。

変数・基本データ型・リテラル

変数は、データを保持するための名前付き記憶領域です。IEC 61131-3では、変数を使用する前に宣言セクション(var~end_varなど)で型と名前を宣言する必要があります。リテラルは、プログラムコードに直接記述する値の表現です(例:3, 3.14, true, 'hello')。

基本データ型

下表に示す通り、整数値、実数値、ビット列値、真偽値、時間、時間軸上の点、文字、文字列など多くの基本データ型があります。

基本データ型の一覧
データ型キーワードビット幅範囲デフォルト初期値リテラル※1の例説明
符号あり整数型sint8-128~1270-59,
13,
1_234_567,
2#11_01,
-16#2f
int16-32768~32767
dint32-2147483648~2147483647
lint64-9223372036854775808~9223372036854775807
符号なし整数型usint80~255
uint160~65535
udint320~4294967295
ulint640~18446744073709551615
浮動小数点型real32IEC 60559単精度浮動小数点規格の範囲に従う00.1,
1.0e-6,
-1e3,
-5,
16#ff
lreal64IEC 60559倍精度浮動小数点規格の範囲に従う
ビット列型byte816#00~16#ff16#00byte#16#ff,
byte#2#1111_1111_1111_1111,
dword#16#cafe_babe
word1616#0000~16#ffff16#0000
dword3216#0000_0000~16#ffff_ffff16#0000_0000
lword6416#0000_0000_0000_0000~16#ffff_ffff_ffff_ffff16#0000_0000_0000_0000
ブール型bool1false(偽)、又はtrue(真)falsetrue,
bool#0
時間型 time実装依存実装依存0秒time#2s,
time#0s,
time#-3s,
t#2s
最小単位も含めて実装依存のため、time型ではなく、ltime型の使用を推奨します。
ltime64-9223372036854775808ナノ秒~9223372036854775807ナノ秒(-106751日23時間47分16.854775808秒~106751日23時間47分16.854775807秒)
最小単位はナノ秒
ltime#2s,
ltime#0s,
ltime#-3s,
lt#1s,
lt#1d2h3m4s5ms6us7ns,
lt#1.234567ms,
lt#1m234us567ns
約585年間を表現可能
日付型date実装依存実装依存
最小単位は1日
実装依存date#1978-9-7,
d#2022-11-07
ldate641970年1月1日~実装依存※2
最小単位は1日
1970年1月1日ldate#1978-9-7,
ld#2026-03-21
時刻型time_of_day実装依存実装依存実装依存time_of_day#23:59:59,
tod#23:59:59
tod
ltime_of_day640時0分0秒~23時59分59.999999999秒
最小単位はナノ秒
0時0分0秒ltime_of_day#23:59:59.999_999_999,
ltod#23:59:59.999999999
ltod
日付時刻型date_and_time実装依存実装依存実装依存date_and_time#1970-1-1-23:59:59,
dt#1970-01-01-23:59:59
dt
ldate_and_time641970年1月1日0時0分0秒~2262年4月11日23時47分16.854775807秒
最小単位はナノ秒
1970年1月1日0時0分0秒ldate_and_time#2022-11-07-23:59:59.999_999_999,
ldt#2262-04-11-23:47:16.854775807
ldt
文字型char8実装依存; ISO/IEC 10646規格の符号化文字集合の部分的な一文字NULL文字('$00''a',
'$00' // NULL文字
wchar16実装依存; ISO/IEC 10646規格の符号化文字集合の部分的な一文字NULL文字("$0000""a",
"$0000" // NULL文字
文字列型string8*N実装依存; ISO/IEC 10646規格の符号化文字集合の部分的な0個以上N個以下の文字の列空文字列('''hello',
'abc',
'$61$62$63' // = 'abc',
'' // 空文字列
wstring16*N実装依存; ISO/IEC 10646規格の符号化文字集合の部分的な0個以上N個以下の文字の列空文字列("""hello",
"abc",
"$0061$0062$0063" // = 'abc',
"" // 空文字列

※1 リテラルは、値のプログラミング言語における表現です。例えば、IEC 61131-3の真偽値である「真」の値の表現は、「true」や「bool#1」があります。

※2 最小単位は1日ですが、必ずしも264日間の日付とはならないことに注意してください。

整数型

整数型は整数値を表現する型です。符号あり整数型と符号なし整数型の2系統があり、それぞれビット幅の違いにより4種類ずつあります。型名の先頭のuは符号なしを意味するunsigned、sは8ビットを意味するsmall、dは32ビットを意味するdouble、lは64ビットを意味するlongを表します。

整数型の一覧
種類データ型ビット幅範囲デフォルト初期値
符号ありsint8-128 〜 1270
int16-32768 〜 327670
dint32-2147483648 〜 21474836470
lint64-9223372036854775808 〜 92233720368547758070
符号なしusint80 〜 2550
uint160 〜 655350
udint320 〜 42949672950
ulint640 〜 184467440737095516150
リテラル

整数リテラルは、10進数・16進数(16#)・2進数(2#)で記述できます。可読性のためにアンダースコアで桁区切りできます。型名プレフィックス(sint#など)で型を明示することもできます。なお、8進数(8#)のサポート可否については、実装依存です。

iv := 255;            // 10進数
iv := 16#FF;          // 16進数
iv := 2#1111_1111;    // 2進数、アンダースコアで桁区切り
iv := 8#377;          // 8進数 実装依存
iv := 1_000_000;      // 10進数、アンダースコアで桁区切り
iv := dint#-1;        // 型指定形式
演算

整数型には加算・減算・乗算・除算・剰余・べき乗・比較・等価の演算が使用できます。整数の除算(/)は切り捨て(ゼロ方向の丸め)です。

iv := 10 / 3;    // 3; 切り捨て
iv := -7 / 2;    // -3; ゼロ方向への切り捨て
iv := 10 mod 3;  // 1; 剰余
iv := 2 ** 10;   // 1024; べき乗

なお、ビット演算(and/or/xor/not)は整数型には適用できず、ビット列型専用の演算です。

オーバーフロー

演算結果が型の表現範囲を超えた場合(オーバーフロー)の動作は実装依存です。ラップアラウンド(2の補数で折り返す)する実装が多いですが、エラーや飽和演算(クランプ)にする実装もあります。

si := sint#127 + 1; // 実装依存: -128(ラップ)またはエラー
ui := usint#255 + 1; // 実装依存: 0(ラップ)またはエラー
型変換

整数型同士の変換には型変換ファンクション(int_to_dintなど)を使用します。小さい型から大きい型への変換(ウィデニング)は値が保たれますが、大きい型から小さい型(ナローイング)では上位ビットが切り捨てられます。

di := int_to_dint(-5);    // -5(符号拡張)
si := dint_to_sint(200);  // 実装依存: -56(ラップ)またはエラー(200 > 127)
ui := int_to_uint(-1);    // 実装依存: 65535(ラップ)またはエラー
rv := dint_to_real(12345); // 12345.0
iv := real_to_int(3.7);   // 4(IEC 60559 に従い最近偶数丸め: 3.7 は 4 に丸め)
iv := real_to_int(2.5);   // 2(最近偶数丸め: 2.5 は偶数の 2 に丸め、3 にはならない)
iv := trunc_int(3.7);     // 3(trunc はゼロ方向への切り捨て)

実数型

実数型は浮動小数点数を表現する型です。IEC 60559(IEEE 754と同等)規格に準拠します。単精度浮動小数点型real(32ビット、有効桁数約7桁)と倍精度浮動小数点型lreal(64ビット、有効桁数約15〜16桁)があります。精度が重要な演算(制御計算、誤差が蓄積する繰り返し演算など)にはlrealの使用を推奨します。

実数型の一覧
データ型ビット幅有効桁数範囲(絶対値)デフォルト初期値
real32約7桁±1.175494351e-38 〜 ±3.402823466e+380.0
lreal64約15〜16桁±2.2250738585072014e-308 〜 ±1.7976931348623157e+3080.0
リテラル

実数リテラルは小数点表記(e.g. 3.14)または指数表記(e.g. 1.5e3)で記述します。型指定形式(real#/lreal#)も使用できます。整数リテラルを実数型変数へ代入することも有効です。

rv := 3.1415926535; // 3.1415926535
rv := 1.5e3;        // 1500.0 = 1.5 × 10^3
rv := -2.5e-2;      // -0.025 = -2.5 × 10^-2
rv := real#0.1;     // 型指定形式
lrv := lreal#3.14;  // 型指定形式
lrv := 5;           // 整数リテラルも代入可能(5.0に変換)
演算

実数型には加算・減算・乗算・除算・べき乗の算術演算が使用できます。また、標準POUの数学ファンクションsqrtsincostanexplnなど)も使用できます。

lrv := sqrt(2.0);                       // 1.4142135623730951
lrv := sin(3.141592653589793 / 2.0);    // 1.0
lrv := exp(1.0);                        // 2.718281828459045
lrv := ln(2.718281828459045);           // 1.0
lrv := 1.0 / 3.0 * 3.0;                 // lrealは高精度で1.0に近い値
rv  := 1.0 / 3.0 * 3.0;                 // realは精度が低いため誤差が出る場合あり
型変換

整数型との変換には型変換ファンクションを使用します。浮動小数点から整数への変換では、最も近い整数に丸めます。ちょうど中間の値の場合は最も近い偶数に丸めます。

rv  := int_to_real(42);     // 42.0
lrv := dint_to_lreal(-5);   // -5.0
iv1  := real_to_int(3.7);   // 4
iv2  := real_to_int(-2.3);  // -2
iv3  := real_to_int(2.5);   // 2
lrv := real_to_lreal(rv);   // 精度を高める変換
rv2 := lreal_to_real(lrv);  // 精度が落ちる可能性あり
注意事項

NaN(非数)や無限大(Infinity)の扱いは実装依存です。例えば、0.0による除算の結果や負の数に対するsqrtについて、エラーになるか特殊な値を返すかはベンダーにより異なります。

ビット列型

ビット列型は、コンピュータサイエンスにおけるビット列(0と1のみから構成されるデータの列)を表現する型です。

表現できるビット数の違いにより、8ビットのbyte型、16ビットのword型、32ビットのdword型、および64ビットのlword型の4つのビット列型があります。例えば、word型は、2#0000_0000_0000_0000から2#1111_1111_1111_1111までの65536(216)パターンのビット列を表現できます。

ビット列型の一覧
データ型ビット幅範囲デフォルト初期値
byte816#00 〜 16#FF16#00
word1616#0000 〜 16#FFFF16#0000
dword3216#00000000 〜 16#FFFFFFFF16#00000000
lword6416#0000000000000000 〜 16#FFFFFFFFFFFFFFFF16#0000000000000000

プログラムコードにおいて、ビット列値は、非負の整数リテラルを使用します。ビット列型固有のリテラルはありません。

wordv := 2#1010_0101_0000_1111;
dwordv := 16#A50F;
wordv := 10; // 16#000A
bytev := byte#10; // 16#0A

次の例にも示す負の整数リテラルの代入は、不正なプログラムコードです。負の整数リテラルがシステムにより整数値として解釈され、整数からビット列への暗黙の型変換がないためです。

dwordv := -1; // error

以下は、ビット列型の要素に固有の演算です。これらの演算が整数型の要素には用意されていないことに注意してください。

特にバイナリ転写は実数型のフォーマットであるIEEE 754のビット列を扱うことができます。※1

lreal_to_lword(10.0); // lword#16#4024_0000_0000_0000
dword_to_real(16#3DCC_CCCD); // real#0.1

もちろん整数型との間で相互変換できます。負整数を2の補数で得たりすることができます。

dword_to_dint(16#FF); // 255
dword_to_dint(16#FFFF_FFFF); // -1
dword_to_udint(16#FFFF_FFFF); // 4294967295; 2^32-1
dint_to_dword(-1); // dword#16#FFFF_FFFF

※1 執筆時点では、本規定に従う実装をしているベンダーは、Phoenix Contact社のPLCnext Engineerのみかも知れません。他のベンダーの場合、上記例では、整数変換され、lwordv=16#Arealv=1.036831936e+9を得ます。

bool型

bool型は、真(true)または偽(false)の2値を表現する型です。条件分岐、繰り返し制御、比較演算の結果、インタロック条件など、制御プログラムの多くの場面で使用します。

bool型のデフォルト初期値はfalseです。論理値の保持に特化した型であり、整数型・ビット列型・実数型とは区別されます。

bool型の一覧
データ型ビット幅値域デフォルト初期値
bool1false / truefalse
リテラル

bool型のリテラルには、キーワード形式(true/false)と型指定形式(bool#1, bool#0)があります。

boolv := true;
boolv := false;
boolv := bool#1; // true
boolv := bool#0; // false
演算

bool型には、主に論理演算と比較演算が使用されます。

ready := sensor_ok and (not alarm);
start := manual_start or auto_start;
changed := req xor prev_req;
same := (mode_a = mode_b);
different := (mode_a <> mode_b);
型変換

bool型は、型変換ファンクションを使用して数値型などに変換できます。逆変換のサポート可否は、実装依存です。

iv := bool_to_int(true);    // 1
iv := bool_to_int(false);   // 0
flag := int_to_bool(0);     // 実装依存
flag := int_to_bool(5);     // 実装依存

0以外の整数をtrueとするかどうか、また実数型(e.g. -1, NaN, Infinity)や文字列型(e.g. "TRUE", "true")との変換規則の詳細は実装依存です。

用途

bool型は、比較演算の結果を受け取り、if文やwhile文の条件として使用するのが基本です。

program Main
	var
		temperature: lreal;
		high_temp: bool;
		fan_on: bool;
	end_var
	//
	high_temp := (temperature >= 60.0);
	if high_temp then
		fan_on := true;
	else
		fan_on := false;
	end_if;
end_program

持続時間型

持続時間型は、時間の長さ(持続時間; duration)を表現する型です。

time型とltime型の2つのデータ型があります。time型のビット幅・範囲・最小単位・精度が実装依存であるため、特別な理由がない限り、ltime型の使用を推奨します。ltime型のビット幅は64ビットであり、符号あり整数で表現されるナノ秒精度の値を持ち、約585年間を表現できます。

持続時間型の一覧
データ型ビット幅範囲最小単位デフォルト初期値
time実装依存実装依存実装依存0秒
ltime64-9223372036854775808ナノ秒 ~ 9223372036854775807ナノ秒ナノ秒0秒
リテラル

持続時間リテラルは、プレフィックス(time#/t#/ltime#/lt#)に続いて、日(d)、時(h)、分(m)、秒(s)、ミリ秒(ms)、マイクロ秒(us)、ナノ秒(ns)の単位を指定する構成です。

ltimev := ltime#1d2h3m4s5ms6us7ns; // 1日2時間3分4秒5ミリ秒6マイクロ秒7ナノ秒
ltimev := lt#500ms; // 500ミリ秒
ltimev := lt#1m30s; // 1分30秒
ltimev := lt#0s; // 0秒
timev := time#5s; // 5秒
timev := t#100ms; // 100ミリ秒

各単位の値には、小数も使用できます。

ltimev := lt#1.5s; // 1.5秒 = lt#1s500ms
ltimev := lt#1.234567ms; // 1.234567ミリ秒 = lt#1ms234us567ns
ltimev := lt#0.001s; // 0.001秒 = lt#1ms
ltimev := lt#1.5s200ms; // 1.7秒 = lt#1s700ms

負の持続時間は、プレフィックスに続けて負符号を記述します。

ltimev := lt#-500ms; // -500ミリ秒
ltimev := lt#-1d12h; // -1日12時間

アンダースコアによる桁区切りは、他のリテラルと同様に使用できます。

ltimev := lt#1d2h3m4s5ms6us7ns; // 桁区切りなし
ltimev := lt#1d_2h_3m_4s_5ms_6us_7ns; // 単位間にアンダースコア
演算

持続時間型の値には、以下の演算が定義されています。

// 加算・減算
ltimev := lt#1s + lt#500ms; // lt#1s500ms
ltimev := lt#3s - lt#1s; // lt#2s
ltimev := lt#10s + lt#-3s; // lt#7s

// 数値との乗算・除算
ltimev := lt#1s * 3; // lt#3s
ltimev := lt#100ms * 3.2; // lt#320ms
ltimev := 3 * lt#100ms; // 実装依存
ltimev := lt#10s / 2; // lt#5s

// 比較
boolv := lt#1s > lt#500ms; // true
boolv := lt#1s = lt#1000ms; // true
boolv := lt#500ms <= lt#1s; // true

なお、無次元の数値となる持続時間同士の除算は定義されていません。標準ファンクションとしては、 add/add_time/add_ltime, sub/sub_time/sub_ltime, mul/mul_time/mul_ltime, div/div_time/div_ltime が用意されています。

用途

持続時間型は、主にタイマファンクションブロック(tp, ton, tof)のプリセット時間や経過時間で使用されます。

program Main
	var
		timer: ton; // オンディレイタイマ
		start: bool;
		elapsed: time;
	end_var
	//
	timer(in := start, pt := time#5s); // 5秒のタイマ
	elapsed := timer.et; // 経過時間を取得
	if timer.q then
		// 5秒経過後の処理
		;
	end_if;
end_program

日付型・時刻型・日付時刻型

日付型・時刻型・日付時刻型は、時間軸上の点を表現する型です。カレンダー上の日付を表す日付型(date, ldate型)、1日のうちの時刻を表す時刻型(tod, ltod型)、およびそれらを組み合わせた日付時刻型(dt, ldt型)の3種類があります。

各種類にビット幅・範囲・精度が実装依存のデータ型と、64ビット・ナノ秒精度のデータ型があります。持続時間型と同様に、特別な理由がない限り、64ビットのデータ型(ldate, ltod, ldt)の使用を推奨します。

日付型・時刻型・日付時刻型の一覧
種類データ型ビット幅範囲最小単位デフォルト初期値
日付型date実装依存実装依存実装依存実装依存
ldate641970年1月1日 ~ 実装依存1日1970年1月1日
時刻型time_of_day (tod)実装依存実装依存実装依存実装依存
ltime_of_day (ltod)640時0分0秒~23時59分59.999999999秒ナノ秒0時0分0秒
日付時刻型date_and_time (dt)実装依存実装依存実装依存実装依存
ldate_and_time (ldt)641970年1月1日0時0分0秒 ~ 実装依存ナノ秒1970年1月1日0時0分0秒

ldate, ldtの上限の値は、実装依存です。ldt型は64ビット符号付き整数でナノ秒を表現するため、起点の1970年1月1日0時0分0秒に263-1ナノ秒(≒ 292年)を加算した値は、2262年4月付近となりますが、実際の上限は実装に依存します。

リテラル

日付リテラルは、プレフィックス(date#/d#/ldate#/ld#)に続いて、年-月-日を指定します。

ldatev := ldate#2026-3-14; // 2026年3月14日
ldatev := ld#1970-01-01; // 1970年1月1日(エポック)
datev := date#2026-03-14;
datev := d#2026-3-14;

時刻リテラルは、プレフィックス(time_of_day#/tod#/ltime_of_day#/ltod#)に続いて、時:分:秒を指定します。ltime_of_day型/ltod型では、秒の小数部でナノ秒精度まで表現できます。

ltodv := ltod#14:30:00; // 14時30分0秒
ltodv := ltod#23:59:59.999_999_999; // 23時59分59.999999999秒
todv := tod#8:0:0; // 8時0分0秒
todv := time_of_day#23:59:59;

日付時刻リテラルは、プレフィックス(date_and_time#/dt#/ldate_and_time#/ldt#)に続いて、年-月-日-時:分:秒を指定します。

ldtv := ldt#2026-3-14-14:30:00; // 2026年3月14日14時30分0秒
ldtv := ldt#2022-11-07-23:59:59.999_999_999;
dtv := dt#1970-01-01-0:0:0;
dtv := date_and_time#2026-3-14-8:0:0;

グレゴリオ暦で存在しない日付(e.g. 2月30日)を異常と扱ったり、閏年(e.g. 2020年2月29日)や閏秒(e.g. 23:59:60)を許すかどうかは実装依存です。

演算

日付型・時刻型・日付時刻型の値には、以下の演算が定義されています。

// 時刻に持続時間を加算・減算
ltodv := ltod#14:00:00 + lt#1h30m; // ltod#15:30:00
ltodv := ltod#14:00:00 - lt#30m; // ltod#13:30:00

// 日付時刻に持続時間を加算・減算
ldtv := ldt#2026-3-14-12:0:0 + lt#1d; // ldt#2026-3-15-12:0:0
ldtv := ldt#2026-3-14-12:0:0 - lt#12h; // ldt#2026-3-14-0:0:0

// 同型同士の減算(結果は持続時間)
ltimev := ltod#15:00:00 - ltod#14:00:00; // lt#1h
ltimev := ldt#2026-3-15-0:0:0 - ldt#2026-3-14-0:0:0; // lt#1d
ltimev := ld#2026-3-15 - ld#2026-3-14; // lt#1d

// 比較
boolv := ltod#15:00:00 > ltod#14:00:00; // true
boolv := ld#2026-3-14 = ld#2026-3-14; // true

標準ファンクションとして、 add_tod_time/add_ltod_ltime, add_dt_time/add_ldt_ltime, sub_tod_time/sub_ltod_ltime, sub_tod_tod/sub_ltod_ltod, sub_dt_time/sub_ldt_ltime, sub_dt_dt/sub_ldt_ldt, sub_date_date/sub_ldate_ldate が用意されています。

結合・分解

日付と時刻から日付時刻を結合したり、日付時刻を日付と時刻に分解するための標準ファンクションが用意されています。

// 日付と時刻から日付時刻を結合
ldtv := concat_date_ltod(
	date_part := ld#2026-3-14,
	tod_part := ltod#14:30:00); // ldt#2026-3-14-14:30:00

// 年・月・日から日付を結合
ldatev := concat_date(
	year := 2026,
	month := 3,
	day := 14); // ld#2026-3-14

// 時・分・秒から時刻を結合
ltodv := concat_ltod(
	hour := 14,
	minute := 30,
	second := 0); // ltod#14:30:00

// 日付時刻を分解
split_ldt(
	in := ldt#2026-3-14-14:30:00,
	date_part => ldatev, // ld#2026-3-14
	tod_part => ltodv); // ltod#14:30:00

// 日付を分解
split_date(
	in := ld#2026-3-14,
	year => yearv, // 2026
	month => monthv, // 3
	day => dayv); // 14

また、day_of_weekファンクションで曜日(1=月曜日~7=日曜日)を取得できます。

weekday := day_of_week(ld#2026-3-14); // 6(土曜日)

文字列型

文字列型は、文字の列を表現する型です。string型は1文字を1バイトで表し、wstring型は1文字を2バイトで表します。文字符号化方式は、ISO/IEC 10646(日本の対応規格はJIS X 0221) のいずれかに従いますが、stringwstringともに、符号化文字集合や扱える文字の範囲は実装依存です※1。文字の最大個数をstring[100]のように指定できます。省略時の最大長は実装依存です。デフォルト初期値はどちらも空文字列です。

var
	strv: string[10] := 'xyz'; // 最大10文字のstring型、'xyz'で初期化
	wstrv: wstring[10] := "xyz"; // 最大10文字のwstring型、"xyz"で初期化
	emptystr: string[10]; // 初期値省略 -> 空文字列
end_var
//
strv := 'abc';
wstrv := "abc";

括弧内の数字は格納可能な文字数です※2。省略したときの格納可能な文字数は実装依存です。

文字列型の一覧
データ型1文字のバイト数符号化文字集合文字符号化方式最大長デフォルト初期値
string1実装依存; ISO/IEC 10646の符号化文字集合の部分実装依存; UTF-8?実装依存''(空文字列)
wstring2実装依存; ISO/IEC 10646の符号化文字集合の部分実装依存; UTF-16?実装依存""(空文字列)
リテラル

string型のリテラルはシングルクォート(')、wstring型はダブルクォート(")で囲みます。特殊文字はドル記号($)によるエスケープシーケンスで表現します。型指定形式(string#'abc'/wstring#"abc")も使用できます。

sv := 'hello';              // 通常の文字列
sv := 'It$'s a string.';    // $' はシングルクォート
sv := 'Line1$NLine2';       // $N は改行(実装依存; 多くはLF ※3)
sv := 'Tab$Tchar';          // $T はタブ
sv := '$41$42$43';          // 'ABC' (16進数文字コード)
sv := '$$';                 // $ 一文字
sv := string#'abc';         // 型指定形式
wv := "ワイド文字列";        // wstring リテラル
wv := "$0041$0042";         // "AB" (4桁16進数文字コード)
wv := wstring#"abc";        // 型指定形式
エスケープシーケンス

文字列リテラル内でドル記号($)を使ったエスケープシーケンスにより特殊文字を表現できます。

エスケープシーケンスの一覧
表記意味文字コード
$$ドル記号 $16#24
$'シングルクォーテーション '16#27
$"ダブルクォーテーション "16#22
$l/$Lラインフィード LF16#0A
$n/$N改行実装依存※3
$p/$Pページ送り FF16#0C
$r/$R復帰 CR16#0D
$t/$T水平タブ HT16#09
$hh2桁16進数で表した文字コードの文字(string型リテラル内のみ)例: $41 → A
$hhhh4桁16進数で表した文字コードの文字(wstring型リテラル内のみ)例: $0041 → A
主なファンクション

文字列操作には標準ファンクションを使用します。文字の位置を指定する引数(pos)は1始まりです。これはC言語など多くの言語の0始まりとは異なる点に注意してください。例えば、find('abcde', 'cd')3を返します。

sv := 'hello, world';
len := len(sv);                 // 12
sv2 := concat('foo', 'bar');    // 'foobar'
sv2 := left(sv, 5);             // 'hello'
sv2 := right(sv, 5);            // 'world'
sv2 := mid(sv, 5, 8);           // 'world'
pos := find(sv, 'world');       // 8
型変換

文字列型と数値型の変換には型変換ファンクション(int_to_stringstring_to_intなど)を使用します。変換できない文字列を数値に変換した場合の動作は実装依存です。

sv := int_to_string(42);         // '42'
sv := lreal_to_string(3.14);     // '3.14'(書式は実装依存)
iv := string_to_int('123');      // 123
rv := string_to_real('3.14');    // 3.14
注意事項

最大長を超えた文字列を代入した場合の動作(切り捨てかエラーか)は実装依存です。また、stringwstringは互いに暗黙の変換がなく、異なる型として扱われます。マルチバイト文字(日本語など)を含む場合、lenが返すのはバイト数ではなく文字数であることにも注意してください。移植性を考慮するのであれば、wstringについてもASCII範囲で使用するのが無難です。

※1 char型およびstring型の移植性が保証されるのはASCII範囲のみと想定されます。wchar型およびwstring型については、より広い範囲の文字を使用できますが、サロゲートペアなども含めて実装依存です。

※2 C言語のstrlenなどと同様に、string[n]と宣言した場合にNUL終端分として実際にはn+1バイトを確保するベンダーがあります。wstring型の場合は2n+2バイトです。at指定によるメモリマッピングや構造体のメモリレイアウトを扱う際に注意が必要です。

※3 多くの実装では$nはLF(16#0A)ですが、改行コードがシステムにより異なる場合は実装依存となります。

変数の初期値

変数宣言時に:= 初期値で初期値を指定できます。省略した場合はそのデータ型のデフォルト初期値が適用されます(整数型は0、bool型はfalse、実数型は0.0、ビット列型は16#0、string型は空文字列など)。

var
	i: int := 10;             // 初期値 10
	f: lreal := 1.5;          // 初期値 1.5
	b: bool := true;          // 初期値 true
	s: string := 'ok';        // 初期値 'ok'
	n: int;                   // デフォルト初期値 0
	t: ltime := lt#500ms;     // 初期値 500ミリ秒
	c: Color := Color#eGreen; // ユーザー定義型も初期値指定可
end_var

内部変数(var)および一時変数(var_temp)の初期値が評価されるタイミングは実装依存です。コールドスタート時(システム初期起動時)にのみ評価される場合と、POU定義の変更後に反映される場合があります。

retain属性を持つ変数の初期値は、コールドスタート(初回起動または初期化リセット)時のみ適用されます。ウォームスタート(電源再投入)時は保持された値が使用され、初期値は上書きされません。

var retain
	total: dint := 0; // 初回起動時のみ0に初期化。電源再投入後は前回値を保持。
end_var

変数の種類

IEC 61131-3の変数はその用途と宣言場所によって種類が分かれます。主な変数種別として、内部変数(var)、一時変数(var_temp)、入力変数(var_input)、出力変数(var_output)、入出力変数(var_in_out)、外部変数(var_external)、グローバル変数(var_global)があります。

内部変数

内部変数(var ~ end_var)は、POUのローカル変数です。POU内でのみ参照・変更でき、呼び出しをまたいで値を保持します(静的変数)。ファンクションブロック(FB)・クラス・プログラムPOUで宣言でき、ファンクションでも使用できます。ファンクション内で宣言する場合、値を保持することはありません。

外部から直接参照・変更できないため、カプセル化に寄与します。ただしアクセス修飾子(publicなど)を指定することで外部からのアクセスを許可することも可能です(詳細はアクセス修飾子を参照)。

function_block Counter
	var
		count: int := 0; // 呼び出しをまたいで値を保持(静的変数)
	end_var
	count := count + 1;
end_function_block

program Main
	var c: Counter; end_var
	c(); // count = 1
	c(); // count = 2
	c(); // count = 3
end_program

一時変数

一時変数(var_temp ~ end_var)は、POU実行中のみ有効な変数です。内部変数(var)と異なり、呼び出し間で値を保持しません。呼び出しのたびにデフォルト初期値(または宣言時指定値)に初期化されます。中間計算の一時バッファとして使用します。

なお、ファンクションの局所変数はすべて一時変数と同等の動作(呼び出しをまたいで値を保持しない)が求められます。

function_block Compute
	var
		result: dint := 0; // 呼び出しをまたいで保持(静的)
	end_var
	var_temp
		tmp1: dint;        // 毎回初期化(非静的)
		tmp2: dint;
	end_var
	tmp1 := result * 2;    // 一時的な中間値
	tmp2 := tmp1 + 1;
	result := tmp2;
end_function_block

入力変数・出力変数

入力変数(var_input ~ end_var)はPOUへの入力パラメータ、出力変数(var_output ~ end_var)はPOUからの出力パラメータです。入力変数はPOU内では読み取り専用です。書き換え時はコンパイルエラーまたは実装依存の動作です。出力変数は呼び出し側から書き込めません。

フォーマルな呼び出しでは、入力変数は:=で値を渡し、出力変数は=>で受け取ります。省略した入力変数にはその変数の初期値が使用されます。FBの場合は前回呼び出し時の値が保持されます。

function_block PIDController
	var_input
		setpoint: lreal;
		process_value: lreal;
		kp: lreal := 1.0; // 省略時のデフォルト値
	end_var
	var_output
		output: lreal;
		error: lreal;
	end_var
	var
		prev_error: lreal := 0.0;
	end_var
	error := setpoint - process_value;
	output := kp * error;
	prev_error := error;
end_function_block

program Main
	var
		pid: PIDController;
		ctrl_out: lreal;
		ctrl_err: lreal;
	end_var
	// フォーマルな呼び出し(出力を受け取る)
	pid(setpoint := 100.0, process_value := 80.0, output => ctrl_out, error => ctrl_err);
	// ctrl_out = 20.0, ctrl_err = 20.0

	// インフォーマルな呼び出し(出力変数を受け取る場合はフォーマルが必要)
	pid(100.0, 80.0);
	ctrl_out := pid.output; // インスタンスのメンバアクセスでも取得可能(FBのみ)
end_program

入出力変数

入出力変数(var_in_out ~ end_var)は、呼び出し側の変数を参照渡しで受け取るパラメータです。POU内での変更が呼び出し側の変数に反映されます。大きなデータ(配列・構造体など)の受け渡しにも有効です。

入出力変数に渡せるのは変数のみです。定数リテラル・式は渡せません。呼び出し記法はフォーマル(仮引数名 := 変数名)またはインフォーマル(位置引数)どちらも使用できます。

function swap
	var_in_out
		a: int;
		b: int;
	end_var
	var_temp t: int; end_var
	t := a; a := b; b := t;
end_function

function scale_array
	var_in_out
		arr: array[0..9] of lreal; // 配列を参照渡し(コピーなし)
	end_var
	var_input factor: lreal; end_var
	var_temp i: int; end_var
	for i := 0 to 9 do
		arr[i] := arr[i] * factor;
	end_for;
end_function

program Main
	var
		x: int := 3;
		y: int := 7;
		data: array[0..9] of lreal := [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
	end_var
	swap(a := x, b := y);        // x=7, y=3
	scale_array(arr := data, factor := 2.0); // data の各要素が2倍に
	// swap(5, y); // Compile-time error! リテラルは渡せない
end_program

外部変数

外部変数は、他のモジュールに定義されるグローバル変数を参照するために使用します。C言語におけるextern変数に相当します。

グローバル変数の用途の一つに、複数のPOU間で共有するデータがあります。あるPOUにて同じ名前のローカル変数定義があるときにエラーになるのと同じように、あるユーザーのシステム全体にて同じ名前のグローバル変数定義があるときにもエラーになります。複数POU間で共有するデータとして使用するグローバル変数をエラーにならないようにシステム全体のどこで定義し、どうPOUで使用するのか、解決策や指針が必要です。

IEC 61131-3におけるグローバル変数は、configuration、又はresourceにのみ定義することができます。POU内においては、どのグローバル変数を使用するかを特定する必要があり、このときに外部変数宣言を使用し、名前によってグローバル変数を特定します。

configuration Cfg
	resource Rsc on Plc
		var_global
			gintv: int;
			gboolv: bool;
		end_var
	end_resource
end_configuration
program Main
	var_external
		gintv: int;
	end_var
	//
	gintv := 2; // Rscのグローバル変数gintvへの書き込み
end_program

program Sub1
	var_external
		gboolv: bool;
	end_var
	//
	gboolv := true; // Rscのグローバル変数gboolvへの書き込み
	gintv := 3; // Compile time error! Unknown symbol 'gintv'.
end_program

program Sub2
	var_external
		gwordv: word; // Link time error! No global variable 'gwordv' found.
	end_var
end_program

外部変数宣言は、グローバル変数を特定するためのリンクのようなものであり、グローバル変数を定義しないことに注意が必要です。

外部変数宣言に関して、次のようなケースはエラーです。

🛈なぜ外部変数宣言のような仕組みが必要なのか補足します。C言語にてextern宣言が必要な理由は、コンパイルの概念があるからです。つまり、C言語で記述された一つのソースファイルをコンパイルしモジュール化することを可能にするために、extern宣言の構文があります。extern宣言により変数の名前やデータ型を人間がコンパイラに教えてあげることにより、コンパイラはシステム全体を知らずともコンパイルできるのです。では、IEC 61131-3にコンパイルの概念はあるのでしょうか?外部変数宣言があるので、そうしたかったのかも知れませんが、実現しているベンダー(POUに対するコンパイルというメニューがあるベンダー)は皆無と思います。これは、IEC 61131-3の機能不足に原因があると考えています。C言語では、コンパイルを実現するためにもう一つの構文があります。関数のextern宣言(関数のプロトタイプ宣言)です。これもまた、関数の戻り値のデータ型や、引数のデータ型を人間がコンパイラに教えるためにあります。IEC 61131-3においても、ファンクションのみならず全てのPOUに対するextern宣言、もしくは、それらをまとめて行うimport構文相当があって初めてコンパイルを実現できます。グローバル変数を定義しているのに外部変数宣言を再度書かなければいけない理由に納得できないのはその通りで、IEC 61131-3の機能不足に一因があります。

グローバル変数 var_global

グローバル変数(var_global~end_var)は、configurationまたはresourceに宣言し、複数のPOU間でデータを共有するために使用します。POU内からアクセスするには外部変数var_external)で対応するグローバル変数を宣言して参照します。

constant属性を組み合わせて定数グローバル変数を定義することもできます。

configuration Cfg
	resource Rsc on Plc
		var_global
			shared_count: int := 0;
			system_ready: bool := false;
		end_var
		var_global constant
			max_retry: int := 3;
			CYCLE_TIME: ltime := lt#10ms;
		end_var
		task MainTask(interval := t#10ms, priority := 1);
		program Main with MainTask: MainProg;
	end_resource
end_configuration

program MainProg
	var_external
		shared_count: int;
		system_ready: bool;
	end_var
	var_external constant
		max_retry: int;
	end_var
	if shared_count < max_retry then
		shared_count := shared_count + 1;
	end_if;
end_program

変数のその他の属性

変数宣言には、constantatr_edge/f_edgeretain/non_retainなどの追加属性を付加できます。属性は型名の後に記述します。

constant属性

constant属性を付加した変数は定数となり、宣言時の初期値から変更できません。var constant(ローカル定数)またはvar_global constant(グローバル定数)のように使用します。var_external constantはグローバル定数を外部参照する場合に使用します。

定数はマジックナンバーの排除・可読性の向上に加え、コンパイラが定数伝播・最適化を行える利点もあります。

var constant
	MAX_COUNT: int := 100;
	PI: lreal := 3.141592653589793;
	ZERO_TIME: ltime := lt#0s;
end_var

MAX_COUNT := 200; // Compile-time error!
PI := 3.0;        // Compile-time error!

次の使い方はエラーとするかは実装依存です。

var constant
	LIMIT: int; // 初期値の指定がない
end_var

function_block FB
	var_input constant
		in: int; // 入力変数にconstant属性
	end_var
end_function_block

at属性

at属性は変数をハードウェアの物理アドレス(I/Oデバイス等)に関連付けます。変数へのアクセスが、指定した物理アドレスへのアクセスになります。

アドレス構文

アドレスは%に続いてロケーション・幅・番号を組み合わせて記述します。

var
	sensor0  at %IX0.0  : bool; // 入力ビット:エリア0のビット0
	sensor7  at %IX0.7  : bool; // 入力ビット:エリア0のビット7
	output0  at %QX0.0  : bool; // 出力ビット
	analog_i at %IW2    : int;  // 入力ワード(アナログ入力など)
	analog_o at %QW3    : int;  // 出力ワード(アナログ出力など)
	mem_dint at %MD10   : dint; // 内部メモリ ダブルワード
end_var

詳細な番号体系はベンダーおよびハードウェアに依存します。constant属性、retain属性との組み合わせ可否も実装依存です。なお、var_configを使用することで、at属性の設定をPOUのソースコードから切り離し、コンフィギュレーションレベルで行うことができます(詳細はvar_configを参照)。

エッジ属性

エッジ属性は、FBインスタンス呼び出しにおけるbool型入力変数の値の変化を検出します。例えば、信号の立ち上がり検出では、オフからオンに変化したとき、又はオンからオフに変化したときだけ処理を実行したいときに有用です。

r_edge属性・f_edge属性を総称してエッジ属性といい、FBにおけるbool型入力変数に付加できます※1

r_edge属性(Rising EDGE)は、オフからオンに変化したときにオン、即ち、前回呼び出し時の実引数の値がfalseで今回呼び出し時がtrueのとき、当該入力変数の値がtrueになります。f_edge属性(Falling EDGE)は、オンからオフに変化したときにオン、即ち、前回呼び出し時の実引数の値がtrueで、今回呼び出し時がfalseのとき、当該入力変数の値がtrueになります。

function_block REdgeAttrSample
	var_input d: bool r_edge; end_var
end_function_block

function_block FEdgeAttrSample
	var_input d: bool f_edge; end_var
end_function_block

program Main
	var
		r: REdgeAttrSample;
		f: FEdgeAttrSample;
	end_var
	// r_edge変数の例
	r(true);    // d=true、初回呼び出し時にtrueの場合、r_edge変数の値はtrueです。
	r(true);    // d=false
	r(false);   // d=false
	r(false);   // d=false
	r(true);    // d=true

	// f_edge変数の例
	f(false);   // d=true、初回呼び出し時にfalseの場合、f_edge変数の値はtrueです。
	f(false);   // d=false
	f(true);    // d=false
	f(true);    // d=false
	f(false);   // d=true
end_program

※1 en変数にエッジ属性を付加して、いわゆるラダーの微分型命令を実現したいところですが、IEC 61131-3にそのような例はありません。そもそもユーザーにenの宣言を書かせるのか、書かせないのか実装依存ですので、文法的には、FBに対する属性とするのが良いのかも知れません。

保持・非保持属性

retain属性を付加した変数は、電源断・再起動(ウォームスタート)後も値を保持します。バッテリバックアップRAMや不揮発性メモリ(フラッシュ等)に保存されます。non_retain属性は保持しないことを明示し、電源再投入時に初期値に戻ります。

コールドスタート(システムの完全初期化・ダウンロード後の初回起動)時は、retain変数も含めてすべての変数が初期値に戻ります。

var retain
	production_count: dint := 0; // 電源断後も保持(ウォームスタート時は継続)
	last_error_code: int := 0;   // コールドスタート時のみ0に初期化
	alarm_history: array[0..9] of int; // 履歴を保持
end_var

var non_retain
	cycle_flag: bool;            // 電源再投入後は false に初期化
	temp_buffer: string;         // 一時データは保持不要
end_var

特別な変数

実行制御 en変数・eno変数

LD言語におけるパワーフローや、FBD言語における信号の流れによる実行制御は、IEC 61131-3の特徴の一つです。電気的に導通しているときや信号が入ったときに機能を実行することをソフトウェア的に実現する仕組みとして、en変数(ENable)・eno変数(ENable Out)があります。en・enoを使用することにより、機能の実行・非実行、即ちPOUの呼び出し・非呼び出しを制御できます。本節に示すことは、ST言語にも適用されます。

en・enoは、個々のファンクション、FBインスタンス、およびメソッド内に次のように定義される変数です。

var_input en: bool := true; end_var
var_output eno: bool; end_var

本変数をベンダーが定義しておくか、ユーザーが定義するかは実装依存ですが、どちらが定義するにしても、本変数には以下で示す特別な動作仕様があります。

重要なことは、POU呼び出し側でenがfalseであれば、そのPOUは実行されないし、呼び出し側でenoが falseと観測されれば、その呼び出し後の出力(出力変数、入出力変数、および戻り値)は意図したものではない可能性があるということです。また、上記動作仕様から、POU内部ではenの値はtrueであることが分かります。

POU内でユーザーがenoの値を書き換え可能ですが、ユーザーは上記のような動作仕様があることを踏まえておかなければいけません。

なお、ST言語において、en変数、eno変数を指定する場合、フォーマルな呼び出し、即ち仮引数指定での呼び出し記法を使用する必要があります。例えば、move(en := cond, in := src, out => dest, eno => result) のように記述します。move(src, dest) のようなインフォーマルな呼び出し記法では、enenoを指定できません。また、en変数・eno変数に対し、ref演算を使用することはできません。

⚠️en変数・eno変数に関する本規定は有用なものですが、難解さと実装の選択肢の多さにより致命的なポータビリティ低下を招く規定の一つです。筆者個人としては、他ベンダーからの移行可能性を高めるためにも、ベンダーは次に示すユーザー向け仕様を維持する実装が望ましいと考えます。

戻り値変数

ファンクションやメソッドを呼び出した結果の戻り値は、戻り値変数の値で決まります。

戻り値変数は、ファンクションやメソッド内に暗黙的に定義されるローカル変数です。

function is_even_num: bool
	var_input in: int; end_var
	//
	is_even_num := (in mod 2) = 0;
end_function

program Main
	var r: bool; end_var
	//
	r := is_even_num(3); // r=false
	r := is_even_num(6); // r=true
end_program

戻り値のないファンクションで、戻り値変数が暗黙的に定義されるか否かは、実装依存です。

function no_return_function
	var_input in: int; end_var
	...
	no_return_function := false; // ?
end_function

ユーザー定義データ型

ユーザー定義データ型は、type ~ end_typeブロックで宣言します。列挙型、構造体型、範囲型、参照型、配列型などを定義できます。ユーザー定義型を使用することで、プログラムの可読性・保守性が向上します。

列挙型

bool型と宣言した変数は真と偽の2つの値しか取れないことからも分かる通り、データ型は変数がとり得る値の集合を制約する特性があります。列挙型は、その値の集合をユーザー自身が定義できるデータ型です。例えば、次のプログラムは、eRed, eGreen, eBlueの3つの値しか取れないColor列挙型、eSun, eMon, eTue, eWed, eThu, eFri, eSatの7つの値しか取れないDay列挙型を宣言しています。列挙型を使用することにより、マジックナンバーを使用するよりも可読性が高くなり、変数がとり得る値をシステムが保証することができるなどのメリットがあります。

type
	// 列挙型Colorの宣言、デフォルト初期値: eRed。
	Color: (eRed, eGreen, eBlue);
	// 列挙型Day(曜日)の宣言、デフォルト初期値: eMon。
	Day: (eSun, eMon, eTue, eWed, eThu, eFri, eSat) := eMon;
end_type;

program Main
	var
		colorv: Color; // 初期値は Color#eRed です。
		dayv: Day := Day#eMon;
	end_var
	//
	colorv := Color#eGreen;
	colorv := 2; // Error!
	colorv := Day#eSun; // Error!
end_program

上記Dayの宣言において、eMonをデフォルト初期値として宣言しています。デフォルト初期値を指定しないとき、デフォルト初期値は、列挙型宣言において最初に登場する値です。上記プログラムにおけるColor列挙型のデフォルト初期値は、Color#eRedとなります。

Color#eRedDay#eSatなどの列挙型の値を列挙子といいます。列挙子のプログラム上の表現は、データ型名#列挙子です。ただし、列挙子のスコープは、その列挙子のデータ型と同じスコープをもつため、列挙型#を省略してもエラーとはなりません。一方、列挙子のこの広いスコープは、他の要素のスコープと重複する可能性を高めます。本書のプログラム例でも示す通り、列挙子名は接頭辞eを付加するなど、他と重複し難い命名規則にすることを推奨します。また、列挙型同士の重複も回避するため、列挙型#を省略しないことを推奨します。

type
	Color: (eRed, eGreen, eBlue);
	Signal: (eBlue, eYellow, eRed);
end_type;

program Main
	var
		colorv: Color;
	end_var
	//
	colorv := eGreen; // OK. colorv = Color#eGreen
	colorv := eRed; // Error! Color#eRedなのか、Signal#eRedなのか区別できない。
end_program

コンパイラや実行エンジンは、列挙型の値の実体を整数値で実装することも多く、IEC 61131-3においても列挙子に対し、整数値を設定する構文が存在します。実体の値を指定する列挙型は、単なる列挙型とは区別され、値付き列挙型といいます。

列挙型に定義できる列挙値の最大個数は実装依存です。

値付き列挙型

列挙子に定数を明示的に割り当てた列挙型です。単なる列挙型と値付き列挙型を混在させることはできません。

値付き列挙型は通信プロトコルや外部機器との整数コードとのマッピング、または既存の整数定数との互換維持に有用です。

type
	// 値を明示した列挙型
	ErrorCode: (
		eNone    := 0,
		eTimeout := 10,
		eOverrun := 11, // 直前 +1 で 11
		eComm    := 20,
		eUnknown := 99
	);

	// 一部のみ値を指定(省略分は直前 +1)
	Priority: (
		eLow    := 1,
		eMedium,  // 2
		eHigh,    // 3
		eCritical := 10
	);
end_type;

program Main
	var
		err: ErrorCode := ErrorCode#eNone;
		prio: Priority;
		code: int;
	end_var
	err := ErrorCode#eTimeout;
	code := ErrorCode_to_int(err); // 10
	prio := Priority#eHigh;
	code := Priority_to_int(prio); // 3
end_program

構造体型

構造体型は、複数のメンバ変数をひとつの型としてまとめたユーザー定義型です。struct ~ end_structで定義し、メンバには基本データ型・列挙型・他の構造体型・配列など、あらゆる型を使用できます。メンバへのアクセスはドット演算子(.)を使用します。

構造体型の変数は代入演算子(:=)で全メンバをまとめてコピーできます。比較演算子は使用できません(等価判定はメンバごとに行う必要があります)。

type
	Point: struct
		x: lreal;
		y: lreal;
	end_struct;

	Rect: struct
		top_left: Point;
		width: lreal;
		height: lreal;
	end_struct;

	AlarmRecord: struct
		code: int;
		message: string[64];
		timestamp: ldt;
		active: bool;
	end_struct;
end_type;

program Main
	var
		pt: Point;
		rc: Rect;
		alarm: AlarmRecord;
		pt2: Point;
	end_var
	pt.x := 1.0;
	pt.y := 2.0;
	pt2 := pt;            // 構造体の全体コピー(pt2.x=1.0, pt2.y=2.0)
	rc.top_left := pt;
	rc.width := 100.0;
	rc.height := 50.0;
	alarm.code := 42;
	alarm.message := 'Overheat detected';
	alarm.active := true;
end_program

構造体型変数の初期化

構造体型変数の初期化は、変数宣言時に構造体初期化子を使用します。

(<メンバ名1> := <初期値1>, <メンバ名2> := <初期値2>, ...)

メンバを省略した場合はそのメンバのデフォルト初期値が使用されます。ネストした構造体も同様に初期化子を入れ子にして記述できます。

type
	s_t: struct
		m0: dint;
		m1: array[0..2] of dint;
	end_struct;

	t_t: struct
		n0: dint;
		n1: s_t;
	end_struct;
end_type;

program Main
	var
		sv1: s_t := (m0 := 2, m1 := [3, 5, 7]);
		sv2: s_t := (m0 := 11); // 部分的な指定 (m0 := 2, m1 := [0, 0, 0]) と同義です。
		tv: t_t := (
			n0 := 13,
			n1 := (
				m0 := 17,
				m1 := [19, 23, 29]
			)
		);
	end_var
end_program

構造体初期化子はコード部では使用できません。以下は不正なプログラムです。

:
program Main
	var sv: s_t; end_var
	//
	sv := (m0 := 2, m1 := [3, 5, 7]); // Compile-time error.
end_program

相対位置付き構造体型

相対位置付き構造体型は、メンバのメモリ上の相対位置(オフセット)をatで明示的に指定できる構造体型です。overlap属性と合わせて全メンバのオフセットを0にすることでC言語の共用体(union)と同等の使い方ができます。

type
	// 共用体相当: dword型とbyte配列の同じメモリを共有
	DWordBytes: struct overlap
		dw at %b0: dword;
		b0 at %b0: byte;
		b1 at %b1: byte;
		b2 at %b2: byte;
		b3 at %b3: byte;
	end_struct;
end_type;

program Main
	var u: DWordBytes; end_var
	u.dw := 16#12345678;
	// u.b0 = 16#78, u.b1 = 16#56, u.b2 = 16#34, u.b3 = 16#12(リトルエンディアン)
end_program

絶対位置付き構造体型

絶対位置付き構造体型は、メンバのアドレスを物理アドレス(%I, %Q, %Mなど)で指定する構造体型です。ハードウェアのI/Oマップと変数を対応付けるときに使用します。

type
	IoMap: struct
		input0  at %IX0.0: bool;
		input1  at %IX0.1: bool;
		output0 at %QX0.0: bool;
		analog  at %IW2  : int;
	end_struct;
end_type;

program Main
	var io: IoMap; end_var
	io.output0 := io.input0 and io.input1;
end_program

範囲型

範囲型は、整数型に許容値の範囲制約を付加したユーザー定義型です。基底整数型 (下限..上限) の構文で定義します。範囲外の値を代入した場合の動作は実装依存です(コンパイルエラー、実行時エラー、クランプなど)。

範囲型は型安全性を高め、センサー値・パーセント・角度など意味的に範囲が限定された値の管理に有用です。基底型との代入の互換性は実装依存です。

type
	Percent : int (0..100);        // 0〜100%
	Angle   : int (0..359);        // 0〜359°
	AxisId  : usint (1..8);        // 軸番号1〜8
	Temperature : int (-200..800); // -200〜800℃
end_type;

program Main
	var
		pct   : Percent := 50;
		angle : Angle;
		axis  : AxisId := AxisId#1;
	end_var
	pct := 80;
	pct := 150;  // 実装依存: エラーまたはクランプ(100に)
	angle := 360; // 実装依存: エラーまたは 0 になど
	axis := AxisId#3;
end_program

参照型

参照型は、他の変数・FBインスタンス・配列要素などへの参照(エイリアス)を保持する型です。ref_to データ型で宣言します。参照先の設定にはref演算子を使用し、参照先の値へのアクセスには^演算子(デリファレンス)を使用します。参照を持たない状態はnullで表します。

参照型はファンクション・メソッドの引数としてFBインスタンスや大きな構造体を渡す場合(var_in_outの代替)や、動的なデータ構造の実現に使用します。

program Main
	var
		x: int := 10;
		y: int := 20;
		r: ref_to int;
		valid: bool;
	end_var
	// 参照の設定とnullチェック
	r := ref x;
	valid := (r <> null); // true
	r^ := 42;             // x = 42
	r := ref y;
	r^ := r^ + 1;         // y = 21
	r := null;          // 参照解除
	valid := (r <> null); // false
	// r^ := 99; // nullデリファレンスは実装依存の動作
end_program
// 参照型を使ったFBインスタンスへのポリモーフィックな参照
function process_fb
	var_input fb_ref: ref_to SomeFB; end_var
	if fb_ref <> null then
		fb_ref^();             // FBを呼び出す
		result := fb_ref^.output;
	end_if;
end_function

配列

配列は、同じ型の要素を連続して格納するデータ構造です。array[下限..上限] of 型で宣言します。添字の下限・上限は任意の整数値(負の値・0以外の値も可)を指定でき、多次元配列はカンマ区切りで宣言します。添字の範囲外アクセスの動作は実装依存です。配列の最大次元数・最大要素数・添字の最大範囲も実装依存です。

var
	a1: array[0..4] of int;             // 5要素(添字0〜4)
	a2: array[1..3, 1..3] of lreal;     // 3×3の2次元配列(添字1〜3)
	a3: array[-2..2] of bool;           // 負の添字も可(-2〜2の5要素)
	bytes: array[0..3] of byte;         // バイト配列
	history: array[0..99] of dint;      // 100要素の履歴バッファ
end_var
a1[0] := 10;
a1[4] := 50;
a2[1, 1] := 1.0;
a2[2, 3] := 6.0;
a3[-2] := true;
a1[5] := 99; // 実装依存: 範囲外アクセス(エラーまたは未定義動作)

ある次元の要素数はupper_bound(arr, <次元;1以上の値>) - lower_bound(arr, <次元;1以上の値>) + 1で取得できます。配列変数は代入演算子(:=)で全要素をまとめてコピーできます。

var
	src: array[0..4] of int := [1, 2, 3, 4, 5];
	dst: array[0..4] of int;
	len: int;
end_var
dst := src;                                    // 全要素をコピー
len := upper_bound(src, 1) - lower_bound(src, 1) + 1; // 5

ユーザー定義型として配列型を名前付きで定義することもできます。

type
	IntArray10: array[0..9] of int;
	Matrix3x3: array[1..3, 1..3] of lreal;
end_type;

var
	data: IntArray10;
	mat: Matrix3x3;
end_var

配列変数の初期化

配列変数の初期化は、変数宣言時に配列初期化子を使用します。多次元配列の場合、すべての要素を一次元的に並べて記述します。

[<初期値1>, <初期値2>, ...]
program Main
	var
		// a[3] = 2
		// a[4] = 3
		// a[5] = 5
		a1: array[3..5] of dint := [2, 3, 5];

		// a[0,0] = 16#00
		// a[0,1] = 16#01
		// a[0,2] = 16#02
		// a[1,0] = 16#10
		// a[1,1] = 16#11
		// a[1,2] = 16#12
		a2: array[0..1, 0..2] of dint := [
			16#00, 16#01, 16#02,
			16#10, 16#11, 16#12
		];

		// ※多次元配列における以下のネスト記法は、規格には示されておらず実装依存です。
		a3: array[0..1, 0..2] of dint := [
			[16#00, 16#01, 16#02],
			[16#10, 16#11, 16#12]
		];
	end_var
end_program

配列初期化子はコード部では使用できません。以下は不正なプログラムです。

program Main
	var av: array[3..5] of dint; end_var
	//
	av := [2, 3, 5]; // Compile-time error.
end_program

以下のように連続指定することができます。

type
	s_t: struct
		m0: dint;
		m1: dint;
	end_struct;
end_type

program Main
	var
		// v1[0]=0
		// :
		// v1[7]=0
		v1: array[0..7] of dint := [8(0)];
		// v2[0]=0
		// :
		// v2[4]=1
		// :
		v2: array[0..7] of dint := [4(0), 4(1)];
		// v3[0].m0=2
		// v3[0].m1=3
		// v3[1].m0=2
		// v3[1].m1=3
		// v3[2].m0=2
		// v3[2].m1=3
		v3: array[0..2] of s_t := [3((m0 := 2, m1 := 3))]; // 括弧の数に注意。
		// v4[0,0]=0
		// :
		v4: array[0..1, 0..2] of dint := [6(0)];
	end_var
end_program

可変長配列

可変長配列は、配列長が実行時に定まる変数の型です。入出力変数の型として使用できます※1。可変長配列を使用することで、あらゆる配列要素数にも対応できる汎用的なライブラリを作成することができます。

1次元の可変長配列の定義は、次のように書きます。

<変数名>: array[*] of <基底型>

多次元の可変長配列の定義は、カンマで区切って次のように書きます。

<変数名>: array[*, *, ...] of <基底型>
// lreal型配列要素の総和を求めます。
function sum: lreal
	var_in_out
		values: array[*] of lreal; // 1次元可変長配列変数の定義
	end_var
	var_temp
		i: dint;
	end_var
	//
	sum := 0.0;
	// 配列添字の始点・終点は、標準ファンクションのlower_bound、upper_boundで取得します。
	// 実引数の1は、1次元目の始点・終点を取得することを意味します。
	for i := lower_bound(values, 1) to upper_bound(values, 1) by 1 do
		sum := sum + values[i];
	end_for;
end_function

program Main
	var
		v1: array[0..4] of lreal := [10, 20, 30, 40, 50];
		v2: array[1..2] of lreal := [100, 200];
		s1, s2: lreal;
	end_var
	//
	s1 := sum(v1); // 150 (=10+20+30+40+50)
	s2 := sum(v2); // 300 (=100+200)
end_program

実行時に配列次元の始点と終点の情報を取得するには、標準ファンクションlower_boundupper_boundを使用します。

lower_boundupper_boundファンクションは、「any_int lower_bound(arr: any_array(*var_in_out*), dim: any_int(*var_input*))」のように2つの引数をとります。引数arrには、通常可変長配列を指定しますが、固定長配列を指定することもできます。引数dimには、どの次元の始点(又は終点)の値を取得するかを1以上で指定します。戻り値の正確な型は実装依存です。

program Main
	var
		a1: array[3..5] of lreal;
		a3: array[7..11, 13..17, 19..23] of lreal;
	end_var
	//
	lower_bound(a1, 1); // 3
	upper_bound(a1, 1); // 5
	//
	lower_bound(a3, 1); // 7
	upper_bound(a3, 1); // 11
	lower_bound(a3, 2); // 13
	upper_bound(a3, 2); // 17
	lower_bound(a3, 3); // 19
	upper_bound(a3, 3); // 23
end_program

※1 規格上は「可変長配列をFBの入力変数、出力変数にも適用可能」とありますが、動的メモリ確保が必要となることについての説明なく規定は不完全です。また「可変長配列をファンクションの入出力変数、およびFBの入力変数、入出力変数、出力変数のデータ型として適用可能」とちぐはぐな規定ともなっており、本書では「可変長配列を入出力変数の型として使用可能」と限定しました。

直接派生型

直接派生型は、既存の基本データ型またはユーザー定義型を基底型として新たな型名を定義する型です。型の意味を明確化し、プログラムの可読性・型安全性を高めます。

異なる直接派生型間の暗黙の型変換の可否については、実装依存です※1

type
	Meters   : lreal;       // 距離(メートル)
	Seconds  : lreal;       // 時間(秒)
	MotorId  : usint;       // モータID(1〜)
	NodeAddr : byte;        // ネットワークノードアドレス
end_type;

program Main
	var
		dist    : Meters  := 1.5;
		time_s  : Seconds := 2.0;
		speed   : lreal;
		motor   : MotorId := MotorId#1;
	end_var
	//
	speed := dist / time_s;   // 0.75(同じ基底型lrealとして演算)
	motor := MotorId#3;
	dist := time_s; // 実装依存: 型不一致エラーになる実装もある
end_program

型名に初期値を付加することで、デフォルト初期値を基底型のデフォルトから変更することもできます。

type
	AxisId : usint := 1;    // デフォルト初期値を 0 から 1 に変更
	Gain   : lreal := 1.0;  // デフォルト初期値を 0.0 から 1.0 に変更
end_type;

program Main
	var
		axis: AxisId; // 初期値は 1 です。
		gain: Gain;   // 初期値は 1.0 です。
		gain2: Gain := 3.0;   // 初期値は 3.0 です。
	end_var
end_program

※1 直接派生型は、派生型ではなく、エイリアスとせざるを得ない気はします。型Metersにlrealの値を代入できていることもその一例です。

ファンクション

ファンクションは、一般的なプログラミング言語の関数(function)と同等の機能をもつPOUです。ファンクションに引数(入力)を与えて呼び出すことにより、ファンクション内の処理が実行され、呼び出した側では、処理の結果である戻り値(出力)を得ることができます。ファンクションを適切に利用することにより、同じプログラムコードを何度も書く手間を減らし、可読性を高め、管理を容易にします。

// 戻り値のデータ型がlrealのファンクションabsmaxの定義です。
// 2つの引数のうち絶対値の大きい方を結果として得ます。
function absmax: lreal
	// absmaxは、lreal型のinput1とinput2の2つの引数をもちます。
	var_input
		input1: lreal;
		input2: lreal;
	end_var
	//
	// ファンクション名と同じ変数(戻り値変数)の値を、呼び出し側で戻り値として得ることができます。
	if abs(input1) > abs(input2) then
		absmax := input1;
	else
		absmax := input2;
	end_if;
end_function

program Main
	var
		y1, y2: lreal;
	end_var
	//
	// ファンクションabsmaxの呼び出しです。
	// input1に2、input2に3を設定し、absmaxの処理が実行され、y1に3が代入されます。
	y1 := absmax(input1 := 2.0, input2 := 3.0);
	// input1に-7、input2に5を設定し、absmaxの処理が実行され、y2に-7が代入されます。
	y2 := absmax(-7.0, 5.0);
end_program
// 二つのint型変数の値を入れ替えるファンクションです。戻り値はありません。
function swap_INT
	// swap_INTは、in1とin2の2つの引数をもちます。
	var_in_out
		in1, in2: int;
	end_var
	var_temp
		t: int;
	end_var
	//
	t := in1;
	in1 := in2;
	in2 := t;
end_function

program Main
	var
		x1: int := 2;
		x2: int := 3;
	end_var
	// x1=2, x2=3
	swap_INT(x1, x2);
	// x1=3, x2=2
end_program

IEC 61131-3の関数の特徴

一般的なプログラミング言語の関数との違いや特徴を以下に示します。

ファンクション定義の記述方法

ファンクション定義を一般的に次のように記述できます。

function <ファンクション名>:<戻り値のデータ型>
	<引数変数の宣言>
	<変数の宣言>
	<処理>
end_function

<ファンクション名>は、ファンクションを呼び出すときに使用する名前です。この名前を使って、プログラムコードの別の場所からファンクション内の処理を実行することができます。ファンクション内では、ファンクションの実行結果である戻り値を意味する変数の名前にもなります。

<戻り値のデータ型>は、ファンクションの戻り値のデータ型です。コロン(:)以降を省略する場合、戻り値のないファンクションとなります。また、配列の戻り値とする場合、配列型の直接派生型での指定が必要になります※1

<引数変数の宣言>には、以下に示す3つの引数変数を宣言することができます。

<変数の宣言>には、ファンクション内で使用する変数を宣言します。内部変数(キーワードvar)、一時変数(var_temp)、および外部変数(var_external)※2を宣言できます。ファンクションにおいては、内部変数と一時変数の違いはありません※3

<処理>には、ファンクションが実行する処理(プログラムコード)を記述します。制御文を記述できます。<ファンクション名>を名前とする戻り値変数に値を代入しておくことで、呼び出し側でその値を受け取ることができます。<処理>の終端に達するか、return文が実行されると、呼び出し元に制御が戻ります。なお、自分自身を呼び出す再帰呼び出しの可否については実装依存です※4

※1 function fun: array[0..2] of lreal ~ end_functionのような配列戻り値の構文サポートについては実装依存です。
※2 入力が同じであれば、同じ出力を返す関数を一般的に純関数といいます。純関数には、関数外へ副作用がないことに起因する様々なメリットがあります。一方、外部変数宣言を含むファンクションは、ファンクション外部の環境に依存したり影響を与えるため、純関数となりません。
※3 IEC 61131-3は、ファンクション内でのインスタンス変数の定義を禁止しています。インスタンスは状態を持つから、というのが理由のようですが、基本レベルの汎用的なFBやクラスであれば、普通に使いたくなるものです。ベンダーは必ずしも本制約に従う必要はないと考えます。
※4 再帰呼び出しを制限する主な理由は、実行系において、必要量が予測困難で動的なメモリ確保を必要とするためです。

ファンクション呼び出しの記述方法

ファンクション呼び出しの記述方法には、仮引数を記述するフォーマル呼び出しと、仮引数を省略するインフォーマル呼び出しがあります。インフォーマル呼び出しでは、すべての引数の値を記述しないといけないのに対し、フォーマル呼び出しでは、一部又は全ての引数の記述を省略できます。省略した引数の値は、当該変数の初期値やデータ型のデフォルト値が処理系により設定されています。

フォーマル呼び出し

ファンクションのフォーマル呼び出しは、一般的に次のように記述できます。

<ファンクション名>(<仮引数名1> := <実引数値1>, <仮引数名2> := <実引数値2>, ...)

例:

y := absmax(input1 := 13, input2 := -17); // -17
move(in := 3, out => intv); // intvは3。出力変数の演算子には、=>を使用します。

フォーマル呼び出しにおいて、入力変数と出力変数の引数を省略することができます。省略する場合、変数の値は、変数宣言時に指定した初期値、又は初期値を指定していなければ、データ型のデフォルト値です。入出力変数の引数を省略することはできません。また、出力変数の引数のみ演算子に => を使用します(e.g. output2 => intv)。演算子からも分かる通り、呼び出し後に左辺値を右辺に代入します。bool型の出力変数の場合のみ使用できるnot bool_output => boolvの構文では、左辺値を否定した値を右辺に代入します※1

フォーマル呼び出しでのみen変数・eno変数を指定できます。インフォーマル呼び出しでは、en変数・eno変数を指定できません。

y := div(
	in1 := intv1,
	in2 := intv2,
	not eno => maybe_intv2_is_zero);
インフォーマル呼び出し

ファンクションのインフォーマル呼び出しは、一般的に次のように記述できます。

<ファンクション名>(<実引数値1>, <実引数値2>, ...)

例:

y := absmax(13, -17); // -17
move(3, intv); // intvは3

インフォーマル呼び出しの括弧内で引数を記述する順番は、ファンクション定義時の引数変数定義の順番に一致します。en変数・eno変数を含まないことに注意してください。

// 線形合同法による擬似乱数生成器
function LCGrand: ulint
	var_in_out state: ulint; end_var
	var_input a, b, m: ulint; end_var
	//
	state := (a * state + b) mod m;
	LCGrand := state;
end_function

// Park & Miller提案の定数に従う擬似乱数生成器
function LMrand: ulint
	var_in_out state: ulint; end_var
	//
	LMrand := LCGrand(state, 48271, 0, 16#7fff_ffff); // state, a, b, mの順番です。
end_function

※1 POU呼び出しにおけるnot bool_output => boolvの構文は、LD言語やFBD言語の出力変数信号線の"o"の表記に対応します。

ファンクションブロック

※本章には、オブジェクト指向拡張されたファンクションブロックの説明を含みません。

ファンクションブロック(FB)は、ただ一つの処理が関連付けされた構造体、もしくはクラスに相当するPOUです。コンピュータサイエンスにおけるカプセル化の特徴をもちます。FBは、特定の種類のプログラム要素を作成するためのテンプレートとして機能し、属性(データ)と一つの処理をFBにまとめて定義し、管理できます。

// 呼び出すたびにカウントするFBの定義です。
// 属性nと、インクリメントする処理がまとめられています。
function_block Counter
	var_output n: int := 0; end_var
	//
	n := n + 1;
end_function_block

program Main
	var
		// Counter FBのインスタンスの宣言です。
		// c.nは0で初期化されます。
		c: Counter;
		v: int;
	end_var
	//
	c();
	v := c.n; // 1
	c();
	c();
	v := c.n; // 3
end_program

FBに定義した処理の実行は、FB呼び出し、又はFBインスタンス呼び出しと呼ばれるファンクション呼び出しに似た構文を用います。ファンクションとの違いの一つが、呼び出しを、FBインスタンスと呼ぶFBの実体に対して行うことです。FBインスタンスは、構造体の変数と同様、ユーザーが必要なときに定義し、FBインスタンスごとに独立した属性値(FBインスタンスメンバの値)の環境の元で処理を実行させることができます。また、その環境は呼び出しによって変更されることもあり、同じFBインスタンス、同じ入力であっても呼び出しの結果は、必ずしも一致しません。

構造体変数のメンバアクセスと同じように、FBインスタンスのメンバにもドット演算子を使用してアクセスできます(e.g. myfbinstance.outputvar)。ただし、デフォルトでアクセスできるのは、FBの入力変数や出力変数のみです。

// 平均値を計算するFBの定義です。
function_block Mean
	var_input x: lreal; end_var
	var_output output: lreal := 0.0; end_var
	var
		n: dint := 0;
		sum: lreal := 0.0;
	end_var
	//
	sum := sum + x;
	n := n + 1;
	output := sum / n;
end_function_block

program Main
	var
		// Mean FBのインスタンスの宣言です。
		a, b: Mean;
		m_a, m_b: lreal;
	end_var
	//
	a(2); a(3);
	b(2); b(3); b(5); b(7);
	//
	m_a := a.output; // 2.5=(2+3)/2
	m_b := b.output; // 4.25=(2+3+5+7)/4
end_program

FBの注意点

一般的なプログラミング言語の知識を持たれている方は、本節の内容に注意してください。

FB定義の記述方法

FB定義を一般的に次のように記述できます。

function_block <FB名>
	<引数変数の宣言>
	<変数の宣言>
	<処理>
end_function_block

<FB名>は、FBインスタンスを定義するときに使用する名前です。

<引数変数の宣言>には、以下に示す3つの引数変数を宣言することができます。

<変数の宣言>には、FB内で使用する変数を宣言します。内部変数(キーワードvar)、一時変数(var_temp)、および外部変数(var_external)を宣言できます。内部変数は呼び出しにおいて値を保持しますが、一時変数は呼び出しのたびに初期化されます。

function_block FBVarBehaviorSample
	var_output
		o: int := 1;
	end_var
	var
		v: int := 0;
	end_var
	var_temp
		t: int := 0;
	end_var
	//
	v := v + 100;
	t := t + 10;
	o := o + v + t;
end_function_block

program Main
	var
		x: FBVarBehaviorSample;
		y: int;
	end_var
	//
	x(o => y);
	// y=111=1+100+10
	x(o => y);
	// y=321=111+200+10
end_program

<処理>には、FBが実行する処理(プログラムコード)を記述します。制御文を記述できます。<処理>の終端に達するか、RETURN文が実行されると、呼び出し元に制御が戻ります。

FBインスタンス呼び出しの記述方法

FBインスタンス呼び出しの記述方法には、仮引数を記述するフォーマル呼び出しと、仮引数を省略するインフォーマル呼び出しがあります。インフォーマル呼び出しでは、すべての引数の値を記述しないといけないのに対し、フォーマル呼び出しでは、一部又は全ての引数の記述を省略できます。省略した引数の値は、前回呼び出したときの値が保持されるか、初回呼び出し時であれば、当該変数の初期値やデータ型のデフォルト値が処理系により設定されています。同一のFBインスタンスに対する呼び出しの記法において、フォーマル呼び出しとインフォーマル呼び出しを混在させることができるかどうかは実装依存です。

function_block FBArgBehaviorSample
	var_input
		i1: int := 1;
		i2: int;
	end_var
	var_output
		o: int;
	end_var
	//
	o := i1 + i2;
end_function_block

program Main
	var
		x: FBArgBehaviorSample;
		y: int;
	end_var
	//
	x(o => y);
	// y=1; 1+0
	x(in1 := 2, in2 := 3, o => y);
	// y=5; 2+3
	x(in2 := 5, o => y);
	// y=7; 2+5
end_program
フォーマル呼び出し

FBインスタンスのフォーマル呼び出しは、一般的に次のように記述できます。

<FBインスタンス名>(<仮引数名1> := <実引数値1>, <仮引数名2> := <実引数値2>, ...)

例:

ton_instance(in := on, pt := time#5s, q => timeup, et => cur); // 出力変数の演算子には、=>を使用します。
ctu_int_instance(cu := counting, pv := 17, q => countup); // 引数を省略できます。

フォーマル呼び出しにおいて、入力変数と出力変数の引数を省略することができます。省略する場合、変数の値は、変数宣言時に指定した初期値、又は初期値を指定していなければ、データ型のデフォルト値です。入出力変数の引数を省略することはできません。また、出力変数の引数のみ演算子に=>を使用します(e.g. q => trig)。演算子からも分かる通り、呼び出し後に左辺値を右辺に代入します。BOOL型の出力変数の場合のみ使用できるnot bool_output => boolvの構文では、左辺値を否定した値を右辺に代入します※1

フォーマル呼び出しでのみen変数・eno変数を指定できます。インフォーマル呼び出しでは、en変数・eno変数を指定できません。

ctu_int_instance(
	en := no_error,
	cu := using_stock,
	pv := 19,
	not q => stock_available,
	not eno => no_error);
インフォーマル呼び出し

FBインスタンスのインフォーマル呼び出しは、一般的に次のように記述できます。

<FBインスタンス名>(<実引数値1>, <実引数値2>, ...)

例:

ton_instance(on, time#5s, trig, cur); // in, pt, q, et
ctu_int_instance(counting, false, 17, countup, counter); // cu, r, pv, q, cv

インフォーマル呼び出しの括弧内で引数を記述する順番は、FB定義時の引数変数定義の順番に一致します。en変数・eno変数を含まないことに注意してください。

function_block ArgumentsOrderSample
	var_input
		in1, in2: int;
		in3: int;
	end_var
	var_output
		q: bool;
	end_var
	var_input
		opt: int := 0;
	end_var
end_function_block

program Main
	var
		x: ArgumentsOrderSample;
		r: bool;
	end_var
	//
	x(
		2, // in1
		3, // in2
		5, // in3
		r, // q
		0 // opt
	);
end_program

※1 POU呼び出しにおけるnot bool_output => boolvの構文は、LD言語やFBD言語の出力変数信号線の"o"の表記に対応します。

FBインスタンスメンバへのアクセスの記述方法

FBインスタンスを介したメンバへのアクセスは、一般的に次のように記述できます。

<FBインスタンス名>.<ローカル変数名>

例:

// 入力変数へのアクセス
ton_instance.en := true;
ton_instance.in := on;
ton_instance.pt := time#5s;
// インスタンス呼び出し
ton_instance();
// 出力変数へのアクセス
timeup := ton_instance.q;
cur := ton_instance.et;
no_error := ton_instance.eno;

// 上記は、以下のプログラムコードと等価です。
// ton_instance(en := true, in := on, pt := time#5s, q => timeup, et => cur, eno => no_error);

デフォルトでは、内部変数にはアクセスできません。public修飾子のある内部変数にはアクセスできます。

function_block AccessModifierSample
	var
		private_v: int;
	end_var
	var public
		public_v: int;
	end_var
end_function_block

program Main
	var
		x: AccessModifierSample;
	end_var
	//
	x.private_v := 2; // Compile time error! Inaccessible element.
	x.public_v := 3; // ok
end_program

なお、アクセス修飾は静的変数に対してのみ可能です。入力変数・出力変数は暗黙的に常にpublic、外部変数は暗黙的に常にprotected、一時変数は暗黙的に常にprivateです。入出力変数について、アクセス修飾子の概念があるかないかは実装依存ですが、呼び出した内部でのみアクセス可能です。

プログラムPOU

プログラムPOU(program ~ end_program)は、制御アプリケーションの最上位の実行単位です。configurationのresource内でtaskに関連付けられ、そのtaskの周期または条件に従って繰り返し実行されます。ファンクションやFBと異なり、他のPOUから直接呼び出すことはできません。プログラムPOUは、タスクによってスケジュールされ、システムからのみ呼び出されます。

プログラムPOUは、内部変数(var)・一時変数(var_temp)・外部変数(var_external)・入力変数(var_input)・出力変数(var_output)・入出力変数(var_in_out)を持てます。

program Main
	var_input
		start_cmd at %IX0.0 : bool; // 外部入力信号
	end_var
	var_output
		motor_out at %QX0.0 : bool; // 外部出力信号
	end_var
	var
		cycle     : dint := 0;
		running   : bool := false;
	end_var
	// タスク周期ごとに呼ばれる本体処理
	if start_cmd and not running then
		running := true;
	end_if;
	cycle := cycle + 1;
	motor_out := running;
end_program

クラス・継承・OOFB

クラス(class)・継承(extends)・インタフェース(interface)・オブジェクト指向ファンクションブロック(OOFB)については、別ページで詳しく解説しています。

→ オブジェクト指向プログラミング(クラス・継承・インタフェース・OOFB)

名前空間

名前空間(namespace ~ end_namespace)は、型・POU・グローバル変数などの識別子をグループ化して名前の衝突を防ぐ仕組みです。名前空間は入れ子にできます。また、同じ名前の名前空間を複数回宣言することで要素を追加(拡張)できます。

namespace MathLib
    function add: int
        var_input a: int; b: int; end_var
        add := a + b;
    end_function
end_namespace

// 同じ名前の名前空間に要素を追加(名前空間の拡張)
namespace MathLib
    function multiply: int
        var_input a: int; b: int; end_var
        multiply := a * b;
    end_function
end_namespace

ドット区切りの完全修飾名を使って入れ子の名前空間を一度に宣言することもできます(namespace Outer.Innernamespace Outer の中に namespace Inner を宣言するのと等価です)。

// 完全修飾名による宣言(入れ子と等価)
namespace Standard.Timers.HighResolution
    function_block HRTimer
        // ...
    end_function_block
end_namespace

// 上記は以下と等価
// namespace Standard
//     namespace Timers
//         namespace HighResolution
//             ...

識別子のスコープ

IEC 61131-3では、変数・型・POUの識別子は宣言されたスコープ内でのみ有効です。

変数種別ごとのスコープ
宣言スコープ・ライフタイム
varそのPOU内のみ有効。FB・PROGRAMでは呼び出し間で値を保持。FUNCTIONでは保持しない(実装依存)。
var_tempそのPOU内のみ有効。呼び出しごとにデフォルト値へ初期化される。
var_input / var_output / var_in_outそのPOUの正式パラメータ。呼び出し元から渡す・受け取る。
var_externalグローバル変数(var_global)へのアクセス用。型が一致しなければならない。
var_globalグローバルスコープ。var_externalで明示再宣言したPOUからのみアクセス可。

グローバル変数には var_external による明示的な再宣言が必要です。他のPOUの内部変数(var)は外部から直接参照できません。

var_global
    g_counter: int := 0;
    g_limit  : int := 100;
end_var

function_block Counter
    var_external            // グローバル変数への明示的アクセス宣言
        g_counter: int;     // var_global の型と一致しなければならない
    end_var
    var_external constant   // 定数グローバルへのアクセスには constant 修飾子が必要
        g_limit: int;
    end_var
    //
    if g_counter < g_limit then
        g_counter := g_counter + 1;
    end_if;
end_function_block

名前解決の規則

IEC 61131-3の名前解決規則の要点:

namespace MathLib
    function add: int
        var_input a: int; b: int; end_var
        add := a + b;
    end_function

    function clamp: lreal
        var_input val: lreal; lo: lreal; hi: lreal; end_var
        clamp := min(hi, max(lo, val));
    end_function

    namespace Trig
        function deg_to_rad: lreal
            var_input deg: lreal; end_var
            deg_to_rad := deg * 3.141592653589793 / 180.0;
        end_function
    end_namespace
end_namespace

program Main
    var
        result: int;
        angle: lreal;
        val: lreal;
    end_var
    result := MathLib.add(3, 5);             // 8
    val := MathLib.clamp(150.0, 0.0, 100.0); // 100.0
    angle := MathLib.Trig.deg_to_rad(90.0);  // 1.5707...
end_program

// using 宣言で修飾を省略
program Sub
    using MathLib;
    var r: int; end_var
    r := add(10, 20); // MathLib.add を修飾なしで呼べる
    // r := Trig.deg_to_rad(90.0); // ERROR: 入れ子のTrigはusingでインポートされない
end_program
using の制約:入れ子の名前空間は自動インポートされない
// NG: using MathLib だけでは MathLib.Trig の要素を修飾なしに使えない
program P1
    using MathLib;
    var angle: lreal; end_var
    angle := deg_to_rad(45.0);       // ERROR: deg_to_rad は Trig の中にある
    angle := Trig.deg_to_rad(45.0);  // ERROR: Trig も修飾が必要(MathLib.Trig)
    angle := MathLib.Trig.deg_to_rad(45.0); // OK: 完全修飾名
end_program

// OK: 入れ子の名前空間を直接 using する
program P2
    using MathLib.Trig;
    var angle: lreal; end_var
    angle := deg_to_rad(45.0); // OK
end_program
_. プレフィックスによるグローバル名前空間の明示参照
namespace MyNS
    function_block ton   // 標準FBと同名のFBを定義
        // ...
    end_function_block

    function_block MyFB
        var
            // 同一名前空間に ton が存在するため、標準 ton を明示するには _. を使う
            tmr: _.ton; // グローバル名前空間の標準タイマFB
        end_var
    end_function_block
end_namespace

名前空間のアクセス修飾子

名前空間自体と、その内部の要素(POU・型・変数)のそれぞれにアクセス修飾子を持ちます。名前空間に internal を付けると、その名前空間は外側(親)の名前空間からのみ参照できます。要素の修飾子のデフォルトは public です。

名前空間修飾子要素の修飾子アクセスできる範囲
(省略/デフォルト)publicすべての名前空間・POUから可
(省略/デフォルト)internal同一名前空間内のみ
internalpublic直接の親(外側)名前空間からのみ可
internalinternal同一名前空間内のみ
namespace OuterNS
    namespace internal InnerNS   // InnerNS は OuterNS からのみ参照可
        function public InnerFn: int  // OuterNS からはアクセス可
            var_input x: int; end_var
            InnerFn := x * 2;
        end_function

        function internal HiddenFn: int  // InnerNS 内のみ
            var_input x: int; end_var
            HiddenFn := x * 3;
        end_function
    end_namespace

    function public OuterFn: int  // どこからでもアクセス可
        var_input x: int; end_var
        OuterFn := InnerNS.InnerFn(x); // OuterNS から InnerNS へのアクセスは可
    end_function
end_namespace

// program Main
//     OuterNS.OuterFn(5);         // OK
//     OuterNS.InnerNS.InnerFn(5); // ERROR: InnerNS は OuterNS 外から参照不可

識別子の競合と解決

同名の識別子が複数のスコープに存在する場合、以下の規則が適用されます。

列挙子の同名衝突

複数の列挙型が同じ名前の列挙子を持つ場合、文脈から一意に決まらないとエラーになります。型名#列挙子 の型修飾で解決します。

type
    TrafficLight: (Red, Amber, Green);
    PaintColor  : (Red, Yellow, Green, Blue) := Blue;
end_type

var
    light: TrafficLight;
end_var

// light = Red;          // ERROR: Red が TrafficLight#Red か PaintColor#Red か一意に決まらない
light := TrafficLight#Red;  // OK: 型名で明示
light := Amber;             // OK: Amber は TrafficLight にのみ存在するため一意
クラス・FB内でのスコープとシャドウイング

メソッド内の var_temp と FB の var(インスタンス変数)で同名が宣言された場合の解決方法は実装依存です。規格は「クラスインスタンスは同じ名前解決スコープのファンクションと同じ名前を使用しないほうがよい」と勧告しています。this. で自インスタンスのメンバを明示できます(実装依存)。

function_block Counter
    var
        count: int := 0;
    end_var

    method Increment
        var_temp
            count: int; // FB の count と同名: 実装依存の挙動
        end_var
        count := count + 1;       // どちらの count か実装依存
        this.count := this.count + 1; // this. で FB インスタンス変数を明示(実装依存)
    end_method
end_function_block
var_external と var_global の型一致要件

var_external で参照するグローバル変数の型は、対応する var_global の型と完全に一致しなければなりません。また、var_global constant で宣言した変数にアクセスするには var_external constant が必要です(constant 修飾子なしの var_external でアクセスしようとするとエラー)。

var_global
    g_val  : int := 0;
    g_const: int := 42;  // constant ではない通常のグローバル変数
end_var
var_global constant
    G_MAX  : int := 100; // 定数グローバル変数
end_var

function_block Example
    var_external
        g_val  : int;    // OK: 型が一致
        // g_val: sint;  // ERROR: 型不一致
        g_const: int;    // OK: constant でないグローバル変数は通常の var_external で参照可
    end_var
    var_external constant
        G_MAX : int;     // OK: constant グローバル変数には constant が必要
        // G_MAX : int;  // var_external(constant なし)でのアクセスはエラー
    end_var
end_function_block

var_config

var_configは、変数のat属性の設定と初期値の設定を、プログラム実行前まで遅延できる機能です。

通常、ある変数に対するat属性と初期値の設定値については、プログラムを記述した時点で定まる設定値を用いてプログラムが実行されます。

例えば、次のファンクションブロックfbにおいて、変数nが示すデバイスは%IW7であり、初期値は2です。これは、どのFBインスタンスでも変わりません。v1.n、v2.nの双方IW7にアクセスします。インスタンスごとにアクセス先を変えたい場合、ファンクションブロックの複製、ソースコードの修正、再コンパイル、そして再テストが必要になることもあります。

function_block fb
	var n at %IW7 : int := 2; end_var
	:
end_function_block

program Main
	var
		v1: fb;
		v2: fb;
	end_var
	//
	v1.n := 3; // v2.nも3になる。
	v2.n := 4; // v1.nも4になる。
	x := v1 = v2; // true
end_program

var_configは、次の例に示す変数nのようにat属性の設定値を未決定(*)と設定し、var_config~end_var内で表現・設定される追加の設定にて、%IW5や%IW6などの具体的なat属性と、初期値を設定します。この設定が、mainやfbのソースコードに影響を与えることなく、プログラム実行直前に設定できることがポイントです。本例では、同じソースコード内にすべて記載していますが、実運用上においては、var_config~end_var部分は、時間的にもファイル的にも分離されることになります。

function_block fb
	var n at %I* : int; end_var
	:
end_function_block

program Main
	var
		v1: fb;
		v2: fb;
	end_var
	//
	v1.n := 3; // var_configにて別のデバイス先に設定されているため、v2.nには影響しない。
	v2.n := 4; // 同様に、v1.nには影響しない。
	x := v1.n = v2.n; // false
end_program

configuration cfg
	resource rsc on plc
		task t();
		program main_task with t: Main();
	end_resource
	var_config
		rsc.main_task.v1.n at %IW5 : int := 16;
		rsc.main_task.v2.n at %IW6 : int := 17;
	end_var
end_configuration

at属性の*指定については、FBだけでなく、プログラムPOU内の変数に対しても使用できます。出力変数、静的変数、グローバル変数に使用できます。プログラム実行前に、*が設定されているすべての変数に対して、at属性が設定されていなければ異常です。%I*の表現については、他にも特性に応じて、%I*, %QW*などを使用できます。※1

※1 *を含むパス文字列は、部分的にat属性を設定するという意味で、規格上は「部分指定」という表現が使用されています。全てを省略する%*という表現は、IEC 61131-3規格に示されません。効用的には、部分指定というより、遅延指定という表現の方が適切と考えていますし、%*という表現もあって良いと考えます。

演算子

演算子の一覧

IEC 61131-3の演算子の一覧を示します。※1

複数の演算子が混在する式においては、優先順位の高いものから評価されます。同一の優先順位をもつ場合、左から右に結合します(左結合)。他のプログラミング言語同様、括弧を使用することで、優先的に評価することができます。

ただし、結合規則はどの部分式を先にグループ化するかを定めるものであり、各オペランドの実際の評価順序は実装依存です。

r := 1 + 2; // 3
r := 1 + 2 * 3; // 7 = 1 + (2 * 3)
r := (1 + 2) * 3; // 9
r := -2 ** 4; // 16 = (-2)**4
r := f() * g() * h(); // (f()*g())*h() の順にグループ化されるが、f・g・hの呼び出し順序は実装依存
r := f() + g() * h(); // f→g→hか、g→h→fの順に呼ばれるか実装依存
演算子の一覧
演算子 名称 優先順位 結合規則
() 呼び出し 11 実装依存(一般的に左)
. メンバアクセス部分アクセス 実装依存 実装依存(一般的に左)
[] 配列添え字 実装依存 実装依存(一般的に左)
# 列挙子アクセス 実装依存 実装依存(一般的に左)
ref リファレンス 実装依存 実装依存(一般的に右)
^ デリファレンス 10 実装依存(一般的に右)
+ - 単項プラス・単項マイナス 9 実装依存(一般的に右)
not 論理否定ビット反転 9 実装依存(一般的に右)
** べき乗 8
* 乗算 7
/ 除算 7
mod 剰余 7
+ 加算 6
- 減算 6
> >= <= < 比較 5
= <> 等価 非等価 4
and & 論理積ビット積 3
xor 排他的論理和ビット排他的論理和 2
or 論理和ビット和 1
:= => ?= 代入引数代入試行代入 実装依存 実装依存(一般的に右)

※1 優先順位が実装依存となっている演算子は、IEC 61131-3の規定に"演算子"として定義されていないものです。一般的なプログラミング言語で演算子として定義されているものは筆者の判断で含めています。

式の型と型変換

式の型は、式を構成するオペランドと演算子の組み合わせによって決まります。異なる型の間での代入や演算では型変換が必要になります。

明示的型変換と暗黙的型変換

型変換には明示的型変換(explicit)と暗黙的型変換(implicit)の2種類があります。明示的型変換はint_to_dint等の変換ファンクションを使用者が呼び出すものです。暗黙的型変換は、コンパイラが自動的に挿入するものです。どちらの変換が可能かは型の組み合わせによって規格で定められています。

暗黙的型変換の適用規則

暗黙的型変換には以下の適用規則があります:

// 暗黙的型変換の例
var
    a: usint;
    b: uint;
    c: udint;
    r: real;
end_var

c := a * b;    // a は udint へ暗黙変換。乗算結果は udint。c に代入。
r := a;        // usint → real は暗黙変換可(精度が保たれるため)
// a := c;     // udint → usint は暗黙変換不可(精度低下)。明示変換が必要。
a := udint_to_usint(c); // OK: 明示的型変換
暗黙的・明示的変換表

型の組み合わせごとに i(暗黙の型変換可)・e(明示的な型変換が必要、暗黙の型変換不可)・-(未定義)・空欄(変換不要、恒等変換)が規定されています。- のセルは実装者が独自仕様でサポートしてもよいとされており、コンパイラにより動作が異なります。

暗黙的(i)・明示的(e)型変換表
変換元 変換先
符号あり整数 符号なし整数 浮動小数点 ビット列 ブール 時間 日付時刻 文字 文字列
sintintdintlint usintuintudintulint reallreal byteworddwordlword bool timeltime ldate/dateldt/dt charwchar stringwstring
sint iii eeee ii eeee - -- -- -- --
int eii eeee ii eeee - -- -- -- --
dint eei eeee ei eeee - -- -- -- --
lint eee eeee ee eeee - -- -- -- --
usint eiii iii ii eeee - -- -- -- --
uint eeii eii ii eeee - -- -- -- --
udint eeei eei ei eeee - -- -- -- --
ulint eeee eee ee eeee - -- -- -- --
real eeee eeee i ---- - -- -- -- --
lreal eeee eeee e ---- - -- -- -- --
byte eeee eeee -- iii e -- -- e- --
word eeee eeee -- eii e -- -- e- --
dword eeee eeee e- eei e -- -- -- --
lword eeee eeee -e eee e -- -- -- --
bool eeee eeee -- iiii -- -- -- --
time ---- ---- -- ---- - i -- -- --
ltime ---- ---- -- ---- - e -- -- --
char ---- ---- -- eeee e -- -- i --
wchar ---- ---- -- -eee - -- -- i --
string ---- ---- -- ---- - -- -- e- -
wstring---- ---- -- ---- - -- -- -- e

凡例: i=暗黙の型変換可(明示的な型変換も可)、e=明示的な型変換が必要(暗黙の型変換不可)、-=未定義(実装者が独自仕様でサポートしてもよい)、空欄=変換不要(恒等変換)

表に示されない日付時刻型どうし(例: ldt→dt)は明示変換ファンクション(ldt_to_dt等)が標準で用意されています(規格で定義あり、表は省略)。stringwstringcharwchare(暗黙変換不可)なのは、使用している文字集合との不整合を避けるためです。

二項演算の結果の型

演算子の結果型については、多重定義演算子の場合、オペランドが同じデータ型になるように暗黙変換を適用でき、演算結果はそのデータ型になります。例えば uint * uint の結果は uint です。代入先変数の型は演算結果の型に影響しません。

var
    a: int;
    b: int;
    c: dint;
    d: dint;
    x: uint := 200;
    y: uint := 300;
    z: udint;
end_var

// 演算結果の型は代入先ではなくオペランドの型で決まる
c := a + b;    // a + b は int 型で計算される(代入先 c の型 dint は影響しない)
               // 計算後に int → dint の暗黙変換で c に代入される
c := d + a;    // 型の暗黙変換規則により a が dint に暗黙変換され、dint 加算が行われる

// x * y は uint 型で計算される
z := x * y;   // 60000 は uint 範囲内(0~65535)なのでOK。代入時に uint → udint の暗黙変換
// x * y が uint の範囲を超える場合は桁あふれ(実装依存)
z := uint_to_udint(x) * uint_to_udint(y);  // 先に udint へ明示変換して安全に演算
ブール式の短絡評価

ブール型の式は、結果が確定した時点で残りの評価を省略してよいとされています(短絡評価)。ただし、どこまで評価するかは実装依存です。副作用(関数呼び出しや変数更新)を含むブール式では、評価されない場合があります。

// 短絡評価の例(実装依存)
// (A > B) が false の場合、(C < D) は評価されないことがある
if (a > b) and (c < d) then
    ...
end_if;

// 副作用がある場合は注意
if check_flag() and update_counter() then  // update_counter()が呼ばれない場合がある
    ...
end_if;

値渡しと参照渡し

呼び出しにおいて、入力変数(var_input)と出力変数(var_output)は値渡しであり、入出力変数(var_in_out)は参照渡しです※1。入出力変数は、C++言語の参照変数と同等の機能をもちます。

以下の例に示す通り、入出力変数への書き込みは、呼び出し元の変数への書き込みになります。

type
	t: struct
		m: int;
	end_struct;
end_type

function call_by_ref_sample
	var_in_out
		rintv: int;
		rstrv: string;
		rtv: t;
	end_var
	//
	rintv := 4;
	rstrv := 'five';
	rtv.m := 6;
end_function

program Main
	var
		intv: int := 1;
		strv: string := 'two';
		tv: t := (m := 3);
	end_var
	//
	// intv=1, strv='two', tv.m=3
	call_by_ref_sample(
		rintv := intv,
		rstrv := strv,
		rtv := tv
	);
	// intv=4, strv='five', tv.m=6
end_program

参照渡しは、呼び出し元の変数の値を変更する必要がある機能の実現で有用です。また、一般的な参照渡しの特性として文字列型、配列、構造体型などの大きなデータを比較的高速に渡すことができます。

※1 IEC 61131-3には、入出力変数が参照渡しであるとは明確には記載されていません。しかし、呼び出し元の変数に作用あること、入出力変数による多態性の特性あること、ファンクションの可変長配列の宣言が入出力変数にのみ許されていることから参照渡しでの実現が望ましいと判断しています。他の実現方法として、呼び出し直前に値を渡し、呼び出し直後に値を受け取ることで、参照渡しに似た機能を実現できます。この実現方法の場合、呼び出しの処理中には呼び出し元の変数に作用がない特性があります。また、書き込みが一回多いため高速化どころか低速化します。

呼び出し

POUの呼び出しは、ファンクション・FBインスタンス・メソッドに対して行います。呼び出し時に実引数を渡し、出力変数で結果を受け取ります。

形式として、仮引数有り呼び出し(フォーマルな呼び出し)と、仮引数無し呼び出し(インフォーマルな呼び出し)があります。一部の引数を省略するためには、仮引数有り呼び出しを使用する必要があります。

pou_instance(in1 := 2, in2 := true, out => v); // インスタンス呼び出し(フォーマル)
move(in := 3, out => intv); // 戻り値のないファンクション呼び出し(フォーマル)
strv := concat(in1 := key, in2 := ':', in3 := value); // 戻り値のあるファンクション呼び出し(フォーマル)
pou_instance(2, true, v); // インスタンス呼び出し(インフォーマル)
move(3, intv); // 戻り値のないファンクション呼び出し(インフォーマル)
strv := concat(key, ':', value); // 戻り値のあるファンクション呼び出し(インフォーマル)

メンバアクセス

構造体型変数、FBインスタンス、クラスインスタンスのメンバへのアクセスにはドット演算子(.)を使用します。変数名.メンバ名と記述します。ドット演算子は連鎖して記述できます(rc.top_left.xなど)。

program Main
	var
		fb1   : SomeFB;
		pt    : Point;
		rc    : Rect;
		clk   : ton;         // 標準タイマFB
	end_var
	// FBインスタンスの入出力メンバアクセス
	fb1.input := 42;
	fb1();
	result := fb1.output;

	// 構造体メンバアクセス
	pt.x := 1.0;
	pt.y := 2.0;

	// ネストした構造体のメンバアクセス(チェーン)
	rc.top_left.x := 10.0;
	rc.top_left.y := 20.0;

	// タイマFBの操作
	clk(IN := start_signal, PT := lt#5s);
	output := clk.Q;         // タイマ出力をメンバアクセスで取得
end_program
外部からのメンバアクセスの制限

FBインスタンスのメンバに外部からドット演算子でアクセスできる範囲は、変数の種別ごとに規定されています。

FBインスタンスの変数種別ごとの外部アクセス可否
変数種別外部からの読み取り外部からの書き込み備考
var_output不可
var_input不可通常は呼び出し時のパラメータ指定(fb(input := val);)で設定する
var_in_out不可不可呼び出し時のパラメータ指定のみ有効
var(内部変数)不可(デフォルト)不可(デフォルト)var public と宣言した場合のみ var_output と同様に外部から読み取り可能

クラスインスタンスでは、var セクションにアクセス指定子を付与して外部アクセスを制御します。デフォルトは protected(クラス内部および派生クラスのみ)です。public のみ外部からのドット演算子アクセスが可能で、private はクラス内部のみ、internal は同一名前空間内のみに制限されます。

参照型経由のメンバアクセス

参照型変数(ref_to)が指すインスタンスのメンバにアクセスするには、デリファレンス(^)とドット演算子を組み合わせます。逆参照は明示的に行う必要があります。

var
	r: ref_to Point;
	pt: Point;
end_var
r ref= pt;
r^.x := 3.0;   // デリファレンス後にメンバアクセス
r^.y := 4.0;

配列アクセス

配列要素へのアクセスには角括弧([添字])演算子を使用します。多次元配列はカンマ区切りで添字を指定します。STでは添字に any_int のサブタイプの値を返す式(変数・計算式)を使用できます。宣言で指定する添字の範囲はリテラルまたは定数式でなければならず、変数は使用できません。

var
	a: array[0..4] of int;
	m: array[0..1, 0..2] of int;
	i: int;
end_var
a[0] := 10;
a[4] := 50;
m[0, 0] := 1;
m[1, 2] := 6;

// 変数・式による添字
i := 2;
a[i] := 30;          // a[2] = 30
a[i + 1] := 40;      // a[3] = 40

// 構造体配列のメンバアクセス
points[3].x := 5.0;  // 配列要素の構造体メンバへのアクセス

添字が宣言で指定した範囲外の場合はエラーです。ただし、変数や式による添字の場合は実行時にのみ検出できます。添字の最大個数・配列の最大サイズ・添字値の最大範囲は実装依存です。

各次元の宣言範囲は lower_bound(arr, dim)upper_bound(arr, dim) で取得できます。dim は次元番号(1始まり)です。存在しない次元番号を指定した場合はエラーです。これらは可変長配列(array[*])と組み合わせて使用します。

リファレンス

参照型変数(ref_to 型)に参照先変数を設定するにはref=演算子を使用します。ref=の右辺には変数・配列要素・構造体メンバ・FBインスタンスを指定できます。nullを設定すると参照なし状態になります。

:=(代入)とref=(参照設定)は異なります。:=は参照先の値をコピーし、ref=は参照先そのものを変更します。

var
	x: int := 5;
	y: int := 10;
	r: ref_to int;
end_var
r ref= x;     // rはxへの参照
r ref= y;     // rをyへの参照に変更(xは変化しない)
r ref= null;  // 参照解除(nullチェックが必要になる)

デリファレンス

参照型変数が指す先の値を読み書きするには^演算子(デリファレンス)を使用します。r^は参照先変数と同じように扱えます。デリファレンスは明示的に行う必要があります。nullのデリファレンスはエラーです(規格で明示的に禁止されています)。

var
	x: int := 10;
	r: ref_to int;
end_var
r ref= x;
r^ := 42;           // x = 42(参照先への書き込み)
y := r^ + 1;        // y = 43(参照先からの読み取り)
r^ := r^ * 2;       // x = 84

// nullチェックを忘れずに
if r <> null then
	r^ := 99;
end_if;

単項プラス・単項マイナス

単項プラス(+)は値をそのまま返します。単項マイナス(-)は符号を反転します(算術的な符号反転:-x = 0 - x)。整数型・実数型に適用できます。符号付き整数の最小値(lint#-9223372036854775808など)を単項マイナスで反転した場合の動作は実装依存です(オーバーフロー)。

x := +5;           // 5
x := -5;           // -5
x := -(-3);        // 3
r := -(1.5 + 2.5); // -4.0(式全体の符号反転)
// 符号付き整数最小値の符号反転はオーバーフロー
// si := -sint#-128; // 実装依存: -128(ラップ)またはエラー

加算・減算

演算子+は加算、-は減算を行います。整数型・実数型・持続時間型(time型同士、ltime型同士)に使用できます。異なる型間の演算(整数と実数の混在など)の暗黙変換は実装依存です。

i := 3 + 5;              // 8
i := 10 - 3;             // 7
r := 1.5 + 2.3;          // 3.8
t := t#1s + t#500ms;     // t#1s500ms
t := t#2s - t#300ms;     // t#1s700ms
lt := lt#1s + lt#500ms;  // lt#1s500ms
lt := lt#2s - lt#300ms;  // lt#1s700ms
// i := 3 + 1.5;         // 実装依存: 整数と実数の混在演算

乗算・除算・剰余算

演算子*は乗算、/は除算、modは剰余算を行います。整数の除算はゼロ方向への切り捨てです。0による除算の動作については、実装依存です(エラー、0を返す、未定義動作など)。

i := 3 * 4;       // 12
i := 10 / 3;      // 3(整数除算、切り捨て)
i := -7 / 2;      // -3(ゼロ方向への切り捨て)
i := 10 mod 3;    // 1(剰余)
i := -7 mod 2;    //  -1(= -7 - (-7/2)*2 = -7+6)
r := 10.0 / 3.0;  // 3.3333...
i := 5 / 0;       // 実装依存: エラーまたは 0 など

べき乗

演算子**は、べき乗を計算します。

r := 2 ** 3; // 8 = 2 * 2 * 2
r := 4.0 ** 0.5; // 2 = sqrt(4)
r := 2.0 ** (-3); // 0.125 = 1/(2 ** 3) = 1 / (2 * 2 * 2)
r := 5 ** 0; // 1
r := -2 ** 4; // 16 = (-2) ** 4
r := 2 ** 3 ** 2; // 64 = (2 ** 3) ** 2 = 8 * 8
r := -2 ** 0.5; // 実装依存; 複素数
r := 0 ** 2; // 0
r := 0 ** 0; // 実装依存
🛈数学の2^3^2は、2^(3^2)=512と右から左へ評価されますが、IEC 61131-3の 2**3**2 は、(2**3)**2=64と左から右に評価されます。

比較

比較演算子(<, >, <=, >=)は2つの値の大小関係を評価し、bool型の結果を返します。整数型・実数型・時間型・文字型・文字列型などに使用できます。文字列の比較は各文字のコード値(数値)の昇順による辞書順です。長さが異なる場合、短い方の末尾をコード値0の文字で埋めて比較されます。

bv := 3 < 5;          // true
bv := 5 > 5;          // false
bv := 5 <= 5;         // true
bv := 3 >= 5;         // false
bv := lt#1s < lt#2s;  // true(時間型の比較)
bv := 'abc' < 'abd';  // true(文字列の辞書順比較)
bv := 'abc' < 'abcd'; // true(文字列長が短い方が小さい)

実数型の比較では、浮動小数点演算の誤差(丸め誤差)に注意してください。厳密な等値比較(=)ではなく、差の絶対値と許容誤差を比較する方法が推奨されます。

// 実数の比較は誤差に注意
bv := (1.0 / 3.0 * 3.0) = 1.0;          // 実装依存: trueにならない場合あり
bv := abs(1.0 / 3.0 * 3.0 - 1.0) < 1.0e-9; // 誤差を考慮した比較(推奨)

等価

等価演算子=は2つの値が等しければtrue、不等価演算子<>は等しくなければtrueを返します。基本データ型(整数型・実数型・時間型・文字列型など)および列挙型に使用できます。構造体型・配列型・FBインスタンス型への適用は規格に定義がなく、実装依存です。多くの実装では使用不可です。

bv := 5 = 5;             // true
bv := 5 = 6;             // false
bv := 5 <> 6;            // true
bv := 'abc' = 'abc';     // true
bv := true = false;      // false
bv := lt#1s = lt#1000ms; // true(同じ時間値)
bv := Color#eRed = Color#eGreen; // false
// bv := struct1 = struct2;  // 実装依存

論理否定( not )

論理否定演算は、単項演算子notのオペランドがブール型の値に対して使用される演算です。ブール値を反転した値を得ます。

boolv := not true; // false
boolv := not false; // true

論理積・論理和・排他的論理和( and, or, xor )

論理積、論理和、排他的論理和演算は、それぞれ二項演算子and, or, xorの両オペランドがブール型の値に対して使用される演算です。

論理積( and )は、2つのブール値がともに true の場合に true を得ます。

boolv := false and false; // false
boolv := false and true; // false
boolv := true and false; // false
boolv := true and true; // true

論理和( or )は、2つのブール値がともに false の場合に false を得ます。

boolv := false or false; // false
boolv := false or true; // true
boolv := true or false; // true
boolv := true or true; // true

排他的論理和( xor )は、2つのブール値が異なる場合に true を得ます。

boolv := false xor false; // false
boolv := false xor true; // true
boolv := true xor false; // true
boolv := true xor true; // false
⚠️論理演算の評価方法が、短絡評価であるか、非短絡評価であるかは、実装依存です。

例えば、boolv := (p > 0) and (mid(text, 1, p) = '0'); において、pが0以下であるケースを考えます。短絡評価の処理系(オムロンなど)では、and の左オペランドが偽に確定しているため、右オペランドを評価せずに、即座にboolvにfalseを代入します。一方、非短絡評価の処理系(キーエンスなど)では、必ず and の両オペランドを評価します。よって、このプログラムの場合、非短絡評価の処理系では、不正な引数でmidが呼ばれることになり実行時にエラーが発生します。

短絡評価で規定するプログラミング言語の一般的な話として、論理演算は本質的にif文を内在しています。移植性が重要なプロジェクトでは、if p <= 0 then boolv := false; else boolv := mid(text, 1, p) = '0'; end_if; のように記述し、評価方法を処理系依存にしないことも検討ください。

ビット反転( not )

ビット反転演算は、単項演算子notのオペランドがビット列型の値に対して使用される演算です。全ビットを反転した値を得ます。bool型の論理否定と同じキーワードnotですが、オペランドの型によって区別されます。

wv := not word#16#FF00;            // word#16#00FF
bv := not byte#2#0000_1111;        // byte#2#1111_0000
lv := not lword#16#FFFF_0000_0000_0000; // lword#16#0000_FFFF_FFFF_FFFF

典型的な用途はビットマスクの反転です。

var
	flags   : word := word#16#1234;
	mask    : word := word#16#00FF; // 下位バイトのマスク
	cleared : word;
	inverted: word;
end_var
cleared  := flags and (not mask);  // 下位バイトをクリア: word#16#1200
inverted := flags xor mask;        // 下位バイトを反転: word#16#12CB

ビット積・ビット和・ビット排他的論理和( and, or, xor )

ビット演算は、両オペランドがビット列型の場合に使用される演算です。andはビット積(両ビットが1のとき1)、orはビット和(いずれかが1のとき1)、xorはビット排他的論理和(ビットが異なるとき1)を行います。bool型の論理演算と同じキーワードですが、オペランドの型によって区別されます。

// 典型的なビット操作パターン
wv := flags and word#16#000F;       // 下位4ビットのマスク(抽出)
wv := flags or  word#16#8000;       // 最上位ビットをセット
wv := flags xor word#16#00FF;       // 下位バイトを反転(トグル)
wv := word#16#FF00 and word#16#F0F0; // word#16#F000
wv := word#16#FF00 or  word#16#00FF; // word#16#FFFF
wv := word#16#FF00 xor word#16#F0F0; // word#16#0FF0

異なるビット幅のビット列型同士の演算は実装依存です。同じ型同士での演算を推奨します。

部分アクセス

ビット列型変数の部分的なビット列値に簡単にアクセスできます。演算子ドット(.)を使用し、<ビット列型変数>.%<幅><位置>のように記述します。

boolv0 := wordv.%X0 ; // word型変数のビット0(最下位ビット)
boolv7 := wordv.%X7 ; // word型変数のビット7(最上位ビット)
low_bytev := wordv.%B0 ; // word型変数のバイト0(下位バイト)
high_bytev := wordv.%B1 ; // word型変数のバイト1(上位バイト)
lowlow_bytev := dwordv.%B0 ; // dword型変数のバイト0(最下位バイト)

<幅>は、X(bool), B(byte), W(word), D(dword), L(lword)のいずれかです。<位置>は、0以上で指定し、ビット列型変数のビット幅を超えないように指定する必要があります。例えば、dword型変数に対する部分アクセスであれば、%X0~%X31, %B0~%B3, %W0~%W1 となります。

単一のビットアクセスの場合には省略記法があり、<ビット列型変数>.<位置>のように記述できます。

boolv6 := wordv.6 ; // wordv.%X6と等価です。

なお、次のようなプログラムについての明確な規定はありません。

wordv.%X0 := true; // 実装依存: 部分アクセス式に対する書き込み。
boolv := word#16#FF.%X0 ; // 実装依存: ビット列リテラルに対する部分アクセス。
boolv := (word#16#000F and wordv).%X3 ; // 実装依存: ビット列型の式の結果に対する部分アクセス。
boolv := wordFunction().%X1 ;
wordv := wordv.%W0 ; // 実装依存: 恒等的な部分アクセス。なお、<幅>にLが定義されており、lword.%L0 としか使いようがない。

代入

代入演算子:=は、右辺の評価値を左辺の変数に代入します。IEC 61131-3の代入は文(statement)であり式(expression)ではないため、代入式を他の式の一部として使うことはできません(多重代入a := b := 1はシンタックスエラー)。

単一要素変数への代入では、左辺と右辺が同型、または右辺が左辺へ暗黙変換可能な型であれば許されます。構造体・配列など複数要素変数への代入では、左辺と右辺のデータ型が同じでなければならない(暗黙変換は適用されない)。FBインスタンス型への代入の仕様(コピー代入の可否、内部状態の取り扱いなど)については実装依存です。

x := 5;
y := x + 3;          // y = 8
s := 'hello';
b := true;
arr[2] := 42;        // 配列要素への代入
pt.x := 1.0;         // 構造体メンバへの代入
x := y := 7;         // Compile-time error! syntax error.
代入時の型変換における実装依存の範囲

規格は型の組み合わせごとに「暗黙の型変換可(i)」「明示的な型変換が必要(e)」「未定義(-)」を規定しています。「未定義(-)」の組み合わせは実装者が独自仕様でサポートしてもよいとされており、コンパイラにより動作が異なります。

特に注意が必要な組み合わせ:

// 安全: 暗黙変換可(i)
var
    si: sint := 100;
    n : int;
    r : real;
    ud: udint;
    ui: uint := 300;
end_var
n  := si;            // sint → int: 暗黙変換可(小→大)
r  := n;             // int  → real: 暗黙変換可
ud := ui;            // uint → udint: 暗黙変換可(小→大)

// 注意: 明示変換が必要(e)
// n  := ud;         // udint → int: 明示変換が必要
n    := udint_to_int(ud);   // 明示的型変換

// 実装依存: 非型付リテラル
// not(0) の結果の型は実装依存(bool#1 か word#16#ffff か dword#16#ffffffff か)
// → 型付リテラルで明示することを推奨
var w: word; end_var
w := not(word#0);    // 明確: word#16#ffff
// w := not(0);      // 曖昧: 実装依存

試行代入

試行代入演算子?=(Ed.4)は、インタフェース型・クラス型・FBインスタンス型の参照間で安全なダウンキャスト(下位型への変換)を行う演算子です。右辺の参照が実際に左辺の型を実装・継承している場合は左辺に有効な参照が代入され、そうでない場合は左辺がnullになります(エラーにはなりません)。左辺・右辺ともに参照型またはインタフェース型でなければなりません。

// インタフェース間のダウンキャスト
// CLASS C implements ITF1, ITF2 とする
var
	inst: c;
	interf1: itf1;
	interf2: itf2;
	interf3: itf3;
end_var
interf1 := inst;
interf2 ?= interf1;  // c は itf2 を実装しているため interf2 に有効な参照が入る
interf3 ?= interf1;  // c は itf3 を実装していないため interf3 は null になる
// クラス参照のダウンキャスト(基底クラス→派生クラス)
// CLASS ClDerived EXTENDS ClBase とする
var
	rinstBase1: ref_to clbase;   // clbase インスタンスへの参照
	rinstBase2: ref_to clbase;   // clderived インスタンスへの参照
	rinstDerived1: ref_to clderived;
	rinstDerived2: ref_to clderived;
end_var
rinstDerived1 ?= rinstBase1;  // 実際は clbase インスタンス → rinstDerived1 は null
rinstDerived2 ?= rinstBase2;  // 実際は clderived インスタンス → 有効な参照が入る

制御文

制御文は、プログラムの実行フローを制御する文です。代入文、条件分岐(if文・case文)、繰り返し(for文・while文・repeat文)、およびループ制御(continue文・exit文)とreturn文があります。

代入文

代入文は、演算子:=を使用して変数に値を代入する文です。右辺には式を記述できます。

x := 2;
x := 3 + 5;
x := y := 7; // Compile-time error! syntax error.
構文

代入文の構文は以下のとおりです。

// 代入文 ::= 変数 ':=' 式 ';'
x := 式;

左辺には書き込み可能な変数を指定します。constant修飾子を持つ変数や、ファンクションの入力変数(var_input)には代入できません。

型の整合性

右辺の式の型は、左辺の変数の型と適合する必要があります。型の適合には、暗黙的な型変換(例: intからdintへの拡張変換)と明示的な型変換(型変換ファンクションの使用)があります。型が不適合な場合はコンパイルエラーになります。ただし、一部の実装では実装依存の挙動(警告のみ、または自動変換)をとるケースがあります。

複合型への代入

構造体・配列は代入演算子(:=)で全メンバ/全要素をまとめてコピーできます。

// 構造体への代入(全メンバコピー)
type TPoint:
	struct
		x: real;
		y: real;
	end_struct
end_type

var
	p1, p2: TPoint;
end_var

p1.x := 1.0;
p1.y := 2.0;
p2 := p1;  // p2.x = 1.0, p2.y = 2.0 (全メンバがコピーされる)
// 配列への代入(全要素コピー)
var
	arr1: array[1..3] of int := [10, 20, 30];
	arr2: array[1..3] of int;
end_var

arr2 := arr1;  // arr2[1]=10, arr2[2]=20, arr2[3]=30

ファンクションブロック(FB)インスタンスへの代入は、コピー代入の可否・内部状態の取り扱いなど仕様の詳細が実装依存です。

試行代入演算子 ?= (Ed.4)

試行代入演算子?=は、インタフェース型・クラス型・FBインスタンス型の参照間の安全なダウンキャスト(下位型への変換代入)に使用します。右辺の参照が左辺の型を実装・継承している場合は代入が成功し、そうでない場合は左辺がnullになります。左辺・右辺ともに参照型またはインタフェース型でなければなりません。試行代入を参照。

呼び出し文

呼び出しを参照。

if文

if文は、条件式がtrueの場合に処理を実行する条件分岐文です。elsifで複数条件、elseでそれ以外の処理を記述できます。

if x = 0 then
	zero_num := zero_num + 1;
end_if;
if (x mod 2) = 0 then
	even_num := even_num + 1;
else
	odd_num := odd_num + 1;
end_if;
if x > 0 then
	positive_num := positive_num + 1;
elsif x < 0 then
	negative_num := negative_num + 1;
else
	zero_num := zero_num + 1;
end_if;
構文

if文の構文は以下のとおりです。

// if文 ::= 'if' 条件式 'then' 文列
//          { 'elsif' 条件式 'then' 文列 }
//          [ 'else' 文列 ]
//          'end_if' ';'
if 条件式 then
	文列
elsif 条件式 then
	文列
else
	文列
end_if;
意味論

if文の動作は以下のとおりです。

注意

末尾のセミコロンはend_ifの直後に付けます(end_if;)。

// ネストしたif文
if x > 0 then
	if x < 100 then
		// x は 1 以上 99 以下
		in_range := true;
	else
		// x は 100 以上
		in_range := false;
		overflow := true;
	end_if;
else
	// x は 0 以下
	in_range := false;
end_if;

case文

case文は、整数型(any_int)または列挙型の式の値に応じて処理を分岐する選択文です。一つの節に複数の値をカンマ区切りで指定したり、範囲(a..b)を指定したりできます。どの値にも一致しない場合はelse節が実行されます(省略可)。C言語のswitch文と異なり、フォールスルー(次の節に処理が流れ込む動作)はありません。case文に記述できる選択肢の最大数は実装依存です。

n := 3;
case n of
2:
	// n is 2
	k := 4;
3:
	// n is 3
	k := 9;
4:
	// n is 4
	k := 16;
5..7:
	// n is 5 or 6 or 7.
	k := n * n;
else
	// n is other value.
	k := -1;
end_case;
// k = 9

カンマ区切りで複数の値を一つの節にまとめることができます。

// 複数の値をカンマ区切りでまとめる
case weekday of
1, 7:           // 日曜日または土曜日
	is_weekend := true;
2, 3, 4, 5, 6:  // 月曜日〜金曜日
	is_weekend := false;
end_case;

列挙型と組み合わせると可読性が向上します。

// 列挙型とcase文の組み合わせ
case state of
ErrorCode#eNone:
	; // 処理なし
ErrorCode#eTimeout, ErrorCode#eOverrun:
	// 通信系エラー
	retry_count := retry_count + 1;
	reconnect := (retry_count < max_retry);
ErrorCode#eUnknown:
	// 未知エラー
	emergency_stop := true;
end_case;
構文

case文の構文は以下のとおりです。

// case文       ::= 'case' 選択式 'of'
//                  選択節 { 選択節 }
//                  [ 'else' 文列 ]
//                  'end_case' ';'
// 選択節       ::= 選択値リスト ':' 文列
// 選択値リスト ::= 選択値 { ',' 選択値 }
// 選択値       ::= 定数 | 定数 '..' 定数
case 選択式 of
値1:
	文列
値2, 値3:
	文列
値4..値5:
	文列
else
	文列
end_case;
意味論

case文の動作は以下のとおりです。

for文

for文は、制御変数を指定した範囲で繰り返す反復文です。byでステップ幅を指定できます(省略時は1)。制御変数はfor文の外(囲むPOU内)で宣言する必要があります。for文終了後の制御変数の値は実装依存です(終了値を超えた値になる実装が多いですが、規格では保証されていません)。

ステップには負の値も指定でき、カウントダウンが可能です。終了条件の評価は各ループ先頭で行われます(前判定ループ)。

sum := 0;
for n := 1 to 4 by 1 do
	sum := sum + n;
end_for;
// sum = 10 = 1 + 2 + 3 + 4, n = 5(実装依存)
sum := 0;
for n := 1 to 4 do
	sum := sum + n;
end_for;
// sum = 10 = 1 + 2 + 3 + 4, n = 5(実装依存)

ステップに負の値を指定するとカウントダウンできます。

// カウントダウン
for n := 4 to 1 by -1 do
	// n = 4, 3, 2, 1 の順に処理
end_for;
// n = 0(実装依存)

// 配列の全要素処理
for i := lower_bound(arr, 1) to upper_bound(arr, 1) do
	arr[i] := arr[i] * 2;
end_for;
構文

for文の構文は以下のとおりです。

// for文 ::= 'for' 制御変数 ':=' 初期値式 'to' 終了値式 [ 'by' ステップ式 ] 'do'
//           文列
//           'end_for' ';'
for 制御変数 := 初期値式 to 終了値式 by ステップ式 do
	文列
end_for;
意味論

for文の動作は以下のとおりです。

// ステップが0の場合の注意(実装依存: 多くの実装で無限ループになる)
step := 0;
for i := 1 to 10 by step do  // 危険: stepが0のため無限ループの可能性
	process(i);
end_for;

// ループ本体内での制御変数の変更(規格違反)
for i := 1 to 10 do
	if condition then
		i := i + 1; // 規格違反: 制御変数をループ本体内で変更してはならない
	end_if;
	process(i);
end_for;

while文

while文は、条件式がtrueの間、処理を繰り返す前判定ループ文です。条件は本体実行前に評価されるため、最初から条件がfalseであれば一度も実行されません。繰り返し回数が事前に定まらない場合(収束まで繰り返すなど)に適しています。

sum := 0;
n := 1;
while n <= 4 do
	sum := sum + n;
	n := n + 1;
end_while;
// sum = 10 = 1 + 2 + 3 + 4, n = 5.

文字列処理や収束判定など、終了条件が動的に変化する場合に有用です。

// 文字列内の特定文字をカウント
count := 0;
i := 1;
while i <= len(text) do
	if mid(text, 1, i) = target_char then
		count := count + 1;
	end_if;
	i := i + 1;
end_while;
構文

while文の構文は以下のとおりです。

// while文 ::= 'while' 条件式 'do'
//             文列
//             'end_while' ';'
while 条件式 do
	文列
end_while;
意味論

while文の動作は以下のとおりです。

// 無限ループパターンとexitによる脱出
while true do
	data := read_sensor();
	if data < 0 then
		exit; // 異常値を検出したらループを抜ける
	end_if;
	process(data);
end_while;

repeat文

repeat文は、until条件がtrueになるまで処理を繰り返す後判定ループ文です。条件の評価は本体の実行後に行われるため、少なくとも1回は必ず実行されます。「まず1回実行してから条件を確認する」という処理(入力の取得と検証など)に適しています。while文との違いは、条件判定のタイミングのみです。

sum := 0;
n := 1;
repeat
	sum := sum + n;
	n := n + 1;
until n = 5 end_repeat;
// sum = 10 = 1 + 2 + 3 + 4, n = 5.

入力の再試行パターンにも適しています。

// 有効な値が入力されるまで繰り返す(最低1回は実行)
retry := 0;
repeat
	value := read_sensor();
	retry := retry + 1;
until (value >= 0 and value <= 1000) or retry >= max_retry end_repeat;
構文

repeat文の構文は以下のとおりです。

// repeat文 ::= 'repeat'
//              文列
//              'until' 条件式 'end_repeat' ';'
repeat
	文列
until 条件式 end_repeat;
意味論

repeat文の動作は以下のとおりです。

while文との比較:

項目whilerepeat...until
条件判定タイミング本体実行前本体実行後
最小実行回数0回1回
ループ継続条件条件 = true条件 = false

continue文

continue文は、for文・while文・repeat文の現在のイテレーションの残り処理をスキップし、次のイテレーションに進みます。ループ自体は継続します。特定の条件を満たす要素を除外して処理したい場合に有用です。

for文でcontinueを使う場合、制御変数の更新(次の値への加算)は自動で行われます。while文・repeat文では、continue前に自分でカウンタを更新する必要があります(忘れると無限ループになります)。

sum := 0;
n := 1;
while n <= 4 do
	if n = 2 then
		n := n + 1; // continueの前にカウンタを更新
		continue;
	end_if;
	sum := sum + n;
	n := n + 1;
end_while;
// sum = 8 = 1 + 3 + 4, n = 5.
// for文でのcontinue(カウンタ更新は自動)
sum := 0;
for i := 1 to 10 do
	if (i mod 2) = 0 then
		continue; // 偶数をスキップ
	end_if;
	sum := sum + i; // 奇数のみ加算
end_for;
// sum = 25 = 1 + 3 + 5 + 7 + 9
各ループでのcontinue動作

各ループでのcontinueの動作は以下のとおりです。

// repeat文でのcontinue(untilへジャンプ)
n := 0;
repeat
	n := n + 1;
	if n = 3 then
		continue; // until節へジャンプ(本体先頭には戻らない)
	end_if;
	process(n); // n=3のときはスキップされる
until n >= 5 end_repeat;
// process は n = 1, 2, 4, 5 のときのみ呼ばれる

exit文

exit文は、for文・while文・repeat文のループを直ちに終了し、ループ本体外の次の文から実行を継続します。配列の線形探索など「目的の要素が見つかったらループを抜ける」処理に有用です。ネストしたループの場合、直接の囲みループのみを抜けます(外側のループは継続)。

sum := 0;
n := 1;
while n <= 4 do
	if n = 3 then
		exit;
	end_if;
	sum := sum + n;
	n := n + 1;
end_while;
// sum = 3 = 1 + 2, n = 3.
// 配列からの線形探索
found := false;
found_idx := -1;
for i := lower_bound(data, 1) to upper_bound(data, 1) do
	if data[i] = target then
		found := true;
		found_idx := i;
		exit; // 見つかったらループ終了
	end_if;
end_for;
ネストしたループでのexit

exit直接の囲みループのみを終了させます。外側のループは継続します。外側のループも終了させたい場合は、フラグ変数を使用します。

// 二重ループでフラグを使って外側も抜けるパターン
found := false;
found_row := -1;
found_col := -1;

for row := 1 to rows do
	for col := 1 to cols do
		if matrix[row, col] = target then
			found := true;
			found_row := row;
			found_col := col;
			exit; // 内側のfor文のみ終了(外側は継続)
		end_if;
	end_for;
	if found then
		exit; // フラグを使って外側のfor文も終了
	end_if;
end_for;

return文

return文は、POUの実行を途中で終了して呼び出し元に戻ります。ファンクション・FBのメソッド・プログラムPOUのいずれでも使用できます。ファンクションでは戻り値変数(ファンクション名と同名の変数)に値を設定した上でreturnを実行します。returnに値を直接指定することはできません(シンタックスエラー)。

早期リターン(ガード節パターン)により、ネストの深い条件分岐を平坦化できます。

// ガード節パターン(早期リターンでネストを浅く)
function safe_divide: lreal
	var_input
		a: lreal;
		b: lreal;
	end_var
	if b = 0.0 then
		safe_divide := 0.0; // 戻り値を設定
		return;             // 早期リターン
	end_if;
	safe_divide := a / b;
end_function

program Main
	...
	if not system_ready then
		return; // 前提条件が満たされなければスキップ
	end_if;
	// 以降の処理はsystem_readyがtrueの場合のみ実行
	...
end_program
// return 値; は文法エラー
function f: int
	var_input n: int; end_var
	f := n * 2;
	return;       // OK: returnで早期脱出
	// return 99; // Compile-time error! Syntax error.
end_function
使用できるPOU

returnを使用できるPOUと、各POUでの動作を以下に示します。

// ファンクションブロックでのreturn(内部状態は保持される)
function_block TCounter
	var
		count: int := 0;
	end_var
	var_input
		enable: bool;
		reset: bool;
	end_var

	if reset then
		count := 0;
		return; // ここでFB本体の実行終了(count=0が保持される)
	end_if;

	if not enable then
		return; // enableがfalseなら以降の処理をスキップ
	end_if;

	count := count + 1;
end_function_block
// プログラムPOUでのreturn(次スキャンサイクルで最初から再開)
program Main
	var
		initialized: bool := false;
	end_var

	if not initialized then
		init_system();
		initialized := true;
		return; // 初期化スキャンはここで終了
	end_if;

	// 通常処理(初期化済みの場合のみ実行)
	run_main_loop();
end_program

標準POU

IEC 61131-3 では、標準関数群と標準ファンクションブロック群が定義されています。各POUの詳細なインターフェース、動作仕様、規格参照、使用例については、 IEC 61131-3 標準POU を参照ください。

自作ライブラリ

JiecLib@GitHubに有用・無用含めて自作のライブラリを公開しています。

ツールの試用期間活用し、オムロン、キーエンスにて動作確認しています。不具合ありましたら、こちらのブログにご連絡いただければ可能な範囲で対応致します。本書のプログラム例などで示されるIEC 61131-3規格のテキスト表現をIEC 61131-10規格のXML表現に変換するツールJieccも使用ください。いくつかのベンダーでインポートできるようになります。

APPENDIX

IEC 61131-3にないもの