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.