前報、前々報からの続きで、電力計として仕上げます。電流センサは分電盤内の単相三線式100V配線のL1相、L2相を測ります。電圧センサのトランスはACアダプタなので100Vコンセントに差し込みます。
表示はBlynkを使います。将来は常時表示を思っています。データはAmbientへ送り蓄積します。
構成
ハード
- マイコン:ESP32 Devkit V1
- CTセンサ:YHDC SCT-013-050 ; 2個 割れたコアは瞬間接着剤、セロテープ、ホットボンドで固めました。
- 電圧センサ:ACアダプター改造;Sony DC adapter output 4,5V 500mA
- AC/DCアダプター:5V 1A出力;iphone用
- 回路図;周波数検出の信号は20kΩから、4.7kΩに変更しました。電流高負荷時に矩形波整形がうまくいかない問題がでて、その対策です。
手前の32,33と書いたのがCTセンサ入力のステレオピンジャックで、その左のDCジャックが電圧の入力です。
ソフト(スケッチ)
- 電力計:なんでも作っちゃう、かも。Arduino電力計 - 回路図とスケッチ
- AC電源周波数:ラジオペンチ ESP32を使って電源周波数の変動をAmbientに保存、可視化
- wifi切れ対策:ネットモール土佐 esp-wroom-02(ESP8266) の Wi-Fi Lanがよく切れる件です。
- 表示:Blynkで2秒おきにスマホに表示
- 記録:Ambientに30秒ごとに取得データを送信
電流、電圧は1サイクルを100分割して、1サイクルあたりの2乗平均を求めています。Blynkへは2秒ごとに平均値を渡し、Ambientへは送信間隔30秒間の最大値を渡しています。
AC電源周波数は、attachInterrupt()で波形がHighからLowへ切り換わるタイミングで割り込みを入れて指定個数(50か60サイクル)の波形をカウントし、それに要する時間をmicros()関数で求め、電源周波数に同期する形で周波数に変換しています。Blynkへは2秒ごとに平均値を渡し、Ambientへは送信間隔30秒間の最大値と最小値を渡しています。
各々の測定に値は合わせ込みのため、実験的に求めた補正係数をかけています。周波数は基準はオシロの読み値にしました。補正係数を掛けないと2%程度高めになります。途中の値やサイクル数などを細かくチェックしたのですが、まちがいはなく原因はわかりませんでした。micros()関数の精度なのでしょうか?
しばらく使っていたのですが、27日くらいでwifiが切れて繋がらない現象が起きました。正確に経過時間(日数)を測定した訳ではないのですが、ESP32の起動後27日くらいで何度かwifiが繋がらなくなりました。その対策として、wifiが繋がらないとESP32をリセットするようにスケッチを変更しました。(上記のスケッチをそのまま入れ込みました。)
つなげて作ったスクリプトをしまします。約500行あるので非表示にしています。クリックすると見えます。
/*
"なんでも作っちゃう、かも。Arduino電力計 - 回路図とスケッチ"から引用
http://arms22.blog91.fc2.com/
簡単に処理の流れを解説します。まずsetupでいくつかの変数を初期化。
loopで1秒間に数十回calcWattをコール、1秒間の電力の平均を求め、シリアルで出力します。
電圧・電流のサンプリング、電力の計算はcalcWattで行います。
calcWattは交流電源の1サイクル分(関西だと60Hz、関東だと50Hz)の電圧・電流をサンプリングし、
サンプリングした値から1サイクル分の電圧・電流、電力を計算しています。
計算した値はVrms変数、Arms変数、Watt変数に格納します。
電圧はanalogReadで読み取った値をVに変換し、実験で求めた係数(87.5734677)を掛けて求めます。
電流は次の式で求められます。Io = Eo / (K * RL / n)
ESP32で商用電源周波数をAmbientに書き込み File:20210303AcFreqLogToAmbient.ino
2021/03/03 ラジオペンチ http://radiopench.blog96.fc2.com/
(セマフォやミューテックスを使った排他制御を行っていないので問題が出るかも?)
*/
// <<完成型>>
// 電圧1ch, 電流 2ch
// Blynk 2秒間隔で送信
// Ambient 30秒間の最大値を30秒間隔で送信
//for Blynk
/* Comment this out to disable prints and save space */
#define BLYNK_PRINT Serial
#include <WiFi.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>
// You should get Auth Token in the Blynk App.
// Go to the Project Settings (nut icon).
char auth[] = "**********BLYNK auth************_";
// Your WiFi credentials.
// Set password to "" for open networks.
char ssid[] = "***your id***";
char password[] = "***your pass***";
char server[] = "192.168.**.**"; // IP for your Local Server
int port = 8080;
BlynkTimer timer;// Create a Timer object called "timer"
// for ambient
#include "Ambient.h"
unsigned int channelId = *****;
const char* writeKey = "****writeKey****";
WiFiClient client;
Ambient ambient;
// 端子定義
// ESP32のピン番号は、基板に書かれているIOxxのxx 他のArduino互換機と違うので注意!!
const int arefPin = 35; // 仮想GND
const int voltagePin = 34; // AC Voltage
const int freqPin = 36; // AC Freqency
const int currentPin_A = 32; // CT_A
const int currentPin_B = 33; // CT_B
// 商用電源周波数
#define AC_FREQ (60)
// 1サイクルあたりのサンプル数
#define NUMBER_OF_SAMPLES (100)
// サンプリング間隔(マイクロ秒)
#define SAMPLING_INTERVAL (1000000/(AC_FREQ * NUMBER_OF_SAMPLES))
//シリアルとBlynkに出力する間隔(秒)
#define OUTPUT_INTERVAL (2)
//Ambientに出力する間隔(秒)
#define AMBIENT_INTERVAL (30)
// デバッグ用
#define DEBUG 0
// 実効電圧、実効電流、有効電力
float Vrms;
float Irms_A;
float Irms_B;
float Watt;
// サンプリング用バッファ数
int VASamples[NUMBER_OF_SAMPLES*6];
#define KV (1.414*102.2/1.032/2.2447/1000*0.971) // 電圧換算係数数*実験的補正係数
#define KC_A (1.414*2.2447*13.823 / 0.986) // 電流換算係数/実験的補正係数
#define KC_B (1.414*2.2447*13.823 /1.14) // 電流換算係数/実験的補正係数
#define Def_A 0.285 // 実測にもとづくY軸切片 (A)
#define Def_B 0.303 // 実測にもとづくY軸切片 (A)
#define K_Freq 0.977 // 周波数補正係数 (デフォルトは1.0)
unsigned long t1,t2;
int i,r,v1,a1,a2,b1,b2,v2;
float watt_hour;
float vrms_sum;
float Irms_A_sum;
float Irms_B_sum;
float app_Power;
float watt_sum;
float Power_factor;
int num_samples;
unsigned long last_update;
boolean Bflag; // Ambient送信後、最大、最小、現在のAC周波数が揃ってしまうので一回目読み飛ばす
int logCount = 0; // 1秒間に50(60)サイクルをひとかたまりではlogCount = 1
volatile uint32_t t_freq; // 50(60)サイクル分の周期(us単位、標準1000000us)
volatile boolean flag;
float fCul; // 周波数の1秒平均値
float fMax = 0.0; // 最大周波数
float fMin = 1000.0; // 最小周波数
float fAve; // 平均周波数
float fSum = 0.0;
//ambientに送る30秒区間での最大値
float vrms_max;
float Irms_A_max;
float Irms_B_max;
float Irms_AB_max;
float app_Power_max;
float watt_max;
float fSum_max = 0.0;
float fSum_min = 1000.0;
void IRAM_ATTR acIrq() { // ピン割込み:電源ピンがHIGHからLOWになる(1サイクル)毎に割込み
static uint16_t n;
static uint32_t lastMicros = 0;
uint32_t x;
n++; // サイクルカウンタの値を1増やす
if (n >= AC_FREQ) { // 指定回数(50 or 60)なら
x = micros(); // 現在の時刻を記録
t_freq = x - lastMicros; // 経過時間を計算(この値はほぼ100000になる);指定回数(50or60cycles)にかかった時間
lastMicros = x;
n = 0;
flag = true; // 指定回数(1秒)毎にvoid mesureAC_FREQ()に吐き出すためにフラグをセット
}
}
void mesureAC_FREQ(void) {
if (flag) { // flag = trueで指定回数(50 or 60サイクル)経過でデーターが準備できているならば
fCul = K_Freq * 1.0E6 * AC_FREQ / t_freq; // 1秒間の平均周波数を計算
logCount++; // サイクルカウンタの値を1増やす
if (fCul < fMin) fMin = fCul;
if (fCul > fMax) fMax = fCul;
fSum += fCul;
}
flag = false;
}
void calcWatt(void) //NUMBER_OF_SAMPLES(100)個のデータの2乗平均(rms)
{
t1 = micros();
// 1サイクル分のAD値をサンプリング
for(i=0; i<NUMBER_OF_SAMPLES; i++){
r = analogRead(arefPin);
v1 = analogRead(voltagePin);
a1 = analogRead(currentPin_A);
a2 = analogRead(currentPin_A);
b1 = analogRead(currentPin_B);
b2 = analogRead(currentPin_B);
v2 = analogRead(voltagePin);
VASamples[(i*6)+0] = v1 - r;
VASamples[(i*6)+1] = a1 - r;
VASamples[(i*6)+2] = a2 - r;
VASamples[(i*6)+3] = v2 - r;
VASamples[(i*6)+4] = b1 - r;
VASamples[(i*6)+5] = b2 - r;
do {
t2 = micros();
}
while((t2 - t1) < SAMPLING_INTERVAL);//サンプリング間隔(167マイクロ秒)の間は待つ
t1 += SAMPLING_INTERVAL;
}
// 1サイクル分の電圧と電流、電力を加算
Vrms = 0;
Irms_A = 0;
Irms_B = 0;
Watt = 0;
for(i=0; i<NUMBER_OF_SAMPLES; i++){
v1 = VASamples[(i*6)+0];
a1 = VASamples[(i*6)+1];
a2 = VASamples[(i*6)+2];
v2 = VASamples[(i*6)+3];
b1 = VASamples[(i*6)+4];
b2 = VASamples[(i*6)+5];
float vv = (v1+v2)/2 * KV;
float aa = (a1+a2)/2 / KC_A;
float bb = (b1+b2)/2 / KC_B;
Vrms += vv * vv; // Vrms = Vrms + vv * vv;
Irms_A += aa * aa;
Irms_B += bb * bb;
Watt += vv * ( aa + bb );
}
// 2乗平均平方根(rms)を求める
Vrms = sqrt(Vrms / NUMBER_OF_SAMPLES);
Irms_A = sqrt(Irms_A / NUMBER_OF_SAMPLES) ;if (Irms_A < Def_A) Irms_A = 0;
Irms_B = sqrt(Irms_B / NUMBER_OF_SAMPLES) ;if (Irms_B < Def_B) Irms_B = 0;
// 平均電力を求める
Watt = Watt / NUMBER_OF_SAMPLES;
}
// for Blynk
// This function sends Arduino's up time every second to Virtual Pin().
// In the app, Widget's reading frequency should be set to PUSH. This means
// that you define how often to send data to Blynk App.
void myTimerEvent()
{
unsigned long curr_time;
// You can send any value at any time.
// Please don't send more that 10 values per second.
// 1秒あたり10を超える値を送信しないでください
// timer.setIntervalで設定した秒数(OUTPUT_INTERVAL)経過したらシリアルとBlynkに出力
//設定秒数(OUTPUT_INTERVAL)の間に積算したデータをサンプル数で割り平均を出す
vrms_sum /= num_samples; // vrms_sum = vrms_sum/num_samples
Irms_A_sum /= num_samples;
Irms_B_sum /= num_samples;
watt_sum /= num_samples;
fSum /= logCount;
app_Power = vrms_sum * ( Irms_A_sum + Irms_B_sum );//皮相電力 VA
Power_factor = watt_sum / app_Power *100;// 力率 % = 有効電力 / 皮相電力
#if DEBUG
for(int i=0; i<NUMBER_OF_SAMPLES; i++){
Serial.print(num_samples);
Serial.print('\t');
Serial.print(VASamples[(i*6)+0]); //v1
Serial.print('\t');
Serial.print(VASamples[(i*6)+1]); //a1
Serial.print('\t');
Serial.println(VASamples[(i*6)+4]); //b1
Serial.print(num_samples);
Serial.print('\t');
Serial.print(VASamples[(i*6)+3]); //v2
Serial.print('\t');
Serial.print(VASamples[(i*6)+2]); //a2
Serial.print('\t');
Serial.println(VASamples[(i*6)+5]); //b2
}
#endif
Blynk.virtualWrite(V0, vrms_sum);//電圧 V
Blynk.virtualWrite(V1, Irms_A_sum + Irms_B_sum);//CT_A + CT_B電流 A
Blynk.virtualWrite(V2, Irms_A_sum);//CT_A電流 A
Blynk.virtualWrite(V3, Irms_B_sum);//CT_B電流 A
Blynk.virtualWrite(V4, app_Power);//皮相電力 VA
Blynk.virtualWrite(V5, watt_sum);//電力 W
Blynk.virtualWrite(V6, Power_factor);// 力率 % = 有効電力 / 皮相電力
Blynk.virtualWrite(V7, watt_hour);// 積算 Wh
Blynk.virtualWrite(V8, fSum);// AC周波数 Hz
//ambientに送る30秒区間での最大値を記録する
if(vrms_max < vrms_sum){vrms_max = vrms_sum;}
if(Irms_A_max < Irms_A_sum){Irms_A_max = Irms_A_sum;}
if(Irms_B_max < Irms_B_sum){Irms_B_max = Irms_B_sum;}
if(Irms_AB_max < Irms_A_sum + Irms_B_sum){Irms_AB_max = Irms_A_sum + Irms_B_sum;}
if(app_Power_max < app_Power){app_Power_max = app_Power;}
if(watt_max < watt_sum){watt_max = watt_sum;}
if(fSum_max < fSum){fSum_max = fSum;}
if(fSum_min > fSum){fSum_min = fSum;}
if (Bflag) {
Serial.print(vrms_sum);
Serial.print("V, ");
Serial.print(Irms_A_sum + Irms_B_sum);
Serial.print("A, ");
Serial.print(Irms_A_sum);
Serial.print("A, ");
Serial.print(Irms_B_sum);
Serial.print("A, ");
Serial.print(app_Power);
Serial.print("VA, ");
Serial.print(watt_sum);
Serial.print("W, ");
// AC周波数
Serial.print(fSum);
Serial.print("Hz, ");
// min AC周波数 30sec間
Serial.print(fSum_min);
Serial.print("Hz, ");
// max AC周波数 30sec間
Serial.print(fSum_max);
Serial.print("Hz ");
// 力率 = 有効電力 / 皮相電力
Serial.print(Power_factor);
Serial.print("%, ");
// 積算Whを求める
watt_hour += watt_sum / 3600.0;
Serial.print(watt_hour);
Serial.println("Wh, ");
}
Bflag = true;
// AMBIENT_INTERVAL(30)秒経過したらAmbientに出力
curr_time = millis();
if( (curr_time - last_update) > AMBIENT_INTERVAL *1000 ){
//#if DEBUG
Serial.print(curr_time/1000);Serial.print(" usec");
Serial.print('\t');
Serial.print(last_update/1000);Serial.print(" usec");
Serial.println('\t');
Serial.println("Now, sending the Maximum value during the period to Ambient .....");
Serial.print(vrms_max);
Serial.print("V, ");
Serial.print(Irms_AB_max);
Serial.print("A, ");
Serial.print(Irms_A_max);
Serial.print("A, ");
Serial.print(Irms_B_max);
Serial.print("A, ");
Serial.print(app_Power_max);
Serial.print("VA, ");
Serial.print(watt_max);
Serial.print("W, ");
// AC周波数
Serial.print(fSum);
Serial.print("Hz, ");
// min AC周波数 30sec間
Serial.print(fSum_min);
Serial.print("Hz, ");
// max AC周波数 30sec間
Serial.print(fSum_max);
Serial.println("Hz");
//#endif
ambient.set(1, vrms_max);//電圧 V
ambient.set(2, Irms_AB_max);//CT_A + CT_B電流 A
ambient.set(3, Irms_A_max);//CT_A電流 A
ambient.set(4, Irms_B_max);//CT_B電流 A
ambient.set(5, app_Power);//皮相電力 VA
ambient.set(6, watt_max);//電力 W
ambient.set(7, fSum_max);// 区間最大周波数
ambient.set(8, fSum_min);// 区間最小周波数
ambient.send();
vrms_max = Irms_A_max = Irms_B_max = Irms_AB_max = watt_max = app_Power_max = 0;
fSum_max = 0.0;
fSum_min = 1000.0;
last_update = curr_time;
Bflag = false;
}
num_samples = 0;
vrms_sum = Irms_A_sum = Irms_B_sum = watt_sum = 0;
fMin = 1000.0; // 次回の測定用に変数を初期化
fMax = 0.0;
fSum = 0.0;
logCount = 0;
}
void setup()
{
Serial.begin(115200);
analogSetAttenuation(ADC_6db); // アッテネーターを設定
pinMode(arefPin, INPUT);
pinMode(freqPin, INPUT);
pinMode(voltagePin, INPUT);
pinMode(currentPin_A, INPUT);
pinMode(currentPin_B, INPUT);
watt_hour = 0;
vrms_sum = vrms_max = 0;
Irms_A_sum = Irms_A_max = 0;
Irms_B_sum = Irms_B_max = 0;
Irms_AB_max = 0;
watt_sum = watt_max = 0;
app_Power = app_Power_max =0;
num_samples = 0;
last_update = millis();
attachInterrupt(freqPin, acIrq, FALLING); // ピンがHIGHからLOWになった時に割り込みacIrq関数の割り込み発生
Blynk.begin(auth, ssid, password, server, port); // Blocking until server connected
// You can also specify server:
//Blynk.begin(auth, ssid, password, "blynk-cloud.com", 80);
//Blynk.begin(auth, ssid, password, IPAddress(192,168,1,100), 8080);
// Setup a function to be called every interval second
timer.setInterval(OUTPUT_INTERVAL * 1000L, myTimerEvent);
// for Ambient
ambient.begin(channelId, writeKey, &client);
}
/*
* void wifi_lan_check()
* WiFi Lan の監視をします。
* WiFi.status() で判断するよりも、ping で応答をチェックした方が、早いかも。
*/
void wifi_lan_check(){
int i;
if(WiFi.status() !=3){ // 3はWL_CONNECTED
//Serial.print("wifi_lan_check() :#1 st=");
//Serial.println(WiFi.status());
}
if(WiFi.status() != WL_CONNECTED){
//Serial.println("WiFi disconnected");
WiFi.disconnect();
delay(50);
WiFi.begin(ssid, password);
i=50;
// 結局は、回復しないで、wdt reset になるので、直ぐ自分で restart させた方が良いかも。
while (WiFi.status() != WL_CONNECTED) {
delay(250);
//Serial.print(".");
i--;
if(i<0){
//Serial.println("wifi_lan_check() :#2 ESP.restart()");
delay(250);
// 自分で restart しないで、放っておくと、 wdt reset になるので、
// 敢えて ESP.restart() しなくてもよいのだが、気が短い人は、どうぞ。
ESP.restart();
}
}
}
}
void loop()
{
/*
// check for incomming client connections frequently in the main loop:
server.handleClient(); // この処理でクライアントからのアクセスに対応 server.handleClient();
*/
// WiFi Lan の監視をします。接続していないとリセット
wifi_lan_check();
// 電力を計算(100個のデータのrms)
calcWatt();
// AC周波数測定
mesureAC_FREQ();
// 指定秒数(OUTPUT_INTERVAL)分加算する
vrms_sum += Vrms; // vrms_sum = vrms_sum + Vrms
Irms_A_sum += Irms_A;
Irms_B_sum += Irms_B;
watt_sum += Watt;
num_samples++; // num_samples = num_samples + 1
// 加算後、Blynk Object の myTimerEvent() へデータを渡す
// そこで、指定秒数間の平均を計算する
Blynk.run(); // Blynk起動
timer.run(); // BlynkTimer object 開始
}
測定結果
Blynk live表示
ドライヤを負荷と問たときの送風、cool、hotでの結果です。電流はL1相(白色)、L2相(青色)、L1+L2(オレンジ色)です。同じ電流を測っていますが、わずかの違いは出ています。電流は時系列でも見えるようにしました。
上から、電流(L1, L2, L1+L2)、電圧と最大/最小周波数、電力の約4時間の時系列波形です。