はじめての電子工作超入門

第56回 SORACOM Air×3GIMによるArduinoの3G通信 その4~Arduinoのsleepモードで省エネ実装

DSC_0081

前回、3GIMとセンサ評価キットを使って簡易のデータロガーを作成しました。今回は、実際の運用をする場合を想定して一番のネックになる電源について、Arduinoのスリープ機能を使って省電力での稼働を目的に実装を行ってみたいと思います。

目次

  1. スリープ機能ってなに?
  2. 基本的なスリープ処理の流れと種類
  3. 実際にスリープを試してみる
  4. まとめ

1. スリープ機能(sleep)ってなに?

スリープ機能(sleep)は、その名のとおりArduinoの処理を停止(寝かせる)する機能です。みなさんもパソコンを利用されていれば電源を消そうとする際に「シャットダウン」・「スリープ」(または休止)などという選択肢で見たことがあると思います。詳細は後述するとして、スリープ機能はArduinoの動作を必要最低限にして、消費電力を節約するときに利用します。

データロガーなどのように、実際にデータを取得して送信して、また3分後に同じ処理を繰り返すなどといった動作の場合、その3分間はArduinoは何の処理もしていない状態なので、これまでの場合であればdelay(180000);として、3分間待つようにしています。しかし、Arduinoの基本的な電力は消費されている状態です。このような場合にスリープ機能が役に立ちます。
※実際にはArduinoの基盤には色々な機能をもった部品が動いているため、極限まで省エネで稼働させる場合はCPUと最小限の部品の状態で稼働させる必要があります。

図1 通常稼動とスリープ機能を稼動させた場合

図1 通常稼動とスリープ機能を稼動させた場合

 

 

2. 基本的なスリープ処理の流れと種類

Arduinoには色々なモードのスリープ機能を使うことができます。スリープの実行中はArduinoに処理をさせることができないため、再度処理をさせたい場合は、スリープから復帰させる必要があります。復帰の方法は「割り込み」処理をしたり、Arduino自体をリセットさせるなどスリープの種類で変わってきます。

スリープ機能自体は、Arduinoのこのページに、基本的な説明とサンプルが記載されています。

 

「割り込み」ってなに?

割り込み処理とは、たとえるならばAさんがある一つの作業をやっているところに、Bさんから「Aさんちょっとその作業よりも僕の作業をお願いしたいのだけれども」と作業を依頼された際に、自分の作業よりもBさんの作業を優先(割り込み)させて処理するのが、割り込み処理のイメージになります。

これまでのArduinoのプログラムであれば、loop関数の中で、上から順に処理を実行して、たとえばセンサのデータを取得する関数が呼ばれた際に、その値を取得していましたが、割り込み処理を担った場合、任意のタイミング(たとえばボタンを押された場合)で割り込み処理を発生させて、データの取得を優先させる、などということも可能になります。

この割り込み処理を使って、スリープ状態のArduinoに割り込み処理で復帰を実行することができます。

Arduinoでの割り込み処理

この割り込み処理はArduinoの場合、attachinterrupt()という関数を使って実現できます。この関数は、Arduinoで割り込み処理が可能な2番ピン・3番ピンの信号が変化した場合(※1)、割り込み処理を実行することができるようになっています。
※1.Arduinoの種類や搭載されているCPUによっては他のピンも動作させることが可能です


attachInterrupt([1.利用するピン設定], [2.実行する関数], [3.割り込み発生のタイミング設定])

  1. 利用するピン設定
    0-2番ピン、1-3番ピンを設定します。
  2. 実行する関数
    割り込み処理が発生したときに実行したい関数を指定することができます。
  3. 割り込み発生のタイミング設定
    信号がどのように変化した時に割り込み処理を発生させるかを設定できます。設定できるタイミングは下記の4つです。
    LOW:信号がLOWの状態(入力がない場合)、LOWの限りは割り込み処理が何回でも発生します。
    CHANGE:LOW⇔HIGHで変化したときに発生
    RISING:LOW→HIGHに変化したときに発生
    FALLING:HIGH→LOWに変化したときに発生

 

下記のプログラムでは、割り込み処理を使って、2番ピンの入力信号が変わったらLEDを光らせる例です。

割り込み処理のサンプルプログラム


int ledPin = 13; //LEDのピン番号
volatile int state = LOW; //割り込みピン信号の状態

void setup(){
pinMode(ledPin, OUTPUT);
attachInterrupt(0, blink, CHANGE); //0-2番ピンを指定して、信号変化の際にblink関数を実行
}

void loop(){
digitalWrite(ledPin, state); //割り込みピン信号の状態をそのままLED表示に
}

void blink(){
state = !state; //割り込み処理が呼ばれたら状態を変更させる
}

図2.割り込み処理の回路

図2.割り込み処理の回路

写真1 割り込み回路

写真1 割り込み回路

トグルスイッチを切り替えると2番ピンがLOW-HIGHと切り替わるので、それによって割り込み処理が発生してLEDが光ったり消えたりするのが確認できますね。

基本的にはこの割り込み処理を使って、スリープ状態からArduinoを復帰させることができます。割り込み処理に関しては、もっと詳しく学んでいくと色々な制約や使い方ができますので、気になる方は調べてみてください。

 

スリープの種類

Arduinoのスリープ機能は下記の5種類あるようです。スリープの種類によって省エネ効率や、復帰できる方法が違うようですね。復帰の方法は各モードで違うようですが、基本的には先ほどの割り込みでいけるようなので、実際に動かしながら試してみたいと思います。

モード 説明 省エネ効率
SLEEP_MODE_PWR_IDLE アイドル状態、システムクロックは停止するが、内臓タイマーや外部割込み、シリアルポートなどの機能は動作します。復帰方法は外部割込み、ウォッチドッグタイマー、ADCの入力変化、リセットピンによる復帰が可能 普通
SLEEP_MODE_PWR_ADC AD変換ノイズ低減用に利用されます(ADC対応のAVRでないと利用不可)
SLEEP_MODE_PWR_DOWN パワーダウンモード、最低限の動作のみのもっとも消費電力が少なくなるモード。外部割込み、ウォッチドッグタイマー、リセットピンによる復帰が可能。 もっとも高い
SLEEP_MODE_PWR_SAVE パワーセーブモード、タイマー用の外部発信器は動作しているので、外部割込み、ウォッチドッグタイマー、リセットピンによる復帰が可能。 高い
SLEEP_MODE_PWR_STANDBY スタンバイモード、パワーダウンと比べて、メインクロックが動作しているので復帰が少しだけ早いモードです。 かなり高い

3. 実際にスリープを試してみる

スリープ機能の一連の流れがわかったところで、実際にプログラムを動かして試してみます。このプログラムでは、先ほどの割り込み処理の回路を使って、シリアルモニターから「S」が入力されるとスリープモードに入り、トグルスイッチをLOWの状態にするとスリープモードから復帰するような流れになっています。

 

vol56-1

 

スリープ機能のサンプルプログラム


#include <avr/sleep.h>
int wakePin = 2; //割り込み用のピン番号

void wakeUpNow(){
Serial.println("wake up!"); //復帰時にシリアルモニターに表示
delay(500);
}

void setup()
{
pinMode(wakePin, INPUT);
Serial.begin(9600);
}

void sleepNow(){
set_sleep_mode(SLEEP_MODE_PWR_DOWN); //スリープモードの設定
sleep_enable(); //スリープを有効にする

attachInterrupt(0,wakeUpNow, LOW); //割り込み処理の設定
sleep_mode(); //指定したモードでスリープを開始
sleep_disable(); //割り込みによってスリープから復帰
detachInterrupt(0); //割り込み処理を解除する
}

void loop()
{
if (Serial.available()) { //シリアルモニターからの入力があった場合
int val = Serial.read();
if (val == 'S') { //"S"が入力された場合にスリープモードにする
Serial.println("Goodnight!");
delay(100);
sleepNow(); //スリープモードにする
}
}
Serial.println("Awake!"); //動作中の表示
delay(500);
}

図3 Sleep状態→復帰の様子

図3 Sleep状態→復帰の様子

シリアルモニタ上で「S」を入力するとGoodnight!の表示とともに、Arduinoがスリープモードに入ります。その後、トグルスイッチでスリープモードから復帰していることが確認できました。

では、スリープは実装できたようですが、実際に一定時間動かすためにはどうすれば良いでしょうか?それにはスリープ時でも一定時間計測して、一定時間経過すると割り込み処理をしてくれるウォッチドッグタイマという機能を利用します。

ウォッチドッグタイマのプログラムは今回ラジオペンチさんのブログで紹介されているdelayWDTという関数を利用してウォッチドッグを使ったスリープの実装を試してみます。この関数では最大8秒間スリープをかけることができるので、8秒毎にスリープモードから復帰させた際にカウントアップをさせた変数を見てデータの取得タイミングであればその処理を、違うのであれば再度スリープに入る、という処理を組み入れることで、一定時間毎での処理が可能となります。

 

定期的なスリープ実装プログラム

#include <avr/sleep.h>
#include <avr/wdt.h>
int wakePin = 2; //割り込み用のピン番号
int ledPin = 13;

volatile int wdt_cycle = 0; 
volatile int wdt_counter = 0; 

/*
 * ウォッチドッグ処理の参考元:2014/11/17 ラジオペンチさん http://radiopench.blog96.fc2.com/
 */
void delayWDT_setup(unsigned int ii) { // ウォッチドッグタイマーをセット。
 // 引数はWDTCSRにセットするWDP0-WDP3の値。設定値と動作時間は概略下記
 // 0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms
 // 6=1sec, 7=2sec, 8=4sec, 9=8sec
 byte bb;
 if (ii > 9 ){ // 変な値を排除
 ii = 9;
 }
 bb =ii & 7; // 下位3ビットをbbに
 if (ii > 7){ // 7以上(7.8,9)なら
 bb |= (1 << 5); // bbの5ビット目(WDP3)を1にする
 }
 bb |= ( 1 << WDCE );

 MCUSR &= ~(1 << WDRF); // MCU Status Reg. Watchdog Reset Flag ->0
 // start timed sequence
 WDTCSR |= (1 << WDCE) | (1<<WDE); // ウォッチドッグ変更許可(WDCEは4サイクルで自動リセット)
 // set new watchdog timeout value
 WDTCSR = bb; // 制御レジスタを設定
 WDTCSR |= _BV(WDIE);
} 

ISR(WDT_vect) { // WDTがタイムアップした時に実行される処理
 // wdt_cycle++; // 必要ならコメントアウトを外す
}


void delayWDT(unsigned long t) { // パワーダウンモードでdelayを実行
 Serial.println("Goodnight!"); //動作中の表示
 delay(100);

 delayWDT_setup(t); // ウォッチドッグタイマー割り込み条件設定
 ADCSRA &= ~(1 << ADEN); // ADENビットをクリアしてADCを停止(120μA節約)
 set_sleep_mode(SLEEP_MODE_PWR_DOWN); // パワーダウンモード
 sleep_enable();

 sleep_mode(); // ここでスリープに入る

 sleep_disable(); // WDTがタイムアップでここから動作再開 
 ADCSRA |= (1 << ADEN); // ADCの電源をON } void setup() { pinMode(wakePin, INPUT); Serial.begin(9600); } void loop(){ Serial.println("Awake!"); //動作中の表示 delay(50); delayWDT(9); // 8sec wdt_counter++; if( wdt_counter > 10 ){
 digitalWrite(ledPin,HIGH);
 delay(3000);
 digitalWrite(ledPin,LOW);
 wdt_counter=0;
 }
 Serial.print("WakeUp!:"); //動作中の表示
 Serial.println(wdt_counter);
}
図4 スリープ毎にカウントアップして10になったらLEDを光らせて最初から

図4 スリープ毎にカウントアップして10になったらLEDを光らせてカウントリセット

これで、スリープ機能の実装が完了したので、これまで実装したデータロガーと組み合わせてみます。最終的なプログラムはこちらです。前回のプログラムからは、ウォッチドッグによるスリープの追加と、時刻の取得をサーバーサイドに任せて3GIM側では取得しないようにしたこと、送信のパラメータを省略して送付するデータ量を調整しています。

データロガープログラム

// 3GIM(V2) sample sketch -- httpGET
#include <SoftwareSerial.h>
#include "a3gim.h"
#include <Wire.h> 
#include <BD1020.h> //温度センサ用
#include <BM1383GLV.h> //気圧センサ用
#include <avr/sleep.h>
#include <avr/wdt.h>

BM1383GLV bm1383;//気圧センサ用
int tempout_pin = A2; //温度センサ用ピン設定
BD1020 bd1020; //温度センサ用

//スリープ・ウォッチドッグ用
int wakePin = 2; //割り込み番号指定(実際は0→1ピンを指定/1→2ピンを指定)
bool initFlg = true;
volatile int wdt_cycle = 0; 
volatile int wdt_counter = 0; 

SoftwareSerial Serial3g(4,5);

#define baudrate 9600UL
const int powerPin = 7; // 3gim power pin(If not using power control, 0 is set.)
const char *server = "deviceplus.jp";
const char *path = "";
int port = 80;

char res[a3gsMAX_RESULT_LENGTH+1];
int len;

String imei = "";
int rssi = 0;
float lat = 0;
float lng = 0;
int height = 0;
int utc = 0;
int number = 0;
int quality = 0;
String date = "";

/*****************************************/
/*
 * ウォッチドッグ処理の参考元:2014/11/17 ラジオペンチさん http://radiopench.blog96.fc2.com/
 */
/*****************************************/
void delayWDT_setup(unsigned int ii) { // ウォッチドッグタイマーをセット。
 // 引数はWDTCSRにセットするWDP0-WDP3の値。設定値と動作時間は概略下記
 // 0=16ms, 1=32ms, 2=64ms, 3=128ms, 4=250ms, 5=500ms
 // 6=1sec, 7=2sec, 8=4sec, 9=8sec
 byte bb;
 if (ii > 9 ){ // 変な値を排除
 ii = 9;
 }
 bb =ii & 7; // 下位3ビットをbbに
 if (ii > 7){ // 7以上(7.8,9)なら
 bb |= (1 << 5); // bbの5ビット目(WDP3)を1にする
 }
 bb |= ( 1 << WDCE );

 MCUSR &= ~(1 << WDRF); // MCU Status Reg. Watchdog Reset Flag ->0
 // start timed sequence
 WDTCSR |= (1 << WDCE) | (1<<WDE); // ウォッチドッグ変更許可(WDCEは4サイクルで自動リセット)
 // set new watchdog timeout value
 WDTCSR = bb; // 制御レジスタを設定
 WDTCSR |= _BV(WDIE);
} 

ISR(WDT_vect) { // WDTがタイムアップした時に実行される処理
 // wdt_cycle++; // 必要ならコメントアウトを外す
}


void delayWDT(unsigned long t) { // パワーダウンモードでdelayを実行
 Serial.println("Goodnight!"); //動作中の表示
 delay(100);

 delayWDT_setup(t); // ウォッチドッグタイマー割り込み条件設定
 ADCSRA &= ~(1 << ADEN); // ADENビットをクリアしてADCを停止(120μA節約)
 set_sleep_mode(SLEEP_MODE_PWR_DOWN); // パワーダウンモード
 sleep_enable();

 sleep_mode(); // ここでスリープに入る

 sleep_disable(); // WDTがタイムアップでここから動作再開 
 ADCSRA |= (1 << ADEN); // ADCの電源をON (|=が!=になっていたバグを修正2014/11/17)

}
/*****************************************/
/* END WATCH DOG / SLEEP */
/*****************************************/

// setup AGPS function
void setupAGPS()
{
 char apn[20], user[20], password[20];
 if (a3gs.getDefaultProfile(apn, user, password) == 0) {
 char atwppp[50];
 sprintf(atwppp,"at+wppp=2,4,\"%s\",\"%s\"",user,password);
 Serial.println(atwppp);
 a3gs.enterAT(2);
 a3gSerial.println(atwppp);
 delay(200);
 Serial.println("Assisted GPS set OK");
 }
}



void setup()
{
 Serial.begin(baudrate);
 Serial3g.begin(38400);
 pinMode(wakePin, INPUT);

 delay(3000); // Wait for Start Serial Monitor
 
 //温度センサ用
 bd1020.init(tempout_pin);
 
 //気圧センサ用
 byte rc;
 Wire.begin();
 rc = bm1383.init();
}

void loop()
{
 wdt_counter++;
 //初回を除き、カウンターが10以下なら再度スリープ状態へ
 if(!initFlg && wdt_counter < 10 ){ delayWDT(9); // 8sec return; } if(wdt_counter >= 10 ){
 wdt_counter = 0;
 }
 initFlg = false;

 //************************************
 //1.センサからデータを取得する
 //************************************

 //温度センサのデータを取得
 float temp;
 bd1020.get_val(&temp);

 //気圧センサのデータを取得
 byte rc;
 float pressure; 
 rc = bm1383.get_val(&pressure);

 //************************************
 //2.3GIMでデータをサーバーに送信
 //************************************
 if (a3gs.start(powerPin) == 0 && a3gs.begin(0, baudrate) == 0) {

 delay(25000); // ウェイトを持たせる
 len = sizeof(res);

 //***************************************************
 //get rssi
 //***************************************************
 if (a3gs.getRSSI(rssi) == 0) {
 if(rssi > 0 || rssi <= -113 ){
 //電波強度が取得できない場合は最初から
 Serial.println("retry.");
 return;
 }
 }
 else{
 //電波強度が取得できない場合は最初から
 Serial.println("retry.");
 return ;
 }

 //***************************************************
 //get imei
 //***************************************************
 char imei[a3gsIMEI_SIZE];
 if (a3gs.getIMEI(imei) == 0) {
 }


 //***************************************************
 //GPS-location
 //***************************************************
 setupAGPS();
 char lat[15], lng[15], utc[7], height[8];
 if (a3gs.getLocation2(lat, lng, height, utc, &quality, &number) == 0) {
 }

 String pathStr = "/api.php?i=";
 String tempS = String(temp);
 String pressureS = String(pressure);
 String imeiS = String(imei);
 String rssiS = String(rssi);
 String latS = String(lat);
 String lngS = String(lng);
 String utcS = String(utc);
 String qualityS = String(quality);
 String numberS = String(number);
 String heightS = String(height);
 String dateS = String(date);

 pathStr = pathStr+imeiS+"&r="+rssiS+"&p="+pressureS+"&t="+tempS+"&u="+utcS+"&lat="+latS+"&lng="+lngS+"&q="+qualityS+"&n="+numberS+"&h="+height;

 Serial.println(pathStr);

 int rst = a3gs.httpGET(server, port, pathStr.c_str(), res, len);

 
// Serial.print("RESULT:");
 Serial.println(rst);
 if (rst == 0) {
 Serial.println("OK!");
 }
 }
 else{
 Serial.println("Failed.");
 }
 Serial.println("Shutdown..");
 a3gs.end();
 a3gs.shutdown();
}

// END
写真2 モバイルバッテリーでデータロガーを稼働

写真2 モバイルバッテリーでデータロガーを稼働

これで、定期的にデータを取得してサーバーに送信するデータロガーが完成しました。これであればデータの取得間隔を1日数回などにすればモバイルバッテリーや電池などでも長期間の運用が可能ですね。ソーラーパネルで発電することで、さらに長期間安定した運用なども可能になるかと思います。

 

まとめ

今回、実際の長期間の運用も想定してデータロガーにスリープ機能を実装して定期的な観測が可能になりました。使い方や工夫次第でやりたいことが実現できるArduino、ラズパイやmbedなど他にも色々なマイコンがありますが、それぞれ長所・短所があるので、それらを学びながらArduinoをもっと楽しめるようになりたいと思います!

 

■関連記事

SORACOM Air×3GIMによるArduinoの3G通信〜センサ評価キットと組み合わせてデータロガー作成(3)SORACOM Air×3GIMによるArduinoの3G通信〜Arduinoで3G通信をする方法(2)
SORACOM Airを使ってArduinoで通信できる?~Arduinoで3G通信をする方法(1)
ラズベリーパイとSORACOM Airでインターネット接続!(1)登録編

電子工作マニュアル Vol.4
赤川シホロ

電子工作や新しいデバイスをこよなく愛するエンジニア。日常生活のちょっとしたことを電子工作で作って試して、おもしろく過ごしたいと日々考えています。