概要
居間で使っているエアコン用のリモコンの、一番肝心な「開始」ボタンの反応が悪くなった。ボタンが劣化したかゴミが溜まったものと思われる。分解して具合を見たり掃除したりしようと思ったものの、ビスが見当たらない。角にツメを立てて開けようとしたが、ちょっと力を入れるとバキッと壊れそうである。しょうがないので取り急ぎ赤外線リモコンを作ることにした。
このエアコンを操作するための機能はちょっと前に作ったTFT液晶を使ったリモコンでもやっているから、特に技術的な課題はない。以下のように目標設定した。
- テーブルに置いても邪魔にならない程度の大きさ
- 手のひらに載せたり握った状態で使える
- 機能は最小限
- 数日で作れること
ということで、3日間ほどで完成したのがこちら。
4つで100円で売っていた小型のタッパーを使った。
TFT液晶を使ったリモコンについてはブレッドボード上の試作ではうまくいったものの、小さな液晶を見ながらスタイラスペンで操作するのは(はじめから分かってはいたものの)実用的ではなかった。手でリモコンを握った状態で、対象物(エアコンとかテレビとか)にIrLEDの発光部を向け、対象物を見ながら操作するのが自然だろう。IrLEDは指向性が強いので、テーブルに置いた状態で使えるようにすることも厄介だった。
構成の概要
ハードウェア/ソフトウェア共に以前に作ったシャワートイレ用の赤外線リモコンとほとんど同じ構成とした。
- 液晶などの表示装置は無し。キー操作とエアコンの反応だけで使えるようなリモコンとする。
- Arduino PRO MINI 3.3V/8MHz動作版を使う。ただ、BODLEVEL FUSEは1.8Vに変更(FUSEの変更についてはこちらを参照)し、2.7Vでリセットがかからなくする。
- 電源は単4電池2本、電圧レギュレータ無し(3.3~2.0Vの範囲での動作を想定)。電池はアルカリを考えているが、NiMH充電池でも動くだろう。
- IrLED(OSI5LA5113A)は1本とし、3.3V時に最大で90mA程度流すことにする。
- キーマトリックスは3×2として6キー。運転の開始と終了の他、温度と風量を変更できるようにする。滅多にやらない暖房と冷房の切換は、基板にスライドスイッチで付けることで対応する。ドライ(除湿)運転はとりあえずあきらめた。
- 電池の消耗を最小限とするため、マイコンは基本的にスリープ状態(SLEEP_MODE_PWR_DOWN)とする。いずれかのキーの押下によってスリープ状態から復帰し、対応するコードを赤外線を使って発射し次第スリープに戻る。省電力のために、いろいろやる。
- ATmega328Pに外部8MHzクロックを与える場合、2.7V以下での動作は運次第と思われる。リセット時にATmega328Pのシステムクロックプリスケーラを使って内部クロックを4MHzとすることで、+1.8Vまで動いてくれることを期待している。基本的に電池を外すまでリセットはかからない。
回路
回路も以前とあまり変わらない。キーの数が増えたこととスライドスイッチを着けたくらいか。
マイコン周り
例によって、Arduino PRO MINI (The SIMPLE) に載っている定電圧レギュレータとパワーオンLEDはハンダゴテをあててはずした。写真は後半で。
IrLED周り
IrLEDを駆動するトランジスタ周辺の抵抗(R1とR2)は、電源電圧が3.3V時にIrLEDに流れる電流(IF)が、トランジスタやIrLEDの絶対最大定格(100mA)を超えない程度の値とする。R1=22Ωとすることで、90mA(@3.3V)、32mA(@2.0V)となるはず。
R2については、2.0V時にトランジスタのベース電流を2mA以上とするために680Ωとした。これは、コレクタ-エミッタ間に生じるの電位差をなるべく小さくするために、トランジスタを飽和領域で使うことを意図した。
電源電圧が2.0Vまで低下したときにエアコンの操作ができるのかどうかについては、しばらく使ってるうちに分かるだろう。+1.8Vになっても赤外線信号が正しく発射されることはArduino nanoを使った受光用の仕掛けを使って確認した。
キーマトリックスとワイヤードOR回路
回路図のようなキーマトリックスを組む場合、スイッチがオンになったことを検出するためにデータ側(D6,D7,D8)のラインにプルアップ抵抗を付けるのが普通なのだけど、部品点数を減らすためにATmega328Pの内部プルアップ抵抗で代用した。pinMode(port, INPUT_PULLUP); としておくことで、約30KΩのプルアップ抵抗を付けるのと同じことになる (抵抗値についは諸説ある?)。
スイッチングダイオード(1SS270)を3本使って、キーマトリックスのデータ側をワイヤードORしている。これは、いずれかのキーが押されたとき、ポートD2(INT0)にHレベルの信号を生じさせ、スリープから復帰するための割込み源とするためである。
スリープに入る直前にマトリックスのスキャン側(D4,D5)を pinMode(port, INPUT_PULLUP); として内部プルアップ抵抗に接続しておくことで、いずれかのキーが押されるとダイオードのアノード側に電圧が生じてD2はHレベルとなる。全キーオフならば100KΩのプルダウン抵抗によりLレベルが維持される。
スライドスイッチ(SW7)は、暖房とクーラーの切換え用。10KΩでプルアップしたH側(暖房)とGNDに直結するC側(クーラー)のいずれかがCOM(Common)に常時接続されるので、スリープから復帰するたびごとに、その値をD3ポートで読むことで運転モードを決める。
製作
自分の場合、ブレッドボード上での電子工作遊びではなくて実際に使うものを作るときに一番悩むのはハコ、入れ物。アルミケースでは冷たいし、立派なプラケースでは加工が面倒である。そのため、100円ショップの台所用品やコスメ用品(オシャレっぽいケース)のコーナーをウロツイて物色することが多い。今回は最初の写真のような、4つで100円(税抜き)の小さくてチープなタッパーを使うことにした。寸法は90(W)×65(D)×40(H)mmで、抵抗やコンデンサなどの素材を整理しておくのにもちょうどいい。
このタッパーのフタと、秋月電子で売っているユニバーサル基板(72×47mm 両面スルーホール、ガラスコンポジット)の大きさがうまくマッチする。タッパーのフタに基板をネジ止めし、タッパー本体側には電池ボックスを入れることにした。
ユニバーサル基板に空いているネジ穴にあわせてフタの四隅にドリルで穴を開け、基板に並べたキースイッチ(やはり秋月のタクトスイッチ(大)10個セット)にあわせてフタの内側を切り抜いた。
ブレッドボードで試作するのも面倒だったのですぐに基板にハンダ付けした。表側はスイッチと抵抗、ダイオードなどを取り付けた。左側の余白の裏側に、Arduino PRO MINIを直付けしている。
基板は小型の凸型スペーサー(高さ5mm)を使ってタッパーのフタに固定している。5mmしかないので、基板の厚みをはさまないとプラネジが締まらなかった。
タッパーのフタと基板の間の隙間を小さくできるように、Arduino PRO MINIやトランジスタ、IrLEDは裏側に取り付けている。IrLEDのアノード側とトランジスタのエミッタに接続するVCCとGNDについては、ロジック用とは異なるちょっと太めのケーブルを使った。
電池ボックスは両面テープでタッパー本体の底部に接着していて、電池ボックスからのリード線はPRO MINIのVCCとGNDに直結している。上の写真のように、PRO MINIにもともと載っていたPON LEDの直列抵抗と電圧レギュレータは省電力のために除去している。
そのあたりを外してしまうなら、Arduino PRO MINIではなく素のATmega328Pを使うことも考えられるが、この状態でもVCC-GND間のバイパスコンデンサやリセットスイッチ、さらにD13に接続されているLEDが生きている。また、表面実装タイプのマイコンをうまく扱う自信もない。しかも、Arduino PRO MINI (The Simple)は300円程度で入手できるわけだから、このやり方が一番いいように感じている。
現在も写真に登場する単4サイズのNiMH充電池を利用しているが、フル充電状態で2.65V程度の電源電圧だった。この電源電圧では、BODLEVEL FUSEの書き換えを行わなければ起動もできない。ATmega328Pは動作電圧範囲も広く、消費電力も小さくて小規模な電子工作にはとても使いやすい。
スケッチの投入や初期のテストは、USB-シリアル変換用のAE-FT231Xから、PRO MINIのシリアル接続用のポートにジャンパケーブルを接続しておこなった。このときは、電池を抜いてUSB側から電源(+3.3V)を供給している。シリアル接続用のピンヘッダをつけたままでもフタが閉まるのがミソ。
スケッチ
スケッチは以下のとおり。
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 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
#include <avr/sleep.h> #include <avr/wdt.h> #include <avr/pgmspace.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_123 4 #define SCAN_456 5 #define KEY_14 6 #define KEY_25 7 #define KEY_36 8 #define SW7 3 // スライドスイッチのCOMMピンを接続。 #define TRIGGER_PIN 12 // PWM波観測時のトリガ用 // --------------------- KEY-PAD related. // setup KEY related ports void setup_port(bool sleep) { if (sleep) { pinMode(SCAN_123, INPUT_PULLUP); pinMode(SCAN_456, INPUT_PULLUP); pinMode(KEY_14, INPUT); pinMode(KEY_25, INPUT); pinMode(KEY_36, INPUT); } else { pinMode(SCAN_123, OUTPUT); pinMode(SCAN_456, OUTPUT); pinMode(KEY_14, INPUT_PULLUP); pinMode(KEY_25, INPUT_PULLUP); pinMode(KEY_36, INPUT_PULLUP); } } int scan() { int n = 0; digitalWrite(SCAN_123, 0); digitalWrite(SCAN_456, 1); PDELAY(1); n |= digitalRead(KEY_14); n |= (digitalRead(KEY_25) << 1); n |= (digitalRead(KEY_36) << 2); digitalWrite(SCAN_123, 1); digitalWrite(SCAN_456, 0); PDELAY(1); n |= (digitalRead(KEY_14) << 3); n |= (digitalRead(KEY_25) << 4); n |= (digitalRead(KEY_36) << 5); digitalWrite(SCAN_123, 0); return n; } int get_key() { int scan_result = 0x3f; 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; if (scan_result & 16) return 5; if (scan_result & 32) return 6; return 0; } //------------------ IR related. #define NEC_T (int)(560 / PRESCALE_RATIO) #define AEHA_T (int)(420 / PRESCALE_RATIO) int format_T = AEHA_T; // 基準フレーム時間 // system clk = 8MHz. (no prescaler) // 38KHz, duty 1:3 // 26.3<s 7 0.125 = 210 : 周期 // 6.3<s 7 0.125 = 52.8 : H期間 // FastPWM mode. WGM(13-11)=1111 void pwm_out(int t_count) { TCNT1 = 0; 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; } 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); PDELAY(10); } void send_frame(uint8_t* p, int8_t bytes) { send_leader(); for(int i = 0; i < bytes; i++) send_byte(p[i]); send_trailer(); } #define AC_DRIVE_STOP 0 #define AC_DRIVE_COOL 1 #define AC_DRIVE_HEAT 2 #define AC_DRIVE_DRY 3 #define AC_TEMP_H_DEF 23 #define AC_TEMP_C_DEF 25 #define AC_INTENSITY_MAX 5 #define AC_INTENSITY_MIN 2 #define AC_INTENSITY_AUTO 1 #define AC_HDIR_MAX 5 #define AC_HDIR_MIDDLE 3 #define AC_HDIR_MIN 1 #define AC_VDIR_UP 1 #define AC_VDIR_MIDDLE 2 #define AC_VDIR_DOWN 3 // エアコンの動作状態 static int8_t ac_drive = 0; // 0:停止、1:冷房、2:暖房、3:ドライ static int8_t ac_temp = AC_TEMP_H_DEF; static int8_t ac_temp_max; static int8_t ac_temp_min; static int8_t ac_wind_intensity = AC_INTENSITY_MAX; // 風量、1~5 1 = AUTO. static int8_t ac_h_dir = AC_HDIR_MAX; // 水平方向、左 1 -- 3 -- 5 右 static int8_t ac_v_dir = AC_VDIR_DOWN; // 1 : 水平向き、2 : 中間、3 : 下向き // 0xffの部分は設定に応じて変更される。 const PROGMEM uint8_t ac_sequence1[] = {2, 0x20, 0xe0, 4, 0, 0, 6 }; const PROGMEM uint8_t ac_sequence2[] = {2, 0x20, 0xe0, 4, 0, 0xff, 0xff, 0x80, 0xff, 0xf, 0, 0xe, 0xe0, 0, 0, 0x81, 0, 0, 0xff} ; // 第2フレームの作成。 void setup_ac_frame(uint8_t* p, int8_t bytes) { uint8_t val = 0; if (ac_drive == AC_DRIVE_COOL) val = 0x39; else if (ac_drive == AC_DRIVE_HEAT) val = 0x49; else if (ac_drive == AC_DRIVE_DRY) val = 0x29; p[5] = val; if (ac_drive == AC_DRIVE_DRY) val = 0xc0 | (((ac_temp - 8) & 0xf) << 1); else val = 0x20 | (((ac_temp - 16) << 1) & 0x1e); p[6] = val; // 設定温度 val = ac_wind_intensity == 1 ? 0xa0 : (ac_wind_intensity + 1) << 4; if (ac_wind_intensity == AC_INTENSITY_MAX) val |= 0x10; if (ac_v_dir == AC_VDIR_DOWN) val |= 3; else if (ac_v_dir == AC_VDIR_UP) val |= 1; else if (ac_v_dir == AC_VDIR_DOWN) val |= 5; p[8] = val; // 上下風向と風量 if (ac_h_dir == AC_HDIR_MIDDLE) val = 6; else if (ac_h_dir < AC_HDIR_MIDDLE) val= ac_h_dir + 8; else val = ac_h_dir + 7; p[9] = val; // 左右風向 p[13] = 0x00; // オプション val = 0; for(int8_t i = 0; i < bytes -1; i++) val += p[i]; p[18] = val; // チェックサム } void send_command(int key) { uint8_t data[20]; // 最大20バイトまで。 int8_t data_count; if (key != 1 && key != 4 && ac_drive == AC_DRIVE_STOP) // 動作中でなければ無視。 return; data_count = sizeof(ac_sequence1) ; memcpy_P(data, ac_sequence1, data_count); send_frame(data, data_count); data_count = sizeof(ac_sequence2) ; memcpy_P(data, ac_sequence2, data_count); if (key == 4) { // 左上のキーをオンとする。 // オンのときに常に初期値を設定する。 if (!digitalRead(SW7)) { // スライドスイッチを読む ac_drive = AC_DRIVE_COOL; ac_temp = AC_TEMP_C_DEF; } else { ac_drive = AC_DRIVE_HEAT; ac_temp = AC_TEMP_H_DEF; } ac_temp_max = ac_temp + 3; ac_temp_min = ac_temp - 3; ac_wind_intensity = AC_INTENSITY_MAX; } else if (key == 1) ac_drive = AC_DRIVE_STOP; else if (key == 5 && ac_temp < ac_temp_max) // 左側のキー2つで温度 ac_temp++; else if (key == 6 && ac_temp > ac_temp_min) ac_temp--; else if (key == 2 && ac_wind_intensity < AC_INTENSITY_MAX) // 右側のキー2つで風量 ac_wind_intensity++; else if (key == 3 && ac_wind_intensity > AC_INTENSITY_MIN) ac_wind_intensity--; // 風量自動は無し。 setup_ac_frame(data, data_count); send_frame(data, data_count); } void setup() { // Serial.begin(115200); // プリスケーラで1/2してるので、端末側は57600bpsとなる。 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(4); attachInterrupt(0, isr, RISING ); set_sleep_mode(SLEEP_MODE_PWR_DOWN); sleep_enable(); MCUCR |= 0x60; MCUCR = (MCUCR & 0xdf) | 0x40; sleep_cpu(); detachInterrupt(0); digitalWrite(LED, 1); setup_port(false); int n = get_key(); // Serial.println(n); PDELAY(20); if ( n >=1 && n <= 6) send_command( n ); digitalWrite(LED, 0); } |
スケッチも以前のリモコンとさほど変わらない。スキャンするキー数が6つに増えたことと、AEHAフォーマットを使っていることくらいか。
デューティー比1:4の38KHzの赤外線発光は、ATmega328PのTC1(Timer Counter1)のPWM機能を使って行っている。システムクロックプリスケーラを使って4MHzクロック動作としているので、PWM用のレジスタに与えるカウント値を分周比(PRESCALE_RATIO == 2)で割っていることに注意。
エアコンの制御データ
家電を制御するための赤外線リモコンが送出するコードは各家電ごとに異なっている。うちのエアコンの詳細を書いても他で使えるわけでもないので、特徴的な部分だけ挙げると以下のような感じか。
- AEHAフォーマットで2フレーム構成。
- 第一フレームのデータは(調べた限りにおいて)常に固定。
- 第二フレームが運転状態(運転モード、温度、風量、風向など)を表す。
- 第一、第二フレーム共に最終バイトはチェックサム(フレーム全体の和の下位8ビット)。
- 温度や風量のみを変更した場合も、リモコンが知っている動作状態すべてが送出される。
今回のリモコンでは、運転開始ボタン(SW4)を押したときにスライドスイッチを読んで運転モードを決める。そして、各運転モードごとにスケッチで決めた温度、風量、風向を初期値として設定することにした(風向は変更できない)。本物のリモコンでは、リモコンの液晶に表示されている温度、風量、風向を送り出すようになっている。今回のリモコンでも各状態は変数に記憶しているので、前回運転時の温度や風量で開始することはできるが、わかり易さのために初期値を与えることにした。
詳細については、setup_ac_frame(); send_command(); の各関数を参照のこと。AEHAフォーマット用のフレームデータの送出は、send_frame(); 関数で行っている。フレーム終端を作る send_trailer(); において10msecの無信号期間をおいているが、これがないとうまく動かなかった。
各フレームの基本となるデータは、
1 2 |
const PROGMEM uint8_t ac_sequence1[] = {2, 0x20, 0xe0, 4, 0, 0, 6 }; const PROGMEM uint8_t ac_sequence2[] = {2, 0x20, 0xe0, 4, 0, 0xff, 0xff, 0x80, 0xff, 0xf, 0, 0xe, 0xe0, 0, 0, 0x81, 0, 0, 0xff} ; |
としてフラッシュメモリにおいている。まぁ、ほとんどデータのないスケッチなのでふつうにSRAMに置いても良かったのだけど、PROGMEMを使いたかったから、といったところ。
なお、Arduino IDEでビルドすると以下のように表示された。
1 2 |
最大30720バイトのフラッシュメモリのうち、スケッチが2696バイト(8%)を使っています。 最大2048バイトのRAMのうち、グローバル変数が18バイト(0%)を使っていて、ローカル変数で2030バイト使うことができます。 |
容量的にはもっと小規模なマイコンでも十分だろう。
きょうのまとめ
利用開始時点で電源電圧は2.65Vだった。IrLEDを正確にエアコンに向けた場合、約5mほどの距離から操作することができる。3m程度で操作できれば実用的なので、もうすこし電池が消耗しても大丈夫だろう。今後の知見のため、実用的な操作ができなくなったときの電源電圧を測るのが楽しみでもある。
この手のリモコンを作ったのは二度目なので、前よりは手早くできたような気がする。ただ、目が悪くなって小さなもののハンダ付けは厄介なので、テレビCMでもおなじみのメガネ型ルーペを買うかどうか迷っている。
TFT液晶を使った集約型リモコンの発展形を考えると、どうしてもスマホで操作するところに行き着く。赤外線発光部は目立たない場所に固定しておき、Bluetoothで発光すべきデータを送りつけてやることになるだろう。ただ、画面タッチで操作するのではつまらないので、スマホをインターネットにつないで音声認識させ、その結果を解釈してBTでコマンドを送るのはどうかと考えているところ。