最近の寒波で電力供給の危機が叫ばれています。原子力発電所が震災以来(ほぼ?)停止していて、CO2の対策として石炭火力や石油での火力もだいぶ止まっているような話を聞きます。そこへ来て、大雪によってソーラーパネルに雪が積もって発電できないのですから当たり前といえば当たり前です。
電力供給が厳しくなると発電所から見た負荷が重くなるので、電源周波数の変動として見えてくるはずです。そこで、電源周波数の変動を見てみよう、ということになります。
電源周波数の取り出し
ちゃんと見ようと思うと、どうしても商用電源へのアクセスが必要で感電防止とかを考えると大がかりになりがちですが、今回は100円ショップで売っているニッケル水素電池の充電器を使うことにしました。100円ショップで入手したものは、以前分解したときに(電池を入れない状態では)電池のマイナス端子に内部のトランスの端子がそのまま出ていることに気づきました。そこで、ここから商用電源の信号を取ることにします。(注意: 感電の恐れが大きい部分に触れることなく取り出せますが、この充電器の本来の使い方とは異なりますので、まねする場合には十分注意して行うとともに、自己責任で行ってください。似た形状で回路構成が違うものもあるかもしれませんし。)
このようにオシロスコープのプローブを2つの電池のマイナス端子に接続して波形観測すると、Peak-to-Peakで12Vくらいあることがわかります。ここにフォトカプラを入れて信号を取り出すことを考えます。
手持ちのフォトカプラPC817のデータシートを見ると、以下の特性を持っています。
フォトカプラの内蔵LEDにはIFは5mAも流してやれば良さそうです。VF=1.2V程度なので、ピーク時の6Vの際に10mA程度をターゲットに470Ωをつけてやることにします。抵抗には10mA程度流れるときに5Vの電圧がかかるので、50mWの消費電力・・・と思ったけど直流ではないので平均はもっと少ないですが、1/6Wのものを使うことにしました。あとはフォトカプラの内蔵LEDと逆向きに適当なLEDをつけておきます。これは何でもよく、AC電圧が逆相の際にフォトカプラの内蔵LEDに逆電圧がかかるのを防止するためです。
フォトカプラのフォトトランジスタ側の負荷抵抗は50Hzや60Hzでは周波数特性もへったくれもないので、IC=0.5mAも流れていれば十分ですから4.7kΩとしました。(よく考えたら、この抵抗は無しでIO8の内蔵プルアップを有効にするだけで十分かもしれない)
ブレッドボード上に組んで波形を観測してみました。
右側のボードは秋月のAE-ATmega328ボードがボードのみで売られていた時代に組んだものです。ATmega328を載せてブートローダを書き込んだのだと思います。クリスタルは16MHzのものが載せてあって、Arduino Pro互換として動作していると思います。フォトカプラの出力はIO8へ接続しています。ATmega328のTCC1のカウント値をキャプチャする機能の入力信号ICP1として使用するためです。

上が充電器の2つの端子の電圧で、下がArduino互換ボードへの信号です。狙ったとおりの信号が得られています。
周期の計測
これをATmega328で周波数を計測します。TCC1の16ビットタイマをCPUのクロックを8分周したもの(つまり2MHz=0.5uS)でフリーランさせて、インプットキャプチャ機能でICP1信号(IO8)の下がりエッジでその瞬間の値をICR1レジスタに取得します。取得したら割り込みでただちにICR1レジスタの値を読み出して保存します。実際には1秒周期で平均周波数を求めてホストに送信するので、割り込み内ではlongの変数に積算するとともに、積算回数をカウントします。また、よりデータを取る場合にはその1秒間での最短周期や最長周期も取得しておきます。
メインのループ側では1秒毎に積算値から平均の周期もしくは周波数を求めてシリアルでホストに送信します。それをArduino IDEのシリアルプロッタの機能でグラフ化するとこんな感じになります。

縦軸は周波数で、単位は1/1000Hzです。50000が50Hzぴったりということになります。このグラフの例では縦軸は49.92Hzから50.16Hzまでが表示されていて、電源の周波数は49.94Hzくらいから50.1Hzくらいまで変動しているということがわかります。右側はサンプル数ですが、データ送信は1秒間に1回なので、過去500秒間の電源周波数変動が見えます。
これをブレッドボードではなくて、ユニバーサル基板で組んでみました。
Arduino用のユニバーサル基板もあったのですが、回路規模が小さいのでより小さなユニバーサル基板で組んでみました。この範囲には5Vがないので、IO12をH出力してフォトカプラの内蔵フォトトランジスタの負荷抵抗の電源端子として使用することにしました。(注:実際には、IO8の内蔵プルアップを有効にするだけでも十分そうです。頑張れば更に小さいユニバーサル基板でも入ったかも。)
さらにソフトを修正して、1秒間の間の最短周期と最大周期も送信するようにしたところ、以下のような感じになりました。
理由はきちんと確認していませんが、1秒間の中での周波数変動が大きい時間と小さい時間があるようです。おそらくはインバータ機器などが動作している/していない期間なのでしょう。
ソフトウェア
平均のみをホストに送信する場合
#define TCCCLK 2000000 // CPUクロックを8分周した値。 // Pro Miniなど8MHzで動作している製品の場合は 1000000 にする #define ICP1 8 // ICP1の端子を定義 #define PWR 12 // フォトカプラの負荷抵抗に電源を供給するための端子 // 周期測定ののための変数 volatile uint16_t NumV; volatile uint32_t SumV; volatile uint16_t CapV; volatile uint16_t PreCap,NowCap; // 1秒間の最大最小平均を格納するための変数 uint16_t tNumV; float tAveV; uint32_t tSumV; unsigned long current_time; // 割り込みハンドラ ISR(TIMER1_CAPT_vect) { TIFR1 |= (1<<ICF1); NowCap = ICR1; CapV = NowCap - PreCap; // 差分を求めて周期を算出 PreCap = NowCap; SumV += CapV; NumV++; } void setup() { Serial.begin(38400); pinMode(LED_BUILTIN, OUTPUT); // TCC1でICP1信号(PB0端子=IO8)の周期を計測する TCCR1A = 0; //initialize Timer1 TCCR1B = 0; TCNT1 = 0; // 4.7kΩなしで端子の内蔵プルアップで済ます場合 // pinMode(ICP1, INPUT_PULLUP); //ICP1の端子をプルアップなしの入力に設定する // pinMode(PWR, INPUT); // 4.7kΩのプルアップありでこれをIO12から供給する場合 pinMode(ICP1, INPUT); //ICP1の端子をプルアップなしの入力に設定する pinMode(PWR, OUTPUT); digitalWrite(PWR, HIGH); TCCR1B = 0x02; // 16MHzを8分周 TIMSK1 |= (1 << ICIE1); // キャプチャ割り込み許可 current_time = millis(); } #define FREQ // 計測を周波数で行う場合 void loop() { static uint8_t skp = 3; // 最初の何回かはデータを捨てる if (millis() - current_time >= 1000) { // 割り込みで更新している変数をコピー&初期化する noInterrupts(); //割り込み停止 tSumV = SumV; tNumV = NumV; SumV = 0; NumV = 0; interrupts(); //割り込み再開 tAveV = (float)tSumV / (float)tNumV; digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); current_time = millis(); // PCに送信 if(skp == 0){ #ifdef FREQ Serial.println((double)TCCCLK/(double)tAveV*1000.); #else Serial.println(tAveV/2); // これで単位がusになる #endif } else { skp--; } } }
1秒間の中の最大/最小も送信する場合
#define TCCCLK 2000000 // CPUクロックを8分周した値。 // Pro Miniなど8MHzで動作している製品の場合は 1000000 にする #define ICP1 8 // ICP1の端子を定義 #define PWR 12 // フォトカプラの負荷抵抗に電源を供給するための端子 // 周期測定ののための変数 volatile uint16_t MaxV,MinV,NumV; volatile uint32_t SumV; volatile uint16_t CapV; volatile uint16_t PreCap,NowCap; // 1秒間の最大最小平均を格納するための変数 uint16_t tMaxV,tMinV,tNumV; float tAveV; uint32_t tSumV; unsigned long current_time; // 割り込みハンドラ ISR(TIMER1_CAPT_vect) { TIFR1 |= (1<<ICF1); NowCap = ICR1; CapV = NowCap - PreCap; // 差分を求めて周期を算出 PreCap = NowCap; if(CapV > MaxV) MaxV = CapV; if(CapV < MinV) MinV = CapV; SumV += CapV; NumV++; } void setup() { Serial.begin(38400); pinMode(LED_BUILTIN, OUTPUT); // TCC1でICP1信号(PB0端子=IO8)の周期を計測する TCCR1A = 0; //initialize Timer1 TCCR1B = 0; TCNT1 = 0; // 4.7kΩなしで端子の内蔵プルアップで済ます場合 // pinMode(ICP1, INPUT_PULLUP); //ICP1の端子をプルアップなしの入力に設定する // pinMode(PWR, INPUT); // 4.7kΩのプルアップありでこれをIO12から供給する場合 pinMode(ICP1, INPUT); //ICP1の端子をプルアップなしの入力に設定する pinMode(PWR, OUTPUT); digitalWrite(PWR, HIGH); TCCR1B = 0x02; // 16MHzを8分周 TIMSK1 |= (1 << ICIE1); // キャプチャ割り込み許可 current_time = millis(); } #define FREQ // 計測を周波数で行う場合 void loop() { static uint8_t skp = 3; // 最初の何回かはデータを捨てる if (millis() - current_time >= 1000) { // 割り込みで更新している変数をコピー&初期化する noInterrupts(); //割り込み停止 tMaxV = MaxV; tMinV = MinV; tSumV = SumV; tNumV = NumV; MaxV = 0; MinV = 65535; SumV = 0; NumV = 0; interrupts(); //割り込み再開 tAveV = (float)tSumV / (float)tNumV; digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); current_time = millis(); // PCに送信 if(skp == 0){ #ifdef FREQ Serial.print((double)TCCCLK/(double)tMaxV*1000.); Serial.print(","); Serial.print((double)TCCCLK/(double)tMinV*1000.); Serial.print(","); Serial.println((double)TCCCLK/(double)tAveV*1000.); #else Serial.print(tMaxV/2); Serial.print(","); Serial.print(tMinV/2); Serial.print(","); Serial.println(tAveV/2); // これで単位がusになる #endif } else { skp--; } } }
おまけ
実は、AC電源から信号を取ってこなくてもAC電源の周波数変動は大雑把には計測できます。(バッテリで動作しているノートPCの場合は無理かも)
ICP1端子に適当なケーブルをつないで、ICP1端子をプルアップなしの入力端子にしてやると、周りとの静電誘導にて電源周波数のノイズが回り込みます。これで一応電源周波数のモニターができます。
ただし、外部からのノイズに非常に弱いと思われますし、静電気などで破壊されるリスクも増えると思います。
#define TCCCLK 2000000 // CPUクロックを8分周した値。 // Pro Miniなど8MHzで動作している製品の場合は 1000000 にする #define ICP1 8 // ICP1の端子を定義 // 周期測定ののための変数 volatile uint16_t NumV; volatile uint32_t SumV; volatile uint16_t CapV; volatile uint16_t PreCap,NowCap; // 1秒間の最大最小平均を格納するための変数 uint16_t tNumV; float tAveV; uint32_t tSumV; unsigned long current_time; // 割り込みハンドラ ISR(TIMER1_CAPT_vect) { TIFR1 |= (1<<ICF1); NowCap = ICR1; CapV = NowCap - PreCap; // 差分を求めて周期を算出 PreCap = NowCap; SumV += CapV; NumV++; } void setup() { Serial.begin(38400); pinMode(LED_BUILTIN, OUTPUT); // TCC1でICP1信号(PB0端子=IO8)の周期を計測する TCCR1A = 0; //initialize Timer1 TCCR1B = 0; TCNT1 = 0; pinMode(ICP1, INPUT); //ICP1の端子をプルアップなしの入力に設定する TCCR1B = 0x02; // 16MHzを8分周 TIMSK1 |= (1 << ICIE1); // キャプチャ割り込み許可 current_time = millis(); } #define FREQ // 計測を周波数で行う場合 void loop() { static uint8_t skp = 3; // 最初の何回かはデータを捨てる if (millis() - current_time >= 1000) { // 割り込みで更新している変数をコピー&初期化する noInterrupts(); //割り込み停止 tSumV = SumV; tNumV = NumV; SumV = 0; NumV = 0; interrupts(); //割り込み再開 tAveV = (float)tSumV / (float)tNumV; digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); current_time = millis(); // PCに送信 if(skp == 0){ #ifdef FREQ Serial.println((double)TCCCLK/(double)tAveV*1000.); #else Serial.println(tAveV/2); // これで単位がusになる #endif } else { skp--; } } }