「C Sharpの基礎 - MODBUS」の版間の差分

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動
(ページの作成:「== 概要 == MODBUSは1979年にModicon (現Schneider Electric) により開発された産業用通信プロトコルである。<br> 単純で堅牢な設計により、製造業、ビル管理、エネルギー管理等の様々な産業分野で使用されている。<br> <br> 通信方式として、マスター・スレーブ方式を採用しており、1台のマスター機器が複数のスレーブ機器と通信を行う。<br> マスターからの要…」)
 
 
31行目: 31行目:
セキュリティおいては、認証機能は備えていないため、閉じたネットワーク内での使用が推奨される。<br>
セキュリティおいては、認証機能は備えていないため、閉じたネットワーク内での使用が推奨される。<br>
近年では、暗号化やセキュリティ機能を追加した拡張仕様も提案されている。<br>
近年では、暗号化やセキュリティ機能を追加した拡張仕様も提案されている。<br>
<br><br>
== MODBUSのデータ型 ==
Modbusには、4種類の主要なデータ型がある。<br>
<br>
==== コイル ====
1ビットのデジタル出力 (読み書き可能) が可能である。<br>
<br>
* アドレス範囲
*: 00001 - 09999
<br>
* ファンクションコード
** 01
**: Read Coils
** 05
**: Write Single Coil
** 15
**: Write Multiple Coils
<br>
用途例を以下に示す。<br>
* ON / OFF制御
*: バルブの開閉状態, モータの起動 / 停止, ランプの点灯 / 消灯
<br>
==== ディスクリート入力 ====
1ビットのデジタル入力 (読み取り専用) が可能である。<br>
<br>
* アドレス範囲
*: 10001 - 19999
<br>
* ファンクションコード
** 02
**: Read Discrete Inputs
<br>
用途例を以下に示す。<br>
* リミットスイッチの状態
* センサのON / OFF信号
* アラーム信号
<br>
==== 入力レジスタ ====
16ビットのデータレジスタ (読み取り専用) である。<br>
<br>
* アドレス範囲
*: 30001 - 39999
<br>
* ファンクションコード
** 04
**: Read Input Registers
<br>
用途例を以下に示す。<br>
* アナログ入力値
* 測定値 (温度、圧力等)
* カウンタ値
<br>
==== 保持レジスタ ====
16ビットのデータレジスタ (読み書き可能) である。<br>
<br>
* アドレス範囲
*: 40001 - 49999
<br>
* ファンクションコード
** 03
**: Read Holding Registers
** 06
**: Write Single Register
** 16
**: Write Multiple Registers
<br>
用途例を以下に示す。<br>
* 設定値
*: 温度、速度、時間等
* 制御パラメータ
* 出力値の調整
* アナログ出力の制御
<br>
==== 実装上の注意点 ====
* アドレッシング
*: プログラム内でのアドレス指定は通常0ベースである。
*: 例 : コイル00001は、実際にはアドレス0として指定する。
<br>
* データ形式
*: 保持レジスタは、16ビット整数値を扱う。
*: 浮動小数点数を扱う場合は、2つのレジスタを組み合わせて使用する。
*: ビッグエンディアン / リトルエンディアンを考慮する必要がある。
<br>
* アクセス制御
*: 読み取り専用、あるいは、読み書き可能かを判断する必要がある。
<br>
* エラーハンドリング
*: 不正なアドレスへのアクセス
*: 範囲外の値の書き込み
*: 通信エラー
<br>
==== 使用例 ====
以下の例では、コイルと保持レジスタを使用している。<br>
<br>
コイルはデジタル制御 (ON / OFF)、保持レジスタはアナログ値や設定値の制御に使用する。<br>
<br>
<syntaxhighlight lang="c#">
// コイルの制御
public async Task ControlValveExample(IModbusMaster master)
{
    // バルブの開閉制御
    await master.WriteSingleCoilAsync(
      slaveId: 1,
      coilAddress: 0,    // コイル00001
      value: true        // バルブを開く
    );
    // 複数のコイルの状態を一度に読み取る
    bool[] valveStates = await master.ReadCoilsAsync(
      slaveId: 1,
      startAddress: 0,  // コイル00001から
      numberOfPoints: 4  // 4つのコイルの状態を読む
    );
}
</syntaxhighlight>
<br>
<syntaxhighlight lang="c#">
// 保持レジスタの制御
public async Task ControlTemperatureExample(IModbusMaster master)
{
    // 温度設定値の書き込み (例 : 25.5[℃])
    ushort temperatureValue = 255;  // 0.1[℃]単位で設定
    await master.WriteSingleRegisterAsync(
      slaveId: 1,
      registerAddress: 0,    // レジスタ40001
      value: temperatureValue
    );
    // 現在の設定値を読み取る
    ushort[] settings = await master.ReadHoldingRegistersAsync(
      slaveId: 1,
      startAddress: 0,    // レジスタ40001から
      numberOfPoints: 1  // 1つのレジスタを読む
    );
    decimal actualTemperature = settings[0] / 10.0m; // 実際の温度値に変換
}
</syntaxhighlight>
<br><br>
<br><br>



2025年2月16日 (日) 01:25時点における最新版

概要

MODBUSは1979年にModicon (現Schneider Electric) により開発された産業用通信プロトコルである。
単純で堅牢な設計により、製造業、ビル管理、エネルギー管理等の様々な産業分野で使用されている。

通信方式として、マスター・スレーブ方式を採用しており、1台のマスター機器が複数のスレーブ機器と通信を行う。
マスターからの要求に対して、スレーブが応答するという単純な構造になっている。

特に、MODBUSは産業オートメーションの基盤となる通信規格 (レガシーシステムとの互換性維持やシンプルな制御システムの構築) として、今日でも広く使用されている。

通信媒体としては、RS-485やRS-232等のシリアル通信 (MODBUS RTU / ASCII)、TCP/IPネットワーク (MODBUS TCP) が使用される。
特に、MODBUS TCPは、産業用イーサネットの標準プロトコルとして普及している。

データモデルの構造

  • コイル
    1ビットの読み書き可能なデジタル出力
  • ディスクリート入力
    1ビットの読み取り専用デジタル入力
  • 保持レジスタ
    16ビットの読み書き可能なデータ
  • 入力レジスタ
    16ビットの読み取り専用データ


MODBUSの特徴

  • オープンな仕様で、ロイヤリティフリーである。
  • 実装が簡単であり、開発コストを抑えられる。
  • 異なるメーカーの機器間での相互運用性が高い。


通信手順は、ファンクションコードと呼ばれる命令コードを使用してデータの読み書きを行う。
例えば、スレーブ機器のレジスタを読み取る場合、マスターは対象のスレーブアドレス、ファンクションコード、データアドレス、データ数等を含むメッセージを送信する。

セキュリティおいては、認証機能は備えていないため、閉じたネットワーク内での使用が推奨される。
近年では、暗号化やセキュリティ機能を追加した拡張仕様も提案されている。


MODBUSのデータ型

Modbusには、4種類の主要なデータ型がある。

コイル

1ビットのデジタル出力 (読み書き可能) が可能である。

  • アドレス範囲
    00001 - 09999


  • ファンクションコード
    • 01
      Read Coils
    • 05
      Write Single Coil
    • 15
      Write Multiple Coils


用途例を以下に示す。

  • ON / OFF制御
    バルブの開閉状態, モータの起動 / 停止, ランプの点灯 / 消灯


ディスクリート入力

1ビットのデジタル入力 (読み取り専用) が可能である。

  • アドレス範囲
    10001 - 19999


  • ファンクションコード
    • 02
      Read Discrete Inputs


用途例を以下に示す。

  • リミットスイッチの状態
  • センサのON / OFF信号
  • アラーム信号


入力レジスタ

16ビットのデータレジスタ (読み取り専用) である。

  • アドレス範囲
    30001 - 39999


  • ファンクションコード
    • 04
      Read Input Registers


用途例を以下に示す。

  • アナログ入力値
  • 測定値 (温度、圧力等)
  • カウンタ値


保持レジスタ

16ビットのデータレジスタ (読み書き可能) である。

  • アドレス範囲
    40001 - 49999


  • ファンクションコード
    • 03
      Read Holding Registers
    • 06
      Write Single Register
    • 16
      Write Multiple Registers


用途例を以下に示す。

  • 設定値
    温度、速度、時間等
  • 制御パラメータ
  • 出力値の調整
  • アナログ出力の制御


実装上の注意点

  • アドレッシング
    プログラム内でのアドレス指定は通常0ベースである。
    例 : コイル00001は、実際にはアドレス0として指定する。


  • データ形式
    保持レジスタは、16ビット整数値を扱う。
    浮動小数点数を扱う場合は、2つのレジスタを組み合わせて使用する。
    ビッグエンディアン / リトルエンディアンを考慮する必要がある。


  • アクセス制御
    読み取り専用、あるいは、読み書き可能かを判断する必要がある。


  • エラーハンドリング
    不正なアドレスへのアクセス
    範囲外の値の書き込み
    通信エラー


使用例

以下の例では、コイルと保持レジスタを使用している。

コイルはデジタル制御 (ON / OFF)、保持レジスタはアナログ値や設定値の制御に使用する。

 // コイルの制御
 public async Task ControlValveExample(IModbusMaster master)
 {
    // バルブの開閉制御
    await master.WriteSingleCoilAsync(
       slaveId: 1,
       coilAddress: 0,    // コイル00001
       value: true        // バルブを開く
    );
 
    // 複数のコイルの状態を一度に読み取る
    bool[] valveStates = await master.ReadCoilsAsync(
       slaveId: 1,
       startAddress: 0,   // コイル00001から
       numberOfPoints: 4  // 4つのコイルの状態を読む
    );
 }


 // 保持レジスタの制御
 public async Task ControlTemperatureExample(IModbusMaster master)
 {
    // 温度設定値の書き込み (例 : 25.5[℃])
    ushort temperatureValue = 255;  // 0.1[℃]単位で設定
    await master.WriteSingleRegisterAsync(
       slaveId: 1,
       registerAddress: 0,    // レジスタ40001
       value: temperatureValue
    );
 
    // 現在の設定値を読み取る
    ushort[] settings = await master.ReadHoldingRegistersAsync(
       slaveId: 1,
       startAddress: 0,    // レジスタ40001から
       numberOfPoints: 1   // 1つのレジスタを読む
    );
 
    decimal actualTemperature = settings[0] / 10.0m; // 実際の温度値に変換
 }



MODBUS ASCII

初期化とシリアルポート設定

MODBUS ASCII通信に必要なシリアルポートの設定を行う。
以下の例では、MODBUS ASCIIの標準的な設定であるデータ長7ビット、偶数パリティとして設定している。

 public ModbusAsciiClient(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits)
 {
    _serialPort = new SerialPort
    {
       PortName = portName,
       BaudRate = baudRate,
       Parity = parity,
       DataBits = dataBits,
       StopBits = stopBits,
       ReadTimeout = 1000,
       WriteTimeout = 1000
    };
 }


リクエストメッセージの作成

MODBUS ASCIIメッセージを作成する。
各バイトを16進数の文字列に変換して、開始文字 (:)、データ、LRC、終了文字 (CR LF) の順に構築する。

 private byte[] CreateAsciiMessage(byte slaveAddress, byte functionCode, ushort startAddress, ushort length)
 {
    // バイナリデータの作成
    byte[] data = new byte[]
    {
       slaveAddress,
       functionCode,
       (byte)(startAddress >> 8),
       (byte)startAddress,
       (byte)(length >> 8),
       (byte)length
    };
 
    // LRCの計算
    byte lrc = CalculateLRC(data);
 
    // ASCII文字列の作成
    StringBuilder asciiMessage = new StringBuilder();
    asciiMessage.Append(':');  // 開始文字
    foreach (byte b in data)
    {
       asciiMessage.Append(b.ToString("X2"));
    }
    asciiMessage.Append(lrc.ToString("X2"));
    asciiMessage.Append("\r\n"); // 終了文字
 
    return Encoding.ASCII.GetBytes(asciiMessage.ToString());
 }


メッセージの送受信

実際の通信処理を行う。

データ送信時はスレッドセーフで処理、データ受信時は開始文字から終了文字までを非同期で読み取る。

 private async Task<byte[]> SendReceiveAsync(byte[] request)
 {
    if (!_serialPort.IsOpen)
    {
       throw new InvalidOperationException("シリアルポートが開かれていません");
    }
 
    lock (_lockObject)
    {
       // バッファをクリア
       _serialPort.DiscardInBuffer();
       _serialPort.DiscardOutBuffer();
 
       // リクエストを送信
       _serialPort.Write(request, 0, request.Length);
    }
 
    // 応答を待機
    return await Task.Run(() =>
    {
       byte[] buffer = new byte[256];
       int bytesRead = 0;
 
       // 開始文字 ":" を待機
       while (_serialPort.ReadByte() != ':')
       {
          if (!_serialPort.IsOpen) throw new Exception("ポートが閉じられました");
       }
 
       // 終了文字まで読み込み
       while (bytesRead < buffer.Length)
       {
          int b = _serialPort.ReadByte();
          if (b == -1) throw new Exception("タイムアウトが発生");
 
          buffer[bytesRead++] = (byte)b;
 
          if (bytesRead >= 2 && buffer[bytesRead - 2] == '\r' && buffer[bytesRead - 1] == '\n')
          {
             break;
          }
       }
 
       byte[] response = new byte[bytesRead];
       Array.Copy(buffer, response, bytesRead);
 
       return response;
    });
 }


応答の検証

受信したデータの妥当性を検証する。

  • メッセージの最小長
  • スレーブアドレスとファンクションコードの一致
  • LRCによるデータ整合性


 private bool ValidateResponse(byte[] response, byte expectedSlaveAddress, byte expectedFunctionCode)
 {
    if (response == null || response.Length < 9)
    {
       return false;
    }
 
    // ASCII文字列からバイナリデータに変換
    string asciiHex = Encoding.ASCII.GetString(response, 1, response.Length - 3);
    byte[] binary = new byte[asciiHex.Length / 2];
    for (int i = 0; i < binary.Length; i++)
    {
       binary[i] = Convert.ToByte(asciiHex.Substring(i * 2, 2), 16);
    }
 
    // スレーブアドレスとファンクションコードの検証
    if (binary[0] != expectedSlaveAddress || binary[1] != expectedFunctionCode)
    {
       return false;
    }
 
    // LRCの検証
    byte calculatedLrc = CalculateLRC(binary.AsSpan(0, binary.Length - 1).ToArray());
    byte receivedLrc = binary[binary.Length - 1];
 
    return calculatedLrc == receivedLrc;
 }


データの抽出

応答メッセージからデータ部分を抽出して、ASCII形式からバイナリデータに変換する。

 private byte[] ConvertAsciiResponseToData(byte[] response)
 {
    // ":" と CR LF を除いたASCII文字列を取得
    string asciiHex = Encoding.ASCII.GetString(response, 1, response.Length - 3);
 
    // データ長を取得
    int dataLength = Convert.ToByte(asciiHex.Substring(4, 2), 16);
    byte[] data = new byte[dataLength];
 
    // データ部分を変換
    for (int i = 0; i < dataLength; i++)
    {
       data[i] = Convert.ToByte(asciiHex.Substring(6 + i * 2, 2), 16);
    }
 
    return data;
 }


リソース管理

最後に、シリアルポートのクリーンアップを行う。

 public void Dispose()
 {
    Dispose(true);
    GC.SuppressFinalize(this);
 }
 
 protected virtual void Dispose(bool disposing)
 {
    if (!_isDisposed)
    {
       if (disposing)
       {
          if (_serialPort != null)
          {
             if (_serialPort.IsOpen)
             {
                _serialPort.Close();
             }
             _serialPort.Dispose();
             _serialPort = null;
          }
       }
       _isDisposed = true;
    }
 }


使用方法

 async Task ExampleUsage()
 {
    using (var client = new ModbusAsciiClient("COM1", 9600))
    {
       if (await client.OpenAsync())
       {
          try
          {
             // スレーブアドレス1、開始アドレス0から2レジスタ分読み込み
             byte[] data = await client.ReadHoldingRegistersAsync(1, 0, 2);
 
             // データの処理
             // ...略
          }
          catch (Exception ex)
          {
             Console.WriteLine($"エラーが発生しました: {ex.Message}");
          }
       }
    }
 }