Modbus 交流電力測定器PZEM-004Tの使い方

modbus電力測定器の使い方アイキャッチ

M5StackでModbus通信を使用した「交流電力測定器」の使い方を詳しく紹介します。
Modbus通信で取得したデータをM5Stack BASICの液晶画面に表示させて確認できます。

電力アラームの出力機能もあるため、デマンド管理を安価に実現することができるようになります

まずは「基礎編」として、基本的な測定データの表示を行い、各種データの取得方法等を確認します。
次に「応用編」として、測定できる全てのデータと電力アラーム等の設定値表示、さらに各設定値の変更方法をサンプルプログラムで詳しく紹介していきます。

動作確認には「M5Stack Basic Ver2.6」を使用しました。
「M5Stack Basic」の使い方や開発環境の準備については以下のリンクで詳しく紹介しています。

M5Stack Basicの使い方、端子配列、サンプルプログラムで詳しく紹介
基本仕様、端子配列、サンプルプログラムで「Lチカ」やアナログ入力(ADC)、アナログ出力(DAC)、シリアル通信(UART)、液晶表示、バッテリー残量表示等の基本的な動作まで詳しく紹介します。
M5StackシリーズのためのArduino IDEのインストール方法と初期設定、使い方紹介
ArduinoIDEバージョン2のインストール方法から初期設定、スケッチ例の書き込み、コピペでの使い方まで詳しく紹介します。インストールはArduinoでも同じです。

Modbus通信を使用した4チャンネルリレーや、温湿度計「XY-MD02」の使い方も、以下のリンクで詳しく紹介しています。

M5Stack Modbus 485リレー4CHの使い方
M5StackでModbus(Arduinoライブラリ)を使用した4CHリレーの使い方を詳しく紹介。2本の配線だけで4つのリレーをPLCのように制御してみましょう。
M5Stack Modbus温度,湿度測定器XY-MD02の使い方
M5Stackを使ってArduinoコマンドで、Modbus通信を使用した温湿度表示器XY-MD02の使い方をModbusプロトコルと合わせて詳しく紹介します。
スポンサーリンク

1.Modbusとは

「Modbus」とは、通信プロトコル(通信手順)の1つで、主に産業機器(PLC、センサー、モーターなど)の制御に使用され、マスタースレーブ方式で動作し、1つのマスターに対して複数のスレーブ機器を接続して制御できます。

シリアル通信や、TCP/IP上のEthernet通信(LAN、WAN)でも使用でき、通信に必要な配線は2本(A+、B-)だけです。

通信距離はシリアル通信(RS485)では数百m〜1km程、TCP / IPではLANで100m程(中継可)WANならネットワーク環境によって広範囲の通信が可能です。

比較的簡単に実装でき柔軟性があるため、工場やプロセスオートメーション、エネルギー管理などの分野で広く使用されています。

スポンサーリンク

2.電力測定モジュールPZEM-004Tについて

・外観、付属品

電力測定モジュール「PZEM-004T」の梱包状態や外観、付属品は下写真のようになります。

modbus電力測定器外観

梱包状態は上写真のようになります。
梱包箱には型式は書かれていません。

modbus電力測定器外観

梱包箱の裏側にも型式は書かれていませんが、タイプや付属品のUSBケーブルの有無がチェックマークでわかるようになっています。
今回購入したものはAC(交流)用、 測定上限100A、分割カレントトランス付、USBケーブル付です。

modbus電力測定器梱包状態

箱の中身はこんな感じで、USBケーブル以外は袋に入って綺麗に梱包されています。

modbus電力測定器付属品

上写真中央が本体で、右側が電流を測定するためのカレントトランスで電線に挟み込んで使用できるタイプです。
左側がUSBケーブルでJST製XHの4Pコネクタで、本体と接続してパソコンのシリアルモニタ等からコマンドを送信して動作確認できます。
取説は英語と中国語で書かれたもので日本語は無いです。

USBケーブルは付属しないものもあるため、購入時には注意しましょう。
今回購入したのは「100A(004T-100A)」まで測定できるものですが「10A(004T-10A)」タイプもあります。
また、カレントトランスは「分割式(SPLIT-CT)」のものを購入しましたが、分割できない「リング状(COIL-CT)」で線を通すタイプが付属したものもあるため、よく確認してから購入しましょう。

・端子配列

端子配列は下写真のようになります。

modbus電力測定器端子配列

ネジ式端子台の方は上写真左側の赤黒の細い線がカレントトランス用、右側の白黒の太い配線がAC100Vの入力用です。
※ネジ端子部は指では触れられないと思いますが、危険なので上からテープを貼る等の絶縁処理をしておきましょう。

modbus電力測定器端子配列

通信端子はコネクタ(JST製XH)になっています。
Vcc-GND間は5V、RXはデータ受信用、TXはデータ送信用で3.3V系でもそのまま動作はできました。
※modbus通信で操作しますが、通信配線出力は「UART」になっています。
このため通信線は数十cm程にしておきましょう。

modbus電力測定器外観

本体裏側には配線図が書かれていますが、非常に見にくいので取説で確認しましょう。

modbus電力測定器配線図

配線図を上写真のように画像上に書き込んでみました。
詳細な配線図はこの後で、より詳しく紹介しています。

・基本仕様

「測定項目」や「測定精度」等の基本仕様は以下表のようになります。

modbus電力測定器仕様
今回使用したものは「100A」仕様のため、測定最小単位は「0.001A」ですが、測定精度は「100Aの0.5%」で「±0.5A」程になります。

・レジスタ構成

測定データや設定値、スレーブアドレスが格納されているレジスタ構成は以下のようになります。

測定データ格納レジスタ

測定データが格納されているレジスタ構成は以下表のようになります。

modbus電力測定器測定データレジスタ構成
各レジスタのサイズは16bitで、上位と下位で構成されている値は合わせて32bitに変換して使用します。
データの取得はファンクションコード「0x04(入力レジスタ読込)」で行います。

電力アラーム設定値、スレーブアドレス格納レジスタ

設定変更可能なデータは「電力アラーム設定値」と「スレーブアドレス」のみで、以下表のようになります。

modbus電力測定器測定データレジスタ構成
各レジスタのサイズは16bitです。
データの取得はファンクションコード「0x03(保持レジスタ読込)」、設定変更は「0x06(保持レジスタ単独書込)」で行います。
各設定値の初期値は以下になります。
・電力アラーム設定値:23000
・スレーブアドレス:0x0001

積算電力値リセット

積算電力値のリセットには特殊なファンクションコード「0x42」を使用します。
これには今回使用したライブラリでは指定できないので、直接シリアル通信でコマンドを送信します。

送信するコマンドでは「レジスタアドレス」は指定せず、スレーブアドレスとファンクションコード、CRCを送信するのみです。送信コマンドは以下になります。

スレーブアドレス + ファンクションコード + CRC上位 + CRC下位
→「0x01」+「0x42」+「0x80」+「0x11」
「CRC」の値はスレーブアドレス「1」の場合の値です。スレーブアドレスを変更した場合はModbusツール等で確認して書き換える必要があります。
詳細はサンプルプログラム(応用編)の「積算電力値のリセット(特殊ファンクションコード:0x42)」で詳しく紹介しています。
スポンサーリンク

3.動作確認の準備

動作確認は以下のように、測定データ表示用の「液晶表示器」と「AC100Vコンセント配線」に「電流測定センサのカレントトランス」をクランプして行いました。

modbus電力測定器端子配列例

実際に使用した部品は以下になります。

・準備するもの

・M5Stack BASIC:受信した測定データ表示用
・AC100Vコンセントプラグ(オス/メス):電源供給と測定対象の接続用
・電線(1.25㎟ 白/黒):色は任意です。電線サイズは測定対象に合わせて準備してください。
            今回は15Aまでを想定しています。
・ワンタッチコネクタ(WAGO製 WFR-3):配線接続用
・Groveコネクタ配線:電力測定器とM5Stackの通信配線接続用

各部品の詳細は以下になります。

M5Stack BASIC

「M5Stack Basic」とは「M5Stackテクノロジー社」のマイコンボードです。

M5Stack basicの外観
M5Stack basicの使い方

2.0インチ(320*240)液晶表示器を搭載しており、入出力端子、3個のボタン、3軸ジャイロ+3軸加速度センサ、スピーカ搭載でmicroSDカードリーダーやバッテリーも内蔵されています。

他にも無線通信のWiFiやBluetoothはもちろん、シリアル通信(UART)にも対応しているため、今回は電力測定器から通信で取得したデータを液晶に表示させて確認するために使用しています。

AC100Vコンセントプラグ(オス/メス)

今回使用する電力測定器は交流用のため、以下のようなAC100V用のコンセントプラグ(オス/メス)を使用します。

配線は施工説明書または取扱説明書をよく読んで正しく配線しましょう。
配線後は短絡していないかテスターで確認して、各配線を引っ張って抜けてこないか確認してからコンセントに差し込みましょう。(必要に応じて適切な絶縁処理を行ってください)

電線(1.25㎟ 白/黒)

電線はAC100Vで使用するのでAC100V以上で使用可能な配線を準備しましょう。
電線サイズは最低でも0.75㎟で色は2種類あればなんでも良いです。

2.0㎟は太すぎて端子台に入れにくいので、1.25㎟がおすすめです。

ワンタッチコネクタ(WAGO製 WFR-3)

AC100V配線の接続にはWAGO製の「ワンタッチコネクタ」が簡単で安全です。

圧着端子は工具が必要で、作業にもバラツキが出ますが、このコネクタなら被覆の剥き長さ(11mm)さえ間違えなければ確実に接続できて手軽で便利です。(ホームセンターでも売ってます。)

使い方は、オレンジのレバーを起こして、被覆を11mm剥いた配線を奥まで差し込んでからレバーを戻すだけです。
WAGOワンタッチコネクタ使い方
配線を差し込んだ後は引っ張って抜けないか必ず確認しましょう。
また、レバーは完全に起こさなくても半分程度起こした時点で配線を引っ張ると抜けるので、上画像のようにレバーが動かないようにテープで止めておくことをおすすめします。

Groveコネクタ付き配線

通信線の接続にはGroveコネクタ付配線があれば差し込むだけで簡単に接続できます。
「電力測定器」側がJST製XHコネクタ4Pです。
「M5Stack」側はジャンパーコネクタの「メス」タイプの方が抜けにくいです。

「seeed studio社製」と「M5Stack社製」等で「黄色」と「白色」配線の端子配列が逆のものがあるので注意しましょう。

・配線図

動作確認時の配線図は以下になります。
※配線色が変則的になります。実際に使用する配線の色と配線図をよく確認して配線してください。

modbus電力測定器配線図
電力測定器の通信端子「RX/TX」はM5Stackの通信端子へ「RX→TX(T2)」、「TX→RX(R2)」となるように接続してください。
電流測定センサの「カレントトランス」には向きがあります。
「カレントトランス」に表示されている矢印を配線図の矢印に合わせてください。

実際に配線したものは下写真になります。

modbus電力測定器端子配列例

・使用ライブラリ

使用するライブラリは以下表のようになります。
「ArduinoIDE」で事前にインストールしておいてください。

ライブラリ名用途バージョン検索名
M5StackM5Stack制御用0.4.6M5Stack
M5GFX液晶画面制御用0.2.9M5G
ModbusMasterModbus通信用2.0.1ModbusMaster
バージョンは動作確認時のバージョンです。
「ArduinoIDE」で書き込みエラーになるときは、一旦インストール済みのライブラリを削除して再インストールすると書き込みできるようになることがあります。

開発環境「ArduinoIDE」の使い方は、以下のリンクで詳しく紹介しています。

M5StackシリーズのためのArduino IDEのインストール方法と初期設定、使い方紹介
ArduinoIDEバージョン2のインストール方法から初期設定、スケッチ例の書き込み、コピペでの使い方まで詳しく紹介します。インストールはArduinoでも同じです。

4.基礎編:基本的な通信方法の確認(電圧、電流、電力のみ表示)

・動作紹介

まずは基礎編として、下写真のように電圧と電流、電力のみの測定データを取得して表示させます。

modbus電力測定器シンプル表示

・サンプルプログラム(基礎編)

サンプルプログラムは以下になります。コピペで貼り付けて書き込んで実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include <M5Stack.h>
#include <ModbusMaster.h> // Modbus通信用ライブラリ
#include <M5GFX.h>        // 液晶表示用ライブラリのインクルード

M5GFX lcd;              // 直接表示のインスタンスを作成(M5GFXクラスを使ってlcdコマンドでいろいろできるようにする)
M5Canvas canvas(&lcd);  // メモリ描画領域表示(スプライト)のインスタンスを作成(必要に応じて複数作成)
ModbusMaster node1;     // ModbusMasterオブジェクトのインスタンス化

// グローバル変数宣言 *****************************************************
bool err = false;       // エラー確認用
uint16_t get_data[5];   // 取得データ格納用配列
double volt;            // 電圧(V)
double current;         // 電流値(A)
double watt;            // 電力値(W)

/******************* タイトル、単位、設定情報表示関数 *******************/
void displayHeader() {
  canvas.fillScreen(BLACK);      // 背景色(黒)
  canvas.setFont(&fonts::Font4); // フォント
  canvas.setTextSize(1);         // テキストサイズ
  canvas.setTextColor(WHITE);    // 文字色
  
  // タイトル表示
  canvas.drawFastHLine(0, 0, 320, WHITE);           // タイトルエリア境界線(上)
  canvas.fillRect(0, 1, 320, 27, (uint16_t)0x2c4a); // タイトルエリア背景
  canvas.drawFastHLine(0, 29, 320, WHITE);          // タイトルエリア境界線(下)
  canvas.setCursor(25, 5);           // 座標
  canvas.print("1PH POWER MONITOR"); // タイトル
  
   // 単位表示
  canvas.setTextSize(1.4);           // テキストサイズ
  canvas.setCursor(250, 65); canvas.print("V");  // 電圧V
  canvas.setCursor(250, 135); canvas.print("A"); // 電流A
  canvas.setCursor(245, 205); canvas.print("W"); // 電力W

  // 境界線表示
  canvas.drawFastHLine(10, 100, 300, WHITE); // 境界線1
  canvas.drawFastHLine(10, 170, 300, WHITE); // 境界線2
}

/******************* 測定データ表示関数 *******************/
void drawData() {
  canvas.setFont(&fonts::Font7); // フォント(7セグ)
  canvas.setTextSize(1.3);       // テキストサイズ

  canvas.setTextColor((uint16_t)0xaf7d);  // 電圧(V)
  canvas.setCursor(45, 35);
  canvas.printf("%05.1f", volt);

  canvas.setTextColor((uint16_t)0x07ef);  // 電流(A)
  canvas.setCursor(45, 105);
  canvas.printf(current < 10 ? "%05.3f" : "%05.2f", current); // 10より小さければ0.000、10以上なら00.00

  canvas.setTextColor((uint16_t)0xfea0);  // 電力(W)
  canvas.setCursor(45, 175);
  canvas.printf(watt < 1000 ? "%05.1f" : "%04.0f", watt); // 1000より小さければ000.0、1000以上なら0000

  canvas.pushSprite(&lcd, 0, 0); // 画面表示実行
}

/******************* データ変換関数 *******************/
void convertData() {
  volt = get_data[0] * 0.1; // 電圧 (0.1W単位)
  current = ((uint32_t)get_data[2] << 16 | get_data[1]) * 0.001;   // 電流 (0.001A単位)
  watt = ((uint32_t)get_data[4] << 16 | get_data[3]) * 0.1;        // 電力 (0.1W単位)
}

/******************* データ取得、エラーチェック関数 *******************/
void fetchModbusData() {
  err = false; // エラーフラグ初期化
  uint8_t result = node1.readInputRegisters(0x0000, 0x05); // Modbus通信実行(開始レジスタ, レジスタ数)

  if (result == node1.ku8MBSuccess) { // データ受信成功なら
      for (int i = 0; i < 5; i++) { // 受信データ5個分繰り返し
        get_data[i] = node1.getResponseBuffer(i); // 受信データバッファをget_data配列に格納
        Serial.print(get_data[i]);                // 格納データシリアル表示
        Serial.print(" ");                        // スペース
      }
      Serial.print("\n"); // 改行
  } else {                   // データ受信失敗なら
    err = true;              // エラーフラグtrue
    Serial.println("Error"); // エラー表示
  }
}

/******************* エラー画面表示関数 *******************/
void displayError() {
  canvas.fillScreen(RED);        // 背景色
  canvas.setFont(&fonts::Font4); // フォント
  canvas.setTextSize(1);         // テキストサイズ
  canvas.setTextColor(WHITE);    // 文字色
  
  // エラー表示
  canvas.setCursor(120, 100);
  canvas.print("ERROR!");        // エラー表示

  canvas.pushSprite(&lcd, 0, 0); // 画面表示実行
}

// 初期設定 *********************************************************************
void setup() {
  M5.begin();
  M5.Power.begin();

  lcd.begin();             // 画面初期化
  // メモリ描画領域の初期設定(スプライト)
  canvas.setColorDepth(8); // カラーモード設定(初期値16bit)Basic はメモリ描画領域サイズを大きく(320x173以上?)すると16bit以上で表示されないため8bit推奨
  canvas.createSprite(lcd.width(), lcd.height()); // メモリ描画領域を準備

  // シリアル通信設定
  Serial.begin(9600); // 標準のUSB通信ポート設定 3, 1 (RX, TX)
  Serial2.begin(9600, SERIAL_8N1, 16, 17); // Modbus通信ポート設定
  node1.begin(1, Serial2); // Serialポートを介してスレーブ1とMosbus通信開始 (スレーブアドレス, 通信ポート)
}

// メイン **************************************************************************
void loop() {
  M5.update();

  // 測定データ取得表示
  displayHeader();   // タイトル、単位、設定情報表示関数
  fetchModbusData(); // データ取得、エラーチェック関数
  if (err != true) {
    convertData();   // データ変換関数
    drawData();      // 測定データ表示関数
  } else {
    displayError();  // エラー表示関数
  }
  delay(500);
}

・初期設定詳細

Modbus通信を行うための初期設定はサンプルプログラム内で以下のように行っています。

まずはサンプルプログラムの「2行目」で以下のように、Modbus通信を行うためのライブラリをインクルードします。

#include <M5GFX.h> // 液晶表示用ライブラリのインクルード

次に「node1.〜」でModbus通信の各種操作が実行できるように、「7行目」で以下のように、「ModbusMaster」のインスタンスを「node1」として作成します。

ModbusMaster node1; // ModbusMasterオブジェクトのインスタンス化

初期設定として、「setup()」関数の「112〜113行目」で以下のように、Modbus通信を行うためのシリアル通信端子を設定し、スレーブアドレスを指定してModbus通信を開始します。

Serial2.begin(9600, SERIAL_8N1, 16, 17); // Modbus通信ポート設定
node1.begin(1, Serial2); // Serialポートを介してスレーブ1とMosbus通信開始 (スレーブアドレス, 通信ポート)

・測定データ受信方法(入力レジスタ読込:0x04)

測定したデータはサンプルプログラムの「69〜84行目」で以下のようにModbus通信で取得することができます。

void fetchModbusData() {
  err = false; // エラーフラグ初期化
  uint8_t result = node1.readInputRegisters(0x0000, 0x05); // Modbus通信実行(開始レジスタ, レジスタ数)

  if (result == node1.ku8MBSuccess) { // データ受信成功なら
      for (int i = 0; i < 5; i++) { // 受信データ5個分繰り返し
        get_data[i] = node1.getResponseBuffer(i); // 受信データバッファをget_data配列に格納
        Serial.print(get_data[i]);                // 格納データシリアル表示
        Serial.print(" ");                        // スペース
      }
      Serial.print("\n"); // 改行
  } else {                   // データ受信失敗なら
    err = true;              // エラーフラグtrue
    Serial.println("Error"); // エラー表示
  }
}

上コードは長々と書いてますが、基本的には以下の2手順でデータを取得できます。

  1. 3行目」でスレーブアドレスを指定して、データをリクエストするコマンドを送信すると対象のスレーブ機器からレスポンスとしてデータが送信されてきます。
  2. 6行目」から送信されてきたデータを取得して変数に格納しています。

3行目」の詳細は以下のようになります。

result = node1.readInputRegisters(開始アドレス, 読み取りレジスタ数);
  • node1:スレーブアドレス「1」との通信を表します。
  • readInputRegisters:Modbus通信のファンクションコード「0x04」を表し入力レジスタ」からデータを取得するためのコマンドです。
    各測定データは「入力レジスタ」に格納されているため、このコマンドを使用します。
    連続したレジスタのデータであれば、まとめて取得することができます。
  • 開始アドレス:取得したいデータが格納されている先頭のアドレスを指定します。
    測定データ格納レジスタ」の表によると、「電圧」測定値はアドレス「0x0000」に、「電流値の下位」の測定値はアドレス「0x0001」「電流値の上位」の測定値はアドレス「0x0002」に格納されています。
    このように連続したレジスタに格納されているデータは「開始アドレス」の「0x0000」を指定して次の「読み取りレジスタ数」を指定することで、連続して取得することができます。
  • 読み取りレジスタ数:取得したいデータのレジスタ数を表します。
    1つなら「1」ですが、今回は連続した5つのデータ(電圧、電流下位、電流上位、電力下位、電力上位)を取得したいため「5」を指定します。
  • result:通信結果が格納されます。通信成功の場合は「0」で「0以外」は通信エラーです。

6行目」の詳細は以下のようになります。
連続してデータを取得するために「for文」を使用しています。

for (int i = 0; i < 5; i++) {                           // 読取りレジスタ数分繰返す
  get_data[i] = node1.getResponseBuffer(i);   // 受信データバッファ取得
}
  • node1.getResponseBuffer(i):これでスレーブから送信されてきたデータを取得できます。
    連続したデータは配列として取得して使用します。
  • get_data[i]:取得したデータを格納する配列です。
    受信するデータのバイト数に合わせて準備しておく必要があります。
測定データ格納レジスタ」の表によると「入力レジスタ」のデータは「16bit」のため「16bit」以上の型で準備しておく必要があります。
今回は、「uint16_t」型としました。

・測定データ換算(16bit → 32bit)

取得したデータは「測定データ格納レジスタ」の表に応じて、各データごとにサンプルプログラムの「62〜66行目」で以下のように表示データに換算します。

void convertData() {
  volt = get_data[0] * 0.1; // 電圧 (0.1W単位)
  current = ((uint32_t)get_data[2] << 16 | get_data[1]) * 0.001;   // 電流 (0.001A単位)
  watt = ((uint32_t)get_data[4] << 16 | get_data[3]) * 0.1;        // 電力 (0.1W単位)
}

電圧は取得した「16bit」のデータをそのまま使用し最小単位は「0.1V」として換算します。
電流電力は「下位16bit」、「上位16bit」を結合して「32bit」のデータとして使用し、電流の最小単位は「0.001A」、電力も同様に32bitにして最小単位は「0.1W」として換算します。

・エラー処理について

エラー処理は上記の「測定データ受信方法」で測定データの受信でデータを受信できなかったときにサンプルプログラムの「87〜98行目」で以下のように、シンプルに背景色を赤にして「ERROR」とだけ表示するようにしています。

void displayError() {
  canvas.fillScreen(RED);        // 背景色
  canvas.setFont(&fonts::Font4); // フォント
  canvas.setTextSize(1);         // テキストサイズ
  canvas.setTextColor(WHITE);    // 文字色
  
  // エラー表示
  canvas.setCursor(120, 100);
  canvas.print("ERROR!");        // エラー表示

  canvas.pushSprite(&lcd, 0, 0); // 画面表示実行
}
基礎編」では、シンプルな「データ取得と表示」方法を確認しました。
次の「応用編」では、測定できる「全てのデータと設定データ」の表示、さらに「設定データの変更」方法について動作確認する方法を、サンプルプログラムで詳しく紹介しています。

5.応用編:全データ受信、内部設定確認/変更

・動作紹介

次に応用編として、測定可能な全てのデータ(電圧、電流、電力、周波数、力率、積算電力)を取得して表示させます。

modbus電力測定器詳細表示
画面左下には「スレーブアドレス」、右下には「電力アラーム設定値」を表示させています。

さらに、「電力アラーム設定値」を変更する設定画面も表示できるようにして、設定した電力値以上になると背景色を変えて出力端子をONするようにしています。設定方法は下写真のようになります。

modbus電力測定器電力アラーム設定

左ボタン]を約1秒以上押すと上写真のような設定画面が表示されます。
再度[左ボタン]を約1秒以上押すと測定画面に戻ります。

modbus電力測定器電力アラーム設定

右ボタン]を押すと設定値が10Wづつ増えます。

modbus電力測定器電力アラーム設定

中央のボタン]を押すと設定値が10Wづつ減ります。

電力値が「電力アラーム設定値」以上になると、下写真のように背景が赤くなり、出力端子「5」がLOWレベルになります。動作確認では下写真のように抵抗内蔵LEDを点灯させています。

modbus電力測定器電力アラーム動作確認
modbus電力測定器電力アラーム動作確認

また、「積算電力値(画面右下のkW値)」は常に積算データとして記録され続けていますが、下写真のように[右ボタン]を押すことで0リセットできるようにしています。

modbus電力測定器積算電力リセット
modbus電力測定器積算電力リセット

通信エラーでデータの取得ができない時は、下写真のように「ERROR!」が表示されます。

modbus電力測定器エラー

・サンプルプログラム(応用編)

サンプルプログラムは以下になります。コピペで貼り付けて書き込んで実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

#include <M5Stack.h>
#include <ModbusMaster.h> // Modbus通信用ライブラリ
#include <M5GFX.h>        // 液晶表示用ライブラリのインクルード

M5GFX lcd;              // 直接表示のインスタンスを作成(M5GFXクラスを使ってlcdコマンドでいろいろできるようにする)
M5Canvas canvas(&lcd);  // メモリ描画領域表示(スプライト)のインスタンスを作成(必要に応じて複数作成)
ModbusMaster node1;     // ModbusMasterオブジェクトのインスタンス化

#define ARARM_OUT 5  // 出力端子

// グローバル変数宣言 *****************************************************
bool err = false;       // エラー確認用
uint8_t result;         // 通信結果格納用
uint16_t get_data[10];  // 取得データ格納用配列
double volt;            // 電圧(V)
double current;         // 電流値(A)
double watt;            // 電力値(W)
double wattHour;        // 積算電力値(W)
float freq;             // 周波数(Hz)
uint16_t pf;            // 力率(%)
bool alarmSignal;       // アラーム出力
uint16_t alarmLevel;    // アラームレベル設定値(W)
uint16_t slaveAddress;  // スレーブアドレス
bool settingModeFlag = false;   // 設定モードフラグ

/******************* タイトル、単位、設定情報表示関数 *******************/
void displayHeader() {
  if (alarmSignal == false) { // 電力アラーム状態でなければ
    canvas.fillScreen(BLACK);      // 背景色(黒)
    digitalWrite(ARARM_OUT, HIGH); // 外部出力OFF
  } else {                    // 電力アラーム状態でなければ
    canvas.fillScreen(RED);        // 背景色(赤)
    digitalWrite(ARARM_OUT, LOW);  // 外部出力ON
  }
  canvas.setFont(&fonts::Font4); // フォント
  canvas.setTextSize(1);         // テキストサイズ
  canvas.setTextColor(WHITE);    // 文字色
  
  // タイトル表示
  canvas.drawFastHLine(0, 0, 320, WHITE);           // タイトルエリア境界線(上)
  canvas.fillRect(0, 1, 320, 27, (uint16_t)0x2c4a); // タイトルエリア背景
  canvas.drawFastHLine(0, 29, 320, WHITE);          // タイトルエリア境界線(下)
  canvas.setCursor(25, 5);           // 座標
  canvas.print("1PH POWER MONITOR"); // タイトル
  
   // 単位表示
  canvas.setCursor(135, 60);  // 座標
  canvas.print("V");          // 表示内容
  canvas.setCursor(135, 120);
  canvas.print("A");
  canvas.setCursor(135, 180);
  canvas.print("W");
  canvas.setCursor(282, 60);
  canvas.print("Hz");
  canvas.setCursor(282, 120);
  canvas.print("%");
  canvas.setCursor(282, 180);
  canvas.print("kW");

  // 境界線表示
  canvas.drawFastHLine(5, 90, 310, WHITE); // 境界線1
  canvas.drawFastHLine(5, 150, 310, WHITE); // 境界線2
  canvas.drawFastHLine(5, 210, 310, WHITE); // 境界線3

  // 設定情報表示
  canvas.setCursor(20, 215);    // 座標
  canvas.setFont(&fonts::Font2); // フォント
  canvas.printf("Slave Address : %d", slaveAddress); // スレーブアドレス
  canvas.setCursor(170, 215);
  canvas.setFont(&fonts::Font2);
  canvas.printf("Alarm Level : %dW", alarmLevel); // 電力アラームレベル設定値

}

/******************* 測定データ表示関数 *******************/
void drawData() {
  canvas.setFont(&fonts::Font7); // フォント(7セグ)
  canvas.setTextSize(0.9);       // テキストサイズ

  canvas.setTextColor((uint16_t)0xaf7d);  // 電圧(V)
  canvas.setCursor(3, 40);
  canvas.printf("%05.1f", volt);

  canvas.setTextColor((uint16_t)0x07ef);  // 電流(A)
  canvas.setCursor(3, 100);
  canvas.printf(current < 10 ? "%05.3f" : "%05.2f", current); // 10より小さければ0.000、10以上なら00.00

  canvas.setTextColor((uint16_t)0xfea0);  // 電力(W)
  canvas.setCursor(3, 160);
  canvas.printf(watt < 1000 ? "%05.1f" : "%04.0f", watt); // 1000より小さければ000.0、1000以上なら0000

  canvas.setTextColor((uint16_t)0xaf7d);  // 周波数(Hz)
  canvas.setCursor(180, 40);
  canvas.printf("%04.1f", freq);

  canvas.setTextColor((uint16_t)0x07ef);  // 力率(%)
  canvas.setCursor(200, 100);
  canvas.printf("%2d", pf);

  canvas.setTextColor((uint16_t)0xfea0);  // 積算電力量(kW)
  canvas.setCursor(180, 160);
  canvas.printf(wattHour < 100 ? "%04.1f" : "%04.0f", wattHour); // 100より小さければ00.0、100以上なら000
  
  canvas.pushSprite(&lcd, 0, 0); // 画面表示実行
}

/******************* データ変換関数 *******************/
void convertData() {
  volt = get_data[0] * 0.1; // 電圧 (0.1W単位)
  current = ((uint32_t)get_data[2] << 16 | get_data[1]) * 0.001;   // 電流 (0.001A単位)
  watt = ((uint32_t)get_data[4] << 16 | get_data[3]) * 0.1;        // 電力 (0.1W単位)
  wattHour = ((uint32_t)get_data[6] << 16 | get_data[5]) * 0.001;  // 電力量 (1kW単位)
  freq = get_data[7] * 0.1; // 周波数 (0.1W単位)
  pf = get_data[8];         // 力率
  if (get_data[9] == 0) {
    alarmSignal = false;
  } else {
    alarmSignal = true;
  }
}

/******************* データ取得、エラーチェック関数 *******************/
void fetchModbusData() {
  err = false; // エラーフラグ初期化
  result = node1.readInputRegisters(0x0000, 0x0A); // Modbus通信実行(開始レジスタ, レジスタ数)

  if (result == node1.ku8MBSuccess) { // データ受信成功なら
      for (int i = 0; i < 10; i++) { // 受信データ10個分繰り返し
        get_data[i] = node1.getResponseBuffer(i); // 受信データバッファをget_data配列に格納
        Serial.print(get_data[i]);                // 格納データシリアル表示
        Serial.print(" ");                        // スペース
      }
      Serial.print("\n"); // 改行
  } else { // 
    err = true;              // エラーフラグtrue
    Serial.println("Error"); // エラー表示
  }
}

/******************* エラー画面表示関数 *******************/
void displayError() {
  canvas.fillScreen(RED);      // 背景色
  canvas.setFont(&fonts::Font4); // フォント
  canvas.setTextSize(1);         // テキストサイズ
  canvas.setTextColor(WHITE);    // 文字色
  
  // エラー表示
  canvas.setCursor(120, 100);
  canvas.print("ERROR!"); // タイトル

  canvas.pushSprite(&lcd, 0, 0); // 画面表示実行
}

/******************* スレーブアドレス読み出し関数 *******************/
void readAddress() {
  result = node1.readHoldingRegisters(0x0002, 0x0001); // (アドレス, 読取レジスタ数n)
  slaveAddress = node1.getResponseBuffer(0); // 受信データバッファ取得
  Serial.println(slaveAddress);
}

/******************* 電力アラームレベル読み出し関数 *******************/
void readAlarmLevel() {
  result = node1.readHoldingRegisters(0x0001, 0x0001); // (アドレス, 読取レジスタ数n)
  alarmLevel = node1.getResponseBuffer(0); // 受信データバッファ取得
  Serial.println(alarmLevel);
}

/******************* アラーム電力値書き込み関数 *******************/
void writeAlarmLevel() {
  result = node1.writeSingleRegister(0x0001, alarmLevel);  // (アドレス, 書き込む値)
  readAlarmLevel(); // 電力アラームレベル読み出し関数
}

/******************* 設定モード画面表示 *******************/
void displaySettingMode() {
  canvas.fillScreen(BLACK);      // 背景色
  canvas.setFont(&fonts::Font4); // フォント
  canvas.setTextSize(1);         // テキストサイズ
  canvas.setTextColor(WHITE);    // 文字色

  // タイトル表示
  canvas.drawFastHLine(0, 0, 320, WHITE);  // タイトルエリア境界線(上)
  canvas.fillRect(0, 1, 320, 27, MAROON);  // タイトルエリア背景
  canvas.drawFastHLine(0, 29, 320, WHITE); // タイトルエリア境界線(下)
  canvas.setCursor(25, 5);           // 座標
  canvas.print("1PH POWER MONITOR"); // タイトル
  
  if (M5.BtnB.isPressed()) { // BボタンONなら
    alarmLevel = alarmLevel - 10;
  }

  if (M5.BtnC.isPressed()) { // CボタンONなら
    alarmLevel = alarmLevel + 10;
  }

  canvas.setCursor(10, 50);           // 座標
  canvas.setFont(&fonts::Font4);      // フォント
  canvas.println(" Setting Mode");    // 表示内容
  canvas.println("   AlarmPowerLevel");
  canvas.setCursor(0, 115);           // 座標
  canvas.setFont(&fonts::Font7);
  canvas.setTextSize(2);              // テキストサイズ
  canvas.printf(" %d", alarmLevel);   // 電力アラームレベル設定値
  canvas.setCursor(285, 195);
  canvas.setFont(&fonts::Font4);
  canvas.setTextSize(1);              // テキストサイズ
  canvas.print(" W");

  // 境界線表示
  canvas.drawFastHLine(5, 105, 310, WHITE); // 境界線1
  canvas.drawFastHLine(5, 220, 310, WHITE); // 境界線2

  canvas.pushSprite(&lcd, 0, 0); // 画面表示実行
}

/******************* 積算電力値リセット関数 *******************/
void resetWatt() {
  // ファンクションコード0x42は一般的なModbusプロトコル(ライブラリにも)に存在しないため直接送信
  Serial2.write(0x01); // スレーブアドレス
  Serial2.write(0x42); // ファンクションコード0x42(積算電力リセット用)
  Serial2.write(0x80); // CRC
  Serial2.write(0x11); // CRC

  delay(100); // 受信データ待ち時間(50ms以上)

  while (Serial2.available()) { // 受信データがなくなるまで繰り返し(送信データと同じものが受信される)
    uint16_t data = Serial2.read();  // データ受信
    Serial.printf("%0X", data);      // 受信データシリアル表示
    Serial.print(" ");               // スペース
  }
  Serial.print("\n"); // 改行
}

// 初期設定 *********************************************************************
void setup() {
  M5.begin();
  M5.Power.begin();

  lcd.begin();             // 画面初期化
  // メモリ描画領域の初期設定(スプライト)
  canvas.setColorDepth(8); // カラーモード設定(初期値16bit)Basic はメモリ描画領域サイズを大きく(320x173以上?)すると16bit以上で表示されないため8bit推奨
  canvas.createSprite(lcd.width(), lcd.height()); // メモリ描画領域を準備

  // シリアル通信設定
  Serial.begin(9600); // 標準のUSB通信ポート設定 3, 1 (RX, TX)
  Serial2.begin(9600, SERIAL_8N1, 16, 17); // Modbus通信ポート設定
  node1.begin(1, Serial2); // Serialポートを介してスレーブ1とMosbus通信開始 (スレーブアドレス, 通信ポート)

  // 出力設定
  pinMode(ARARM_OUT, OUTPUT);      // 出力設定
  digitalWrite(ARARM_OUT, HIGH);   // 出力初期値

  readAlarmLevel(); // 電力アラームレベル読み出し関数
  readAddress();    // スレーブアドレス読み出し関数
}

// メイン **************************************************************************
void loop() {
  M5.update();

  // 測定データ取得表示
  if (settingModeFlag == false) {
    displayHeader();   // タイトル、単位、設定情報表示関数
    fetchModbusData(); // データ取得、エラーチェック関数
    if (err != true) {
      convertData();   // データ変換関数
      drawData();      // 測定データ表示関数
    } else {
      displayError();  // エラー表示関数
    }
  } else {
    displaySettingMode(); // 設定モード画面表示
  }
  
  // 設定モード(電力アラームレベル設定)
  if (M5.BtnA.wasPressed()) { // AボタンONなら
    settingModeFlag = !settingModeFlag;
    if (settingModeFlag == true) {
      readAlarmLevel();  // 電力アラームレベル読み出し関数
    } else {
      writeAlarmLevel(); // アラーム電力値書き込み関数
    }
  }

  // 積算電力値リセット
  if (M5.BtnC.pressedFor(3000) && settingModeFlag == false) { // Cボタン3秒以上ONで設定モードでなければ
    resetWatt(); // 積算電力値リセット関数
  }

  delay(500);
}

・全データ受信方法(入力レジスタ読込:0x04)

全データの受信方法は「基礎編の測定データ受信方法」と同じで、サンプルプログラムの「125行目」で以下のようにModbus通信で取得しています。

result = node1.readInputRegisters(0x0000, 0x0A); // Modbus通信実行(開始レジスタ, レジスタ数)

「基礎編」との違いは、取得するデータ(レジスタ)数が5個から、全データの10個になったことです。

取得したデータは「基礎編」と同様にデータ(16bit)ごとに配列に格納して、必要に応じて32bit変換して表示データに換算しています。
詳細は「基礎編の測定データ受信方法」を参照してください。

・スレーブアドレスの確認(保持レジスタ読込:0x03)

スレーブアドレスの確認はサンプルプログラムの「155〜159行目」で以下のように行います。

void readAddress() {
  result = node1.readHoldingRegisters(0x0002, 0x0001); // (アドレス, 読取レジスタ数n)
  slaveAddress = node1.getResponseBuffer(0); // 受信データバッファ取得
  Serial.println(slaveAddress);
}

上コードの「2行目」でスレーブアドレスが格納されている「保持レジスタ」の値を読み込んでいます。


保持レジスタ読込みコマンドの詳細は以下のようになります。

result = node1.readHoldingRegisters(開始アドレス, 読込みレジスタ数);
  • node1:スレーブアドレス「1」との通信を表します。
  • readHoldingRegisters:Modbus通信のファンクションコード「0x03」を表し保持レジスタ」の内容を読込むためのコマンドです。
    スレーブアドレスは「保持レジスタ」に格納されているため「開始アドレス」と「書込みレジスタ数」を指定することで、値を読み込むことができます。
  • 開始アドレス:読込み対象の「保持レジスタ」の最初のレジスタアドレスを指定します。
  • 読込みレジスタ数:読込みを行うレジスタ数を指定します。1つだけ読み込む場合は「1」です。
  • result:通信結果が格納されます。通信成功の場合は「0」で「0以外」は通信エラーです。

上コード「3行目」で以下のように受信したデータをバッファとして以下のように取得します。

slaveAddress = node1.getResponseBuffer(0); // 受信データバッファ取得
  • node1.getResponseBuffer(0):これでスレーブから送信されてきたデータを取得できます。
  • slaveAddress:取得したデータを格納する変数です。
    受信するデータのバイト数に合わせて準備しておく必要があります。
レジスタ構成で紹介した表の「スレーブアドレス格納レジスタ」によると「スレーブアドレス」のデータは「16bit」のため「16bit」以上の型で準備しておく必要があります。
今回は、「uint16_t」型としました。

・電力アラーム設定値の確認(保持レジスタ読込:0x03)

電力アラーム設定値の確認はサンプルプログラムの「162〜166行目」で以下のように行っています。

void readAlarmLevel() {
  result = node1.readHoldingRegisters(0x0001, 0x0001); // (アドレス, 読取レジスタ数n)
  alarmLevel = node1.getResponseBuffer(0); // 受信データバッファをget_data配列に格納
  Serial.println(alarmLevel);
}
「電力アラーム設定値」の確認方法は「スレーブアドレス」の確認方法と同じで、「レジスタアドレス」が異なる(0x0002→0x0001)だけです。

・スレーブアドレスの変更(保持レジスタ単独書込:0x06)

スレーブアドレスの変更は今回のサンプルプログラムでは行っていません。
変更方法は、この次の「電力アラーム設定値の変更」とレジスタアドレスが異なるだけで同じため、そちらを参考にしてください。

スレーブアドレスを変更すると、プログラム全体で指定しているスレーブアドレスも変更しないと、全ての通信が出来なくなるためご注意ください。

・電力アラーム設定値の変更(保持レジスタ単独書込:0x06)

電力アラーム設定値の変更はサンプルプログラムの「169〜172行目」で以下のように行っています。

void writeAlarmLevel() {
  result = node1.writeSingleRegister(0x0001, alarmLevel);  // (アドレス, 書き込む値)
  readAlarmLevel(); // 電力アラームレベル読み出し関数
}

上コードの「2行目」で電力アラーム設定値が格納されている「保持レジスタ」の値を書き込んで変更しています。


保持レジスタの書込みコマンドの詳細は以下のようになります。

result = node1.writeSingleRegister(レジスタアドレス, 書き込む値);
  • node1:スレーブアドレス「1」との通信を表します。
  • writeSingleRegisters:Modbus通信のファンクションコード「0x06」を表し保持レジスタ」の内容を書込むためのコマンドです。
    スレーブアドレスは「保持レジスタ」に格納されているため「レジスタアドレス」と「書き込む値」を指定することで、値を変更することができます。
  • レジスタアドレス:書込み対象の「保持レジスタ」のレジスタアドレスを指定します。
  • 書き込む値:書き込むデータを指定します。
  • result:通信結果が格納されます。通信成功の場合は「0」で「0以外」は通信エラーです。
「書き込む値」の「電力アラーム設定値」は16bitで指定します。
「電力アラーム設定値」の初期値は「23000(23000W)」で、最小単位は1Wです。

・積算電力値のリセット(特殊ファンクションコード:0x42)

積算電力値をリセットするためのファンクションコードは「0x42」で、特殊なものです。
このため、ライブラリでは指定できないため、シリアル通信で直接指定します。

サンプルプログラムでは「217〜232行目」で以下のように指定しています。

void resetWatt() {
  // ファンクションコード0x42は一般的なModbusプロトコル(ライブラリにも)に存在しないため直接送信
  Serial2.write(0x01); // スレーブアドレス
  Serial2.write(0x42); // ファンクションコード0x42(積算電力リセット用)
  Serial2.write(0x80); // CRC
  Serial2.write(0x11); // CRC

  delay(100); // 受信データ待ち時間(50ms以上)

  while (Serial2.available()) { // 受信データがなくなるまで繰り返し(送信データと同じものが受信される)
    uint16_t data = Serial2.read();  // データ受信
    Serial.printf("%0X", data);      // 受信データシリアル表示
    Serial.print(" ");               // スペース
  }
  Serial.print("\n"); // 改行
}

シリアル通信を行うにはmodbus通信で使用しているポートを指定して以下のように行います。

Serial2.write(送信データ)
  • Serial2:modbus通信で使用している通信ポートを指定します。
  • write:指定したデータをそのまま(16進数)送信する時に使用します。
  • 送信データ:送信データを16進数で指定します。

上コードの「3〜6行目」で上記のコマンドを使用して、以下のデータを順番に送信しています。

スレーブアドレス + ファンクションコード + CRC上位 + CRC下位
→「0x01」+「0x42」+「0x80」+「0x11」
設定変更が完了すると、送信したデータと同じものが受信できるため、シリアルモニタで確認することができます。必要に応じてエラー処理等追加して使用してください。

6.まとめ

「Modbus通信」で測定データを確認できる「交流電力測定器(PZEM-004T)」の使い方をサンプルプログラムを使用して詳しく紹介しました。

測定データの確認には液晶表示器と複数のシリアル通信端子を備えた「M5Stack BASIC」を使用することで、Modbus通信で測定可能な全てのデータの受信と表示から、電力アラーム等の設定値の変更も容易に行うことができます。

産業用途では、社内の電力(デマンド)管理のために多くのデバイスが販売されています。
しかし、一般家庭で使用できるものを探した時、データ表示ができるものはたくさんありますが、受信したデータを取得して利用できるものは意外に少なく、今回使用した「PZEM-004T」が一番自由度が高いように思います。

個人的には、もうすぐ自宅の太陽光発電の「FIT (固定価格買取制度) 」が終了するため、最近安価になった蓄電池やパワコンを使用して、太陽光発電を効率良く、売らずに使うシステムを作りたいなと企んでいて、そのために必要な電力監視デバイスを探していたところ、これに辿りつきました。

安価で入手できるのでこれを使って、目指せオフグリッドで色々試してみようかと模索中^^
今回使用して致命的だったのは、マイナスの電力が表示されないので買電売電の判断が単独でできないこと・・・これには複数使用して判断することで最適な充放電を制御する必要がありそう。

うまくいったら、ここでも紹介していきたいと思います。

M5Stack Modbus 485リレー4CHの使い方
M5StackでModbus(Arduinoライブラリ)を使用した4CHリレーの使い方を詳しく紹介。2本の配線だけで4つのリレーをPLCのように制御してみましょう。
M5Stack Modbus温度,湿度測定器XY-MD02の使い方
M5Stackを使ってArduinoコマンドで、Modbus通信を使用した温湿度表示器XY-MD02の使い方をModbusプロトコルと合わせて詳しく紹介します。

コメント

タイトルとURLをコピーしました