赤外線で信号を送ってLEDをオン・オフしたい!
LEDに限らずDC電源で動くものなら何でも赤外線で操作したい!
というわけで秋月電気で1個70円(2024年12月時点)で売っている安いマイコン、ATtiny202を使ってリモコン信号の受信部を作りました。
ささっと回路設計してElecrowで回路製造したのがこちら。
赤外線受信モジュールはSPS-440を使っています。信号出力ピンはマイコンの5番ピンに接続されています。ややこしいですがこれはデジタル3番ピンに相当するので、Arduinoコード上では「3」です。
出力はデジタル2番ピンです。直接LEDを駆動しようとするとほんのちょっとした電流量しか流せず明るいLED照明の操作には使えないので、2番ピンは直接LEDにつながっているわけではなく、MOSFETのゲートに接続されています。MOSFETを介して外部LEDに、DCジャックからの電源を供給するようにしています。
マイコンを赤外線リモコンにするというとかなり有名なIRremoteライブラリを使った作例がインターネットに山のようにありますが、私のスキルではATtiny202にIRremoteはそのまま入れられなかったので、仕方なく自分なりの赤外線通信プロトコルで実装しました。
自分なりの実装だと、NECプロトコルのような既存の赤外線通信規格を用いる場合に比べて、偶然信号が被ってしまって誤動作する確率は減るんじゃないかな~という狙いもちょっぴりありました。
というわけで、こんな感じの波形で通信することにしました。
NECプロトコルを参考にしました。
まずはリーダー部として、赤外線を8msオン、その後3msオフというふうに設定しました。オン時間とオフ時間が一致したとき、マイコンはその次に続くデータ部の読み取りを開始します。
この8msオン、3msオフというのは私が適当に決めたものなので、おそらくご家庭にある一般家電のリモコン信号とは一致せず、データの読み取りをしないので誤動作防止になっているはずです。たぶん……
データ部は16bitのデータなので、1または0が合計16回送信されます。赤外線が2msオンのときは「1」、4msオンのときは「0」が送られてきたと判定します。送られてきたデータがどちらの場合も、1bitのデータの後には赤外線オフの時間が2msあります。
データ部を受信し終えたら、受信エラー等が無いかを確認するため、エラーチェック用のデータを受信します。これは単純にデータ部の反転データです。例えばデータ部が1001だったのなら、0110がエラーチェック用データとなります。(NECプロトコルのパクリ)
合計32bitの信号を受信し終えたら、エラーチェック用データ16bitを反転させて、最初に受信した16bitのデータ部と一致するかどうかを検証します。
一致したらノイズ等の影響なくちゃんと受信できているということなので、受信した信号内容に応じて処理をする。
というのが受信側の流れです。
ソースコードを書いておきます。気をつけてほしいのは、3番ピンに繋がっている赤外線受信モジュールSPS-440は、赤外線が来ていない時に出力ハイ、赤外線が来たらローになります。直感と逆な感じがしますがそういう仕様です。もしお手元の受信モジュールの仕様が逆の場合は、下記のコードの検出部のハイとローを逆にしてください。
ソースコード(受信)
#define IR 3
#define OUT_PIN 2
void signalCheck();
int errorCheck();
void setup() {
pinMode(IR, INPUT_PULLUP);
pinMode(OUT_PIN, OUTPUT);
}
const uint16_t mySigON = 0b0100110100000001;
const uint16_t mySigOFF = 0b0100110100110001;
uint16_t recvSig = 0b1111111111111111;//受信した信号格納
uint16_t recvSigComp = 0b1111111111111111;//受信した信号のエラーチェック用
void signalCheck(){
if(recvSig == mySigON){//「ON」を表す信号を受信したとき
digitalWrite(OUT_PIN, HIGH);//ピン出力をオンにする
}
else if(recvSig == mySigOFF){ //「OFF」を表す信号を受信したとき
digitalWrite(OUT_PIN, LOW); //ピン出力をオンにする
}
else{
//他の機器向けの信号を受け取った場合なので何もしない
}
}
int errorCheck(){
recvSigComp = ~recvSigComp;//エラーチェックデータを反転
if(recvSig == recvSigComp){//データ部と一致するか確認
return 1;
}
else return 0;
}
void loop() {
//開始信号:IR on 8msのあと、IR off 3ms
//データ信号:IR on 2msで1、IR on 4msで0、いずれも送信後、IR offを2ms
unsigned long pre, width; //4 bytes micros variable
while(digitalRead(IR)==HIGH){;}//IRが来ていないとき、待機
pre=micros();
while(digitalRead(IR)==LOW){;}//IR受光中、何もせず待機
width = micros() - pre; //IRパルスの長さ
if(width > 8000*0.9 && width < 8000*1.1){//パルスの長さが8msだったら
pre=micros();
while(digitalRead(IR)==HIGH){;}//IRが来ていないとき、待機
width = micros() - pre; //IRパルスの長さ ※ここでは、IRオフの長さを見ている
if(width > 3000*0.9 && width < 3000*1.1){//オフ時間長さが3msだったら
//データ部16bitの読み取り処理
for(int i=0;i<16;i++){
uint16_t val = 0b0000000000000000;
while(digitalRead(IR)==HIGH){;}//IRが来ていないとき、待機
pre=micros();
while(digitalRead(IR)==LOW){;}//IR受光中、何もせず待機
width = micros() - pre; //IRパルスの長さ
if(width > 4000*0.8 && width < 4000*1.2){//IR onが長い=4msで送った「0」と判断
val = 0b0000000000000000;
}
else if(width > 2000*0.8 && width < 2000*1.2){//IR onが短い=2msで送った「1」と判断
val = 0b1111111111111111;
}
uint16_t valMask = 0b0000000000000001;//まずは最小桁のみ1のマスクを作る
valMask = valMask << i; //注目するケタまで1を移動させる
valMask = ~valMask;//NOTでビット反転。注目するケタだけ0のマスクになる
val = val | valMask; //or演算。valの注目しているケタが1ならvalはall 1、0ならそのケタだけ0、他1の変数になる。
recvSig = recvSig & val;//valのiケタ目が0ならrecvSigのそのケタが0, 1なら1になる。他のケタはいじらない
}
//エラーチェックデータ部16bitの読み取り処理
for(int i=0;i<16;i++){
uint16_t val = 0b0000000000000000;
while(digitalRead(IR)==HIGH){;}//IRが来ていないとき、待機
pre=micros();
while(digitalRead(IR)==LOW){;}//IR受光中、何もせず待機
width = micros() - pre; //IRパルスの長さ
if(width > 4000*0.8 && width < 4000*1.2){//IR onが長い=4msで送った「0」と判断
val = 0b0000000000000000;
}
else if(width > 2000*0.8 && width < 2000*1.2){//IR onが短い=2msで送った「1」と判断
val = 0b1111111111111111;
}
uint16_t valMask = 0b0000000000000001;//まずは最小桁のみ1のマスクを作る
valMask = valMask << i; //注目するケタまで1を移動させる
valMask = ~valMask;//NOTでビット反転。注目するケタだけ0のマスクになる
val = val | valMask; //or演算。valの注目しているケタが1ならvalはall 1、0ならそのケタだけ0、他1の変数になる。
recvSigComp = recvSigComp & val;//valのiケタ目が0ならrecvSigのそのケタが0, 1なら1になる。他のケタはいじらない
}
//受信終了
if(errorCheck()){
signalCheck();//エラーチェックをクリアした場合、コードの中身を読む
}
recvSig = 0b1111111111111111;//読んだ後は初期化する
recvSigComp = 0b1111111111111111;//読んだ後は初期化する
}
}
}
コード内では16ビットのデータを1桁ずつ読み取って1か0か判定している感じをわかりやすくするために16ケタの2進数表記にしていますが、実際はもっとわかりやすく、
データ部の書き方の例
const uint16_t mySigON = 50;
const uint16_t mySigOFF = 80;
のような10進数表記の書き方でも問題ないです。1台目の機器は10~19, 2台目の機器は20~29……みたいなルールを決めて管理するとわかりやすいので、実用上は10進数表記がいいですね。
検出側の時間精度でどうしても誤差が出てしまうので、例えばパルス長が8msかどうかを判定するときは、8ms±10%の範囲か、というふうに範囲を設けています。コード内では±10%だったり±20%だったりまちまちですが、試行錯誤の結果をそのまま残してるだけですので使い分けている意味は特にありません。全部±10%でもいいと思います。
送信側は、さっき書いたデータのルール通りに赤外線LEDをオン・オフするプログラムを組めばOK。
赤外線リモコン(送信)
#include <M5Stack.h>
const uint8_t outputPin = 26;
const uint16_t mySigON = 50;
const uint16_t mySigOFF = 80;
uint16_t sendData = 0b0000000000000000;
const int longPulse = 4000;
const int shortPulse = 2000;
const int headerPulse = 8000;
const int headerWait = 3000;
const int dataWait = 2000;
// 38kHzのパルスを生成する関数
void sendPulse38kHz(unsigned long duration) {
unsigned long startTime = micros();
while (micros() - startTime < duration) {
digitalWrite(outputPin, HIGH);
delayMicroseconds(12); // 38kHzの半周期 (13µs)※ただし12にしておいたほうが実際のパルスは38kHzに近い
digitalWrite(outputPin, LOW);
delayMicroseconds(12); // 38kHzの半周期 (13µs)
}
}
void setup() {
M5.begin();
pinMode(outputPin, OUTPUT);
Serial.begin(115200);
}
void loop() {
Serial.println("ON code start");
//leader送信
sendPulse38kHz(headerPulse);
delayMicroseconds(headerWait);
for(int i=0;i<16;i++){//data送信
sendData = mySigON;
sendData = sendData >> i;
sendData = sendData & 0b0000000000000001;
if(sendData == 1){
sendPulse38kHz(shortPulse);
delayMicroseconds(dataWait);
}
else{
sendPulse38kHz(longPulse);
delayMicroseconds(dataWait);
}
}
for(int i=0;i<16;i++){//エラー補償用の反転データ送信
sendData = mySigON;
sendData = sendData >> i;
sendData = sendData & 0b0000000000000001;
if(sendData == 1){
sendPulse38kHz(longPulse);//反転データなので、1のとき0を送る
delayMicroseconds(dataWait);
}
else{
sendPulse38kHz(shortPulse);
delayMicroseconds(dataWait);
}
}
Serial.println("ON code finish.");
}
とりあえず「ON」を表す信号を送信する動作テスト用コードです。
コードを見れば分かると思いますが、こっちはM5Stack用で組んでますので、そのままATtiny202には書き込めません。
ピン番号を修正して、シリアル通信部を全部削除すればATtiny202でも使えると思います……
データ部はNECプロトコルは確か8bitですが、今回は16bitにしたので、0~65535のデータを扱うことができます。つまり6万通り以上の信号を用意することができるので、操作したい機器が多少増えても信号割当てが足りなくなるということはきっと無いはず!もし足りなくなったらビット数増やしてください。
-
KTR-27S
さんが
2024/12/11
に
編集
をしました。
(メッセージ: 初版)
-
KTR-27S
さんが
2024/12/11
に
編集
をしました。
-
KTR-27S
さんが
2024/12/16
に
編集
をしました。
ログインしてコメントを投稿する