IEC 61131-3は、FA(Factory Automation)のプログラミング言語に関する国際規格です。次の特徴があります。
一方、安全性を重視しているため、関数の再帰呼出しを制限していたり、関数ポインタを含むポインタ型を代替する機能が制約されていたりと機能的な自由度は高くありません。「APPENDIX. IEC 61131-3にないもの」も参照ください。
日本のFAにおけるIEC 61131-3の浸透は十分とは言えません。従来のアドレスプログラミングやラダーによる巻物スタイルのプログラミングは根強く残っており、IEC 61131-3が良いだろうことは感じていても、約20年もの間、IEC 61131-3への移行は進んでいません。
これには、IEC 61131-3規格側の問題、ベンダー側(PLCメーカーなど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演算子をキーワードに含めていません。
識別子は、変数やデータ型などを識別するための名前であり、次のすべてを満たさなければいけません。
例えば、`abc`、`D2E_F3`、`_hi_jk_lmn`は識別子になりえますが、`5op`、`__qr`、`s__t`、`uvw_`は識別子ではありません。
また、識別子の大文字・小文字を区別しません。`Abc`、`abc`、`ABC`は同じ識別子と解釈されます。
パス、又はパス文字列は、以下の要素で登場する%で始まる文字列です。
%[A-Z0-9\.\*]+%[A-Z0-9](\.?[A-Z0-9])*\*?|%\*%[IQM][XBWDL]?[0-9]+(\.[0-9]+)*|%[IQM][XBWDL]?\*|%\*|%[XBWDL][0-9]+(\.[0-9]+)?プラグマは、{ ~ }で囲われた要素であり、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の例 | 説明 |
|---|---|---|---|---|---|---|
| 符号あり整数型 | sint | 8 | -128~127 | 0 | -59,13,1_234_567,2#11_01,-16#2f
| |
int | 16 | -32768~32767 | ||||
dint | 32 | -2147483648~2147483647 | ||||
lint | 64 | -9223372036854775808~9223372036854775807 | ||||
| 符号なし整数型 | usint | 8 | 0~255 | |||
uint | 16 | 0~65535 | ||||
udint | 32 | 0~4294967295 | ||||
ulint | 64 | 0~18446744073709551615 | ||||
| 浮動小数点型 | real | 32 | IEC 60559単精度浮動小数点規格の範囲に従う | 0 | 0.1,1.0e-6,-1e3,-5,16#ff | |
lreal | 64 | IEC 60559倍精度浮動小数点規格の範囲に従う | ||||
| ビット列型 | byte | 8 | 16#00~16#ff | 16#00 | byte#16#ff,byte#2#1111_1111_1111_1111,dword#16#cafe_babe | |
word | 16 | 16#0000~16#ffff | 16#0000 | |||
dword | 32 | 16#0000_0000~16#ffff_ffff | 16#0000_0000 | |||
lword | 64 | 16#0000_0000_0000_0000~16#ffff_ffff_ffff_ffff | 16#0000_0000_0000_0000 | |||
| ブール型 | bool | 1 | false(偽)、又はtrue(真) | false | true,bool#0 | |
| 時間型 | time | 実装依存 | 実装依存 | 0秒 | time#2s,time#0s,time#-3s,t#2s | 最小単位も含めて実装依存のため、time型ではなく、ltime型の使用を推奨します。 |
ltime | 64 | -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 | |
ldate | 64 | 1970年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_day | 64 | 0時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_time | 64 | 1970年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 | ||||||
| 文字型 | char | 8 | 実装依存; ISO/IEC 10646規格の符号化文字集合の部分的な一文字 | NULL文字('$00') | 'a','$00' // NULL文字 | |
wchar | 16 | 実装依存; ISO/IEC 10646規格の符号化文字集合の部分的な一文字 | NULL文字("$0000") | "a","$0000" // NULL文字 | ||
| 文字列型 | string | 8*N | 実装依存; ISO/IEC 10646規格の符号化文字集合の部分的な0個以上N個以下の文字の列 | 空文字列('') | 'hello','abc','$61$62$63' // = 'abc','' // 空文字列 | |
wstring | 16*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を表します。
| 種類 | データ型 | ビット幅 | 範囲 | デフォルト初期値 |
|---|---|---|---|---|
| 符号あり | sint | 8 | -128 〜 127 | 0 |
int | 16 | -32768 〜 32767 | 0 | |
dint | 32 | -2147483648 〜 2147483647 | 0 | |
lint | 64 | -9223372036854775808 〜 9223372036854775807 | 0 | |
| 符号なし | usint | 8 | 0 〜 255 | 0 |
uint | 16 | 0 〜 65535 | 0 | |
udint | 32 | 0 〜 4294967295 | 0 | |
ulint | 64 | 0 〜 18446744073709551615 | 0 |
整数リテラルは、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の使用を推奨します。
| データ型 | ビット幅 | 有効桁数 | 範囲(絶対値) | デフォルト初期値 |
|---|---|---|---|---|
real | 32 | 約7桁 | ±1.175494351e-38 〜 ±3.402823466e+38 | 0.0 |
lreal | 64 | 約15〜16桁 | ±2.2250738585072014e-308 〜 ±1.7976931348623157e+308 | 0.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の数学ファンクション(sqrt、sin、cos、tan、exp、lnなど)も使用できます。
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)パターンのビット列を表現できます。
| データ型 | ビット幅 | 範囲 | デフォルト初期値 |
|---|---|---|---|
byte | 8 | 16#00 〜 16#FF | 16#00 |
word | 16 | 16#0000 〜 16#FFFF | 16#0000 |
dword | 32 | 16#00000000 〜 16#FFFFFFFF | 16#00000000 |
lword | 64 | 16#0000000000000000 〜 16#FFFFFFFFFFFFFFFF | 16#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#Aやrealv=1.036831936e+9を得ます。
bool型は、真(true)または偽(false)の2値を表現する型です。条件分岐、繰り返し制御、比較演算の結果、インタロック条件など、制御プログラムの多くの場面で使用します。
bool型のデフォルト初期値はfalseです。論理値の保持に特化した型であり、整数型・ビット列型・実数型とは区別されます。
| データ型 | ビット幅 | 値域 | デフォルト初期値 |
|---|---|---|---|
bool | 1 | false / true | false |
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秒 |
ltime | 64 | -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 | 実装依存 | 実装依存 | 実装依存 | 実装依存 |
ldate | 64 | 1970年1月1日 ~ 実装依存 | 1日 | 1970年1月1日 | |
| 時刻型 | time_of_day (tod) | 実装依存 | 実装依存 | 実装依存 | 実装依存 |
ltime_of_day (ltod) | 64 | 0時0分0秒~23時59分59.999999999秒 | ナノ秒 | 0時0分0秒 | |
| 日付時刻型 | date_and_time (dt) | 実装依存 | 実装依存 | 実装依存 | 実装依存 |
ldate_and_time (ldt) | 64 | 1970年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) のいずれかに従いますが、string、wstringともに、符号化文字集合や扱える文字の範囲は実装依存です※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文字のバイト数 | 符号化文字集合 | 文字符号化方式 | 最大長 | デフォルト初期値 |
|---|---|---|---|---|---|
string | 1 | 実装依存; ISO/IEC 10646の符号化文字集合の部分 | 実装依存; UTF-8? | 実装依存 | ''(空文字列) |
wstring | 2 | 実装依存; 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 | ラインフィード LF | 16#0A |
$n/$N | 改行 | 実装依存※3 |
$p/$P | ページ送り FF | 16#0C |
$r/$R | 復帰 CR | 16#0D |
$t/$T | 水平タブ HT | 16#09 |
$hh | 2桁16進数で表した文字コードの文字(string型リテラル内のみ) | 例: $41 → A |
$hhhh | 4桁16進数で表した文字コードの文字(wstring型リテラル内のみ) | 例: $0041 → A |
文字列操作には標準ファンクションを使用します。文字の位置を指定する引数(pos)は1始まりです。これはC言語など多くの言語の0始まりとは異なる点に注意してください。例えば、find('abcde', 'cd')は3を返します。
len(str): 文字数を返すconcat(str1, str2, ...): 文字列を連結するleft(str, len)/right(str, len): 左端/右端からlen文字を取得mid(str, len, pos): pos文字目からlen文字を取得(1始まり)insert(str1, str2, pos): pos文字目の位置にstr2を挿入delete(str, len, pos): pos文字目からlen文字を削除replace(str1, str2, len, pos): pos文字目からlen文字をstr2で置換find(str1, str2): str1内でstr2が最初に現れる位置を返す(0=見つからない)
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_string、string_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
最大長を超えた文字列を代入した場合の動作(切り捨てかエラーか)は実装依存です。また、stringとwstringは互いに暗黙の変換がなく、異なる型として扱われます。マルチバイト文字(日本語など)を含む場合、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~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
変数宣言には、constant、at、r_edge/f_edge、retain/non_retainなどの追加属性を付加できます。属性は型名の後に記述します。
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属性は変数をハードウェアの物理アドレス(I/Oデバイス等)に関連付けます。変数へのアクセスが、指定した物理アドレスへのアクセスになります。
アドレスは%に続いてロケーション・幅・番号を組み合わせて記述します。
I(入力)、Q(出力)、M(内部メモリ)X(1ビット)、B(バイト)、W(ワード)、D(ダブルワード)、L(ロングワード)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
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) のようなインフォーマルな呼び出し記法では、en、enoを指定できません。また、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#eRed、Day#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_bound、upper_boundを使用します。
lower_bound、upper_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
一般的なプログラミング言語の関数との違いや特徴を以下に示します。
ファンクション定義を一般的に次のように記述できます。
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定義を一般的に次のように記述できます。
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インスタンスに対する呼び出しの記法において、フォーマル呼び出しとインフォーマル呼び出しを混在させることができるかどうかは実装依存です。
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インスタンス名>.<ローカル変数名>
例:
// 入力変数へのアクセス 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(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
クラス(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.Inner は namespace 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の名前解決規則の要点:
名前空間名.識別子 と修飾します。using 名前空間名; を宣言すると、そのPOU/メソッドのスコープ内で修飾なしに参照できます。using は直接指定した名前空間のみ有効で、入れ子になった名前空間は自動的にインポートされません。using はグローバル名前空間・名前空間本体・POU本体・メソッド本体のいずれにも記述でき、記述したスコープ内でのみ有効です。_. プレフィックスを使用します(名前空間内に同名の要素がある場合にグローバル名前空間の識別子を明示したい場合など)。
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
// 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 | 同一名前空間内のみ |
| internal | public | 直接の親(外側)名前空間からのみ可 |
| internal | internal | 同一名前空間内のみ |
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 にのみ存在するため一意
メソッド内の 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_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は、変数の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規格に示されません。効用的には、部分指定というより、遅延指定という表現の方が適切と考えていますし、%*という表現もあって良いと考えます。
複数の演算子が混在する式においては、優先順位の高いものから評価されます。同一の優先順位をもつ場合、左から右に結合します(左結合)。他のプログラミング言語同様、括弧を使用することで、優先的に評価することができます。
ただし、結合規則はどの部分式を先にグループ化するかを定めるものであり、各オペランドの実際の評価順序は実装依存です。
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_input)・出力パラメータ(var_output)の割付けに適用できるadd_int)の入力・出力パラメータの割付けにも適用できるvar_in_out(入出力パラメータ)への割付けには適用してはならない(型は完全一致が必要)0、1.5など型プレフィックスのないリテラル)の型解釈は実装依存。型付リテラル(int#0等)を使うと曖昧さを回避できる
// 暗黙的型変換の例
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(明示的な型変換が必要、暗黙の型変換不可)・-(未定義)・空欄(変換不要、恒等変換)が規定されています。- のセルは実装者が独自仕様でサポートしてもよいとされており、コンパイラにより動作が異なります。
| 変換元 | 変換先 | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 符号あり整数 | 符号なし整数 | 浮動小数点 | ビット列 | ブール | 時間 | 日付時刻 | 文字 | 文字列 | |||||||||||||||
| sint | int | dint | lint | usint | uint | udint | ulint | real | lreal | byte | word | dword | lword | bool | time | ltime | ldate/date | ldt/dt | char | wchar | string | wstring | |
| sint | i | i | i | e | e | e | e | i | i | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| int | e | i | i | e | e | e | e | i | i | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| dint | e | e | i | e | e | e | e | e | i | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| lint | e | e | e | e | e | e | e | e | e | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| usint | e | i | i | i | i | i | i | i | i | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| uint | e | e | i | i | e | i | i | i | i | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| udint | e | e | e | i | e | e | i | e | i | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| ulint | e | e | e | e | e | e | e | e | e | e | e | e | e | - | - | - | - | - | - | - | - | - | |
| real | e | e | e | e | e | e | e | e | i | - | - | - | - | - | - | - | - | - | - | - | - | - | |
| lreal | e | e | e | e | e | e | e | e | e | - | - | - | - | - | - | - | - | - | - | - | - | - | |
| byte | e | e | e | e | e | e | e | e | - | - | i | i | i | e | - | - | - | - | e | - | - | - | |
| word | e | e | e | e | e | e | e | e | - | - | e | i | i | e | - | - | - | - | e | - | - | - | |
| dword | e | e | e | e | e | e | e | e | e | - | e | e | i | e | - | - | - | - | - | - | - | - | |
| lword | e | e | e | e | e | e | e | e | - | e | e | e | e | e | - | - | - | - | - | - | - | - | |
| bool | e | e | e | e | e | e | e | e | - | - | i | i | i | i | - | - | - | - | - | - | - | - | |
| time | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | i | - | - | - | - | - | - | |
| ltime | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | e | - | - | - | - | - | - | |
| char | - | - | - | - | - | - | - | - | - | - | e | e | e | e | e | - | - | - | - | i | - | - | |
| wchar | - | - | - | - | - | - | - | - | - | - | - | e | e | e | - | - | - | - | - | i | - | - | |
| string | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | e | - | - | |
| wstring | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | e | |
凡例: i=暗黙の型変換可(明示的な型変換も可)、e=明示的な型変換が必要(暗黙の型変換不可)、-=未定義(実装者が独自仕様でサポートしてもよい)、空欄=変換不要(恒等変換)
表に示されない日付時刻型どうし(例: ldt→dt)は明示変換ファンクション(ldt_to_dt等)が標準で用意されています(規格で定義あり、表は省略)。string→wstring・char→wcharが e(暗黙変換不可)なのは、使用している文字集合との不整合を避けるためです。
演算子の結果型については、多重定義演算子の場合、オペランドが同じデータ型になるように暗黙変換を適用でき、演算結果はそのデータ型になります。例えば 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インスタンスのメンバに外部からドット演算子でアクセスできる範囲は、変数の種別ごとに規定されています。
| 変数種別 | 外部からの読み取り | 外部からの書き込み | 備考 |
|---|---|---|---|
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=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のオペランドがブール型の値に対して使用される演算です。ブール値を反転した値を得ます。
boolv := not true; // false boolv := not false; // true
論理積、論理和、排他的論理和演算は、それぞれ二項演算子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のオペランドがビット列型の値に対して使用される演算です。全ビットを反転した値を得ます。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はビット積(両ビットが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)。規格上、安全に代入できる。e)。コンパイラがエラーを出すべきだが、警告のみで通す実装もある。i)。ただし lint→real など精度が落ちる場合がある(変換は可能だが精度損失が起きうる)。e)。x := 0 の 0、myWord := not(0) の 0): 実装依存。型プレフィックス(word#0 など)で明示することを推奨。
// 安全: 暗黙変換可(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)インスタンスへの代入は、コピー代入の可否・内部状態の取り扱いなど仕様の詳細が実装依存です。
試行代入演算子?=は、インタフェース型・クラス型・FBインスタンス型の参照間の安全なダウンキャスト(下位型への変換代入)に使用します。右辺の参照が左辺の型を実装・継承している場合は代入が成功し、そうでない場合は左辺がnullになります。左辺・右辺ともに参照型またはインタフェース型でなければなりません。試行代入を参照。
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文の動作は以下のとおりです。
bool型でなければなりません。trueになった節の文列のみを実行します。falseでelse節がない場合、何も実行しません(エラーではありません)。elsif節は0個以上記述できます。else節は0個か1個です。末尾のセミコロンは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文は、整数型(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文の動作は以下のとおりです。
any_int)または列挙型のみ使用できます。breakは不要、かつ存在しません)。a..bはa ≤ bが前提です。a > bの場合の動作は実装依存です。else節はどの値にも一致しなかった場合に実行されます(省略可)。else節を省略し何も一致しなかった場合、何も実行しません(エラーではありません)。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文の動作は以下のとおりです。
any_int)でなければなりません。byを省略した場合のステップは+1です。初期値が終了値より大きい場合(e.g. for n := 4 to 1 do)、最初から終了条件を満たすためループ本体は0回実行されます。// ステップが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文は、条件式が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文の動作は以下のとおりです。
bool型でなければなりません。falseであれば、本体は一度も実行されません。while true do ... end_while;で意図的な無限ループを作れます。無限ループから抜けるにはexitを使用します。// 無限ループパターンとexitによる脱出 while true do data := read_sensor(); if data < 0 then exit; // 異常値を検出したらループを抜ける end_if; process(data); end_while;
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文の動作は以下のとおりです。
bool型でなければなりません。until条件がtrueになるとループを終了します(whileのループ継続条件とは向きが逆であることに注意してください)。while文との比較:
| 項目 | while | repeat...until |
|---|---|---|
| 条件判定タイミング | 本体実行前 | 本体実行後 |
| 最小実行回数 | 0回 | 1回 |
| ループ継続条件 | 条件 = true | 条件 = false |
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の動作は以下のとおりです。
until節の条件を評価します(本体先頭には戻りません)。// 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文は、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は直接の囲みループのみを終了させます。外側のループは継続します。外側のループも終了させたい場合は、フラグ変数を使用します。
// 二重ループでフラグを使って外側も抜けるパターン 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文は、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
returnを使用できるPOUと、各POUでの動作を以下に示します。
returnを実行します。returnは戻り値を引数に取りません(return 値;は文法エラーです)。returnでFBの実行を終了します。内部状態(var変数)はreturn後も保持されます。returnで当該メソッドの実行を終了します。戻り値を設定してからreturnを実行します。returnでプログラムの実行を終了します。次のスキャンサイクルで最初から再開されます。// ファンクションブロックでの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
IEC 61131-3 では、標準関数群と標準ファンクションブロック群が定義されています。各POUの詳細なインターフェース、動作仕様、規格参照、使用例については、 IEC 61131-3 標準POU を参照ください。
JiecLib@GitHubに有用・無用含めて自作のライブラリを公開しています。
array - 基本的な配列操作ライブラリです。lreal / dint の型別テンプレート対応。bitset - 32ビット整数(DWORD)をビット集合として扱うライブラリです。個々のビットの設定・取得からビットフィールドの抽出・挿入まで対応します。compress - データ圧縮ライブラリです。入力配列と出力配列を var_in_out 引数で受け取り、書き込んだバイト数を返します。container - テンプレートベースのコンテナライブラリです。arraylist、linkedlist、dequeue、hashmap、orderedmap、linkedhashmap、hashset、orderedset、linkedhashset、stack、ringbuffer を含みます。encoding - エンコーディング関連ライブラリです。hash - ハッシュ関連ライブラリです。文字列入力版(_string)とバイト配列入力版(_bytes)を提供します。linalg - 行列中心の線形代数ライブラリです。math - 数学関数ライブラリです。random - 乱数生成ライブラリです。FA用途に必要なアルゴリズムと統計分布を提供します。stats - 統計ライブラリです。string_lib - 基本的な文字列操作ライブラリです。JavaScriptやPythonライクなAPIを提供します。vector - ベクトル演算ライブラリです。ツールの試用期間活用し、オムロン、キーエンスにて動作確認しています。不具合ありましたら、こちらのブログにご連絡いただければ可能な範囲で対応致します。本書のプログラム例などで示されるIEC 61131-3規格のテキスト表現をIEC 61131-10規格のXML表現に変換するツールJieccも使用ください。いくつかのベンダーでインポートできるようになります。