Arduino PRO MINI (3.3V, 8MHz動作)を使って赤外線リモコンを試作した話の続きになるが、今回は省電力化と実用的な(?)リモコンの製作、そして、ソフトウェアについて。ATmega328Pの各種機能や仕様については、データシート (“Atmel-42735B-ATmega328/P_Datasheet_Complete-11/2016” ) を参照した。データシートのページ番号はリビジョンによっても異なるので省略。
オンボードの電圧レギュレータとPower On LED用の直列抵抗を外したArduino PRO MINI (The Simple)を使用。特に工夫なく+3.3Vを与えてスリープさせると約100μAの電流が流れる。
省電力化と回路の変更
リモコンの中のマイコンがアクティブになるのは、24時間のうちの1秒間にも満たない。アクティブ時であっても、赤外線LEDが発光する時間はきわめて短いと考えられる。乾電池を長持ちさせるためには、マイコンをできるだけ長くスリープ状態 (SLEEP_MODE_PWR_DOWN)に置くことと、このときの消費電力(待機電力)を抑えることがキモだと考えた。そのあたりの知識を得るために、データシートの、 14.11. Minimizing Power Consumption を読んでポイントとなる項目を洗い出してみた。
- 使わないならA/Dコンバータの動作を禁止しておけ。
- スリープ時はアナログコンパレータをオフにしろ。
- 不要ならBODはオフにしておけ。
- 要らないなら、ウォッチドッグタイマ(WDT)も禁止しろ 。
- スリープ時の未接続ポートピンには用心しろ。
飲み屋の湯呑み風にまとめるとこんな感じか。スケッチに反映して試したところ、スリープ中の消費電流は、
- A/Dコンバータの動作禁止により80μAの減少。
- WDTの禁止により5μAの減少。
となった(+3.3V時)。これらにより、回路全体で約18μAしか流れなくなった(テスターによる測定)。
データシートによれば上記以外にも、
- Power Reduction Register= PRR の操作
- システムクロックプリスケーラ (CLKPR) による内部の低クロック化
といった方法がありそうだが、いずれもアクティブな期間の省電力には有効だが、各部へのクロック供給が停止するSLEEP_MODE_PWR_DOWN でのスリープ中は関係ないように読めた。
ただ、CLKPR で動作周波数を落とすことで、電源電圧が下がっても動作を持続できるかもしれないと思って取り入れることにし、試作時とは回路を変更した。
変更点は以下の3箇所。
- 電源を単4アルカリ乾電池 2本に。
- IRLEDを1本に。
2本使うと、電源電圧が2.6V前後で点灯できなくなる。 - IRLEDの直列抵抗(R1)を32(10 + 22)Ωに。
IRLEDを1本にしたことと、電源電圧の変動範囲が変わったので。
アルカリ乾電池を直列2本にしたので、電源電圧は+3.3V~+2.0Vまで変動することになる。ATmega328Pを8MHzクロックで動かす場合の最低電源電圧は+2.7Vと考えられるので、このままでは乾電池の寿命が尽きる前に動作しなくなるのは間違いない。ただ、クロックが4MHzならば+1.8Vまで動作することになっていることに気がついた。
Arduinoの void setup(); においてCLKPRを操作して内部動作周波数を4MHzに落とす。そして、一度動かし始めたら決して電源を切らずリセットもしないという条件ならばこの構成でも乾電池の寿命まで動くんじゃないのか、と思った次第。リスタート時の内部クロックは8MHz動作なので、+2Vでは動作しない。ちゃんとやるなら、ヒューズを書き換えて内部RCオシレータを分周して使うか、PRO MINI上の水晶発振子を交換するのだろうが、今回は手間のかからない、単なる思い付きを採用した。
追記 (この思い付きだけではダメ)
この思い付きだけではダメでした。Arduino PRO MINI (3.3V, 8MHz)に、電圧可変のレギュレータをつないでスリープ中に電圧を下げてみたところ、2.7Vを下回ったあたりでリセットがかかりました。BODのせい?
電池が弱る前にもうちょっと調べて対策します。中国からもっと早く可変降圧型レギュレータユニット(LM2596使用のヤツ)が届いてれば良かったんだけど。
で、調べてみたところArduino PRO MINIのBODLEVEL Fuseの値は0xfdとなっており、電源電圧が2.7Vtyp. を下回った時点でリセットがかかるようになっていた。BOD (Brown Out Detection) についてや、2.7V未満の電圧で確認した話、BODヒューズの書き換えなどについては、別の投稿にまとめることにします(ヒューズ書き換えたら動きました)。
製作
秋月電子のCタイプ(72×48mm)の両面スルーホールガラスエポキシ基板にArduino PRO MINIと4つのタクトスイッチ、トランジスタ、IrLED、抵抗などハンダ付けした。ぜんぜんキレイに仕上がらなかったのだけど、写真を掲載しておきます。
入れ物には3つで100円の小型のタッパーを使った。基板はフタにネジ止めし、四角く切り抜いてスイッチ部分が露出するようにした。電池ボックスは両面テープでタッパー底部に接着し、基板表側の配線が指に触れないよう、スイッチ以外の部分にはテープを貼った。タクトスイッチは、弱、中、強、停止の4つ。
このタッパー(おっと、リモコン)を、電池が下になる向きでトイレの壁面にぶら下げている。IrLEDは、シャワートイレの赤外線受光部に近くなるような位置にした。なおかつ、受光部を向くように脚をちょっと曲げたりしている。
以下に載せたスケッチの書込みは、電池ボックスからのリード線をハンダ付けする前に、USB-シリアル変換用のAE-FT231Xを接続し、+3.3Vを与えて行った。リード線をハンダ付けする前に現場でテストを繰り返したことは言うまでもない。
以前、Arduino nanoを使った赤外線リモコンデータの受信器をふつうのブレッドボードの上に作ったのだけど、ミニブレッドボードに移し替えて動くようにした。+5Vを引っ張るためにジャンパワイヤを貼り、受光素子の脚を曲げ、受信データをD2で受けるように直しただけだが。今回も出力データの確認に活躍してくれました。
スケッチ
170行にもならなかったので、以下に掲載。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
#include <avr/sleep.h> #include <avr/wdt.h> #define PRESCALE_RATIO 2 #define PDELAY(msec) delay((msec < PRESCALE_RATIO ? PRESCALE_RATIO : msec) / PRESCALE_RATIO); #define LED 13 #define PWM_OC1A 9 #define PWM_OC1B 10 #define SCAN_12 4 #define SCAN_34 5 #define KEY_13 6 #define KEY_24 7 #define TRIGGER_PIN 12 // --------------------- KEY-PAD related. // setup KEY related ports void setup_port(bool sleep) { if (sleep) { pinMode(SCAN_12, INPUT_PULLUP); pinMode(SCAN_34, INPUT_PULLUP); pinMode(KEY_13, INPUT); pinMode(KEY_24, INPUT); } else { pinMode(SCAN_12, OUTPUT); pinMode(SCAN_34, OUTPUT); pinMode(KEY_13, INPUT_PULLUP); pinMode(KEY_24, INPUT_PULLUP); } } int scan() { int n = 0; digitalWrite(SCAN_12, 0); digitalWrite(SCAN_34, 1); PDELAY(1); n |= digitalRead(KEY_13); n |= (digitalRead(KEY_24) << 1); digitalWrite(SCAN_12, 1); digitalWrite(SCAN_34, 0); PDELAY(1); n |= (digitalRead(KEY_13) << 2); n |= (digitalRead(KEY_24) << 3); digitalWrite(SCAN_12, 0); return n; } int get_key() { int scan_result = 0xf; for(int i = 0; i < 3; i++) { PDELAY(10); scan_result &= ~scan(); } if (scan_result & 1) return 1; if (scan_result & 2) return 2; if (scan_result & 4) return 3; if (scan_result & 8) return 4; return 0; } //------------------ IR related. #define NEC_T (int)(560 / PRESCALE_RATIO) #define AEHA_T (int)(425 / PRESCALE_RATIO) int format_T = NEC_T; // 基準フレーム時間 // system clk = 8MHz. (no prescaler) // 38KHz, duty 1:3 // 26.3μs ÷ 0.125 = 210 : 周期 // 6.3μs ÷ 0.125 = 52.8 : H期間 // FastPWM mode. WGM(13-11)=1111 void pwm_out(int t_count) { TCNT1 = 0; PORTB |= B00010000; //PB4(D12) ON (ロジアナのトリガ用) TCCR1A = B01100011; // Inverted, HIGH at BOTTOM, LOW on match. TCCR1B = B00011001; // ICNC1 = 0, ICES1 = 0, no prescaler. OCR1A = (210 / PRESCALE_RATIO) - 1; // TOP value. OCR1B = (53 / PRESCALE_RATIO) - 1; // H period of OC1B delayMicroseconds(format_T * t_count); TCCR1A = B00000000; TCCR1B = B00000000; PORTB &= B11101111; //PB4 OFF ( D12のH期間から波形出力時間を確認) } inline void send_bit(int value) { pwm_out(1); delayMicroseconds(value ? format_T * 3 : format_T); } void send_byte(byte value) { send_bit(value & 1); send_bit(value & 2); send_bit(value & 4); send_bit(value & 8); send_bit(value & 16); send_bit(value & 32); send_bit(value & 64); send_bit(value & 128); } void send_leader() { if (format_T == NEC_T) { pwm_out(16); delayMicroseconds(format_T * 8); } else { pwm_out(8); delayMicroseconds(format_T * 4); } } // '0'を4回送信。 void send_preframe() { for(int i = 0; i < 4; i++) send_bit(0); } // 適当な終端 void send_trailer() { send_bit(0); digitalWrite(PWM_OC1B, 0); } void send_command(byte cmd) { send_leader(); send_byte(0x3a); // customer code send_byte(cmd); // command data send_byte(~cmd); // check-code. send_trailer(); } static const byte command_data[] = { 0xc1, 0xa1, 0x81, 0x0d }; // 弱、中、強、停止 void setup() { PDELAY(100); byte save_SREG = SREG; cli(); CLKPR = 0x80; CLKPR = 1; // プリスケーラ設定。クロックを半分に。 ADCSRA &= 0x7f; // ADC禁止。MSB を 0に。 ACSR |= 0x80; // アナログコンパレータ禁止 SREG = save_SREG; pinMode(2, INPUT); pinMode(3, INPUT); for(int i = 4; i < 17; i++) // INPUT_PULLUP pinMode(i, INPUT_PULLUP); pinMode(LED, OUTPUT); pinMode(PWM_OC1A, OUTPUT); pinMode(PWM_OC1B, OUTPUT); pinMode(TRIGGER_PIN, OUTPUT); digitalWrite(LED, 0); digitalWrite(PWM_OC1A, 0); digitalWrite(PWM_OC1B, 0); digitalWrite(TRIGGER_PIN, 0); wdt_disable(); } void isr() { } void loop() { setup_port(true); PDELAY(10); attachInterrupt(0, isr, RISING ); set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_mode(); detachInterrupt(0); digitalWrite(LED, 1); setup_port(false); int n = get_key(); PDELAY(20); if ( n >=1 && n <= 4) send_command( command_data[n - 1] ); digitalWrite(LED, 0); } |
コードの順序とは前後するが、各関数についての説明。
(追記: BODLEVEL fuseの修正にともない、スケッチにも若干の変更を入れた。)
プリスケーラ関係の定義
前に書いたように、8MHzをシステムクロックプリスケーラで2分周し、内部クロックは4MHzとしている。このスケッチは、Arduino IDE (1.8.3)を使ってビルドしているが、IDE側は8MHz動作と思ってコードを生成するので、delay(); や delayMicroseconds(); といった計時用の関数が影響を受け、例えばdelay(100); を呼んだ場合、100msec後ではなく200msec後に帰ってくる。そのため、1/2に分周するならばこれらのパラメータも1/2にしてやる必要がある。
PWMの周波数も影響を受けるが、それについては後述する。
I/Oポート関係の定義
PWM出力のためのポート(PWM_OC1A, PWM_OC1BおよびTRIGGER_PIN)と、キースキャン関係のポート(SCAN_12, SCAN_34, KEY_13, KEY_24)を#defineで定義している。
PWM_OC1AとTRIGGER_PINは、赤外線リモコン用の38KHzデューティー25%の波形をロジアナで観測するためだけに用いており、リモコンで必要なのはIrLEDに接続するPWM_OC1B (D10)のみである。
void setup();
システムクロックプリスケーラの設定の後、A/Dコンバータおよびアナログコンパレータの動作を禁止する。CLKPR (Clock Prescale Register) の操作については、データシートの 13.11. System Clock Prescaler を参照した。最上位ビットを1にセットして4サイクル以内に設定値を書き込む必要があるとのこと。こんなコードでも現実に4MHzで動いているので、何とか間に合っているようだ。
その後、D4~D17をINPUT_PULLUPに設定してフローティング状態のままになるのを防ぎ、回路で使っているポートの初期化したあとで、wdt_disable(); によってWDTを禁止している。
D2, D3は、ハイインピーダンス状態にしておきたいので、INPUTを指定している(D3は使っていないのだが)。
スリープ状態と復帰 (void loop(); )
void loop() の先頭でスリープ状態に入るための準備 ( setup_port(true); ) をし、すぐにスリープに入る。そしてD2ピンが論理Hレベルを検出するとスリープから復帰するようになっている。復帰後に押されたキーを識別しIrLEDを点灯させる。
スリープから復帰するためのHレベルは、ワイヤードOR回路によって4つのキーのうちのいずれかが押下されたときに生じる。
まず、D2ピンは100KΩの抵抗を介してGNDに接地されているから、何もキーが押されていないときはLレベルになっている。
スリープに入る前にポートSCAN_12とSCAN_34にINPUT_PULLUP指定を行い、マイコン内部のプルアップ抵抗(おおむね33KΩ)に接続する。キーが押されていないときは、どこにも電流は流れない。しかしながら、いずれかのキースイッチがオンになると、マイコン内部からスイッチングダイオードD1またはD2を介して100KΩの抵抗にHレベルの電圧がかかる。同時にD2はHレベルとなるので割込みが発生しスリープ状態が解除される。
キースキャン (int scan(); および int get_key();)
スリープ状態から復帰すると、今度は setup_port(false); を呼びキースキャンのために SCAN_XXポートを出力モードに指定している。そして以下のようにして各キースイッチの状態を読み取る。
- SW1を読むため、SCAN_12に0、SCAN_34に1を出力した状態でKEY_13を読む。オンならば0がオフならば1が帰る。
- SW2を読むため、SCAN_XX側は変えずKEY_24を読む。オンならば0がオフならば1が帰る。
- SW3を読むため、SCAN_12に1、SCAN_34に0を出力した状態でKEY_13を読む。オンならば0がオフならば1が帰る。
- SW4を読むため、SCAN_XX側は変えずKEY_24を読む。オンならば0がオフならば1が帰る。
- scan(); は、スイッチの押下状態を戻り値の下位4ビットで表す。
- getkey(); ではscan(); を10msecごとに3回呼び、3回とも同じ内容ならば、オンになっているビット位置に対応するキーコード(1~4)を返す。正しく検出できなかった場合は0を返す。
ATmegaの内部プルアップ抵抗を利用することで、キーマトリックス回路に抵抗を使わずに済ませた。複数のキーを同時に押したときの電流の回り込みの配慮もしていないので、ダイオードも使わなかった。
もうちょっとまともな(?)キーマトリックが欲しいときは、秋月電子で350円で売っている 4×3キーパッド作成キット がおすすめ。あるいはちょっと前に取り上げた、TTP229を使った静電容量キーパッドもいいかもしれないが、箱に収めるのが厄介そうではある。
赤外線発光
今回はリモコンに必要な38KHzの赤外線発光を行うにあたり、ATmegaのPWM機能を用いた。3つあるタイマー/カウンターユニットのうち、16ビットのカウントが可能なTC1ユニットを使った。PWMの詳細についてはデータシートの 20. TC1-16-bit Timer/Counter1 with PWM を参照。
赤外線リモコンのデューティー比は、参考にした資料ごとに1/3 (33%)だったり1:3 (25%)と書いてあったりして迷う。試してみたところ、1:1 (50%)でも1:3 (25%)でもリモコンとしては機能した。例えば25%といった場合、発光期間が1周期の25%ということになるから、38KHzならば1周期約26μsecのうちの6μsecだけ光ることになる。つまり、デューティー比が小さい方が電力面では有利ということになる。
PWM出力のシミュレーション
データシートに書いてある内容を読んでも、なかなか直感的に理解できなかったので、時間の経過(クロック印加)に対応したカウンター(TCNT1)と出力比較ピン(OC1A/B)の動きをJavaScriptで適当に記述し、GoogleChartで波形を再現させてみた。入力クロック8MHz、システムクロックプリスケーラ、TCプリスケーラ共に無しの前提で、CTC、FastPWM、PhaseCorrectPWM の3モードを対象とした。
興味のある方は、https://okiraku-camera.tokyo/blog/wp-content/uploads/extra/pwmwave1.html を参照しソースを見てください。以下に示す図は、この投稿を書いている時点でのスクリーンショットです。今後変更するかもしれませんが。
横軸が時間(μsec)、縦軸がカウンター値で、斜めに立ち上がる黒線がカウンター値が時間と共に増加していく様子を表している。そして赤線がOC1Bのロジック出力を表現している。
青線は、OC1Aを表しており、OCR1Aとカウンター値が一致したとき(つまりTOP値になったとき)、OC1Aの出力を反転させている。青線と赤線が重なると分かりにくいので、ロジック1を表す高さを変えた。
PWMユニットの設定 (pwm_out(); )
以下のような考え方で、PWM用のモードビット(TCCR1A/B)を設定した(内部クロック8MHzを前提している)。
- TCNT1を、カウント値0から開始し26.3μsec後にTOP値に達するカウンターとして構成する。8MHzなので 26.3 ÷ 0.125 = 210 クロックでTOPに達するようにすればよい。0からカウントが始まるのでOCR1Aに209を設定し、OCR1AをTOP値として使うよう設定する。
- OC1Bには、(TOP値の次のクロックで)カウント値が0に更新されたときに1を出力させる。そして、OCR1Bの値がTCNT1のカウント値に一致したらOC1Bに0を出力させる。
- Hの期間(6.6μsec)に相当するカウント数は 6.6 ÷ 0.125 = 52.8 。したがって、OCR1Bには52をセットする。
- ここまでは8MHzクロックが前提。システムクロックプリスケーラで2分周するから、おのおのを2で割った値をレジスタに設定している。
具体的には以下のようにした。
- OCR1A = 209 / 2
- OCR1B = 52 / 2
- TCCR1A = B01100011
COM1A = 01 ( Toggle OC1A on Compare Match)、COM1B = 10 (Clear OC1B on Compare Match, set OC1B at BOTTOM)。
WGM11 = 1、WGM10 = 1。 - TCCR1B = B00011001
WGM13=1、WGM12=1。
CS = 001 (No Clock Prescaler)。 - WGM(13-11)=1111 。→ FastPWM Mode。
(各レジスタ内のモードビットの詳細については、20.14. Register Description を参照。)
必要な期間のPWM波生成が終わったら、OC1BとOC1AをPWMユニットから切り離し発生を止める。
リモコンとしてのデータ送信
今回ターゲットにしたリモコン(シャワートイレ)は、フレーム構成はNECフォーマットのようだがフレーム内のデータは3バイトというものだった。NECフォーマットのタイミングなので 1bitを送信するときに赤外光を点滅させる基準期間(T期間)は560μsecになる(フォーットの話は以前の投稿を参照)。
1bitの出力を行う send_bit() 関数や、そこから呼ばれる上記の pwm_out() 関数では、delayMicroseconds() を使ってT期間またはその倍数の遅延を置いている。スケッチ内では、あらかじめT期間をPRESCALE_RATIOで割った値を定数として定義しているので、個々の呼び出しには含まれていないことに注意。
void send_byte() では、1バイトのデータをLSBから順に赤外光の点滅に置き換える。そして、フレームの開始を表す send_leader() でもNECフォーマットに合わせた期間の波形の生成を行う。
void send_command(); で各キースイッチに対応したデータを送信している。スイッチーに対応したデータ自体は command_data[]; という配列に格納した。フレーム内の先頭は 0x3aで固定、2番めにスイッチに対応したデータ、3番目にビット反転したデータを送信する。
きょうのまとめ
思いつきでシステムクロックプリスケーラを使うことにし、なおかつ電池本数も試作時から減らしてしまった。電池がある程度消耗したあとでリセットしたら起動しない構成というのは、なかなかシロウト臭くていいんじゃなかろうか。電源電圧が2Vを割り込むまでは動いていて欲しいものなのだけど、ホントに動くのか半信半疑ではある。(追記:この投稿内容のままではダメでした。続きとして別稿に対策をまとめます)。
省電力化についてもうちょっとツメておきたいので、ロジックで電圧を変更できる電圧レギュレータや、マイコンに与えるクロックを自由に変更できる仕組みなどを用意しようかと思って考え始めた。そのうち掲載できると思います。
今回は表立った場所で使うものではないので、外観については考慮しなかったが、もうちょっとカッコよく作りたいものであります。3Dプリンターとか使っちゃうんでしょうかね。