概要
リモコン用赤外線受信モジュールを使った調査により、家電用リモコンからの信号の受信と、リモコンが送信している内容のコード化ができたので、次はそれが正しいのかどうかを確認するため、赤外線の発信側を作ってみた。解析したコードを使って家電用リモコンと同じ内容が送信できるのか、最終的には家電のコントロールができるのか、ということ。
IrLEDの駆動回路
まず最初に、IrLEDを駆動するための回路を考えることにした。IrLEDとしては、一般的なリモコンと同じ波長940nmの赤外線を発光するものということで、秋月電子で10本100円で売っているOSI5LA5113Aという製品を使った。データシートはこちら。
抵抗値の算出
今まで作った回路では、赤や緑のLEDがポートの操作や電源の状態に応じてぼんやりと光ればよかったのだけど、今回は赤外線リモコン用ということで強い光が欲しい。そのため、IrLEDを直列に2本並べ、おのおのに大きな電流が流れるようなパラメータを決めることにした。LEDのデータシートの中でポイントとなる項目は、
- 順方向電流 IF (max) : 100mA
- 順方向電圧 VF (@IF=100mA ) : 1.35V(Typ.)、1.6V(max.)
という2点だろう。データシートにIFとVFの関係を示すグラフが載っていないのか残念で、例えばLEDに50mA流すときのVFはいくつになるのかが分からない。

まずは、 単純なテスト回路を用いて直列抵抗のR1を決めることとし、IF (max) = 100mAに対して余裕のあるIF =60mA、VF = 1.35VとしてR1を算出した。
R1 = (3.3 – 1.35×2) ÷ 0.06 = 10Ω
LEDにパルスとして電流を流す際にはより大きな電流を流すこともできる (データシートのIFP) 。テスト回路では常時点灯させるので IF (max) よりも小さな電流値とした。
テスターによる実測
ブレッドボード上に図1のテスト用回路を組んで、テスターを使って電流や電圧を測ったみたところ、以下のようになった。
- 電源電圧:3.26V
- IrLED1のアノード-カソード間の電位差:1.26V
- IrLED2のアノード-カソード間の電位差:1.26V
- R1両端の電位差 : 0.74V
10Ω(±5%) のR1両端間の電位差から、直列2本のLEDを流れるIF は約74mAとなることが分かった。また、電源電圧や各IrLEDで測定した電位差(VF)を当てはめて計算してみても、
IF = (3.26 – 1.26×2) ÷ 10Ω = 74mA
となり、IF (max)にはまだ余裕がある。実際の回路でもR1=10Ωを使うこととした。0.74×0.74÷10 = 0.05W なので、抵抗も1/4W規格のもので十分である。
トランジスタによるスイッチング
赤外線リモコンを作るには、波長940nmの赤外線を38kHz(デューティー比1/3)でオン/オフしつつ、送信フォーマットに合わせて論理値を出力してやる必要があるから、プログラムによるポート操作でオン/オフをコントロールする。
また、ESP8266EXのデータシートによれば、GPIOポートが流せる電流は最大12mAと決まっているから、WROOM02のGPIOに図1の回路を直結して駆動することはできない。そのため、トランジスタを使ったスイッチング回路を用意した。

四角い枠で囲まれた #5 は、WROOM02の5番ピン(GPIO13)に接続することを示しており、digitalWrite(13, HIGH) とかLOWとかやることで、IrLEDを点滅させることにした。
ベース抵抗R2の決定
トランジスタを増幅器として用いる場合は、
コレクタ電流 (IC) = ベース電流(IB) × トランジスタのhFE値
という式を使う。2SC1815GRのhFEの代表値は200なので今回の回路に必要なベース電流は、
IB = 70mA / 200 = 0.35mA
でよいように思える。
ただ今回の回路では、コレクタ-エミッタ間の電位差(VCE)をなるべく小さくすることでコレクタ電流(IC)を大きくしたい。VCEを最小値近くで動作させるためには、トランジスタを飽和させて使う必要がある。
2SC1815のデータシートにあるいろいろな特性図のうち、以下に示すIC-VCE曲線を見ると、小さなVCE値を維持したままICが直線的に立ち上がっている領域がある。

飽和させて使うときには、この領域になるようなIB を使うことになる。今回は、IB を4mA程度とすることにした。4mA流す場合のR2の値は、
R2 = (3.3 – 0.6)V / 4mA = 675Ω
となるから R2 = 680Ωを使うことにした。0.6Vはベース-エミッタ間の電圧。この値の場合、WROOM02のGPIOポートをHIGHとしたときに約 4mA 流れ出すことになるが、最大定格値以下である。なお、実使用時にはIrLEDは短い期間点滅するだけなので、もうちょっと流してやってもいいかもしれない。
テスターによる実測
WROOM02のGPIO13を常時HIGHにするスケッチを動かしておき、テスターで各部の電圧を測定した。
- 電源電圧:3.26V
- R1両端の電位差 : 0.61V
R1の両端の電位差がトランジスタを使わない素通しのときに比べて 0.74 – 0.61 = 0.13V減っている。これがVCE だろう。また、肝心のLEDのIF は 0.61÷10 = 61mAとなった。式にすると、以下のようになるだろう。
- IF = (Vcc – VF × 2 – VCE) ÷ R1
- IF = (3.26 – 1.26×2 – 0.13) ÷ 10Ω = 61mA
IC÷IB の値(hFE)は約15ということになる。
では、最初に書いた IB = 70mA / 200 = 0.35mA という関係を信じた場合にはどうなのかということで、IB が約0.4mAになるようR2を6.8kΩに変えてみたところ、R1の両端の電位差は0.38Vになった。つまり、VCE の上昇によってコレクタ電流が減り、LEDのIF は38mAになったことになる。もっと飽和させて使わないと、もったいないのである。なお、上に示したIC-VCE曲線を見ると IB =0.4mA ならばICの値はこの実測値くらいで妥当だろう。
WROOM02-IRLED
WROOM02に上記のIrLED駆動回路を組み合わせたものを以下に示す。
この回路には、先の赤外線受光モジュールも含んでいるが、受光用に使うか発光用に使うかはスケッチ次第ということ。ただ送受信のテストを行うためには、別々のボードに用意した方がやりやすい。
IrLEDの点灯の様子
赤外線は肉眼では見えないものの、デジカメのセンサーには写る。デジカメやカメラ付き携帯電話の登場以来、リモコンの電池がチェックがしやすくなったものである。
IrLEDが発光する様子を写すために、スマホを含めて手持ちのいくつかのカメラで試してみたところ、オリンパスのE-PM2 (ペンミニ2)が一番明るく写ってくれた。レンズはMZD60mm F2.8を使用。指向性の強さも分かるので、動画にしてYoutubeに置いた。
横からみると、ほのかに光っている程度に見えるが正面から見るとかなり明るいことが分かる。LEDを漏斗状あるいはパイプ状のカラーで囲んでやって、先端が対象(エアコンやテレビ)に向いていることがはっきり分かるようにした方が良さそうである。
プログラム
赤外線リモコンとして動作させるプログラムは、以下のような方針で記述することにした。
- WiFiアクセスポイントとして構成し、PCやスマホのブラウザから接続して操作できるようにする。
- ESP8266 Arduino core に用意されている、ESP8266WebServerクラスを利用してwebサーバーとして構成する。
- リクエストパラメータとして送信フォーマットと送出用コマンドコードを与えると、IrLEDをチカチカさせて送信するURIを用意する。
- テスト用の常時点灯などを実行するURIも用意する。
ソースコード (ESP_IRSERVER1.ino)
ソースコードは以下のようになった。
#include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #define START_MSG "\n" + String(__FILE__) + " start." ESP8266WebServer server(80); const char* ssid = "ESP8266AP"; const char* password = "password"; const int RED_LED = 12; const int IR_LED = 13; void handleCommand(); void handleIrTest(); void setup(void){ delay(10); Serial.begin(115200); pinMode(RED_LED, OUTPUT); digitalWrite(RED_LED, 0); pinMode(IR_LED, OUTPUT); digitalWrite(IR_LED, 0); Serial.println(START_MSG); WiFi.softAP(ssid, password); IPAddress ip = WiFi.softAPIP(); Serial.println(""); Serial.println(ssid + String(" starts...(") + ip.toString() + ")"); server.on("/irtest", handleIrTest); server.on("/ircmd", handleCommand); server.begin(); Serial.println("HTTP server started"); } void loop(void){ server.handleClient(); } #define MAX_FRAME 4 #define MAX_DATA 60 typedef struct { char format; int data_index; int data_length[MAX_FRAME]; byte data[MAX_FRAME][MAX_DATA]; } ircmd_t; #define NEC_T 560 #define AEHA_T 425 int format_T; // 基準フレーム時間 int loop_limit; // 38kHz 1波分のループ回数 // 1周期 26usec, H : 6usec, L : 20usec. void init_format(char c) { format_T = c == 'N' ? NEC_T : AEHA_T; loop_limit = format_T / 26; } void send_H(int t) { int limit = t == 1 ? loop_limit : loop_limit * t; for(int i = 0; i < limit; i++) { digitalWrite(IR_LED, HIGH); delayMicroseconds(6); digitalWrite(IR_LED, LOW); delayMicroseconds(18); } } void send_bit(int bit_value) { send_H(1); delayMicroseconds(bit_value ? format_T * 3 : format_T); } // valueのLSBから8ビット送信。 void send_byte(byte value) { for(int i = 0; i < 8 ; i++) { send_bit(value & 1); value >>= 1; } } void send_leader() { if (format_T == NEC_T) { send_H(16); delayMicroseconds(format_T * 8); } else { send_H(8); delayMicroseconds(format_T * 4); } } void send_preframe() { for(int i = 0; i < 4; i++) send_bit(0); } // 適当な終端 void send_trailer() { send_bit(0); // stopbit of NEC. digitalWrite(IR_LED, LOW); delay(10); // 10msecの無信号 } // 16進表記の文字列をbyteに変換。必ず2桁であること。 static const String acp = "0123456789abcdef"; byte hex2bin(String src) { src.toLowerCase(); src.trim(); if (src.length() != 2) return 0; byte b = acp.indexOf(src.charAt(0)) << 4; b += acp.indexOf(src.charAt(1)); return b & 0xff; } ircmd_t* newCommand() { ircmd_t* pcmd = (ircmd_t*)malloc(sizeof(ircmd_t)); if (pcmd) memset((void*) pcmd, 0, sizeof(ircmd_t)); return pcmd; } void dump_struct(ircmd_t* pcmd) { Serial.println("dump ircmd_t"); Serial.println("format = " + String(pcmd->format)); Serial.println("data_index = " + String(pcmd->data_index)); for(int index = 0; index < pcmd->data_index; index++) { Serial.print("index "); Serial.print(index); Serial.print(" (length="); Serial.print(pcmd->data_length[index]); Serial.print(")\t"); for(int i = 0; i < pcmd->data_length[index]; i++) { char tmp[12]; sprintf(tmp, "%02X", pcmd->data[index][i]); Serial.print(tmp); Serial.print(" "); } Serial.println("."); } } // cmd=$N 00 11 22 ab $t // cmd=$A 00 11 22 ab ff dd $t // といったパラメータを受け取り、それに応じてIRLEDを点灯する。 // $N : NEC, $A, AEHA // $t : トレーラー void handleCommand() { String param = server.arg("cmd"); Serial.println(" handleCommand() cmd = " + param); ircmd_t* pcmd = newCommand(); if (pcmd == NULL) { server.send(200, "text/plain", "NG ALLOC FAILED"); return; } digitalWrite(RED_LED, 1); char* cp = (char*)param.c_str(); cp = strtok(cp, " "); while(cp) { if (*cp == '$') { cp++; if (*cp == 'N' || *cp == 'A') pcmd->format = *cp; else if (*cp == 't') { // トレーラー pcmd->data_index++; if (pcmd->data_index >= MAX_FRAME) break; } } else if (strlen(cp) == 2) { int index = pcmd->data_index; int data_length = pcmd->data_length[index]; pcmd->data[index][data_length++] = hex2bin(cp); if (data_length >= MAX_DATA) break; pcmd->data_length[index] = data_length; } cp = strtok(NULL, " "); } dump_struct(pcmd); // sending data init_format(pcmd->format); for(int index = 0; index < MAX_FRAME; index++) { if (pcmd->data_length[index] > 0 && pcmd->data_length[index] > 0) { send_preframe(); send_leader(); for(int i = 0; i < pcmd->data_length[index]; i++) send_byte(pcmd->data[index][i]); send_trailer(); } } free(pcmd); server.send(200, "text/plain", "OK"); digitalWrite(RED_LED, 0); } // 電流測定のために15秒間つけっぱなしにする。 void handleIrTest() { digitalWrite(RED_LED, 1); Serial.println(" handleIrTest()"); server.send(200, "text/plain", "OK"); digitalWrite(IR_LED, HIGH); delay(15000); digitalWrite(IR_LED, LOW); digitalWrite(RED_LED, 0); }
いくつかの関数についての説明。赤外線送信フォーマット等については、前回の内容を参照してください。
赤外線関係
void send_H(int t)
38KHzでIrLEDを点滅させ、パラメータtの期間のオン信号を送信する。tには、基準時間Tの整数倍を指定する。
GPIOポートの上げ下げによってデューティー比1/3の38KHz信号を、フォーマットごとのT相当時間だけ発射し続ける。波形を観測したわけではないのだけど、前回使った受信側回路や実際の家電機器は一応意図通りに動いてくれた。
void send_bit(int bit_value)
1ビット相当の信号を、オン期間はsend_H()の呼出しで、オフ期間は指定時間の無信号で実現している。
void send_byte(byte value)
1バイト相当の信号を、send_bit()の呼び出しを下位から順に8回繰り返すことで送信する。
繰り返しによる遅延が気になるときには、
send_bit(value & 1); send_bit(value & 2); send_bit(value & 4); ....
といったベタな書き方もあり。
void send_leader(), send_trailer(), send_preframe()
フレームを構成するデータ以外の部分の送信を行う関数。
send_leader()はフォーマットごとに異なった内容を送信しているが、send_trailer()およびsend_preframe()は共通。
send_preframe() に相当する信号は規格には存在しないが、実際のリモコンから受信したデータを見るとリーダーの前に何やらパラパラでていることが多かったので書いてみた。
コマンドデータ受信関係
httpdリクエストを受けて処理する部分。
byte hex2bin(String src)
16進数表記の文字列をbyteに変換する。どんな環境でもこの関数を書くことが多い。今回はString のメソッドを使って実装。
ところで、ネット検索でArduinoのStringクラスのメソッドを検索してみると、以下のような文字列操作を行うメソッドが、あたかも値を返すかのように書いてるサンプルや説明に出くわす。
void toLowerCase(void);
void toUpperCase(void);
void trim(void);
コンパイルすればエラーになるから分かるでしょ、ということかな。
void handleCommand()
リクエスト /ircmd を受けたときESP8266WebServerクラスから呼ばれるリクエストハンドラ。コマンドデータを受け取ってIrLEDを使って送出する。
setup() において
server.on(“/irtest”, handleIrTest);
server.on(“/ircmd”, handleCommand);
といった具合にハンドラを登録しておけば、リクエストを受けたときに呼び出される。
cmdという名前のパラメータに16進表記のコマンドデータが入ってくるので、これを分解してircmd_t型の構造体に格納する。’$’の次の文字が送信フォーマット(NおよびA)、トレーラー(t)を表現している。コマンドデータは2文字ずつ空白文字で区切られている必要がある。
使い方
WiFiをもつノートPCやスマホから、WROOM02のアクセスポイントに接続する。接続したときのIPアドレスは、今回のプログラムならば最初は192.168.4.1になることが多い。ブラウザを開いてアドレス欄に、例えばT社のRというブランドのテレビ用リモコンの電源ボタンのフリをするならば、
http://192.168.4.1/ircmd?cmd=$N 40 bf 12 ed
と入力してEnter。URIに空白は許されないが、ブラウザが空白を%20に置き換えて送信してくれるから、手入力するときはそのまま打てばよい。先頭の$NがNECフォーマットを表しており、40 bf 12 ed というコマンドデータを送出させている。
2フレームからなるエアコン(AEHAフォーマット)に対して、1フレーム目が 11 da 27 00 01、2フレーム目が01 10 00 40 bfならば、先頭の$Aでフォーマットを指定し、フレームの区切りとしてトレーラーを意味する$tを挿入して、
http://192.168.4.1/ircmd?cmd=$A 11 da 27 00 01 $t 01 10 00 40 bf
とする。
WROOM02がコマンドをリクエストを受信すると、IrLEDに関する処理をしている間、GPIO12に接続した赤色のLEDが点灯する。
※ 初期の投稿では使い方が抜けていたので追加しました。
きょうのまとめ
IrLED用の回路とトランジスタを使ったスイッチングについて少々まとめてみた。間違いがなければよいのですが。
いつも変わり映えがしないが、小型のブレッドボードに組み上げたところ。IrLEDを4本(直列2本を並列に2組)にしようかとも思ったが、ノートPCのUSB端子を電源とすることを考えると、電流を抑え気味にする必要がありそうである。
テレビとエアコンを使って動作を確認したが良好な結果だった。IrLEDの先端を家電機器に向けた場合、5m程度の距離ならば問題なかった。ただ、エアコン用の長いコマンドデータを書くのは、コピペで済むとはいえ面倒だった。
せっかくブラウザからコマンドを送るので、それらしいhtmlを用意して使い勝手をよくする必要がありそうである。エアコン用のリモコンに求められる内部状態(エアコン動作状態)の維持は後回しにするとして、次はSPIFFSを使ってWROOM02のフラッシュメモリにhtmlファイルを置き、対象の家電ごとに操作パネル(のようなもの)を表示できるようにしたい。