Unoを使って赤外線リモコン
概要†
Arduino から赤外線リモコンでいろいろ動かしたい
やりたいこと:
- Arduino に赤外線受信機と赤外線LEDを接続する
- リモコンからのコマンドを赤外線受信機で読み取ってコマンドを覚えさせる
- LED からコマンドを送って、テレビ、電灯、その他をマイコンから制御する
目次†
開発環境を整える†
- amazon で以下を購入
- Arduino UNO (っぽいなにか)を購入
- Arduino UNO 用のアクリルケースを購入
- アクリルケースに不良があったので交換してもらう
- 秋月で以下を購入
- 940 nm の LED
- 赤外線リモコン用センサー
- Arduino UNO を USB で PC に接続
- デバイスマネージャーで [ポート(COM と LPT)] を見ながら繋ぐ
- USB-SERIAL CH340 (COM7) が増えたのを確認
- Windows 10 の Microsoft Store で Arduino IDE をインストール&起動
- [ツール]-[ボード] で Arduino UNO を選択
- [ツール]-[ボード] で COM7 を選択
- コードを何も変更せずに [スケッチ]-[マイコンボードに書き込む] する
- IDE に 「ボードへの書き込みが完了しました。」と表示され、ボード上の "L" とマークされた LED が点灯する
ここまでで開発環境が整ったことになる。
マイコン機能をどう使うか勉強する†
Arduino UNO 仕様†
ピンの上げ下げ†
13 番ピンがボード上の "L" とマークされた LED につながっている。
LANG:C #define LED_L 13 // オンボード LED void setup() { // put your setup code here, to run once: pinMode(LED_L, OUTPUT); digitalWrite(LED_L, HIGH); delay(1000); // 1000 ミリ秒 = 1 秒 digitalWrite(LED_L, LOW); } void loop() { // put your main code here, to run repeatedly: }
コードをこう変えて Ctrl+U を押したら、プログラムの書き込み終了後、1秒だけ LED が灯って消えた。
プログラムは ROM へ書き込まれているので、ボード上のタクトスイッチ(リセットボタン)を押すとまたプログラムが最初から実行されて、1秒だけ LED が点灯する。
繰り返し処理†
LANG:C #define LED_L 13 // オンボード LED void setup() { // put your setup code here, to run once: pinMode(LED_L, OUTPUT); } void loop() { // put your main code here, to run repeatedly: digitalWrite(LED_L, HIGH); delay(1000); // 1000 ミリ秒 = 1 秒 digitalWrite(LED_L, LOW); delay(1000); // 1000 ミリ秒 = 1 秒 }
こうすると、1秒ごとについたり消えたりする。
シリアル:ボードから PC へ†
LANG:C void setup() { // put your setup code here, to run once: Serial.begin(9600); // 9600 ボー Serial.println("Hello!"); } void loop() { // put your main code here, to run repeatedly: }
[ツール]-[シリアルモニタ] を見ると、ちゃんとメッセージが届いていることを確認できる。
シリアル:PC からボードへ†
arduino uno では int は 2 バイトだそうだ。
LANG:C #define LED_L 13 #define CHAR_EOL 10 #define SERIAL_BUFFER_LEN 256 void setup() { // put your setup code here, to run once: pinMode(LED_L , OUTPUT); // オンボード LED Serial.begin(9600); // 9600 ボー } volatile char serialBuffer[SERIAL_BUFFER_LEN]; volatile unsigned char serial_wp = 0; void serialEvent() { // loop() 呼出し後にシリアルデータが届いていたら呼ばれる char c = Serial.read(); if( c == CHAR_EOL ) { serialBuffer[serial_wp] = 0; serial_wp = 255; } else { serialBuffer[serial_wp] = c; serial_wp++; } } unsigned led = 0; void loop() { // put your main code here, to run repeatedly: if(serial_wp == 255){ // 1行届いた serial_wp = 0; led = !led; // LED を反転 digitalWrite(LED_L, led); } }
1行何か送るたびに LED がついたり消えたりする。
赤外線受信機をつなぐ†
ちょっと手抜きをして赤外線受信機の
- 電源(黄線で配線)を 8 番ピンに
- GND(紫線で配線)を 9 番ピンに
- 信号出力(赤線で配線)を 10 番ピンに
繋いだ。
LANG:C #define LED_L 13 // オンボード LED #define RECV_POW 4 // 赤外線受信機 電源 #define RECV_GND 3 // 赤外線受信機 GND #define RECV_SIG 2 // 赤外線受信機 信号出力 void setup() { // put your setup code here, to run once: pinMode(LED_L , OUTPUT); pinMode(RECV_GND, OUTPUT); digitalWrite(RECV_GND, LOW); pinMode(RECV_POW, OUTPUT); digitalWrite(RECV_POW, HIGH); } void loop() { // put your main code here, to run repeatedly: digitalWrite(LED_L, digitalRead(RECV_SIG)); }
赤外線リモコンを赤外線受信機に向けて適当なボタンを押すとボード上の LED が明滅することを確認できる。
赤外線受信機は 38~40kHz で明滅する波長 960 nm 程度の赤外線を受信したときに信号を LOW にする。
必要ないかもしれないけれど、このつなぎ方だと 10 番ピンを LOW にすることで赤外線受信機を off にできる。
外部割込み†
2番ピンと 3番ピン しか割り込み入力として利用できないらしいので注意が必要。
setup にて、
LANG:C attachInterrupt(digitalPinToInterrupt(RECV_SIG), receiverEvent, CHANGE);
とするだけで、RECV_SIG ピン (2番) が変更されたら receiverEvent が呼ばれるらしい。
http://elm-chan.org/docs/ir_format.html
によるとリモコンコマンドは大体 500us かその倍数ごとに 0 と 1 とが切り替わるらしい。
- unsigned long micro() で、プログラムが起動してからの時間をマイクロ秒単位で得られる
- マルチバイト整数の実装はリトルエンディアン
LANG:C #define LED_L 13 // オンボード LED #define RECV_POW 4 // 赤外線受信機 電源 #define RECV_GND 3 // 赤外線受信機 GND #define RECV_SIG 2 // 赤外線受信機 信号出力 #define CHAR_EOL 10 // 行末文字 = LF #define SERIAL_BUFFER_LEN 256 void setup() { // put your setup code here, to run once: pinMode(LED_L , OUTPUT); pinMode(RECV_GND, OUTPUT); digitalWrite(RECV_GND, LOW); pinMode(RECV_POW, OUTPUT); digitalWrite(RECV_POW, HIGH); Serial.begin(2000000); // 2Mbaud delay(1000); // これがないと変なデータを拾ってしまう // 割り込み処理を開始 attachInterrupt(digitalPinToInterrupt(RECV_SIG), receiverEvent, CHANGE); } // d (0 <= d < 16) を16進文字に直す char print_hex_sub(unsigned char d) { if(d < 10){ return '0' + d; } else { return ('A'-10) + d; } } // 1バイトを2桁の16進文字にしてシリアル出力 void print_hex(unsigned char d) { Serial.write(print_hex_sub(d >> 4)); Serial.write(print_hex_sub(d & 0x0f)); } // 直前に赤外線受信機の出力が変化した時刻 unsigned long receiver_last = 0; volatile unsigned int received_data[256]; unsigned char received_data_wp = 0; unsigned char received_data_rp = 0; // 赤外線受信機の出力が変化したら呼ばれる void receiverEvent() { unsigned long now = micros(); unsigned long receiver_delta = now - receiver_last; receiver_last = now; // 300us 以下はノイズとして読み飛ばす if (receiver_delta < 300) return; // 長すぎるのは丸める if (receiver_delta > 16384) // およそ 16ミリ秒 receiver_delta = 32767; // バッファに入れる received_data[received_data_wp] = receiver_delta; received_data_wp ++; } void loop() { // put your main code here, to run repeatedly: if(((received_data_rp - received_data_wp) & 0xff) == 0x01) { // buffer overflow Serial.print("!!!!"); received_data_rp = received_data_wp; } else if(received_data_rp != received_data_wp) { // キューにたまったデータを PC に送信する unsigned char *p = (unsigned char *)&received_data[received_data_rp++]; if(digitalRead(RECV_SIG)) { print_hex(p[1] | 0x80); } else { print_hex(p[1]); } print_hex(p[0]); Serial.println(""); } }
リモコンを赤外線受信機に向けてボタンを押すと、コマンドに応じた文字列をシリアルモニタで確認できるようになった。
リモコン出力†
https://miso-engine.hatenablog.com/entry/2015/07/20/221014
によると、
* Timer0 8bitのタイマーでArduinoの時間を管理する用途で利用されている。delay(), millis(), micros()などである。UNOでは5,6番ピンのPWMで利用されている。
* Timer1 16bitのタイマーでUNOではServoライブラリと9,10番ピンのPWMで利用されている。
* Timer2 8bitのタイマーでUNOではtone()と3,11番ピンのPWMで利用されている。
とのこと。リモコン受信で micros() を使っているので Timer0 は使えない。
Timer1 は 16bit あるおかげで 65,535us まで測れるから、 パルス長を測るのにちょうど良い。
すると必然的に Timer2 を 38~40 kHz のキャリアー波を出すための PWM に使うことになる。
3番は入力で使っているので 11番を出力に使おう。
Timer1 の設定†
ベースが 16M Hz なので、これを 1/64 に分周すると 4us 刻みのタイマーになる。
16ビットレジスタのアクセス
書き込み操作については、16ビットレジスタの上位バイトが下位バイトに先立って書かれなければなりません。そしてその上位バイトは一時レジスタに書かれます。16ビットレジスタの下位バイトが書かれる時に、同じクロック周期で一時レジスタが16ビットレジスタの上位バイトに複写されます。
読み込み操作については、16ビットレジスタの下位バイトが上位バイトに先立って読まれなければなりません。CPUによって下位バイトが読まれる時に、下位バイトが読まれるのと同じクロック周期で16ビットレジスタの上位バイトが一時レジスタに複写されます。その後に上位バイトが読まれる時は、この一時レジスタから読まれます。
使うレジスタ
TCCR1A, TCCR1B, TCCR1C, TCNT1H, TCNT1L, OCR1AH, OCR1AL, OCR1BH, OCR1BL, ICR1H, ICR1L, TIMSK1, TIFR1
https://avr.jp/user/DS/PDF/mega328P.pdf#page=80
- OCR1A = マイクロ秒/4 - 1 : TOP値として使う
- CS1 = 0x011 : 1/64 分周
- TCCR1A[7:6] = COM1A = 0b00
- TCCR1A[5:4] = COM1B = 0b00
- TCCR1A[1:0] = WGM1 = 0b0100 : 比較一致タイマ/カウンタ解除(CTC)動作(TOP=OCR1A)
- TIMSK1[1] = OCIE1A = 1 : CTC-A 割り込み許可
https://qiita.com/masayoko/items/1650fd33a362de61d13e
のようにすればいいみたい。
LANG:c #define LED_L 13 // オンボード LED void setup() { // put your setup code here, to run once: pinMode(LED_L, OUTPUT); noInterrupts(); // COM1A = 00 : OC1A切断 // COM1B = 00 : OC1B切断 // WGM1[1:0] = 0100 : CTC TCCR1A = (B00 << 6) | (B00 << 4) | (B00 << 0); // WGM1[3:2] = 0100 : CTC // CS1 = 101 : 16 MHz / 1024 (約 16 kHz) TCCR1B = (B01 << 3) | (B101 << 0); // カウンタ初期値 TCNT1 = 0; // カウンタ TOP OCR1A = 16000; // 約1秒おきに割り込みを発生させる TIMSK1 = bit(OCIE1A); // タイマー1A の割り込みを許可 interrupts(); } ISR (TIMER1_COMPA_vect) { // 割り込み処理 // LED 出力をトグルする static int led_sw = 0; digitalWrite(LED_L, led_sw); led_sw = !led_sw; } void loop() { // put your main code here, to run repeatedly: }
としてみたところ、ちゃんと1秒おきに LED が付いたり消えたりした。
imer2 の設定†
https://avr.jp/user/DS/PDF/mega328P.pdf#page=101
ベースが 16M Hz なので、これを 1/(8 * 52) = 1/416 に分周すると 38.46 kHz となる。
使うレジスタ
TCCR2A, TCCR2B, TCNT2, OCR2A, OCR2B, TIMSK2, TIFR2
https://avr.jp/user/DS/PDF/mega328P.pdf#page=111
- TCCR2B[2:0] = CS2 = 0b010 : 8分周
- {TCCR2B[3], TCCR2A[1:0]} = WGM2 = 0b111 : TOP を OCR2A で指定 (BOTTOM はゼロ固定?)
- TCCR2A[7:6] = COM2A = 0b00 : 使わないので何でもいい
- TCCR2A[5:4] = COM2B = 0b11 : OC2B は TCNT2 > OCR2B で 1 になる
- OCR2A = 52 - 1 = 51
- OCR2B = 26 - 1 とすると PWM 出力、51 以上にするとゼロを出力
あれ? OC2B を使いたいなら出力は3番に出るのか。
ピン配置を換えないと。
方針†
- 普段 OCR2B = 52 としておくと赤外線は出力されない
- OCR2B = 25 とすると赤外線が 38.46 kHz で断続的に出力される
- アイドル時には 10 ms 間隔で割り込みを受ける(OCR1A を 10000 >> 2 に設定)
- シリアルから送られてくる16進数はバッファーに貯める
- タイマー1の割り込み処理ルーチンにて
- バッファーに貯まった値があれば
- 最上位ビットの値で赤外線出力を切り替える
- OCR1A を送られてきた値で設定
- バッファーに貯まった値がなければ
- 最上位ビットの値で赤外線出力をoff
- OCR1A を 10000 >> 2 に設定
- バッファーに貯まった値があれば
PC 側の制御ソフトウェアを作る†
- Visual Studio を入れる
- Windows Forms アプリケーション (.Net Framework) を作成
https://qiita.com/mag2/items/d15bc3c9d66ce0c8f6b1
を参考に。
シリアルで受信する†
- フォームにテキストボックスを1つ置いて複数行モードにする
- serialPort コンポーネントを1つ置いて DataReceived を割り当てる
- 受信機の出力は普段 HIGH で、コマンドの初めでリーダーとして LOW が出るので、 コマンドの初めは長い時間がたってから LOW になったことを表す 7FFF から始まることになる。
LANG:C# using System.IO.Ports; namespace ircmd { public partial class Form1 : Form { public Form1() { InitializeComponent(); serialPort1.BaudRate = 2000000; serialPort1.Parity = Parity.None; serialPort1.DataBits = 8; serialPort1.StopBits = StopBits.One; serialPort1.Handshake = Handshake.None; serialPort1.PortName = "COM7"; serialPort1.Open(); } delegate void SetTextCallback(string text); private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e) { string str = ""; while (serialPort1.BytesToRead != 0) { string s = serialPort1.ReadLine(); if (s == "7FFF\r") { str += "\r\n"; } else { str += s + " "; } } // メインスレッド上で AppendText を呼び出す Invoke( new SetTextCallback((s) => textBox1.AppendText(s)), new object[] { str } ); } } }
これでデータを受信できた。