🧅

Sysmac Studioでの組み込み命令によるキューと双方向リストによるキュー

に公開

はじめに

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

今回は、組み込み命令と双方向リストによるキューを確認します。Sysmac Studioの命令語リファレンスにスタックのサンプルプログラムはありますが、キューのサンプルプログラムが無いので確認します。キュー(待ち行列)という用語をあまり耳にしなくてもシフト命令を使用しているのであれば、何らかの形で実質的に先入れ先出しのキューを扱っている可能性があります。

キューの操作に使用する組み込み命令は以下です。いずれも"Stack"という単語を含んでいますが、これらがデータ構造としてのキューを操作する命令です。詳細は、命令語リファレンスを確認するのが確実です。

  • StackPush
  • StackFIFO
  • StackIns
  • StackDel

キューそのものと双方向リストについての確認はしません。いずれも書籍、インターネットから有益な情報を入手できます。また、Geminiのようなチャットボットからでも十分な回答が得られます。

Sysmacプロジェクト

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

https://github.com/kmu2030/QueueExample

組み込み命令を使用した値の配列によるキュー

まずは、命令語リファレンスに出てきそうなキューを確認します。リファレンスのスタックの使用例を参考にすれば、値の配列に対してキューを構築することになります。キューへの要素追加(エンキュー)はStackPush、キューからの要素取得(デキュー)はStackFIFOです。これらは、PLCとされるものではほぼ存在するであろうシフト命令のような振る舞いをします。つまり、配列の要素を直接ずらします。StackInsは指定したオフセットへの要素挿入、StackDelは指定したオフセットの要素削除ですが、これらも配列の要素を直接ずらします。

サンプルプログラムは以下です。キュー操作とした場合の操作をコメントとして付してあります。シミュレータでステップ実行することで値の配列の変化を確認できます。

// Queue_init()
// Entities:||
Clear(iEntities);
Clear(iEntityNum);
Clear(iEntity);
iQueueCapacity := SizeOfAry(iEntities);

iNextValue := 100;

// Queue_enqueue()
// Queue:|100|
iEntity.Value := iNextValue;
iEntity.Index := iEntityNum;
StackPush(In:=iEntity,
          InOut:=iEntities[0],
          Size:=iQueueCapacity,
          Num:=iEntityNum);

Inc(iNextValue);

// Queue_enqueue()
// Queue:|100,101|
iEntity.Value := iNextValue;
iEntity.Index := iEntityNum;
StackPush(In:=iEntity,
          InOut:=iEntities[0],
          Size:=iQueueCapacity,
          Num:=iEntityNum);

Inc(iNextValue);

// Queue_enqueue()
// Queue:|100,101,102|
iEntity.Value := iNextValue;
iEntity.Index := iEntityNum;
StackPush(In:=iEntity,
          InOut:=iEntities[0],
          Size:=iQueueCapacity,
          Num:=iEntityNum);

Inc(iNextValue);

// Queue_dequeue()
// Queue:|101,102|
// 残存Entityは、この時点で保持するIndexが無効になる。
// そのため、EntityがIndexを保持するのであれば更新する。
StackFIFO(InOut:=iEntities[0],
          OutVal:=iEntity,
          Size:=iQueueCapacity,
          Num:=iEntityNum);
FOR i := 0 TO iEntityNum - 1 DO
    iEntities[i].Index := i;
END_FOR;

// Queue_enqueue()
// Queue:|101,102,103|
iEntity.Value := iNextValue;
iEntity.Index := iEntityNum;
StackPush(In:=iEntity,
          InOut:=iEntities[0],
          Size:=iQueueCapacity,
          Num:=iEntityNum);

Inc(iNextValue);

// Queue_remove()
// Entities:|101,103|
iValue := 102;
RecSearch(In:=iEntities[0],
          Size:=iEntityNum,
          Member:=iEntities[0].Value,
          Key:=iValue,
          InOutPos:=iPos[0],
          Num=>iNum);
StackDel(InOut:=iEntities[0],
         Size:=iQueueCapacity,
         Num:=iEntityNum,
         Offset:=iPos[0]);
FOR i := 0 TO iEntityNum - 1 DO
    iEntities[i].Index := i;
END_FOR;

// Queue_enqueue()
// Queue:|101,103,104|
iEntity.Value := iNextValue;
iEntity.Index := iEntityNum;
StackPush(In:=iEntity,
          InOut:=iEntities[0],
          Size:=iQueueCapacity,
          Num:=iEntityNum);

Inc(iNextValue);

// Queue_toArray()
// Array:[101,103,104]
FOR i := 0 TO iEntityNum - 1 DO
    iValues[i] := iEntities[i].Value;
END_FOR;

エンキュー、デキュー共に組み込み命令を使用するだけです。変数は箱、配列は連続した箱というメタファーやシフト命令に慣れている場合に分かりやすいと思います。率直な手段ですが、要件を満たしコストが見合うのであれば、使用することに問題があるわけではありません。小さなキューであれば、組み込み命令を使用した値のキューは有益です。

この方法では、値の配列でのインデックスは、都度更新をしなければ参照情報として機能しません。実用的ではありませんが、他と合わせるために値にインデックスを持たせ、必要な個所で更新しています。

組み込み命令を使用した値の配列のインデックスによるキュー

次に、値を保持する配列のインデックスに対してキューを構築します。配列のインデックスに対してキューを構築することで、構造と値を分離することができます。構造と値を分離することで、複数POUからの読み出し参照ができるようになり、ワークの状態を一元的に管理するマスターテーブルを構築してモニタを簡素にすること等ができます。

インデックスは、値が存在する間の疑似的なポインタです。多くのPLCの実行環境にはポインタはありませんが、必要であればその概念を限定的に導入することはできます。ダングリングポインタのような問題も引き込むことになりますが、適当な対策と過大な構想を避けることでポインタと見なすことによる利点を活かすこともできます。

インデックスは、依然として配列のインデックスであることに変わりありません。他の環境にあるようなポインタとは異なります。無理に慣れない考え方を持ち込む必要はありません。手段として負担なく扱えるのであれば、考え方やフレームを持ち込めるというだけです。素朴な手段で十分であることは多々あるはずです。しかし、素朴な手段や考え方では扱いきれない複雑さや規模に対し、異なる見方を持ち込むことで対処できることもあります。

サンプルプログラムは以下です。値の配列をキューとした場合とそれほど変わりません。しかし、この少しの差異が柔軟性をもたらします。サンプルプログラムでは、値を生成するタイミングとキューに追加するタイミングが同じですが、必ずしもそうである必要はありません。

// Queue_init()
// Queue:||
Clear(iEntities);
Clear(iIndexes);
Clear(iQueueNum);
Clear(iEntity);
Clear(iEntityHead);
iQueueCapacity := SizeOfAry(iEntities);

iNextValue := 100;

// Queue_enqueue()
// Queue:|100|
iEntity.Value := iNextValue;
iEntity.Index := iEntityHead;
iEntities[iEntity.Index] := iEntity;
StackPush(In:=iEntity.Index,
          InOut:=iIndexes[0],
          Size:=iQueueCapacity,
          Num:=iQueueNum);

Inc(iNextValue);
Inc(iEntityHead);

// Queue_enqueue()
// Queue:|100,101|
iEntity.Value := iNextValue;
iEntity.Index := iEntityHead;
iEntities[iEntity.Index] := iEntity;
StackPush(In:=iEntity.Index,
          InOut:=iIndexes[0],
          Size:=iQueueCapacity,
          Num:=iQueueNum);

Inc(iNextValue);
Inc(iEntityHead);

// Queue_enqueue()
// Queue:|100,101,102|
iEntity.Value := iNextValue;
iEntity.Index := iEntityHead;
iEntities[iEntity.Index] := iEntity;
StackPush(In:=iEntity.Index,
          InOut:=iIndexes[0],
          Size:=iQueueCapacity,
          Num:=iQueueNum);

Inc(iNextValue);
Inc(iEntityHead);

// Queue_dequeue()
// Queue:|101,102|
StackFIFO(InOut:=iIndexes[0],
          OutVal:=iIndex,
          Size:=iQueueCapacity,
          Num:=iQueueNum);
iEntity := iEntities[iIndex];

// Queue_enqueue()
// Queue:|101,102,103|
iEntity.Value := iNextValue;
iEntity.Index := iEntityHead;
iEntities[iEntity.Index] := iEntity;
StackPush(In:=iEntity.Index,
          InOut:=iIndexes[0],
          Size:=iQueueCapacity,
          Num:=iQueueNum);

Inc(iNextValue);
Inc(iEntityHead);

// Queue_remove()
// Queue:|101,103|
iValue := 102;
i := 0;
WHILE i < iQueueNum DO
    IF iEntities[iIndexes[i]].Value = iValue THEN
        StackDel(InOut:=iIndexes[0],
                 Size:=iQueueCapacity,
                 Num:=iQueueNum,
                 Offset:=i);
        EXIT;
    END_IF;
    Inc(i);
END_WHILE;

// Queue_enqueue()
// Queue:|101,103,104|
iEntity.Value := iNextValue;
iEntity.Index := iEntityHead;
iEntities[iEntity.Index] := iEntity;
StackPush(In:=iEntity.Index,
          InOut:=iIndexes[0],
          Size:=iQueueCapacity,
          Num:=iQueueNum);

Inc(iNextValue);
Inc(iEntityHead);

// Queue_toArray()
// Array:[101,103,104]
FOR i := 0 TO iQueueNum - 1 DO
    iValues[i] := iEntities[iIndexes[i]].Value;
END_FOR;

値の配列によるキューと比較すると、値を参照するのにインデックスの配列を介することが異なります。値の配列の使用済み領域を再利用するための仕組みは省略しています。使用済み領域の再利用は、値の配列の使用済み領域のインデックスをスタックかキューで管理することで実現できます。メリットが見えてこないかもしれないので、使えそうな場面を考えてみます。

複数の工程があり、かつ、各工程は進行状況によって組み換え可能であるとします。あるいは、ボトルネック回避のためにいくつかの工程は多重化し、ワークはその時々で最も余力のあるラインに振り分けるとします。これを単一のPLCで構築するのであれば、各工程の待ち行列(キュー)とワークの状態を把握するためのテーブルを構築することになります。もちろん、異なる設計もあり得ますし、そのような指示はDCSやSCADAの役割かもしれません。しかし、振り分けの計算をPLCで賄えるのであればできないことではありません。

双方向リストによるキュー

双方向リストは、キューを構築するオーソドックスなデータ構造です。処理系によって双方向リストの実装手段は異なりますが、STであれば構造体を使用して実装すると分かりやすくなります。双方向リストの実装は難しくありません。2ないし3つのUINT型メンバーを持つ構造体の配列があれば、双方向リストを構築できます。

UINT型の一次元配列でも双方向リストは構築できます。基本型のみで双方向リストを構築したい場合、UINT型の一次元配列を双方向リストとして操作する一連のPOUを定義します。汎用的なデータ構造ライブラリを目指す場合に適した方針ですが、デバッグに手間がかかるので品質が悪いと使い物になりません。十分なテストを必要とします。

今回は、手持ちの双方向リストライブラリを使用します。少しクセがあるのですが、必要とするリスト操作は備えているので使用します。サンプルプログラムは以下です。双方向リストはメモリを再利用する仕組みを持っていないため、キューの構造体は双方向リストを2つメンバーに持たせて、使用済みのメモリを再利用するようにします。また、リスト長を任意とするために、連結を保持する配列(Indexes)は外部化しています。そして、キュー操作のための一連のリスト操作をキュー操作のPOUとして定義しています。

// Queue_init()
// Entities:||
Queue_init(Queue:=iQueue,
           Indexes:=iIndexes);

iNextValue := 100;

// Queue_enqueue()
// Queue:|100|
Queue_enqueue(Queue:=iQueue,
              Indexes:=iIndexes,
              Index=>iEntity.Index);
iEntity.Value := iNextValue;
iEntities[iEntity.Index] := iEntity;

Inc(iNextValue);

// Queue_enqueue()
// Queue:|100,101|
Queue_enqueue(Queue:=iQueue,
              Indexes:=iIndexes,
              Index=>iEntity.Index);
iEntity.Value := iNextValue;
iEntities[iEntity.Index] := iEntity;

Inc(iNextValue);

// Queue_enqueue()
// Queue:|100,101,102|
Queue_enqueue(Queue:=iQueue,
              Indexes:=iIndexes,
              Index=>iEntity.Index);
iEntity.Value := iNextValue;
iEntities[iEntity.Index] := iEntity;

Inc(iNextValue);

// Queue_dequeue()
// Queue:|101,102|
Queue_dequeue(Queue:=iQueue,
              Indexes:=iIndexes,
              Index=>iIndex);
iEntity := iEntities[iIndex];

// Queue_enqueue()
// Queue:|101,102,103|
Queue_enqueue(Queue:=iQueue,
              Indexes:=iIndexes,
              Index=>iEntity.Index);
iEntity.Value := iNextValue;
iEntities[iEntity.Index] := iEntity;

Inc(iNextValue);

// Queue_remove()
// Queue:|101,103|
// Entityからの削除であれば、Indexが分かっているので検索は不要。
iValue := 102;
WHILE Queue_forEach(
          Queue:=iQueue,
          Indexes:=iIndexes,
          Iterator:=iIterator,
          Index=>iIndex,
          ListIndex=>iListIndex)
DO
    IF iEntities[iIndex].Value = iValue THEN
        Queue_remove(Queue:=iQueue,
                     Indexes:=iIndexes,
                     Index:=iIndex);
        Clear(iIterator);
        EXIT;
    END_IF;
END_WHILE;

// Queue_enqueue()
// Queue:|101,103,104|
Queue_enqueue(Queue:=iQueue,
              Indexes:=iIndexes,
              Index=>iEntity.Index);
iEntity.Value := iNextValue;
iEntities[iEntity.Index] := iEntity;

// Queue_toArray()
// Array:[101,103,104]
WHILE Queue_forEach(
          Queue:=iQueue,
          Indexes:=iIndexes,
          Iterator:=iIterator,
          Index=>iIndex,
          ListIndex=>iListIndex)
DO
    iValues[iListIndex] := iEntities[iIndex].Value;
END_WHILE;

組み込み命令を使用した値の配列のインデックスによるキューと実質的に同じです。キューの容量を固定し、ある程度使いまわしが効くものにするのであれば、インデックスの配列をキューの構造体のメンバーにします。値の配列をキューの構造体のメンバーにすると、値の配列によるキューに似た操作性になります。また、値がキューに存在する限り同一の参照インデックスを提供できます。

素朴な双方向リストであれば実装は困難ではありません。使える場面もあるので、実装して損になることもありません。今回使用した双方向リストは目的を持って作成したものなのでクセがあります。基本的なキュー操作に必要な双方向リスト操作は少ないので、双方向リストを自作して置き換えてみることをおススメします。

まとめ

今回は、Sysmac Studioでのキューを確認しました。3通りの方法で、同じ結果となるキュー操作をしましたが、キューの比較以外の見方もできます。POUは処理をまとめる手段と認識されているかもしれませんが、抽象化する手段でもあるということです。

何れもキューとしての振る舞いは同じですがそれ以外の機能性や処理コスト、実装の平易さに差異があります。また、何れのプログラムもキュー操作としてのPOUをコメントとして付しましたが、もし、これらのシグネチャが同一であるようにそれぞれのPOUを実装すれば、それらは実質的に可換です。ユーザーが認識するキューは、一連のPOUと構造体の定義であり、その実装ではないということです。

POU、特にFunctionはインターフェースではありませんが、実装を隠蔽する手段とすることができます。必ずしも言語仕様、あるいは処理系に隠蔽、抽象化手段が無ければそれらを実現できないわけではありません。上手く作られたC言語のライブラリはヘッダーの定義を確認するだけで必要なことを把握でき、データ型の詳細に踏み込まなくても必要とする処理を行うことができます。これと同じことを、インターフェースがサポートされていないST処理系でも行うことができます。

Discussion