
Habréには、ASU TPに関する率直な記事はほとんどありません。 さらに、ほとんどのハブロフスク市民向けの産業オートメーション業界でのプログラミングは、奇妙な伝説や生き物がいる魔法のような暗い森だと思います。 ですから、教育目的でこの森の短いツアーを実施したかったのですが、教育目的に限定することはせず、プロセス制御システムで作業を始めたばかりの人や問題の種類のコントローラーに最初に出会った人たちにこの資料を役立てようとします。


だから、知り合いになる。 これは、Schneider Electric(旧Control Microsystems)のSCADAPackと呼ばれるプログラマブルロジックコントローラー(PLC)です。
プログラマブルロジックコントローラー(PLCと略記、英語のプログラマブルロジックコントローラー、PLCと略記、ロシア語へのより正確な翻訳はプログラマブルロジックを備えたコントローラー)、プログラマブルコントローラーは技術プロセスを自動化するために使用される産業用コントローラーです。 PLCの主な動作モードは、深刻なメンテナンスなしで、実質的に人間の介入なしで、多くの場合、悪環境下での長期的な自律使用です。 [ウィキペディア]
これらのPLCは、その信頼性と豊富なプログラミング機能で名声を得ています。 コントローラーの内部には、シリーズに応じて、VxWorksオペレーティングシステムを実行するARMプロセッサーがあります。
産業オートメーションでは、LD / LAD、FDB、STなどのIEC言語が認識されている標準です。 それらの最初のものは、リレー論理回路に似た回路にすぎません。 2つ目は、論理素子と電子部品(タイマー、カウンターなど)を備えた回路に似た回路にすぎません。 3番目は、パスカルの思い出を呼び起こすテキスト言語です。 しかし、今日はそれらについて(いつでもグーグルで検索できる)についてではなく、これらのコントローラーをCで開発することについて説明します。まず、「単純なプログラマー」に非常に近く、次に、必要に応じて複雑な数学計算のプログラミングや非標準の通信プロトコルの実装を節約します。
コンパイルには、実際にはコンパイラ、ヘッダーファイル、および標準コントローラーライブラリが必要です。 これはすべて、
C Toolsという製造元のWebサイトで見つけることができ、
APIの説明もそこにあります。
開発は、Makefile(ソースからバイナリファイルにプロジェクトをビルドするためのスクリプト)の作成から始まります。 サンプルのメイクファイルは、C Toolsディレクトリにあります。
C:\Program Files\Control Microsystems\CTools\Controller\Framework Applications\TelePACE
main.cppとappstart.cppファイルの例もあります(アセンブリにも必要です)。
Ctools.h C:\Program Files\Control Microsystems\CTools\Controller\TelePACE
3つの価値があることに注意してください。
objects = appstart.o main.o
この行は、ファームウェアをビルドするためにどのファイルをコンパイルする必要があるかを設定します。 プロジェクトがヘッダー付きの.cまたは.cppファイル(.hまたは.hppファイル)に分割されている(および分割されている必要がある)場合、このセクションにリストする必要があります。 突然何かを忘れると、コンパイラは未定義の参照エラーでこれを思い出させます。
CTOOLS_PATH = C:\Program Files\Control Microsystems\CTools
これがCtoolsへの道です。 異なる場合は修正してください。
TARGET = SCADAPack350
この行は、ファームウェアをコンパイルするコントローラーのファミリーを決定します。 可能なオプション:
SCADAPack350 ( 357, ..), SCADAPack33x, 4203
ファームウェアのコンパイルは、コマンドラインから
make
コマンドによって実行されます。
このコマンドが見つからないというメッセージが表示された場合は、PATHシステム変数にC-Toolsライブラリへのパスがあることを確認してください。
C:\Program Files\Control Microsystems\CTools\Arm7\host\x86-win32\bin
シンプルなアプリケーション
シンプルな(そして空の)ファームウェアをコンパイルするには、
Makefile
、appstart.cppおよびmain.cppファイルが必要です。
それらの例は、上記の同じC Toolsディレクトリにあります。 Appstart.cppはハードウェアとランタイムの初期化を担当し、main.cppで必要なコードを既に記述できます。
SPのCプログラムの一般的な構造は次のようになります。
#include <ctools.h> #include "nvMemory.h" int main(void) { // - , , , , .. while (TRUE) { // release_processor(); } }
release_processor()呼び出しは各サイクルで必要です。これは、プログラムに加えて、コントローラーOSが他のサービスプロセス(ポートおよびプロトコルハンドラー、ランタイムなど)も実行するためです。 たとえば、この関数を呼び出さないと、プログラムを起動した後、プログラムを停止したり、コントローラーを再フラッシュしたりすることはできません。
悲しいかな、C Toolsのコーディングスタイルには多くの要望があります:関数の命名には異なるスタイルがあります(process_io()とrelease_processor()がありますが、ioReadDin16()とaddRegAssignment()、および同様の関数ではgetclock()/ setclock())同じ引数を使用すると、これらの同じ引数は入れ替わります。要するに、注意してください。
信頼性の高い組み込みシステムを開発するための一般的なヒント:できるだけシンプルで簡単なコードを記述し、選択したコーディング標準に準拠し、型変換に注意し、動的メモリ割り当てとポインター演算をあまり必要としないようにします。
ルールの基礎として、MISRA標準(自動車ファームウェア開発標準)またはJSF(航空用)の個々の条項を使用できます。
プログラムをコントローラーにダウンロードする
そのためにはTelePACEが必要です。 RS232 / 485、イーサネット、さらにはUSB(スカダパックの使用モデルに搭載されている場合)を介してコントローラーに接続できます。

原則はほぼ同じです。
- [プロトコル]フィールドで、必要なプロトコル(Modbus RTU、Modbus TCPまたはModbus USB)を選択します
- [設定の構成]をクリックして、必要なすべてのデータ(RTUアドレス、RS232 / 485のポート速度、またはTCPのIPアドレス)を設定します。
- [接続]をクリックし、コントローラーに接続してください。
- [初期化]タブで、コントローラーを元の形式にリセットできます。すべてのプログラム、LADプロジェクト、ポート設定、およびレジスター割り当てを削除します。
- C / C ++タブでは、プログラムがどのようにコントローラーにロードされるか、停止/開始、新しいものをロードする方法を確認できます(タブ上部のボタンを調べてください!)。
ちなみに、ウェブ上では、プロセスを示すビデオがありました。
タイマーを操作する
最も単純なプログラムであるHello Worldから始めましょう。つまり、コントローラーのLEDを点滅させます。
#include <ctools.h> #include "nvMemory.h" // ID . 10, primitiv.h #define TIMER1EVENT 10 int main(void) { int led_state = 0; // , 1 1 ( 0.1 ) startTimedEvent(TIMER1EVENT,10); while (TRUE) { // , if (poll_event(TIMER1EVENT)) { if (led_state == 0) ledPower(LED_ON); else ledPower(LED_OFF); led_state = led_state ^ 1; } release_processor(); } }
modbusレジスターの操作
Modbusは、マスタースレーブアーキテクチャに基づくオープンな通信プロトコルです。 電子機器間の通信を整理するために業界で広く使用されています。 シリアル通信ラインRS-485、RS-422、RS-232、およびTCP / IPネットワーク(Modbus TCP)を介したデータ伝送に使用できます。 UDPを使用した非標準の実装も利用できます。 この規格の主な利点は、開放性とマスキャラクターです。 業界は現在、センサー、アクチュエータ、信号処理および正規化モジュールなどの多くのタイプとモデルを生産しています。ほとんどすべての産業用監視および制御システムには、MODBUSネットワークで動作するソフトウェアドライバーがあります。 [ウィキペディア]
SCADAPackでは、Modbusとの連携が美しく便利に実装されています。
コントローラのmodbusレジスタのメモリは不揮発性であり、再起動または電源障害が発生しても失われません。 一方で、これは少し不便です(必要に応じて、起動時にレジスタをクリアまたは初期化することを忘れないでください)が、一方で、これにより、設定、設定、さらには小さなアーカイブをmodbusアドレス空間に保存することができます。
さらに、さまざまなコマンドの結果(ディスクリートおよびアナログ入力のステータスの読み取り、外部デバイスのポーリングなど)がmodbusレジスタに配置されます。
バックアップパッケージのmodbusプロセッサはオペレーティングシステムレベルで実装されているため、プログラムを開始した後、すべてのCOMポート(およびイーサネット)について、すぐにmodbassでコントローラーに問い合わせることができます(ポート設定については少し後で説明します)。 さらに、プログラムが停止していても、コントローラーはmodbus要求に応答します。
modbusレジスタの書き込みと読み取りは、たとえば次のように、dbase()およびsetdbase()関数によって行われます。
request_resource(IO_SYSTEM); a = dbase(MODBUS, 30001); b = dbase(MODBUS, 30002); setdbase(MODBUS, 40020, a * b); release_resource(IO_SYSTEM);
この例では、入力ゾーンの0001および0002レジスタから2つの数値を読み取り、それらの乗算結果を保持ゾーンの0020レジスタに格納します。 すべてがシンプルです。
I / O信号を操作する
入力モジュールと出力モジュールを操作するには、3つの方法があります。
最初の方法
最初のオプションは、「レジスタ割り当て」を作成することです。 DINおよびAINモジュールの入力チャンネルの状態(離散およびアナログ値)はmodbusレジスタに自動的に「マッピング」され、逆もまた同様に、DOUTおよびAOUTモジュールの場合、出力の状態はレジスタに記録された値によって決定されます。 これを行うには、clearRegAssignmnet(すべての古い割り当てをクリア)およびaddRegAssignment(新規作成)関数が使用されます。
addRegAssignment関数の最初の引数はモジュールのタイプです(定数のリストはTelePACEのドキュメントとヘッダーファイルにあります。DIN_5401、DIN_5404、AIN_5301などがあります)、2番目はモジュールのアドレスです(通常はモジュール自体のジャンパーで指定され、組み込みの場合はSP入力、それは0に等しい)、そして記録が行われるべきレジスタのアドレスがあります(モジュールが異なるタイプのデータを提供する場合、レジスタのいくつかのグループがあります-コイル、ステータス、入力など)
ランタイムを強制的に停止すると(runTarget(FALSE))、動作しなくなりますが、最適化のために標準のappstart.cファイルに変更を加えなかった場合、心配することはありません。
clearRegAssignment()を忘れずに実行してください。 あなたがそれらを使用しなくても、あなたがプログラムを開始するとき-誰があなたの前にこのコントローラーで誰が何をしたかを知っています。
ファームウェアを起動するコントローラーにどのI / Oボードがあるか正確にわからない場合は、
OZNAスペシャリストが提案するソリューションを使用できます。
第二の方法
指定されたmodbusレジスタにデータを保存する関数を明示的に読み取ります。
関数ioRead8Din、ioRead8Ain、ioRead16Din、ioRead16Ain、ioRead5604Inputs、ioWrite16Dout、ioWrite5604Outputs、ioRead4Counter(入力のカウント用)、ioReadSP2(コントローラーの組み込み入力の読み取り用)を使用できます。 これらのコマンドの詳細と構文は、C Toolsのドキュメントに記載されています。通常、最初の引数はモジュールのアドレス(組み込み入力の場合は0)、2番目と2番目はmodbusレジスタのアドレスであり、そこから信号値を保存する必要がある(または書き込みのためにそれらを取得する場所)週末のチャンネルへ)。
例:
ioWrite *関数を呼び出すと、同様に必要な値がmodbusレジスターからコントローラーとモジュールの出力チャンネルに設定されます。
第三の方法
同じですが、結果をmodbusレジスタではなく変数または配列に保存します。 関数も同じように呼び出されますが、他の引数を使用した実装がオーバーロードされています。
int Module5607Addr = 0;
同様のことが、出力チャネルに書き込むデータで実行できます(たとえば、リレー出力を閉じます)。
UINT16 InputType[8]; for(int i=0;i < 8; i++) InputType[i] = 3; UINT16 InputFilter = 3; UINT16 ScanFrequency = 0; UINT16 OutputType = 1; UINT16 Mask2 = 0; ioWrite5607Outputs(Module5607Addr,DOData,AOData,InputType,InputFilter,ScanFrequency,OutputType); ioRequest(MT_5607Outputs, Module5607Addr);
使用されるモジュールのデータを読み書きするための特定の関数と定数については、ドキュメントとC Toolsヘッダーファイルを参照してください。 たとえば、5607モジュールの機能には、上記のように、入力タイプ、フィルターパラメーターなどを設定するための追加オプションがあります。 これらはドキュメントにも記載されています。
現在のコントローラーの温度とバッテリー電圧を取得する関数もあります-readThermistor(T_CELSIUS)、readBattery()。 些細なことですが、便利です。
AIスケーリング
コントローラモジュールのアナログ入力チャンネルは、さまざまなモードで動作できます(たとえば、入力範囲が0〜20または4〜20 mAの場合、これはモジュールのジャンパによって決定されます)。 それらはデータをADC単位で提供し、それらを必要なスケールに変換します(たとえば、4-20 mAは0-100%に対応します):
当然、100の代わりに、必要なスケールの上限を数値に掛けることができます。
RS-232 / RS-485ポートの構成
ここでも、魔法はありません:
PROTOCOL_SETTINGS comsettings; pconfig portSettings;
ポート設定はEEPROMに保存されますが、コントローラーの構成、ライブラリのバージョンなどに応じて、たとえば特定のイベント(コマンド、ジャンパークロージャなど)の同じmodbusレジスタから読み取り、自動的に再構成し、それらを更新します。
Modbusでのポーリングデバイス
スレーブデバイスとして動作するだけでなく、他のコントローラーやセンサーに自分で問い合わせたい場合は、modbassで外部デバイスをポーリングし、結果をレジスタに保存して、それらを読み取って使用できるようにするmaster_message()という形式の関数がありますアルゴリズムで(または単に上位レベルを提供する)。 ドキュメントには例がありますが、2つのニュアンスを考慮してください:2番目のリクエストを送信するか、受信したデータを操作する前にget_protocol_status()コマンドで関数実行の結果を確認する必要があり、2番目のニュアンス:使用するポートでmodbusプロセッサをオフにするか、そのアドレスがポーリングされるデバイスのアドレスと一致しないようにします(そうしないと、未定義の動作や奇妙なエラーが発生する可能性があります)。
extern unsigned master_message(FILE * stream, unsigned function, unsigned slave_station, unsigned slave_address, unsigned master_address, unsigned length);
streamはデータの交換に使用されるポート(com1など)、functionはリクエストのmodbus機能番号(3など)、slave_stationはポーリングされるデバイスのRTUアドレス、slave_addressは読み取りたいリモートデバイスのレジスタの開始アドレス、master_address -データが書き込まれるコントローラーのレジスターの開始アドレス、長さ-読み取るレジスターの数)。
例:
request_resource(IO_SYSTEM);
独自のModbusパケットハンドラー(installModbusHandler())をインストールして、プロトコル拡張機能を実装することもできます。
システムは、交換の統計(受信/送信、エラー数など)を保持します。これは、対応する構造から読み取ることもできます。
コミュニケーションズ
COMポート経由で受信したデータの独自のハンドラー(install_handle())をインストールして、いくつかの非標準プロトコルを実装することもできます。
TCP / IPでは、すべてがうまくいきます。 BSDソケットの標準機能が利用可能です:bind()、getsockopt()など。 コントローラのネットワーク設定は、プログラムで読み書きできます(ethernetGetIP()、ethernetSetIP())。
アーカイブ
これらの目的のためのSCADAPackドキュメントでは、C ToolsライブラリのDataLogを使用することが提案されていますが、残念ながら、多くの欠点があります。その主な理由は、アーカイブエントリへの直接の一貫性のないアクセスを提供しないことです。 一部の開発者はアーカイブをレジスタメモリに直接保存することもできますが、コントローラーの保持数と入力数(必要に応じて使用することもできます)が限られていることを考慮すると、このソリューションも常に機能するとは限りません。
実際、コントローラーのオペレーティングシステムは非常にPOSIX互換であり、コントローラー自体がボード上に完全に通常のファイルシステムを搭載しているため(さらに、USBフラッシュドライブを挿入するオプションもあります)、ボード上のフラッシュドライブ上のファイルにアーカイブを保存できますコントローラーは、ドキュメントで非常に簡単に言及されていました。
Cプログラムと同様にファイルを操作できます。
FILE *mdata; char* file_mdata = "/d0/logs.dat"; mdata = fopen(file_mdata, "w"); fputs("test log string", mdata); fclose(mdata);
つまり、(オプションとして-周期的なアーカイブを保持する)シリアル化された構造をファイルに書き込み、次に自由にナビゲートし、アーカイブの特定の部分をmodbusレジスターのスペースにマッピングするか、ユーザー(カスタム)modbusに与えることにより、ユーザーに提供するのに障害はありません大きなブロックで機能します。
ファイルシステムはポイント/ d0 /にマウントされ、外部USBフラッシュドライブは/ bd0 /にマウントされます。繰り返しになりますが、コントローラのボタンをクリックしたときに、内蔵メモリからUSBフラッシュドライブにアーカイブをコピーすること、および他の多くのオプションを実装しても問題はありません。TelePACEでは、ファイルシステムは非常に穏やかに表示され、デバッグやデータ収集に役立ちます。不揮発性メモリ
すでに述べたように、不揮発性メモリには、まずModbusレジスタのデータが保存されます。つまり、保存された値は、コントローラが再起動された後でも使用可能になります。さらに、nvMemory.hファイルで宣言された最大サイズ8キロバイトの別のs_nvMemory構造があり、必要に応じて(必要なフィールドを作成することにより)編集し、停電や再起動後も一貫性を保つ必要があるデータを保存できますコントローラー。これで十分でない場合、さらにallocateMemory()メソッドは、合計サイズが最大1メガバイトの不揮発性メモリブロックを割り当てて、アーカイブやログなどを保存できます。この場合の最も重要なことは、メモリブロックを割り当てた後、前述のs_nvMemory構造体にポインタを保存することを忘れないでください。そうしないと、割り当てられたメモリが失われ、使用または解放できなくなります(freeMemory())。また、不揮発性メモリにアクセスするときは、DYNAMIC_MEMORYリソースを取得することを忘れないでください。マルチタスクとマルチプロセッシング
複数の異なるプログラムを同時に実行したり、複数の異なるメソッドを同時に実行したり(createTask()、getTaskInfo()関数など)、それらの優先順位を設定したり、プロセス間でタスクとメッセージ間でシグナルを送信したりすることができます「封筒」)。マルチタスクは協調的です。つまり、タスクとプログラムの切り替えは、オペレーティングシステムによる強制ではなく、実行をさらに転送する準備ができたとき、または特定のイベントが発生したときに交互に実行されます。一方で、これはデータを共有するときの操作の原子性の問題を解決します。他方で、release_processor()を呼び出して、リソースを長く取得しないことを忘れないでください。マルチタスクプログラムの例と、イベント発生時の状態変化のグラフは、「RTOSサンプルアプリケーションプログラム」および「タスク実行の説明」の章のドキュメントに記載されています。動的メモリ
malloc()およびfree()は、newおよびdeleteと同様に機能します。唯一重要なニュアンスは、プログラムが停止した場合(たとえば、[停止]ボタンを押して)、割り当てられたメモリがシステムに戻らないことです。プログラム終了ハンドラー(installExitHandler)を意識的にインストールし、要求されたブロックをすべてヒープから解放する必要があります。リアルタイムクロックを使用する
コントローラには、リアルタイムクロックも搭載されています。現在の時刻を取得し、getclockおよびsetclock関数を使用して設定できます。ドキュメントには詳細な例があります。簡単なアルゴリズムの例
静電容量のアナログレベルセンサーが接続されているAI 2チャネルにポンプスターターリレーが接続されているDO1チャネルに、I / Oボード5604を備えたコントローラがあるとします。タンク内の指定された液体レベルを維持する必要があります(必要なレベルはSCADAまたはHMIパネルから設定されます)。つまり、レベルが高すぎる場合は、必要なレベルに達したときに過剰をポンピングするポンプのオンとオフを切り替える必要があります。ホールディングゾーン(40020)のレジスタ0020に、SCADAまたはHMIパネルに表示するための低減レベル値を書き込みます。また、レベルが目的のレベル付近で変動する場合にポンプリレーが「クリック」から保護されるように、小さなヒステリシスを提供する必要があります。コントローラはCOM2ポートを介してポーリングされます。 #include <ctools.h> #include "nvMemory.h" /** * @brief 5604 modbus- */ void initialize_io() { request_resource(IO_SYSTEM); clearRegAssignment(); addRegAssignment(SCADAPack_5604IO, 0, 1, 10001, 30001, 40001); release_resource(IO_SYSTEM); } /** * @brief COM2 Modbus Slave, = 1, = 9600 */ void initialize_ports() { request_resource(IO_SYSTEM); PROTOCOL_SETTINGS comsettings; pconfig portSettings; getProtocolSettings(com2, &comsettings); comsettings.station = 1; comsettings.type = MODBUS_RTU; comsettings.mode = AM_standard; get_port(com2,&portSettings); portSettings.baud = BAUD9600; portSettings.duplex = HALF; portSettings.parity = PARITY_NONE; portSettings.data_bits = DATA8; portSettings.stop_bits = STOP1; portSettings.flow_tx = TFC_NONE; portSettings.flow_rx = RFC_MODBUS_RTU; portSettings.type = RS232; setProtocolSettings(com2, &comsettings); set_port(com2,&portSettings); request_resource(IO_SYSTEM); } /** * @brief AI- * @param ai_value "" AI * @param max AI ( ) * @return */ float scale_ain(int ai_value, float max) { return ((float)ai_value - 6554) / 26214 * max; } /* . #include */ #define AILevelReg 30002 // , AI (2- AI) #define LevelScaledReg 40020 // , #define PumpLevelTriggerReg 40050 // , SCADA ( ) #define PumpOutReg 1 // DO, (1- DO) #define LevelHyst 50 // . , #define MaxLevelMeters 500 // ( ), int main(void) { // COM initialize_ports(); // - initialize_io(); while (TRUE) { request_resource(IO_SYSTEM); // AI , int level_raw = dbase(MODBUS, AILevelReg); float level_scaled = scale_ain(level_raw, MaxLevelMeters); setdbase(MODBUS, LevelScaledReg, (int)level_scaled); // , if (level_scaled > (dbase(MODBUS, PumpLevelTriggerReg) + LevelHyst)) setdbase(MODBUS, PumpOutReg, 1); else // - if (level_scaled < (dbase(MODBUS, PumpLevelTriggerReg) - LevelHyst)) setdbase(MODBUS, PumpOutReg, 0); release_resource(IO_SYSTEM); release_processor(); } }
それだけです
すべての関数の説明とその使用例は、Cツールのドキュメントにあります。コードを美しく、エラーなく記述するために、Cの完全な知識がある(たとえば、アルゴリズムを実装する場合、これは型変換に適用される)ことは非常に良いことです。学び、実験し、すべてがうまくいく!材料と貴重なコメントの準備に協力してくれたデニスに感謝します:)