「C Sharpの基礎 - MODBUS」の版間の差分
(ページの作成:「== 概要 == 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
- 01
用途例を以下に示す。
- ON / OFF制御
- バルブの開閉状態, モータの起動 / 停止, ランプの点灯 / 消灯
ディスクリート入力
1ビットのデジタル入力 (読み取り専用) が可能である。
- アドレス範囲
- 10001 - 19999
- ファンクションコード
- 02
- Read Discrete Inputs
- 02
用途例を以下に示す。
- リミットスイッチの状態
- センサのON / OFF信号
- アラーム信号
入力レジスタ
16ビットのデータレジスタ (読み取り専用) である。
- アドレス範囲
- 30001 - 39999
- ファンクションコード
- 04
- Read Input Registers
- 04
用途例を以下に示す。
- アナログ入力値
- 測定値 (温度、圧力等)
- カウンタ値
保持レジスタ
16ビットのデータレジスタ (読み書き可能) である。
- アドレス範囲
- 40001 - 49999
- ファンクションコード
- 03
- Read Holding Registers
- 06
- Write Single Register
- 16
- Write Multiple Registers
- 03
用途例を以下に示す。
- 設定値
- 温度、速度、時間等
- 制御パラメータ
- 出力値の調整
- アナログ出力の制御
実装上の注意点
- アドレッシング
- プログラム内でのアドレス指定は通常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}");
}
}
}
}