Unoを使って赤外線リモコン の履歴(No.8)
更新概要†
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 の設定に使うレジスタ
TCCR1A, TCCR1B, TCCR1C, TCNT1H, TCNT1L, OCR1AH, OCR1AL, OCR1BH, OCR1BL, ICR1H, ICR1L, TIMSK1, TIFR1
Timer2 の設定に使うレジスタ
TCCR2A, TCCR2B, TCNT2, OCR2A, OCR2B, TIMSK2, TIFR2
ベースが 16M Hz なので、これを 1/(8 * 52) = 1/416 に分周すると 38.46 kHz となる。
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 } ); } } }
これでデータを受信できた。