ModbusRTU Master for Arduino Modbus Slave 2019/08/19, 21
・2019/08/21 読み込みのファンクションコード 02H が間違っていたのを修正しました。02Hを03Hに。
Arduino で作成した Modbus Slave のテストツールです。他の機能はありません。
Arduino 側のスケッチは、Modbus-Master-Slave-for-Arduino のサンプルスケッチ advanced_slave.ino をそのまま使っています。
■Arduino 側
スレーブの ID は 1、データアドレスは下記のようになるようです。
ファンクションコード 03H で読むとそれぞれの値が読み込まれます。
0 : デジタル入力 × 4 点 (Pin 2 ~ 5) : 0 ~ 15
1 : デジタル出力 × 4 点 (Pin 6 ~ 9) : 0 ~ 15
2 : PWM 出力 × 1 点 (Pin 10) : 0 ~ 255
3 : PWM 出力 × 1 点 (Pin 11) : 0 ~ 255
4 : アナログ入力 × 1 点 (Pin A0) : 0 ~ 1023
5 : アナログ入力 × 1 点 (Pin A1) : 0 ~ 1023
6 : 受信したメッセージ数 (デバッグ用)
7 : 送信したメッセージ数 (デバッグ用)
8 : エラーの数 (デバッグ用)
書き込みはファンクションコード 06H を使用します。
例えば、アドレス 2 に 255 を書き込むと、PWM 出力 1 が 100% 出力になります。
■パソコン側
COM ポート、機器 ID、アドレスを合わせて、[READ] ボタンをクリックすると、アドレス 0 ~ 8 のデータが読み込まれます。
デジタル出力を変更する場合は、DO のチェックボックスをクリックします。
PWM 出力を変更する場合は、「WRITE (DEC)」欄に 0 ~ 255 の数値を入力し、[Enter] キーを押すか [PWM (10,11)]
ボタンをクリックします。
※読み込みは連続ではありません。
通信を中断するには [Close] ボタンををクリックします。再開は、[READ]、[DO (6..9)]、[PWM (10,11)] のいずれかです。
■ダウンロード
ModbusRtuUno.zip (exe 本体のみ)
■パソコン側ソースコード
Delphi 10.3 Community Edition
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, Vcl.CheckLst; 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; Label4: TLabel; Edit1: TEdit; Label5: TLabel; Edit2: TEdit; Edit3: TEdit; Label6: TLabel; Timer1: TTimer; UpDown1: TUpDown; BitBtn8: TBitBtn; Shape1: TShape; Shape2: TShape; Shape3: TShape; Shape4: TShape; Label7: TLabel; Shape5: TShape; Shape6: TShape; Shape7: TShape; Shape8: TShape; Label8: TLabel; CheckListBox1: TCheckListBox; BitBtn1: TBitBtn; BitBtn2: TBitBtn; Label9: TLabel; Button1: TButton; procedure ApdComPort1TriggerAvail(CP: TObject; Count: Word); procedure FormCreate(Sender: TObject); procedure CheckBox1Click(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure Edit4KeyPress(Sender: TObject; var Key: Char); procedure Timer1Timer(Sender: TObject); procedure StringGrid1KeyPress(Sender: TObject; var Key: Char); procedure StringGrid1Click(Sender: TObject); procedure UpDown1Click(Sender: TObject; Button: TUDBtnType); procedure BitBtn8Click(Sender: TObject); procedure CheckListBox1Click(Sender: TObject); procedure BitBtn1Click(Sender: TObject); procedure Button1Click(Sender: TObject); 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 //DObuf : array [0..15] of bool; // Comport 設定 procedure setComPortParams; procedure clearEdits; procedure setShapeColor(idx, sw : integer; coOn, coOff : TColor); procedure setCheckList(sw : integer); procedure sendMbFunc06H(idx : integer; val : word); 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.setCheckList(sw : integer); // DO の値を変更 var i : integer; begin for i := 0 to 3 do CheckListBox1.Checked[i] := sw and IntPower(2, i) > 0; end; procedure TForm4.setShapeColor(idx, sw : integer; coOn, coOff : TColor); // Shape の色を変える // ixd は 1 ~ var i : integer; shp : TShape; co : TColor; begin for i := 0 to 3 do begin shp := Findcomponent('Shape' + IntToStr(idx + i)) as TShape; if shp <> nil then begin if sw and IntPower(2, i) > 0 then co := coOn else co := coOff; shp.Brush.Color := co; end; end; 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.StringGrid1Click(Sender: TObject); begin // COL = 4 AND Row =3,4 のみ編集許可 with StringGrid1 do if (Col = 4) and ((Row = 3) or (Row = 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 ((Row = 3) or (Row = 4)) and (Cells[Col, Row] <> '') then begin // PWM 変更 BitBtn1Click(BitBtn2); end; end; end; end; procedure TForm4.Timer1Timer(Sender: TObject); begin // 再読み込み if cmdMode = 101 then begin Timer1.Enabled := false; BitBtn8Click(self); //StringGrid1.SetFocus; end; end; procedure TForm4.UpDown1Click(Sender: TObject; Button: TUDBtnType); // 接続 ID Up / Down begin BitBtn8Click(self); end; procedure TForm4.ApdComPort1TriggerAvail(CP: TObject; Count: Word); // Comport 受信 var i, j : 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 = $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); if (j >= 1) and (j <= 3) then Cells[4, j + 1] := IntToStr(readData[j]); end; if j >= 15 then break; end; setShapeColor(1, readData[0], clLime, clGray); setShapeColor(5, readData[1], clLime, clGray); setCheckList(readData[1]); 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); // DO, PWM 出力値書き込み var btn : TBitBtn; s : string; idx : integer; v : integer; begin btn := Sender as TBitBtn; s := btn.Name; s := Copy(s, Length(s)); idx := StrToIntDef(s, 0); if idx > 0 then begin v := StrToIntDef(StringGrid1.Cells[4, idx+1], -1); if v >= 0 then begin if v > 255 then v := 255; sendMbFunc06H(idx, v); if idx = 2 then begin // PWM v := StrToIntDef(StringGrid1.Cells[4, idx + 2], -1); if v >= 0 then begin if v > 255 then v := 255; Sleep(10); sendMbFunc06H(idx+1, v); end; end; cmdMode := 101; Timer1.Enabled := true; end; end; end; procedure TForm4.BitBtn8Click(Sender: TObject); // READ (03H) 9 データ 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 Edit3.Text := 'アドレスが不正です.' else begin with StringGrid1 do begin for i := 1 to 9 do begin Cells[2, i] := ''; Cells[3, i] := ''; Cells[4, i] := ''; end; end; txBuf[0] := id; // 機器アドレス txBuf[1] := 3; // ファンクション番号 txBuf[2] := adr shr 8; // 先頭アドレス上位 txBuf[3] := adr and $ff; // 先頭アドレス下位 txBuf[4] := 0; // 読み出し数上位 txBuf[5] := 9; // 読み出し数下位 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.Button1Click(Sender: TObject); // Comport CLOSE begin if ApdComport1.Open then ApdComport1.Open := False; end; procedure TForm4.sendMbFunc06H(idx : integer; val : word); // Modbus ファンクション 06H var i : integer; adr : integer; s : string; crc : integer; id : byte; begin clearEdits; id := StrToIntDef(Edit7.Text, 1); // データの先頭アドレス if CheckBox1.Checked then adr := StrToIntDef('$' + Edit4.Text, -1) else adr := StrToIntDef(Edit4.Text, -1); if adr < 0 then Edit3.Text := 'アドレスが不正です.' else begin adr := adr + idx; if true then begin txBuf[0] := id; // 機器アドレス txBuf[1] := $06; // ファンクション番号 txBuf[2] := adr shr 8; // 先頭アドレス上位 txBuf[3] := adr and $ff; // 先頭アドレス下位 txBuf[4] := val shr 8; // 書き込みデータ上位 txBuf[5] := val and $ff; // 書き込みデータ下位 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; // 再読み込み cmdMode := 101; Timer1.Enabled := True; except end; end; end; end; end; procedure TForm4.CheckBox1Click(Sender: TObject); // 先頭アドレス HEX / DEC 変更 var n : integer; begin if CheckBox1.Checked then begin n := StrToIntDef(Edit4.Text, -1); if n < 0 then Edit3.Text := 'アドレスが不正です.' else Edit4.Text := IntToHex(n, 4); end else begin n := StrToIntDef('$' + Edit4.Text, -1); if n < 0 then Edit3.Text := 'アドレスが不正です.' else Edit4.Text := IntToStr(n); end; end; procedure TForm4.CheckListBox1Click(Sender: TObject); // DO 変更 var i : integer; n : word; begin n := 0; with CheckListBox1 do begin for i := 0 to 3 do if Checked[i] then n := n + IntPower(2, i); end; StringGrid1.Cells[4, 2] := IntToStr(n); sendMbFunc06h(1, n); end; procedure TForm4.Edit4KeyPress(Sender: TObject; var Key: Char); // 先頭アドレス変更 begin if Key = #13 then begin Key := #0; BitBtn8Click(self); end; end; procedure TForm4.FormCreate(Sender: TObject); var i : integer; ini : TIniFile; comNo : integer; adr : word; begin clearEdits; 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', 0); ComboBox5.ItemIndex := ini.ReadInteger('Com', 'StopBits', 1) - 1; UpDown1.Position := ini.ReadInteger('Modbus', 'IDNUM', 1); CheckBox1.Checked := ini.ReadBool('Modbus', 'HEX', false); adr := ini.ReadInteger('Modbus', 'ADRDEC', 0); 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] := 'Name(PIN)'; Cells[2, 0] := 'READ(DEC)'; Cells[3, 0] := 'READ(HEX)'; Cells[4, 0] := 'WRITE(DEC)'; Cells[1, 1] := 'DI (2..5)'; Cells[1, 2] := 'DO (6..9)'; Cells[1, 3] := 'PWM (10)'; Cells[1, 4] := 'PWM (11)'; Cells[1, 5] := 'AI (A0)'; Cells[1, 6] := 'AI (A1)'; Cells[1, 7] := '受信回数'; Cells[1, 8] := '送信回数'; Cells[1, 9] := 'エラー回数'; for i := 1 to 9 do Cells[0, i] := IntToStr(adr + i - 1); end; Timer1.Interval := 100; 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.