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.