YOKOGAWA UT35A/32A RS485 通信 2019/06/15

横河のデジタル指示調節計 UTAdvanced シリーズ RS485 通信の Delphi サンプルコードです。
Modbus の部分は、azbil SDC/R RS485 通信 とほぼ同じです。
RS485-USB コンバータの仮想 COM ポート経由で通信しています。シリアル通信は、ApdComport コンポーネントを使っています。

Modbus RTU 通信はバイナリデータを使用しますが、確認しやすい文字列で作成しそれをバイナリに変換しています。
受信データもバイナリを文字列に変換しています。実用の場合は、文字列に変換する必要はありません。


{
  Yokogawa UT32A RS485 通信
  R485 - PSL(プロトコル)を
    ・PCL(パソコンリンク通信チェックサムなし)
    ・MbASC(Modbus ASCII)
    ・MbRTU (Modbus RTU)(デフォルト)
   に設定
  通信パラメータはデフォルト値を使用

  Modbus ファンクションコード
    0x01 ; コイル読み込み/ビット
    0x02 ; 入力ステータス読み込み/ビット
    0x03 ; 保持レジスタ読み込み/ワード
    0x04 ; 入力レジスタ読み込み/ワード
    0x05 ; 単一コイル書き込み/ビット
    0x06 ; 単一保持レジスタ書き込み/ワード
    0x10 ; 複数レジスタ書き込み

  参考資料:
  「Modbusプロトコル概説書」エム・システム技研
  https://www.m-system.co.jp/mssjapanese/kaisetsu/nmmodbus.pdf
}

unit UT32AUnit;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, OoMisc, AdPort;

type
  TForm4 = class(TForm)
    ApdComPort1: TApdComPort;
    Button1: TButton;
    Edit1: TEdit;
    Button2: TButton;
    Edit2: TEdit;
    Button3: TButton;
    Button4: TButton;
    Button5: TButton;
    Button6: TButton;
    Button7: TButton;
    procedure Button1Click(Sender: TObject);
    procedure ApdComPort1TriggerAvail(CP: TObject; Count: Word);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure Button5Click(Sender: TObject);
    procedure Button6Click(Sender: TObject);
    procedure Button7Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
    buf : string;
    cmdMode : integer;
    // プロトコル
    pslType : integer;
  end;

var
  Form4: TForm4;

const
  PSL_PCL = 1001;
  PSL_MBASC = 1003;
  PSL_MBRTU = 1004;

implementation

{$R *.dfm}

procedure TForm4.ApdComPort1TriggerAvail(CP: TObject; Count: Word);
// UT32A からの受信データ
var
  i : Word;
  sp : SmallInt; // マイナス表示が必要なので SmallInt 型を使用
  B : array [0..511] of Byte;
begin
  // Modbus RTU
  if pslType = PSL_MBRTU then begin
    ApdComPort1.GetBlock(B, Count);
    // 受信バイナリデータを文字列にする
    for i := 0 to Count -1 do
      buf := buf + IntToHex(B[i], 2);
    //
    Edit1.Text := buf;
    if buf.Length >= 10 then
      // 値のみを取得
      Edit2.Text := IntToStr(StrToInt('$' + Copy(buf, 7, 4)));
  end;

  // Modbus Ascii
  if pslType = PSL_MBASC then begin
    for i := 1 to Count do
      buf := buf + string(ApdComPort1.GetChar);
    Edit1.Text := buf;
    // 終端(CrLf)の確認
    if Copy(buf, buf.Length) = #$0A then begin
      // SP 取得であれば・・・
      if cmdMode = 101 then begin
        // 16進数表記の文字列を数値に変換
        sp := StrToInt('$' + Copy(buf, 8, 4));
        Edit2.Text := sp.ToString;
      end;
    end;
  end;

  // PCL
  if pslType = PSL_PCL then begin
    for i := 1 to Count do
      buf := buf + string(ApdComPort1.GetChar);
    Edit1.Text := buf;
    // 終端(Cr)の確認
    if Copy(buf, buf.Length) = #$0D then begin
      // 正常終了の確認
      if Copy(buf, 6, 2) = 'OK' then begin
        // SP 取得であれば・・・
        if cmdMode = 101 then begin
          // 終端の制御文字を外した16進数表記の文字列を数値に変換
          sp := StrToInt('$' + Trim(Copy(buf, 8)));
          Edit2.Text := sp.ToString;
        end;
      end;
    end;
  end;
end;

procedure TForm4.Button1Click(Sender: TObject);
// INF6
// 型番、基本仕様コード、バージョンの読み出し
var
  cmd : string;
begin
  ApdComPort1.ComNumber := 17;
  ApdComPort1.Baud := 19200;
  ApdComPort1.StopBits := 1;
  ApdComPort1.DataBits := 8;
  ApdComPort1.Parity := TParity.pEven;
  try
    ApdComPort1.Open := True;
    // プロトコル
    pslType := PSL_PCL;
    cmdMode := 0;
    buf := '';
    // 送信
    cmd := #02; // STX
    cmd := cmd + '01';  // アドレス番号
    cmd := cmd + '01';  // CPU番号('01'に固定)
    cmd := cmd + '0';   // 応答待ち時間
    cmd := cmd + 'INF'; // コマンド3バイト(INF6 = 型番、基本仕様コード、バージョンの読み出し)
    cmd := cmd + '6';   // コマンドデータ可変長
    cmd := cmd + #03;   // ETX
    cmd := cmd + #$0D;  // CR

    ApdComPort1.PutString(cmd);
    // レスポンス
    //' 0101OKUT32ANNNR1.03.012001001120010000'
    // STX(02)+アドレス番号2バイト('01')+CPU番号2バイト('01')+'OK'+パラメータデータ+ETX(03)+CR($0D)
    // 上の例では、'OK'以降がパラメータデータ
    // UT32ANNNR1.03.012001001120010000
  except
    ShowMessage('ERROR');
  end;
end;

procedure TForm4.Button2Click(Sender: TObject);
// WRD ワード単位の読み出し
// 使用中の目標設定値 SP 取得
var
  cmd : string;
begin
  ApdComPort1.ComNumber := 17;
  ApdComPort1.Baud := 19200;
  ApdComPort1.StopBits := 1;
  ApdComPort1.DataBits := 8;
  ApdComPort1.Parity := TParity.pEven;
  try
    ApdComPort1.Open := True;
    // プロトコル
    pslType := PSL_PCL;
    cmdMode := 101;
    buf := '';
    // 送信
    cmd := #02; // STX
    cmd := cmd + '01';  // アドレス番号
    cmd := cmd + '01';  // CPU番号('01'に固定)
    cmd := cmd + '0';   // 応答待ち時間
    cmd := cmd + 'WRD'; // コマンド 3バイト(WRD = ワード単位の読み出し)
    cmd := cmd + 'D2101';   // レジスタ番号 5バイト   D2101=使用中の目標設定値SP
    cmd := cmd + ',';   //','またはスペース
    cmd := cmd + '01';  // ワード数 2バイト
    cmd := cmd + #03;   // ETX
    cmd := cmd + #$0D;  // CR

    ApdComPort1.PutString(cmd);
    // レスポンス
    //'0101OK01F4'
    // STX(02)+アドレス番号2バイト('01')+CPU番号2バイト('01')+'OK'+パラメータデータ+ETX(03)+CR($0D)
    // 上の例では、'OK'以降がパラメータデータ
    // '01F4' 16進表記なので10進に変換すると設定値'500'(小数点を除いた値)が得られる
    // 事前に小数点位置 DP (レジスタ番号=D5202)を取得しておく
  except
    ShowMessage('ERROR');
  end;
end;

procedure TForm4.Button3Click(Sender: TObject);
// WWR ワード単位の書き込み
// 使用中の目標設定値 SP の変更
var
  cmd : string;
begin
  // COM ポート番号
  ApdComPort1.ComNumber := 17; 

  // UT32A デフォルト値を使用
  ApdComPort1.Baud := 19200;
  ApdComPort1.StopBits := 1;
  ApdComPort1.DataBits := 8;
  ApdComPort1.Parity := TParity.pEven;
  try
    ApdComPort1.Open := True;
    // プロトコル
    pslType := PSL_PCL;
    cmdMode := 0;
    buf := '';
    // 送信
    cmd := #02; // STX
    cmd := cmd + '01';  // アドレス番号
    cmd := cmd + '01';  // CPU番号('01'に固定)
    cmd := cmd + '0';   // 応答待ち時間
    cmd := cmd + 'WWR'; // コマンド 3バイト(WRD = ワード単位の読み出し)
    cmd := cmd + 'D2101';   // レジスタ番号 5バイト   D2101=使用中の目標設定値 SP
    cmd := cmd + ',';   //','またはスペース
    cmd := cmd + '01';  // ワード数 2バイト
    cmd := cmd + ',';   //','またはスペース
    // 書き込みデータは小数点を除いた値を16進数4桁で指定
    cmd := cmd + IntToHex(250, 4); //書き込みデータ 4バイト×データ数 SP を25.0 に変更
    cmd := cmd + #03;   // ETX
    cmd := cmd + #$0D;  // CR

    ApdComPort1.PutString(cmd);
    // レスポンス
    //'0101OK'
    // STX(02)+アドレス番号2バイト('01')+CPU番号2バイト('01')+'OK'+ETX(03)+CR($0D)
  except
    ShowMessage('ERROR');
  end;
end;

procedure TForm4.Button4Click(Sender: TObject);
// WRD ワード単位の読み出し
// 小数点位置 の取得
var
  cmd : string;
begin
  ApdComPort1.ComNumber := 17; 
  ApdComPort1.Baud := 19200;
  ApdComPort1.StopBits := 1;
  ApdComPort1.DataBits := 8;
  ApdComPort1.Parity := TParity.pEven;
  try
    ApdComPort1.Open := True;
    // プロトコル
    pslType := PSL_PCL;
    cmdMode := 0;
    buf := '';
    // 送信
    cmd := #02; // STX
    cmd := cmd + '01';  // アドレス番号
    cmd := cmd + '01';  // CPU番号('01'に固定)
    cmd := cmd + '0';   // 応答待ち時間
    cmd := cmd + 'WRD'; // コマンド 3バイト(WRD = ワード単位の読み出し)
    cmd := cmd + 'D5202';   // レジスタ番号 5バイト   D5202=小数点位置 P.DP_L1
    cmd := cmd + ',';   //','またはスペース
    cmd := cmd + '01';  // ワード数 2バイト
    cmd := cmd + #03;   // ETX
    cmd := cmd + #$0D;  // CR

    ApdComPort1.PutString(cmd);
    // レスポンス
    //'0101OK0001'
    // STX(02)+アドレス番号2バイト('01')+CPU番号2バイト('01')+'OK'+パラメータデータ+ETX(03)+CR($0D)
    // 上の例では、'OK'以降がパラメータデータ
  except
    ShowMessage('ERROR');
  end;
end;

procedure TForm4.Button5Click(Sender: TObject);
// WRD ワード単位の読み出し
// 測定値 PV_L1 の取得
var
  cmd : string;
begin
  ApdComPort1.ComNumber := 17; 
  ApdComPort1.Baud := 19200;
  ApdComPort1.StopBits := 1;
  ApdComPort1.DataBits := 8;
  ApdComPort1.Parity := TParity.pEven;
  try
    ApdComPort1.Open := True;
    // プロトコル
    pslType := PSL_PCL;
    cmdMode := 101;
    buf := '';
    // 送信
    cmd := #02; // STX
    cmd := cmd + '01';  // アドレス番号
    cmd := cmd + '01';  // CPU番号('01'に固定)
    cmd := cmd + '0';   // 応答待ち時間
    cmd := cmd + 'WRD'; // コマンド 3バイト(WRD = ワード単位の読み出し)
    cmd := cmd + 'D2003';   // レジスタ番号 5バイト   D2003= PV_L1
    cmd := cmd + ',';   //','またはスペース
    cmd := cmd + '01';  // ワード数 2バイト
    cmd := cmd + #03;   // ETX
    cmd := cmd + #$0D;  // CR

    ApdComPort1.PutString(cmd);
  except
    ShowMessage('ERROR');
  end;
end;

function CRC16(buf : array of Byte; BufLen: integer): word;
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.Button6Click(Sender: TObject);
// Modbus RTU
// SP 読み出し
var
  s : string;
  i : integer;
  TxBuf : array [0..255] of Byte;
  TxBufLen : integer;
  crc : word;
begin
  ApdComPort1.ComNumber := 17;
  ApdComPort1.Baud := 19200;
  ApdComPort1.StopBits := 1;
  ApdComPort1.DataBits := 8;
  ApdComPort1.Parity := TParity.pEven;
  try
    ApdComPort1.Open := True;
    s := '01';       // 機器アドレス(2桁)
    s := s + '03';   // 複数レジスタの値読み出しコマンド
    // D2101 の Ref,No 42101 - 40001 = 2100 を16進表記に
    s := s + '0834';  //読み出し先頭アドレス(16進4桁)H No.
    s := s + '0001'; //読み出し数(4桁)

    TxBufLen := s.Length div 2;
    for i := 1 to TxBufLen do
      TxBuf[i-1] := StrToInt('$' + s[i * 2 - 1] + s[i * 2]);

    // チェックサム(CRC)計算
    crc := CRC16(TxBuf, TxBufLen);
    Edit1.Text := s;

    s := IntToHex(crc, 4);
    // 文字列として表示
    Edit1.Text := Edit1.Text + s;

    // 送信データに CRC を追加
    TxBuf[TxBufLen ]    := StrToInt('$' + Copy(s, 1, 2));
    TxBuf[TxBufLen + 1] := StrToInt('$' + Copy(s, 3, 2));
    TxBufLen := TxBufLen + 2;
    // プロトコル
    pslType := PSL_MBRTU;
    buf := '';
    // 送信
    ApdComPort1.PutBlock(TxBuf, TxBufLen);
    // レスポンス='01030200FA3807'
    // アドレス番号:01 + ファンクションコード:03 + データバイト数:02 + データ:00FA
    // 最後の4文字はエラーチェック
    // 先頭 7 文字目から 4 文字(16進)$00FA = 250
  except
    ShowMessage('ERROR');
  end;
end;

procedure TForm4.Button7Click(Sender: TObject);
// Modbus ASCII
// SP 読み出し
const
  CRLF = #$0D#$0A;
var
  s, head, chksum : string;
  i : integer;
  sum : integer;
begin
  ApdComPort1.ComNumber := 17;
  ApdComPort1.Baud := 19200;
  ApdComPort1.StopBits := 1;
  ApdComPort1.DataBits := 7;  // ASCII は 7 Bits
  ApdComPort1.Parity := TParity.pEven;
  try
    ApdComPort1.Open := True;
    head := ':';     // 電文先頭
    s := '01';       // 機器アドレス(2桁)
    s := s + '03';   // 読み出しコマンド
    // D2101 の Ref,No 42101 - 40001 = 2100 を16進表記に
    s := s + '0834';  //読み出し先頭アドレス(16進4桁)H No.
    s := s + '0001'; //読み出し数(4桁)

    // チェックサム(LRC)の作成
    // 機器アドレスの先頭からチェックサムの直前まで
    sum := 0;
    // 16進2文字ずつを数値に変換し、加算する
    for i := 1 to s.Length div 2 do
      sum := sum + StrToInt('$' + Copy(s, i * 2 - 1, 2));

    // 加算結果の下位1バイト
    sum := sum and $000000FF;

    // 2の補数
    sum := (- sum and $000000FF);

    // それを2バイトの ASCII コードに変換
    chksum := IntToHex(sum, 2);

    s := s + chksum; // チェックサム
    s := s + CRLF;   // 電文の最後

    // プロトコル
    pslType := PSL_MBASC;
    cmdMode := 101;
    buf := '';

    // 送信
    ApdComPort1.PutString(head + s);
    // ':010302012CCD'+CRLF
    // ':' + アドレス番号:01 + ファンクションコード:03 + データバイト数:02 + データ:012C + CRLF
    // 先頭 8 文字目から 4 文字(16進)$012C = 300
    // 終端 CRLF は $0D0A
  except
    ShowMessage('ERROR');
  end;
end;

end.