M5StackでModbus通信を使用した「交流電力測定器」の使い方を詳しく紹介します。
Modbus通信で取得したデータをM5Stack BASICの液晶画面に表示させて確認できます。
電力アラームの出力機能もあるため、デマンド管理を安価に実現することができるようになります。
次に「応用編」として、測定できる全てのデータと電力アラーム等の設定値表示、さらに各設定値の変更方法をサンプルプログラムで詳しく紹介していきます。
動作確認には「M5Stack Basic Ver2.6」を使用しました。
「M5Stack Basic」の使い方や開発環境の準備については以下のリンクで詳しく紹介しています。


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


1.Modbusとは
2.電力測定モジュールPZEM-004Tについて
・外観、付属品
・端子配列
・基本仕様
・レジスタ構成
3.動作確認の準備
・準備するもの
・配線図
・使用ライブラリ
4.基礎編:基本的な通信方法の確認(電圧、電流、電力のみ表示)
・動作紹介
・サンプルプログラム(基礎編)
・初期設定詳細
・測定データ受信方法(入力レジスタ読込:0x04)
・測定データ換算(16bit → 32bit)
・エラー処理について
5.応用編:全データ受信、内部設定確認/変更
・動作紹介
・サンプルプログラム(応用編)
・全データ受信方法(入力レジスタ読込:0x04)
・スレーブアドレスの確認(保持レジスタ読込:0x03)
・電力アラーム設定値の確認(保持レジスタ読込:0x03)
・スレーブアドレスの変更(保持レジスタ単独書込:0x06)
・電力アラーム設定値の変更(保持レジスタ単独書込:0x06)
・積算電力値のリセット(特殊ファンクションコード:0x42)
6.まとめ
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」の梱包状態や外観、付属品は下写真のようになります。

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

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

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

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

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

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

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

配線図を上写真のように画像上に書き込んでみました。
詳細な配線図はこの後で、より詳しく紹介しています。
・基本仕様
「測定項目」や「測定精度」等の基本仕様は以下表のようになります。

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

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

積算電力値リセット
積算電力値のリセットには特殊なファンクションコード「0x42」を使用します。
これには今回使用したライブラリでは指定できないので、直接シリアル通信でコマンドを送信します。
送信するコマンドでは「レジスタアドレス」は指定せず、スレーブアドレスとファンクションコード、CRCを送信するのみです。送信コマンドは以下になります。
→「0x01」+「0x42」+「0x80」+「0x11」
3.動作確認の準備
動作確認は以下のように、測定データ表示用の「液晶表示器」と「AC100Vコンセント配線」に「電流測定センサのカレントトランス」をクランプして行いました。

実際に使用した部品は以下になります。
・準備するもの
・M5Stack BASIC:受信した測定データ表示用
・AC100Vコンセントプラグ(オス/メス):電源供給と測定対象の接続用
・電線(1.25㎟ 白/黒):色は任意です。電線サイズは測定対象に合わせて準備してください。
今回は15Aまでを想定しています。
・ワンタッチコネクタ(WAGO製 WFR-3):配線接続用
・Groveコネクタ配線:電力測定器とM5Stackの通信配線接続用
各部品の詳細は以下になります。
M5Stack BASIC
「M5Stack Basic」とは「M5Stackテクノロジー社」のマイコンボードです。


2.0インチ(320*240)液晶表示器を搭載しており、入出力端子、3個のボタン、3軸ジャイロ+3軸加速度センサ、スピーカ搭載でmicroSDカードリーダーやバッテリーも内蔵されています。
他にも無線通信のWiFiやBluetoothはもちろん、シリアル通信(UART)にも対応しているため、今回は電力測定器から通信で取得したデータを液晶に表示させて確認するために使用しています。
AC100Vコンセントプラグ(オス/メス)
今回使用する電力測定器は交流用のため、以下のようなAC100V用のコンセントプラグ(オス/メス)を使用します。
電線(1.25㎟ 白/黒)
電線はAC100Vで使用するのでAC100V以上で使用可能な配線を準備しましょう。
電線サイズは最低でも0.75㎟で色は2種類あればなんでも良いです。
ワンタッチコネクタ(WAGO製 WFR-3)
AC100V配線の接続にはWAGO製の「ワンタッチコネクタ」が簡単で安全です。
圧着端子は工具が必要で、作業にもバラツキが出ますが、このコネクタなら被覆の剥き長さ(11mm)さえ間違えなければ確実に接続できて手軽で便利です。(ホームセンターでも売ってます。)

Groveコネクタ付き配線
通信線の接続にはGroveコネクタ付配線があれば差し込むだけで簡単に接続できます。
「電力測定器」側がJST製XHコネクタ4Pです。
「M5Stack」側はジャンパーコネクタの「メス」タイプの方が抜けにくいです。
・配線図
動作確認時の配線図は以下になります。
※配線色が変則的になります。実際に使用する配線の色と配線図をよく確認して配線してください。

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

・使用ライブラリ
使用するライブラリは以下表のようになります。
「ArduinoIDE」で事前にインストールしておいてください。
ライブラリ名 | 用途 | バージョン | 検索名 |
---|---|---|---|
M5Stack | M5Stack制御用 | 0.4.6 | M5Stack |
M5GFX | 液晶画面制御用 | 0.2.9 | M5G |
ModbusMaster | Modbus通信用 | 2.0.1 | ModbusMaster |
開発環境「ArduinoIDE」の使い方は、以下のリンクで詳しく紹介しています。

4.基礎編:基本的な通信方法の確認(電圧、電流、電力のみ表示)
・動作紹介
まずは基礎編として、下写真のように電圧と電流、電力のみの測定データを取得して表示させます。

・サンプルプログラム(基礎編)
サンプルプログラムは以下になります。コピペで貼り付けて書き込んで実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。
#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手順でデータを取得できます。
- 「3行目」でスレーブアドレスを指定して、データをリクエストするコマンドを送信すると対象のスレーブ機器からレスポンスとしてデータが送信されてきます。
- 「6行目」から送信されてきたデータを取得して変数に格納しています。
「3行目」の詳細は以下のようになります。
- node1:スレーブアドレス「1」との通信を表します。
- readInputRegisters:Modbus通信のファンクションコード「0x04」を表し「入力レジスタ」からデータを取得するためのコマンドです。
各測定データは「入力レジスタ」に格納されているため、このコマンドを使用します。
連続したレジスタのデータであれば、まとめて取得することができます。 - 開始アドレス:取得したいデータが格納されている先頭のアドレスを指定します。
「測定データ格納レジスタ」の表によると、「電圧」測定値はアドレス「0x0000」に、「電流値の下位」の測定値はアドレス「0x0001」「電流値の上位」の測定値はアドレス「0x0002」に格納されています。
このように連続したレジスタに格納されているデータは「開始アドレス」の「0x0000」を指定して次の「読み取りレジスタ数」を指定することで、連続して取得することができます。 - 読み取りレジスタ数:取得したいデータのレジスタ数を表します。
1つなら「1」ですが、今回は連続した5つのデータ(電圧、電流下位、電流上位、電力下位、電力上位)を取得したいため「5」を指定します。 - result:通信結果が格納されます。通信成功の場合は「0」で「0以外」は通信エラーです。
「6行目」の詳細は以下のようになります。
連続してデータを取得するために「for文」を使用しています。
get_data[i] = node1.getResponseBuffer(i); // 受信データバッファ取得
}
- node1.getResponseBuffer(i):これでスレーブから送信されてきたデータを取得できます。
連続したデータは配列として取得して使用します。 - get_data[i]:取得したデータを格納する配列です。
受信するデータのバイト数に合わせて準備しておく必要があります。
・測定データ換算(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.応用編:全データ受信、内部設定確認/変更
・動作紹介
次に応用編として、測定可能な全てのデータ(電圧、電流、電力、周波数、力率、積算電力)を取得して表示させます。

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

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

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

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


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


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

・サンプルプログラム(応用編)
サンプルプログラムは以下になります。コピペで貼り付けて書き込んで実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。
#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個になったことです。
・スレーブアドレスの確認(保持レジスタ読込:0x03)
スレーブアドレスの確認はサンプルプログラムの「155〜159行目」で以下のように行います。
void readAddress() {
result = node1.readHoldingRegisters(0x0002, 0x0001); // (アドレス, 読取レジスタ数n)
slaveAddress = node1.getResponseBuffer(0); // 受信データバッファ取得
Serial.println(slaveAddress);
}
上コードの「2行目」でスレーブアドレスが格納されている「保持レジスタ」の値を読み込んでいます。
保持レジスタ読込みコマンドの詳細は以下のようになります。
- node1:スレーブアドレス「1」との通信を表します。
- readHoldingRegisters:Modbus通信のファンクションコード「0x03」を表し「保持レジスタ」の内容を読込むためのコマンドです。
スレーブアドレスは「保持レジスタ」に格納されているため「開始アドレス」と「書込みレジスタ数」を指定することで、値を読み込むことができます。 - 開始アドレス:読込み対象の「保持レジスタ」の最初のレジスタアドレスを指定します。
- 読込みレジスタ数:読込みを行うレジスタ数を指定します。1つだけ読み込む場合は「1」です。
- result:通信結果が格納されます。通信成功の場合は「0」で「0以外」は通信エラーです。
上コード「3行目」で以下のように受信したデータをバッファとして以下のように取得します。
- node1.getResponseBuffer(0):これでスレーブから送信されてきたデータを取得できます。
- slaveAddress:取得したデータを格納する変数です。
受信するデータのバイト数に合わせて準備しておく必要があります。
・電力アラーム設定値の確認(保持レジスタ読込:0x03)
電力アラーム設定値の確認はサンプルプログラムの「162〜166行目」で以下のように行っています。
void readAlarmLevel() {
result = node1.readHoldingRegisters(0x0001, 0x0001); // (アドレス, 読取レジスタ数n)
alarmLevel = node1.getResponseBuffer(0); // 受信データバッファをget_data配列に格納
Serial.println(alarmLevel);
}
・スレーブアドレスの変更(保持レジスタ単独書込:0x06)
スレーブアドレスの変更は今回のサンプルプログラムでは行っていません。
変更方法は、この次の「電力アラーム設定値の変更」とレジスタアドレスが異なるだけで同じため、そちらを参考にしてください。
・電力アラーム設定値の変更(保持レジスタ単独書込:0x06)
電力アラーム設定値の変更はサンプルプログラムの「169〜172行目」で以下のように行っています。
void writeAlarmLevel() {
result = node1.writeSingleRegister(0x0001, alarmLevel); // (アドレス, 書き込む値)
readAlarmLevel(); // 電力アラームレベル読み出し関数
}
上コードの「2行目」で電力アラーム設定値が格納されている「保持レジスタ」の値を書き込んで変更しています。
保持レジスタの書込みコマンドの詳細は以下のようになります。
- node1:スレーブアドレス「1」との通信を表します。
- writeSingleRegisters:Modbus通信のファンクションコード「0x06」を表し「保持レジスタ」の内容を書込むためのコマンドです。
スレーブアドレスは「保持レジスタ」に格納されているため「レジスタアドレス」と「書き込む値」を指定することで、値を変更することができます。 - レジスタアドレス:書込み対象の「保持レジスタ」のレジスタアドレスを指定します。
- 書き込む値:書き込むデータを指定します。
- result:通信結果が格納されます。通信成功の場合は「0」で「0以外」は通信エラーです。
・積算電力値のリセット(特殊ファンクションコード: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:modbus通信で使用している通信ポートを指定します。
- write:指定したデータをそのまま(16進数)送信する時に使用します。
- 送信データ:送信データを16進数で指定します。
上コードの「3〜6行目」で上記のコマンドを使用して、以下のデータを順番に送信しています。
→「0x01」+「0x42」+「0x80」+「0x11」
6.まとめ
「Modbus通信」で測定データを確認できる「交流電力測定器(PZEM-004T)」の使い方をサンプルプログラムを使用して詳しく紹介しました。
測定データの確認には液晶表示器と複数のシリアル通信端子を備えた「M5Stack BASIC」を使用することで、Modbus通信で測定可能な全てのデータの受信と表示から、電力アラーム等の設定値の変更も容易に行うことができます。
産業用途では、社内の電力(デマンド)管理のために多くのデバイスが販売されています。
しかし、一般家庭で使用できるものを探した時、データ表示ができるものはたくさんありますが、受信したデータを取得して利用できるものは意外に少なく、今回使用した「PZEM-004T」が一番自由度が高いように思います。
個人的には、もうすぐ自宅の太陽光発電の「FIT (固定価格買取制度) 」が終了するため、最近安価になった蓄電池やパワコンを使用して、太陽光発電を効率良く、売らずに使うシステムを作りたいなと企んでいて、そのために必要な電力監視デバイスを探していたところ、これに辿りつきました。
安価で入手できるのでこれを使って、目指せオフグリッドで色々試してみようかと模索中^^
今回使用して致命的だったのは、マイナスの電力が表示されないので買電売電の判断が単独でできないこと・・・これには複数使用して判断することで最適な充放電を制御する必要がありそう。
うまくいったら、ここでも紹介していきたいと思います。


コメント