HIROBIRO

HIROBIRO

金銭的・精神的自由を目指すブログ。

炊飯器の残量カウンターを電子工作で作った(3)(完成編)


前回の記事まででとりあえず残量カウンターの試作機は出来上がりました。

実際に炊飯器に取りつけてフィールドテストを行った結果、いくつか課題が見つかったので修正していきます。

カウンターを作ると決めてから思ったより時間がかかってしまい、モチベーションを失いそうになることもありました。

しかし米の炊き忘れが発生して悲しい思いをする度に何度も気力を取り戻し、とりあえず完成まで漕ぎつけました。



 ちなみに前回、前々回の記事は以下です。

 

www.hirobiro-life.com


www.hirobiro-life.com


 

フィールドテストで見つけた課題

炊飯器に残量カウンターを取り付け、3日ほど様子を見てみました。

炊飯器の蓋の開閉回数から残量を予測するのでかなり誤差は大きいのではないかと思ってましたが、意外にも大丈夫そうでした。

 

我が家の場合、1食で1人が食べる量がだいたい0.5合だったようで、カウンターを使って上手いこと残量計算できています。

たまにちょっとだけ(茶碗1/4くらい)お代わりすることもありますが、その場合はカウンターのボタンで数字をいじってやって調整してます。

この程度の手間は仕方ありません。

 
 

しかし課題も見つかりました。

①ディスプレイに文字を表示しただけでは残量が少なくなっても気づきにくい

②ディスプレイが24時間点灯しっぱなしなので不要なときは消えて欲しい

 

この2つの問題点を解決することにしました。

①ディスプレイに文字を表示しただけでは残量が少なくなっても気づきにくい

これはやる前から分かってはいました。
一番手軽にできるのはスピーカーを取り付けて音で知らせてくれるようにすることです。


昔秋月電子で買っておいたスピーカーを使い、まずは音が出るかどうかの確認です。


f:id:hirokun1735:20160330145147j:plain
スピーカー 8Ω8W: パーツ一般 秋月電子通商-電子部品・ネット通販


ネットで以下のサンプルプログラムを発見したので利用させて頂きました。

#define BEAT 300   // 音の長さを指定
#define PINNO 12   // 圧電スピーカを接続したピン番号

void setup() {
}
void loop() {
     tone(PINNO,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(PINNO,294,BEAT) ;  // レ
     delay(BEAT) ;
     tone(PINNO,330,BEAT) ;  // ミ
     delay(BEAT) ;
     tone(PINNO,349,BEAT) ;  // ファ
     delay(BEAT) ;
     tone(PINNO,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(PINNO,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(PINNO,494,BEAT) ;  // シ
     delay(BEAT) ;
     tone(PINNO,523,BEAT) ;  // ド
     delay(3000) ;           // 3秒後に繰り返す
}

ホームページ移転のお知らせ - Yahoo!ジオシティーズ


スピーカーをD12とGNDの端子に接続し、プログラムを書き込むと自動的に「ドレミファソラシド」を鳴らしてくれます。
音を鳴らすのはすぐに成功しましたが、私の持っていたスピーカーはかなり音が大きいです。
そのため750Ωの抵抗を直列に入れて電流制限すると少し小さめの音になりました。


普通に「ドレミファソラシド」を鳴らすだけでは芸が無いので、「クマのプーさん」の曲を鳴らしてみました。
楽譜を持っていないので苦労しましたが、試行錯誤した結果まあまあいい感じに鳴らせるようになりました。
耳コピでやっているので多少ずれているのは勘弁してください。



Arduinoでプーさんのテーマ曲を鳴らしてみた



さて音の確認ができたところで、問題はどのタイミングで鳴らすかです。
米の残量が0のときは当然頻繁に知らせる必要があります。
音が鳴っていてもそのタイミングで部屋にいないかもしれません。
ただあまりにも頻繁に鳴り過ぎるとそれはそれで困るので、とりあえず1時間に1回鳴らすことにしました。


また残量が0でなくても、中途半端な量が残っている状態も困ります。
うちは夫婦二人暮らしなので少なくとも2杯分は常に残っている必要があります。
なので米の残量が残り2杯分になってからは蓋の開け閉めのときにも音を鳴らすことにしました。
具体的なソースコードは最後にまとめて載せます。

②ディスプレイが24時間点灯しっぱなしなので不要なときは消えて欲しい


f:id:hirokun1735:20180303140017j:plain


購入したLCDシールドはLED付きなので、24時間常に点灯しています。
必要のないときまでずっと点灯していると無駄に電気を食うことになります。
ただ先に結論を言うとディスプレイのLED消灯は難しそうなので止めました。


f:id:hirokun1735:20180321093735p:plain


上の図はLCDディスプレイのLED周りの回路図です。
VCCに電源電圧が加えられてトランジスタQ1のベースに電圧が加わるとQ1が導通し、VCCからLEDを通ってGNDまで電流が流れます。
LEDを消灯するにはVCCの電圧をOFFすれば良いのですが、VCCはArduino本体から供給されています。
VCCを止めればLEDだけでなくArduino自体も電源OFFすることになります。


回路の抵抗を外してトランジスタを新たに追加するとか、少し改造してやれば何とかできそうな気もしますが面倒なので止めました。
LCDを基板から剥がしてやる必要がありそうですし、そこまで手間をかける必要もないかと。
電源はアダプターから供給するので電池切れなどの心配もありません。


それでも、少しでも電力消費を抑えるためにSleep modeを使えないか調べました。
以下のリンク先のページを参考にしました。
TetsuakiBaba.jp


ArduinoのSleep modeには以下のものがあります。
どのモードを選ぶかによって消費電力の大きさやスリープからの復帰方法が変わります。
消費電力が少ないモードを選ぶと色々なものを寝かせることになるため、スリープからの復帰方法が限定されてしまいます。


f:id:hirokun1735:20180321083327j:plain



このうち最も消費電力が少ないのはPower-downモードです。
スリープからの復帰方法はウォッチドッグタイマ、シリアル割込み、外部ピン(D2,D3)からの割込みの3つです。


今回作成している炊飯器の残量カウンターではボタン操作でカウント値を修正できるようにしています。
そのためボタンを押したときにスリープから復帰できると嬉しいのですが、このボタン入力はアナログピンで受け取っているためPower-downモードでは復帰しません。
ADコンバータへの入力で復帰するにはIdleモードかADC Noise Reductionモードです。


ただ今回はボタン入力からの復帰よりも消費電力の低減を取ることにし、Power-downモードを採用しました。
カウント値の修正は蓋を開け閉めした直後のスリープから起きた状態でのみできれば十分だと思います。

Sleep modeの搭載

炊飯器の残量カウンターにSleep modeを搭載することを考えた場合、スリープからの復帰は次の2パターンあります。


①蓋を開けて傾斜センサがONしたとき
②1時間に1回スピーカーからアラームを鳴らすとき


①は傾斜センサからの信号を入力するピンの外部割込みによって行います。
②は一定周期で復帰させる必要があるため、ウォッチドッグタイマを利用します。


通常ウォッチドッグタイマは最大8秒しかカウントできないのですが、タイマ復帰後に呼ばれるISR(WDT_vect)にてカウンタを利用することで1時間のカウントを実現しています。
詳細は下記のリンク先のページを参考にしています。
Arduinoで割り込みとウォッチドッグタイマを使ったスリープ復帰 - Qiita


#include <avr/sleep.h>
#include <avr/wdt.h>

volatile int sleep_flg=0;
volatile int counter;
int count_max=15;   //4second * 15 = 60 second 
int wait_minutes=60;   //wait_time (minutes)

//////////////////////////////////////////////////////////////////////
//Sleep
//////////////////////////////////////////////////////////////////////

void wakeUpNow()        // 外部割込み後の復帰

{
  //sleep end
  sleep_flg = 0;
  //counter reset
  counter = 0;
}

void sleepNow()         // here we put the arduino to sleep
{
  wdt_set();      //watch dog timer set
  sleep_flg=1;      //enable on sleep flag
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here
  sleep_enable();  // enables the sleep bit in the mcucr register
                           // so sleep is possible. just a safety pin                            
  attachInterrupt(0,wakeUpNow, LOW); 
  // use interrupt 0 (pin 2) and run function
  // wakeUpNow when pin 2 gets FALLING
  while(sleep_flg){
    sleep_mode();            // here the device is actually put to sleep!!
  }
    sleep_disable();         // first thing after waking from sleep:
                                     // disable sleep...
  wdt_unset(); //watch dog timer unset
  detachInterrupt(0);      // disables interrupt 0 on pin 2 so the
                                      // wakeUpNow code will not be executed
                                      // during normal running time. 
}

//////////////////////////////////////////////////////////////////////
//Watch Dog Timer
//////////////////////////////////////////////////////////////////////

//watch dog timer setup
void wdt_set()
{
  wdt_reset();
  cli();      //disable interrupt
  MCUSR = 0;    
  WDTCSR |= 0b00011000;    //WDCE WDE set
  WDTCSR =  0b01000000 | 0b100000; //WDIE set  |WDIF set  scale 4s
  sei();     //able interrupt
}

//watch dog timer unset
void wdt_unset()
{
  wdt_reset();
  cli();
  MCUSR = 0;
  WDTCSR |= 0b00011000; //WDCE WDE set
  WDTCSR =  0b00000000; //status clear
  sei();
}

//WDTによる復帰
ISR(WDT_vect)
{
  //wdt_reset();
  if(sleep_flg == 1)
  {
    counter++;
    if( counter >= (count_max * wait_minutes))    
    //sleepさせたい時間をカウント
    {
      //sleep end
      sleep_flg = 0;
      //counter reset
      counter = 0;
    }
  }
  else
  {
  }
}


これらに加えて、スリープ中は不要な機能をストップさせることにしました。
ADコンバータと低電圧検出器を停止させます。
やり方の詳細は先ほども紹介した下記ページを参考にしました。
TetsuakiBaba.jp


残量カウンターの完成

これにて残量カウンターはとりあえず完成しました。
全体のプログラムをまとめて載せておきます。

/*
Rice_counter
1.残り米量「○○杯分」を設定
2.蓋の開閉ごとにカウントダウン
3.残り米量2杯分になると一定時間ごとに音を鳴らす
*/

#include <LiquidCrystal.h>
#include <LCDKeypad.h>
#include <avr/sleep.h>
#include <avr/wdt.h>

#define DEBUG 0
//4分音符=120 (duration=500)
#define BEAT 250   // 音の長さを指定 (8分音符)
#define SOUND 3   // 圧電スピーカを接続したピン番号(D3)

//Pin assignments for SainSmart LCD Keypad Shield
LiquidCrystal lcd(8, 9, 4, 5, 6, 7); 
//---------------------------------------------

LCDKeypad keypad;

int localKey = -1;
int mode = 1;
int before_mode = 1;
int first_rice = 5;   //米の量、初期設定 茶碗何杯分か
int open_flag = 0;  //蓋開け閉めのフラグ
int rice = 0;
int sensor = 2;   //センサ信号デジタル入力ピン
int en_input1 = 1; //ボタン入力受付
int en_input2 = 0; //センサ入力受付
int sleepStatus = 0;     // variable to store a request for sleep
int count = 0;   //sleep用カウンタ
volatile int sleep_flg=0;
volatile int counter;   //wdt用カウンタ

int count_max=15;   //4second * 15 = 60 second 
int wait_minutes=60;   //wait_time (minutes)
           
void setup() 
{ 
  lcd.begin(16, 2);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Rice Counter");
  lcd.setCursor(0, 1);
  lcd.print("Start");
  delay(2500);

  rice = first_rice;

  Serial.begin(9600);   // 9600 bpsで通信
  Serial.println("Start");   

  pinMode(sensor, INPUT); //デジタルピン2をinputに設定

  counter=0;  //WDTカウンタ初期化
}

///////////////////////////////////////////////////////////////////////////////////////////////
//Main Loop
///////////////////////////////////////////////////////////////////////////////////////////////

void loop() 
{ 
  localKey = keypad.button();
  
  #if DEBUG
  //Serial.println(localKey);
  #endif
  
  if(mode == 1){
    //mode1:カウンタ
    #if DEBUG
    Serial.println("mode1");
    #endif
    
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Rice Counter");
    lcd.setCursor(0, 1);
    lcd.print(rice);
    delay(100);

    if(en_input1 == 1){
      if(localKey == 1) {   //UPボタンで増やす
        rice += 1;
        en_input1 = 0;
        count = 0;
      }else if(localKey == 2) {   //DOWNボタンで減らす
        
        #if DEBUG
        Serial.println("down");
        #endif
        
        rice -= 1;
        en_input1 = 0;
        count = 0;
      }
    }else if(localKey == -1){   //UP,DOWNボタンから指を離すと次の入力受付
      en_input1 = 1;
    }

    //デジタル入力、傾斜センサonでLOW
    if(en_input2 == 1){
      if(digitalRead(sensor) == LOW){   
        
        #if DEBUG
        Serial.println("sensor_on");
        #endif
        
        rice -= 1;
        en_input2 = 0;
        count = 0;
        delay(1500);
        if(rice <= 2){  //残り2杯分になると蓋を開け閉めしたときに音を鳴らす
          speaker();
        }
      }
    }else if(digitalRead(sensor) == HIGH){  //傾きが戻るまでカウンタ停止
      en_input2 = 1;
    }

      if(rice == 0) {
        Serial.println("rice=0");
        mode = 2; //残り0で表示固定モードに移行
        count = 0;
      }

    count++;
    Serial.println(count);
    if(count >= 300){      //0.1s*300=30s経過&蓋が閉まっているとsleep
      count = 0;
      if(digitalRead(sensor) == HIGH){
        mode = 3;
      }
    }
    delay(100);
    
  }else if(mode == 2){
    //mode2:米の量が0
    #if DEBUG
    Serial.println("mode2");
    #endif
    
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("Rice Empty");
    lcd.setCursor(0, 1);
    lcd.print(rice);
    delay(100);    
    before_mode = 2;

    count++;
    if(count >= 300){      //30s経過でsleep
      count = 0;
      mode = 3;
    }
    delay(100);
    
  }else if(mode == 3){    
      //mode3:スリープモード
      Serial.println("sleep start");
      delay(100);
      
      sleepNow();     // sleep function called here    
      if(open_flag == 1){  //蓋が空いたので米残量をマイナス1
        Serial.println("open_flag");
        if(rice != 0){
          rice -= 1;
        }
        en_input2 = 0;
        open_flag = 0;
      }
      if(rice <= 2){  //米残量0なら一定時間ごとに音を鳴らす
        speaker();
      }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////
//Sleep
///////////////////////////////////////////////////////////////////////////////////////////////

void sleepNow()         //スリープモードの中身
{
  wdt_set(); //watch dog timer set
  sleep_flg=1; //enable on sleep flag
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);   // sleep mode is set here
  ADCSRA &= ~(1 << ADEN); // ADC_OFF
  sleep_enable();          // enables the sleep bit in the mcucr register
                             // so sleep is possible. just a safety pin
  if(before_mode == 1){      //米残量0のときは傾斜センサで割込み入れない                     
    attachInterrupt(0,wakeUpNow, LOW); // use interrupt 0 (pin 2) and run function
                                       // wakeUpNow when pin 2 gets FALLING
  }
  while(sleep_flg){
    // BOD設定変更のため,BODSとBODSEに同時に1を出力.
    MCUCR |= (1 << BODSE)|(1 << BODS);
    // その後4クロック周期内にBODSに1,BODSEに0を出力してBODをOFFにする.
    MCUCR = (MCUCR & ~(1 << BODSE))|(1 << BODS);
    // その後3クロック周期内にスリープ状態へ移行.
    sleep_mode();            // here the device is actually put to sleep!!
                               // THE PROGRAM CONTINUES FROM HERE AFTER WAKING UP
  }
  sleep_disable();         // first thing after waking from sleep:
                             // disable sleep...
  ADCSRA |= (1 << ADEN);  // ADC_ON
  wdt_unset(); //watch dog timer unset
  detachInterrupt(0);      // disables interrupt 0 on pin 2 so the
                             // wakeUpNow code will not be executed
                             // during normal running time. 
}

void wakeUpNow()        // 外部割込み後の復帰
{
  Serial.println("Wakeup");
  delay(100);
  //sleep end
  sleep_flg = 0;
  //counter reset
  counter = 0;
  open_flag = 1;  //蓋が空いたので米残量をマイナス1する
  mode = before_mode;   //前回のモードに復帰、何もなければ10s後にスリープ
}


///////////////////////////////////////////////////////////////////////////////////////////////
//Watch Dog Timer
///////////////////////////////////////////////////////////////////////////////////////////////

//watch dog timer setup
void wdt_set()
{
  wdt_reset();
  cli();      //disable interrupt
  MCUSR = 0;    
  WDTCSR |= 0b00011000; //WDCE WDE set 
  WDTCSR =  0b01000000 | 0b100000;//WDIE set  |WDIF set  scale 4 seconds 
  sei();     //able interrupt
}

//watch dog timer unset
void wdt_unset()
{
  wdt_reset();
  cli();
  MCUSR = 0;
  WDTCSR |= 0b00011000; //WDCE WDE set
  WDTCSR =  0b00000000; //status clear
  sei();
}

//WDTによる復帰
ISR(WDT_vect)
{
  //wdt_reset();
  if(sleep_flg == 1)
  {
    counter++;
    Serial.println("wdt");  
    if( counter >= (count_max * wait_minutes))    
    //sleepさせたい時間をカウント
    {
      //sleep end
      sleep_flg = 0;
      //counter reset
      counter = 0;
      mode = before_mode;   //前回のモードに復帰、何もなければ10s後にスリープ
    }
  }
  else
  {
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////
//Speaker
///////////////////////////////////////////////////////////////////////////////////////////////
void speaker()   //Winnie the Pooh
{
      tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;
     tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;

     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,349,BEAT) ;  // ファ
     delay(BEAT) ;
     tone(SOUND,349,BEAT) ;  // ファ
     delay(BEAT) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT) ;
     tone(SOUND,294,BEAT) ;  // レ
     delay(BEAT) ;
     tone(SOUND,294,BEAT) ;  // レ
     delay(BEAT) ;
     tone(SOUND,311,BEAT) ;  // レ#
     delay(BEAT) ;
     tone(SOUND,311,BEAT) ;  // レ#
     delay(BEAT) ;

     tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;
     tone(SOUND,330,BEAT*1.5) ;  // ミ
     delay(BEAT*1.5) ;
     tone(SOUND,294,BEAT/2) ;  // レ
     delay(BEAT/2) ;
     tone(SOUND,330,BEAT) ;  // ミ
     delay(BEAT*2) ;
     tone(SOUND,294,BEAT*2) ;  // レ-
     delay(BEAT*4) ;

     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,262,BEAT) ;  // ド
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,466,BEAT) ;  // シb
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,440,BEAT) ;  // ラ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,392,BEAT) ;  // ソ
     delay(BEAT) ;
     tone(SOUND,349,BEAT*3) ;  // ファ-
     delay(BEAT*3) ;

     delay(3000) ;           // 3秒後に繰り返す
}

回路図は以下のようになっています。
S1が傾斜センサでR3がプルアップ用の465Ω抵抗、R4がスピーカーの電流制限用の750Ω抵抗です。


f:id:hirokun1735:20180324093911p:plain


f:id:hirokun1735:20180324122516p:plain


最後に説明動画を載せます。
全体的な動作をまとめています。



炊飯器の残量カウンターをArduinoで作った


まとめと感想

炊飯器の残量カウンターが完成しました。
全体的な動作をまとめると以下のようになります。


①カウンターを起動し、炊いた米の量を設定する
②蓋を開閉するたびにカウントが-1される
③残り米量が茶碗2杯分以下で蓋の開閉時に音を鳴らす
④30秒放置でスリープ、1時間に1度音を鳴らす
⑤カウント0でカウンター停止、リセットボタンで初期状態に戻る


アイデア自体は割とすぐ思いついたのですが、スリープモードの検討やプログラムのバグ取りにちょっと時間がかかってしまいました。
1週間ほど使っていますが、前よりも米の炊き忘れは減りそうです。

米の残量が少ないときのアラームを1時間に1回鳴らしていますが、家に帰ってすぐ食べようとするときはあまり意味が無いです。
もし2号機を作る場合は米が少ないときはメールをスマホに送ってくれる機能を付けたいですね。


(18/4/30追記)メール送信機能を追加しました。
詳細は下記の記事にあります。
www.hirobiro-life.com