C言語によるPLCのプログラミング:デジタル I/Oの構造化処理のためのポインタの活用


APDahlen Applications Engineer

この記事は、数回にわたってC言語でPLCをプログラミングする方法を説明する記事の最初のものです。機械制御と安全性に重点を置いてプログラミングスキルを向上させたい中級プログラマーを対象としています。開発プラットフォームとして、図1に示すArduino Optaを使用します。購入情報および関連するTechForum記事のリストについては、このページを参照してください。

主な目的は、プログラムの煩雑さを減らしてプログラムのロジックを簡素化することです。その例をこのコードスニペットに示します。ifステートメントはセレクタスイッチ、赤の押ボタン、緑の押ボタンを調べます。ステータスに基づき、実行状態と赤/緑のパネルランプの色を設定します。このコードの手法は、入力と出力がスーパーループの先頭で読み込まれる従来のPLCプログラムスキャンに従って強化されたポインタ活用であると表現するのが最適です。

コードのダウンロード:DemoIOUtil.zip(2.0 KB)

 if (localSelectorSwitch.value && localStartPB.value && !localStopPB.value) {
    runState = true;
    localPLRed.value = false;
    localPLGreen.value = true;
  }

技術的なヒント: 組み込みシステムにおいてC言語とC++のどちらを使うかについての議論が続いています。Arduinoのコードベースの大部分はC++で書かれています。しかし、C++に関連する追加の保護、抽象化、および洗練さと比較して、C言語の速度とオーバーヘッドの大きさの間にはバランスがあるため、これは普遍的に受け入れられているわけではありません。組み込みプロジェクトがどのように実装されるかによって、これらのすべてが適用されたり、適用されなかったりします。

この記事は、使いやすいArduinoから組み込みプログラミングへの移行期にある学生向けに書かれています。良くも悪くも、C言語のような構造体が使われています。次のステップに進み、ルーチンをC++に変換し、組み込みプロジェクトにどちらの方法が適しているか自分で判断することをお勧めします。

図1: DigiKeyトレーナに取り付けられたArduino Optaの写真。IEC 61131-3のC言語またはC++でプログラミングが可能です。

技術的なヒント: PLCプログラミングの出発点として、IEC 61131-3のラダーロジックを使用したPLCプログラミングを学ぶことをお勧めします。PLCの習得は、単なるプログラミング以上のものであることがわかります。なぜなら PLCは物理的な世界とリアルタイムでやり取りするように設計されているからです。ラダーロジックは、PLC制御とインターフェースの大部分を占めるオン/オフの条件分岐についての考え方を形づくる良い方法です。

Arduinoの快適さを超える移行

図1のOptaは過小評価されている製品です。Opta自体は8つの入力と4つのリレー出力を持っていますが、拡張可能であることが分かっています。例えば、デジタルとアナログの拡張モジュールを追加することができます。また、Modbus通信ポートとEthernetポートもあり、外部ハードウェアを使用して入出力の合計数を大幅に拡張できます。

技術的なヒント: Optaを単なる小さなPLCや「スマートリレー」と思わないでください。Modbusおよび Ethernet接続を活用すると、高性能32 ビットの2つのARM プロセッサ (Cortex-M7 および Cortex-M4)による多言語機能の威力を発揮します。外部のサポートハードウェアと組み合わせることで、Arduino Optaは、最新のエッジコンピューティングのための独自のコミュニティサポートソリューションを提供します。しかしまず、コードベースを簡素化するためのプログラミング構造が必要です。

この記事は、この拡張を念頭に置いて書かれています。何十もの入力および出力の接続を持つ可能性のあるOptaをリアルタイムで応答するようにプログラミングすることは、簡単な作業ではありません。ある時点で一線を超え、抽象化とプログラミング構造が必要不可欠になります。それらなしではコードは管理不能になり、トラブルシューティングはほとんど不可能になります。

それでは、これから構造と抽象化の作成方法を学び、ソフトウェア開発について深く掘り下げていきますので心の準備をしてください。この記事は、安全性が重視されるシステムにおけるコードの安全性、移植性、信頼性という未来像を描きながら、Arduinoスケッチの穏やかで馴染みのある世界と、モジュールプログラミングの構造化された世界とをつなぐ架け橋となるでしょう。

構造体はプログラム最適化の重要な要素

以前の記事で述べたように、構造体はプログラムを構成する上で重要な要素です。その記事では、構造体を使用して、RC(ラジコン)ロボットアームの各サーボモータに関連する変数と定数を統合しました。この統合プロセスは、面倒な学習プロセスかもしれませんが、最終的な結果は納得のいくものでした。最終的には、サーボモータを1つの実体として扱うことができ、HomeAllServos()のような関数を非常に簡単に実装できるようになりました。

1) 構造体の説明

LocalInputPin構造体とLocalOutputPin構造体を使用して、このカプセル化のアイデアをOptaの入力ピンと出力ピンに拡張できます。

typedef struct {
    uint8_t pin;         // Arduino physical pin number
    const char *name;    // Descriptive name (e.g., "Start Pushbutton")
    bool inverted;       // True for invert e.g., a normally closed pushbutton
    bool value;          // Current state (updated by LocalPinUtil.cpp)
} LocalInputPin;

typedef struct {
    uint8_t pin;         // Arduino pin number
    const char *name;    // Descriptive name (e.g., "Motor Enable")
    bool value;          // Current state
} LocalOutputPin;

2) 各入力の構造体と各出力の構造体のインスタンス化

入力ピンと出力ピンの構造体をインスタンス化するときに、主要な「.ino」ファイルで不思議なことが起こります。このコードでは、I/Oピンに関連するすべての変数と定数が構造体に追加されています。これには、Arduinoの物理的なI/Oピンと、I/Oを示す可読文字列が含まれます。また、ストップボタンのようなノーマリクローズのスイッチに自然に対応できるように、入力ピンのロジックを反転させるパラメータもあります。出力ピンの構造体は入力ピンの構造体と同じです。負論理の状況を処理するために、出力を反転させるコードを追加してもしなくてもかまいません。

LocalInputPin localSelectorSwitch = { A0, "Local Switch", 0, 0 };
LocalInputPin localStopPB = { A1, "Stop Switch", 1, 0 };
LocalInputPin localStartPB = { A2, "Start Switch", 0, 0 };
LocalInputPin localProxSensor = { A3, "Prox Sensor", 0, 0 };

LocalOutputPin localPLGreen = { 0, "Green Panel Lamp", 0 };
LocalOutputPin localPLRed = { 1, "Red Panel Lamp", 1 };
LocalOutputPin localCR1 = { 2, "Control Relay", 0 };
LocalOutputPin localFan = { 3, "Cooling Fan", 0 };

技術的なヒント: ノーマリクローズのストップ押ボタンは、ワイヤの断線を防ぐ伝統的な方法です。一般論として、PLCをスタートさせるものはノーマリオープンのスイッチを組み込む必要があります。マシンをストップさせるには、ノーマリクローズのスイッチを組み込む必要があります。

機械の安全性を必ず評価してください。必要に応じて冗長な回路を追加し、機械の保護と、さらに重要なオペレーターと技術者を保護してください。

3) ポインタの配列の構築

次に、入力ピン用の配列と出力ピン用の配列からなるポインタの配列を構築します。

LocalInputPin* inputPinList[] = {
  &localSelectorSwitch,
  &localStopPB,
  &localStartPB,
  &localProxSensor,
};

LocalOutputPin* outputPinList[] = {
  &localPLGreen,
  &localPLRed,
  &localCR1,
  &localFan,
};

4) ポインタの配列のサイズを自動的に決定

構造体の合計サイズ(バイト)を構造体の要素数で割ることにより、各構造体の入力ピンと出力ピンの数を自動的に計算します。

static const uint8_t numInputPins = sizeof(inputPinList) / sizeof(inputPinList[0]);
static const uint8_t numOutputPins = sizeof(outputPinList) / sizeof(outputPinList[0]);

技術的なヒント: 入力ポインタと出力ポインタの数を手動で固定的に記述することができました。しかし、それは入力と出力の数が変更されたときにエラーにつながる可能性があります。

5) 構造体の配列の登録とI/Oの初期化

次のステップは、構造体の配列をLocalPinUtil.cppに登録することです。この「ポインタの活用」は、コードを単純化するための重要なステップです。これにより、主要な「.ino」ファイルが元の入出力構造体を所有できるようになります。ただし、その構造体に対して操作する責任は LocalPinUtil.c に移ります。たとえば、pinMode()、digitalWrite()、digitalRead()操作を実行します。
システム全体は、LocalPinUtil.cが主要な「.ino」ファイルに存在する構造体へのstaticポインタを保持することに依存しています。主要な「.ino」ファイルのプログラマーが入力と出力の構造体のメンバ(変数/定数)に容易にアクセスできる一方で、難しい部分はこれらのポインタによって処理されます。

void setup() {
  registerLocalInputPins(inputPinList, numInputPins);
  registerLocalOutputPins(outputPinList, numOutputPins);
  initLocalInOut();
}

6) ループの繰り返しごとの、更新のための関数呼び出し

最後のステップは、LocalPinUtil.cppに対して、ループの繰り返しごとに1回のI/Oを更新するように単純に要求することです。この方法は、PLCのプログラムスキャンのようなものです。PLCのプログラムスキャンは複数のステージで構成されていることを思い出してください。

  • ハウスキーピング処理

  • 入力の読み込み

  • ユーザープログラムの実行

  • 出力の設定

ユーザーのプログラムがI/Oピンを直接操作することは一切ありません。代わりに、メモリ間の操作を実行し、関連する構造体のvalueというメンバ変数を直接変更します。

void loop() {
  LocalInOutUpdate();

    // My program

}

技術的なヒント: 従来のPLCプログラムスキャンでは、入力と出力が即座に更新されないため、多少のレイテンシが発生します。このリアルタイム性能は、現場で使用されている数百万台のPLCに広く採用されています。

より高速な応答時間を必要とするアプリケーションには、指定のハードウェアまたは割り込み対応 PLC を使用できます。

7) 変数の使用

大変な部分の作業は終わったので、これらの変数をプログラムで使用することができます。この例では、runStateと近接センサの値に基づいて冷却ファンを制御します。元の構造体の定義に戻ると、入力構造体と出力構造体にはそれぞれvalueというメンバがあります。目的のvalueメンバにアクセスするには、ドット記法を使用します。

これは、PLCプログラムのロジックを処理するのにほぼ理想的な方法です。最大の利点は、digitalRead()関数とdigitalWrite()関数がユーティリティ「.c」ファイルに移行されたため、わかりやすくなったことです。また、Arduino固有の機能がLocalPinUtil.cに移動していることにも注意してください。これは、コードを他の環境に移植する場合に有利になる可能性があります。

  if (runState && localProxSensor.value) {  //Turn selector switch off and press the red button to clear fault state.
    localFan.value = true;
  } else {
    localFan.value = false;
  }

おわりに

このコードリストでは、C++のオブジェクト指向プログラミング形式とは違い、C言語のアプローチを使用しています。ポインタ活用の処理は比較的単純です。また、メモリ位置に直接アクセスするため、valueの転送にバッファのスタックを使用するのとは違い、高速です。

これからどこに向かいますか?

それは簡単です。記事全体はローカルI/Oに焦点を当てていました。ModbusユーティリティファイルのようなローカルI/Oについて述べる必要があります。注意深くプログラムを構築すれば、リモート制御とローカル制御を切れ目なく統合できるはずです。

このトピックについてあなたの考えをお聞かせください。このポインタ活用手法を使ったプログラミングの経験をぜひお聞かせください。さらに、もっと効果的な方法がありますか?

なぜC++よりC言語の方が優れているのか、あるいはその逆なのか、明確なコメントも歓迎します。

ご健闘をお祈りします。

APDahlen

関連情報

関連する有益な情報については、以下のリンクを参照してください。

著者について

Aaron Dahlen氏、LCDR USCG(退役)は、DigiKeyでアプリケーションエンジニアを務めています。彼は、技術者およびエンジニアとしての27年間の軍役を通じて構築されたユニークなエレクトロニクスおよびオートメーションのベースを持っており、これは12年間教壇に立ったことによってさらに強化されました(経験と知識の融合)。ミネソタ州立大学Mankato校でMSEEの学位を取得したDahlen氏は、ABET認定EEプログラムで教鞭をとり、EETプログラムのプログラムコーディネーターを務め、軍の電子技術者にコンポーネントレベルの修理を教えてきました。彼はミネソタ州北部の自宅に戻り、このような記事のリサーチや執筆を楽しんでいます。




オリジナル・ソース(English)