LEDシーリングライト用赤外線リモコンの製作(9)アイリスオーヤマ用 スリープ対応

アイドル時にスリープさせるようにして、省電力化できました。

●改造概要

おもな変更点は次の通りです。この変更により、アイドル時に大幅に消費電力を抑えることができます。

  • キー入力を単純なポート読み出しに変更して、TIMER0、TIMER2も使用しないように変更。また、両ペリフェラルもパワーダウンさせる
  • スリープ処理を追加し、ピンチェンジ(状態変化)割り込みを発生させて、ウェイクアップさせる

●割り込み処理

状態変化割り込みはArduinoでは今のところサポートされていないようなので、WinAVR/gcc(Cコンパイラ)の機能を直接使って割り込みを発生させ、それでウェイクアップさせることにしました。

割り込み処理を抜粋して簡単に説明します。

#include <avr/sleep.h>          // スリープ
#include <avr/io.h>             //
#include <avr/interrupt.h>      // 割り込み

// ピンチェンジ割り込み2 ハンドラ
ISR(PCINT2_vect)
{
  // 何もしない
}

void setup(void) {
  // ピンチェンジ割り込みマスク D2~D5
  PCMSK2 |= ((1<<PCINT18) | (1<<PCINT19) | (1<<PCINT20) | (1<<PCINT21) );

  // ピンチェンジ割り込み2 許可
  PCICR |= (1<<PCIE2);

  // ペリフェラルのパワーダウン
  PRR = (1<<PRTWI) | (1<<PRTIM1) | (1<<PRSPI) | (1<<PRUSART0)
      | (1<<PRADC) | (1<<PRTIM0) | (1<<PRTIM2);

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);      // スリープモード

  (中略)
}

// メインループ
void loop(void) {
  sleep_mode();                         //ここでsleepに移行
  (ウェイクアップ後に実行される処理)
}

スイッチはD2~D5に接続されているため、ピンチェンジ割り込みの番号はPCINT18~PCINT21(PORTDのビット2~ビット5)となります。

13行目でピンチェンジ割り込みのマスク(どの割り込みを有効にするかを指定)を設定し、16行目でピンチェンジ割り込みの使用を許可します。PCINT18~PCINT21はPCIE2のグループに属しています。

これらの操作で割り込みが発生するようになります。割り込みが発生すると、割り込みベクタに処理が移りますが、そのベクタはあらかじめ設定しておく必要があります。その処理が6行目です。
割り込みが発生すると、この関数(割り込みサービスルーチン)が実行されます。 今回はウェイクアップさせるだけの目的で使うため、この関数では何も処理しません。

19行目ではTIMER0、TIMER2も加えて、ペリフェラルをシャットダウンしています。

●割り込み関係のレジスタ

先の説明で使用したピンチェンジ割り込みで関係あるレジスタをAVR ATmega328のデータシートより引用します。

13.2.4 PCICR – Pin Change Interrupt Control Register

AVR_reg-PCICR

13.2.6 PCMSK2 – Pin Change Mask Register 2

AVR_reg-PCMSK2

●スリープ処理

データシートの10.1 Sleep ModesのTable 10-1.の表でははっきり分からなかったのですが、10.5 Power-down Modeの説明に、ピンチェンジ割り込みでもウェイクアップできるという記述を見つけたため、それを利用することにしました。

リモコンパルスの出力が終わったら、すぐにスリープ(シャットダウン・モード)に移行して、待機します。スイッチが押されると、状態変化割り込みが発生してウェイクアップし、スリープで止まっていた次の行から実行が始まります。その続きで押されたスイッチを特定して、リモコンパルスを出力し、それが終わったら再びスリープに戻ります。これをメインループで繰り返します。

●キー入力処理の簡素化

これまでは、チャタリング防止、キーリピート処理の付いた汎用のキー入力ドライバ(筆者作成のライブラリ)を使用していましたが、今回のような用途ではチャタリングが発生してもあまり支障がないため、そのドライバは使わずに単純にポートからスイッチの状態を読み込むようにしました。

ピンチェンジ割り込みを利用する上でもこの方が簡単です。

割り込みが発生すると、10msのセットアップタイムの後に入力ポートの状態を読み出します。

リモコンパルスの出力中は割り込みを禁止してあるため、この間に発生するスイッチのチャタリングは吸収されます。

なお、リモコン出力が終わってスリープに移行した後もスイッチが押され続けていて、そのあとにスイッチが離されると、そのタイミングでもピンチェンジ割り込みが発生しますが、セットアップタイム後のポート読み出しで該当するスイッチがない状態となり、パルス出力処理は実行されません。この場合は無駄なウェイクアップにはなりますが、誤作動することはありません(一瞬D13のLEDが点灯することがあります)。

●スケッチ(“irRemo2_8M3.ino”)

全ソースを掲載しておきます。

/*
  アイリスオーヤマ LEDシーリングライト用赤外線リモコン

    copyright (c) 2013  www.wsnak.com

    13/10/20 新規作成
    13/10/21 省電力試行
    13/10/22 8MHz版
    13/10/29 波形補正 NOP併用
    13/10/31 スリープモード対応
        電源電圧 2.945V の時のアイドル時の消費電流 113uA

    スイッチ入力によりコマンド切り替え
    メモリ点灯、消灯、全灯、常夜灯
*/

/*
    ディジタル・ポート・アサイン
      スイッチポート D2-D5
      IR発光(LED)  D8
*/

#include <avr/sleep.h>          // スリープ
#include <avr/io.h>             //
#include <avr/interrupt.h>      // 割り込み

#define ON HIGH
#define OFF LOW

#define IR_OFF digitalWrite(8, LOW);    // IR LED(D7)
#define IR_ON  digitalWrite(8, HIGH);   // IR LED(D7)

#define LED_OFF digitalWrite(13, LOW);    // LED(D13)
#define LED_ON  digitalWrite(13, HIGH);   // LED(D13)

// ピンチェンジ割り込み2 ハンドラ
ISR(PCINT2_vect)
{
  // 何もしない
}

//
// 初期化
//
void setup(void) {
  int i;

  // ピンチェンジ割り込みマスク D2~D5
  PCMSK2 |= ((1<<PCINT18) | (1<<PCINT19) | (1<<PCINT20)
          | (1<<PCINT21) );

  // ピンチェンジ割り込み2 許可
  PCICR |= (1<<PCIE2);

  pinMode(13, OUTPUT);   // IR発光 (IR-LED)
  LED_OFF;
  pinMode(8, OUTPUT);   // IR発光 (IR-LED)
  IR_OFF;

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);      // スリープモード

  // スイッチ用入力ポート定義
  pinMode(2, INPUT);   //
  pinMode(3, INPUT);   //
  pinMode(4, INPUT);   //
  pinMode(5, INPUT);   //

//  プルアップ有効化
//  digitalWrite(2, HIGH);
//  digitalWrite(3, HIGH);
//  digitalWrite(4, HIGH);
//  digitalWrite(5, HIGH);

  // ペリフェラルのパワーダウン
  PRR = (1<<PRTWI) | (1<<PRTIM1) | (1<<PRSPI)
    | (1<<PRUSART0) | (1<<PRADC) | (1<<PRTIM0) | (1<<PRTIM2);
}

//
// リモコン コマンド出力
//
void irOut(byte sw) {
  int i, k;

  // (C)
  for(i = 0; i < 77; i++) {
    IR_ON;
    delayMicroseconds(5);
    __asm__(
      "nop\n\t"
      "nop\n\t"
    );

    IR_OFF;
    delayMicroseconds(4);
    __asm__(
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
    );
  }
  delayMicroseconds(998);

  // ********* リピート部分 ************
  for(k = 0; k < 3; k++) {

  // 共通部分
  // (D)
    for(i = 0; i < 211; i++) {
      IR_ON;
      delayMicroseconds(5);
      __asm__(
        "nop\n\t"
        "nop\n\t"
      );
      IR_OFF;
      delayMicroseconds(4);
      __asm__(
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
      );
    }
    delayMicroseconds(996);

    pulseB();       // (E)
    pulseA(7);
    pulseB();
    // 共通部分終わり

    // コマンド依存部分
    switch(sw) {
      case 0:     // 消灯
        pulseA(4);
        pulseB();
        pulseA(19);
        pulseB();
        pulseA(1);
        pulseB();
        pulseB();
        pulseB();
        pulseB();
        pulseA(1);
        pulseA(1);
        break;

      case 1:     // メモリ点灯
        pulseA(6);
        pulseB();
        pulseA(17);
        pulseB();
        pulseA(1);
        pulseB();
        pulseB();
        pulseA(1);
        pulseB();
        pulseB();
        pulseA(1);
        break;

      case 2:     // 全点灯
        pulseA(3);
        pulseB();
        pulseA(20);
        pulseB();
        pulseA(1);
        pulseB();
        pulseA(2);
        pulseB();
        pulseA(2);
        break;

      case 3:     // 常夜灯
        pulseA(5);
        pulseB();
        pulseA(18);
        pulseB();
        pulseA(1);
        pulseB();
        pulseB();
        pulseA(4);
        break;
    }
    delayMicroseconds(9000);
  }
}

//
// パルス(A)出力
//
void pulseA(int n) {
  int i, j;

  for(j = 0; j < n; j++) {
    for(i = 0; i < 19; i++) {
      IR_ON;
      delayMicroseconds(5);
      __asm__(
        "nop\n\t"
        "nop\n\t"
      );
      IR_OFF;
      delayMicroseconds(4);
      __asm__(
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
        "nop\n\t"
      );
    }
    delayMicroseconds(497);
      __asm__(
        "nop\n\t"
        "nop\n\t"
      );
  }
}

//
// パルス(B)出力
//
void pulseB(void) {
  int i;

//  for(i = 0; i < 57; i++) {
  for(i = 0; i < 58; i++) {
    IR_ON;
    delayMicroseconds(5);
    __asm__(
      "nop\n\t"
      "nop\n\t"
    );
    IR_OFF;
    delayMicroseconds(4);
    __asm__(
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
      "nop\n\t"
    );
  }
  delayMicroseconds(495);
}

//
// メインループ
//
void loop(void) {
  int i;

  LED_OFF;
  sleep_mode();                         //ここでsleepに移行

  // -----------------------

  //  wakeup
  noInterrupts();     // 割り込み禁止

  delayMS(10);        // セットアップタイム

  LED_ON;

  if(!digitalRead(2)) {           // SW1(上) メモリ点灯
      irOut(1);
  } else if(!digitalRead(3)) {    // SW2(右) 全灯
      irOut(2);
  } else if(!digitalRead(4)) {    // SW3(下) 消灯
      irOut(0);
  } else if(!digitalRead(5)) {    // SW4(左) 常夜灯
      irOut(3);
  }

  interrupts();       // 割り込み許可
}

// msディレイ
void delayMS(int tm) {
  int i;

  for(i = 0; i < tm; i++) {
    delayMicroseconds(1000);      // 1ms
  }
}

キー入力の処理は259行目の”loop()” (メインループ) の中に入れました。単純にスイッチのつながっているポートの状態を読み出して、押されているときに該当するパルス出力処理をコールします。

スリープ、ウェイクアップを確認するため、D13のLEDをON/OFFさせています。ウェイクアップ中は点灯します。

287行目の”delayMS()”関数は、”delay()” が使えないためにms単位で遅延できるものを作りました。

キー入力を受けてからパルス出力を完了するまでは割り込みは禁止してあります。

●結果

テスタでアイドル時の消費電流を実測してみました。電池の電圧が 2.945V のとき、スリープ状態の消費電流は113uA (0.113mA) でした。これなら、電源スイッチ無しでもいけると思います。

さらに、現在、スイッチのプルアップ抵抗が10kΩですが、50kΩぐらいでも大丈夫だと思います。そうすればもう少し電流を低くできます。

●おまけ AVRの内蔵プルアップを有効にする方法

AVR(ATmega328など)の入力ポートはプルアップ抵抗を内蔵しています。これを有効にするには、入力ポートに設定した後で、”1″を書き込みます。今回の場合は、上記ソースファイルに入れてあります(69~72行目;コメントアウトしてある)。この処理で外部プルアップ抵抗器が省略できます。

ただし、抵抗値が大きいため、あまり強力にプルアップできません。用途に応じて、内部、外部を使い分ける必要があります。


次はパナソニックのLEDシーリングライト用赤外線リモコン送信器の試作も予定しています。基本的には今回の試作の修正、応用でいけると思います。

コメントは受け付けていません。