「#3-9 キッチンタイマを作る」の回では、主に、赤外線リモコンの受信側をプログラムしました。今回は、送信する側を作ってみます。
筆者が購入した、スタータキットには、リモコン送信機と、受信モジュールが含まれていたのですが、送信に必要な赤外線LED(単体、以下IR LED)は含まれていませんでした。まずは、IR LEDの調達からスタートです。
幸い、筆者の手元には、壊れて使えなくなったリモコンがいくつかあります。「使えなくなった」のに、何で捨てずに持っているの?って、ことでしょうが……、貧乏性なもので(1^^;)。「使えなくなった」とは言えど、大抵は、キーの接触(?)部分が劣化して、「ギュっ」と強く押し続けると、何回かに1回くらいは送信できるような、そんな壊れ方です。
つまり、IR LEDが壊れて発光しなくなったわけでは無く、キーの部分が反応しなくなったものがほとんど。そのような状態なら、IR LEDは使えるはずです。ここからIR LEDだけを頂戴して、実験に使いたいと思います。
念のため、IR LEDがちゃんと発光していることを確認してから、取り外して使います。赤外線は目では見えませんが、撮像素子にCMOSセンサを使ったデジカメだと、光っていることを確認できます。
上図、左側はCMOSセンサを使っていると思われるスマホのカメラで撮影した、IR LEDが光っている様子です。右側も同様に撮影したのですが、肉眼と同じで発光を確認できません。右側は、撮像素子にCCDを採用したデジカメで撮影した画像です。IR LEDの発光を確認する時には、CMOSセンサ搭載のデジカメをご用意ください。
※どうなんでしょうね?人の目に見えない光が撮影できてしまうデジカメって?人の見た映像と違って写っているかもしれないわけですよね?←お陰で、肉眼では見ることのできないIR LEDの発光を確認できているので、文句を言う筋ではない……(1^^;)。
「使えなくなった」リモコンの中で、IR LEDが光ることを確認できたものから、IR LEDを取り外して使います。足が長く残っていれば、足の根元からニッパーで切り取れば良いですが、足が短めにはんだ付けされている場合には、はんだごてを使って足のはんだを溶かしながら引き抜きます。2本同時にはんだを溶かさないとうまく外れませんし、あまり長い時間はんだごてを当て続けていると、熱がIR LEDのダイ(die)まで伝わって、熱で破壊してしまいかねないので、慎重に、手早く、取り外します。
IR LEDを取り外したリモコン送信機の回路を確認しておきましょう。リモコンなんて、そんなにお金(原価)をかけられない付属品ですから、大抵は1層基板です。回路を追いかけるのはそれほど難しくないはずです。筆者が分解したリモコンも1層基板でした。そこから起こした、IR LED駆動周りの回路が以下図です(左・右で二台分)。
一つ言い訳しておくと、確かに1層基板なので配線は追えたのですが、トランジスタが良くない。SOTタイプのパッケージだと、足の並びが「C-E-B」のものと「C-B-E」の物が存在していて、型番がわからないとどっちかわからないのです。SOTパッケージだと、マークは精々3〜4文字なので、そこから型番を特定するのは無理でした。よって、「きっとこうだろう」という、筆者の予想で回路図を起こしています。
送信機2個分の回路図が左・右に記載してあります。両方とも、乾電池2本直列で動作するタイプなので、電源電圧は3Vです。
ちょっと不安なのは、IR LEDと直列に入っている、電流制限抵抗(1Ω)が小さすぎること。
IR LEDのVf=1.35V(TYP)、NPNトランジスタのVce(SAT)=0.5Vとすると、電流制限抵抗にはVr=3V−Vf−Vce(SAT)の電圧がかかります。抵抗値が1Ωですので、流れる電流If=Vr/1Ω=1.15Aとなり、ちょっと多すぎ……。
一般的なIR LEDのIf(max)を調べたところ、大概のIR LEDでは、If(max)=100mA程度となっており、やっぱり流し過ぎ。何か、筆者に見えていない要因がありそう……。
※乾電池の内部抵抗も疑ってみましたが、精々、0.5Ω程度だったので、2個直列でも1Ωにしかならず、よって、If=575mAとなるけど、それでもまだ流しすぎ……。
困ったことにIR LEDの場合、通常の表示用(可視光)LEDと異なり、「ただ光れば良い」と言う物ではなく、ある程度の強度で光らないと、信号の到達距離が短くなってしまうという問題があります。
不明点をそのまま放置し、「元回路と同等の電流が流れる」様に実験回路を作ることにして、先に進んでしまいました(この考えが甘かった)……。
一応、スマホのカメラ(CMOSセンサ)でIR LEDの発光を確認し、信号を送信してみたのですが、うまく受信してもらえなくて、原因を調べようとしていた矢先のこと……。IR LEDが全く光らなくなってしまいました。ブレッドボード上に構築した回路なので、どこかの接触が悪くなったとか、どこかのジャンパ線が外れたとか、そういうことを疑ってデバッグしたのですが、悪いところは見つからず。
そこで、IR LEDの代わりに、普通の可視光LEDをつけてみたところ、光りました。どうやら、IR LEDが壊れてしまった模様。
やってしまった……orz。やはり、電流を流し過ぎたのでしょう。何か、筆者に見えていないパラメータがどこかに潜んでいたものと考えられます。でも、何が考えから抜けているのか、わかりませぬ。困りました、いくら貧乏性で物持ちが良いと言っても、あと1個くらいしか、IR LEDを調達可能な「使えなくなった」リモコン送信機は手元にありません。これ以上壊してしまったら、実験を継続できなくなります。
こうなったら基本通り、スペックに従って設計しろってことですよね。IR LEDのIf(max)=100mAを前提として、回路を設計してみます。
乾電池ではなく、Arduinoの5V電源(Vcc)を使うので、Vr=R×If=Vcc−Vf−Vce(SAT)より、R≒31.5Ωです。もう壊してしまうわけにはいかないので、マージン確保と、用意可能な抵抗値の2点から、R=50Ω(100Ω抵抗の2並列)としました。
「100Ω抵抗の2並列」は、結構重要です。
いろいろなサイトで、「抵抗を定格ギリギリで使うと結構発熱するので、定格の半分くらいで使った方が良い」と推奨されています。この前提だと、1/4[W]抵抗は250/2[mW]=125mW以下で使うのが良いのでしょうが、198mWでは超過です(定格内ではあるけど)。
しかし、2並列なら、各々の抵抗に流れる電流は1/2になるので、抵抗1つ当たりの消費電力は198/2[mW]=99mWとなり、定格に対して2倍以上のマージンをもって使用することができます。
これでようやく先に進むことができます。赤外線リモコンに使うIR LEDは、周波数が38kHzで、デューティが1/3の波形で光らせる必要があります。
でも、この波形は、そう簡単には作れません。38kHzなので、1周期は1/38kHz≒26.3μs、デューティ1/3と言うことはHIGH幅が26.3μs/3≒8.8μsです。さすがに、ソフトウェアでポートを叩いて作り出すには、速すぎます。analogWrite()の様に、ハードウェアで、MCUに内蔵されたタイマユニットを使って、発生させる必要があります。
しかし、analogWrite()の周波数は、490Hzまたは980Hzに固定されているので、所望の38kHzとは異なります。
と、言うことで、ここから先は、Arduinoのライブラリに頼らず、ATmega328に搭載されているタイマユニットを直接制御して、所望の波形を出力してみます。
ATmega328には、3種類のタイマユニットが搭載されています。
この中で、PWM出力ができて、その周波数とデューティの両方が制御可能だった、16ビットタイマ(TC1)を使用することにしました。TC1のPWMは、「#3-3 I2C接続 LCD表示モジュール」で示したピンアサイン表で、OC1A,OC1Bに出力されます(Arduino UNOのピン番号でD9,D10)。まずは、1本出力できれば良いので、OC1A(D9)を使おうと思います。
TC1のI/Oレジスタを設定します。波形モードは、周波数とデューティの両方を制御可能な、「Phase and Frequency Correct PWM Mode」と言うやつを使うことにしました。このモードでは、タイマカウンタ(TCNT1)が、0から順にカウントアップして行き、上限値に到達すると今度はカウントダウンを始めます。そのまま0までカウントダウンすると、またカウントアップ……、を繰り返すようになっています。
上限値はインプットキャプチャレジスタ(ICR1)で指定可能であり、周期は2×ICR1となります。PWM出力タイミングは、アウトプットコンペアマッチレジスタ(OCR1A)で指定し、カウントアップ中のOCR1Aマッチで出力クリア、カウントダウン中のOCR1Aマッチで出力セットという波形出力モードを設定しておくと、HIGHパルスの幅を2×OCR1Aに設定することができます。
以上まとめると、以下図となります。
カウントアップクロックは、clkio/1,clkio/8,clkio/64,clkio/256,clkio/1024の中から、所望の周波数とデューティを、所望の精度で実現可能な周波数を選択します(clkio=16MHzです)。analogWrite()で使用しているクロックはclkio/64ですが、今回出力しようとする38kHzのPWMは結構速いので、clkio/64では高い精度で波形を出力できません。今回は一番高速なクロックclkio/1を使うことにします。
※消費電力を抑えたいアプリケーションの場合は、clkio/8を選択するのが吉です。その場合、ICR1=26(または27)で38.5(または37.0)kHzの周波数となります。正確な仕様がわかりませんが、たぶん許容範囲。
設定すべきI/Oレジスタは以下の通りとなります(アドレスはATmega328のメモリマップドI/Oレジスタアドレス)。
レジスタ名 | アドレス | 設定値 | 説明 |
TCCR1A | 0x0080 | 0x80 |
波形モード:Phase and Frequency Correct PWM Mode 出力モード:Clear OC1A on Compare Match when up-counting. Set OC1A on Compare Match when down-counting. |
TCCR1B | 0x0081 | 0x11 |
波形モード:Phase and Frequency Correct PWM Mode クロック選択:clkio/1 |
ICR1 | 0x0086 | 0x00d3 | 周波数:38kHz (0x00d3から逆算すると37.9kHz) |
OCR1A | 0x0088 | 0x0046 |
デューティ:1/3 OCR1A=0に設定することでデューティ0となり、信号出力を 停止(LOW出力)できる。 |
Cでの実装は以下の通りです。
inline void ir_on (void)
{
noInterrupts ();
OCR1A=0x0046; // 1/3 duty
interrupts ();
}
inline void ir_off (void)
{
noInterrupts ();
OCR1A=0; // duty, 0=stop pwm
interrupts ();
}
void setup (void)
{
// set TC1 (16 bits timer) for 38kHz 1/3 duty PWM
TCCR1A=0x80;
TCCR1B=0x11;
noInterrupts ();
ICR1=0x00d3; // 38kHz
OCR1A=0; // duty, 0=stop pwm
interrupts ();
}
setup()での初期化では、OCR1Aに0を書き込んでいますが、IR LEDを発光させる時には、上表で示した値をOCR1Aにセットします(ir_on()関数)。
これらのレジスタの内、ICR1とOCR1Aが16ビットレジスタですので、「#4-2 Logiscopeの拡張」で解説した「アクセス規約」に従っておく必要があります。
では、Logiscopeを使って、出力波形を確認しておきます。非常に高速なPWM波形ですので、UNOをはじめとするATmega系のArduinoでは、サンプリングしきれず、波形を確認することは困難です。従って、STM32F103ボードでLogiscopeを起動します。STM32ボードのPA8(5Vトレラント端子)と、38kHz PWMを出力させるUNOのD9端子をジャンパ線で接続します。2機のGND同士も接続します。
実験サンプル:【 】内はLogiscopeからの送信データ
【trg,8,r,500】
上図から実測では、波形17個分の平均で、周期=26.4μs、周波数=37.9kHz、HIGH幅=9μs、デューティ=1/2.9でした。測定(サンプリング)誤差とかもあるでしょうし、まあ、良いのではないでしょうか?
ここまでで、ようやくキャリア波形の出力ができたわけですが、次は、どんな信号を送信(出力)するか?ですね。NEC方式がデファクトとは言いつつも、そうでは無いリモコンも多数存在することは、「#3-9 キッチンタイマを作る」の実験で知りました。様々な方式に対応するには、各方式の詳細な仕様がわからなくてはなりませんが、独自方式を採用したリモコンでは、仕様がわからない(非公開な)ものも多数存在します。
そこで、特定のリモコンの特定のボタンを押した時の赤外線リモコン信号を取り込んで、そのまま、送信(再生)する方式を取ろうと思います。赤外線リモコン信号データが膨大になる可能性がありますが、どんな方式でも、詳細仕様がわからなくても、送信できるのが最大の利点です。
赤外線受信モジュールの出力は、無発信期間(HIGH)と発信期間(LOW)が交互に登場しますので、以下図の様に変化のあった時間をuint16の配列データに取り込むことにします。
記録する時間は、変化を検知した時のmicros()/4とし、前回変化点からの相対時間とします。0値は赤外線リモコン信号データの最後を表すコードに割り当てます。
取り込んだデータをそのままCのプログラムに埋め込めるような形式でシリアルモニタに出力できるようにしておきます。
実験サンプル:【 】内はシリアルモニタからの送信データ
【captc,200000】
CAPT
PROGMEM const uint16 irsd_data[ ]=
{
2242, 1127, 140, 144, 139, 138, 146, 138,
137, 148, 138, 137, 146, 138, 137, 146,
140, 139, 144, 428, 133, 428, 131, 430,
138, 429, 132, 427, 132, 429, 140, 427,
132, 429, 132, 427, 140, 138, 137, 430,
139, 138, 138, 146, 139, 144, 140, 427,
132, 139, 144, 140, 138, 429, 138, 137,
140, 430, 137, 428, 133, 427, 132, 152,
126, 435, 132, 0
};
END
CAPT〜ENDの間が、Cのソースコードに直接取り込むことができる書式になっています。Cのソースコードにコピー&ペーストし、配列変数名を適切に変えてお使いください。
PROGMEMは"avr/pgmspace.h"で定義された識別子で、宣言した変数をFlash(ROM)上に配置することを指示します。もし、PROGMEMをつけないと、Cのランタイムライブラリがプログラム実行開始時に、配列変数のデータ領域をRAM上に確保して、初期値をRAMにコピーしてしまうため、RAM容量を消費してしまいます。
ATmega328のRAMは2Kバイトしかないので、こんな大きな配列を確保すると、あっという間にRAMが枯渇してしまいます。PROGMEMをつけて宣言すると、CはRAM上の領域確保とコピーを実施しないので、RAMを消費することなく、Flashに記録することが可能です。
ただし、AVR(ATmega328等)は、メモリ種毎に空間が違う(※)MCUなので、普通にRAMにアクセスする命令ではFlashにアクセスできません。アセンブラレベルでは、RAMにアクセスするのとは別なFlash(ROM)アクセス用の命令が設けられています。
CでFlashにアクセスするためには、pgm_read_word()などの関数群を使う必要があります。
dat=irsd_data[10]; // × アクセスできない
dat=pgm_read_word (irsd_data + 10); // ○ アクセス可能
詳しくは、Arduino HP ⇒ REFERENCE ⇒ Variables ⇒ PROGMEM をご覧ください。
※ATmegaのデータシートによると、「メモリ種毎に空間が違う」のではなく、所謂「ハーバードアーキテクチャ」なのだそう。ハーバードは良いけれど、Flash(ROM)がデータ空間に配置されていないのは不便ですね。普通にデータ空間にFlashがマッピングされているMCUだったら、こんな小細工(PROGMEM)は要りません。
コンパイル時のメッセージは以下の様になります。
PROGMEMをつけなかった場合: |
最大32256バイトのフラッシュメモリのうち、スケッチが7290バイト(22%)を使っています。 最大2048バイトのRAMのうち、グローバル変数が2572バイト(125%)を使っていて、ローカル変数で-524バイト使うことができます。 スケッチが使用するメモリが足りません。メモリを節約する方法については、以下のURLのページを参照してください。http://www.arduino.cc/en/Guide/Troubleshooting#size ボードArduino/Genuino Unoに対するコンパイル時にエラーが発生しました。 |
PROGMEMをつけた場合: |
最大32256バイトのフラッシュメモリのうち、スケッチが7290バイト(22%)を使っています。 最大2048バイトのRAMのうち、グローバル変数が836バイト(40%)を使っていて、ローカル変数で1212バイト使うことができます。 |
RAMの容量不足はこれで回避できました。ATmega328の場合、Flashの容量も32Kバイトしかありませんので、無計画に赤外線リモコン信号データをどんどん取り込むと、すぐにFlash容量も足りなくなります。ご利用は計画的に。
※「Serial.println("hello, world");」の様な文字列定数も、RAMにコピーしてから使って(RAMを消費して)いるようです。このケースでは、F()マクロを使って、「Serial.println(F("hello, world"));」とすることで、回避可能です。
前節で取り込んだ赤外線リモコン信号をCのプログラムに組み込んだ上で、以下の様な配列で管理して、「何番目の赤外線リモコン信号を送信」するか、ユーザが選択できるようにしておきます。
const uint16 *irsd_patn[ ]=
{
irsd_pat0, // TV power
irsd_pat1, // TV CH-UP
irsd_pat2, // TV CH-DOWN
irsd_pat3, // TV VOL-UP
irsd_pat4, // TV VOL-DOWN
NULL
};
送信関数は以下の様になります。
int ir_send (
int patn)
{
// send ir pattern #patn
// return: ErrNo / ErrBadPatn
int idx;
uint16 dat;
const uint16 *tpat;
uint32 usec;
uint32 usec_prev;
// parameter
if (patn<0 || int (sizeof (irsd_patn)/sizeof (uint16)) - 1<=patn)
{
// ERROR:
return (ErrBadPatn);
}
tpat=irsd_patn[patn];
// send ir signal
usec=micros ();
usec_prev=usec;
ir_on ();
for (idx=0; ( dat=pgm_read_word (tpat + idx) )!=0; idx++)
{
dat*=4UL;
while (usec - usec_prev<dat)
usec=micros ();
usec_prev += dat;
if (idx%2)
ir_on ();
else
ir_off ();
}
ir_off ();
return (ErrNo);
}
delayMicroseconds()を使わずに、micros()で時間を計測していることに注目してください。delayMicroseconds()は、時間待ちしている間に割り込みがかかると、その割り込み処理時間分だけ間延びします。赤外線リモコン信号が崩れてしまうので、うまく送受信できない要因となります。
では、実際の機器に向けて、信号を送信してみましょう。完成版のプログラムをリンクしておきます。
完成版プログラム:test_ir_send.ino
シリアルモニタから、以下のコマンドを受け付けるようになっています。
プログラムには、サンプル赤外線リモコン信号が入っていますが、筆者宅の赤外線リモコン信号なので、各自、取り込んでプログラムに組み込まないと、無意味ですのでご注意ください。
以下図の様な回路を作成し、Arduino UNOでプログラムを実行します。
IR LEDを対象機器に向けて、【send】コマンドを実行してみます。……反応しました、機器までの距離は約20cm。
いくつか試してみたところ、机の上からでは、約1.8m上の天井灯には信号が届かない様でした。実験装置を高く持ち上げ、天井灯との距離を1mくらいまで近づけたところ、天井灯も反応しました。
やはり心配していた通り、少しパワー不足なのかも?電流制限抵抗1Ω(@3V)だった所を、50Ω(@5V)にしてしまっているので……。TVのリモコンとかだと、普通に3〜4[m]離れても反応しますし、少しくらい違う方向に向けても、壁などで反射して信号が届いたりして、だいぶパワフルな印象があります。
でも、もうIR LEDの替えが無いので、怖くて電流を増やせません!パワー不足な件は、後々、余裕のできたところで試してみます(1^^;)っ。
※さっき抵抗を計算したところで、R=31.5Ωでしたので、100Ωの抵抗3並列(合成抵抗で33Ω)くらいまでは、定格ギリギリで使えるはず……。IR LEDの正確な仕様もわからない状態で、「ギリギリ」ですので、保証は致しかねますm(1__)m。
紆余曲折したものの、赤外線リモコンの送信ができるようになりました。色々と応用が考えられますね。
色々楽しめそうです(1^−^)v。