Delphi Modbus RTU Master 2019/08/19 ~ 2019/11/07
・2019/08/16 初版作成
・2019/08/19 受信廻りのコードを修正、整理(基本的な変更はありません)
・2019/11/07 三菱インバータ FR-A820 の情報を追加
Modbus RTU テスト用のツールです。
パソコン側がマスター、接続機器がスレーブになります。
読み出しコマンド(03H)と書き込みコマンド(10H)のみ使用。通信データは16 ワードです。
16 データの読み込み、16 データまたは 1 データの書き込みが行えます。
アドレスのデータが書き込み不可の場合は、異常メッセージが表示されます。
16 データの書き込みの場合、書き込み許可のアドレスのデータのみが変更されます。
・動作確認には、azbil 調節計 SDC35 と YOKOGAWA 調節計 UT32A を使用しました。
・RS485 - USB 変換には、TTL-RS485 変換モジュールと UART-USB 変換モジュールを組み合わせて使用しました。
・2019/11/07 三菱インバータ FR-A820 で試してみました。
三相電源が無い場合、+24, SD 端子に外部から DC24V を給電すると、設定の他 RS-485 のテストが行えます。
パラメータ Pr.549 = 1 (MODBUS RTU プロトコル)、Pr.331 = 1 (RS-485 通信局番)に変更して、一度電源断(パワーオンリセット)。
9600bps、EVEN、1 StopBits でつながります。正しく(初期値)は、2 StopBits のようですが、1 でもつながり、
アドレス 0400H あたりで何か数値が表示されました。アドレスが不正の場合は、エラー表示されます。
市販の USB-RS485 変換器を 2 線で使用しています。
■使い方
・通信(Comport)設定を行います。
・接続先の機器 ID を入力し[Enter] キーを押すかするか、UpDown ボタンをクリックすると、接続機器から 16 データが読み込まれます。
・アドレスを入力し、[Enter] キーを押すか、[READ (03H)] ボタンをクリックすると、接続機器から16 データが読み込まれます。
・値を変更する場合は、「WRITE (DEC)」 欄に数値(10進)を入力し、[Enter]キーを押すか、[WRITE (10H) 1 データ]
ボタンをクリックすると、接続機器に書き込まれます。
複数のデータを書き込む場合は、数値を入力し、[WRITE (10H) 16 データ] ボタンをクリックします。
書き込み不可のアドレスが含まれていると異常メッセージが表示されます。書き込み許可のアドレスのデータのみ変更されます。
※読み込み ID と違う場合は書き込まないようにしていますが、念のために「接続 ID」、「先頭アドレス」を確認してください。
■ダウンロード
ModbusRtu.zip (exe Ver. 1.01 本体 のみ)
■ソースコード (Delphi 10.3 Community Edition)
{ 2019/08/16 初版作成 2019/08/19 受信廻りのコードを整理、修正 } unit Unit4; interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, OoMisc, AdPort, Vcl.Grids, AdSelCom, Vcl.Buttons, IniFiles, Vcl.ExtCtrls, Vcl.ComCtrls; type TForm4 = class(TForm) ApdComPort1: TApdComPort; StringGrid1: TStringGrid; Edit4: TEdit; Label1: TLabel; Edit5: TEdit; Edit6: TEdit; CheckBox1: TCheckBox; Label2: TLabel; Edit7: TEdit; Label3: TLabel; ComboBox1: TComboBox; ComboBox2: TComboBox; ComboBox3: TComboBox; ComboBox4: TComboBox; ComboBox5: TComboBox; SpeedButton1: TSpeedButton; SpeedButton2: TSpeedButton; SpeedButton3: TSpeedButton; SpeedButton4: TSpeedButton; SpeedButton5: TSpeedButton; SpeedButton6: TSpeedButton; Label4: TLabel; Edit1: TEdit; Label5: TLabel; Edit2: TEdit; Edit3: TEdit; Label6: TLabel; BitBtn1: TBitBtn; BitBtn2: TBitBtn; BitBtn3: TBitBtn; Timer1: TTimer; UpDown1: TUpDown; procedure ApdComPort1TriggerAvail(CP: TObject; Count: Word); procedure FormCreate(Sender: TObject); procedure CheckBox1Click(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure SpeedButton1Click(Sender: TObject); procedure Edit4KeyPress(Sender: TObject; var Key: Char); procedure BitBtn1Click(Sender: TObject); procedure BitBtn2Click(Sender: TObject); procedure BitBtn3Click(Sender: TObject); procedure Timer1Timer(Sender: TObject); procedure StringGrid1KeyPress(Sender: TObject; var Key: Char); procedure StringGrid1Click(Sender: TObject); procedure UpDown1Click(Sender: TObject; Button: TUDBtnType); private { Private 宣言 } public { Public 宣言 } buf : string; cmdMode : integer; mbId : Byte; // 機器のID mbFuncCode : Byte; // Modbus function code mbBytes : Byte; // データのバイト数s mbErrorCode : Byte; // エラーコード txbuf : array [0..41] of Byte; // 送信用バッファ rxBuf : array [0..36] of Byte; // 受信用バッファ rxIndex : integer; // カウンター edIndex : integer; // データの終端 readId : Byte; // Read時のID readAdr : Word; // Read時のアドレス readData : array [0..15] of word; // 受信データ格納 writeData : array [0..15] of word; // 送信データ格納 DIbuf : array [0..15] of bool; // Digital Input // Comport 設定 procedure setComPortParams; procedure clearEdits; end; var Form4: TForm4; implementation {$R *.dfm} // n の k 乗 (Math ユニット不要) function IntPower(n, k : integer):integer; var i : integer; begin result := 1; for i := 1 to k do result := result * n; end; function CRC16(buf : array of Byte; BufLen: integer): word; // crc16 計算 // azbil SDC 取扱説明書 詳細編 CRC計算 を pascal に移植 var i, j : integer; carry : word; crc : word; crcl : Byte; begin crc := $FFFF; for i := 0 to BufLen - 1 do begin crc := word(crc xor Ord(Buf[i])); for j := 0 to 7 do begin carry := crc and $0001; crc := crc shr 1; if carry = 1 then crc := crc xor $A001; end; end; crcl := (crc and $FF00) shr 8; crc := crc shl 8; crc := crc or crcl; result := crc; end; procedure TForm4.setComPortParams; // Comport 設定 var s : string; begin with ComboBox1 do if ItemIndex >= 0 then begin s := Copy(Items[ItemIndex], 4); ApdComPort1.ComNumber := StrToIntDef(s, 0); end; with ComboBox2 do if ItemIndex >= 0 then ApdComPort1.Baud := StrToIntDef(Items[ItemIndex], 19200); with ComboBox3 do if ItemIndex >= 0 then ApdComPort1.DataBits := ItemIndex + 7; with ComboBox4 do if ItemIndex >= 0 then ApdComPort1.Parity := TParity(ItemIndex); with ComboBox5 do if ItemIndex >= 0 then ApdComPort1.StopBits := ItemIndex + 1; end; procedure TForm4.clearEdits; begin Edit1.Text := ''; Edit2.Text := ''; Edit3.Text := ''; Edit5.Text := ''; Edit6.Text := ''; end; procedure TForm4.SpeedButton1Click(Sender: TObject); // アドレス UP/DOWN var adr : integer; stp : integer; btn : TSpeedButton; begin btn := Sender as TSpeedButton; // データの先頭アドレス if CheckBox1.Checked then begin stp := StrToInt(Copy(btn.Caption, 1, 1) + '$' + Copy(btn.Caption, 2)); adr := StrToIntDef('$' + Edit4.Text, 0); adr := adr + stp; if adr > $ffff then adr := $ffff; if adr < 0 then adr := 0; Edit4.Text := IntToHex(adr, 4); end else begin stp := StrToInt(btn.Caption); adr := StrToIntDef(Edit4.Text, 0); adr := adr + stp; if adr > $ffff then adr := $ffff; if adr < 0 then adr := 0; Edit4.Text := IntToStr(adr); end; BitBtn2Click(self); end; procedure TForm4.StringGrid1Click(Sender: TObject); begin // COL = 4 のみ編集許可 with StringGrid1 do if Col = 4 then Options := Options + [goEditing] else Options := Options - [goEditing]; end; procedure TForm4.StringGrid1KeyPress(Sender: TObject; var Key: Char); begin // Enter キーで再読み込み if Key = #13 then begin Key := #0; with StringGrid1 do begin if (Col = 4) and (Cells[Col, Row] <> '') then begin BitBtn1Click(self); end; end; end; end; procedure TForm4.Timer1Timer(Sender: TObject); begin // 再読み込み if cmdMode = 101 then begin Timer1.Enabled := false; BitBtn2Click(self); StringGrid1.SetFocus; end; end; procedure TForm4.UpDown1Click(Sender: TObject; Button: TUDBtnType); begin // 再読み込み BitBtn2Click(self); end; procedure TForm4.ApdComPort1TriggerAvail(CP: TObject; Count: Word); var i, j, k : Word; B : array [0..511] of Byte; s : string; begin ApdComPort1.GetBlock(B, Count); // 受信バイナリデータを文字列にする for i := 0 to Count -1 do begin buf := buf + IntToHex(B[i], 2); rxBuf[rxIndex] := B[i]; case rxIndex of 0: begin mbId := B[i]; Edit1.Text := IntToStr(mbId); end; 1: begin mbFuncCode := B[i]; Edit2.Text := IntToHex(mbFuncCode, 2); end; end; Inc(rxIndex); if rxIndex > 2 then begin if rxIndex = 3 then begin mbErrorCode := 0; // エラーコードをクリア Edit3.Text := ''; if mbFuncCode and $80 > 0 then begin mbBytes := 0; mbErrorCode := rxBuf[2]; // 異常 case mbErrorCode of 0: s := ''; 1: s := 'スレーブは当該ファンクションをサポートしていない'; 2: s := '指定されたデータアドレスは、スレーブには存在しない'; 3: s := '指定されたデータは、許されない'; else s := 'その他の異常'; end; Edit3.Text := s; if s <> '' then begin Edit3.Text := s; ShowMessage(s); end; edIndex := 4; end // 正常 else begin mbBytes := rxBuf[2]; case mbFuncCode of 1..4 : edIndex := mbBytes + 4; 5, 6, 8, $0B, $0F, $10 : edIndex := 7; end; end; end; // 最終まで取得終了 if rxIndex > edIndex then begin if mbFuncCode <= $02 then begin for j := 0 to mbBytes -1 do begin with StringGrid1 do begin Cells[3, j * 8 + 1] := IntToHex(rxBuf[3 + j], 2); for k := 0 to 15 do begin DIBuf[k] := rxBuf[3 + j] and IntPower(2, k) > 0; if DIbuf[k] then Cells[2, j * 8 + k + 1] := '1' else Cells[2, j * 8 + k + 1] := '0'; end; end; if j > 0 then break; end; end; if (mbFuncCode = $03) or (mbFuncCode = $04) then begin for j := 0 to mbBytes div 2 -1 do begin readData[j] := rxBuf[3 + j * 2] shl 8 + rxBuf[3 + j * 2 + 1]; with StringGrid1 do begin Cells[2, j + 1] := IntToStr(readData[j]); Cells[3, j + 1] := IntToHex(readData[j], 4); end; if j >= 15 then break; end; // データ数が 16 に満たない時 if mbBytes div 2 <= 15 then begin for j := mbBytes div 2 to 15 do with StringGrid1 do begin Cells[2, j + 1] := ''; Cells[3, j + 1] := ''; end; end; end; // 受信データを 16 進で表示 buf := ''; for j := 0 to rxIndex - 1 do buf := buf + IntToHex(rxBuf[j], 2); Edit6.Text := buf; rxIndex := 0; end; end; end; end; procedure TForm4.BitBtn1Click(Sender: TObject); // WRITE (10H) x 1 データ // 書き込みはリストの上から1個のみ // 書き込み不可のアドレスでは、データ異常になる var i : integer; adr : integer; s : string; crc : integer; flag : boolean; id : byte; sgRow : integer; begin clearEdits; sgRow := 0; // 機器 ID id := StrToIntDef(Edit7.Text, 1); // データの先頭アドレス if CheckBox1.Checked then adr := StrToIntDef('$' + Edit4.Text, -1) else adr := StrToIntDef(Edit4.Text, -1); if adr < 0 then ShowMessage('アドレスが不正です.') else begin if (id = readId) and (adr = readAdr) then flag := True else begin ShowMessage('読み込み時のID, アドレスが異なります.'); flag := false; end; if flag then begin with StringGrid1 do begin // データが読み込まれているか for i := 1 to 16 do begin if (Cells[2, i] = '') or (Cells[3, i] = '') then begin ShowMessage('データが読み込まれていません.'); flag := false; break; end; end; // 書き込みデータがあるか if flag then begin flag := false; for i := 1 to 16 do begin if (Cells[4, i] <> '') then begin flag := True; sgRow := i; break; end; end; end; if not flag then ShowMessage('書き込みデータがありません.') else begin writeData[0] := StrToInt(Cells[4, sgRow]); adr := StrToInt('$' + Cells[1, sgRow]); //HEX ADR end; end; end; if flag then begin txBuf[0] := id; // 機器アドレス txBuf[1] := $10; // ファンクション番号 txBuf[2] := adr shr 8; // 先頭アドレス上位 txBuf[3] := adr and $ff; // 先頭アドレス下位 txBuf[4] := 0; // 書き込みデータ数上位 txBuf[5] := 1; // 書き込みデータ数下位 txBuf[6] := 2; // 書き込みバイト数 = データ数 x 2 txBuf[7] := writeData[0] shr 8; // 書き込みデータ上位 txBuf[8] := writeData[0] and $ff; // 書き込みデータ下位 crc := CRC16(txBuf, 9); txBuf[9] := crc shr 8; // crc上位 txBuf[10] := crc and $ff; // crc下位 with ApdComport1 do begin setComPortParams; try Open := True; rxIndex := 0; PutBlock(txBuf, 11); Sleep(10); s := ''; for i := 0 to 10 do s := s + IntToHex(txBuf[i], 2); Edit5.Text := s; with StringGrid1 do begin ShowMessage( '機器ID : ' + Edit7.Text + #13 + 'アドレス : ' + Cells[0, sgRow] + ' (hex = ' + Cells[1, sgRow] + ')' + #13 + 'データ : [ ' + Cells[2, sgRow] + ' ] -> [ ' + Cells[4, sgRow] + ' ]' + #13 + '送信しました. 再読み込みします.'); Cells[4, sgRow] := ''; end; // 再読み込み cmdMode := 101; Timer1.Interval := 500; Timer1.Enabled := True; except end; end; end; end; end; procedure TForm4.BitBtn2Click(Sender: TObject); // READ (03H) 16 データ var adr : integer; crc : integer; s : string; i: integer; id : byte; begin clearEdits; // 機器 ID id := StrToIntDef(Edit7.Text, 1); // データの先頭アドレス if CheckBox1.Checked then adr := StrToIntDef('$' + Edit4.Text, -1) else adr := StrToIntDef(Edit4.Text, -1); if adr < 0 then ShowMessage('アドレスが不正です.') else begin with StringGrid1 do begin for i := 1 to 16 do begin Cells[0, i] := IntToStr(adr + i - 1); Cells[1, i] := IntToHex(adr + i - 1, 4); Cells[2, i] := ''; Cells[3, i] := ''; end; end; txBuf[0] := id; // 機器アドレス txBuf[1] := 3; // ファンクション番号 txBuf[2] := adr shr 8; // 先頭アドレス上位 txBuf[3] := adr and $ff; // 先頭アドレス下位 txBuf[4] := 0; // 読み出し数上位 txBuf[5] := 16; // 読み出し数下位 crc := CRC16(txBuf, 6); txBuf[6] := crc shr 8; // crc上位 txBuf[7] := crc and $ff; // crc下位 with ApdComport1 do begin setComPortParams; try Open := True; rxIndex := 0; PutBlock(txBuf, 8); Sleep(10); s := ''; for i := 0 to 7 do s := s + IntToHex(txBuf[i], 2); Edit5.Text := s; // データ受信のIDと先頭アドレスを保持 readAdr := adr; readId := id; except end; end; end; end; procedure TForm4.BitBtn3Click(Sender: TObject); // WRITE (10H) x 16 データ // 書き込み不可のアドレスでは、データ異常になる var i : integer; adr : integer; s : string; crc : integer; flag : boolean; id : byte; begin clearEdits; // 機器 ID id := StrToIntDef(Edit7.Text, 1); // データの先頭アドレス if CheckBox1.Checked then adr := StrToIntDef('$' + Edit4.Text, -1) else adr := StrToIntDef(Edit4.Text, -1); if adr < 0 then ShowMessage('アドレスが不正です.') else begin if (id = readId) and (adr = readAdr) then flag := True else begin ShowMessage('読み込み時のID, アドレスが異なります.'); flag := false; end; if flag then begin with StringGrid1 do begin // データが読み込まれているか for i := 1 to 16 do begin if (Cells[2, i] = '') or (Cells[3, i] = '') then begin ShowMessage('データが読み込まれていません.'); flag := false; break; end; end; // 書き込みデータがあるか if flag then begin flag := false; for i := 1 to 16 do begin if (Cells[4, i] <> '') then begin flag := True; break; end; end; end; if not flag then ShowMessage('書き込みデータがありません.') else for i := 1 to 16 do if Cells[4, i] <> '' then writeData[i-1] := StrToInt(Cells[4, i]) else writeData[i-1] := readData[i-1]; end; end; if flag then begin txBuf[0] := id; // 機器アドレス txBuf[1] := $10; // ファンクション番号 txBuf[2] := adr shr 8; // 先頭アドレス上位 txBuf[3] := adr and $ff; // 先頭アドレス下位 txBuf[4] := 0; // 書き込みデータ数上位 txBuf[5] := 16; // 書き込みデータ数下位 txBuf[6] := 32; // 書き込みバイト数 = データ数 x 2 for i := 0 to 15 do begin txBuf[7 + i * 2] := writeData[i] shr 8; // 書き込みデータ上位 txBuf[8 + i * 2] := writeData[i] and $ff; // 書き込みデータ下位 end; crc := CRC16(txBuf, 7 + 32); txBuf[7 + 32] := crc shr 8; // crc上位 txBuf[8 + 32] := crc and $ff; // crc下位 // 最終はtxbuf[40] with ApdComport1 do begin setComPortParams; try Open := True; rxIndex := 0; PutBlock(txBuf, 41); Sleep(10); s := ''; for i := 0 to 40 do s := s + IntToHex(txBuf[i], 2); Edit5.Text := s; ShowMessage('送信しました. 再読み込みします.'); with StringGrid1 do for i := 1 to 16 do Cells[4, i] := ''; // 再読み込み cmdMode := 101; Timer1.Interval := 500; Timer1.Enabled := True; except end; end; end; end; end; procedure TForm4.CheckBox1Click(Sender: TObject); var n : integer; begin if CheckBox1.Checked then begin n := StrToIntDef(Edit4.Text, -1); if n < 0 then ShowMessage('アドレスが不正です.') else Edit4.Text := IntToHex(n, 4); end else begin n := StrToIntDef('$' + Edit4.Text, -1); if n < 0 then ShowMessage('アドレスが不正です.') else Edit4.Text := IntToStr(n); end; end; procedure TForm4.Edit4KeyPress(Sender: TObject; var Key: Char); begin if Key = #13 then begin Key := #0; BitBtn2Click(self); end; end; procedure TForm4.FormCreate(Sender: TObject); var i : integer; ini : TIniFile; comNo : integer; adr : word; begin clearEdits; BitBtn3.Caption := 'WRITE (10H)' + #13#10 + '16 データ'; BitBtn1.Caption := 'WRITE (10H)' + #13#10 + '1 データ'; BitBtn2.Caption := 'READ (03H)' + #13#10 + '16 データ'; ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini')); try comNo := ini.ReadInteger('Com', 'PortNo', 0); with ComboBox2 do ItemIndex := Items.IndexOf(IntToStr(ini.ReadInteger('Com', 'Baud', 19200))); ComboBox3.ItemIndex := ini.ReadInteger('Com', 'DataBits', 8) - 7; ComboBox4.ItemIndex := ini.ReadInteger('Com', 'Parity', 2); ComboBox5.ItemIndex := ini.ReadInteger('Com', 'StopBits', 1) - 1; UpDown1.Position := ini.ReadInteger('Modbus', 'IDNUM', 1); CheckBox1.Checked := ini.ReadBool('Modbus', 'HEX', true); adr := ini.ReadInteger('Modbus', 'ADRDEC', 9000); if not CheckBox1.Checked then Edit4.Text := IntToStr(adr) else Edit4.Text := IntToHex(adr, 4); finally ini.Free; end; AdSelCom.ShowPortsInUse := False; with ComboBox1 do begin // 使用可能な Comport を検索 for i := 1 to 64 do if AdSelCom.IsPortAvailable(i) then Items.Add (AdPort.ComName(i)); if Items.Count > 0 then ItemIndex := Items.IndexOf('COM' + IntToStr(comNo)); end; with StringGrid1 do begin Cells[0, 0] := 'ADR(DEC)'; Cells[1, 0] := 'ADR(HEX)'; Cells[2, 0] := 'READ(DEC)'; Cells[3, 0] := 'READ(HEX)'; Cells[4, 0] := 'WRITE(DEC)'; end; end; procedure TForm4.FormDestroy(Sender: TObject); var ini : TIniFile; comNo, baud : integer; adr : word; begin if CheckBox1.Checked then adr := StrToIntDef('$' + Edit4.Text, 0) else adr := StrToIntDef(Edit4.Text, 0); ini := TIniFile.Create(ChangeFileExt(ParamStr(0), '.ini')); try comNo := 0; with ComboBox1 do if ItemIndex >= 0 then comNo := StrToIntDef(Copy(Items[ItemIndex], 4), 0); ini.WriteInteger('Com', 'PortNo', comNo); baud := 19200; with ComboBox2 do if ItemIndex >= 0 then baud := StrToIntDef(Items[ItemIndex], 19200); ini.WriteInteger('Com', 'Baud', baud); ini.WriteInteger('Com', 'DataBits', ComboBox3.ItemIndex+7); ini.WriteInteger('Com', 'Parity', ComboBox4.ItemIndex); ini.WriteInteger('Com', 'StopBits', ComboBox5.ItemIndex + 1); ini.WriteInteger('Modbus', 'IDNUM', UpDown1.Position); ini.WriteInteger('Modbus', 'ADRDEC', adr); ini.WriteBool('Modbus', 'HEX', CheckBox1.Checked); finally ini.Free; end; end; end.